首页 新闻 会员 周边

c# MVC里的await Task.Run线程丢失/崩溃?

0
悬赏园豆:5 [已解决问题] 解决于 2020-09-07 17:33

一、问题简述:

跟主线程并列的异步线程(非阻塞线程)上。

通过

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
然后换成我同事的电脑上仍然会如此。

柳城之城的主页 柳城之城 | 初学一级 | 园豆:39
提问于:2020-09-02 17:52
< >
分享
最佳答案
1

看描述 像 async await 死锁问题。
如果action上 async 不准备加的话,底层的 async await 方法 还是把ConfigureAwait(false)加上。

收获园豆:5
gt1987 | 小虾三级 |园豆:1150 | 2020-09-07 12:34

Bingo!
你说得对!谢谢!
看到你说的这个技术点,我立刻去翻了翻书(《C#编程经典实例》),感觉书上得说还不算特别清楚,
又百度到了另一篇文章《在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁》(https://blog.walterlv.com/post/using-configure-await-to-avoid-deadlocks.html),又通过这篇找到了前一篇。前一篇有一句提示很重要:【整个方法调用链都需要使用 .ConfigureAwait(false)】

经过全面测试,问题算是找到了解决的办法,就是那句:【整个方法调用链都需要使用 .ConfigureAwait(false)】
然后我现在有个新问题,能不能在最开始第一次 await之前直接声明,这边的代码(同步程序块和异步程序块)全部都在线程池上运行?

柳城之城 | 园豆:39 (初学一级) | 2020-09-07 15:20

@柳城之城:
能不能在最开始第一次 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 | 园豆:1150 (小虾三级) | 2020-09-07 16:17

@gt1987: 我的意思是辅助线程直接声明成后线线程。Task.Run()这种默认是使用了UI线程。
我自己试过了,是可行的。代码这是样的。
var task = Task.Factory.StartNew(() =>
{
DoworkAsync();
}, TaskCreationOptions.HideScheduler);
vs里面的调试代码已经验证结束了,接下来准备在Win2008 win 2012上分别验证一遍。如果没有问题,准备发到github上。

柳城之城 | 园豆:39 (初学一级) | 2020-09-07 17:38
其他回答(2)
1

Task.Run 里面的方法里面不要使用任何从DI获取的scope作用域的对象。 请求结束后这些对象就被释放了。

czd890 | 园豆:14412 (专家六级) | 2020-09-02 22:30

先前测试的时候没有仔细验证过作用域的问题,因为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,就失败了。

支持(0) 反对(0) 柳城之城 | 园豆:39 (初学一级) | 2020-09-04 12:07

@柳城之城: 建议吧代码整理一下,最小可复现错误的代码,还有代码贴完整

支持(0) 反对(0) czd890 | 园豆:14412 (专家六级) | 2020-09-04 12:19

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

支持(0) 反对(0) 柳城之城 | 园豆:39 (初学一级) | 2020-09-07 09:42

@柳城之城: 把你的project丢到github上吧

支持(0) 反对(0) czd890 | 园豆:14412 (专家六级) | 2020-09-07 09:59

@czd890: 谢谢指教。问题已经解决了。只是不好意思,园豆我不太会发,只能发给一个人。

你要的git我也放完了。
https://github.com/xpnew/MvcAsyncDemo

支持(0) 反对(0) 柳城之城 | 园豆:39 (初学一级) | 2020-09-07 20:36
0
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);
        }
sweetjian | 园豆:276 (菜鸟二级) | 2020-09-02 23:36

【你上面这两段代码,活还没干完,就直接return了。】

这么设计是故意的、刻意的。

主线程负责一部分工作,这部分工作是一些基础的业务逻辑,但是要求尽早结束,返回结果给UI。

跟主线程并列的线程(或者叫做辅助线程),负责一些数据处理的工作,它不能阻塞主线程。
改成了阻塞的话,比如你这样:
var user = await DoworkAsync();
先前已经测试验证过,确实一点问题都没有,但是这跟设计初衷是相悖的。

支持(0) 反对(0) 柳城之城 | 园豆:39 (初学一级) | 2020-09-04 09:36

@柳城之城: 没明白你的设计初衷是什么,请描述清楚。
await 并不会阻塞主线程 !! 异步怎么会阻塞呢

支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-04 09:57

@柳城之城: await 可以这么理解:
主线程调用 await xxx 的时候

  1. 主线程: xxx 剩下的事情交给你了,我先走了
  2. xxx: 收到。主线程返回
  3. xxx 处理后台任务,处理完毕后 post 回主线程,或者xxx自己继续后面的任务
支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-04 10:00

@sweetjian: await就是阻塞呀。await 就是等待异步结束才会继续执行。

支持(0) 反对(0) 柳城之城 | 园豆:39 (初学一级) | 2020-09-04 14:16

@柳城之城: 建议吧代码整理一下,最小可复现错误的代码,还有代码贴完整。

阻塞在线程的角度来说更多的是指调用方被阻塞了,调用方线程不能做其他事情只能等在这里。

支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-04 14:27

@柳城之城:

DoworkAsync().Wait(); //这个叫阻塞
await DoworkAsync(); //这个叫异步等待

支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-04 14:29

@sweetjian: 我说的“阻塞”,是说“阻塞”了主线程。无关具体哪一种实现。
因为这个地方的需求就是要求不能“阻塞”主线程。
然后,这里咬这个字眼没用。用你的话来说,问题是主线程不等待异步的结果继续执行,这个前提下,异步方法里面的异步等待结束之后,就会丢失上下文。

支持(0) 反对(0) 柳城之城 | 园豆:39 (初学一级) | 2020-09-07 10:04

@柳城之城:

线程就会丢失或者崩溃。这时候用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)
});

支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-07 10:53

@柳城之城:

public ActionResult Index()
{
DoworkAsync();
return SendHtml("ok");
}
public async Task<string> Get()
{
DoworkAsync();
return "OK";
}

其他的不说,单看你上面这段代码,是有严重问题的,DoworkAsync(); 或者说任何异步方法返回的Task任何时候都不应该放任不管。

支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-07 11:01

抱歉有一点我说错了,Task的未处理异常不是GC的时候抛出的,而是任务结束之后抛出的。.NET Framework 4.5 之后的版本,默认已经不抛出异常了。参看:
https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception?view=netcore-3.1

支持(0) 反对(0) sweetjian | 园豆:276 (菜鸟二级) | 2020-09-07 11:38
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册