您的位置:首页 > 移动开发 > Objective-C

.Net学习难点讨论系列3 – .线程同步问题之一

2011-05-24 21:00 561 查看
此文基本属于拼凑J
,出于总结知识的目的而写。

线程同步的问题是在多线程编程中常遇到的一个问题。从最底层的操作系统内核编程到高级的.Net托管模式下的编程都可以见到处理线程同步问题的代码的身影。

本文将讨论一下.Net中线程同步的几种实现方式

首先是介绍托管代码中CLR原生支持处理线程同步的方法

方法一:互锁方法

当多个线程访问共享数据是,必须以线程安全的方式访问该数据。最快的以线程安全的方式操作数据的方法是使用互锁方法。这一系列的静态方法位于
System.Threading.Interlocked类。唯一的限制是它们实现的对象有限。下面的例子针对这些方法最常操作的Int32类型变量:
IL
代码中赋值与简单的数值运算都不是原子操作。以下代码演示了这些函数的使用:

public
 
static
 
class
 Interlocked

    {

        
//
自动执行(location++)

        
public
 
static
 Int32 Increment(
ref
 Int32 location);

        
//
自动执行(lcoation--)

        
public
 
static
 Int32 Decrement(
ref
 Int32 location);

        
//
自动执行(location+=value)

        
//
说明:值可以是一个可以进行

        
public
 
static
 Int32 A
4000
dd(
ref
 Int32 location, Int32 value);

        
//
自动执行(localion = value)

        
public
 
static
 Int32 Exchange(
ref
 Int32 location, Int32 value);

        
//
自动执行: if (location = comparand) location = value

        
public
 
static
 Int32 CompareExchange(
ref
 Int32 location, Int32 value, Int32 comparand);

}

 

以上代码展示了这些函数的原型,之所以定义这样的函数是因为在IL
代码中赋值与简单的数值运算都不是原子操作。以下代码演示了这些函数的使用:

public
 
void
 AddOne() 



int
 newVal 
=
 Interlocked.Increment(
ref
 intVal); 



 
参数(需递增的变量)以引用方式传入,Increment
方法不但可以修改传入的参数值,还会返回递增后的新值。

注意:要在程序中使用
Interlocked
类,需引用
System.Threading
这个命名空间。

Exchange() –
将某数值赋给一个成员变量用,示例
 

public
 
void
 SafeAssignment()

    {

        Interlocked.Exchange(
ref
 myInt, 
82
);

    }

 

Interlocked类还提供了
Exchange

CompareExchange
操作
Object

IntPtr

Single

Double
等类型参数的版本。

还有一个泛型版本的
Exchange
,其接受的参数被限制为
class
(任意引用类型)。(由于互锁方法要求其变量的地址对其,所以这个方法实现的前提是
CLR
会自动对齐)

注意,也是出于以上强制变量地址对其这个原因,不要调用操作
Int64
类型的
Interlocked
类的方法,因为无法保证
Int64
类型对象的内存地址对齐。

 

方法二:使用
Monitor
类与同步块



System.Threading
空间下的
Monitor
类几乎允许将任意一段代码设置为在某个时间仅能被一个线程执行,我们称这段代码为临界区。
Monitor
类提供了
Enter(object)

Exit(object)
这两个静态方法。该对象提供了一个简单的方式用于唯一标识那个将以同步方式访问的资源。当一个线程调用了
Enter()
方法,它将等待以获得访问该引用对象的独占权(仅当另一个拥有该权利的时候它才会等待)。一旦该权利被获得并使用,线程可以对同一个对象调用
Exit()
方法以释放该权利。

需要注意的地方:
1.
绝不能将一个值类型的实例作为参数传给
Enter()

Exit()
方法。

2.
不管发生了什么,必须在
finally
子句中调用
Exit()
以释放所有的独占访问权。
C#

lock
语句的内部实现以保证了这一点,所以使用
lock
语句是更好的选择。

