Task.Factory.StartNew + TaskCreationOptions.LongRunning 说明

Task.Factory.StartNew + TaskCreationOptions.LongRunning explanation

我想了解大卫福勒对 Task.Factory.StartNew + TaskCreationOptions.LongRunning here.

的看法

NOTE: Don't use TaskCreationOptions.LongRunning with async code as this will create a new thread which will be destroyed after first 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);
        }
    }
}

David Fowler means that the SendLoopAsync/ReceiveLoopAsync should not be async. There is no point at starting a task as LongRunning, if this task is going to use the starting thread for a duration measured in nanoseconds. The ThreadPool 的发明就是为了准确处理这些类型的情况。如果 ThreadPool 由于已经饱和而响应不够,那么更合乎逻辑的做法是尝试找到饱和的原因并修复它,而不是绕过 ThreadPool 并每隔一段时间创建新线程你有一些 microseconds-worth 工作要做的时候。

下面是 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

Try it on Fiddle.

工作线程的生命周期最多为 6 毫秒。它真正需要做的就是实例化一个异步状态机,并使用 System.Threading.Timer 组件安排回调。对于如此微小的工作量,6 毫秒对我来说就像一个永恒。这 6 毫秒很可能用于 inter-thread 通信,以及线程的创建和销毁。