Asynchronous Programming

原文:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/tree/master

Asynchronous programming has been around for several years on the .NET platform but has historically been very difficult to do well. Since the introduction of async/await
in C# 5 asynchronous programming has become mainstream. Modern frameworks (like ASP.NET Core) are fully asynchronous and it's very hard to avoid the async keyword when writing
web services. As a result, there's been lots of confusion on the best practices for async and how to use it properly. This section will try to lay out some guidance with examples of bad and good patterns of how to write asynchronous code.
异步编程在 .NET 上已有多年历史,但过去很难正确实现。C# 5 引入 async/await 后,它已成为主流。现代框架(如 ASP.NET Core)完全异步,Web 服务几乎无法避开 async。本节用正反示例说明异步代码的最佳实践。

Asynchrony is viral

异步具有传染性

Once you go async, all of your callers SHOULD be async, since efforts to be async amount to nothing unless the entire call stack is async. In many cases, being partially asynchronous can be worse than being entirely synchronous. Therefore it is best to go all in, and make everything async at once.
一旦采用异步,所有调用方也都应该异步;只有整个调用栈异步,异步化才有意义。部分异步往往比完全同步更糟,因此最好彻底异步化。

BAD This example uses the Task.Result and as a result blocks the current thread to wait for the result. This is an example of sync over async.
错误 使用 Task.Result 会阻塞当前线程等待结果。这是同步等待异步的示例。

public int DoSomethingAsync()
{
    var result = CallDependencyAsync().Result;
    return result + 1;
}

GOOD This example uses the await keyword to get the result from CallDependencyAsync.
正确 使用 await 获取 CallDependencyAsync 的结果。

public async Task<int> DoSomethingAsync()
{
    var result = await CallDependencyAsync();
    return result + 1;
}

Async void

Async void

The use of async void in ASP.NET Core applications is ALWAYS bad. Avoid it, never do it. Typically, it's used when developers are trying to implement fire-and-forget patterns triggered by a controller action. Async void methods will crash the process if an exception is thrown. We'll look at more of the patterns that cause developers to do this in ASP.NET Core applications but here's a simple example:
在 ASP.NET Core 中使用 async void 始终是错误的。它常被用于控制器触发的“即发即弃”模式;若抛出异常,会使进程崩溃。下面是简单示例:

BAD Async void methods can't be tracked and therefore unhandled exceptions can result in application crashes.
错误 async void 方法无法跟踪,未处理异常可能使应用崩溃。

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        BackgroundOperationAsync();
        return Accepted();
    }
    
    public async void BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
}

GOOD Task-returning methods are better since unhandled exceptions trigger the TaskScheduler.UnobservedTaskException.
正确 返回 Task 的方法更好,未处理异常会触发 TaskScheduler.UnobservedTaskException

public class MyController : Controller
{
    [HttpPost("/start")]
    public IActionResult Post()
    {
        Task.Run(BackgroundOperationAsync);
        return Accepted();
    }
    
    public async Task BackgroundOperationAsync()
    {
        var result = await CallDependencyAsync();
        DoSomething(result);
    }
}

Prefer Task.FromResult over Task.Run for pre-computed or trivially computed data

对预先计算或计算简单的数据,优先使用 Task.FromResult 而非 Task.Run

For pre-computed results, there's no need to call Task.Run, which will end up queuing a work item to the thread pool that will immediately complete with the pre-computed value. Instead, use Task.FromResult, to create a task wrapping already computed data.
预先计算的结果无需使用 Task.Run 将立即完成的工作排入线程池。应使用 Task.FromResult 封装已计算数据。

BAD This example wastes a thread-pool thread to return a trivially computed value.
错误 浪费线程池线程返回简单计算值。

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.Run(() => a + b);
   }
}

GOOD This example uses Task.FromResult to return the trivially computed value. It does not use any extra threads as a result.
正确 使用 Task.FromResult 返回该值,不使用额外线程。

public class MyLibrary
{
   public Task<int> AddAsync(int a, int b)
   {
       return Task.FromResult(a + b);
   }
}

💡NOTE: Using Task.FromResult will result in a Task allocation. Using ValueTask<T> can completely remove that allocation.
💡注意:Task.FromResult 会分配 TaskValueTask<T> 可消除该分配。

GOOD This example uses a ValueTask<int> to return the trivially computed value. It does not use any extra threads as a result. It also does not allocate an object on the managed heap.
正确 ValueTask<int> 不使用额外线程,也不在托管堆分配对象。

public class MyLibrary
{
   public ValueTask<int> AddAsync(int a, int b)
   {
       return new ValueTask<int>(a + b);
   }
}

Avoid using Task.Run for long-running work that blocks the thread

避免用 Task.Run 执行阻塞线程的长期工作

Long-running work in this context refers to a thread that's running for the lifetime of the application doing background work (like processing queue items, or sleeping and waking up to process some data). Task.Run will queue a work item to the thread pool. The assumption is that that work will finish quickly (or quickly enough to allow reusing that thread within some reasonable timeframe). Stealing a thread-pool thread for long-running work is bad since it takes that thread away from other work that could be done (timer callbacks, task continuations, etc). Instead, spawn a new thread manually to do long-running blocking work.
这里的长期工作指线程在应用整个生命周期执行后台工作。Task.Run 假定工作会较快完成并复用线程。长期占用线程池线程会妨碍计时器回调、任务延续等工作,应手动创建新线程。

💡 NOTE: The thread pool grows if you block threads but it's bad practice to do so.
💡 注意:阻塞时线程池会增长,但这不是良好实践。

💡 NOTE:Task.Factory.StartNew has an option TaskCreationOptions.LongRunning that under the covers creates a new thread and returns a Task that represents the execution. Using this properly requires several non-obvious parameters to be passed in to get the right behavior on all platforms.
💡 注意:Task.Factory.StartNewTaskCreationOptions.LongRunning 会创建新线程并返回表示执行的 Task;正确使用需要多个不直观的参数。

💡 NOTE: Don't use TaskCreationOptions.LongRunning with async code as this will create a new thread which will be destroyed after first await.
💡 注意:不要对异步代码使用 TaskCreationOptions.LongRunning,新线程会在首次 await 后销毁。

BAD This example steals a thread-pool thread forever, to execute queued work on a BlockingCollection<T>.
错误 永久占用线程池线程处理 BlockingCollection<T> 的工作。

public class QueueProcessor
{
    private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    
    public void StartProcessing()
    {
        Task.Run(ProcessQueue);
    }
    
    public void Enqueue(Message message)
    {
        _messageQueue.Add(message);
    }
    
    private void ProcessQueue()
    {
        foreach (var item in _messageQueue.GetConsumingEnumerable())
        {
             ProcessItem(item);
        }
    }
    
    private void ProcessItem(Message message) { }
}

