如何将异步方法作为LongRunning任务运行,使所有续行使用非线程池线程?
问题
我希望将异步方法作为长运行线程运行,即使用TaskCreationOptions.LongRunning选项。在测试中,我将非线程池线程视为“长运行”线程(即预期Thread.CurrentThread.IsThreadPoolThread返回false)。
我的尝试代码如下:
var task = Task.Factory.StartNew(DoWorkAsync, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); task.Unwrap().Wait(); static async Task DoWorkAsync() { Console.WriteLine("IsThreadPoolThread: " + Thread.CurrentThread.IsThreadPoolThread); await Task.Delay(100); Console.WriteLine("IsThreadPoolThread: " + Thread.CurrentThread.IsThreadPoolThread); }
输出结果为:
IsThreadPoolThread: False IsThreadPoolThread: True
这意味着第一次await之后,线程不再是长运行线程。
这个结果符合直觉,因为我向TaskFactory.StartNew()方法传入了Func<Task>,其中Task的泛型类型被简单解释为TaskFactory.StartNew()创建的Task的结果类型,因此最终得到Task<Task>,它会在异步方法返回其Task(即遇到第一个await语句时)完成。
请问是否有办法让异步方法的整个执行过程都作为长运行线程运行,即让所有续行都使用非线程池线程?
解决方案
要让异步方法的所有执行步骤都跑在非线程池的长运行线程上,关键是让await之后的续行也回到同一个非线程池线程。这里提供两种可行的实现方式:
方法一:在长运行线程里同步执行整个异步方法
直接在Task.Factory.StartNew创建的长运行线程中,同步等待异步方法完全结束。这样整个异步流程(包括await后的代码)都会在这个非线程池线程上执行:
var task = Task.Factory.StartNew(() => { Console.WriteLine("IsThreadPoolThread: " + Thread.CurrentThread.IsThreadPoolThread); // 同步等待异步方法完成,避免线程提前退出 DoWorkAsync().GetAwaiter().GetResult(); }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); task.Wait(); static async Task DoWorkAsync() { await Task.Delay(100); Console.WriteLine("IsThreadPoolThread: " + Thread.CurrentThread.IsThreadPoolThread); }
这种方式的本质是让长运行线程一直阻塞到异步方法全部执行完毕,所有await续行都会直接在该线程上完成。
方法二:自定义单线程同步上下文
如果需要更灵活的调度(比如不阻塞长运行线程处理其他任务),可以实现一个单线程同步上下文,让所有await续行都被派发到这个长运行线程上执行:
var syncContext = new SingleThreadSynchronizationContext(); var task = Task.Factory.StartNew(() => { // 给当前长运行线程设置自定义同步上下文 SynchronizationContext.SetSynchronizationContext(syncContext); Console.WriteLine("IsThreadPoolThread: " + Thread.CurrentThread.IsThreadPoolThread); // 启动异步方法,完成后通知同步上下文退出循环 DoWorkAsync().ContinueWith(t => syncContext.Complete(), TaskContinuationOptions.ExecuteSynchronously); // 启动同步上下文的消息循环 syncContext.Run(); }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); task.Wait(); static async Task DoWorkAsync() { await Task.Delay(100); Console.WriteLine("IsThreadPoolThread: " + Thread.CurrentThread.IsThreadPoolThread); } // 自定义单线程同步上下文实现 class SingleThreadSynchronizationContext : SynchronizationContext { private readonly Queue<(SendOrPostCallback Callback, object State)> _taskQueue = new(); private bool _isStopped; public override void Post(SendOrPostCallback callback, object state) { lock (_taskQueue) { _taskQueue.Enqueue((callback, state)); } Monitor.Pulse(_taskQueue); } public override void Send(SendOrPostCallback callback, object state) { Post(callback, state); } public void Run() { while (!_isStopped) { (SendOrPostCallback Callback, object State) workItem; lock (_taskQueue) { // 等待队列中有任务或收到停止信号 while (_taskQueue.Count == 0 && !_isStopped) { Monitor.Wait(_taskQueue); } if (_isStopped) break; workItem = _taskQueue.Dequeue(); } // 执行任务 workItem.Callback(workItem.State); } } public void Complete() { lock (_taskQueue) { _isStopped = true; } Monitor.Pulse(_taskQueue); } }
这个自定义上下文会把await后的续行任务加入队列,由长运行线程的消息循环依次执行,确保全程都在非线程池线程上运行。
原代码问题的原因
你原来的代码里,Task.Factory.StartNew创建的长运行线程只负责启动异步方法,当异步方法遇到第一个await时就会返回一个未完成的Task,此时StartNew创建的外层Task<Task>就会标记为完成,后续的续行由.NET默认的线程池调度,自然会切换到线程池线程。
内容的提问来源于stack exchange,提问作者bgh

