无法将丰富的异步功能嵌入到自定义 TaskScheduler。这个类的设计没有考虑到async/await。使用自定义 TaskScheduler 的标准方法是作为 Task.Factory.StartNew 方法的参数。此方法不理解异步委托。可以提供异步委托,但它被视为返回某些结果的任何其他委托。要获得异步委托的实际等待结果,必须调用Unwrap() 来返回任务。
但这不是问题。问题在于TaskScheduler 基础架构不会将异步委托视为单个工作单元。每个任务被拆分为多个小任务(使用每个await 作为分隔符),每个小任务单独处理。这严重限制了可以在此类之上实现的异步功能。例如,这里是一个自定义的TaskScheduler,它旨在一次将提供的任务排队(换句话说,限制并发):
public class MyTaskScheduler : TaskScheduler
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
protected async override void QueueTask(Task task)
{
await _semaphore.WaitAsync();
try
{
await Task.Run(() => base.TryExecuteTask(task));
await task;
}
finally
{
_semaphore.Release();
}
}
protected override bool TryExecuteTaskInline(Task task,
bool taskWasPreviouslyQueued) => false;
protected override IEnumerable<Task> GetScheduledTasks() { yield break; }
}
SemaphoreSlim 应确保一次只能运行一个 Task。不幸的是,它不起作用。信号量过早释放,因为在调用QueueTask(task) 中传递的Task 不是代表异步委托的全部工作的任务,而只是直到第一个await 之前的部分。其他部分传递给TryExecuteTaskInline 方法。没有办法关联这些任务部分,因为没有提供标识符或其他机制。以下是实际发生的情况:
var taskScheduler = new MyTaskScheduler();
var tasks = Enumerable.Range(1, 5).Select(n => Task.Factory.StartNew(async () =>
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Item {n} Started");
await Task.Delay(1000);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Item {n} Finished");
}, default, TaskCreationOptions.None, taskScheduler))
.Select(t => t.Unwrap())
.ToArray();
Task.WaitAll(tasks);
输出:
05:29:58.346 项目 1 开始
05:29:58.358 项目 2 开始
05:29:58.358 项目 3 开始
05:29:58.358 项目 4 开始
05:29:58.358 项目 5 开始
05:29:59.358 项目 1 完成
05:29:59.374 项目 5 完成
05:29:59.374 项目 4 完成
05:29:59.374 项目 2 完成
05:29:59.374 第 3 项完成
灾难,所有任务一次排队。
结论:当需要高级异步功能时,自定义 TaskScheduler 类不是可行的方法。
更新:这是另一个观察结果,关于在环境 SynchronizationContext 存在下的自定义 TaskSchedulers。 await 机制默认捕获当前SynchronizationContext 或当前TaskScheduler,并在捕获的上下文中调用延续
或调度程序。如果两者都存在,则首选当前的SynchronizationContext,而忽略当前的TaskScheduler。下面是此行为的演示,在 WinForms 应用程序中¹:
private async void Button1_Click(object sender, EventArgs e)
{
await Task.Factory.StartNew(async () =>
{
MessageBox.Show($"{Thread.CurrentThread.ManagedThreadId}, {TaskScheduler.Current}");
await Task.Delay(1000);
MessageBox.Show($"{Thread.CurrentThread.ManagedThreadId}, {TaskScheduler.Current}");
}, default, TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
}
单击该按钮会依次弹出两条消息,其中包含以下信息:
1、System.Threading.Tasks.SynchronizationContextTaskScheduler
1、System.Threading.Tasks.ThreadPoolTaskScheduler
这个实验表明,只有异步委托的第一部分,即第一个 await 之前的部分,在非默认调度程序上被调度。
这种行为进一步限制了自定义 TaskSchedulers 在启用异步/等待的环境中的实际用途。
¹ 当调用Application.Run 方法时,Windows 窗体应用程序会自动安装WindowsFormsSynchronizationContext。