##前言

我在写代码的时候(.net core)有时候会碰到void方法里,调用async方法并且Wait,而且我还看到别人这么写了。而且我这么写的时候,编译器没有提示任何警告。但是看了dudu的文章:一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相 了解了,这样写是有问题的。但是为什么会有问题呢?我又阅读了dudu文章里提到的一篇博文:.NET Threadpool starvation, and how queuing makes it worse 加上自己亲手实验,写下自己的理解,算是对dudu博文的一个补充和丰富吧。

##同步方法里调用异步方法

同步方法里调用异步方法,一种是wait() 一种是不wait()

void fun()

{

funAsync.Wait();

funAsync();

}

这两种场景都没有编译错误。

首先我们来看一下,在 void里调用 async 方法,并且要等待async的结果出来之后,才能进行后续的操作。

using System;

using System.Threading;

using System.Threading.Tasks;

namespace ConsoleTool2

{

class Program

{

static void Main(string[] args)

{

Producer();

}

static void Producer()

{

var result = Process().Result;

//或者

//Process().Wait();

}

static async Task Process()

{

await Task.Run(() =>

{

Thread.Sleep(1000);

});

Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());

return true;

}

}

}

咱们看这个Producer,这是一个void方法,里面调用了异步方法Process(),其中Process()是一个执行1秒的异步方法,调用的方式是Process().Result 或者Process().Wait()。咱们来运行一遍。

没有任何问题。看起来,这样写完全没有问题啊,不报错,运行也是正常的。

接下来,我们修改一下代码,让代码更加接近生产环境的状态。

using System;

using System.Threading;

using System.Threading.Tasks;

namespace ConsoleTool2

{

class Program

{

static void Main(string[] args)

{

while (true)

{

Task.Run(Producer);

Thread.Sleep(200);

}

}

static void Producer()

{

var result = Process().Result;

}

static async Task Process()

{

await Task.Run(() =>

{

Thread.Sleep(1000);

});

Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());

return true;

}

}

}

我们在Main函数里加了for循环,并且1秒钟执行5次Producer(),使用Task.Run(),1秒钟有5个Task产生。相当于生产环境的qps=5。

接下来我们再执行下,看看结果:

在第一秒里只执行了两次Task,就卡住了。我们再看下进程信息:

没有CPU消耗,但是线程数一直增加,直到突破一台电脑的最大线程数,导致服务器宕机。

这明显出现问题了,线程肯定发生了死锁,而且还在不断产生新的线程。

至于为什么只执行了两次Task,我们可以猜测是因为程序中初始的TreadPool 中只有两个线程,所以执行了两次Task,然后就发生了死锁。

现在我们定义一个Produce2() 这是一个正常的方法,异步函数调用异步函数。

static async Task Producer2()

{

await Process();

}

我们再Main函数的循环里,执行Producer2() ,执行信息如下:

仔细观察这个图,我们发现第一秒执行了一个Task,第二秒执行了三个Task,从第三秒开始,就稳定执行了4-5次Task,这里的时间统计不是很精确,但是可以肯定从某个时间开始,程序达到了预期效果,TreadPool中的线程每秒中都能稳定的完成任务。而且我们还能观察到,在最开始,程序是反应很慢的,那个时候线程不够用,同时应该在申请新的线程,直到后来线程足够处理这样的情况了。咱们再看看这个时候的进程信息:

线程数一直稳定在25个,也就是说25个线程就能满足这个程序的运行了。

到此我们可以证明,在同步方法里调用异步方法确实是不安全的,尤其在并发量很高的情况下。

探究原因

我们再深层次讨论下为什么同步方法里调用异步方法会卡死,而异步方法调用异步方法则很安全呢?

咱们回到一开始的代码里,我们加上一个初始化线程数量的代码,看看这样是否还是会出现卡死的状况。

由于前面的分析我们知道,这个程序在一秒中并行执行5个Task,每个Task里面也就是Producer 都会执行一个Processer 异步方法,所以粗略估计需要10个线程。于是我们就初始化线程数为10个。

using System;

using System.Threading;

using System.Threading.Tasks;

namespace ConsoleTool2

{

class Program

{

static void Main(string[] args)

{

ThreadPool.SetMinThreads(10, 10);

while (true)

{

Task.Run(Producer2);

Thread.Sleep(200);

}

}

static void Producer()

{

var result = Process().Result;

}

static async Task Producer2()

{

await Process();

}

static async Task Process()

{

await Task.Run(() =>

{

Thread.Sleep(1000);

});

Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());

return true;

}

}

}

