其实正确的说,应该所有地方都慎用async void
,应当尽可能的只在用作事件Event
方法时才采用async void
,其它地方应当将async Task
作为返回值。
项目中使用的是EF6,在测试过程中,产生了一个System.NotSupportedException
异常,其内容如下
A second operation started on this context before a previous asynchronous operation completed.
Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context.
Any instance members are not guaranteed to be thread safe.
提示内容已经很明确的告知我们,要通过await
来顺序执行所有涉及EF的Task
,在stackoverflow
上也有此对应的问题。当EF中有多个涉及数据库的异步操作时,要顺序对每个请求进行await
,下面分别将stackoverflow
上错误和正确的写法罗列下
#region 错误会产生异常的写法
var banner = context.Banners.ToListAsync()
var newsGroup = context.NewsGroups.ToListAsync()
await Task.WhenAll(banner, newsGroup);
#endregion
#region 正确的写法
var banner = await context.Banners.ToListAsync();
var newsGroup = await context.NewsGroups.ToListAsync();
#endregion
检查了项目中相关部分的代码,发现所有异步操作都是采用了正确await
的写法,并没有如stackoverflow
上那样描述类似的问题写法,那问题究竟在哪呢?反复检查代码,突然发现该部分代码调用的一个async void
方法代码,该代码作用是向钉钉发送消息推送,其间需要从数据库中读取当前用户对应钉钉中的id
,其大致代码如下
public async void SendNotice(string uid,string message)
{
//await方式从db中,根据uid读取其在钉钉中的uid
//如果读取到,那么就调用钉钉的sdk进行消息推送
}
之所以这个通知代码会写成async void
是有历史原因的,因为系统中存在大量的同步代码,以及少量的异步代码(吐槽下都9102年了,居然还有那么多人不知道Task
),为了同时兼容同步以及异步代码,并且这个通知本质上也不会影响业务,所以async void
代码由此产生,而在之前的使用中之所以没产生System.NotSupportedException
异常的原因也很简单,之前的代码都是在所有业务数据处理完毕并保存Save
之后,才会调用通知代码,而异常部分的代码是业务处理过程中,就进行了通知代码调用(理论上这是不应该的,毕竟业务数据有可能产生失败,如果失败的话,就不应该进行消息推送,只是为了偷懒,加上这是个定制项目,并不需要严格意义上的逻辑正确),而在最后调用Save
时,System.NotSupportedException
也就闪亮登场。
既然知道了问题产生的原因,解决起来也就简单了,新增一个返回Task
的通知方法,将旧代码全部复制到新方法里去,并修改旧方法调用新方法以便兼容系统中现有代码,
public async void SendNotice(string uid,string message)
{
await SendNoticeAsync(uid,message);
}
public async Task SendNoticeAsync(string uid,string message)
{
//await方式从db中,根据uid读取其在钉钉中的uid
//如果读取到,那么就调用钉钉的sdk进行消息推送
}
在将异常部分代码改为调用await SendNoticeAsync
后,异常顺利解决。