C#异步编程最佳实践

C#异步编程最佳实践,在.NET平台上,异步编程已经存在了数年之久,但是从历史上看,它很难很好地完成。自从C#5中引入异步/等待以来,异步编程已成为主流。

C#异步编程最佳实践,在.NET平台上,异步编程已经存在了数年之久,但是从历史上看,它很难很好地完成。自从C#5中引入异步/等待以来,异步编程已成为主流。现代框架(例如ASP.NET Core)是完全异步的,并且在编写Web服务时很难避免使用async关键字。结果,对于异步的最佳实践以及如何正确使用它,存在很多困惑。本节将尝试通过一些不好的例子和如何编写异步代码的方式来提供一些指导。

C#异步编程最佳实践
C#异步编程最佳实践

C#异步编程最佳实践

这是来自于github.com上davidfowl撰写的内容,是关于异步编程的一些最佳实践,因此翻译整理过来分享给大伙。原文点击这里。

异步到底 Asynchrony is viral

用异步方法之后,所有调用者都应该保持异步,因为除非整个调用栈都是异步的,否则异步的努力将毫无意义。在许多情况下,部分异步可能比完全同步更糟。因此,最好的实践是异步到底。

异步到底是指如果调用异步方法后,则从上到下,都使用异步,不要同步,异步方法混合调用。

❌ 错误实践:此示例使用Task.Result作为结果阻止当前线程等待结果。这是通过async进行同步的示例。

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

✅ 最佳实践:此示例使用await关键字从中获取结果CallDependencyAsync

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

无返回值异步

ASP.NET Core应用程序中使用无返回值异步方法总是很糟糕的,应该避免这样使用。通常,它在开发人员尝试实现触发并忘记由控制器操作触发的模式时使用。如果抛出异常,异步void方法将使进程崩溃。我们将研究导致开发人员在ASP.NET Core应用程序中执行此操作的更多模式,但这是一个简单的示例:

❌ 错误实践:无返回值异步方法无法跟踪,因此未处理的异常可能导致应用程序崩溃。

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

✅ 最佳实践: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);
    }
}

Task.FromResultTask.Run更适用于返回预期计算结果

C#异步编程最佳实践,对于预先计算的结果,不需要调用Task.Run,这将最终使工作项排队到线程池中,该线程将立即用预先计算的值完成。 而是使用Task.FromResult创建一个包装已计算数据的任务。

❌ 错误实践:这个例子浪费了线程池线程来返回一个简单的计算值。

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

✅ 最佳实践:此示例用于Task.FromResult返回用于预先计算或简单计算的数据。因此,它不使用任何额外的线程。

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

💡注意:使用Task.FromResult将导致Task分配。使用ValueTask<T>可以完全避免分配。

✅ 最佳实践:此示例使用ValueTask<int>返回简单计算的值。因此,它不使用任何额外的线程。它也不会在托管堆上分配对象。

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

避免将Task.Run用于阻塞线程的长时间运行工作

C#异步编程最佳实践,在这种情况下,长时间运行的工作是指在执行后台工作的应用程序的整个生命周期中正在运行的线程(例如,处理队列项目,或休眠和唤醒以处理某些数据)。Task.Run将工作项排队到线程池中。假定工作将很快完成(或足够快以允许在合理的时间范围内重用该线程)。长时间占用线程池线程以进行长时间工作是很糟糕的,因为它将挤占线程做其他短时间可以完成的工作(定时器回调,任务继续等)。因此最好手动实例化一个新线程来执行长时间运行的阻塞工作。

💡 注意:如果您阻塞线程,则线程池会增加,但是这样做是不好的做法。

💡 注意:Task.Factory.StartNew有一个选项TaskCreationOptions.LongRunning,可以在幕后创建一个新线程并返回一个代表执行的Task。正确使用此参数需要传递几个非显而易见的参数,以在所有平台上获得正确的行为。

