C#使用异步操作时的注意要点(翻译)

Posted UP技术控

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#使用异步操作时的注意要点(翻译)相关的知识,希望对你有一定的参考价值。

异步操作时应注意的要点

  • 使用异步方法返回值应避免使用void

  • 对于预计算或者简单计算的函数建议使用Task.FromResult代替Task.Run

  • 避免使用Task.Run()方法执行长时间堵塞线程的工作

  • 避免使用Task.Result和Task.Wait()来堵塞线程

  • 建议使用await来代替continueWith任务

  • 创建TaskCompletionSource 时建议使用TaskCreationOptions.RunContinuationsAsynchronously属性

  • 建议使用CancellationTokenSource(s)进行超时管理时总是释放(dispose)

  • 建议将协作式取消对象(CancellationToken)传递给所有使用到的API

  • 建议取消那些不会自动取消的操作(CancellationTokenRegistry,timer)

  • 使用StreamWriter(s)或Stream(s)时在Dispose之前建议先调用FlushAsync

  • 建议使用 async/await而不是直接返回Task

使用场景

  • 使用定时器回调函数

  • 创建回调函数参数时注意避免 async void

  • 使用ConcurrentDictionary.GetOrAdd注意场景

  • 构造函数对于异步的问题

异步操作时需要注意的要点

1.使用异步方法返回值应当避免使用void

在使用异步方法中最好不要使用void当做返回值,无返回值也应使用Task作为返回值,因为使用void作为返回值具有以下缺点

  • 无法得知异步函数的状态机在什么时候执行完毕

  • 如果异步函数中出现异常,则会导致进程崩溃

❌异步函数不应该返回void

static void Main(string[] args)
{
try
{
// 如果Run方法无异常正常执行,那么程序无法得知其状态机什么时候执行完毕
Run();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.Read();
}
static async void Run()
{
// 由于方法返回的为void,所以在调用此方法时无法捕捉异常,使得进程崩溃
throw new Exception("异常了");
await Task.Run(() => { });

}

☑️应该将异步函数返回Task

static async Task Main(string[] args)
{
try
{
// 因为在此进行await,所以主程序知道什么时候状态机执行完成
await RunAsync();
Console.Read();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
static async Task RunAsync()
{
// 因为此异步方法返回的为Task,所以此异常可以被捕捉
throw new Exception("异常了");
await Task.Run(() => { });

}

注:事件是一个例外,异步事件也是返回void

2.对于预计算或者简单计算的函数建议使用Task.FromResult代替Task.Run

对于一些预先知道的结果或者只是一个简单的计算函数,使用Task,FromResult要比Task.Run性能要好,因为Task.FromResult只是创建了一个包装已计算任务的任务,而Task.Run会将一个工作项在线程池进行排队,计算,返回.并且使用Task.FromResult在具有SynchronizationContext 程序中(例如WinForm)调用Result或wait()并不会死锁(虽然并不建议这么干)

❌对于预计算或普通计算的函数不应该这么写

public async Task<int> RunAsync()
{
return await Task.Run(()=>1+1);
}

☑️而应该使用Task.FromResult代替

public async Task<int> RunAsync()
{
return await Task.FromResult(1 + 1);
}

还有另外一种代替方法,那就是使用ValueTask类型,ValueTask是一个可被等待异步结构,所以并不会在堆中分配内存和任务分配,从而性能更优化.

☑️使用ValueTask 代替

static async Task Main(string[] args)
{
await AddAsync(1, 1);
}
static ValueTask<int> AddAsync(int a, int b)
{
// 返回一个可被等待的ValueTask类型
return new ValueTask<int>(a + b);
}

注: ValueTask结构是C#7.0加入的,存在于Sysntem,Threading.Task.Extensions包中

ValueTask 相关文章

ValueTask 相关文章

3.避免使用Task.Run()方法执行长时间堵塞线程的工作

长时间运行的工作是指在应用程序生命周期执行后台工作的线程,如:执行processing queue items,执行sleeping,执行waiting或者处理某些数据,此类线程不建议使用Task.Run方法执行,因为Task.Run方法是将任务在线程池内进行排队执行,如果线程池线程进行长时间堵塞,会导致线程池增长,进而浪费性能,所以如果想要运行长时间的工作建议直接创建一个新线程进行工作

❌下面这个例子就利用了线程池执行长时间的阻塞工作

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

以上是关于C#使用异步操作时的注意要点(翻译)的主要内容,如果未能解决你的问题,请参考以下文章

C# :异步编程的注意点

[翻译]用一个用户场景来掌握它们

c#代码片段快速构建代码

EF Core 异步编程注意要点

EF Core 异步编程注意要点

1.node.js在遇到“循环+异步”时的注意事项