使用Monitor类做同步示例:(代码来自CLR Via C#)

internal
 
sealed
 
class
 Transaction {

   

   
//
 表示最后一次事务处理执行的时间的字段

   
private
 DateTime timeOfLastTransaction;

   
public
 
void
 PerformTransaction() 

{

//
 对this对象加锁

//
 按Jeffrey Richter书中的介绍,此处不应使用this,而应创建一个私有类对象,并将其传入作为锁对象

      Monitor.Enter(
this
); 

      
//
 执行事务处理


      
//
 记录最近一次事务处理的时间

      timeOfLastTransaction 
=
 DateTime.Now;

      
//
 对this对象解锁

      Monitor.Exit(
this
);  

   }

   
//
 下面只读属性返回最后一次事务处理执行的时间

   
public
 DateTime LastTransaction {

      
get
 {

         
//
 对this对象加锁

         Monitor.Enter(
this
); 

         
//
 在一个临时变量中保存最后一次事务处理的时间

         DateTime dt 
=
 timeOfLastTransaction;

         
//
 对this对象解锁

         Monitor.Exit(
this
); 

         
//
 返回已保存的日期和时间

         
return
(dt); 

      }

   }

}

 

以上代码示范了如何使用Monitor的Enter方法和Exit方法对对象的同步块进行加锁和解锁。Enter方法与Exit方法需成对出现,否则会出现异常。注意此处的dt对象必不可少,它可以防止返回可能被破坏的值。

示例2(此示例来自Practical .Net2 and C#2)

using
 System.Threading;

class
 Program {

   
static
 
long
 counter 
=
 
1
;

   
static
 
void
 Main() {

      Thread t1 
=
 
new
 Thread( f1 );

      Thread t2 
=
 
new
 Thread( f2 );

      t1.Start(); t2.Start(); t1.Join(); t2.Join();

   }

   
static
 
void
 f1() {

      
for
 (
int
 i 
=
 
0
; i 
<
 
5
; i
++
){

         
try
{

            Monitor.Enter( 
typeof
( Program ) );

            counter 
*=
 counter;

         }

         
finally
{ Monitor.Exit( 
typeof
( Program ) ); }

         System.Console.WriteLine(
"
counter^2 {0}
"
, counter);

         Thread.Sleep(
10
);

      }

   }

   
static
 
void
 f2() {

      
for
 (
int
 i 
=
 
0
; i 
<
 
5
; i
++
){

     
12cf8
    
try
{

            Monitor.Enter( 
typeof
( Program ) );

            counter 
*=
 
2
;

         }

         
finally
{ Monitor.Exit( 
typeof
( Program ) ); }

         System.Console.WriteLine(
"
counter*2 {0}
"
, counter);

         Thread.Sleep(
10
);

      }

   }

}

 

 使用C#的lock语句简化代码

代码示例:(此代码使用了私有对象做锁对象,这是安全的使用lock的方式,上文也有提到,代码功能同上段)

internal
 
sealed
 
class
 TransactionWithLockObject {

   
//
 分配一个用于加锁的private对象

   
private
 Object m_lock 
=
 
new
 Object();

   
private
 DateTime timeOfLastTransaction;

   
public
 
void
 PerformTransaction() 

   {

        
//
 对私有字段对象加锁

        
lock
 (m_lock)

        {  

         
//
 执行事务处理


         timeOfLastTransaction 
=
 DateTime.Now;

      }  
//
 对私有字段对象解锁

   }

   
public
 DateTime LastTransaction

   {

      
get

      {

         
lock
 (m_lock)

         {  

            
return
 timeOfLastTransaction; 

         } 

      }

   }

}

 

示例2的lock版:

using
 System.Threading;

class
 Program {

   
static
 
long
 counter 
=
 
1
;

   
static
 
void
 Main() {

      Thread t1 
=
 
new
 Thread(f1);

      Thread t2 
=
 
new
 Thread(f2);

      t1.Start(); t2.Start(); t1.Join(); t2.Join();

   }

   
static
 
void
 f1() {

      
for
 (
int
 i 
=
 
0
; i 
<
 
5
; i
++
){

         
lock

typeof
(Program) ) { counter 
*=
 counter; }

         System.Console.WriteLine(
"
counter^2 {0}
"
, counter);

         Thread.Sleep(
10
);

      }

   }

   
static
 
void
 f2() {

      
for
 (
int
 i 
=
 
0
; i 
<
 
5
; i
++
){

         
lock

typeof
(Program) ) { counter 
*=
 
2
; }

         System.Console.WriteLine(
"
counter*2 {0}
"
, counter);

         Thread.Sleep(
10
);

      }

   }

}

 

