之前讨论了基元用户模式和内核模式线程同步构造。其他所有线程同步构造都基于它们,而且一般都合并了用户模式和内核模式构造,我们称为混合线程同步构造。没有线程竞争时,混合构造提供了基元用户模式构造所具有的性能优势。多个线程竞争一个构造时,混合构造通过基元内核模式的构造来提供不自旋的优势。由于大多数应用程序的线程都很少同时竞争一个构造,所以性能上的增强可以使你的应用程序表现得更出色。

本章最后展示如何使用fcl的并发集合类来取代混合构造,从而最小化资源使用并提升性能。同时还讨论了异步的同步构造,允许以同步方式访问资源,同时不造成任何线程的阻塞,从而减少资源消耗,并提高了伸缩性。

一个简单的混合锁

  1. internal sealed class SimpleHybridLock:IDisposable
  2. {
  3. //int由基元用户模式构造(interlocked的方法)使用
  4. private Int32 m_waiters = ;
  5.  
  6. //autoResetEvent基元内核模式构造
  7. private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
  8.  
  9. private void Enter()
  10. {
  11. //指出这个线程想要获得锁
  12. if (Interlocked.Increment(ref m_waiters) ==)
  13. {
  14. return;//锁可以使用,无竞争,直接返回
  15. }
  16. //另一个线程用有锁(发送竞争),使这个线程等待
  17. m_waiterLock.WaitOne();//这里产生性能消耗,但是也比自旋要好(当然也存在自旋几毫秒就获得锁的情况。。。。)
  18. //waitone返回后,这个线程拿到了锁
  19. }
  20. public void Leave()
  21. {
  22. //这个线程准备释放锁
  23. if (Interlocked.Decrement(ref m_waiters)==)
  24. {
  25. return;
  26. }
  27. //其他线程正在阻塞,唤醒其中一个
  28. m_waiterLock.Set();
  29.  
  30. }
  31. public void Dispose()
  32. {
  33. m_waiterLock.Dispose();
  34. }
  35. }

  SimpleHybridLock包含两字字段:一个int32,由基元用户模式的构造来操作;以及一个AutoResetEvent,他是一个基元内核模式的构造。为了获得出色的性能,锁要尽量操作int32,尽量少操作AutoResetEvent。也有出现竞争情况才去创建AutoResetEvent的设计,后面的示例代码也有这种设计。

  在实际应用中,任何线程都可以在任何时候调用leave方法,因为enter并没有记录哪个线程获得了锁。虽然可以添加字段和代码维护这个信息,但是会增大内存开销而且影响enter和leave的性能,我情愿有一个性能高超的锁,并确保我的代码以正确的方式使用它。你会注意到,事件和信号量都没有维护这种信息,只有互斥体才有维护。

自旋、线程所有权和递归

  由于转换为内核模式会造成巨大的性能损失,而且线程占有锁的时间通常很短,所以为了提升应用的总体性能,可以让一个线程在用户模式自旋一小段时间,再让线程转换为内核模式。如果线程正在等待的锁在自旋期间变得可用,就能避免模式转换了。

此外,有的锁限制只能由获得锁的线程释放锁,有的锁允许递归,所以可以通过一些逻辑支持自旋、线程所有权和递归,下面是一个例子:

  1. internal sealed class AnotherHybridLock:IDisposable
  2. {
  3. //int由基元用户模式构造(interlocked的方法)使用
  4. private Int32 m_waiters = ;
  5.  
  6. //autoResetEvent基元内核模式构造
  7. private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
  8.  
  9. //这个字段控制自旋,希望能提升性能
  10. private Int32 m_spincount = ;
  11. //这些字段指出哪个线程拥有锁,以及拥有了多少次
  12. private Int32 m_owingThreadId=,m_recursion=;
  13. private void Enter()
  14. {
  15. //如果调用线程已经拥有锁,递增递归计数并返回
  16. Int32 threadId = Thread.CurrentThread.ManagedThreadId;
  17. if (threadId==m_owingThreadId)
  18. {
  19. m_recursion++;return;
  20. }
  21. //调用线程不拥有锁,尝试获取他
  22. SpinWait spinwait = new SpinWait();
  23. for (int i = ; i < m_spincount; i++)
  24. {
  25. //锁国锁可以自由使用了,这个线程获取
  26. //比较location1与comparand,如果不相等,什么都不做;如果location1与comparand相等,则用value替换location1的值。无论比较结果相等与否,返回值都是location1中原有的值
  27. if (Interlocked.CompareExchange(ref m_waiters,,) == )
  28. {
  29. goto GotLock;
  30. }
  31. //黑科技:给其他线程运行的机会,希望锁会被释放
  32. spinwait.SpinOnce();
  33. }
  34.  
  35. //自旋结束,再次尝试
  36. if (Interlocked.Increment(ref m_waiters) > )
  37. {
  38. //仍然是竞争状态,线程阻塞
  39. m_waiterLock.WaitOne();
  40. //等待结束之后,他拥有锁
  41. }
  42. GotLock:
  43. //一个线程获得锁时,记录他的id,并指出线程拥有锁一次
  44. m_owingThreadId = threadId;m_recursion = ;
  45. }
  46. public void Leave()
  47. {
  48. //这个线程准备释放锁
  49. if (Interlocked.Decrement(ref m_waiters) == )
  50. {
  51. return;
  52. }
  53. //其他线程正在阻塞,唤醒其中一个
  54. m_waiterLock.Set();
  55.  
  56. }
  57. public void Dispose()
  58. {
  59. m_waiterLock.Dispose();
  60. }
  61. }

改进的混合锁

执行结果

注意,anotherHybridLock的性能不如simpleHybridLock。这是因为需要额外的逻辑和错误检查来管理线程所有权和递归行为。

FCL中的混合构造

Fcl是framework class library的简称。

ManualResetEventSlim类和SemaphoreSlim类

这两个构造的工作方式和对应的内核模式完全一致,只是他们都在用户模式中自旋,而且都推迟到发生第一次竞争时,才创建内核模式的构造。这里就不多介绍了。

Monitor类和同步块

  因为Monitor是资格最老的构造,所以用的比较多,但是存在一些问题。这个类是lock关键字使用的混合锁的类型。ConcurrentDictory里面对key进行的锁定也是使用这个构造。

堆中的每个对象都可关联一个名为同步块的数据结构。同步块包含字段,他为内核对象、线程id、递归计数以及等待线程计数提供了相应的字段。

monitor是静态类,他的方法接收任何堆对象的引用,这些方法对指定对象的同步块中的字段进行操作。

  显然,为堆中每个对象都关联一个同步块数据结构显得浪费,尤其是大部分都从不使用。为了节省内存,clr团队采用一种更经济的方式提供刚才描述的功能。他的工作原理:clr初始化时在堆中分配一个同步块数组(每当一个对象在堆中创建的时候,都有两个额外开销的字段与它管理,第一个是“类型对象指针”另一个是“同步索引块”,包含同步块数组中的一个整数索引)。

一个对象在构造时,他的同步索引块初始化为-1,表明不引用任何同步块。然后调用monitor.Enter时,clr在数组中找到一个空白同步块,并设置对象的同步索引块,让他引用该同步块。调用exit时,会检查是否有其他线程正在等待使用对象的同步块。如果没有线程等待它,同步块就自由了,对象同步块索引设会-1。

monitor类使用方式如下:

  1. internal sealed class MonitorTransaction
  2. {
  3. private DateTime m_timeOfLastTrans;
  4. public void PerformTransaction()
  5. {
  6. Monitor.Enter(this);
  7. //以下代码拥有对数据的独占访问权
  8. m_timeOfLastTrans = DateTime.Now;
  9. Monitor.Exit(this);
  10. }
  11. public DateTime LastTransaction
  12. {
  13. get
  14. {
  15. Monitor.Enter(this);
  16. DateTime temp = m_timeOfLastTrans;
  17. Monitor.Exit(this);
  18. return temp;
  19. }
  20. }
  21. }

monitor可能出现的问题:

  每个对象的同步块索引都是隐式为公共的。因为你使用一个对象来进行Monitor.Enter(t)的时候,有可能t会被其他线程使用,如果这个线程也对t使用了一些会使用锁的操作,那么就会死锁,而且比较难判断究竟是哪里造成的死锁。

所以,最好的办法是坚持使用私有锁。私有锁是必要的,防止职责不清晰。

  1. internal sealed class MonitorTransactionBetter
  2. {
  3. private readonly object m_lock = new object();//现在每个transaction对象都有私有锁
  4. private DateTime m_timeOfLastTrans;
  5. public void PerformTransaction()
  6. {
  7. Monitor.Enter(m_lock);
  8. //以下代码拥有对数据的独占访问权
  9. m_timeOfLastTrans = DateTime.Now;
  10. Monitor.Exit(m_lock);
  11. }
  12. public DateTime LastTransaction
  13. {
  14. get
  15. {
  16. Monitor.Enter(m_lock);
  17. DateTime temp = m_timeOfLastTrans;
  18. Monitor.Exit(m_lock);
  19. return temp;
  20. }
  21. }
  22. }

  通过以上讨论,我们可以看出,monitor不应该实现成静态类;他应该像其他所有同步构造那样实现。

  由于monitor的方法获取一个object,所以传递值类型会导致值类型装箱,造成线程在已装箱对象上获取锁。每次调用monitor.enter都会在一个完全不同的对象上获取锁,造成完全无法实现线程同步。

这也是为什么被锁的对象一定要使用引用类型。

  还有一个问题,就是前面说过的lock关键字,这个关键字的使用相当于在代码的最后加上了一个finally,里面判断如果是锁定的状态,会自动退出锁。但是如果代码一旦在finally里更改状态出现异常,那么这个锁就处于损坏状态。还有一个问题,进入和离开try块会影响方法的性能。所以使用lock关键字并不是看上去的那么方便。

ReaderWriterLockSlim类

ReaderWriterLockSlim构造执行逻辑如下:

1、 一个线程向数据写入时,请求访问的其他所有线程都被阻塞。

2、 一个线程从数据读取时,请求读取的其他线程允许继续执行,但请求写入的线程仍被阻塞。

3、 向线程写入的线程结束后,要么解除一个写入线程的阻塞,那么解除所有读取线程的阻塞。如果没有线程阻塞,那么锁进入自由状态。

4、 所有读取的线程结束后,一个writer线程才会被解除阻塞。

下面展示了这个类

以下代码演示这个构造的用法

  1. internal sealed class ReaderWriterTransation:IDisposable
  2. {
  3. private readonly ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
  4.  
  5. private DateTime m_timeOfLastTrans;
  6.  
  7. public void PerformTransaction()
  8. {
  9. m_lock.EnterWriteLock();
  10. m_timeOfLastTrans = DateTime.Now;
  11. m_lock.ExitWriteLock();
  12. }
  13. public DateTime LastTransaction
  14. {
  15. get {
  16. m_lock.EnterReadLock();
  17. DateTime temp = m_timeOfLastTrans;
  18. m_lock.ExitReadLock();
  19. return temp;
  20. }
  21. }
  22. public void Dispose() { m_lock.Dispose(); }
  23. }

ReaderWriterLockSlim类提供了一些额外的方法,比如允许读线程升级为写线程,但如果去使用这个功能,性能会大打折扣。

线程同步构造小结

我的建议是,代码尽量不要阻塞任何线程。执行异步计算或I/O操作时,将数据从一个线程交给另一个线程时,应避免多个线程同时访问数据。如果不能完全做到这一点,请尽量使用Volatile和InterLocked的方法,因为他们速度很快,且绝不阻塞线程。遗憾的是,这些方法只能操作简单类型,但你可以像InterLocked Anything模式描述的那样执行丰富的操作。

