首页 新闻 会员 周边

在同步方法中调用异步方法时如何避免死锁问题

1
悬赏园豆:200 [已解决问题] 解决于 2016-12-11 13:09

这是在将 memcached 客户端 EnyimMemcached 迁移至 .Net Core 遇到的问题。

在 MemcachedClient 的构造函数中创建 socket 连接池时,有一个将主机名解析为 IP 地址的操作,之前调用的是同步的 System.Net.Dns.GetHostEntry() 方法,但在 .NET Core 中只有异步的 System.Net.Dns.GetHostAddressesAsync() 方法,所以被迫改为这样调用:

var addresses = System.Net.Dns.GetHostAddressesAsync(host).Result;

结果只要使用 EnyimMemcached 的 ASP.NET Core 站点在 Linux 上一运行,多个请求进来,就发生死锁(deadlock)。从浏览器端看,请求发出后,就一直处于等待状态。
不管改为:

var addresses = GetHostAddressesAsync(host).ConfigureAwait(false).GetAwaiter().GetResult();

还是改为:

var addresses = await Task.Run(async () =>
{
    return await System.Net.Dns.GetHostAddressesAsync(host);
});

都会发生死锁。

你也许会说,干脆将上层调用方法都改成异步的,但是最上层是 MemcachedClient 的构造函数,没法改为异步的。如果真要将创建 socket 连接池的操作改为异步的,需要对 EnyimMemcached 的代码进行大动作。
目前还是打算直接在同步方法中调用异步方法,所以请教一下大家看有没有什么法子避开死锁?

dudu的主页 dudu | 高人七级 | 园豆:30994
提问于:2016-09-20 16:10
< >
分享
最佳答案
1

2016年12月11日更新:


更新2:详见 你的眼睛背叛你的心:解决 .NET Core 中 GetHostAddressesAsync 引起的 EnyimMemcached 死锁问题

更新1:在添加补充问题后突然发现Dns.GetHostAddressesAsync()没有使用async关键字。


最终发现这个问题竟然是因为在MVC视图中调用了异步方法(弄错了,不是这个原因):

@inject CNBlogs.AboutUs.Application.ITabNavService tabNavService
<div id="side_left">
    <ul id="nav_block">
        @foreach (var nav in (await tabNavService.GetAll()))
        {
            <p>@Html.TabLink(nav.Title, nav.Url, nav.ActionName)</p>
        }
    </ul>
</div>

改为在控制器Action中调用就没这个问题了。
在视图中调用异步方法是ASP.NET Core MVC的一个新特性,没想到有这个坑。


dudu | 高人七级 |园豆:30994 | 2016-09-21 08:06

汗,搞错!不是这个原因,请求量上去后,死锁问题又出现了。早上只是请求量少,发生死锁的概率降低了。

dudu | 园豆:30994 (高人七级) | 2016-09-21 11:42

同步方法中调用异步方法的参考资料:How would I run an async Task method synchronously?

dudu | 园豆:30994 (高人七级) | 2016-09-21 21:00
其他回答(9)
0

如果在异步completed事件中实例化MemcachedClient类可行吗

收获园豆:30
nil | 园豆:879 (小虾三级) | 2016-09-20 16:26

MemcachedClient的实例创建是通过依赖注入

支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-20 16:46
0
        private async void Init()
        {
            var addresses = await Task.Run(async () =>
            {
                return await System.Net.Dns.GetHostAddressesAsync(host);
            });
        }

构造函数李调用这个Init();可以试试,

收获园豆:30
oiliu | 园豆:234 (菜鸟二级) | 2016-09-20 16:47
 async 
支持(0) 反对(0) oiliu | 园豆:234 (菜鸟二级) | 2016-09-20 16:48

这样调用,如果Init()方法执行中出错,调用方就不知道

支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-20 17:18

@dudu: 出错的话可以抛事件,

支持(0) 反对(0) oiliu | 园豆:234 (菜鸟二级) | 2016-09-20 17:25
0

var task=System.Net.Dns.GetHostAddressesAsync(host);

task.ContinueWith(_=>

{

var iplists=task.Result;

....

});

收获园豆:40
Daniel Cai | 园豆:10424 (专家六级) | 2016-09-20 17:25

使用ContinueWith()要使用Wait(),一用Wait()也是同样的问题