Monitor的其它方法

TryEnter()方法,此方法与Enter()相似,只不过它是非阻塞的。如果资源的独占访问权已经被另一个线程占据,该方法将立即返回一个false的返回值。下面的代码说明了TryEnter()使用的一些问题:

using
 System.Threading;

class
 Program {

   
private
 
static
 
object
 staticSyncRoot 
=
 
new
 
object
();

   
static
 
void
 Main() {

      

      Monitor.Enter( staticSyncRoot );      

      Thread t1 
=
 
new
 Thread(f1);

      t1.Start(); t1.Join();

   }

   
static
 
void
 f1() {

      
bool
 bOwner 
=
 
false
;

      
try
 {

         
if

!
 Monitor.TryEnter( staticSyncRoot ) )

            
return
;

         bOwner 
=
 
true
;

         
//
 


      }

      
finally
 {

          
//
当你没有获得访问权时不要调用Monitor.Exit()

          
//
使用bOwner变量做指示是否TryEnter()已获取访问权

         
if
( bOwner )

            Monitor.Exit( staticSyncRoot );

      }

   }

}

 

Monitor类中方法控制线程的作用
Wait()
方法,
Pusle()
方法与
PulseAll()

Wait()

Pusle()

PulseAll()
这三个方法常放在一起使用,当一个线程获得了某个对象的独占访问权,而它决定等待(通过调用
Wait()
)直到该对象的状态发生变化。为此,该线程必须暂时失去对象独占访问权,以便让另一个线程修改对象的状态。修改对象状态的线程必须使用
Pulse()
方法通知那个等待的线程修改完成。

(此示例及其代码出自
Practical .Net2 and C#2


以下通过一个场景介绍它们的使用

拥有obj对象独占访问权的线程T1,调用Wait(obj)方法将它自己注册到obj对象的被动等待列表中。

由于以上调用,T1失去了对obj的独占访问权。因此,另一个线程T2通过调用Enter(obj)获得obj的独占访问权。

T2最终修改了obj的状态并调用Pulse(obj)通知了这次修改。该调用将
导致obj被动等待列表中的第一个线程(这里是T1)被移动到obj的主动等待列表的首位。(PulseAll()将被动等待列表中的线程全部转移到主动
等待列表,这些线程将按照它们调用Wait()的顺序到达非阻塞态。)

一旦obj的独立访问权被释放,obj主动等待列表中的第一个线程将被确保可以获得obj的独占访问权,然后它就从Wait(obj)方法中退出等待状态。

在我们的场景中,T2调用Exit(obj)以释放对obj的独占访问权,接着T1恢复访问权并从Wait(obj)方法退出。

注意:以上描述的场景中,T2需要成对调用Enter()与Exit()函数,这样才能保证T2放弃对obj的独占权,使T1重新获得独占访问。(当然前提是Pulse()已被调用从而通知了这次修改)

示例代码贴在下面:

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();

      threadPing.Join();  threadPong.Join();

   }

   
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!
"
);

   }

}

 

运行结果:

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!

 

C#实现双检锁(double-check locking)技巧(摘自CLR Via C#)

 

public
 
sealed
 
class
 Singleton {

   

   
private
 
static
 Object s_lock 
=
 
new
 Object();

   
private
 
static
 Singleton s_value; 

   

   
//
私有构造器组织这个类之外的任何代码创建实例

    
private
 Singleton() {}

   
//
 下述共有,静态属性返回单实例对象

   
public
 
static
 Singleton Value {

      
get
 {

         
//
 检查是否已被创建

         
if
 (s_value 
==
 
null
) {

            
//
 如果没有,则创建

            
lock
 (s_lock) {

               
//
 检查有没有另一个进程创建了它

               
if
 (s_value 
==
 
null
) {

                  
//
 现在可以创建对象了

                  s_value 
=
 
new
 Singleton();

               }

            }

         }

         
return
 s_value;

      }

   }

}

 

实现相同效果更简便的方法

namespace
 SimplifiedSingleton {

   
public
 
sealed
 
class
 Singleton {

      
private
 
static
 Singleton s_value 
=
 
new
 Singleton();

      
private
 Singleton() { }

      
public
 
static
 Singleton Value {

         
get
 {

            
return
 s_value;

         }

      }

   }

}

 