GOOD This example uses a dedicated thread to process the message queue instead of a thread-pool thread.
正确 使用专用线程处理消息队列。

public class QueueProcessor
{
    private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    
    public void StartProcessing()
    {
        var thread = new Thread(ProcessQueue) 
        {
            // This is important as it allows the process to exit while this thread is running
            IsBackground = true
        };
        thread.Start();
    }
    
    public void Enqueue(Message message)
    {
        _messageQueue.Add(message);
    }
    
    private void ProcessQueue()
    {
        foreach (var item in _messageQueue.GetConsumingEnumerable())
        {
             ProcessItem(item);
        }
    }
    
    private void ProcessItem(Message message) { }
}

GOOD This example utilizes a TaskFactory with TaskCreationOptions.LongRunning to process the message queue instead of creating a thread manually.
正确 使用带 TaskCreationOptions.LongRunningTaskFactory,而非手动创建线程。

public class QueueProcessor
{
    private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();

    public Task StartProcessing() => Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);

    public void Enqueue(Message message)
    {
        _messageQueue.Add(message);
    }

    private void ProcessQueue()
    {
        foreach (var item in _messageQueue.GetConsumingEnumerable())
        {
            ProcessItem(item);
        }
    }

    private void ProcessItem(Message message) { }
}

Utilizing TaskCreationOptions.LongRunning introduces several advantages in comparison with manual thread creation:
TaskCreationOptions.LongRunning 相比手动创建线程有以下优势:

  • It can be easily combined with await and TPL APIs, such as Task.WhenAll, amongst others.
  • It provides a superior exception-handling mechanism. For instance, in the event of an unhandled exception in a manually created thread, the application will crash (unless handled via AppDomain.CurrentDomain.UnhandledException), but with .LongRunning, it will be wrapped into a Task as an AggregateException.
  • 易与 awaitTask.WhenAll 等 TPL API 组合。
  • 异常处理更好:手动线程中的未处理异常会使应用崩溃;.LongRunning 会将其作为 AggregateException 封装进 Task

💡 NOTE: The TaskCreationOptions.LongRunning option is essentially a recommendation to the TaskScheduler, which may interpret it differently in custom TaskScheduler applications or runtimes, or future updates to the .NET runtime libraries. If your primary goal is to spawn a new dedicated thread, then you might consider using the manual thread creation approach discussed previously.
💡 注意:LongRunning 只是对 TaskScheduler 的建议,具体解释可能变化。若目标是专用线程,可使用手动创建方式。

Avoid using Task.Result and Task.Wait

避免使用 Task.ResultTask.Wait

There are very few ways to use Task.Result and Task.Wait correctly so the general advice is to completely avoid using them in your code.
正确使用它们的方式极少,因此一般应完全避免。

⚠️ Sync over async

⚠️ 同步等待 async

Using Task.Result or Task.Wait to block waiting on an asynchronous operation to complete is MUCH worse than calling a truly synchronous API to block. This phenomenon is dubbed "Sync over async". Here is what happens at a very high level:
Task.ResultTask.Wait 阻塞等待异步操作,比调用同步 API 阻塞糟糕得多,称为“同步等待异步”:

  • An asynchronous operation is kicked off.
  • The calling thread is blocked waiting for that operation to complete.
  • When the asynchronous operation completes, it unblocks the code waiting on that operation. This takes place on another thread.
  • 启动异步操作。
  • 调用线程阻塞等待。
  • 操作完成后,在另一线程解除阻塞。

The result is that we need to use 2 threads instead of 1 to complete synchronous operations. This usually leads to thread-pool starvation and results in service outages.
完成同步操作因此需要两个线程,常导致线程池饥饿和服务中断。

⚠️ Deadlocks

⚠️ 死锁

The SynchronizationContext is an abstraction that gives application models a chance to control where asynchronous continuations run. ASP.NET (non-core), WPF, and Windows Forms each have an implementation that will result in a deadlock if Task.Wait or Task.Result is used on the main thread. This behavior has led to a bunch of "clever" code snippets that show the "right" way to block waiting for a Task. The truth is, there's no good way to block waiting for a Task to complete.
SynchronizationContext 控制异步延续的运行位置。ASP.NET(非 Core)、WPF 和 Windows Forms 中,在主线程使用 Task.Wait 或 Task.Result 会死锁。不存在阻塞等待 Task 的好方法。

💡NOTE: ASP.NET Core does not have a SynchronizationContext and is not prone to the deadlock problem.
💡注意:ASP.NET Core 没有 SynchronizationContext,不易出现该死锁。

BAD The below are all examples that are, in one way or another, trying to avoid the deadlock situation but still succumb to "sync over async" problems.
错误 以下做法试图规避死锁,但仍有“同步等待异步”问题。

public string DoOperationBlocking()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return Task.Run(() => DoAsyncOperation()).Result;
}

public string DoOperationBlocking2()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, and remove the risk of deadlocking.
    // In the case of an exception, this method will throw the exception without wrapping it in an AggregateException.
    return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
}

public string DoOperationBlocking3()
{
    // Bad - Blocking the thread that enters, and blocking the threadpool thread inside.
    // In the case of an exception, this method will throw an AggregateException containing another AggregateException, containing the original exception.
    return Task.Run(() => DoAsyncOperation().Result).Result;
}

public string DoOperationBlocking4()
{
    // Bad - Blocking the thread that enters, and blocking the threadpool thread inside.
    return Task.Run(() => DoAsyncOperation().GetAwaiter().GetResult()).GetAwaiter().GetResult();
}

public string DoOperationBlocking5()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return DoAsyncOperation().Result;
}

public string DoOperationBlocking6()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    return DoAsyncOperation().GetAwaiter().GetResult();
}

public string DoOperationBlocking7()
{
    // Bad - Blocking the thread that enters.
    // Bad - No effort has been made to prevent a present SynchonizationContext from becoming deadlocked.
    var task = DoAsyncOperation();
    task.Wait();
    return task.GetAwaiter().GetResult();
}

Prefer await over ContinueWith

优先使用 await 而非 ContinueWith

Task existed before the async/await keywords were introduced and as such provided ways to execute continuations without relying on the language. Although these methods are still valid to use, we generally recommend that you prefer async/await to using ContinueWith. ContinueWith also does not capture the SynchronizationContext and as a result is actually semantically different to async/await.
Task 早于 async/await,因而提供了语言之外的延续方式。虽仍可用,但建议优先使用 async/awaitContinueWith 不捕获 SynchronizationContext,语义也不同。

BAD The example uses ContinueWith instead of async
错误 使用 ContinueWith 而非 async

public Task<int> DoSomethingAsync()
{
    return CallDependencyAsync().ContinueWith(task =>
    {
        return task.Result + 1;
    });
}

GOOD This example uses the await keyword to get the result from CallDependencyAsync.
正确 使用 await 获取结果。

