当前位置: 首页 > news >正文

5 种避免使用 C# lock 关键字的方法

5 种避免使用 C# lock 关键字的方法

https://zhuanlan.zhihu.com/p/136031306

提起多线程编程,始终离不开线程安全(资源竞争)的问题。如果没有处理好这些问题,往往在会出现开发一时爽,调试火葬场的情况。大都数语言中都会提供一些特定的方法来简化多线程开发,比如 C# 就提供了 lock 关键字来解决这些问题。

如果你在开发的过程中正确的使用了 lock 关键字,将有效的避免许多线程安全的问题。但是任何解决方案都是存在代价的,一味使用 lock 的话也会照成意想不到的性能(逼格)损失。本文就列举了 5 种情况下应避免使用 lock 关键字。

使用 System.Collections.Concurrent 命名空间

比如直接使用 ConcurrentDictionary 替代 Dictionary ,而不是使用 lock:

//使用 lock + Dictionary
lock (dict)
{
    if (dict.ContainsKey(key))
    {
        dict[key] = dict[key] + value;
    }
    else
    {
        dict.Add(key, value);
    }
}
//使用 ConcurrentDictionary
dict.AddOrUpdate(key, value, (k, v) => v + value);

在需要线程安全的情景下应该使用 ConcurrentDictionary<TKey,TValue> 代替 Dictionary<TKey,TValue>,ConcurrentBag<T> 代替 List<T> ,同时还有线程安全的 ConcurrentQueue<T>、ConcurrentStack<T> 等在该命名空间可供使用。值得注意的是 ConcurrentBag 是一个无序的集合同时也并不实现 IList 接口,所以无法使用索引,也无法在需要 IList 的地方代替。

我相信大部分人都知道 System.Collections.Concurrent 这个命名空间,本不想写这一段,但是有点不可思议是我经常见因为不知道 ConcurrentDictionary 的存在而手写了一个 “SynchronizedDictionary ”的人,而且一般手写的“SynchronizedDictionary ”都是有很大问题的,这点可以从 ConcurrentDictionary 选择显示实现 IDictionary 接口上学到不少设计一个线程安全对象 API 的技巧。

使用 Interlocked

使用 Interlocked 而不是使用 lock:

//使用 lock 
lock (counter)
{
    counter.Progress += val;
}
//使用 Interlocked
Interlocked.Add(ref counter.Progress, val);

Interlocked 对于 int,long 这样的基础类型进行线程安全的运算操作时特别方便。线程安全要求保证对共享变量的任何写入或读取访问都是原子的,不然的话,你正在处理数据可能已不可用,或者读取出来的值可能不正确。总之,使用 Interlocked 不仅性能比 lock 更好,而且代码更为清晰简单。

使用 ThreadStaticAttribute 或 ThreadLocal

在处理一些特定场景中需要为每个线程提供单独的变量,比如于多线程下载时,每个线程都需要记录下载的字节数,那么使用 ThreadStaticAttribute 最合适不过了:

//使用 ConcurrentDictionary 下载字节数
ConcurrentDictionary<int, int> ThreadDownloadBytes = new ConcurrentDictionary<int, int>();
void DownloadFileThread()
{
    //... 记录下载字节数
    ThreadDownloadBytes.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, readedByteCount, (k, v) => v + readedByteCount);
}

//使用 ThreadStaticAttribute 来使不同线程有各自独立的存储空间
[ThreadStatic]
static int ThreadDownloadBytes;
void DownloadFileThread()
{
    //... 记录下载字节数
    ThreadDownloadBytes += readedByteCount;
}

上面的场景可能跟实际应用中有出入,只是作为例子说明问题。你可能会说这个场景中并没有使用 lock 啊,那是已经使用 ConcurrentDictionary 来简化代码。

但此处请注意,使用 ThreadStaticAttribute 特性标注的静态变量的初始化并不可靠,因为初始化这个行为只在一个线程发生。如果需要对对象进行初始化或者读取不同线程储存的值可以考虑使用 ThreadLocal<T>

使用 ReaderWriterLockSlim

在需要对资源读写的线程安全中,简单使用 lock 没有太多问题,但是如果这个资源的读取频率高,写入频率相对比较低,则可以使用 ReaderWriterLockSlim 来进一步提升性能,这种情况在一些需要线程安全的缓存场景特别常见。

//使用 lock 方法进行读写的线程安全管理
private object lockObject = new object();
private void Read()
{
    lock (lockObject)
    {
        //具体实现
    }
}
private void Write(string value)
{
    lock (lockObject)
    {
        //具体实现
    }
}
//使用 ReaderWriterLockSlim 进行类似的操作
private ReaderWriterLockSlim LockSlim = new ReaderWriterLockSlim();
private void Read()
{
    LockSlim.EnterReadLock();
    try
    {
        //具体实现
    }
    finally
    {
        LockSlim.ExitReadLock();
    }
}
private void Write(string value)
{
    LockSlim.EnterWriteLock();
    try
    {
        //具体实现
    }
    finally
    {
        LockSlim.ExitWriteLock();
    }
}

看起来好像 ReaderWriterLockSlim 更麻烦一点,不过之所以 ReaderWriterLockSlim 性能更好,是因为它可以允许多个线程进行读取操作,而当进行写入操作时进入独占模式。如果你的场景读取和写入频率不确定,则不应该使用 ReaderWriterLockSlim。ReaderWriterLockSlim 提供了许多方法对资源进行控制,请务必详读 MSDN 里相关内容后再尝试开始使用。

使用 SpinLock 自旋锁

