Как создать задачу с помощью TaskFactory.FromAsync и пользовательских процедур Async - PullRequest
4 голосов
/ 07 апреля 2011

Я пытаюсь протестировать некоторые классы, которые полагаются на задачу для выполнения некоторых фоновых вычислений (получение данных из сетевого расположения). Класс получает незапущенный экземпляр Задачи, добавляет метод ContinueWith и затем вызывает Start для Задачи. Примерно так:

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

Класс получает экземпляр Task с помощью интерфейса, поэтому я могу добавить экземпляр Task, которым управляет мой тест. Однако я не могу создать тот, над которым у меня достаточно контроля.

Я не хочу вводить потоки в тест, но все же хочу, чтобы Задача велась асинхронно, поэтому я попытался написать класс, реализующий шаблон BeginInvoke / EndInvoke без потоков, и использовать TaskFactory .FromAsync метод для создания Задачи.

Идея состоит в том, что тест может вызывать метод в классе, который запускает задачу, а затем, когда он возвращает тест, может предоставить данные результата в объект Async, который завершает операцию, оставаясь в том же потоке.

Однако, когда я пытаюсь вызвать Start для этой задачи, я получаю сообщение об ошибке, в котором говорится, что «Start не может быть вызван для задачи с нулевым действием». К сожалению, Google не сильно помогает мне в этом сообщении, поэтому я не уверен, правильно ли я реализовал свой объект 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, если я его разверну.

Кто-нибудь может увидеть, что я делаю не так?

[Edit: я также написал тест, который подтверждает, что класс NonThreadedAsync правильно работает с классическим шаблоном Begin / End (или, по крайней мере, my пониманием шаблона 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);
}

1 Ответ

5 голосов
/ 07 апреля 2011

О, я понял.Наш API был неправ в том, что пытался вернуть незапущенные Задачи.Удаление Start () из тестируемого класса решает проблему.Однако, исследуя это, я также обнаружил, что слишком много делал, чтобы получить контролируемую тестом асинхронную задачу.Согласно сообщению Стивена Туба здесь , мы можем просто использовать 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);
}
...