У меня есть следующий сценарий, который, я думаю, может быть довольно распространенным:
-
Существует задача (обработчик команд UI), которая может выполняться синхронно или асинхронно.
-
Команды могут прибывать быстрее, чем они обрабатываются.
-
Если для команды уже есть ожидающая задача, задача нового обработчика команд должна быть поставлена в очередь и обрабатываться последовательно.
-
Каждый новый результат задачи может зависеть от результата предыдущей задачи.
Отмена должна быть соблюдена, но я хотел бы оставить ее вне сферы применения этого вопроса для простоты. Кроме того, безопасность потоков (concurrency) не является обязательным требованием, но требуется поддержка повторного входа.
Вот базовый пример того, чего я пытаюсь достичь (как консольное приложение, для простоты):
using System;
using System.Threading.Tasks;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var asyncOp = new AsyncOp<int>();
Func<int, Task<int>> handleAsync = async (arg) =>
{
Console.WriteLine("this task arg: " + arg);
//await Task.Delay(arg); // make it async
return await Task.FromResult(arg); // sync
};
Console.WriteLine("Test #1...");
asyncOp.RunAsync(() => handleAsync(1000));
asyncOp.RunAsync(() => handleAsync(900));
asyncOp.RunAsync(() => handleAsync(800));
asyncOp.CurrentTask.Wait();
Console.WriteLine("\nPress any key to continue to test #2...");
Console.ReadLine();
asyncOp.RunAsync(() =>
{
asyncOp.RunAsync(() => handleAsync(200));
return handleAsync(100);
});
asyncOp.CurrentTask.Wait();
Console.WriteLine("\nPress any key to exit...");
Console.ReadLine();
}
// AsyncOp
class AsyncOp<T>
{
Task<T> _pending = Task.FromResult(default(T));
public Task<T> CurrentTask { get { return _pending; } }
public Task<T> RunAsync(Func<Task<T>> handler)
{
var pending = _pending;
Func<Task<T>> wrapper = async () =>
{
// await the prev task
var prevResult = await pending;
Console.WriteLine("\nprev task result: " + prevResult);
// start and await the handler
return await handler();
};
_pending = wrapper();
return _pending;
}
}
}
}
Выход:
Test #1... prev task result: 0 this task arg: 1000 prev task result: 1000 this task arg: 900 prev task result: 900 this task arg: 800 Press any key to continue to test #2... prev task result: 800 prev task result: 800 this task arg: 200 this task arg: 100 Press any key to exit...
Он работает в соответствии с требованиями, пока повторное включение не будет введено в тесте № 2:
asyncOp.RunAsync(() =>
{
asyncOp.RunAsync(() => handleAsync(200));
return handleAsync(100);
});
Желаемый результат должен быть 100
, 200
, а не 200
, 100
, потому что уже существует ожидающая внешняя задача для 100
. Это очевидно потому, что внутренняя задача выполняется синхронно, нарушая логику var pending = _pending; /* ... */ _pending = wrapper()
для внешней задачи.
Как заставить его работать и для теста # 2?
Одним из решений было бы обеспечить асинхронность для каждой задачи с помощью Task.Factory.StartNew(..., TaskScheduler.FromCurrentSynchronizationContext()
. Однако я не хочу налагать асинхронное выполнение на обработчики команд, которые могут быть синхронными внутри. Кроме того, я не хочу зависеть от поведения какого-либо конкретного контекста синхронизации (т.е. Полагаясь на то, что Task.Factory.StartNew
должен вернуться до того, как созданная задача была фактически запущена).
В реальном проекте я отвечаю за то, что AsyncOp
выше, но не имеет никакого контроля над обработчиками команд (т.е. что находится внутри handleAsync
).