var continueTask = task.ContinueWith(_ =>
{                    
    var addresses = task.Result;                    
});
continueTask.Wait();
支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-20 18:56
0

还是http async content的问题。task指定一个新的async content。不使用http上下文的asynccontent。

 

收获园豆:30
czd890 | 园豆:14412 (专家六级) | 2016-09-20 19:31
0

Hi, dudu

关于 Dns.GetHostAddressesAsync() 的问题,其实是这样的:

你说的慢的问题,并不是死锁,也不是因为异步的问题,而且线程池的线程耗尽了,在被阻塞等待处理(理解为死锁了也没有太大关系)。


在.NET Core 中, 关于 Dns.GetHostAddressesAsync 的内部实现 它其实是一个包装器,通过阻塞的方式包装了一个线程池队列,然后当有大量请求的时候,线程池的线程已经不够用了,也就是没了空闲的线程去处理新请求了,自然就会别阻塞等待新线程释放。所以会变得很慢,甚至卡死。。

见代码 : https://github.com/dotnet/corefx/blob/1baa9ad06a466a5d6399e1e0a7a7a02564ab51b0/src/System.Net.NameResolution/src/System/Net/DNS.cs#L274


在.NET Framework 中,虽然和 .NET Core的实现有一点不一样,但是仍然存在这个问题。

见代码 :
http://referencesource.microsoft.com/#System/net/System/Net/DNS.cs,744


解决方法就是:
Windows 操作系统和 Linux 操作系统都提供了 真正的 异步的 解析 DNS 的实现,可以使用操作系统的。

windows dns api: https://msdn.microsoft.com/en-us/library/ms738524(VS.85).aspx

linux dns api: http://www.binarytides.com/dns-query-code-in-c-with-linux-sockets/

关于同步异步的IO问题,可以参见这个讨论:
https://github.com/dotnet/corefx/issues/5044

给分吧,哈哈,不谢。



参考资料:https://github.com/dotnet/corefx/issues/8371

收获园豆:50
Savorboard | 园豆:411 (菜鸟二级) | 2016-09-20 22:26

我试试调用Linux的getent命令进行解析

支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-20 22:58

调用getent命令解析成功

var psi = new System.Diagnostics.ProcessStartInfo("getent", " hosts " + host);
psi.RedirectStandardOutput = true;
using (var process = System.Diagnostics.Process.Start(psi))
{
    var output = process.StandardOutput.ReadToEnd();
    if(!string.IsNullOrWhiteSpace(output))
    {
        var ipString = Regex.Match(output, @"^\d+\.\d+\.\d+\.\d+").Value;
        if(!string.IsNullOrEmpty(ipString))
        {
            address = IPAddress.Parse(ipString);
        }
    }
}
支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-20 23:33

不是“线程池的线程耗尽了”的原因,在异步方法调用Dns.GetHostAddressesAsync()没这个问题。

支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-21 07:35

@dudu: dudu,我刚才测试了一下, 在10个以下连结的时候,响应时间都在5ms以内,随着连接数的增多,响应时间逐渐变长,当到60个线程的时候,响应时间35-45毫秒了。 我测试用的Web程序的Action是异步方法,负载工具用的Load Runner。

支持(0) 反对(0) Savorboard | 园豆:411 (菜鸟二级) | 2016-09-21 09:28

@Savorboard: 即使有这个问题,也影响不大,只是在初始化socket池或者创建新的socket连接时会调用SocketDns.GetHostAddressesAsync(),而且dns解析本身就有缓存。

支持(0) 反对(0) dudu | 园豆:30994 (高人七级) | 2016-09-21 10:15
0

学习了!

JackWang-CUMT | 园豆:2866 (老鸟四级) | 2016-09-21 08:20
0

目前我见到的最高的豆~哈哈~我是冲着豆进来看的

 

小刺猬001 | 园豆:660 (小虾三级) | 2016-09-21 08:57
0

不知所云。

 

NiceCatch90 | 园豆:72 (初学一级) | 2016-09-21 09:07
0

Task.Factory.StartNew(() => DoAsync()).Unwrap().GetAwaiter().GetResult()

怎样疯狂的涛涛 | 园豆:116 (初学一级) | 2023-03-28 21:16
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册