posts - 1, comments - 45, trackbacks - 5, articles - 1
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理
    最近由于在准备Collection对象培训的PPT,因为涉及到SyncRoot的属性的讲解,所以对怎样在多线程应用程序中同步资源访做了个总结:
对于引用类型和非线程安全的资源的同步处理,有四种相关处理:lock关键字,监视器(Monitor), 事件和等待句 mutex类。
Lock关键字
    本人愚钝,在以前编程中遇到lock的问题总是使用lock(this)一锁了之,出问题后翻看MSDN突然发现下面几行字:通常,避免 public 型,否则实例将超出代的控制范。常 lock (this)lock (typeof (MyType)) lock ("myLock") 反此准如果例可以被公共访问,将出 lock (this) 问题如果 MyType 可以被公共访问,将出 lock (typeof (MyType)) 问题由于程中使用同一字符串的任何其他代将共享同一个,所以出 lock(“myLock”) 问题。lock(this)问题:如果有一个类Class1,该类有一个方法用lock(this)来实现互斥:
public void Method2()
        {
            
lock (this)
            {
                System.Windows.Forms.MessageBox.Show(
"Method2 End");
            }
        }
    如果在同一个Class1的实例中,Method2能够互斥的执行。但是如果是2Class1的实例分别来执行Method2,是没有互斥效果的。因为这里的lock,只是对当前的实例对象进行了加锁。
    Lock(typeof(MyType))锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类型对象(该对象是typeof的返回结果),锁定它,就锁定了该对象的所有实例,微软现在建议(原文请参考:http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/SDaskgui06032003.mspx?mfr=true)不要使用lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。
    锁住一个字符串更为神奇,只要字符串内容相同,就能引起程序挂起。原因是在.NET中,字符串会被暂时存放,如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。到此,微软给出了个lock的建议用法:锁定一个私有的static 成员变量。
      .NET在一些集合类中(比如ArrayList,HashTableQueueStack)已经提供了一个供lock使用的对象SyncRoot,用Reflector工具查看了SyncRoot属性的代码,在Array中,该属性只有一句话:return this,这样和lock array的当前实例是一样的。ArrayList中的SyncRoot有所不同
 get
    {
        
if (this._syncRoot == null)
        {
            Interlocked.CompareExchange(
ref this._syncRoot, new object(), null);
        }
        
return this._syncRoot;
   其中Interlocked类是专门为多个线程共享的变量提供原子操作(如果你想锁定的对象是基本数据类型,那么请使用这个类),CompareExchange方法将当前syncRootnull做比较,如果相等,就替换成new object(),这样做是为了保证多个线程在使用syncRoot时是线程安全的。集合类中还有一个方法是和同步相关的:Synchronized,该方法返回一个对应的集合类的wrapper类,该类是线程安全的,因为他的大部分方法都用lock来进行了同步处理,比如Add方法:
public override void Add(object key, object value)
{
    
lock (this._table.SyncRoot)
    {
        
this._table.Add(key, value);
    }
}
    这里要特别注意的是MSDN提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合:
Queue myCollection = new Queue();
  
lock(myCollection.SyncRoot) {
  
foreach (Object item in myCollection) {
  
// Insert your code here.
  }
 }
Monitor
该类功效和lock类似:
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}
    lock关键字比Monitor简洁,其实lock就是对MonitorEnterExit的一个封装。另外Monitor还有几个常用的方法:TryEnter能够有效的决绝长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间bool gotLock = Monitor.TryEntermyobject,1000,让当前线程在等待1000秒后根据返回的bool值来决定是否继续下面的操作。Pulse以及PulseAll还有Wait方法是成对使用的,它们能让你更精确的控制线程之间的并发,MSDN关于这3个方法的解释很含糊,有必要用一个具体的例子来说明一下:
using System.Threading;
public class Program {
   
static object ball = new object();
   
public static void Main() {
      Thread threadPing 
= new Thread( ThreadPingProc );
      Thread threadPong 
= new Thread( ThreadPongProc );
      threadPing.Start(); threadPong.Start();
      }
   
static void ThreadPongProc() {
      System.Console.WriteLine(
"ThreadPong: Hello!");
      
lock ( ball )
         
for (int i = 0; i < 5; i++){
            System.Console.WriteLine(
"ThreadPong: Pong ");
            Monitor.Pulse( ball );
            Monitor.Wait( ball );
         }
      System.Console.WriteLine(
"ThreadPong: Bye!");
   }
   
static void ThreadPingProc() {
      System.Console.WriteLine(
"ThreadPing: Hello!");
      
lock ( ball )
         
for(int i=0; i< 5; i++){
            System.Console.WriteLine(
"ThreadPing: Ping ");
            Monitor.Pulse( ball );
            Monitor.Wait( ball );
         }
      System.Console.WriteLine(
"ThreadPing: Bye!");
   }
}
   执行结果如下(有可能是ThreadPong先执行):
ThreadPing: Hello!
ThreadPing: Ping
ThreadPong: Hello
!
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Bye
!
   当threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball );后,它通知threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball )阻塞自己后,它放弃了了对ball的锁定,所以threadPong得以执行。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进程发送通知信号,如果只有一个线程被阻塞,那么请使用Pulse方法。
同步事件和等待句柄
   同步事件和等待句柄用于解决更复杂的同步情况,比如一个一个大的计算步骤包含3个步骤result = first term + second term + third term,如果现在想写个多线程程序,同时计算first term,second term 和third term,等所有3个步骤计算好后再把它们汇总起来,我们就需要使用到同步事件和等待句柄,同步事件分有两个,分别为AutoResetEvent和ManualResetEvent,这两个类可以用来代表某个线程的运行状态:终止和非终止,等待句柄用来判断ResetEvent的状态,如果是非终止状态就一直等待,否则放行,让等待句柄下面的代码继续运行。下面的代码示例阐释了如何使用等待句柄来发送复杂数字计算的不同阶段的完成信号。此计算的格式为:result = first term + second term + third term