💡 注意:请勿TaskCreationOptions.LongRunning与异步代码一起使用,因为这将创建一个新线程,该线程将在first之后销毁await

❌ 错误实践:此示例永远挤占线程池线程,以便在线程池上执行阻塞的工作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) { }
}

✅ 最佳实践:此示例使用专用线程而不是线程池线程来处理消息队列。

public class QueueProcessor
{
    private readonly BlockingCollection<Message> _messageQueue = new BlockingCollection<Message>();
    
    public void StartProcessing()
    {
        var thread = new Thread(ProcessQueue) 
        {
            //设置为后台工作线程,非常重要
            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) { }
}

避免使用Task.ResultTask.Wait

C#异步编程最佳实践,正确使用的方法很少调用Task.ResultTask.Wait因此一般建议是完全避免在代码中使用它们。

⚠️ 异步中同步 Sync over async

使用 Task.Result 或 Task.Wait 阻止等待异步操作完成要比调用真正的同步API阻止要糟糕得多。 此现象称为“异步同步”。 这是一个非常高的级别:

  • 异步操作开始。
  • 调用线程被阻塞,等待该操作完成。
  • 异步操作完成后,它将解除阻塞等待该操作的代码。这发生在另一个线程上。

结果是我们需要使用2个线程而不是1个线程来完成同步操作。这通常会导致线程池饥饿并导致服务中断。

⚠️ 死锁Deadlocks

SynchronizationContext是一种抽象,它使应用程序模型有机会控制异步继续的运行位置。 ASP.NET(非 core),WPF和Windows窗体都有各自的实现,如果在主线程上使用Task.Wait或Task.Result,则会导致死锁。 这种行为导致了一系列“聪明”的代码片段,这些代码片段显示了阻止任务等待的“正确”方式。 事实是,没有什么好的方法可以阻止等待任务完成。

💡注意:ASP.NET Core没有SynchronizationContext且不容易出现死锁问题。

❌ 错误实践:以下是所有尝试以某种方式避免死锁但仍然属于“异步中同步”问题的示例。

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.
    return Task.Run(() => DoAsyncOperation()).GetAwaiter().GetResult();
}

public string DoOperationBlocking3()
{
    // Bad - Blocking the thread that enters, and blocking the theadpool 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 theadpool 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();
}

await优先于ContinueWith

C#异步编程最佳实践,Task在引入async / await关键字之前就已经存在,因此提供了无需依赖语言即可执行延续的方法。虽然这些方法仍然有效使用,我们一般建议你喜欢async/await使用ContinueWithContinueWith也不会捕获,SynchronizationContext因此其在语义上实际上不同于asyncawait

❌ 错误实践:该示例使用ContinueWith代替async

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

✅ 最佳实践:此示例使用await关键字从中获取结果CallDependencyAsync

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

总是为TaskCompletionSource<T>用TaskCreationOptions.RunContinuationsAsynchronously

TaskCompletionSource 是一个重要的建立阻塞库,这些库试图通过Task来适应本来无法等待的事物。 它还通常用于在现有异步API的基础上构建更高级别的操作(例如批处理和其他组合器)。 默认情况下,任务继续将在调用Try / Set(Result / Exception / Canceled)的同一线程上内联运行。 作为库的作者,这意味着必须了解调用代码可以直接在您的线程上继续。 这是非常危险的,可能导致死锁,线程池不足,状态损坏(如果代码意外运行)等等。

创建TaskCompletionSource 时,请始终异步使用TaskCreationOptions.RunContinuations。 这会将连续性调度到线程池上,而不是内联执行。

❌ 错误实践:此示例TaskCreationOptions.RunContinuationsAsynchronously在创建时不使用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;
}

✅ 最佳实践:此示例TaskCreationOptions.RunContinuationsAsynchronously在创建时使用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;
}

💡注意:有两个看起来相似的枚举。TaskCreationOptions.RunContinuationsAsynchronouslyTaskContinuationOptions.RunContinuationsAsynchronously。注意不要混淆其用法。