C#异步编程(四)混合模式线程同步的更多相关文章

  1. Python并行编程(四):线程同步之RLock

    1.基本概念 如果想让只有拿到锁的线程才能释放该锁,那么应该使用RLock()对象.当需要在类外面保证线程安全,又要在类内使用同样方法的时候RLock()就很使用. RLock叫做Reentrant ...

  2. Linux多线程编程——多线程与线程同步

    多线程 使用多线程好处: 一.通过为每种事件类型的处理单独分配线程,可以简化处理异步事件的代码,线程处理事件可以采用同步编程模式,启闭异步编程模式简单 二.方便的通信和数据交换 由于进程之间具有独立的 ...

  3. Posix线程编程指南(3) 线程同步

    互斥锁 尽管在Posix Thread中同样可以使用IPC的信号量机制来实现互斥锁mutex功能,但显然semphore的功能过于强大了,在Posix Thread中定义了另外一套专门用于线程同步的m ...

  4. 《Linux多线程服务端编程》笔记——线程同步精要

    并发编程基本模型 message passing和shared memory. 线程同步的四项原则 尽量最低限度地共享对象,减少需要同步的场合.如果确实需要,优先考虑共享 immutable 对象. ...

  5. Delphi异步编程:匿名线程与匿名方法

    异步编程,是项目中非常有用的而且常用的一种方法,大多以线程实现. 而Delphi传统方法使用线程略为烦琐,好在其后续版本中,提供一些方法,简化一些操作. 几个概念: 匿名线程:TAnonymousTh ...

  6. Python之路(第四十四篇)线程同步锁、死锁、递归锁、信号量

    在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用python多线程中提供Lock ...

  7. Python并行编程(五):线程同步之信号量

    1.基本概念 信号量是由操作系统管理的一种抽象数据类型,用于在多线程中同步对共享资源的使用.本质上说,信号量是一个内部数据,用于标明当前的共享资源可以有多少并发读取. 同样在threading中,信号 ...

  8. Python并行编程(三):线程同步之Lock

    1.基础概念 当两个或以上对共享内存操作的并发线程中,如果有一个改变数据,又没有同步机制的条件下,就会产生竞争条件,可能会导致执行无效代码.bug等异常行为. 竞争条件最简单的解决方法是使用锁.锁的操 ...

  9. [C# 线程处理系列]专题四:线程同步

    目录: 一.线程同步概述 二.线程同步的使用 三 .总结 一.线程同步概述 前面的文章都是讲创建多线程来实现让我们能够更好的响应应用程序,然而当我们创建了多个线程时,就存在多个线程同时访问一个共享的资 ...

随机推荐

  1. 第二篇 Python图片处理模块PIL(pillow)

    本篇包含:16.Point    17.Putalpha    18.Putdata    19.Putpalette    20.Putpixel      21.Quantize     22.R ...

  2. jdbc驱动jar导入eclipse

    在使用JDBC编程时需要连接数据库,导入JAR包是必须的,导入其它的jar包方法同样如此,导入的方法是 打开eclipse 1.右击要导入jar包的项目,点properties 2.左边选择java ...

  3. Linux进程优先级查看及修改

    进程cpu资源分配就是指进程的优先权(priority).优先权高的进程有优先执行权利.配置进程优先权对多任务环境的Linux很有用,可以改善系统性能.还可以把进程运行到指定的CPU上,这样一来,把不 ...

  4. Java数据类型 及 转换原则

    一.数据类型分类:主要分为 基本类型.引用类型两大类: 二.基本类型 转换原则 1.类型转换主要在在 赋值.方法调用.算术运算 三种情况下发生. a.赋值和方法调用 转换规则:从低位类型到高位类型自动 ...

  5. winter 2018 02 01 关于模运算的一道题

    题目:给出一个正整数n,问是否存在x,满足条件2^x mod n=1,如果存在,求出x的最小值. 分析:1.若给出的n是1,则肯定不存在这样的x;     2.若给出的是偶数,2的次幂取余一个偶数得到 ...

  6. QT线程

    一.QObject子类 说明:以串口线程传输文件为例子,使用的是MoveTothread函数. void QObject::moveToThread(QThread *targetThread)可以将 ...

  7. centos6 多段Ip添加脚本

    #!/bin/bash export device=`ifconfig|grep eth0|head -n 1|awk '{print ($1)}'`export ipcfg_pre="/e ...

  8. MapReduce-shuffle过程详解

    Shuffle map端 map函数开始产生输出时,并不是简单地将它写到磁盘.这个过程很复杂,它利用缓冲的方式写到内存并出于效率的考虑进行预排序.每个map任务都有一个环形内存缓冲区用于存储任务输出. ...

  9. 【转】Android中的IOC框架,完全注解方式就可以进行UI绑定和事件绑定

    转载请注明出处:http://blog.csdn.net/blog_wang/article/details/38468547 相信很多使用过Afinal和Xutils的朋友会发现框架中自带View控 ...

  10. 比较运算符in/instanceof/typeof 逻辑表达式||/&&

    1.比较运算符in in运算符希望它的左侧操作数是一个字符串或可以转换为字符串,希望它的右操作数是一个对象, 如果右侧的对象拥有一个名为左侧操作数值的属性名,那么表达式返回true, eg:var a ...