using System;
using System.Threading;
class CalculateTest
{
    
static void Main()
    {
        Calculate calc 
= new Calculate();
        Console.WriteLine(
"Result = {0}."
            calc.Result(
234).ToString());
        Console.WriteLine(
"Result = {0}."
            calc.Result(
55).ToString());
    }
}
class Calculate
{
    
double baseNumber, firstTerm, secondTerm, thirdTerm;
    AutoResetEvent[] autoEvents;
    ManualResetEvent manualEvent;
    
// Generate random numbers to simulate the actual calculations.
    Random randomGenerator;
    
public Calculate()
    {
        autoEvents 
= new AutoResetEvent[]
        {
            
new AutoResetEvent(false),
            
new AutoResetEvent(false),
            
new AutoResetEvent(false)
        };
        manualEvent 
= new ManualResetEvent(false);
    }
    
void CalculateBase(object stateInfo)
    {
        baseNumber 
= randomGenerator.NextDouble();

        
// Signal that baseNumber is ready.
        manualEvent.Set();
    }
    
// The following CalculateX methods all perform the same
    
// series of steps as commented in CalculateFirstTerm.
    void CalculateFirstTerm(object stateInfo)
    {
        
// Perform a precalculation.
        double preCalc = randomGenerator.NextDouble();
        
// Wait for baseNumber to be calculated.
        manualEvent.WaitOne();
        
// Calculate the first term from preCalc and baseNumber.
        firstTerm = preCalc * baseNumber * 
            randomGenerator.NextDouble();
        
// Signal that the calculation is finished.
        autoEvents[0].Set();
    }
    
void CalculateSecondTerm(object stateInfo)
    {
        
double preCalc = randomGenerator.NextDouble();
        manualEvent.WaitOne();
        secondTerm 
= preCalc * baseNumber * 
            randomGenerator.NextDouble();
        autoEvents[
1].Set();
    }
    
void CalculateThirdTerm(object stateInfo)
    {
        
double preCalc = randomGenerator.NextDouble();
        manualEvent.WaitOne();
        thirdTerm 
= preCalc * baseNumber * 
            randomGenerator.NextDouble();
        autoEvents[
2].Set();
    }
    
public double Result(int seed)
    {
        randomGenerator 
= new Random(seed);

        
// Simultaneously calculate the terms.
        ThreadPool.QueueUserWorkItem(
            
new WaitCallback(CalculateBase));
        ThreadPool.QueueUserWorkItem(
            
new WaitCallback(CalculateFirstTerm));
        ThreadPool.QueueUserWorkItem(
            
new WaitCallback(CalculateSecondTerm));
        ThreadPool.QueueUserWorkItem(
            
new WaitCallback(CalculateThirdTerm));
        
// Wait for all of the terms to be calculated.
        WaitHandle.WaitAll(autoEvents);
        
// Reset the wait handle for the next calculation.
        manualEvent.Reset();
        
return firstTerm + secondTerm + thirdTerm;
    }
}
   该示例一共有4个ResetEvent,一个ManualEvent,三个AutoResetEvent,分别反映4个线程的运行状态。ManualEvent和AutoResetEvent有一点不同:AutoResetEvent是在当前线程调用set方法激活某线程之后,AutoResetEvent状态自动重置,而ManualEvent则需要手动调用Reset方法来重置状态。接着来看看上面那段代码的执行顺序,Main方法首先调用的是Result 方法,Result方法开启4个线程分别去执行,主线程阻塞在WaitHandle.WaitAll(autoEvents)处,等待3个计算步骤的完成。4个ResetEvent初始化状态都是非终止(构造实例时传入了false),CalculateBase首先执行完毕,其他3个线程阻塞在manualEvent.WaitOne()处,等待CalculateBase执行完成。CalculateBase生成baseNumber后,把代表自己的ManualEvent状态设置为终止状态。其他几个线程从manualEvent.WaitOne()处恢复执行,在执行完自己的代码后把自己对应的AutoResetEvent状态置为终止。当3个计算步骤执行完后,主线程从阻塞中恢复,把三个计算结果累加后返回。还要多补充一点的是WaitHandle的WaitOne,WaitAll,WaitAny方法,如果等待多个进程就用WaitAll,如本例中的:WaitHandle.WaitAll(autoEvents),WaitAny是等待的线程中有一个结束则停止等待。
Mutex 对象
    Mutex与Monitor类似,这里不再累赘,需要注意的是Mutex分两种:一种是本地Mutex一种是系统级Mutex,系统级Mutex可以用来进行跨进程间的线程的同步。尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
注:文中代码示例来源于MSDNCodeProject

Feedback

#1楼    回复  引用  查看    

2007-12-12 16:58 by 徐少侠      
好东西
曾经接触过一些。
其实有些常用的线程命令很多在net1.1的东西如今在2.0、3.0中已经不建议使用了
的确有必要仔细研究下了。
感谢博主

#2楼    回复  引用    

2007-12-12 17:08 by lzz [未注册用户]
好多字哦。。。刘老师,我要转.net不跟蒋老师混了

#3楼    回复  引用  查看    

2007-12-12 18:03 by micYng      
Jeffrey关于线程syncblock的介绍,看了之后就什么都明白了 :)

#4楼    回复  引用  查看    

2007-12-12 18:57 by Cat Chen      
lock是.NET中最好用的也是最优化的,但在某些情况下只有Mutex与Semaphore才适用。

#5楼    回复  引用    

2007-12-12 19:51 by junchangzhang [未注册用户]
有时候[methodimpl(methodimploptions.synchronized)]也是不错选择

#6楼    回复  引用    

