我们在编程的时候有时会使用多线程来解决问题比如你的程序需要在后台处理一大堆数据但还要使用户界面处于可操作状态或者你的程序需要访问一些外部资源如数据库或网络文件等这些情况你都可以创建一个子线程去处理然而多线程不可避免地会带来一个问题就是线程同步的问题如果这个问题处理不好我们就会得到一些非预期的结果
在网上也看过一些关于线程同步的文章其实线程同步有好几种方法下面我就简单的做一下归纳
一volatile关键字
volatile是最简单的一种同步方法当然简单是要付出代价的它只能在变量一级做同步volatile的含义就是告诉处理器 不要将我放入工作内存 请直接在主存操作我(「转自 」)因此当多线程同时访问该变量时都将直接操作主存从本质上做到了变量共享
能够被标识为volatile的必须是以下几种类型
Any reference type
Any pointer type (in an unsafe context)
The types sbyte byte short ushort int uint char float bool
An enum type with an enum base type of byte sbyte short ushort int or uint
如
Code
public class A
{
private volatile int _i;
public int I
{
get { return _i; }
set { _i = value; }
}
}
但volatile并不能实现真正的同步因为它的操作级别只停留在变量级别而不是原子级别如果是在单处理器系统中是没有任何问题的变量在主存中没有机会被其他人修改因为只有一个处理器这就叫作processor SelfConsistency但在多处理器系统中可能就会有问题 每个处理器都有自己的data cach而且被更新的数据也不一定会立即写回到主存所以可能会造成不同步但这种情况很难发生因为cach的读写速度相当高flush的频率也相当高只有在压力测试的时候才有可能发生几率非常非常小
二lock关键字
lock是一种比较好用的简单的线程同步方式它是通过为给定对象获取互斥锁来实现同步的它可以保证当一个线程在关键代码段的时候另一个线程不会进来它只能等待等到那个线程对象被释放也就是说线程出了临界区用法
Code
public void Function()
{
object lockThis = new object ();
lock (lockThis)
{
// Access threadsensitive resources
}
}
lock的参数必须是基于引用类型的对象不要是基本类型像boolint什么的这样根本不能同步原因是lock的参数要求是对象如果传入int势必要发生装箱操作这样每次lock的都将是一个新的不同的对象最好避免使用public类型或不受程序控制的对象实例因为这样很可能导致死锁特别是不要使用字符串作为lock的参数因为字符串被CLR暂留就是说整个应用程序中给定的字符串都只有一个实例因此更容易造成死锁现象建议使用不被暂留的私有或受保护成员作为参数其实某些类已经提供了专门用于被锁的成员比如Array类型提供SyncRoot许多其它集合类型也都提供了SyncRoot
所以使用lock应该注意以下几点
1如果一个类的实例是public的最好不要lock(this)因为使用你的类的人也许不知道你用了lock如果他new了一个实例并且对这个实例上锁就很容易造成死锁
2如果MyType是public的不要lock(typeof(MyType))
3永远也不要lock一个字符串
三SystemThreadingInterlocked
对于整数数据类型的简单操作可以用 Interlocked 类的成员来实现线程同步存在于SystemThreading命名空间Interlocked类有以下方法Increment Decrement Exchange 和CompareExchange 使用Increment 和Decrement 可以保证对一个整数的加减为一个原子操作Exchange 方法自动交换指定变量的值CompareExchange 方法组合了两个操作比较两个值以及根据比较的结果将第三个值存储在其中一个变量中比较和交换操作也是按原子操作执行的如
Code
int i = ;
SystemThreadingInterlockedIncrement( ref i);
ConsoleWriteLine(i);
SystemThreadingInterlockedDecrement( ref i);
ConsoleWriteLine(i);
SystemThreadingInterlockedExchange( ref i );
ConsoleWriteLine(i);
SystemThreadingInterlockedCompareExchange( ref i );
Output:
四Monitor
Monitor类提供了与lock类似的功能不过与lock不同的是它能更好的控制同步块当调用了Monitor的Enter(Object o)方法时会获取o的独占权直到调用Exit(Object o)方法时才会释放对o的独占权可以多次调用Enter(Object o)方法只需要调用同样次数的Exit(Object o)方法即可Monitor类同时提供了TryEnter(Object o[int])的一个重载方法该方法尝试获取o对象的独占权当获取独占权失败时将返回false
但使用 lock 通常比直接使用 Monitor 更可取一方面是因为 lock 更简洁另一方面是因为 lock 确保了即使受保护的代码引发异常也可以释放基础监视器这是通过 finally 中调用Exit来实现的事实上lock 就是用 Monitor 类来实现的下面两段代码是等效的
Code
lock (x)
{
DoSomething();
}
等效于
object obj = ( object )x;
SystemThreadingMonitorEnter(obj);
try
{
DoSomething();
}
finally
{
SystemThreadingMonitorExit(obj);
}
关于用法请参考下面的代码
Code
private static object m_monitorObject = new object ();
[STAThread]
static void Main( string [] args)
{
Thread thread = new Thread( new ThreadStart(Do));
threadName = Thread ;
Thread thread = new Thread( new ThreadStart(Do));
threadName = Thread ;
threadStart();
threadStart();
threadJoin();
threadJoin();
ConsoleRead();
}
static void Do()
{
if ( ! MonitorTryEnter(m_monitorObject))
{
ConsoleWriteLine( Cant visit Object + ThreadCurrentThreadName);
return ;
}
try
{
MonitorEnter(m_monitorObject);
ConsoleWriteLine( Enter Monitor + ThreadCurrentThreadName);
ThreadSleep( );
}
finally
{
MonitorExit(m_monitorObject);
}
}
当线程获取了m_monitorObject对象独占权时线程尝试调用TryEnter(m_monitorObject)此时会由于无法获取独占权而返回false输出信息如下
另外Monitor还提供了三个静态方法MonitorPulse(Object o)MonitorPulseAll(Object o)和MonitorWait(Object o ) 用来实现一种唤醒机制的同步关于这三个方法的用法可以参考MSDN这里就不详述了
五Mutex
在使用上Mutex与上述的Monitor比较接近不过Mutex不具备WaitPulsePulseAll的功能因此我们不能使用Mutex实现类似的唤醒的功能不过Mutex有一个比较大的特点Mutex是跨进程的因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体尽管Mutex也可以实现进程内的线程同步而且功能也更强大但这种情况下还是推荐使用Monitor因为Mutex类是win封装的所以它所需要的互操作转换更耗资源
六ReaderWriterLock
在考虑资源访问的时候惯性上我们会对资源实施lock机制但是在某些情况下我们仅仅需要读取资源的数据而不是修改资源的数据在这种情况下获取资源的独占权无疑会影响运行效率因此Net提供了一种机制使用ReaderWriterLock进行资源访问时如果在某一时刻资源并没有获取写的独占权那么可以获得多个读的访问权单个写入的独占权如果某一时刻已经获取了写入的独占权那么其它读取的访问权必须进行等待参考以下代码
Code
private static ReaderWriterLock m_readerWriterLock = new ReaderWriterLock();
private static int m_int = ;
[STAThread]
static void Main(string[] args)
{
Thread readThread = new Thread(new ThreadStart(Read));
readThreadName = ReadThread;
Thread readThread = new Thread(new ThreadStart(Read));
readThreadName = ReadThread;
Thread writeThread = new Thread(new ThreadStart(Writer));
writeThreadName = WriterThread;
readThreadStart();
readThreadStart();
writeThreadStart();
readThreadJoin();
readThreadJoin();
writeThreadJoin();
ConsoleReadLine();
}
private static void Read()
{
while (true)
{
ConsoleWriteLine(ThreadName + ThreadCurrentThreadName + AcquireReaderLock);
m_readerWriterLockAcquireReaderLock();
ConsoleWriteLine(StringFormat(ThreadName : {} m_int : {} ThreadCurrentThreadName m_int));
m_readerWriterLockReleaseReaderLock();
}
}
private static void Writer()
{
while (true)
{
ConsoleWriteLine(ThreadName + ThreadCurrentThreadName + AcquireWriterLock);
m_readerWriterLockAcquireWriterLock();
InterlockedIncrement(ref m_int);
ThreadSleep();
m_readerWriterLockReleaseWriterLock();
ConsoleWriteLine(ThreadName + ThreadCurrentThreadName + ReleaseWriterLock);
}
}
在程序中我们启动两个线程获取m_int的读取访问权使用一个线程获取m_int的写入独占权执行代码后输出如下
可以看到当WriterThread获取到写入独占权后任何其它读取的线程都必须等待直到WriterThread释放掉写入独占权后才能获取到数据的访问权应该注意的是上述打印信息很明显显示出可以多个线程同时获取数据的读取权这从ReadThread和ReadThread的信息交互输出可以看出
七SynchronizationAttribute
当我们确定某个类的实例在同一时刻只能被一个线程访问时我们可以直接将类标识成Synchronization的这样CLR会自动对这个类实施同步机制实际上这里面涉及到同步域的概念当类按如下设计时我们可以确保类的实例无法被多个线程同时访问
) 在类的声明中添加SystemRuntimeRemotingContextsSynchronizationAttribute属性
) 继承至SystemContextBoundObject
需要注意的是要实现上述机制类必须继承至SystemContextBoundObject换句话说类必须是上下文绑定的
一个示范类代码如下
Code
[SystemRuntimeRemotingContextsSynchronization]
public class SynchronizedClass : SystemContextBoundObject
{
}
八MethodImplAttribute
如果临界区是跨越整个方法的也就是说整个方法内部的代码都需要上锁的话使用MethodImplAttribute属性会更简单一些这样就不用在方法内部加锁了只需要在方法上面加上 [MethodImpl(MethodImplOptionsSynchronized)] 就可以了MehthodImpl和MethodImplOptions都在命名空间SystemRuntimeCompilerServices 里面但要注意这个属性会使整个方法加锁直到方法返回才释放锁因此使用上不太灵活如果要提前释放锁则应该使用Monitor或lock我们来看一个例子
Code
[MethodImpl(MethodImplOptionsSynchronized)]
public void DoSomeWorkSync()
{
ConsoleWriteLine( DoSomeWorkSync() Lock held by Thread +
ThreadCurrentThreadGetHashCode());
ThreadSleep( );
ConsoleWriteLine( DoSomeWorkSync() Lock released by Thread +
ThreadCurrentThreadGetHashCode());
}
public void DoSomeWorkNoSync()
{
ConsoleWriteLine( DoSomeWorkNoSync() Entered Thread is +
ThreadCurrentThreadGetHashCode());
ThreadSleep( );
ConsoleWriteLine( DoSomeWorkNoSync() Leaving Thread is +
ThreadCurrentThreadGetHashCode());
}
[STAThread]
static void Main( string [] args)
{
MethodImplAttr testObj = new MethodImplAttr();
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkNoSync));
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkNoSync));
tStart();
tStart();
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkSync));
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkSync));
tStart();
tStart();
ConsoleReadLine();
}
这里我们有两个方法我们可以对比一下一个是加了属性MethodImpl的DoSomeWorkSync()一个是没加的DoSomeWorkNoSync()在方法中Sleep()是为了在第一个线程还在方法中时第二个线程能够有足够的时间进来对每个方法分别起了两个线程我们先来看一下结果
可以看出对于线程1和2也就是调用没有加属性的方法的线程当线程2进入方法后还没有离开线程1有进来了这就是说方法没有同步我们再来看看线程3和4当线程3进来后方法被锁直到线程3释放了锁以后线程4才进来
九同步事件和等待句柄
用lock和Monitor可以很好地起到线程同步的作用但它们无法实现线程之间传递事件如果要实现线程同步的同时线程之间还要有交互就要用到同步事件同步事件是有两个状态(终止和非终止)的对象它可以用来激活和挂起线程
同步事件有两种AutoResetEvent和 ManualResetEvent它们之间唯一不同的地方就是在激活线程之后状态是否自动由终止变为非终止AutoResetEvent自动变为非终止就是说一个AutoResetEvent只能激活一个线程而ManualResetEvent要等到它的Reset方法被调用状态才变为非终止在这之前ManualResetEvent可以激活任意多个线程
可以调用WaitOneWaitAny或WaitAll来使线程等待事件它们之间的区别可以查看MSDN当调用事件的 Set方法时事件将变为终止状态等待的线程被唤醒
来看一个例子这个例子是MSDN上的因为事件只用于一个线程的激活所以使用 AutoResetEvent 或 ManualResetEvent 类都可以
Code
static AutoResetEvent autoEvent;
static void DoWork()
{
ConsoleWriteLine( worker thread started now waiting on event);
autoEventWaitOne();
ConsoleWriteLine( worker thread reactivated now exiting);
}
[STAThread]
static void Main(string[] args)
{
autoEvent = new AutoResetEvent(false);
ConsoleWriteLine(main thread starting worker thread);
Thread t = new Thread(new ThreadStart(DoWork));
tStart();
ConsoleWriteLine(main thrad sleeping for second);
ThreadSleep();
ConsoleWriteLine(main thread signaling worker thread);
autoEventSet();
ConsoleReadLine();
}
我们先来看一下输出
在主函数中首先创建一个AutoResetEvent的实例参数false表示初始状态为非终止如果是true的话初始状态则为终止然后创建并启动一个子线程在子线程中通过调用AutoResetEvent的WaitOne方法使子线程等待指定事件的发生然后主线程等待一秒后调用AutoResetEvent的Set方法使状态由非终止变为终止重新激活子线程