一、问题简述:
跟主线程并列的异步线程(非阻塞线程)上。
通过
await TaskRun(()=>{}};
开启新的线程并且阻塞等待的话,线程就会丢失或者崩溃。这时候用try{}catch(){}的方式也捕获不到异常。
只有一个出现在System.Web.dll内部的异常。我尝试在Vs 2015上面加载一些System.Web.pdb文件,但是没有找到合适的。
二、伪代码重新描述:
假设有同步Action和异步两个Action
public ActionResult Index()
{
DoworkAsync();
return SendHtml("ok");
}
public async Task<string> Get()
{
DoworkAsync();
return "OK";
}
然后还有两个具体工作的方法,要求按照顺序执行里面,DoworkAsync是一个同步改成异步的await Task.Run
protected async Task DoworkAsync()
{
int UserId
//封装到另一方法里面也没用
//await DoworkAsync2();
await Task.Run(() =>
{
//★重点在此处
System.Threading.Thread.Sleep(6100);
//真正代码
UserId=55;
});
//阻塞之后继续执行的代码
var user = await GetUserAsync(UserId);
}
public async Task<UserInfo> GetUserAsync(int id)
{
var Reuslt = await Db.GetUserAsync(id);
}
protected async Task DoworkAsync2()
{
await Task.Run(() =>
{
System.Threading.Thread.Sleep(6100);
//真正代码
});
}
三、调试结果
1、线程睡眠模拟
在【★重点】那里,不管是Thread.Sleep()还Task.Delay()
都会线程崩溃。
2、实际WebRequest测试
在【★重点】那里,不管是同的方法WebRequest还是await 异步的方法GetResponseAsync
都会线程崩溃。
3、在【★重点】那里,Thread.Sleep()休眠时间为70的时候,第一次可以走到
//阻塞之后继续执行的代码
但是第二次就不行。
因为第一次是时间快,其实是已经崩溃了,【输出】窗口有:
引发的异常:“System.NullReferenceException”(位于 System.Web.dll 中)
四、结论
请问为什么会崩溃,或者求一个替换 await TaskRun(()=>{}};的方案。
因为在VS里面,假如方法名的衣面标识了async,就会提示方法体里面要有await。
补充一点:
这套代码在控制台程序(Console)上运行一点问题都没有,但是放在asp.net MVC 上才出现的问题。
测试机是Windows 10 64位,Vs 2015 /VS 2019 IIS Express
然后换成我同事的电脑上仍然会如此。
看描述 像 async await 死锁问题。
如果action上 async 不准备加的话,底层的 async await 方法 还是把ConfigureAwait(false)加上。
Bingo!
你说得对!谢谢!
看到你说的这个技术点,我立刻去翻了翻书(《C#编程经典实例》),感觉书上得说还不算特别清楚,
又百度到了另一篇文章《在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁》(https://blog.walterlv.com/post/using-configure-await-to-avoid-deadlocks.html),又通过这篇找到了前一篇。前一篇有一句提示很重要:【整个方法调用链都需要使用 .ConfigureAwait(false)】
经过全面测试,问题算是找到了解决的办法,就是那句:【整个方法调用链都需要使用 .ConfigureAwait(false)】
然后我现在有个新问题,能不能在最开始第一次 await之前直接声明,这边的代码(同步程序块和异步程序块)全部都在线程池上运行?
@柳城之城:
能不能在最开始第一次 await之前直接声明。 没明白这句意思?
说明下, async await 发生堵塞的地方 是在对异步方法调用 .Result 或者 .wait()的时候产生。你贴的代码里还没有发现。按道理你的代码 编译器会有警告
其实你的代码 正确的写法就是@ sweetjian 写的那样。
然后controller层可以写成这样,就不需要到处.ConfigureAwait(false)
public async Task<ActionResult> Index()
{
//DoworkAsync 不想堵塞
// var t=**,no warning
var t= DoworkAsync().ContinueWith(x =>
{
//logging i done the work
});
return await Task.FromResult(SendHtml("ok"));
}
@gt1987: 我的意思是辅助线程直接声明成后线线程。Task.Run()这种默认是使用了UI线程。
我自己试过了,是可行的。代码这是样的。
var task = Task.Factory.StartNew(() =>
{
DoworkAsync();
}, TaskCreationOptions.HideScheduler);
vs里面的调试代码已经验证结束了,接下来准备在Win2008 win 2012上分别验证一遍。如果没有问题,准备发到github上。
Task.Run 里面的方法里面不要使用任何从DI获取的scope作用域的对象。 请求结束后这些对象就被释放了。
先前测试的时候没有仔细验证过作用域的问题,因为Task.Run 里面哪怕没有任何引用,只要有一句线程休眠也会崩溃。
但是我们输出 Thread.CurrentThread.ManagedThreadId的时候发现,
Action Index()和Task DoworkAsync的线程Id 是一样的。 (假设是5,多次测试发现,这个数字不固定)
但是 DoworkAsync里面的Task.Run里面的线程id就是不一样了。(假设是6)
所以怀疑是运行到await Task.Run这里,系统会从线程5的上下文(Context)切到线程6的上下文Context。
但是一旦await Task.Run里面的代码执行时间稍长,GC会提前优化代码丢弃掉线程5的Context,结果等到Run的代码执行完毕以后,从线程6的Context恢复到线程5的Context,就失败了。
@柳城之城: 建议吧代码整理一下,最小可复现错误的代码,还有代码贴完整
@czd890: 这就是最小可复现代码了。我自己调试的代码,也就是比这个多了一些输出。
当然我的代码里面没有 GetUserInfo那些。
DoworkAsync简化成这样就可以了。
protected async Task DoworkAsync()
{
await Task.Run(() =>
{
//★重点在此处
System.Threading.Thread.Sleep(6100);
//真正代码
});
//阻塞之后继续执行的代码
}
另外SendHtml是这样:
public ActionResult SendHtml(string html)
{
ContentResult result = new ContentResult();
result.ContentType = "";
result.ContentEncoding = UTF8Encoding.UTF8;
result.Content = html;
return result;
}
@柳城之城: 把你的project丢到github上吧
@czd890: 谢谢指教。问题已经解决了。只是不好意思,园豆我不太会发,只能发给一个人。
你要的git我也放完了。
https://github.com/xpnew/MvcAsyncDemo
public ActionResult Index()
{
DoworkAsync();
return SendHtml("ok");
}
public async Task<string> Get()
{
DoworkAsync();
return "OK";
}
你上面这两段代码,活还没干完,就直接return了。也就意味着DoworkAsync返回的Task你不管了,如果Task有错误,垃圾回收的时候自然在后台报错了。
问题描述感觉有点混乱,改了一下你的代码,供参考
public async Task<string> Get()
{
var user = await DoworkAsync();
return "OK";
}
protected async Task<UserInfo> DoworkAsync()
{
int userId = await Task.Run(() =>
{
//★重点在此处
System.Threading.Thread.Sleep(6100);
//真正代码
return 55;
});
//阻塞之后继续执行的代码 <-- 不是阻塞,用挂起可能更合适
return await GetUserAsync(userId);
}
public async Task<UserInfo> GetUserAsync(int id)
{
return await Db.GetUserAsync(id);
}
【你上面这两段代码,活还没干完,就直接return了。】
这么设计是故意的、刻意的。
主线程负责一部分工作,这部分工作是一些基础的业务逻辑,但是要求尽早结束,返回结果给UI。
跟主线程并列的线程(或者叫做辅助线程),负责一些数据处理的工作,它不能阻塞主线程。
改成了阻塞的话,比如你这样:
var user = await DoworkAsync();
先前已经测试验证过,确实一点问题都没有,但是这跟设计初衷是相悖的。
@柳城之城: 没明白你的设计初衷是什么,请描述清楚。
await 并不会阻塞主线程 !! 异步怎么会阻塞呢
@柳城之城: await 可以这么理解:
主线程调用 await xxx 的时候
@sweetjian: await就是阻塞呀。await 就是等待异步结束才会继续执行。
@柳城之城: 建议吧代码整理一下,最小可复现错误的代码,还有代码贴完整。
阻塞在线程的角度来说更多的是指调用方被阻塞了,调用方线程不能做其他事情只能等在这里。
@柳城之城:
DoworkAsync().Wait(); //这个叫阻塞
await DoworkAsync(); //这个叫异步等待
@sweetjian: 我说的“阻塞”,是说“阻塞”了主线程。无关具体哪一种实现。
因为这个地方的需求就是要求不能“阻塞”主线程。
然后,这里咬这个字眼没用。用你的话来说,问题是主线程不等待异步的结果继续执行,这个前提下,异步方法里面的异步等待结束之后,就会丢失上下文。
@柳城之城:
线程就会丢失或者崩溃。这时候用try{}catch(){}的方式也捕获不到异常
线程丢失或者崩溃,肯定是线程内部的代码出现了问题
我不知道你的try{}catch(){}是指的什么,如果是下面这样
try{
DoworkAsync();
}catch(){}
肯定是catch不到啊,异常会存储到DoworkAsync返回的Task内部,系统回收Task的时候,自然报未处理异常了。
Console环境下GC的时候才会回收,没有GC的时候看起来是一切正常的。
正确的异常处理方法
try{
await DoworkAsync();
}catch(){}
或者
DoworkAsync().ContinueWith(t=>{
if(t.IsFault) Log(t.Exception)
});
@柳城之城:
public ActionResult Index()
{
DoworkAsync();
return SendHtml("ok");
}
public async Task<string> Get()
{
DoworkAsync();
return "OK";
}
其他的不说,单看你上面这段代码,是有严重问题的,DoworkAsync(); 或者说任何异步方法返回的Task任何时候都不应该放任不管。
抱歉有一点我说错了,Task的未处理异常不是GC的时候抛出的,而是任务结束之后抛出的。.NET Framework 4.5 之后的版本,默认已经不抛出异常了。参看:
https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception?view=netcore-3.1