2007-12-12 20:30 by boy5d [未注册用户]
好贴,要收藏

#7楼    回复  引用    

2007-12-12 21:17 by tran收到是 [未注册用户]
http://www.cnblogs.com/idior/articles/864806.html

#8楼    回复  引用  查看    

2007-12-13 08:56 by 九头龙      
关于C#多线程,这么显浅的东西还是不要放在首页。建议看一下这篇文章:《C#中的多线程》:http://knowledge.swanky.wu.googlepages.com/threading_in_c_sharp.html

#9楼 [楼主]   回复  引用  查看    

2007-12-13 09:40 by 老刘很氓      
@九头龙
请你看完文章再FP好不好,什么是浅显的东西,什么是复杂的东西?只是一个技术的应用而已难道也要分割深浅吗?难道BLOG上全部都大谈设计模式和框架设计才叫深吗?另外,你给的链接我看了,和我这篇文章讲的完全不是一个东西。

#10楼    回复  引用  查看    

2007-12-13 10:36 by Yoshow      
好东西啊 比那些讲设计模式和设计框架的实在多了

#11楼    回复  引用    

2007-12-13 10:52 by laoda [未注册用户]
"只是一个技术的应用而已难道也要分割深浅吗"说的好
不错.这些是很有实际意义的。希望刘老师多写些类似的东西.
某些人认为很浅显,就不用理他们了。这些人说不定自己都没搞懂。

#13楼    回复  引用  查看    

2007-12-13 11:27 by 我们      
好文章,受益非浅

#14楼    回复  引用    

2007-12-13 11:35 by 软件下载 [未注册用户]
好,支持下



http://www.shi123.com

#15楼    回复  引用    

2007-12-13 16:23 by 路西菲尔 [未注册用户]
Monitor在线程数小于3个或者在多核下性能不好.建议在dotnet程序中就不要用这种锁了

#16楼    回复  引用  查看    

2007-12-13 22:48 by gguowang      
呵呵 刘老师,受教了啊,多谢啊

#17楼    回复  引用    

2007-12-14 10:09 by ICQ [未注册用户]
你在讲MSDN吗。

#18楼 [楼主]   回复  引用  查看    

2007-12-14 10:15 by 老刘很氓      
@ICQ
MSDN关于这块举的例子在我翻阅相关资料前没有看懂,所以我觉得有必要对MSDN里面的东西做些解释。

#19楼    回复  引用  查看    

2007-12-15 23:28 by 风生水起      
顶,写的很不错

#20楼    回复  引用  查看    

2007-12-25 10:02 by txdlf      
不错,又温习了一下~
技术实在又通俗易懂,要比那些大谈特谈设计模式的好
支持!!!

#21楼    回复  引用    

2008-01-09 15:29 by tin [未注册用户]
九头龙就是个SB!

#22楼    回复  引用    

2008-03-28 15:27 by zhangqian [未注册用户]
学习了,谢谢!

#23楼    回复  引用    

2008-03-29 15:02 by Chris.W [未注册用户]
最近我在学习C#下的多线程编程
以前做java比较多
使用C#(或者说.NET)还是第一次
看了这篇文章 很有启发

#24楼    回复  引用    

2008-03-29 15:10 by Chris.W [未注册用户]
最近我在学习C#下的多线程编程
以前做java比较多
使用C#(或者说.NET)还是第一次
看了这篇文章 很有启发
看了之后我想问一个问题
希望能给予解惑 不胜感激啊