超时总是要释放CancellationTokenSource

C#异步编程最佳实践,CancellationTokenSource用于超时(使用计时器创建或使用该CancelAfter方法)的对象可能会对计时器队列造成压力(如果未处理)。

❌ 错误实践:此示例不会处理,CancellationTokenSource因此,每次发出请求后,计时器都会在队列中保留10秒钟。

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

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

✅ 良好此示例处理CancellationTokenSource并从队列中正确删除了计时器。

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

始终将CancellationToken传递给采用CancellationToken的API

C#异步编程最佳实践,.NET中的取消合作。调用链中的所有内容都必须明确传递CancellationToken,以使其正常工作。这意味着,如果您想最有效地取消操作,则需要将令牌显式传递到采用令牌的其他API。

❌ 错误实践:这个例子忽略传递CancellationTokenStream.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);
}

✅ 最佳实践:此示例将CancellationToken传入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);
}

取消无法取消的操作

C#异步编程最佳实践,执行异步编程时出现的一种编码模式是取消不可取消的操作。 这通常意味着创建另一个任务,该任务在超时或CancellationToken触发时完成,然后使用Task.WhenAny检测完成或取消的操作。

使用CancellationTokens

❌ 错误实践:此示例用于Task.Delay(-1, token)创建TaskCancellationToken触发时完成的,但如果不触发,则无法处置CancellationTokenRegistration。这可能导致内存泄漏。

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    // There's no way to dispose 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;
}

✅ 最佳实践:本示例处理完成CancellationTokenRegistration时之一Task(s)

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;
    }
}

使用超时timeout

❌ 错误实践:此示例即使操作成功完成也不会取消计时器。这意味着您最终可能会遇到很多计时器,这可能会淹没计时器队列。

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;
}

✅ 最佳实践:C#异步编程最佳实践,如果操作成功完成,本示例将取消计时器。

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;
    }
}

始终在调用Dispose之前在StreamWriter或Stream上调用FlushAsync

C#异步编程最佳实践,写入Stream或时StreamWriter,即使使用异步重载进行写入,也可能会缓冲基础数据。缓冲数据后,通过方法处理Stream或将同步写入/刷新,这将导致线程阻塞并可能导致线程池不足。使用异步方法(例如通过)或在调用之前先调用。StreamWriterDisposeDisposeAsyncawait usingFlushAsyncDispose

💡注意:这仅在底层子系统执行IO时才有问题。

❌ 错误实践:此示例最终通过同步写入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("你好,江湖人士。 https://jhrs.com");
    }
});

✅ 最佳实践:此示例在处置时异步刷新所有缓冲的数据StreamWriter

app.Run(async context =>
{
    // The implicit AsyncDispose call will flush asynchronously
    await using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("你还好吗?江湖人士。 https://jhrs.com");
    }
});

✅ 最佳实践:此示例在处置之前异步刷新所有缓冲的数据StreamWriter

app.Run(async context =>
{
    using (var streamWriter = new StreamWriter(context.Response.Body))
    {
        await streamWriter.WriteAsync("你好,江湖人士,https://jhrs.com 这是C#异步编程最佳实践示例代码。");

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

优先选择 async/await 而不是直接返回Task

C#异步编程最佳实践,使用async/await关键字而不是直接返回Task有好处。

  • 异步和同步异常被规范化为始终是异步的。
  • 该代码更易于修改(using例如,考虑添加)。
  • 异步方法的诊断更容易(调试挂起等)。
  • 抛出的异常将自动包装在返回的内容中,Task而不会给调用者带来实际的异常。

❌ 错误实践:此示例直接将返回Task给调用方。

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

✅ 最佳实践:此示例使用async / await而不是直接返回Task。

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

💡注意:使用异步状态机而不是直接返回时,需要考虑性能Task。直接返回总是更快,Task因为它工作量较少,但最终会改变行为,并可能失去异步状态机的某些好处。

情景Scenarios

C#异步编程最佳实践,上面的内容试图提炼一般指导,但是对于导致这样的代码首先被编写的实际情况(错误代码)并没有做到合理。本节尝试从实际应用程序中提取具体示例,并将其转变为简单的示例,以帮助您将这些问题与现有代码库联系起来。

Timer 回调

❌ 错误实践:Timer回调是返回空值的,我们要执行异步工作。 此示例使用异步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("https://jhrs.com/api/ping");
    }
}

❌ 错误实践:这将尝试阻止Timer回调。 这可能会导致线程池不足,并且是异步中同步 Sync over async一个示例

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("https://jhrs.com/api/ping").GetAwaiter().GetResult();
    }
}