这正是设计模式中单例模式,TerryLee的设计模式系列文章有这两段代码

 

方法3:ReaderWriterLock类

ReaderWriterLock类位于System.Threading命名空间下,它实现了多用户读/单用户写
的同步访问机制。在合适的情况下ReaderWriterLock相对于Monitor类或Mutex类是一个更好的选择,因为后者的独占访问模型不允许任何形式的并发访问,这是的它们的处理效率始终不高,应用程序使用读的情况比写的情况要多。

ReaderWriterLock类与后文要介绍的互斥体及事件一样都是在使用前被初始化。必须从用于同步的对象的角度去考虑,而不是被同步的对象。

示例:

 

class
 Program 



static
 
int
 theResource 
=
 
0


static
 ReaderWriterLock rwl 
=
 
new
 ReaderWriterLock(); 

static
 
void
 Main() 



Thread tr0 
=
 
new
 Thread(ThreadReader); 

Thread tr1 
=
 
new
 Thread(ThreadReader); 

Thread tw 
=
 
new
 Thread(ThreadWriter); 

tr0.Start(); tr1.Start(); tw.Start(); 

tr0.Join(); tr1.Join(); tw.Join(); 



static
 
void
 ThreadReader() 



for
 (
int
 i 
=
 
0
; i 
<
 
3
; i
++




try
 



//
 使用AcquireReaderLock()请求读锁,超时触发异常 

rwl.AcquireReaderLock(
1000
); 

Console.WriteLine(
"
Begin Read theResource = {0}
"
,theResource); 

Thread.Sleep(
10
); 

Console.WriteLine(
"
End Read theResource = {0}
"
,theResource); 

//
 读取完毕后释放读锁 

rwl.ReleaseReaderLock(); 



catch
 ( ApplicationException ){} 





static
 
void
 ThreadWriter() { 

for
 (
int
 i 
=
 
0
; i 
<
 
3
; i
++




try
 



//
 通过AcquireWriterLock()请求写锁,超时触发异常 

rwl.AcquireWriterLock(
1000
); 

Console.WriteLine(
"
Begin Write theResource = {0}
"
,theResource); 

Thread.Sleep(
100
); 

theResource 
++


Console.WriteLine(
"
End Write theResource = {0}
"
,theResource); 

//
释放写锁 

rwl.ReleaseWriterLock(); 



catch
 ( ApplicationException ) {} 

}





 

Jeffrey Richter在他的CLR Via
C#中提到不用类库中自带的ReaderWriterLock类,他提到的此类的缺陷如下,首先,进入与离开这个锁的性能非常慢。其次,当面临读线程与写
线程同时等待处理时,这个锁给予了读线程优先级,这将导致处于等待状态的写线程非常慢。(通产我们知道,读线程往往大大多于写线程,所以这样的处理方式常
造成写线程发生饥饿,不能及时完成任务)

Jeffrey Richter推荐他的Power Threading库中它实现的读/写线程锁。详情参见此处


 

方法4:使用[Synchronization]特性进行同步

    首先来说这是一个比较偷懒的进行同步的方法,它不需要我们实际深入线程控制敏感数据的细节,这意味着使用这种方法进行同步是非常简单
的。[Synchronization]特性位于System.Runtime.Remoting.Contexts命名空间下。这个类级别的特性有效地
使对象的所有示例的成员都保持线程安全。当CLR分配带[Synchronization]的对象时,它会把这个对象放在同步上下文中。要想使对象不被在
上下文边界中移动,就必须让它继承ContextBoundObject类。下面代码示范了此代码的使用。

//
注意命名空间的引用 

using
 System.Runtime.Remoting.Contexts; 

[Synchronization] 

public
 
class
 Printer : ContextBoundObject 



    
public
 
void
 PrinterNumbers() 

    { 

        
//



 


 

    } 



 

这种方法的问题在于,即使一个方法没有使用线程敏感的数据,CLR仍然会锁定对该方法的调用。这回明显的降低性能,所以此方法慎用。

 

由于本文过长托管代码包装Windows内核对象完成线程同步的方法放到下一篇文章中。

 

参考书籍:

框架设计(第2版):CLR Via C# 清华大学出版社

C#与.Net3.0高级程序设计(特别版) 人民邮电出版社
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息