public async Task<int> DoSomethingAsync()
{
    var result = await CallDependencyAsync();
    return result + 1;
}

Always create TaskCompletionSource<T> with TaskCreationOptions.RunContinuationsAsynchronously

创建 TaskCompletionSource<T> 时始终使用 TaskCreationOptions.RunContinuationsAsynchronously

TaskCompletionSource<T> is an important building block for libraries trying to adapt things that are not inherently awaitable to be awaitable via a Task. It is also commonly used to build higher-level operations (such as batching and other combinators) on top of existing asynchronous APIs. By default, Task continuations will run inline on the same thread that calls Try/Set(Result/Exception/Canceled). As a library author, this means having to understand that calling code can resume directly on your thread. This is extremely dangerous and can result in deadlocks, thread-pool starvation, corruption of state (if code runs unexpectedly) and more.
TaskCompletionSource<T> 可将不可等待对象适配为 Task,也用于构建批处理等操作。默认延续会在调用 Try/Set 的线程上内联运行,可能导致死锁、线程池饥饿和状态损坏。

Always use TaskCreationOptions.RunContinuationsAsynchronously when creating the TaskCompletionSource<T>. This will dispatch the continuation onto the thread pool instead of executing it inline.
始终使用 RunContinuationsAsynchronously,将延续分派到线程池而非内联执行。

BAD This example does not use TaskCreationOptions.RunContinuationsAsynchronously when creating the TaskCompletionSource<T>.
错误 创建时未使用该选项。

public Task<int> DoSomethingAsync()
{
    var tcs = new TaskCompletionSource<int>();
    
    var operation = new LegacyAsyncOperation();
    operation.Completed += result =>
    {
        // Code awaiting on this task will resume on this thread!
        tcs.SetResult(result);
    };
    
    return tcs.Task;
}

GOOD This example uses TaskCreationOptions.RunContinuationsAsynchronously when creating the TaskCompletionSource<T>.
正确 创建时使用该选项。

public Task<int> DoSomethingAsync()
{
    var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
    
    var operation = new LegacyAsyncOperation();
    operation.Completed += result =>
    {
        // Code awaiting on this task will resume on a different thread-pool thread
        tcs.SetResult(result);
    };
    
    return tcs.Task;
}

💡NOTE: There are 2 enums that look alike. TaskCreationOptions.RunContinuationsAsynchronously and TaskContinuationOptions.RunContinuationsAsynchronously. Be careful not to confuse their usage.
💡注意:不要混淆 TaskCreationOptions.RunContinuationsAsynchronouslyTaskContinuationOptions.RunContinuationsAsynchronously

Always dispose CancellationTokenSource(s) used for timeouts

始终释放用于超时的 CancellationTokenSource

CancellationTokenSource objects that are used for timeouts (are created with timers or use the CancelAfter method), can put pressure on the timer queue if not disposed.
用于超时的 CancellationTokenSource 若不释放,会给计时器队列造成压力。

BAD This example does not dispose of the CancellationTokenSource and as a result, the timer stays in the queue for 10 seconds after each request is made.
错误 未释放它,使计时器在每次请求后继续保留 10 秒。

public async Task<Stream> HttpClientAsyncWithCancellationBad()
{
    var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

    using (var client = _httpClientFactory.CreateClient())
    {
        var response = await client.GetAsync("http://backend/api/1", cts.Token);
        return await response.Content.ReadAsStreamAsync();
    }
}

GOOD This example disposes of the CancellationTokenSource and properly removes the timer from the queue.
正确 释放它并从队列移除计时器。

public async Task<Stream> HttpClientAsyncWithCancellationGood()
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
    {
        using (var client = _httpClientFactory.CreateClient())
        {
            var response = await client.GetAsync("http://backend/api/1", cts.Token);
            return await response.Content.ReadAsStreamAsync();
        }
    }
}

Always flow CancellationToken(s) to APIs that take a CancellationToken

始终向相关 API 传递 CancellationToken

Cancellation is cooperative in .NET. Everything in the call chain has to be explicitly passed the CancellationToken in order for it to work well. This means you need to explicitly pass the token into other APIs that take a token if you want cancellation to be most effective.
.NET 采用协作式取消,调用链每层都必须显式传递令牌。

BAD This example neglects to pass the CancellationToken to Stream.ReadAsync making the operation effectively not cancellable.
错误 未将令牌传给 Stream.ReadAsync,操作实际无法取消。

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
{
   byte[] buffer = new byte[1024];
   // We forgot to pass flow cancellationToken to ReadAsync
   int read = await _stream.ReadAsync(buffer, 0, buffer.Length);
   return Encoding.UTF8.GetString(buffer, 0, read);
}

GOOD This example passes the CancellationToken into Stream.ReadAsync.
正确 将令牌传入 Stream.ReadAsync

public async Task<string> DoAsyncThing(CancellationToken cancellationToken = default)
{
   byte[] buffer = new byte[1024];
   // This properly flows cancellationToken to ReadAsync
   int read = await _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
   return Encoding.UTF8.GetString(buffer, 0, read);
}

Cancelling uncancellable operations

取消不可取消的操作

One of the coding patterns that appears when doing asynchronous programming is canceling an uncancellable operation. This usually means creating another task that completes when a timeout or CancellationToken fires, and then using Task.WhenAny to detect a complete or cancelled operation.
常见做法是创建在超时或令牌触发时完成的另一任务,再用 Task.WhenAny 判断完成或取消。

Using CancellationTokens

使用 CancellationToken

BAD This example uses Task.Delay(-1, token) to create a Task that completes when the CancellationToken fires, but if it doesn't fire, there's no way to dispose of the CancellationTokenRegistration created inside of Task.Delay. This can lead to a memory leak.
错误 Task.Delay(-1, token) 内部的注册在令牌不触发时无法释放,可能内存泄漏。

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    // There's no way to dispose of the registration
    var delayTask = Task.Delay(-1, cancellationToken);

    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
        // Operation cancelled
        throw new OperationCanceledException();
    }

    return await task;
}

GOOD This example disposes of the CancellationTokenRegistration when one of the Task(s) is complete.
正确 任一 Task 完成时释放注册。

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);

    // This disposes the registration as soon as one of the tasks trigger
    using (cancellationToken.Register(state =>
    {
        ((TaskCompletionSource<object>)state).TrySetResult(null);
    },
    tcs))
    {
        var resultTask = await Task.WhenAny(task, tcs.Task);
        if (resultTask == tcs.Task)
        {
            // Operation cancelled
            throw new OperationCanceledException(cancellationToken);
        }

        return await task;
    }
}

GOOD Prefer Task.WaitAsync on .NET >= 6;
正确 .NET >= 6 优先使用 Task.WaitAsync

Using a timeout

使用超时

