【问题标题】:How to create a Task with TaskFactory.FromAsync and custom Async procedures如何使用 TaskFactory.FromAsync 和自定义 Async 过程创建任务
【发布时间】:2011-04-07 03:30:07
【问题描述】:

我正在尝试测试一些依赖任务来进行后台计算的类(从网络位置检索数据)。该类获取一个未启动的任务实例,添加一个 ContinueWith 方法,然后在该任务上调用 Start。像这样的:

private void Search()
{
    Task<SearchResults> searchTask = m_searchProvider.GetSearchTask(m_searchCriteria);
    searchTask.ContinueWith(OnSearchTaskCompleted);
    searchTask.Start();
}

该类通过接口获取 Task 的实例,因此我可以注入我的测试控制的 Task 实例。但是,我似乎无法创建一个我有足够控制权的。

我不想在测试中引入线程,但仍然想让Task异步运行,所以我尝试做的是编写一个实现BeginInvoke/EndInvoke模式的类而不使用线程,并使用TaskFactory .FromAsync 方法来创建Task。

这个想法是测试可以调用启动任务的类上的方法,然后当返回时,测试可以将结果数据提供给 Async 对象,该对象完成操作,同时保持在同一个线程上。

但是,当我尝试在该任务上调用 Start 时,我收到一条错误消息,指出“可能不会在具有空操作的任务上调用 Start”。不幸的是,谷歌对该消息没有太大帮助,所以我不确定我是否错误地实现了我的 Async 对象,或者错误地使用了 TaskFactory.FromAsync。这是我的 NonThreadedAsync 类的代码和一个因异常而崩溃的测试:

public class NonThreadedAsync<TResult>
{
    private NonThreadedAsyncResult<TResult> m_asyncResult;

    public IAsyncResult BeginInvoke(
        AsyncCallback callback,
        object state)
    {
        m_asyncResult = new NonThreadedAsyncResult<TResult>(callback, state);
        return m_asyncResult;
    }

    public TResult EndInvoke(IAsyncResult asyncResult)
    {
        return m_asyncResult.GetResults();
    }

    public void Complete(TResult data)
    {
        m_asyncResult.CompleteAsync(data);
    }
}

public class NonThreadedAsyncResult<TResult> : IAsyncResult
{
    private readonly AsyncCallback m_asyncCallback;
    private readonly object m_state;
    private readonly ManualResetEvent m_waitHandle;
    private bool m_isCompleted;
    private TResult m_resultData;

    public NonThreadedAsyncResult(AsyncCallback asyncCallback, object state)
    {
        m_asyncCallback = asyncCallback;
        m_state = state;
        m_waitHandle = new ManualResetEvent(false);
        m_isCompleted = false;
    }

    public void CompleteAsync(TResult data)
    {
        m_resultData = data;
        m_isCompleted = true;
        m_waitHandle.Set();
        if (m_asyncCallback != null)
        {
            m_asyncCallback(this);
        }
    }

    public TResult GetResults()
    {
        if (!m_isCompleted)
        {
            m_waitHandle.WaitOne();
        }
        return m_resultData;
    }

    #region Implementation of IAsyncResult

    public bool IsCompleted
    {
        get { return m_isCompleted; }
    }

    public WaitHandle AsyncWaitHandle
    {
        get { return m_waitHandle; }
    }

    public object AsyncState
    {
        get { return m_state; }
    }

    public bool CompletedSynchronously
    {
        get { return false; }
    }

    #endregion
}

[TestClass]
public class NonThreadedAsyncTests
{
    [TestMethod]
    public void TaskFactoryFromAsync_CanStartReturnedTask()
    {
        NonThreadedAsync<int> async = new NonThreadedAsync<int>();

        Task<int> task = Task<int>.Factory.FromAsync(async.BeginInvoke, async.EndInvoke, null);

        task.Start();
    }
}

作为进一步的信息,如果我调试该测试,就在它调用 Start() 之前,任务实例将显示在 Locals 窗口中,如下所示:

Id = 1, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"

但是如果我展开它,可见属性中没有 Method 属性。

谁能看出我做错了什么?

[编辑:我还编写了一个测试,确认 NonThreadedAsync 类可以与经典的 Begin/End 模式一起正常工作(或者至少,我的对 Begin/End 模式的理解 :))这通过了:

[TestMethod]
public void NonThreadedAsync_ClassicAccessPattern()
{
    int result = 0;
    bool asyncCompleted = false;

    NonThreadedAsync<int> async = new NonThreadedAsync<int>();

    async.BeginInvoke(asyncResult =>
                            {
                                result = async.EndInvoke(asyncResult);
                                asyncCompleted = true;
                            },
                            null);

    Assert.IsFalse(asyncCompleted);
    Assert.AreEqual(0, result);

    async.Complete(54);

    Assert.IsTrue(asyncCompleted);
    Assert.AreEqual(54, result);
}

【问题讨论】:

    标签: .net c#-4.0 task-parallel-library taskfactory


    【解决方案1】:

    哦,我明白了。我们的 API 是错误的,因为它试图返回未启动的任务。从被测类中删除 Start() 解决了这个问题。然而,在研究这一点时,我还发现我为获得测试控制的异步任务做的太多了。根据 Stephen Toub 的帖子 here,我们可以简单地使用 TaskCompletionSource:

    [TestMethod]
    public void TaskCompletionSource_WorksALotBetterThanMyOverEngineeredCustomStuff()
    {
        int result = 0;
    
        TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    
        Task<int> myTask = tcs.Task;
    
        // Pretend this is the class under test and that we've
        // passed in myTask
        myTask.ContinueWith(t => { result = t.Result; },
            TaskContinuationOptions.ExecuteSynchronously);
    
        Assert.AreEqual(0, result);
    
        tcs.SetResult(54);
    
        Assert.AreEqual(54, result);
    }
    

    【讨论】:

    • 您还应该将调用 ContinueWith 创建的任务返回给调用代码。这允许单元测试在该任务上调用 Wait() 以便您可以断言知道继续任务(可能是您正在测试的副作用发生的地方)也已完成。
    • TaskCompletionSource 可能是 TPL 最漂亮的方面之一......它提供了许多出色的功能来扩展框架。
    • 如何对异步和 tpl 进行单元测试的非常好的示例。感谢您指出这一点。
    猜你喜欢
    • 2021-06-04
    • 2017-06-15
    • 2017-06-04
    • 2019-03-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-01
    相关资源
    最近更新 更多