✅ 最佳实践:此示例使用async Task基于方法,并TaskTimer回调中放弃。如果此方法失败,则不会使进程崩溃。相反,它将触发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("https://jhrs.com/api/ping");
    }
}

隐式异步void委托 (Implicit async void delegates)

C#异步编程最佳实践,想象一下一个带有FireAndForget的BackgroundQueue,它需要一个回调。 此方法将在将来的某个时间执行回调。。

❌ 错误实践:这将迫使调用者阻塞回调或使用异步void委托。

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

❌ 错误实践:此调用代码正在隐式创建异步void方法。 编译器今天完全支持。

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

✅ 最佳实践:此BackgroundQueue实现提供同步和异步回调重载。

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

ConcurrentDictionary.GetOrAdd

C#异步编程最佳实践,缓存异步操作的结果非常普遍,而ConcurrentDictionary是执行此操作的良好数据结构。 GetOrAdd是一种便捷的API,用于尝试获取已存在的项目或添加尚未存在的项目。 回调是同步的,因此很想编写使用Task.Result来生成异步过程的值但可能导致线程池不足的代码。

❌ 错误实践:这可能导致线程池不足,因为如果没有缓存人员数据,我们将阻塞请求线程。

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);
   }
}

✅ 最佳实践:该实现不会导致线程池不足,因为我们存储的是任务而不是结果本身。

⚠️ 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);
   }
}

✅ 最佳实践:此实现通过使用异步延迟加载解决了多次执行回调的问题。。

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)
      {
      }
   }
}

构造函数

C#异步编程最佳实践,构造函数是同步的。如果您需要初始化一些可能是异步的逻辑,可以使用几种模式来处理。

这是一个使用客户端API的示例,该客户端API在使用前需要异步连接。

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

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

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

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

✅ 最佳实践:此实现使用静态工厂模式以允许异步构造:

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

C#异步编程最佳实践,该API将指定的操作作为模拟的Windows身份运行。不幸的是,没有异步版本的回调。

❌ 错误实践:此示例尝试异步执行查询,然后在对的调用之外等待它RunImpersonated。因为查询可能正在模拟环境之外执行,所以将抛出此错误。

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

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

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

✅ 最佳实践:此示例等待RunImpersonated的结果(在这种情况下,委托为Func >>)。

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

C#异步编程最佳实践结论

以上就是关于C#异步编程最佳实践的一些总结,C# 中的细节较多,尤其是涉及到异步编程,感兴趣或者英文比较好的朋友可以直接阅读原文,希望这些异步编程实践可以帮助到你。

点击这里阅读原文。

加入电报群

【江湖人士】(jhrs.com) 投稿作者:IT菜鸟,不代表江湖人士立场,如若转载,请注明出处:https://jhrs.com/2020/39105.html

扫码加入电报群,让你获得国外网赚一手信息。

文章标题:C#异步编程最佳实践

(0)
IT菜鸟的头像IT菜鸟普通会员
上一篇 2020-12-13 22:45
下一篇 2020-12-20 13:46

热门推荐

Leave a Reply

Sending

国外老牌便宜域名服务商Namecheap注册com域名大优惠,抢到就赚到,优惠码:NEWCOM698
$5.98/年
直达官网