精华内容
下载资源
问答
  • Windows临界区简单实现

    千次阅读 2017-04-22 18:10:03
    最近看的书中介绍了线程临界区同步方式,思考之前写过的内存释放相关结构便动手实现一个简易的临界区(支持多个线程同时使用)。关于结构设计的几点介绍: 1.设计采用了先进先出概念,当第一个线程进入临界区后,别...

      最近看的书中介绍了线程临界区同步方式,思考之前写过的内存释放相关结构便动手实现一个简易的临界区(支持多个线程同时使用)。

    关于结构设计的几点介绍:
      1.设计采用了先进先出概念,当第一个线程进入临界区后,别的线程需要在临界区等待,临界区释放后下一个线程才能进入临界区;
      2.设计采用了递增进入方式,比如第一个线程为0顺序,第二个线程为1顺序等等以此类推;
      3.设计采用了轻量级参数,结构安全性比较高,不会出现崩溃情况(按正常临界区方式操作,每次线程退出前需要释放临界区,防止造成死锁)。

    函数定义如下:

    void TestInitializeCriticalSection(pTestCritical obj);/* 初始化函数实际用处不大稳定考虑在构造函数已经初始化,可以不用 */
    
    void TestEnterCriticalSection(pTestCritical obj);//加载临界区
    
    void TestLeaveCriticalSection(pTestCritical obj);//释放临界区
    
    typedef struct TestCritical//临界区结构体
    {
    	TestCritical():nCritical(0),nCount(0)
    	{
    	}
    	volatile int nCritical;/* 临界区运行标志 ,0表示没有进入临界区 ,1 表示进入临界区 */
    	volatile int nCount;/* 临界区等待进入的顺序计数 */
    }*pTestCritical;
    

    代码实现如下:

    void TestInitializeCriticalSection(pTestCritical obj)/* 初始化函数实际用处不大稳定考虑在构造函数已经初始化,可以不用 */
    {
    	while (1 == obj->nCritical) /* 如果临界区已经运行那么初始化失败 */
    		return ;
    	obj->nCritical = 0;
    }
    
    void TestEnterCriticalSection(pTestCritical obj)//加载临界区
    {
    	int nCount = obj->nCount;/*获取到等待顺序,如果为0为第一个等待进入临界区*/
    	obj->nCount++;//这里说明下多个线程情况下可能会造成nCount增加慢了一步,不过没有影响 数据一切正常
    	while (0 != obj->nCritical || (0 != nCount && nCount != obj->nCount))
    		;
    	obj->nCritical = 1;
    
    }
    
    void TestLeaveCriticalSection(pTestCritical obj)//释放临界区
    {
    	if (0 == obj->nCritical)
    	{
    		return;
    	}
    	obj->nCritical = 0;
    	obj->nCount--;
    }
    

    使用方式(测试方法):

    	AfxBeginThread(TestCriticalThread,this);
    	AfxBeginThread(TestCriticalThread2,this);
    	AfxBeginThread(TestCriticalThread,this);
    
    	UINT TestCriticalThread(LPVOID _this)
    {
    	CTestSTLFileDlg* testDlg = (CTestSTLFileDlg*)_this;
    	TestEnterCriticalSection(&test);
    	Sleep(100);
    	for (int i = 0;i < 10;i++)
    		testDlg->TestData[i] = i;
    	TestLeaveCriticalSection(&test);
    	return 0;
    }
    
    UINT TestCriticalThread2(LPVOID _this)
    {
    	CTestSTLFileDlg* testDlg = (CTestSTLFileDlg*)_this;
    	TestEnterCriticalSection(&test);
    	for (int i = 10;i < 20;i++)
    		testDlg->TestData[i] = i;
    	TestLeaveCriticalSection(&test);
    	return 0;
    }	
    

    最近和朋友讨论了一下关与原子量以及计数器的执行原理,应该是通过对硬件的直接操作(比如调用中断达到硬件上的支持)完成计数器的自增或者自减的完成。

    展开全文
  • 预备知识:线程的相关概念和知识,有多线程编码的初步经验。  一个机会,索性把线程同步的问题在C#里面的东西都粗略看了下。  第一印象,C#关于线程同步的...临界区跟Java差不多只不过关键字用lock替代了sync...

    文章原始出处 http://xxinside.blogbus.com/logs/46441956.html

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

      一个机会,索性把线程同步的问题在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(ref this._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封装一个线程安全的容器。

    展开全文
  • 临界区模式是其它多线程编程模式的基础,所以在这里继续深入一下谈谈synchronized相关的一些内容.只要见到synchronized关键字,第一要想到的问题就是,synchronized在保护谁?在上面的例子中,synchronized保护的是...

    [高级主题:关于synchronized]

    其实在多线程编程基础部分,我已经谈过synchronized相关的内容.但临界区模式是其它多线程编程模式的基
    础,所以在这里继续深入一下谈谈synchronized相关的一些内容.

    只要见到synchronized关键字,第一要想到的问题就是,synchronized在保护谁?

    在上面的例子中,synchronized保护的是Corrie对象的counter,name,number三个字段不被"交叉赋值",

    也就是这三个字段同一时间只能被一个线程访问.


    其次我们要考虑的问题是:这些对象都被妥善地保护了吗?

    这是非常重要的问题.无论你花巨资打造一把高安全性锁,把自己的家门牢牢地锁住,可是你却把门旁边的窗子
    敞开着,那么你花巨资打造的锁又要什么意义呢?所以要确保从任何一个通道访问被保护的对象都被加锁控制
    的,比如字段是否都private或protected的,对于protected的子类中的扩展方法是否能保护被保护对象.

    对于上面的例子因为display有可能被外面的方法单独调用,所以它也必须是同步的.而test方法只会在into中
    调用,简单说它只是所有通道被加了锁的大房子中的一个小单元,所以不必担心有人会从外部访问它.

    要注意保护的范围是三个同时需要保护的字段,如果它们被分别放在synchronized方法中保护,并不能保证它们
    本个字段同时只有一个线程访问.

     

    那么我们就有一个问题,获取谁的锁呢?

     

    要保护一个对象,当然直接获取这个对象的锁,我们上面的例子可以理解为要同时保护三个对象,那么其实就是
    要保护这三个对象的所在的容器.也就是它们所在的实例.如果不相关的三个对象要同时保护,一定要放在同时容纳
    它们的容器中,否则无法同时保护它们的状态.对于上面的例子我们同样可以理解为要保护的是Corrie的实例,
    因为这个实例是这三个字段的容器.所以我们用synchronized方法就是等同于synchronized(this){.......}
    如果这个游戏中有多个山洞,而只有一块显示牌,那以我们就需要保护多个实例的三个字段同时只被一个线程
    访问,我们就需要synchronized(Corrie.class)来保证多个实例被多个线程访问时只有一个对程能同时对三个
    字段访问.


    所以获取谁的锁定也是一个很重要的问题,如果你选择了错误的对象,就象你花巨资打了一把锁却锁了别人的
    门.

     

     

    synchronized就是原子操作,简单说在一个线程进行同步块中的代码时其它线程不能进入,这是很明显的.但同时,

    多个同步方法或多个获取同一对象的同步块在同一时候也只能一个线程能访问其中之一,因为控制谁能访问的是要
    获得那个同步对象的锁.如:
    class C{
     synchronized  void a(){}
     synchronized  void b(){}
    }

    当一个线程进入同步方法a后那么其它线程当然不能进入a,同时也不能进入b,因为能进入的条件是获取this对
    象的锁.一个结程进入a后this对象的锁被这个线程获取,其它线程进入b也同样要获取这个锁,而不仅仅是进入
    a要获取这个锁.这一点一定要理解.

    如果我们想所有线程只能有一个线程在同一时间访问a(),而这时也可以只有另一个线程可以访问b();就是a()和

    b()身本都是同步的,但a()和b()之间不产生互斥相,那么就要为a()和b()分别创建用户于同步的对象,这样的

    对象我们习惯叫虚拟锁。

    class C{

     Object aL = new Object();
     Object bL = new Object();

     void a(){

         synchronized (aL){......} }
     void b(){

         synchronized (bL){......}

    }
    }

     

    理解上面的知识我们再回过头来看原子操作.

    JLS规定对于基本类型(除long和double)以外的赋值和引用都是原子操作,并且对于引用类型的赋值和引用也是
    原子操作.

    注意这里有两个方面的知识点:

    1.对于long和double的操作非原子性的.需要说明这只是JLS的规定,但大多数JVM的实现其实已经保证了long和
    double的赋值和引用也是原子性的,只是允许某种实现可以不是原子性的操作.

    对于其它基本类型如int,如果一个线程执行x = 1;另一个线程执行x = 2;
    由于可见性的问题(多线程编程系统中已经介绍),x要么就是1,要么就是2,看谁先同步到主存储区.

    但对于long,l = 1;l = 2;分别由两个线程执行的结果有可能不是你想象的,它们有可能是0,或1,或2,或一个其
    它的随机数,简单说两上线程中l的值的部分bit位可能被另一个线程改写.所以最可靠的是放在synchronized中
    或用volatile 保护.当然这里说的是有“非常可靠”的需要。

    2.我们看到,对于引用对象的赋值和引用也是原子的.

    我们还是看javaworld上dcl的例子.

     那个错误的例子误了好多人,(JAVA与模式的作者就是受害人),我们先不说JAVA内存模型的原因(前面我已经从
    JAVA内存模型上说明了那个例子是错误的,我是说对那个例子的分析是错误的).单从对于"引用对象的赋值和引
    用也是原子的"这句话,就知道对于引用字段的赋值,绝对不可能出现先分配空间,然后再还没有被始化或还没有
    调构造方法之前又被别的线程引用.因为当一个线程在执行赋值的时候是原子性的操作,其它线程的引用操作也是

    原子性的操作 的,在赋值操作没有完成之前其它线程根本不可能见到"分配了空间却没有初始化或没有调用构造方法"

    的这个对象.

    不知道什么原因,这样的一个例子从它诞生开始竟然是所有人都相信了,也许有人责疑过但我不知道.如果你有足
    够的基础知识,就不必跟着别人的感觉走!

    因为这是一个最最基础的模式,暂时不介绍它与其它模式的关系.在以后介绍其它模式时反过来再和它进行比较.

    而一些复杂的模式都是在这个简单的模式的基础上延伸的.

     

     

     

    展开全文
  • 预备知识:线程的相关概念和知识,有多线程编码的初步经验。  一个机会,索性把线程同步的...临界区跟Java差不多只不过关键字用lock替代了synchronized,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,另外

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

        一个机会,索性把线程同步的问题在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封装一个线程安全的容器。

    原文地址:http://www.blogbus.com/xxinside-logs/46441956.html
    展开全文
  • 关于临界区使用在前面一篇文章《C++多线程编程简单实例》中略有提及,此篇转摘的文章更为详细一些。 Original URL: http://beyondjhf-2008.iteye.com/blog/679141 困扰了我很长时间的多线程
  • 预备知识:线程的相关概念和知识,有多线程编码的初步经验。  一个机会,索性把线程...临界区跟Java差不多只不过关键字用lock替代了synchronized,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,另外...
  • 参考书籍:《实战JAVA高并发程序设计》本文仅用于自己参考 一、概念 ...但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个资源就必须等待。 阻塞(Blocking)和非...
  • 资源和临界资源有什么区别?临界区是不是可以是用户代码? 资源和临界资源有什么区别?临界区是不是可以是用户代码?
  • 今早在调试自己手头一个项目的时候,突然发现它不... 临界区的使用详解,请参考网上相关资料,或本人前面的一篇关于临界区的日志。该项目主要代码如下:// DetectService.hclass CDetectService...{public: CDetect
  • 关于消息循环

    2016-12-28 11:16:32
    最近尝试在多线程里使用消息队列代替事件和临界区 原因很简单,内核对象太多维护复杂,虽然处理消息队列的逻辑也很复杂,但相对维护一堆内核对象句柄要容易得多 而且,不影响执行效率(消息队列自动销毁,创建只要调用一次...
  • 并发编程的原则和技巧 单一职责原则 分离并发相关代码和其他代码...两个线程修改共享对象的同一字段时可能会相互干扰,导致不可预期的行为,解决方案之一是构造临界区,但是必须限制临界区的数量。 使...
  • 关于Python的线程

    千次阅读 2006-03-01 11:26:00
     举两个简单的例子希望起到抛砖引玉的作用,关于多线程编程的其他知识例如互斥、信号量、临界区等请参考python的文档及相关资料。 1、调用thread模块中的start_new_thread()函数来产生新的线程,
  • Shopee春招测试岗笔试题

    千次阅读 2020-04-15 17:15:29
    1、若系统中有五个并发进程涉及某个相同的变量A,则变量A的相关临界区是由( )临界区构成。 5个(每个进程都需要有相关的临界区) 2、关于TCP状态 LISTEN:侦听来自远方的TCPport的连接请求 SYN-SENT:再发送连接请求...
  • 临界区 进程中一段需要访问共享资源的代码,共享资源一次只能允许一个进程访问,系统要保证进入临界区的进程不超过一个。 正常执行可以并行,临界区代码执行只能串行。 解决并发问题的核心:控制对共享资源的访问 ...
  • 操作系统进程管理-同步和互斥 在看了操作系统关于进程管理中的同步互斥机制章节之后,甚是困惑,今天通过视频、网上...1.临界区相关概念: 临界资源:也就是一次只允许一个进程操作使用的计算机资源,这里的资源可...
  • java锁总结 (持续更新)

    2019-09-03 23:21:11
    在 Java 中主要2种加锁机制: synchronized 关键字 java.util.concurrent.Lock (Lock是一个接口,ReentrantLock是该...synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时...
  • 六、信号量关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程才能执行临界区的代码;不同的是获取不到信号量时,进程不会原地打转而是进入休眠等待状态。它的定义是include\linux\semaphore.h...
  • 六、信号量关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程才能执行临界区的代码;不同的是获取不到信号量时,进程不会原地打转而是进入休眠等待状态。它的定义是include\linux\semaphore.h...
  • 关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程才能执行临界区的代码;不同的是获取不到信号量时,进程不会原地打转而是进入休眠等待状态。它的定义是include\linux\semaphore.h文件中,...
  • 多线程间的通信和同步

    千次阅读 2019-06-15 11:21:07
    最近看了很多关于网络编程和多线程的书,为了以后查看相关内容方便,整理了几本书的精华形成这篇博文,希望能...(二)临界区 critical section (三)信号量 semaphore (四)事件 event 一、什么是多线程? ...

空空如也

空空如也

1 2 3 4
收藏数 67
精华内容 26
关键字:

关于相关临界区