C#线程同步(1)- 临界区&Lock .

预备知识:线程的相关概念和知识,有多线程编码的初步经验。

  一个机会,索性把线程同步的问题在C#里面的东西都粗略看了下。

  第一印象,C#关于线程同步的东西好多,保持了C#一贯的大杂烩和四不象风格(Java/Delphi)。临界区跟Java差不多只不过关键字用lock替代了synchronized,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,另外又搞出来几个Event……让人甚是不明了。不管那么多,一个一个来吧。

临界区(Critical Section)

  是一段在同一时候只被一个线程进入/执行的代码。为啥要有这个东西?

  1. 是因为这段代码访问了“临界资源”,而这种资源只能同时被互斥地访问。举个例子来说,你的银行账户就是一个互斥资源,一个银行系统里面改变余额(存取)的操作代码就必须用在临界区内。如果你的账户余额是$100,000(如果是真的,那么你就不用再往下看了,还是睡觉去吧),假设有两个人同时给你汇款$50,000。有两个线程分别执行这两笔汇款业务,线程A在获取了你的账户余额后,在它把新余额($150000)储存回数据库以前,操作系统把这个线程暂停转而把CPU的时间片分给另一个线程(是的,这太巧了);那么线程B此时取出的账户余额仍然是$10000,随后线程B幸运的得到的CPU时间把$50000存入你的账户,那么余额变成$150000。而此后某个时候,线程A再次得以执行,它也把“新”余额$150000更新到系统……于是你的$50000就这么凭空消失了。(此段省去常见到一个示例图,请自行想象)
  2. 是因为OS的多任务调度,其实在原因一里面已经提到。如果OS不支持多任务调度,那么线程A/线程B执行更新余额的操作总是一个接一个进行,那么完全不会有上面的问题了。在多线程的世界里,你必须随时做好你的代码执行过程随时失去控制的准备;你需要好好考虑当代码重新执行的时候,是否可以继续正确的执行。一句话,你的程序段在多线程的世界里,你所写的方法并不是“原子性”的操作。

Lock关键字

  C#提供lock关键字实现临界区,MSDN里给出的用法:

Object thisLock = new Object();
lock (thisLock)
{
   // Critical code section
}

  lock实现临界区是通过“对象锁”的方式,注意是“对象”,所以你只能锁定一个引用类型而不能锁定一个值类型。第一个执行该代码的线程,成功获取对这个对象的锁定,进而进入临界区执行代码。而其它线程在进入临界区前也会请求该锁,如果此时第一个线程没有退出临界区,对该对象的锁定并没有解除,那么当前线程会被阻塞,等待对象被释放。

  既然如此,在使用lock时,要注意不同线程是否使用同一个“锁”作为lock的对象。现在回头来看MSDN的这段代码似乎很容易让人误解,容易让人联想到这段代码是在某个方法中存在,以为thisLock是一个局部变量,而局部变量的生命周期是在这个方法内部,所以当不同线程调用这个方法的时候,他们分别请求了不同的局部变量作为锁,那么他们都可以分别进入临界区执行代码。因此在MSDN随后真正的示例中,thisLock实际上是一个private的类成员变量:

using System;
using System.Threading;

class Account
{
    private Object thisLock = new Object();
    int balance;

    Random r = new Random();

    public Account(int initial)
    {
        balance = initial;
    }

    int Withdraw(int amount)
    {

        // This condition will never be true unless the lock statement
        // is commented out:
        if (balance < 0)
        {
            throw new Exception("Negative Balance");
        }

        // Comment out the next line to see the effect of leaving out
        // the lock keyword:
        lock(thisLock)
        {
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Amount to Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
                return amount;
            }
            else
            {
                return 0; // transaction rejected
            }
        }
    }

    public void DoTransactions()
    {
        for (int i = 0; i < 100; i++)
        {
            Withdraw(r.Next(1, 100));
        }
    }
}

class Test
{
    static void Main()
    {
        Thread[] threads = new Thread[10];
        Account acc = new Account(1000);
        for (int i = 0; i < 10; i++)
        {
            Thread t = new Thread(new ThreadStart(acc.DoTransactions));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
    }
}

  这个例子中,Account对象只有一个,所以临界区所请求的“锁”是唯一的,因此用类的成员变量是可以实现互斥意图的,其实用大家通常喜欢的lock(this)也未尝不可,也即请求这个Account实例本身作为锁。但是如果在某种情况你的类实例并不唯一或者一个类的几个方法之间都必须要互斥,那么就要小心了。必须牢记一点,所有因为同一互斥资源而需要互斥的操作,必须请求“同一把锁”才有效。

  假设这个Account类并不只有一个Withdraw方法修改balance,而是用Withdraw()来特定执行取款操作,另有一个Deposit()方法专门执行存款操作。很显然这两个方法必须是互斥执行的,所以这两个方法中所用到的锁也必须一致;不能一个用thisLock,另一个重新用一个private Object thisLock1 = new Object()。再进一步,其实这个操作场景下各个互斥区存在的目的是因为有“Balance”这个互斥资源,所有有关Balance的地方应该都是互斥的(如果你不介意读取操作读到的是脏数据的话,当然也可以不用)。

题外话:
  
这么看来其实用Balance本身作为锁也许更为符合“逻辑”,lock住需要互斥的资源本身不是更好理解么?不过这里Balance是一个值类型,你并不能直接对它lock(你可能需要用到volatile关键字,它能在单CPU的情况下确保只有一个线程修改一个变量)。

Lock使用的建议

  关于使用Lock微软给出的一些建议。你能够在MSDN上找到这么一段话:

  通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:
  1.如果实例可以被公共访问,将出现 lock (this) 问题。
  2.如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
  3.由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock("myLock") 问题。
  4.最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。

  lock(this)的问题我是这么理解:

  1. 处于某种原因Account在整个程序空间内不是唯一,那么不同Account实例的相应方法就不可能互斥,因为他们请求的是不同Accout实例内部的不同的锁。这时候微软示例中的private Object thisLock仍然也避免不了这个问题,而需要使用private static Object thisLock来解决问题,因为static变量是所有类实例共享的。
  2. 猜想就算Account只有一个实例,但是如果在程序内部被多个处理不同任务的线程访问,那么Account实例可能会被某段代码直接作为锁锁定;这相当于你自己锁定了自己,而别人在不告诉你的情况下也可以能锁定你。这些情况都是你在写Account这个类的时候并没有办法作出预测的,所以你的Withdraw代码可能被挂起,在多线程的复杂情况下也容易造成死锁。不管怎样,你写这段代码的时候肯定不会期待外部的代码跟你使用了同一把锁吧?这样很危险。另外,从面向对象来说,这等于把方法内部的东西隐式的暴露出去。为了实现互斥,专门建立不依赖系this的代码机制总是好的;thisLock,专事专用,是个好习惯。

  MyType的问题跟lock(this)差不多理解,不过比lock(this)更严重。因为Lock(typeof(MyType))锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类对象(就是拥有Static成员的那个对象实例),锁定它就锁定了该对象的所有实例。同时lock(typeof(MyType))是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们都有可能锁定类对象,完全阻止你代码的执行,导致你自己代码的挂起或者死锁。

  至于lock("myLock"),是因为在.NET中字符串会被暂时存放。如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。

.NET集合类对lock的支持

  在多线程环境中,常会碰到的互斥资源应该就是一些容器/集合。因此.NET在一些集合类中(比如ArrayList,HashTable,Queue,Stack,包括新增的支持泛型的List)已经提供了一个供lock使用的对象SyncRoot。

  在.Net1.1中大多数集合类的SyncRoot属性只有一行代码:return this,这样和lock(集合的当前实例)是一样的。不过ArrayList中的SyncRoot有所不同(这个并不是我反编译的,我并没有验证这个说法):

get

  if(this._syncRoot==null)
  {
    Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
  }
  returnthis._syncRoot;
}

题外话:
  上面反编译的ArrayList的代码,引出了个Interlocked类,即互锁操作,用以对某个内存位置执行的简单原子操作。举例来说在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:

  1. 将实例变量中的值加载到寄存器中。
  2. 增加或减少该值。
  3. 在实例变量中存储该值。

  线程可能会在执行完前两个步骤后被夺走CPU时间,然后由另一个线程执行所有三个步骤。当第一个线程重新再开始执行时,它改写实例变量中的值,造成第二个线程执行增减操作的结果丢失。这根我们上面提到的银行账户余额的例子是一个道理,不过是更微观上的体现。我们使用该类提供了的Increment和Decrement方法就可以避免这个问题。
  另外,Interlocked类上提供了其它一些能保证对相关变量的操作是原子性的方法。如Exchange()可以保证指定变量的值交换操作的原子性,Read()保证在32位操作系统中对64位变量的原子读取。而这里使用的CompareExchange方法组合了两个操作:保证了比较和交换操作按原子操作执行。此例中CompareExchange方法将当前syncRoot和null做比较,如果相等,就用new object()替换SyncRoot。
  在现代处理器中,Interlocked 类的方法经常可以由单个指令来实现,因此它们的执行性能非常高。虽然Interlocked没有直接提供锁定或者发送信号的能力,但是你可以用它编写锁和信号,从而编写出高效的非阻止并发的应用程序。但是这需要复杂的低级别编程能力,因此大多数情况下使用lock或其它简单锁是更好的选择。

 

 

 

  看到这里是不是已经想给微软一耳光了?一边教导大家不要用lock(this),一边竟然在基础类库中大量使用……呵呵,我只能说据传从.Net2.0开始SyncRoot已经是会返回一个单独的类了,想来大约应该跟ArrayList那种实现差不多,有兴趣的可以反编译验证下。

  这里想说,代码是自己的写的,最好减少自己代码对外部环境的依赖,事实证明即便是.Net基础库也不是那么可靠。自己能想到的问题,最好自己写代码去处理,需要锁就自己声明一个锁;不再需要一个资源那么自己代码去Dispose掉(如果是实现IDisposable接口的)……不要想着什么东西系统已经帮你做了。你永远无法保证你的类将会在什么环境下被使用,你也无法预见到下一版的Framework是否偷偷改变了实现。当你代码莫名其妙不Work的时候,你是很难找出由这些问题引发的麻烦。只有你代码足够的独立(这里没有探讨代码耦合度的问题),才能保证它足够的健壮;别人代码的修改(哪怕是你看来“不当”的修改),造成你的Code无法工作不是总有些可笑么(我还想说“苍蝇不叮无缝的蛋”“不要因为别人的错误连累自己”)?

  一些集合类中还有一个方法是和同步相关的:Synchronized,该方法返回一个集合的内部类,该类是线程安全的,因为他的大部分方法都用lock来进行了同步处理(你会不会想那么SyncRoot显得多余?别急。)。比如,Add方法会类似于:

public override void Add(objectkey,objectvalue) 

  lock(this._table.SyncRoot)
  {
    this._table.Add(key,value);
  } 
}

  不过即便是这个Synchronized集合,在对它进行遍历时,仍然不是一个线程安全的过程。当你遍历它时,其他线程仍可以修改该它(Add、Remove),可能会导致诸如下标越界之类的异常;就算不出错,你也可能读到脏数据。若要在遍历过程中保证线程安全,还必须在整个遍历过程中锁定集合,我想这才是SynRoot存在的目的吧:

Queue myCollection = newQueue();
lock(myCollection.SyncRoot)
{
  foreach(ObjectiteminmyCollection)
  { 
    //Insert your code here.
  } 
}

  提供SynRoot是为了把这个已经“线程安全”的集合内部所使用的“锁”暴露给你,让你和它内部的操作使用同一把锁,这样才能保证在遍历过程互斥掉其它操作,保证你在遍历的同时没有可以修改。另一个可以替代的方法,是使用集合上提供的静态ReadOnly()方法,来返回一个只读的集合,并对它进行遍历,这个返回的只读集合是线程安全的。

  到这里似乎关于集合同步的方法似乎已经比较清楚了,不过如果你是一个很迷信MS基础类库的人,那么这次恐怕又会失望了。微软决定所有从那些自Framwork 3.0以来加入的支持泛型的集合中,如List,取消掉创建同步包装器的能力,也就是它们不再有Synchronized,IsSynchronized也总会返回false;而ReadOnly这个静态方法也变为名为AsReadOnly的实例方法。作为替代,MS建议你仍然使用lock关键字来锁定整个集合。

  至于List之类的泛型集合SyncRoot是怎样实现的,MSDN是这样描述的“在 List<(Of <(T>)>) 的默认实现中,此属性始终返回当前实例。”,赶紧去吐血吧!

自己的SyncRoot

还是上面提过的老话,靠自己,以不变应万变:

public class MySynchronizedList
{
  private readonly object syncRoot = new object();
  private readonly List<intlist = new List<int>();

  public object SyncRoot
  {
    get{return this.syncRoot;}
  }

  public void Add(int i)
  {
    lock(syncRoot)
    {
      list.Add(i);
    }
  }

  //...
}

自已写一个类,用自己的syncRoot封装一个线程安全的容器。

转载于:https://www.cnblogs.com/huoguofeng/archive/2013/04/03/2997495.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/261844.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python atm银行取款系统_Python实现ATM系统

今天偶尔在知乎上看到某大佬用Python写的ATM系统案例&#xff0c;然后观摩了下他的实现思路和源码&#xff0c;感觉受益颇多&#xff0c;于是就根据自己的思路和目前掌握的Python编程基础将ATM实现了一下&#xff0c;以下是案例解析的过程&#xff1a;案例剖析&#xff1a;1.at…

【摘录】BREW应用的c++实现注意点

BREW应用的c实现注意点 从VC6.0的调试器来说吧&#xff0c;肯定支持C语言了&#xff0c;对于ARM或者GCC来说&#xff0c;也是有可用的C编译器&#xff0c;而且任何支持BREW的手机都可以运行通过ARM或者GCC编译连接出来的目标代码&#xff0c;所以从环境来说&#xff0c;BREW开发…

MapXtreme 包含所有自带坐标系一览

CoordSys 对象包含关于 X 和 Y 坐标如何与其在 Earth 上的位置相关联的基本信息。 每个 Geometry 或 Map 对象都有一个关联的坐标系。 CoordSys 对象包含对坐标系的详细说明。 CoordSysFactory 类提供了各种用于创建不同 CoordSys 对象的方法。 所有 CoordSys 对象都是只读的&a…

html4的语法,HTML——语法

文章目录页面结构一个标准的HTML页面如下&#xff1a;Document一、文档声明头标准的HTML页面&#xff0c;第一行是以开头的的语句&#xff0c;这就是文档声明头&#xff0c;即DocType Declaration&#xff0c;简称DTD。DTD可以告知浏览器使用哪种HTML或者XHTML规范。二、页面语…

如何学习streamdecoder类_2019年终巨献:一份拿下了阿里、网易、滴滴等大厂offer的学习笔记...

2019仅剩最后二十天&#xff0c;回顾今年初遇“寒冬”时&#xff0c;自己也挺慌的&#xff0c;但是经历过这么多次面试后&#xff0c;我才“醒悟”&#xff0c;所谓的“寒冬”&#xff0c;“冻死”的都是“衣服穿的少的”。年末了在这里做一个年度总结&#xff0c;今年面试了不…

自定义控件的构建(12)

Share 前面讲了模板的构建&#xff0c;我们忽略了一个细节&#xff0c;如果接触ASP.NET时间不长的话&#xff0c;一般都会看到数据表达式是<%#Eval(‘Name’)%>这种形式的&#xff0c; 那么我们为什么用<%#Container.Name%>这种形式呢&#xff0c;其实前者是ASP.NE…

矩阵快速幂 zoj-3690 Choosing number

题目链接&#xff1a; http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId4973 题目意思&#xff1a; 有n个人&#xff0c;有1——m个数&#xff0c;每个人可以选择1个数&#xff0c;要求相邻的两个人如果选的数相同则必须大于k,求选数的种数。 解题思路&#xff1a…

insertAfter()

<div id"b">bbbbbbbbb</div> <div>dddddd</div> JavaScript window.οnlοadfunction(){var a document.createElement("span");var b document.createTextNode("cssrain");a.appendChild(b);var mubiao document.getE…

python def 参数一直为false_在Python 3中,如果参数为False,则查找惯用的方法来评估为False...

我有一连串的功能,全部定义在课程其他地方&#xff1a;fus(roh(dah(inp)))其中inp是字典或bool(False).期望的结果是,如果inp或任何函数评估为False,则False由函数堆返回.我试图使用三元运算符,但是它们不能正确评估.def func(inp):return int(inp[value]) 1 if inp else Fals…

复试计算机网络与软件工程,2018华南理工大学软件工程复试经验贴

尘埃落定&#xff0c;昨天结束了复试&#xff0c;终于被拟录取了&#xff0c;只有真正经历过才知道不容易&#xff0c;一年来受到王道的很多帮助&#xff0c;论坛上软件工程的信息相对来说还是比较少的&#xff0c;所以也想分享下自己的一些经验&#xff0c;帮助后来人。本人普…

学计算机等级考试电脑版软件,计算机二级考试宝典电脑版

计算机二级考试宝典电脑版是一款专业的二级计算机内容学习软件。该软件由武汉大学团队真情研发&#xff0c;软件包含选择题1600道&#xff0c;非选择题109套&#xff0c;成功实现了考点和重点的全面覆盖式学习目的&#xff0c;对学生们学习起到了巨大的帮助。该版本是通过安卓模…

mysq进阶

学习资料&#xff1a; 官方文档&#xff1a;http://dev.mysql.com/doc/refman/5.0/en/tutorial.html 1.存储过程&#xff1a; 优点&#xff1a;业务逻辑封装在存储过程中&#xff0c;容易维护&#xff0c;执行效率也高。 缺点&#xff1a;不同的数据库功能函数等不一样&#xf…

归纳整理--第4篇--常用软件

CSDN博客不再经常更新&#xff0c;更多优质文章请来 粉丝联盟网 FansUnion.cn! (FansUnion) 随着软硬件设施的提高和网民网络生活的丰富&#xff0c;电脑上的软件越来越多。对于一个专业开发者来说&#xff0c;尤其如此。常用软件主要分2类。1.娱乐休闲类。普通网民使用的一些软…

获取主机的信息

BOOL GetLocalHostInfo(){   //得到主机名称   int nComputerNameLen;   nComputerNameLen MAX_COMPUTERNAME_LENGTH 1;   if(SOCKET_ERROR gethostname(m_chLocalHostName,nComputerNameLen))     return FALSE;   ///end//   ///得到主机IP地址   HOST…

python数据动画_[转载]Maya使用Python获取动画每帧的rotation数据

import maya.cmds asmcimport os#添加一个"Maya动画收集数据"窗口设置动画开始播放#win mc.window(title "Maya动画收集数据", w 400, h 40)#mc.frameLayout( lvFalse )#mc.playbackOptions( minTime0, maxTime20 )mc.select("jamie_arm_R",…

HTML使用vue的 event,vue-js 特殊变量$event常识

背景如果我们要阻止默认事件&#xff0c;在 chrome 等浏览器中&#xff0c;我们可能要写一个&#xff1a;event.preventDefault();而在 IE 中&#xff0c;我们则需要写&#xff1a;event.returnValue false;jquery &#xff0c;跨浏览器的实现&#xff0c;我们统一只需要写&am…

创建非矩形的Windows 窗体

创建非矩形窗体的过程包含三个步骤&#xff1a;• 创建一个作为窗体图面的位图。&#xff08;一种有效的方式是&#xff0c;您可以从矩形中“裁剪掉”所需的窗体形状。&#xff09;• 创建Windows 应用程序项目&#xff0c;将其属性设置为移除标题栏并使用位图作为窗体背景。•…

python3装饰器例子_python 装饰器(三):装饰器实例(一)

示例 7-15 定义了一个装饰器&#xff0c;它会在每次调用被装饰的函数时计时&#xff0c;然后把经过的时间、传入的参数和调用的结果打印出来。示例 7-15 一个简单的装饰器&#xff0c;输出函数的运行时间importtimedefclock(func):def clocked(*args): #➊t0 time.perf_counte…

《c专家编程》笔记--define和typedef的区别

#include <stdio.h> #define peach int typedef int banana;int main(void){unsigned peach a;unsigned banana b;return 0; } 上面的代码中&#xff0c; unsigned banana b; 该行编译会出现错误。 因为typedef是一种彻底的“封装”类型&#xff0c;而#define只是简单的文…

ExtJs6 Desktop Demo 修改测试

一直用Extjs4&#xff0c; extjs6 的变化较大&#xff0c;这几天有兴趣研究一下&#xff0c;把自带的Demo的desktop做了些修改&#xff0c; 1.首先下载安装sencha cmd 2.然后 需要生成新项目 用sencha cmd 命令如下&#xff1a; sencha -sdk E:\ext-6.0.0 generate app linb…