运行一下发现,是没问题的。说明一开始设置多的线程是有用的,经过实验发现,只要初始线程小于10个,都会出现死锁。而.net core的默认初始线程是肯定小于10个的。

那么当初始线程小于10个的时候,发生什么了?发生了大家都听说过的名词,线程饥饿。就是线程不够用了,这个时候ThreadPool生产新的线程满足需求。

然后我们再关注下,同步方法里调用异步方法并且.Wait()的情况下会发生什么。

void Producer()

{

Process().Wait()

}

首先有一个线程A ,开始执行Producer , 它执行到了Process 的时候,新产生了一个的线程 B 去执行这个Task。这个时候 A 会挂起,一直等 B 结束,B被释放,然后A继续执行剩下的过程。这样执行一次Producer 会用到两个线程,并且A 一直挂起,一直不工作,一直在等B。这个时候线程A 就会阻塞。

Task Producer()

{

await Process();

}

这个和上面的区别就是,同时线程A,它执行到Producer的时候,产生了一个新的线程B执行 Process。但是 A 并没有等B,而是被ThreadPool拿来做别的事情,等B结束之后,ThreadPool 再拿一个线程出来执行剩下的部分。所以这个过程是没有线程阻塞的。

再结合线程饥饿的情况,也就是ThreadPool 中发生了线程阻塞+线程饥饿,会发生什么呢?

假设一开始只有8个线程,第一秒中会并行执行5个Task Producer, 5个线程被拿来执行这5个Task,然后这个5个线程(A)都在阻塞,并且ThreadPool 被要求再拿5个线程(B)去执行Process,但是线程池只剩下3个线程,所以ThreadPool 需要再产生2个线程来满足需求。但是ThreadPool 1秒钟最多生产2个线程,等这2个线程被生产出来以后,又过去了1秒,这个时候无情又进来5个Task,又需要10个线程了。别忘了执行第一波Task的一些线程应该释放了,释放多少个呢?应该是3个Task占有的线程,因为有2个在等TreadPool生产新线程嘛。所以释放了6个线程,5个Task,6个线程,计算一下,就可以知道,只有一个Task可以被完全执行,其他4个都因为没有新的线程执行Process而阻塞。

于是ThreadPool 又要去产生4个新的线程去满足4个被阻塞的Task,花了2秒时间,终于生产完了。但是糟糕又来了10个Task,需要20个线程,而之前释放的线程已经不足以让任何一个Task去执行Process了,因为这些不足的线程都被分配到了Producer上,没有线程再可以去执行Process了(经过上面的分析一个Task需要2个线程A,B,并且A阻塞,直到B执行Process完成)。

所以随着时间的流逝,要执行的Task越来越多却没有一个能执行结束,而线程也在不断产生,就产生了我们上面所说的情况。

##我们该怎么办?

经过上面的分析我们知道,在线程饥饿的情况下,使用同步方法调用异步方法并且wait结果,是会出问题的,那么我们应该怎么办呢?

首先当然是应该避免这种有风险的做法。

其次,还有一种方法。经过实验,我发现,使用专有线程

Task.Run(Producer);

改成

Task.Factory.StartNew(

Producer,

TaskCreationOptions.LongRunning

);

就是TaskCreationOptions.LongRunning 选项,就是开辟一个专用线程,而不是在ThreadPool中拿线程,这样是不会发生死锁的。

因为ThreadPool 不管理专用线程,每一个Task进来,都会有专门的线程执行,而Process 则是由ThreadPool 中的线程执行,这样TheadPool中的线程其实是不存在阻塞的,因此也不存在死锁。

##结语

关于ThreadPool 中的线程调用算法,其实很简单,每个线程都有一个自己的工作队列local queue,此外线程池中还有一个global queue全局工作队列,首先一个线程被创建出来后,先看看自己的工作队列有没有被分配task,如果没有的话,就去global queue找task,如果还没有的话,就去别的线程的工作队列找Task。

第二种情况:在同步方法里调用异步方法,不wait()

如果这个异步方法进入的是global Task 则在线程饥饿的情况下,也会发生死锁的情况。至于为什么,可以看那篇博文里的解释,因为global Task的优先级很高,所有新产生的线程都去执行global Task,而global task又需要一个线程去执行local task,所以产生了死锁。

关于找一找教程网

本站文章仅代表作者观点,不代表本站立场,所有文章非营利性免费分享。

本站提供了软件编程、网站开发技术、服务器运维、人工智能等等IT技术文章,希望广大程序员努力学习,让我们用科技改变世界。

[关于同步方法里面调用异步方法的探究]http://www.zyiz.net/tech/detail-131895.html

java同步调用异步方法_关于同步方法里面调用异步方法的探究相关推荐

  1. java 同步的方法_关于Java中的同步方法

    我有一个关于Java中方法同步的问题. 考虑一个具有3个同步方法的类. class MyClass{ public synchronized void methodA(){ ... } public ...

  2. java同步的意思_“同步”是什么意思?

    我对synchronized关键字的用法和重要性有一些疑问. synchronized关键字的意义是什么? 方法应何时synchronized ? 从程序上和逻辑上是什么意思? #1楼 据我了解,同步 ...

  3. java 同步块 抛出异常_不把 wait 放在同步块中,为啥这种情况不会抛出 IllegalMonitorStateException?...

    这是一个来自 Java 编程思想的例子,它只是想表达 sleep 的线程可中断,但同步 IO 等待资源,或同步获得锁失败的线程,是不可同步的. //: concurrency/Interrupting ...

  4. java同步异步区别_同步请求和异步请求的区别

    同步请求和异步请求的区别 先解释一下同步和异步的概念 同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式. 异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的 ...

  5. java 同步和异步_知道什么叫同步和异步吗?

    评论 # re: 知道什么叫同步和异步吗? 2006-11-06 15:34 chicken 你翻译的很垃圾阿 看了英文才懂...  回复  更多评论 # re: 知道什么叫同步和异步吗? 2006- ...

  6. java同步锁售票_线程同步锁之火车站售票案例

    前言: 谈到多线程,就不得不说线程同步,那么什么是线程同步? 线程同步 即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作 ...

  7. java支付宝网页授权_手机浏览器怎么调用支付宝进行用户授权呢?

    题主,我咨询了技术客服,得到了满意的解决方案. 把授权链接encode, 拼接到alipays://platformapi/startapp?appId=20000067&url= 后面即可从 ...

  8. java同步异步调用_详解java 三种调用机制(同步、回调、异步)

    1:同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,jsPwwCe它是一种单向调用 2:回调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口: 3:异步调用:一种类似消 ...

  9. java 线程同步的方法_Java多线程同步方法

    Java多线程同步方法 package com.wkcto.intrinsiclock; /** * synchronized同步实例方法 * 把整个方法体作为同步代码块 * 默认的锁对象是this对 ...

最新文章

  1. 自动驾驶软件工程之目标检测以及传感器融合
  2. php 设置中文 cookie, js获取
  3. UnsupportedClassVersionError 错误解决办法
  4. java static是单例_JAVA基础-static关键字及单例设计模式
  5. 20. Prefer pass-by-reference-to-const to pass-by-value
  6. 速度传感器330104-00-06-10-02-00
  7. 提升生产力,7 款好用的原型图工具推荐给你
  8. 小米路由器3g改无线打印机服务器,小米路由器3G怎么设置?
  9. 运用Excel实现描述性统计分析
  10. 基于fabric的行业联盟链技术研究/司帅帅
  11. android 限制输入 表情以及颜文字及特殊字符
  12. 计算机网络-路由器和交换机的区别
  13. 体脂手环、体脂秤等产品的体脂测量原理及技术方案分析
  14. STM32F103C8T6 0.42寸的OLED屏幕IIC例程
  15. DLM分布式锁的实现机制
  16. 用 Python 玩视频剪辑 让生活简易化
  17. BCD码指令 AAA DAA AAS DAS AAM AAD
  18. RETHINKING SOFT LABELS FOR KNOWLEDGE DISTIL- LATION: A BIAS-VARIANCE TRADEOFF PERSPECTIVE
  19. Android 扫码登录
  20. No module named 'torchvision.ops'的解决办法

热门文章

  1. OpenGL学习脚印: 二维纹理映射(2D textures)
  2. 网络前端第六次培训笔记(js)
  3. MobaXterm将远程服务器窗口界面映射到本地
  4. CAPWAP隧道建立交互过程
  5. RTKLIB(一)——GNSS测量中的数据格式
  6. Tablayout-布局标签
  7. IDE中显示 *.properties 为中文
  8. 哈工大计算机硕士就业年薪,毕业生薪酬:南大、哈工大(深圳)平均年薪超17万...
  9. 动态规划经典例题解析
  10. 最大连续子序列和:动态规划经典题目