Task.Factory.StartNew + TaskCreationOptions.LongRunning 的解释

4
我正在尝试理解David Fowler关于Task.Factory.StartNew+TaskCreationOptions.LongRunning的说法这里

注意:不要在异步代码中使用TaskCreationOptions.LongRunning,因为这将创建一个新线程,该线程将在第一个await之后被销毁。

我知道在这种情况下使用Task.RunTask.Factory.StartNew是没有意义的,因为SendLoopAsync和ReceiveLoopAsync完全是异步的。我还知道,如果其中一个方法中有耗时的同步部分,则应该在该方法内使用Task.Run/Task.Factory.StartNew。
David Fowler在他的声明中是什么意思?他的意思是说,不能从异步任务中使用TaskCreationOptions.LongRunning吗?还是他的意思是说SendLoopAsync/ReceiveLoopAsync不应该是异步的?我也知道TaskCreationOptions.LongRunning意味着任务将立即启动,这不是普通任务所具有的特性,普通任务会被调度程序安排,并可能需要一些时间来启动。当同时启动多个连接时,您可以注意到这种行为,这导致发送和接收循环延迟显着。

public async Task StartAsync(CancellationToken cancellationToken)
{
    _ = Task.Factory.StartNew(_ => SendLoopAsync(cancellationToken), TaskCreationOptions.LongRunning, cancellationToken);
    _ = Task.Factory.StartNew(_ => ReceiveLoopAsync(cancellationToken), TaskCreationOptions.LongRunning, cancellationToken);
}

private async Task SendLoopAsync()
{
    await foreach (var message in _outputChannel.Reader.ReadAllAsync(_cancellationSource?.Token))
    {
        if (_clientWebSocket.State == WebSocketState.Open)
        {
            await _clientWebSocket.SendAsync(message.Data.AsMemory(), message.MessageType, true, CancellationToken.None).ConfigureAwait(false);
        }
    }
}

如果你将SynchonousContextAsyncContext)添加到该任务线程中,你就可以使用await。 - Trương Quốc Khánh
1个回答

2
David Fowler认为SendLoopAsync/ReceiveLoopAsync不应该是异步的。如果这个任务在纳秒级别的时间内就会使用起始线程,那么启动一个LongRunning任务就没有意义了。ThreadPool正是为了处理这些情况而发明的。如果ThreadPool因过度饱和而不够响应,则更合理的做法是寻找饱和原因并加以修复,而不是绕过ThreadPool,在每次需要完成微秒级工作时创建新线程。
以下是将LongRunning与async结合使用时会发生的情况的演示:
var stopwatch = Stopwatch.StartNew();
Thread workerThread = null;
List<(string, long, System.Threading.ThreadState)> entries = new();
Task<Task> taskTask = Task.Factory.StartNew(async () =>
{
    workerThread = Thread.CurrentThread;
    entries.Add(("A", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
    await Task.Delay(500);
    entries.Add(("D", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Default);

taskTask.Wait();
entries.Add(("B", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));

workerThread.Join();
entries.Add(("C", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));

await taskTask.Unwrap();
entries.Add(("E", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));

foreach (var (title, elapsed, state) in entries)
    Console.WriteLine($"{title } after {elapsed,3} msec worker thread is {state}");

输出:

A after   2 msec worker thread is Background
B after   6 msec worker thread is Background, Stopped
C after   6 msec worker thread is Stopped
D after 507 msec worker thread is Stopped
E after 507 msec worker thread is Stopped

在 Fiddle 上试一试

工作线程的生命周期最多为6毫秒。它实际上只需要实例化一个异步状态机,并使用System.Threading.Timer组件安排一个回调函数。对于这样微小的工作负载来说,6毫秒看起来像是一个漫长的时间。很可能这6毫秒都用于线程间通信以及线程的创建和销毁。


谢谢您详细的回答! :) - Hulkstance

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接