public double Result(int seed)
{
//...
// Wait for all of the terms to be calculated.
WaitHandle.WaitAll(autoEvents); //[1]
// Reset the wait handle for the next calculation.
manualEvent.Reset(); //[2]
return firstTerm + secondTerm + thirdTerm;
}
当主线程(第一次)执行到[1]处的代码时
如果autoEvents中有任意一个状态是nonsignaled时
(autoEvents被初始为false[nonsignaled])
主线程将被挂起
直到autoEvents所有的事件变为true[signaled]后
才会被唤醒并继续执行下去
下面假设有两种情况
1-CalculateBase先被调度执行,其他三个线程后被调度执行
1-1 CalculateBase被调度执行
1-2 计算baesNumber的值
1-3 设置manualEvent为true[signaled]
这个时候CalculateBase还会继续执行下去了吧?
也就是说CalculateBase将manualEvent设置为true[signaled]后
还会继续执行下去
执行到了manualEvent.Set()之后并跳出到CalculateBase函数
这样CalculateBase线程也就执行结束了
如果不是使用ThreadPool启动这四个线程
而是让这四个线程一直 while(true) 执行下去的话???
不知道会有什么问题???
1-5 Calculate[First/Second/Third]Term被调度执行
这里又假设CalculateFirstTerm先被调度执行
1-6 CalculateFirstTerm取得随机浮点数
1-7 CalculateFirstTerm等待manualEvent被设置为true[signaled]
由于之前manualEvent已经被CalculateBase线程设置为true[signaled]
所以CalculateFirstTerm线程没有被挂起
会继续往下执行
计算firstTerm的值并把autoEvents对应的值设置为true[signaled]
这个时候CalculateFirstTerm线程执行结束
1-8 CalculateSecondTerm和CalculateThirdTerm的情况如上
1-9 当三个CalculateXXXTerm线程执行完毕之后
主线程被唤醒并马上将manualEvent设置为false[nonsignaled]
计算出三个XXXTerm的和并返回
2-其他三个县城先被调度执行之,之后才轮到CalculateBase被调度执行
假设四个线程被调度执行的顺序如下
CalculateFirstTerm线程
CalculateSecondTerm线程
CalculateThiredTerm线程
CalculateBase线程
2-1 CalculateFirstTerm线程被调度执行
2-2 CalculateFirstTerm取得随机浮点数
2-3 CalculateFirstTerm线程等待manualEvent被设置为true[signaled]
由于此时manualEvent还是nonsignaled状态
所以CalculateFirstTerm线程被挂起
2-4 CalculateSecondTerm线程和CalculateThiredTerm线程的执行情况如上
2-5 CalculateBase线程被调度执行
2-6 计算baesNumber的值
2-7 设置manualEvent为true[signaled]
由于这是一个ManualResetEvent
所以三个CalculateXXXTerm线程都被唤起
于是这三个CalculateXXXTerm线程被放到准备执行队列
等待系统的调度
2-8 CalculateBase线程执行完毕
2-9 三个CalculateXXXTerm线程被调度执行
2-10 三个CalculateXXXTerm线程执行结束
2-11 由于三个CalculateXXXTerm线程和CalculateBase线程都执行结束
所以[1]得等待结束
于是控制返回到主线程
随后主线程计算结果并继续下一次计算

***问题***

CalculateFirstTerm线程
CalculateSecondTerm线程
CalculateThiredTerm线程
CalculateBase线程
主线程会同时启动上四个线程
这四个线程会一直执行下去(内部都有 while(true) 无限循环)
那么当主线程从[1]返回后尚未执行到[2]处的代码
CalculateXXXTerm线程就已经开始被调度执行的话……


#25楼    回复  引用    

2008-03-30 11:20 by uj [未注册用户]
@Chris.W
我不是很明白你的问题,为什么要用while(true)呢?

#26楼    回复  引用  查看    

2008-07-25 15:16 by 陈晨      
@老刘很氓
-------------------------------------------
请教个问题,关于文章中ping pong的例子
两个线程在for循环前都有lock ( ball ),锁定了ball
在一个线程for循环中执行Monitor.Pulse( ball )能使lock锁释放吗?
也就是为什么能输出ping pong...交替的结果?


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-12-12 17:53 编辑过
Google站内搜索

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》



相关文章:

相关链接: