一次 .NET Core 中玩锁的经历:ManualResetEventSlim, Semaphore 与 SemaphoreSlim
最近同事对 .net core memcached 缓存客户端 EnyimMemcachedCore 进行了高并发下的压力测试,发现在 linux 上高并发下使用 async 异步方法读取缓存数据会出现大量失败的情况,比如在一次测试中,100万次读取缓存,只有12次成功,999988次失败,好恐怖。如果改为同步方法,没有一次失败,100%成功。奇怪的是,同样的压力测试程序在 Windows 上异步读取却没问题,100%成功。
排查后发现是2个地方使用的锁引起的,一个是 ManualResetEventSlim ,一个是 Semaphore ,这2个锁是在同步方法中使用的,但 aync 异步方法中调用了这2个同步方法,我们来分别看一下。
使用 ManualResetEventSlim 是在创建 Socket 连接时用于控制连接超时
var args = new SocketAsyncEventArgs();
using (var mres = new ManualResetEventSlim())
{
args.Completed += (s, e) => mres.Set();
if (socket.ConnectAsync(args))
{
if (!mres.Wait(timeout))
{
throw new TimeoutException("Could not connect to " + endpoint);
}
}
}
使用 Semaphore 是在从 EnyimMemcachedCore 自己实现的 Socket 连接池获取 Socket 连接时
if (!this.semaphore.WaitOne(this.queueTimeout))
{
message = "Pool is full, timeouting. " + _endPoint;
if (_isDebugEnabled) _logger.LogDebug(message);
result.Fail(message, new TimeoutException()); // everyone is so busy
return result;
}
为了弃用这个2个锁造成的异步并发问题,采取了下面2个改进措施:
1)对于 ManualResetEventSlim ,参考 corefx 中 SqlClient 的 SNITcpHandle 的实现,改用 CancellationTokenSource 控制连接超时
var cts = new CancellationTokenSource();
cts.CancelAfter(timeout);
void Cancel()
{
if (!socket.Connected)
{
socket.Dispose();
}
}
cts.Token.Register(Cancel); socket.Connect(endpoint);
if (socket.Connected)
{
connected = true;
}
else
{
socket.Dispose();
}
2)对于 Semaphore ,根据同事提交的 PR ,将 Semaphore 换成 SemaphoreSlim ,用 SemaphoreSlim.WaitAsync 方法等待信号量锁
if (!await this.semaphore.WaitAsync(this.queueTimeout))
{
message = "Pool is full, timeouting. " + _endPoint;
if (_isDebugEnabled) _logger.LogDebug(message);
result.Fail(message, new TimeoutException()); // everyone is so busy
return result;
}
改进后,压力测试结果立马与同步方法一样,100% 成功!
为什么会这样?
我们到 github 的 coreclr 仓库(针对 .net core 2.2)中看看 ManualResetEventSlim 与 Semaphore 的实现源码,看能否找到一些线索。
(一)
先看看 ManualResetEventSlim.Wait 方法的实现代码(523开始):
1)先 SpinWait 等待
var spinner = new SpinWait();
while (spinner.Count < spinCount)
{
spinner.SpinOnce(sleep1Threshold: -); if (IsSet)
{
return true;
}
}
SpinWait 等待时间比较短,不会造成长时间阻塞线程。
在高并发下大量线程在争抢锁,所以大量线程在这个阶段等不到锁。
2)然后 Monitor.Wait 等待
try
{
// ** the actual wait **
if (!Monitor.Wait(m_lock, realMillisecondsTimeout))
return false; //return immediately if the timeout has expired.
}
finally
{
// Clean up: we're done waiting.
Waiters = Waiters - ;
}
Monitor.Wait 对应的实现代码
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern bool ObjWait(bool exitContext, int millisecondsTimeout, object obj); public static bool Wait(object obj, int millisecondsTimeout, bool exitContext)
{
if (obj == null)
throw (new ArgumentNullException(nameof(obj)));
return ObjWait(exitContext, millisecondsTimeout, obj);
}
最终调用的是一个本地库的 ObjWait 方法。
查阅一下 Monitor.Wait 方法的帮助文档:
Releases the lock on an object and blocks the current thread until it reacquires the lock. If the specified time-out interval elapses, the thread enters the ready queue.
Monitor.Wait 的确会阻塞当前线程,这在异步高并发下会带来问题,详见一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相。
(二)
再看看 Semaphore 的实现代码,它继承自 WaitHandle , Semaphore.Wait 实际调用的是 WaitHandle.Wait ,后者调用的是 WaitOneNative ,这是一个本地库的方法
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern int WaitOneNative(SafeHandle waitableSafeHandle, uint millisecondsTimeout, bool hasThreadAffinity, bool exitContext);
.net core 3.0 中有些变化,这里调用的是 WaitOneCore 方法
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int WaitOneCore(IntPtr waitHandle, int millisecondsTimeout);
查阅一下 WaitHandle.Wait 方法的帮助文档:
Blocks the current thread until the current WaitHandle receives a signal, using a 32-bit signed integer to specify the time interval in milliseconds.
WaitHandle.Wait 也会阻塞当前线程。
2个地方在等待锁时都会阻塞线程,难怪高并发下会出问题。
(三)
接着阅读 SemaphoreSlim 的源码学习它是如何在 WaitAsync 中实现异步等待锁的?
public Task<bool> WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken)
{
//... lock (m_lockObj!)
{
// If there are counts available, allow this waiter to succeed.
if (m_currentCount > )
{
--m_currentCount;
if (m_waitHandle != null && m_currentCount == ) m_waitHandle.Reset();
return s_trueTask;
}
else if (millisecondsTimeout == )
{
// No counts, if timeout is zero fail fast
return s_falseTask;
}
// If there aren't, create and return a task to the caller.
// The task will be completed either when they've successfully acquired
// the semaphore or when the timeout expired or cancellation was requested.
else
{
Debug.Assert(m_currentCount == , "m_currentCount should never be negative");
var asyncWaiter = CreateAndAddAsyncWaiter();
return (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled) ?
asyncWaiter :
WaitUntilCountOrTimeoutAsync(asyncWaiter, millisecondsTimeout, cancellationToken);
}
}
}
重点看 else 部分的代码,SemaphoreSlim.WaitAsync 造了一个专门用于等待锁的 Task —— TaskNode ,CreateAndAddAsyncWaiter 就用于创建 TaskNode 的实例
private TaskNode CreateAndAddAsyncWaiter()
{
// Create the task
var task = new TaskNode(); // Add it to the linked list
if (m_asyncHead == null)
{
m_asyncHead = task;
m_asyncTail = task;
}
else
{
m_asyncTail.Next = task;
task.Prev = m_asyncTail;
m_asyncTail = task;
} // Hand it back
return task;
}
从上面的代码看到 TaskNode 用到了链表,神奇的等锁专用 Task —— TaskNode 是如何实现的呢?
private sealed class TaskNode : Task<bool>
{
internal TaskNode? Prev, Next;
internal TaskNode() : base((object?)null, TaskCreationOptions.RunContinuationsAsynchronously) { }
}
好简单!
那 SemaphoreSlim.WaitAsync 如何用 TaskNode 实现指定了超时时间的锁等待?
看 WaitUntilCountOrTimeoutAsync 方法的实现源码:
private async Task<bool> WaitUntilCountOrTimeoutAsync(TaskNode asyncWaiter, int millisecondsTimeout, CancellationToken cancellationToken)
{
// Wait until either the task is completed, timeout occurs, or cancellation is requested.
// We need to ensure that the Task.Delay task is appropriately cleaned up if the await
// completes due to the asyncWaiter completing, so we use our own token that we can explicitly
// cancel, and we chain the caller's supplied token into it.
using (var cts = cancellationToken.CanBeCanceled ?
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default(CancellationToken)) :
new CancellationTokenSource())
{
var waitCompleted = Task.WhenAny(asyncWaiter, Task.Delay(millisecondsTimeout, cts.Token));
if (asyncWaiter == await waitCompleted.ConfigureAwait(false))
{
cts.Cancel(); // ensure that the Task.Delay task is cleaned up
return true; // successfully acquired
}
} // If we get here, the wait has timed out or been canceled. // If the await completed synchronously, we still hold the lock. If it didn't,
// we no longer hold the lock. As such, acquire it.
lock (m_lockObj)
{
// Remove the task from the list. If we're successful in doing so,
// we know that no one else has tried to complete this waiter yet,
// so we can safely cancel or timeout.
if (RemoveAsyncWaiter(asyncWaiter))
{
cancellationToken.ThrowIfCancellationRequested(); // cancellation occurred
return false; // timeout occurred
}
} // The waiter had already been removed, which means it's already completed or is about to
// complete, so let it, and don't return until it does.
return await asyncWaiter.ConfigureAwait(false);
}
用 Task.WhenAny 等待 TaskNode 与 Task.Delay ,等其中任一者先完成,简单到可怕。
又一次通过 .net core 源码欣赏了高手是怎么玩转 Task 的。
【2019-5-6更新】
今天将 Task.WhenAny + Task.Delay 的招式用到了异步连接 Socket 的超时控制中
var connTask = _socket.ConnectAsync(_endpoint);
if (await Task.WhenAny(connTask, Task.Delay(_connectionTimeout)) == connTask)
{
await connTask;
}
一次 .NET Core 中玩锁的经历:ManualResetEventSlim, Semaphore 与 SemaphoreSlim的更多相关文章
- 玩转ASP.NET Core中的日志组件
简介 日志组件,作为程序员使用频率最高的组件,给程序员开发调试程序提供了必要的信息.ASP.NET Core中内置了一个通用日志接口ILogger,并实现了多种内置的日志提供器,例如 Console ...
- ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态
原文:Managing Application State 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:高嵩 在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行 ...
- 如何在 ASP.NET Core 中发送邮件
前言 我们知道目前 .NET Core 还不支持 SMTP 协议,当我么在使用到发送邮件功能的时候,需要借助于一些第三方组件来达到目的,今天给大家介绍两款开源的邮件发送组件,它们分别是 MailKit ...
- 在 ASP.NET Core 中执行租户服务
在 ASP.NET Core 中执行租户服务 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: http://gunna ...
- 浅析Entity Framework Core中的并发处理
前言 Entity Framework Core 2.0更新也已经有一段时间了,园子里也有不少的文章.. 本文主要是浅析一下Entity Framework Core的并发处理方式. 1.常见的并发处 ...
- Net Core中数据库事务隔离详解——以Dapper和Mysql为例
Net Core中数据库事务隔离详解--以Dapper和Mysql为例 事务隔离级别 准备工作 Read uncommitted 读未提交 Read committed 读取提交内容 Repeatab ...
- [翻译]在 .NET Core 中的并发编程
原文地址:http://www.dotnetcurry.com/dotnet/1360/concurrent-programming-dotnet-core 今天我们购买的每台电脑都有一个多核心的 C ...
- .NET平台开源项目速览(20)Newlife.Core中简单灵活的配置文件
记得5年前开始拼命翻读X组件的源码,特别是XCode,但对Newlife.Core 的东西了解很少,最多只是会用用,而且用到的只是九牛一毛.里面好用的东西太多了. 最近一年时间,零零散散又学了很多,也 ...
- ASP.Net Core 中使用Zookeeper搭建分布式环境中的配置中心系列一:使用Zookeeper.Net组件演示基本的操作
前言:马上要过年了,祝大家新年快乐!在过年回家前分享一篇关于Zookeeper的文章,我们都知道现在微服务盛行,大数据.分布式系统中经常会使用到Zookeeper,它是微服务.分布式系统中必不可少的分 ...
随机推荐
- spring(3)------控制反转(IOC)/依赖注入(DI)
一.spring核心概念理解 控制反转: 控制反转即IoC (Inversion of Control).它把传统上由程序代码直接操控的对象的调用权交给容器.通过容器来实现对象组件的装配和管理. 所谓 ...
- warning C4996: 'fopen': This function or variable may be unsafe.(_CRT_SECURE_NO_WARNINGS)
在 windows 平台下的 visual studio IDE,使用 fopen 等 CRT 函数(C runtime library(part of the C standard library) ...
- href="javascript:;" href="javascript:void(0);" href="#"区别
一.href="javascript:;" 这种用法不正确,这么用的话会出现浏览器访问"javascript:;"这个地址的现象: 二.href="j ...
- ArcSDE中Compress与Compact的区别
原文 ArcSDE中Compress与Compact的区别 附件一”为两种数据库需要的管理工作. 与所表示的含义与操作是不同的. 对于来说,Compressing与Smart Dat ...
- 前端Js框架汇总(工具多看)
前端Js框架汇总(工具多看) 一.总结 一句话总结: 二.前端Js框架汇总 概述: 有些日子没有正襟危坐写博客了,互联网飞速发展的时代,技术更新迭代的速度也在加快.看着Java.Js.Swift在各领 ...
- JS实现鼠标经过用户头像显示资料卡的效果,可点击
基于项目的须要.须要制作出例如以下的一种页面效果:当用户鼠标经过好友列表中好友头像时,显示该好友的基本资料.事实上也就是类似QQclient的那种功能. 网上找了非常多代码,基本都实现了鼠标悬浮之后弹 ...
- matlab 机器学习相关函数、api
matlab 对数据集的默认组织方式是,X∈Rd×N d:行数,表示特征向量的长度: N:列数,表示样本的数目: 1. 模型.预测.mse % 加载 matlab 内置数据到内存 X = abalon ...
- 【BZOJ 1016】 [JSOI2008]最小生成树计数(matrix-tree定理做法)
[题目链接]:http://www.lydsy.com/JudgeOnline/problem.php?id=1016 [题意] [题解] /* 接上一篇文章; 这里用matrix-tree定理搞最小 ...
- 【38.24%】【codeforces 621E】 Wet Shark and Blocks
time limit per test2 seconds memory limit per test256 megabytes inputstandard input outputstandard o ...
- 【records】10.9-10.16
.