同步上下文;
同步代码区;
手控同步.
1 同步上下文
上下文是一组属性或使用规则,这组属性或使用规则对执行时相关的对象集合是通用的.能够添加的上下文属性包括有关同步,线程式亲缘性的事务处理的策略.简言之,上下文把类似的对象组合在一起.这种策略将使用SynchronizationArrribute类对ContextBoundObject对象进行简单的自动同步,驻留在上下文中,符合上下文规则的对象称为上下文对象称为上下文绑定对象. .NET把同步锁和对象自动关联起来.在每种方法调用前锁定对象,方法返回后释放对象(让其它等待着的线程访问).由于线程同步和并发管理是开发人员遇到的最困难的任务.因此这种方法极大的提高了效率.
SynochronizationAttribute 类对缺少手工处理同步经难的程序员来说是很有用的.因为它包含了实例变量,实例方法和应用这个属性的类的实例字段.然而,它不是处理静态字段和方法同步.如果必须同步代码块,它也不起作用.同步整个对象是对轻松使用必须付出的代码.在使用SynchronizationAttribute编程时,SynchronizationAttribute非常方便.因为一个上下文(例如事务处理)中的对象由 COM+ 运行库组合在一起.我们可以使用下面的代码来使用一个类具有安全性:
[SynchronizationAttribute(SynchronizationOption.Required)]
public class className
{
//class code here
}
SynchronizationAttribute类有两个构造函数.一个不带参数(默认SynchronizationOption.Required),另一个构造函数包含一个SynchronizationOption 枚举.这个枚举的值如下所示:
Disabled :忽略对象的同步请求,即对象不是线程安全的.
NotSupported :创建组件时没有控制同步,即论调用者牌什么状态,对象都不能参与任何同步操作.
Required :确保所创建的所有的对象都已同步.
RequiredNew :无论调用者是什么,组件总是参与新的同步.
Supported :使用这个选项的对象只有存在(取决于调用者)时,才能参与同步操作.
2 同步代码区
第二种同步策略是同步代码区,这些特定的代码区是方法中重要的代码段.它们可以改变对象的状态,或者更新另一个资源(如数据库,文件等).先看Monitor类.
Monitor类用于同步代码区,其方式是使用Monitor.Enter()方法获得一个锁.然后,使用Monitor.Exit()方法释放该锁.锁的概念通常用于解释Monitor类.一个线程获得锁,其它线程就要等到该锁释放后才能使用.一旦在代码区上获取了一个锁,就可以在Monitor.Enter()和Monitor.Exit()程序块内使用如下方法:
Wait()--此方法用于释放对象上的锁.并暂停当前的线程.直到它重新获得锁.
Pulse()-此方法用于通知正在队列中等待的线程,对象的状态已经改变了.
PulseAll():此方法用于通知所有正在队列中等待的线程,对象的状态已经有了改变.
Enter()方法和Exit()方法
Monitor方法是静态方法.能够被Monitor类自身调用,而不是由该类的实例调用.在.NET Framework中,每个对象都有一个与之相关的锁,可以获取和释放该锁,这样,在任一时刻仅有一个线程可以访问对象的实例变量和方法.与之类似,.NET Framework中的每个对象也提供一种允许它处于等待状态的机制.正如锁的机制. 设计这种机制的主要原因是帮助线程间的通信.如果一个线程进入对象的重要代码段,并需要一定的条件才能存在,而另一个线程可以在该代码段中创建该条件,此时就需要这种机制.
诀窍在于,在任一时刻,重要的代码段只允许一个线程访问,当第一个线程进入代码段后,其它的线程就不能再进入了,如果第线程A从数据库中获取了一些数据,在线程A接收到所有的数据,并处理这些数据的过程中,另一个线程B必须调用Wait()方法.等待线程A通知它何时才能接收数据.在接收到数据时,线程A就调用Pulse()方法,通知线程B,让线程B处理数据.这可以通过"等待和发出脉冲"机制来实现.第一个线程进入重要代码段,执行Wait() 方法. Wait() 方法先释放锁, 再使第一个线程进入等待状态.现在第二个线程进入代码段.修改必须的条件.然后调用Pulse() 方法来通知等待的线程.当条件满足后,就可以继续执行.接着第一个线程先重新获得锁,再从Monitor.Wait() 方法中返回,并从调用Monitor.Wait()的地方继续执行.任何两个线程都不能同时进入 Enter() 方法.
下面是一个有关Monitor的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ConsoleApplication1
{
class Program
{
private int result = 0;
public Program()
{
}
public void NonCriticalSection()
{
Console.WriteLine("Enter thread " + Thread.CurrentThread.GetHashCode());
for (int i = 0; i <= 15; i++)
{
Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
Thread.Sleep(2500);
}
Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
}
public void CriticalSection()
{
Monitor.Enter(this);
Console.WriteLine("Entered Thread " + Thread.CurrentThread.GetHashCode());
for (int i = 0; i <= 15; i++)
{
Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
Thread.Sleep(1500);
}
Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
Monitor.Exit(this);
}
public static void Main()
{
Program p = new Program();
int i = Console.Read();
if (i % 2 == 0)
{
Thread t1 = new Thread(new ThreadStart(p.NonCriticalSection));
t1.Start();
Thread t2 = new Thread(new ThreadStart(p.NonCriticalSection));
t2.Start();
}
else
{
Thread t1 = new Thread(new ThreadStart(p.CriticalSection));
t1.Start();
Thread t2 = new Thread(new ThreadStart(p.CriticalSection));
t2.Start();
}
}
}
}
在上面的例子中,重要代码段定义为 Monitor.Enter(this) 和 Monitor.Exit(this)之间的代码块.参数this指定应将锁保存在当前对象上,把哪个对象作为参数传递给Enter()方法况是不好确定.在需要锁定对象,不上其他线程访问它时,应把指针this作为参数传递.
Wait()和Pulse()机制
Wait()和Pulse()机制用于线程式间的交互.当在一个对象上执行Wait()时,正在访问该对象的线程就会进入等待状态,直到它得到一个唤醒信号.Pulse()和PulseAll()用于给等待线程发送信号.下面列出的上一个Wait()和Pulse()方法如何工作的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ConsoleApplication1
{
class LockMe
{
public LockMe()
{
}
}
class Monitor1
{
public LockMe _lm;
public Monitor1(LockMe lm)
{
this._lm = lm;
}
public void CriticalSection()
{
Monitor.Enter(this._lm);
Console.WriteLine("Enter M1");
for(int i= 0 ;i < 10; i++)
{
Thread.Sleep(3000);
Console.WriteLine("M1 Wait");
Monitor.Wait(this._lm);
Console.WriteLine("M1 WorkUP");
Monitor.Pulse(this._lm);
}
Monitor.Exit(this._lm);
}
}
class Monitor2
{
private LockMe _lm;
public Monitor2(LockMe lm)
{
this._lm = lm;
}
public void CriticalSection()
{
Monitor.Enter(this._lm);
Console.WriteLine("Enter M2");
for (int i = 0; i < 10; i++)
{
Thread.Sleep(3000);
Console.WriteLine("M2 WorkUp");
Monitor.Pulse(this._lm);
Console.WriteLine("M2 Wait");
Monitor.Wait(this._lm);
}
Monitor.Exit(this._lm);
}
}
class Program
{
public static void Main()
{
LockMe l = new LockMe();
Monitor1 m1 = new Monitor1(l);
Monitor2 m2 = new Monitor2(l);
Thread t1 = new Thread(new ThreadStart(m1.CriticalSection));
t1.Start();
Thread t2 = new Thread(new ThreadStart(m2.CriticalSection));
t2.Start();
}
}
}
注意:当线程执行Monitor.Wait()方法时,它会临时释放LockMe对象上的锁,以便其它线程可以访问它.线程 t1 进入等待状态后, 线程t2就可以自由访问 LockMe 对象.即使LockMe对象是一个独立的对象,这两个线程也均指向同一个对象的引用. 线程t2获得LockMe对象上的锁后,一进入For循环,就给LockMe对象上等待的线程发送一个运行进通知.之后便进行等待状态.然后t1 醒来,获得LockMe对象上的锁,接着 t1 执行一些操作,并给 LockMe对象上等待的线程发送一个运行时通知.这个循环一直持续到For循环结束.
注意每一个 Enter() 方法都要伴随一个 Exit()方法,否则程序就会进入列循环.
Enter将一个对象作为其参数,如果该对象参数为 null, 方法变量或者值类型对象如整型值,系统将会抛出一个异常.
TryEnter()方法
Moniter类的TryEnter()方法非常类似于Enter()方法,它试图获得对象的独占锁,不过它不会像Enter()方法那样暂停.如果线程成功进入,则TryEnter()方法返回True.
TryEnter()方法有三个重载,其中两个都带超时参数,表示等待锁定的时间.如:
Monitor.TryEnter(this,1000);
......
在可能发生竞争,但不希望线程睡眠某个示指定的时间,就可以使用TryEnter().
Lock
Lock关键字可以用作monitor方法的一个替换用法,下面的两部分代码是等价的:
Monitor.Enter(X);
.....
Monitor.Exit(X);
lock(this)
{
.....
}
ReaderWriterLock类
ReaderWriterLock定义了实现单写程序和多读程序语义的锁.这个类主要用于文件操作.即多个线程可以读取文件,但只能用一个线程来更新文件.ReaderWriterLock类中4个主要的方法是:
AcquireReaderLock():该重载方法获得一个读程序锁,超时值使用整数或TimeSpan.超时是可用于检测死锁的好工具.
AcquireWriterLock():该重载方法获得一个写程序锁.超时值使用整数或TimeSpan.
ReleaseReaderLock():释放读程序锁.
ReleaseWriterLock():释放写程序锁.
使用ReaderWriterLock类时,任意数量的线程都可以同时安全的读取数据.只有当线程进行更新时,数据才被锁定.只有在没有占用锁的写程序线程时,读程序线程才能获得锁.只有没有占用锁的读程序或写程序线程时,写程序才能获得锁.
3手控同步
DotNet Framework提供了一套经典的技术允许程序员使用类似于Win32线程的低级线程API创建和管理多线程应用程序.下表列出了System.Threading命名空间中的一些可以用于手控同步和类.
AutoResetEvent:
AutoResetEvent类可以使线程处于等待状态.直到通过调用Set()方法某事件将它置于信号状态为止.有信号状态是指没有线程在等待.在释放一个等待线程后.系统自动将AutoResetEvent类生置为无信号状态.如果没正在等待的线程,事件对象的状态保持警惕为有信号状态.指定ManualRest参数为False时,AutoResetEvent类对应于Win32的CreateEvent调用.
ManualResetEvent
ManualResetEvent类也用来使线程式处于等待状态.直到通过调用Set()方法某事件将它置于有信号状态.ManualResetEvent对象的状态会一直保持有信号,直到Reset()方法将它设置为无信号状态.指定 bManualReset 参数为True时, ManualResetEvent对应于Win32的CreateEvent调用.
Mutex
Mutex锁提供了跨进程和跨线程同步.如果没有线程占用Mutex锁,Mutex的状态就轩为有信号状态.Mutex并不上具有Monitor类的所有等待和发出脉冲功能.但可以创建在进程之间使用的命名互斥(使用重载构造函数),使用Mutex 类优于使用 Monitor 类,因为Mutex类可以跨进程使用,而Monitor类不行.
Interlocked
Interlocked类为在多个线程之间共享的原子的,非暂停整体更新提供了方法.如果变量位于共享的内在中,不同进程的线程就可以使用这种机制.
ManualResetEvent类
ManulaResetEvent类对象只能拥有两种状态之一:有信号(True)和无信号(False),ManualResetEvent类继承于WaitHandler类,其构造函数的参数可以确定对象的初始状态,Set()和Reset()方法返回一个布尔值,表示是否进行了成功的修改.
下面的代码首先创建一个对象 mansig , 并赋予false值,方法WaitOne()将一直等待 mansig 变为 true 或超时为止. 由于时间在等待中过去.且 mansig 的值没有被设置障为 true,所以程序就停止暂停并返回 false值.
public static void Main(string [] args)
{
ManualResetEvent mansig;
mansig = new ManualResetEvent(false);
Console.WriteLine("ManualResetEvent Before WaitOne");
bool b = mansig.WaitOne(1000,false);
Console.WriteLine("ManualResetEvent After WaitOne" + b);
}
上面的代码输出如下:
ManualResetEvent Before Waitone
ManulaResetEvent After WaitOne false
布尔值 false 把ManualResetEvent对象的初始状态设置为无信号,接着调用基类的 WaitHandle的 WaitOne() 方法,WaitOne()方法有两个参数,第一个参数是线程在WaitOne()方法中等待的毫秒数,因此在线程退出前等待一秒,第二个参数是exitContext , 如果已以在上下文的同步域中,想退出同步上下文,或者要重新获得同步上下文,就把该参数设置为 true;程序在WaitOne()方法中暂停一秒,然后因为超时而退出.ManulaResetEvent的状态仍然是 false,因而WaitOne()返回的布尔值是 false. 如果在创建 ManualResetEvent时就把它的状态设定为有信号(treu)时,如下代码:
ManualResetEvent mansig;
mansig = new ManualResetEvent(true):
Console.WriteLine("ManualResetEvent Before WaitOne");
bool b = mansig.WaitOne(1000,false);
Console.WriteLine("ManualResetEvent After WaitOne " + b);
输出结果如下所示:
ManualResetEvent Before WaitOne
ManualResetEvent After WaitOne true
将ManualResetEvent的初始状态修改为有信号的,即使指定超时值为1000毫秒,线程在WaitOne()中也不会等待.当ManualResetEvent的状态为无信号状态时,线程将等待状态变为有信号状态. 但1000毫秒后线程超时.状态 已经是有信号的.必须调用ManualResetEvent 的Reset()方法.为了把状态修改为有信号的,必须调用Set()方法.
下面的代码演示了如何使用 Set() 方法和 Reset()方法.
static void Main()
{
ManualResetEvent mre = new ManualResetEvent(true);
mre = new ManualResetEvent(1000,true);
bool state = mre.WaitOne(1000,true);
Console.WriteLine("ManualResetEvent After WaitOne" + state);
//change the state non-signaled
mre.Reset();
state = mre.WaitOne(5000,true);
Console.WriteLine("ManualResetEvent After secodn WaitOne" + state);
}
程序输出:
ManualResetEvent Ater first WaitOne true
ManualResetEvent After Second WaitOne false
ManualResetEvent对象的构造函数在将其初始化时设置为有信号的(true),结果,线程不在第一个WaitOne()方法中等待,并返回 true 值,接着将ManualResetEvent对象的状态重新设置为无信号的(false),于是线程在超时之间等待5秒钟.
ManualResetEvent对象的WaitAll() 方法将等待所有的事件对象都变成 true 或者有信号时,否则它将保持不变直到超时.WaitAny()方法等待任一事件对象变成true或有信号.
AutoResetEvent类
AutoResetEvent类的工作方式类似于ManualResetEvent类,它等待时间超时或者事件变成有信号的状态.接着将此事件通知等待线程.ManualResetEvent和AutoResetEvent的一个重要区别就是AutoResetEvent在WaitOne()方法中改变状态.下面的代码展示了AutoResetEvent类的用法:
static void Main()
{
AutoResetEvent are = new AutoResetEvent (true):
Console.WriteLine("Before First WaitOne");
bool state = are.WaitOne(1000,true);
Console.WriteLine("After First WaitOne " + state);
state = are.WaitOne(5000,true);
Console.WriteLine("After Second WaitOne" + state);
}
静态变量,方法和同步
静态变量和方法与同步锁中的实例 变量和方法有不同的作用.静态变量是类变量,而属于一个对象的变量是对象或者实例变量.静态变量只能有一个实例,静态方法由同一类的多个对象共享.同一类的每个对象都有自己的一组实例变量和方法.如果同步一个静态变量或者静态方法,锁就要应用于整个类上,其结果是,其它对象不能使用此类的静态变量.
ThreadStaticAttribute 类
ThreadStaticAttribute类用于在静态变量上为每个执行它的线程创建一个单独的变量,而不是在线程之间共享(默认行为)静态变量.这意味着,带有ThreadStaticAttribute的静态变量不会在访问变量的不同线程之间共享.每个访问变量的线程都有该变量的一个副本.如果一个线程修改了变量,另一个访问变量的线程就不能看到这些变化.这种行为是有背于静态变量的默认行为的.总之,ThreadStaticAttribute提供了最好的方法(静态和实例).如下代码:
{
static void Main(string[] args)
{
StaticThread ts = new StaticThread();
Thread t1 = new Thread(new ThreadStart(ts.Run));
Thread t2 = new Thread(new ThreadStart(ts.Run));
t1.Start();
t2.Start();
Thread.Sleep(1000000);
}
}
class StaticThread
{
[System.ThreadStatic]
public static int x = 1;
public static int y = 1;
public void Run()
{
for (int i = 0; i <= 10; i++)
{
Thread t2 = Thread.CurrentThread;
x++;
y++; Console.WriteLine("i=" + i + "ThreadID=" + t2.GetHashCode() + "x(static attribute)= " + x + " y = " + y);
Thread.Sleep(1000);
}
}
}
通过运行上面的代码可知:静态变量是类量,它的值在类的多个对象中保持不变,ThreadStaticAttribute允许每个访问静态变量的线程拥有自己的副本,上面的代码中,变量x 使 ThreadStaticAttribute应用于它,结果,线程t1和t2将分别拥有静态变量x的一个副本,并且线程t1对变量x的修改对线程t2来说是不可见的.另一方面,线程t1对变量y的修改对线程t2来说是可见的.
带有ThreadStaticAttribute的静态变量和实例变量之间的区别是:静态变量不需求对象访问它,但如果没有创建对象的实例就试图访问实例变量,刚会抛出异常.