BAD This example does not cancel the timer even if the operation successfully completes. This means you could end up with lots of timers, which can flood the timer queue.
错误 操作成功后不取消计时器,可能淹没计时器队列。

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
    var delayTask = Task.Delay(timeout);

    var resultTask = await Task.WhenAny(task, delayTask);
    if (resultTask == delayTask)
    {
        // Operation cancelled
        throw new OperationCanceledException();
    }

    return await task;
}

GOOD This example cancels the timer if the operation successfully completes.
正确 操作成功后取消计时器。

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
    using (var cts = new CancellationTokenSource())
    {
        var delayTask = Task.Delay(timeout, cts.Token);

        var resultTask = await Task.WhenAny(task, delayTask);
        if (resultTask == delayTask)
        {
            // Operation cancelled
            throw new OperationCanceledException();
        }
        else
        {
            // Cancel the timer task so that it does not fire
            cts.Cancel();
        }

        return await task;
    }
}

GOOD Prefer Task.WaitAsync on .NET >= 6;
正确 .NET >= 6 优先使用 Task.WaitAsync

Always call FlushAsync on StreamWriter(s) or Stream(s) before calling Dispose

调用 Dispose 前始终调用 FlushAsync

When writing to a Stream or StreamWriter, even if the asynchronous overloads are used for writing, the underlying data might be buffered. When data is buffered, disposing the Stream or StreamWriter via the Dispose method will synchronously write/flush, which results in blocking the thread and could lead to thread-pool starvation. Either use the asynchronous DisposeAsync method (for example via await using) or call FlushAsync before calling Dispose.
异步写入时底层数据仍可能缓冲;Dispose 会同步刷新并阻塞线程。应使用 DisposeAsync(如 await using)或先调用 FlushAsync

💡NOTE: This is only problematic if the underlying subsystem does IO.
💡注意:仅底层子系统执行 IO 时有问题。

BAD This example ends up blocking the request by writing synchronously to the HTTP-response body.
错误 同步写入 HTTP 响应正文会阻塞请求。

app.Run(async context =>
{
    // The implicit Dispose call will synchronously write to the response body
    using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("Hello World");
    }
});

GOOD This example asynchronously flushes any buffered data while disposing the StreamWriter.
正确 释放时异步刷新缓冲数据。

app.Run(async context =>
{
    // The implicit AsyncDispose call will flush asynchronously
    await using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("Hello World");
    }
});

GOOD This example asynchronously flushes any buffered data before disposing the StreamWriter.
正确 释放前异步刷新缓冲数据。

app.Run(async context =>
{
    using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("Hello World");

        // Force an asynchronous flush
        await streamWriter.FlushAsync();
    }
});

Prefer async/await over directly returning Task

优先使用 async/await 而非直接返回 Task

There are benefits to using the async/await keyword instead of directly returning the Task:

  • Asynchronous and synchronous exceptions are normalized to always be asynchronous.
  • The code is easier to modify (consider adding a using, for example).
  • Diagnostics of asynchronous methods are easier (debugging hangs etc).
  • Exceptions thrown will be automatically wrapped in the returned Task instead of surprising the caller with an actual exception.
  • Async locals will not leak out of async methods. If you set an async local in a non-async method, it will "leak" out of that call.
    优点包括:
  • 同步和异步异常统一为异步。
  • 代码更易修改。
  • 更易诊断。
  • 异常自动封装进 Task
  • Async local 不会泄漏出异步方法。

BAD This example directly returns the Task to the caller.
错误 直接返回 Task

public Task<int> DoSomethingAsync()
{
    return CallDependencyAsync();
}

GOOD This example uses async/await instead of directly returning the Task.
正确 使用 async/await。

public async Task<int> DoSomethingAsync()
{
    return await CallDependencyAsync();
}

💡NOTE: There are performance considerations when using an async state machine over directly returning the Task. It's always faster to directly return the Task since it does less work but you end up changing the behavior and potentially losing some of the benefits of the async state machine.
💡注意:异步状态机有性能成本;直接返回 Task 更快,但会改变行为并失去部分优点。

AsyncLocal<T>

AsyncLocal<T>

Async locals are a way to store/retrieve ambient state throughout an application. This can be a very useful alternative to flowing explicit state everywhere, especially through call sites that you do not have much control over. While it is powerful, it is also dangerous if used incorrectly. Async locals are attached to the execution context which flows everywhere implicitly. Disabling execution context flow requires the use of advanced APIs (typically prefixed with the Unsafe name). As such, there's very little control over what code will attempt to access these values.
Async local 用于存取环境状态,能避免显式传递状态,但误用很危险。它附加到会隐式流向各处执行上下文,很难控制访问者。

Creating an AsyncLocal<T>

创建 AsyncLocal<T>

If you can avoid async locals, do so by explicitly passing state around or using techniques like inversion of control.
能避免时,应显式传递状态或使用控制反转。

If you cannot avoid it, it's best to make sure that anything put into an async local is:
无法避免时,存入的对象应:

  1. Not disposable
  2. Immutable/read-only/thread-safe
  3. 不可释放
  4. 不可变、只读或线程安全

Let's look at 2 examples:
来看两个示例:

  1. BAD A disposable object stored in an async local
  2. 错误 存储可释放对象
using (var thing = new DisposableThing())
{
    // Make the disposable object available ambiently
    DisposableThing.Current = thing;

    Dispatch();

    // We're about to dispose the object so make sure nobody else captures this instance
    DisposableThing.Current = null;
}

void Dispatch()
{
    // Task.Run will capture the current execution context (which means async locals are captured in the callback)
    _ = Task.Run(async () =>
    {
        // Delay for a second then log
        await Task.Delay(1000);

        Log();
    });
}