其实我不应该在这里提到 SpinLock,因为如果读这篇文章到这里还没离开的人可能无法正确区分 SpinLock 的适用场景,虽然它的用法跟不加糖的 lock 特别相似。

//lock
private static object lockObject = new object();
private void Update(DateTime d)
{
    lock (lockObject)
    {
        list.Add(d);
    }
}

//SpinLock
private static SpinLock spinLock = new SpinLock();
private void UpdateWithSpinLock(DateTime d)
{
    bool lockTaken = false;
    try
    {
        spinLock.Enter(ref lockTaken);
        list.Add(d);
    }
    finally
    {
        if (lockTaken) spinLock.Exit(false);
    }
}

简单的解释 lock 和 SpinLock 之间区别是,lock 会在资源发生竞争的时候会切换去执行其它代码等待时机,类似于 Thread.Sleep 会把 CPU 时间让出去;而 SpinLock 在发生资源竞争时尝试自旋几个周期再去尝试,类似执行一个 do while 循环,消耗 CPU 时间。而且 SpinLock 是一个 struct 在大量使用的情况下对 GC 友好。所以当你确认锁独占资源的时间非常短,并且也没有使用其它你不知道源码的方法,可以考虑使用 SpinLock 来代替 lock 。

由于我的表达水平有限,实际情况远比上面的说法要复杂,所以关于 SpinLock 适用场景我直接摘抄 MSDN 的内容:[1]

如果共享资源上的锁不会保留太长时间,SpinLock 可能会很有用。在这种情况下,多核计算机上的阻止线程可高效旋转几个周期,直到锁被释放。通过旋转,线程不会受到阻止,这是一个占用大量 CPU 资源的进程。

但是 MSDN 上关于不适于使用 SpinLock 的情况更多:[2]

通常,在持有自旋锁时,应避免使用以下任何操作:
1.堵塞
2.调用自身可能会阻止的任何内容,
3.同时保留多个自旋锁,
4.进行动态调度的调用(interface 和虚方法),
5.对任何代码进行静态调度调用,而不是任何代码,或
6.分配内存。

最后

多线程编程并不容易驾驭,不然也不会出现“一核有难多核围观”的梗,本文目的也只是抛砖引玉,本人水平有限也不想大篇幅的深入底层细节。上面每个话题在 MSDN 上都有大篇幅的内容可供深入阅读,毕竟微软的文档质量不是盖的。最后,写这么个不入流的文章只是希望看到有人大篇幅的使用 lock 关键字的时候,能有篇东西能发给TA看。

参考

  1. ^如何:使用 SpinLock 进行低级别同步 https://docs.microsoft.com/zh-cn/dotnet/standard/threading/how-to-use-spinlock-for-low-level-synchronization?view=netcore-3.1
  2. ^SpinLock 结构 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.spinlock?view=netcore-3.1

编辑于 2020-05-06

 

 

 

 

相关文章:

  • [玩转UE4动画系统>功能模块] 之 Advanced Locomotion System V4 的工具函数及工具宏详解
  • Lua5.4新特性
  • 探究光线追踪技术及UE4的实现 -- good
  • Unity游戏项目性能优化总结
  • 【UE4源代码观察】观察DDC(DerivedDataCache)
  • Chrome 抓包
  • float.Parse 在不同区域小数表示是不一样的
  • C# GC 垃圾回收
  • Android Studio NDk调试(基于gradle-experimental插件与LLDB)
  • android studio调试c/c++代码
  • 在开发过程中使用 git rebase 还是 git merge,优缺点分别是什么?
  • Visual Studio 2019 远程调试工具(Remote Debugger)使用方法
  • Windbg使用说明书
  • 使用Windbg查看CrashDump
  • 【无标题】windbg 分析dump文件
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • JavaScript HTML DOM
  • JavaScript 基础知识 - 入门篇(一)
  • JavaScript类型识别
  • Js基础——数据类型之Null和Undefined
  • js算法-归并排序(merge_sort)
  • Leetcode 27 Remove Element
  • MQ框架的比较
  • MYSQL 的 IF 函数
  • 闭包--闭包之tab栏切换(四)
  • 从重复到重用
  • 多线程 start 和 run 方法到底有什么区别?
  • 给新手的新浪微博 SDK 集成教程【一】
  • 温故知新之javascript面向对象
  • 我建了一个叫Hello World的项目
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 在Mac OS X上安装 Ruby运行环境
  • CMake 入门1/5:基于阿里云 ECS搭建体验环境
  • FaaS 的简单实践
  • linux 淘宝开源监控工具tsar
  • 如何通过报表单元格右键控制报表跳转到不同链接地址 ...
  • 移动端高清、多屏适配方案
  • 智能情侣枕Pillow Talk,倾听彼此的心跳
  • ​软考-高级-信息系统项目管理师教程 第四版【第19章-配置与变更管理-思维导图】​
  • # 手柄编程_北通阿修罗3动手评:一款兼具功能、操控性的电竞手柄
  • #{}和${}的区别是什么 -- java面试
  • #NOIP 2014#day.2 T1 无限网络发射器选址
  • #stm32整理(一)flash读写
  • ( 10 )MySQL中的外键
  • (2/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (9)YOLO-Pose:使用对象关键点相似性损失增强多人姿态估计的增强版YOLO
  • (function(){})()的分步解析
  • (剑指Offer)面试题34:丑数
  • (五)c52学习之旅-静态数码管
  • (转)http协议
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • (转)四层和七层负载均衡的区别
  • .apk文件,IIS不支持下载解决
  • .net mvc部分视图