void Log()
{
    try
    {
        // Get the current value and make sure it's not null before reading the value
        var thing = DisposableThing.Current;
        if (thing is not null)
        {
            Console.WriteLine($"Logging ambient value {thing.Value}");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

Console.ReadLine();

class DisposableThing : IDisposable
{
    private static readonly AsyncLocal<DisposableThing?> _current = new();

    private bool _disposed;

    public static DisposableThing? Current
    {
        get => _current.Value;
        set
        {
            _current.Value = value;
        }
    }

    public int Value
    {
        get
        {
            if (_disposed) throw new ObjectDisposedException(GetType().FullName);
            return 1;
        }
    }

    public void Dispose()
    {
        _disposed = true;
    }
}

This above example will always result in an ObjectDisposedException being thrown. Even though the Log method defensively checks for null before logging the value, it has a reference to the disposed of DisposableThing. Setting the AsyncLocal<DisposableThing> to null does not affect the code inside of Log, this is because the execution context is copy on write. This means that all future reads DisposableThing.Current will be null, but it won't affect any of the previous reads.
上例始终抛出 ObjectDisposedExceptionLog 持有已释放对象的引用。将 AsyncLocal 设为 null 不影响已复制的执行上下文;未来读取为 null,先前读取不受影响。

When we set DisposableThing.Current = null; we are making a new execution context, not mutating the one that was captured by Task.Run. To get a better understanding of this run the following code:
设置为 null 会创建新执行上下文,而非修改 Task.Run 捕获的上下文:

DisposableThing.Current = new DisposableThing();

Console.WriteLine("After setting thing " + ExecutionContext.Capture().GetHashCode());

DisposableThing.Current = null;

Console.WriteLine("After setting Current to null " + ExecutionContext.Capture().GetHashCode());

The hash code of the execution context is different each time we set a new value.
每次设置新值时,执行上下文哈希都不同。

⚠️ It might be tempting to update the logic in DisposableThing.Current to mutate the original execution context instead of setting the async local directly (StrongBox<T> is a reference type that stores the underlying T in a mutable field):
⚠️ 可能想借助 StrongBox<T> 修改原执行上下文:

class DisposableThing : IDisposable
{
    private static readonly AsyncLocal<StrongBox<DisposableThing?>> _current = new();

    private bool _disposed;

    public static DisposableThing? Current
    {
        get => _current.Value?.Value;
        set
        {
            var box = _current.Value;
            if (box is not null)
            {
                // Mutate the value in any execution context that was copied
                box.Value = null;
            }

            if (value is not null)
            {
                _current.Value = new StrongBox<DisposableThing?>(value);
            }
        }
    }

    public int Value
    {
        get
        {
            if (_disposed) throw new ObjectDisposedException(GetType().FullName);
            return 1;
        }
    }

    public void Dispose()
    {
        _disposed = true;
    }
}

This will have the desired effect and will set the value to null in any execution context that references this async local value.
这会在引用该值的执行上下文中将其设为 null。

DisposableThing.Current = new DisposableThing();

Console.WriteLine("After setting thing " + ExecutionContext.Capture().GetHashCode());

DisposableThing.Current = null;

Console.WriteLine("After setting Current to null " + ExecutionContext.Capture().GetHashCode());

⚠️ While this looks attractive, the reference to DisposableThing.Current might have still been captured before the value was set to null:
⚠️ 但该引用可能在设为 null 前已被捕获:

void Dispatch()
{
    // Task.Run will capture the current execution context (which means async locals are captured in the callback)
    _ = Task.Run(async () =>
    {
        // Get the current reference
        var current = DisposableThing.Current;

        // Delay for a second then log
        await Task.Delay(1000);

        Log(current);
    });
}

void Log(DisposableThing thing)
{
    try
    {
        Console.WriteLine($"Logging ambient value {thing.Value}");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

There's a race condition between the capture of the DisposableThing, the disposal of DisposableThing and setting DisposableThing.Current it to null. In the end, the code is unreliable and may fail at random. Don't store disposable objects in async locals.
捕获、释放和清空之间存在竞态,代码可能随机失败。不要在 async local 中存储可释放对象。

  1. BAD A non-thread-safe object stored in an async local
  2. 错误 存储非线程安全对象
AmbientValues.Current = new Dictionary<int, string>();

Parallel.For(0, 10, i =>
{
    AmbientValues.Current[i] = "processing";
    LogCurrentValues();
    AmbientValues.Current[i] = "done";
});


void LogCurrentValues()
{
    foreach (var pair in AmbientValues.Current)
    {
        Console.WriteLine(pair);
    }
}

class AmbientValues
{
    private static readonly AsyncLocal<Dictionary<int, string>> _current = new();

    public static Dictionary<int, string> Current
    {
        get => _current.Value!;
        set => _current.Value = value;
    }
}

The above example stores a normal Dictionary<int, string> in an async local and does some parallel processing on it. While this may be obvious from the above example, async locals allow arbitrary code on arbitrary threads to access the execution context and thus any async locals associated with said context. As a result, it is important to assume that data can be accessed concurrently and should be made thread-safe as a result.
Async local 允许任意线程访问相关执行上下文,因此数据可能被并发访问,必须线程安全。

class AmbientValues
{
    private static readonly AsyncLocal<ConcurrentDictionary<int, string>> _current = new();

    public static ConcurrentDictionary<int, string> Current
    {
        get => _current.Value!;
        set => _current.Value = value;
    }
}

GOOD The above uses a ConcurrentDictionary<int, string> which is thread safe.
正确 使用线程安全的 ConcurrentDictionary<int, string>

Don't leak your AsyncLocal<T>

不要泄漏 AsyncLocal<T>

Async locals flow across awaits automatically and can be captured by any API that explicitly calls ExecutionContext.Capture. The latter can lead to memory leaks in certain situations.

异步本地变量会自动跨 await 流动,也可能被任何显式调用 ExecutionContext.Capture 的 API 捕获。在某些情况下,后者可能导致内存泄漏。

Common APIs that capture the ExecutionContext

会捕获 ExecutionContext 的常见 API

APIs that run user callbacks usually capture the current execution context in order to preserve async locals between callback registration and execution. Here are examples of some APIs that do this:

运行用户回调的 API 通常会捕获当前执行上下文,以便在回调注册与执行之间保留异步本地变量。以下是一些会这样做的 API:

  • Timer
  • Timer
  • CancellationToken.Register
  • CancellationToken.Register
  • new FileSystemWatcher
  • new FileSystemWatcher
  • SocketAsyncEventArgs
  • SocketAsyncEventArgs
  • Task.Run
  • Task.Run
  • ThreadPool.QueueUserWorkItem
  • ThreadPool.QueueUserWorkItem

BAD Here's an example of an execution context leak that causes memory pressure because of a lifetime mismatch between the API capturing the execution context, and the lifetime of the data stored in the async local.

错误示例 下面是一个执行上下文泄漏的示例。捕获执行上下文的 API 与异步本地变量中所存数据的生命周期不匹配,从而造成内存压力。

using System.Collections.Concurrent;

// Singleton cache
var cache = new NumberCache(TimeSpan.FromHours(1));

var executionContext = ExecutionContext.Capture();

// Simulate 10000 concurrent requests
Parallel.For(0, 10000, i =>
{
    // Restore the initial ExecutionContext per "request"
    ExecutionContext.Restore(executionContext!);

    ChunkyObject.Current = new ChunkyObject();

    cache.Add(i);
});

Console.WriteLine("Before GC: " + BytesAsString(GC.GetGCMemoryInfo().HeapSizeBytes));
Console.ReadLine();

GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine("After GC: " + BytesAsString(GC.GetGCMemoryInfo().HeapSizeBytes));
Console.ReadLine();

static string BytesAsString(long bytes)
{
    string[] suffix = { "B", "KB", "MB", "GB", "TB" };
    int i;
    double doubleBytes = 0;

    for (i = 0; bytes / 1024 > 0; i++, bytes /= 1024)
    {
        doubleBytes = bytes / 1024.0;
    }

    return string.Format("{0:0.00} {1}", doubleBytes, suffix[i]);
}

public class NumberCache
{
    private readonly ConcurrentDictionary<int, CancellationTokenSource> _cache = new ConcurrentDictionary<int, CancellationTokenSource>();
    private TimeSpan _timeSpan;

    public NumberCache(TimeSpan timeSpan)
    {
        _timeSpan = timeSpan;
    }

    public void Add(int key)
    {
        var cts = _cache.GetOrAdd(key, _ => new CancellationTokenSource());
        // Delete entry on expiration
        cts.Token.Register((_, _) => _cache.TryRemove(key, out _), null);

        // Start count down
        cts.CancelAfter(_timeSpan);
    }
}

class ChunkyObject
{
    private static readonly AsyncLocal<ChunkyObject?> _current = new();

    // Stores lots of data (but it should be gen0)
    private readonly string _data = new string('A', 1024 * 32);

    public static ChunkyObject? Current
    {
        get => _current.Value;
        set => _current.Value = value;
    }

    public string Data => _data;
}

The above example has a singleton NumberCache that stores numbers for an hour. We have a ChunkyObject which stores a 32K string in a field, and has an async local so that any code running may access the current ChunkyObject. This object should be collected when the GC runs, but instead, we're implicitly capturing the ChunkyObject in the NumberCache via CancellationToken.Register.

上面的示例包含一个单例 NumberCache,它会将数字存储一小时。还有一个 ChunkyObject,其字段中存储了一个 32K 的字符串,并通过异步本地变量使任何正在运行的代码都能访问当前 ChunkyObject。该对象本应在 GC 运行时被回收,但实际上,我们通过 CancellationToken.RegisterChunkyObject 隐式捕获到了 NumberCache 中。

Instead of just caching the number and a CancellationTokenSource, we're implicitly capturing and storing all async locals attached to the current execution context for an hour!

我们并非只缓存了数字和 CancellationTokenSource,而是隐式捕获并存储了附加到当前执行上下文的所有异步本地变量,时间长达一小时!

Try running the sample locally. Running this on my machine reports numbers like this:

请尝试在本地运行该示例。在我的计算机上运行时,会报告如下数值:

Before GC: 654.65 MB
After GC: 659.68 MB

Here's a look at the heap with those objects. You can see we have stored 10,000 ChunkyObjects, strings rooted by those chunky objects. The object graph looks like
CancellationTokenSource -> ExecutionContext -> AsyncLocalValueMap -> ChunkObject -> string.

下面查看包含这些对象的堆。可以看到,我们存储了 10,000 个 ChunkyObject,以及以这些大型对象为根的字符串。对象图如下:
CancellationTokenSource -> ExecutionContext -> AsyncLocalValueMap -> ChunkObject -> string。

image

With one small tweak to this code, we can avoid the implicit execution context capture.

只需对此代码稍作调整,就可以避免隐式捕获执行上下文。

GOOD Use CancellationToken.UnsafeRegister to avoid capturing the execution context and any async locals as part of the NumberCache:

正确示例 使用 CancellationToken.UnsafeRegister,避免将执行上下文及任何异步本地变量捕获为 NumberCache 的一部分:

public class NumberCache
{
    private readonly ConcurrentDictionary<int, CancellationTokenSource> _cache = new ConcurrentDictionary<int, CancellationTokenSource>();
    private TimeSpan _timeSpan;

    public NumberCache(TimeSpan timeSpan)
    {
        _timeSpan = timeSpan;
    }

    public void Add(int key)
    {
        var cts = _cache.GetOrAdd(key, _ => new CancellationTokenSource());
        // Delete entry on expiration
        cts.Token.UnsafeRegister((_, _) => _cache.TryRemove(key, out _), null);

        // Start count down
        cts.CancelAfter(_timeSpan);
    }
}

The GC numbers after this change:

更改后的 GC 数值如下:

Before GC: 10.32 MB
After GC: 5.10 MB

The heap looks like we'd expect. There's no execution context capture, so the ChunkyObject isn't stored.

堆的情况符合预期。由于没有捕获执行上下文,因此不会存储 ChunkyObject

image

💡 NOTE: You have NO control over how APIs decide to store the execution context, but with this understanding, you should be able to minimize memory leaks by clearing the memory using the technique described in Creating an AsyncLocal<T> section.

💡 注意:你无法控制 API 如何决定存储执行上下文,但理解这一点后,便可以使用 创建 AsyncLocal<T> 一节所述的技术清理内存,从而尽量减少内存泄漏。

using System.Collections.Concurrent;

// Singleton cache
var cache = new NumberCache(TimeSpan.FromHours(1));

var executionContext = ExecutionContext.Capture();

// Simulate 10000 concurrent requests
Parallel.For(0, 10000, i =>
{
    // Restore the initial ExecutionContext per "request"
    ExecutionContext.Restore(executionContext!);

    ChunkyObject.Current = new ChunkyObject();

    cache.Add(i);

    // Null out the chunky object so the GC can release the memory
    ChunkyObject.Current = default;
});

class ChunkyObject
{
    private static readonly AsyncLocal<StrongBox<ChunkyObject?>> _current = new();

    // Stores lots of data (but it should be gen0)
    private readonly string _data = new string('A', 1024 * 32);

    public static ChunkyObject? Current
    {
        get => _current.Value?.Value;
        set
        {
            var box = _current.Value;
            if (box is not null)
            {
                // Mutate the value in any execution context that was copied
                box.Value = null;
            }

            if (value is not null)
            {
                _current.Value = new StrongBox<ChunkyObject?>(value);
            }
        }
    }

    public string Data => _data;
}

This technique reduces the heap memory significantly:

此技术可显著减少堆内存:

Before GC: 7.91 MB
After GC: 5.66 MB

The execution context is storing StrongBox<ChunkyObject> with a null reference to the ChunkyObject. This is technically still a "leak" but we've reduced the impact significantly. Here's a look at the memory profile showing objects with 10,000 allocations (the number of requests we created). You can see the GC has collected ChunkObject instances but there are still 10,000 references to StrongBox<ChunkyObject>.

执行上下文存储的是 StrongBox<ChunkyObject>,其中对 ChunkyObject 的引用为 null。严格来说,这仍然属于“泄漏”,但我们已经显著降低了其影响。下面的内存分析显示了发生 10,000 次分配的对象(也就是我们创建的请求数)。可以看到,GC 已经回收了 ChunkObject 实例,但仍有 10,000 个对 StrongBox<ChunkyObject> 的引用。

image

Avoid setting AsyncLocal<T> values outside of async methods

避免在异步方法之外设置 AsyncLocal<T> 的值

Async methods have a special behavior for async locals that makes sure values do not propagate outside of the async method.

异步方法对异步本地变量具有一种特殊行为,可确保其值不会传播到异步方法之外。

BAD Avoid setting async local values outside of async methods:

错误示例 避免在异步方法之外设置异步本地变量的值:

var local = new AsyncLocal<int>();
MethodA();
Console.WriteLine(local.Value);

void MethodA()
{
    local.Value = 1;
    MethodB();
    Console.WriteLine(local.Value);
}

void MethodB()
{
    local.Value = 2;
    Console.WriteLine(local.Value);
}

The above prints 2, 2, 2. The execution context mutations are being propagated outside of the method. This can lead to extremely confusing behavior and hard-to-track down bugs.

以上代码会输出 2、2、2。对执行上下文的更改传播到了方法之外。这可能导致极其令人困惑的行为和难以追踪的 bug。

GOOD Set async locals in async methods:

正确示例 在异步方法中设置异步本地变量:

var local = new AsyncLocal<int>();
await MethodA();
Console.WriteLine(local.Value);

async Task MethodA()
{
    local.Value = 1;
    await MethodB();
    Console.WriteLine(local.Value);
}

async Task MethodB()
{
    local.Value = 2;
    Console.WriteLine(local.Value);
}

The above will print 2, 1, 0. This is because the async method restores the original execution context on exit.

以上代码将输出 2、1、0。这是因为异步方法退出时会还原原始执行上下文。

ConfigureAwait

ConfigureAwait

TBD

待补充

Scenarios

场景

The above tries to distill general guidance but doesn't do justice to the kinds of real-world situations that cause code like this to be written in the first place (bad code). This section tries to take concrete examples from real applications and turn them into something simple to help you relate these problems to existing codebases.

以上内容尝试提炼通用指导原则,但未能充分反映现实中最初促使人们编写此类代码(错误代码)的各种情形。本节尝试从实际应用中选取具体示例并加以简化,以帮助你将这些问题与现有代码库联系起来。

Timer callbacks

Timer 回调

BAD The Timer callback is void-returning and we have asynchronous work to execute. This example uses async void to accomplish it and as a result, can crash the process if an exception occurs.

错误示例 Timer 回调返回 void,但我们需要执行异步工作。此示例使用 async void 来完成该工作,因此发生异常时可能导致进程崩溃。

public class Pinger
{
    private readonly Timer _timer;
    private readonly HttpClient _client;
    
    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new Timer(Heartbeat, null, 1000, 1000);
    }

    public async void Heartbeat(object state)
    {
        await _client.GetAsync("http://mybackend/api/ping");
    }
}

BAD This attempts to block the Timer callback. This may result in thread-pool starvation and is an example of sync over async

错误示例 此代码尝试阻塞 Timer 回调。这可能导致线程池饥饿,也是以同步方式执行异步操作的一个示例。

public class Pinger
{
    private readonly Timer _timer;
    private readonly HttpClient _client;
    
    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new Timer(Heartbeat, null, 1000, 1000);
    }

    public void Heartbeat(object state)
    {
        _client.GetAsync("http://mybackend/api/ping").GetAwaiter().GetResult();
    }
}

GOOD This example uses an async Task-based method and discards the Task in the Timer callback. If this method fails, it will not crash the process. Instead, it will fire the TaskScheduler.UnobservedTaskException event.

正确示例 此示例使用基于 async Task 的方法,并在 Timer 回调中丢弃 Task。如果该方法失败,不会导致进程崩溃,而是会触发 TaskScheduler.UnobservedTaskException 事件。

public class Pinger
{
    private readonly Timer _timer;
    private readonly HttpClient _client;
    
    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new Timer(Heartbeat, null, 1000, 1000);
    }

    public void Heartbeat(object state)
    {
        // Discard the result
        _ = DoAsyncPing();
    }

    private async Task DoAsyncPing()
    {
        await _client.GetAsync("http://mybackend/api/ping");
    }
}

GOOD This example uses the new PeriodicTimer introduced in .NET 6:

正确示例 此示例使用 .NET 6 中引入的新 PeriodicTimer

public class Pinger : IDisposable
{
    private readonly PeriodicTimer _timer;
    private readonly HttpClient _client;

    public Pinger(HttpClient client)
    {
        _client = client;
        _timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
        _ = Task.Run(DoAsyncPings);
    }

    public void Dispose()
    {
        _timer.Dispose();
    }

    private async Task DoAsyncPings()
    {
        while (await _timer.WaitForNextTickAsync())
        {
            // TODO: Handle exceptions
            await _client.GetAsync("http://mybackend/api/ping");
        }
    }
}

Implicit async void delegates

隐式 async void 委托

Imagine a BackgroundQueue with a FireAndForget that takes a callback. This method will execute the callback at some time in the future.

假设有一个 BackgroundQueue,其中的 FireAndForget 接受一个回调。该方法将在未来某个时刻执行此回调。

BAD This will force callers to either block in the callback or use an async void delegate.

错误示例 这会迫使调用方在回调中执行阻塞操作,或者使用 async void 委托。

public class BackgroundQueue
{
    public static void FireAndForget(Action action) { }
}

BAD This calling code is creating an async void method implicitly. The compiler fully supports this today.

错误示例 此调用代码隐式创建了一个 async void 方法。目前编译器完全允许这样做。

public class Program
{
    public void Main(string[] args)
    {
        var httpClient = new HttpClient();
        BackgroundQueue.FireAndForget(async () =>
        {
            await httpClient.GetAsync("http://pinger/api/1");
        });
        
        Console.ReadLine();
    }
}

GOOD This BackgroundQueue implementation offers both sync and async callback overloads.

正确示例 此 BackgroundQueue 实现同时提供同步回调和 async 回调重载。

public class BackgroundQueue
{
    public static void FireAndForget(Action action) { }
    public static void FireAndForget(Func<Task> action) { }
}

ConcurrentDictionary.GetOrAdd

ConcurrentDictionary.GetOrAdd

It's pretty common to cache the result of an asynchronous operation and ConcurrentDictionary is a good data structure for doing that. GetOrAdd is a convenience API for trying to get an item if it's already there or adding it if it isn't. The callback is synchronous so it's tempting to write code that uses Task.Result to produce the value of an asynchronous process but that can lead to thread-pool starvation.

缓存异步操作的结果十分常见,ConcurrentDictionary 是适合此用途的数据结构。GetOrAdd 是一个便捷 API:若项已存在则尝试获取,否则添加该项。由于回调是同步的,人们很容易写出使用 Task.Result 获取异步过程结果的代码,但这可能导致线程池饥饿。

BAD This may result in thread-pool starvation since we're blocking the request thread if the person data is not cached.

错误示例 如果人员数据尚未缓存,此代码会阻塞请求线程,因此可能导致线程池饥饿。

public class PersonController : Controller
{
   private AppDbContext _db;
   
   // This cache needs expiration
   private static ConcurrentDictionary<int, Person> _cache = new ConcurrentDictionary<int, Person>();
   
   public PersonController(AppDbContext db)
   {
      _db = db;
   }
   
   public IActionResult Get(int id)
   {
       var person = _cache.GetOrAdd(id, (key) => _db.People.FindAsync(key).Result);
       return Ok(person);
   }
}

GOOD This implementation won't result in thread-pool starvation since we're storing a task instead of the result itself.

正确示例 此实现存储的是任务,而不是结果本身,因此不会导致线程池饥饿。

⚠️ ConcurrentDictionary.GetOrAdd, when accessed concurrently, may run the value-constructing delegate multiple times. This can result in needlessly kicking off the same potentially expensive computation multiple times.

⚠️ 并发访问 ConcurrentDictionary.GetOrAdd 时,构造值的委托可能会运行多次。这可能会不必要地多次启动同一个潜在开销较大的计算。

public class PersonController : Controller
{
   private AppDbContext _db;
   
   // This cache needs expiration
   private static ConcurrentDictionary<int, Task<Person>> _cache = new ConcurrentDictionary<int, Task<Person>>();
   
   public PersonController(AppDbContext db)
   {
      _db = db;
   }
   
   public async Task<IActionResult> Get(int id)
   {
       var person = await _cache.GetOrAdd(id, (key) => _db.People.FindAsync(key));
       return Ok(person);
   }
}

GOOD This implementation prevents the delegate from being executed multiple times, by using the async lazy pattern: even if construction of the AsyncLazy instance happens multiple times ("cheap" operation), the delegate will be called only once.

正确示例 此实现使用 async 延迟初始化模式,防止委托执行多次:即使多次构造 AsyncLazy 实例(这是“低开销”操作),委托也只会调用一次。

public class PersonController : Controller
{
   private AppDbContext _db;
   
   // This cache needs expiration
   private static ConcurrentDictionary<int, AsyncLazy<Person>> _cache = new ConcurrentDictionary<int, AsyncLazy<Person>>();
   
   public PersonController(AppDbContext db)
   {
      _db = db;
   }
   
   public async Task<IActionResult> Get(int id)
   {
       var person = await _cache.GetOrAdd(id, (key) => new AsyncLazy<Person>(() => _db.People.FindAsync(key))).Value;
       return Ok(person);
   }
   
   private class AsyncLazy<T> : Lazy<Task<T>>
   {
      public AsyncLazy(Func<Task<T>> valueFactory) : base(valueFactory)
      {
      }
   }
}

Constructors

构造函数

Constructors are synchronous. If you need to initialize some logic that may be asynchronous, there are a couple of patterns for dealing with this.

构造函数是同步的。如果需要初始化可能为异步的逻辑,可以使用若干模式来处理。

Here's an example of using a client API that needs to connect asynchronously before use.

下面的示例使用了一个客户端 API,该 API 在使用前需要以异步方式建立连接。

public interface IRemoteConnectionFactory
{
   Task<IRemoteConnection> ConnectAsync();
}

public interface IRemoteConnection
{
    Task PublishAsync(string channel, string message);
    Task DisposeAsync();
}

BAD This example uses Task.Result to get the connection in the constructor. This could lead to thread-pool starvation and deadlocks.

错误示例 此示例在构造函数中使用 Task.Result 获取连接。这可能导致线程池饥饿和死锁。

public class Service : IService
{
    private readonly IRemoteConnection _connection;
    
    public Service(IRemoteConnectionFactory connectionFactory)
    {
        _connection = connectionFactory.ConnectAsync().Result;
    }
}

GOOD This implementation uses a static factory pattern in order to allow asynchronous construction:

正确示例 此实现使用静态工厂模式,以支持异步构造:

public class Service : IService
{
    private readonly IRemoteConnection _connection;

    private Service(IRemoteConnection connection)
    {
        _connection = connection;
    }

    public static async Task<Service> CreateAsync(IRemoteConnectionFactory connectionFactory)
    {
        return new Service(await connectionFactory.ConnectAsync());
    }
}

WindowsIdentity.RunImpersonated

WindowsIdentity.RunImpersonated

This API runs the specified action as the impersonated Windows identity. An asynchronous version of the callback was introduced in .NET 5.0.

此 API 以模拟的 Windows 身份运行指定操作。.NET 5.0 引入了异步版本的回调

BAD This example tries to execute the query asynchronously, and then wait for it outside of the call to RunImpersonated. This will throw because the query might be executing outside of the impersonation context.

错误示例 此示例尝试以异步方式执行查询,然后在 RunImpersonated 调用之外等待查询完成。由于查询可能在模拟身份上下文之外执行,因此会引发异常。

public async Task<IEnumerable<Product>> GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
{
    Task<IEnumerable<Product>> products = null;
    WindowsIdentity.RunImpersonated(
        safeAccessTokenHandle,
        context =>
        {
            products = _db.QueryAsync("SELECT Name from Products");
        });
    return await products;
}

BAD This example uses Task.Result to execute the query synchronously (sync over async). This could lead to thread-pool starvation and deadlocks.

错误示例 此示例使用 Task.Result 同步执行查询(以同步方式执行异步操作)。这可能导致线程池饥饿和死锁。

public IEnumerable<Product> GetDataImpersonated(SafeAccessTokenHandle safeAccessTokenHandle)
{
    return WindowsIdentity.RunImpersonated(
        safeAccessTokenHandle,
        context => _db.QueryAsync("SELECT Name from Products").Result);
}

GOOD This example awaits the result of RunImpersonated (the delegate is Func<Task<IEnumerable<Product>>> in this case). It is the recommended practice in frameworks earlier than .NET 5.0.

正确示例 此示例等待 RunImpersonated 的结果(此处的委托为 Func<Task<IEnumerable<Product>>>)。这是 .NET 5.0 之前框架中的推荐做法。

public async Task<IEnumerable<Product>> GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
{
    return await WindowsIdentity.RunImpersonated(
        safeAccessTokenHandle, 
        context => _db.QueryAsync("SELECT Name from Products"));
}

GOOD This example uses the asynchronous RunImpersonatedAsync function and awaits its result. It is available in .NET 5.0 or newer.

正确示例 此示例使用异步 RunImpersonatedAsync 函数并等待其结果。该函数在 .NET 5.0 或更高版本中可用。

public async Task<IEnumerable<Product>> GetDataImpersonatedAsync(SafeAccessTokenHandle safeAccessTokenHandle)
{
    return await WindowsIdentity.RunImpersonatedAsync(
        safeAccessTokenHandle, 
        context => _db.QueryAsync("SELECT Name from Products"));
}
posted @ 2026-08-07 14:44  talentzemin  阅读(9)  评论(0)    收藏  举报