线程同步方法总结
在编写多线程程序时无可避免会遇到线程的同步问题。线程同步有多种方法,总结如下:
一:volatile和synchronized
首先:volatile是变量修饰符,而synchronized则作用于一段代码或方法;
1:例如int geti1() {return i1;}
由于每个线程可以有它自己的变量拷贝,而这个变量拷贝值可以和“主”内存区域里存放的不同。导致存在一种可能:“主”内存区域里的i1值是1,线程1里的i1值是2,线程 2里的i1值是3——这在线程1和线程2都改变了它们各自的i1值,而且这个改变还没来得及传递给“主”内存区域或其他线程时就会发生。
2:volatile int i2; int geti2() {return i2;}
geti2()得到的是“主”内存区域的i2数值。volatile的含义就是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我。因此,当多线程同时访问该变量时,都将直接操作主存,从本质上做到了变量共享。但是,volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。
3:synchronized int geti3() {return i3;}
synchronized也同步内存:事实上,synchronized在“主”内存区域同步整个线程的内存。即synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放,监视器能强制保证代码块同时只被一个线程所执行)。
2. 线程内存的数据被消除,从“主”内存区域中读入。
3. 代码块被执行。
4. 对于变量的任何改变现在可以安全地写到“主”内存区域中。
5. 线程释放监视this对象的对象锁。
二:Lock 和Monitor
1:lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
首先lock的参数必须是基于引用类型的对象,不要是基本类型像bool,int什么的,这样根本不能同步,原因是lock的参数要求是对象,如果传入int,势必要发生装箱操作,这样每次lock的都将是一个新的不同的对象。
其次,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则。即缩小锁定范围。
lock (this):同一类的实例间能够互斥,但在两个相同类的实例间则没有互斥效果。因为使用你的类的人也许不知道你用了lock,如果他new了一个实例,并且对这个实例上锁,就很容易造成死锁。 lock(this)的缺点就是在一个线程锁定某对象之后导致整个对象无法被其他线程访问。如下:lock (theClass)锁定了类InternalClass实例,而lock (this)又来锁定本身,需要等到lock (theClass)解锁才行,从而造成死锁。
class InternalClass
{
public void TryLockThis()
{
Thread t = new Thread(ThreadFunction);
t.Start();
}
private void ThreadFunction()
{
Thread.Sleep(3000); // 延迟,等待外部对对象实例加锁
Console.WriteLine("尝试通过lock(this)加锁下面的代码块...");
while (true)
{
lock (this)
{
Console.WriteLine("执行内部锁,1秒后继续...");
Thread.Sleep(1000);
Console.WriteLine("内部锁完成...");
}
}
}
}
class ClassMain
{
private InternalClass theClass = new InternalClass();
public ClassMain()
{
theClass.TryLockThis();
Console.WriteLine("在执行内部锁之前对对象进行加锁...");
lock (theClass) // 如果注释掉这句,ThreadFunction()中的lock将执行成功
{
Console.WriteLine("对象被锁定, 在这里我们获得了一个死锁...");
while (true) { }
}
}
[STAThread]
static void Main(string[] args)
{
ClassMain cm = new ClassMain();
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
}
}
Lock(typeof(MyType)):由于typeof语句返回的是一个类的类型实例,对于一个类来说只有一个,如果在类的方法或实例方法中使用了typeof(className)这样的语句,而在类的外部又尝试对类进行加锁(而类型只有一个),同样可能导致死锁。
lock ("myLock"):
string a = "String Example";
string b = "String Example";
a和b指向的是同一个引用,而lock正是通过引用来区分并加锁临界代码段的。也就是说,如果在我们一程序的一个部分中使用了 lock("thisLock")进行加锁,而在程序的另一个位置同样也使用lock("thisLock")进行加锁,则极有可能导致一个死锁,(需要等待前一个lock("thisLock")释放锁)因而是很危险的。
所以:微软给出了个lock的建议用法:锁定一个私有的static 成员变量。
private static object privateObjectLock = new object();
2:Monitor能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可。Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false。 TryEnter能够有效的决绝长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用TryEnter,可以有效防止死锁或者长时间的等待。
if (!Monitor.TryEnter(m_monitorObject))
{
Console.WriteLine(" Can't visit Object " + Thread.CurrentThread.Name);
return;
}
try
{
Monitor.Enter(m_monitorObject);
Console.WriteLine(" Enter Monitor " + Thread.CurrentThread.Name);
Thread.Sleep(5000);
}
finally
{
Monitor.Exit(m_monitorObject);
}
3:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合。
Queue myCollection = new Queue();
lock(myCollection.SyncRoot) {
foreach (Object item in myCollection) {
}
}
等价的Monitor类 该类功效和lock类似: System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
lock关键字比Monitor简洁,其实lock就是对Monitor的Enter和Exit的一个封装。
另一个实例
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!");
}
Pulse以及PulseAll还有Wait方法是成对使用的,它们能让你更精确的控制线程之间的并发。当threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball );后,它通知threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball )阻塞自己后,它放弃了了对ball的锁定,所以threadPong得以执行。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进 程发送通知信号,如果只有一个线程被阻塞,那么请使用Pulse方法。
三:System.Threading.Interlocked
1:对于整数类型的简单操作,可以使用Interlocked来进行同步。
Interlocked.Increment(ref sum); // 1
Interlocked.Decrement(ref sum); // 0
Interlocked.Add(ref sum, 3); // 3
Console.WriteLine(Interlocked.Read(ref sum)); // 3
Console.WriteLine(Interlocked.Exchange(ref sum, 10)); // 10
// 更新一个字段仅当它符合一个特定的值时(10):
Interlocked.CompareExchange(ref sum, 123, 10); // 123
如线程循环的像缓冲区取写数据。而缓冲区只能容纳n个数据。
生产线程:
int numberOfUseSpace = 0;
//空间满的时候不再生产,以致等待
while (Interlocked.Read(ref numberOfUseSpace) == maxSpace)
{
Thread.Sleep(1000);
}
//写入数据
//增加缓冲区中数据数目
Interlocked.Increment(ref numberOfUseSpace);
消费线程:
while (Interlocked.Read(ref numberOfUseSpace) == 0)
{
Thread.Sleep(1000);
}
//取出数据
//减少缓冲区中数据数目
Interlocked.Decrement(ref numberOfUseSpace);
四:Mutex
Mutex 是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。但所需要的互操作转换更耗资源。 虽然Mutex也可以进行线程内的同步,如果是线呈内的同步,尽量还是用Monitor。
1:两线程各自按照自己时间走,1位80,二为150
private void thread1Func()
{
for (int count = 0; count < 10; count++)
{
TestFunc("Thread1 have run " + count.ToString() + " times");
Thread.Sleep(30);
}
}
private void thread2Func()
{
for (int count = 0; count < 10; count++)
{
TestFunc("Thread2 have run " + count.ToString() + " times");
Thread.Sleep(100);
}
}
private void TestFunc(string str)
{
Console.WriteLine("{0} {1}", str, System.DateTime.Now.Millisecond.ToString());
Thread.Sleep(50);
}
2:函数替换成,连续两次调用thread1之间的时间间隔约为30+50=80;连续两次调用thread2之间的时间间隔约为100+50=150mm。调用thread1和thread2之间的时间间隔为50mm。
private void TestFunc(string str)
{
lock (this)
{
Console.WriteLine("{0} {1}", str, System.DateTime.Now.Millisecond.ToString());
Thread.Sleep(50);
}
}
3:替换成,每隔50MS执行一个,具体执行哪个,顺序不知道。即Mutex只能互斥线程间的调用,但是不能互斥本线程的重复调用,即thread1中 waitOne()只对thread2中的waitOne()起到互斥的作用,但是thread1并不受本wainOne()的影响,可以调用多次,只是 在调用结束后调用相同次数的ReleaseMutex()就可以了。
private void thread1Func()
{
for (int count = 0; count < 10; count++)
{
mutex.WaitOne();
TestFunc("Thread1 have run " + count.ToString() + " times");
mutex.ReleaseMutex();
}
}
private void thread2Func()
{
for (int count = 0; count < 10; count++)
{
mutex.WaitOne();
TestFunc("Thread2 have run " + count.ToString() + " times");
mutex.ReleaseMutex();
}
}
private void TestFunc(string str)
{
Console.WriteLine("{0} {1}", str, System.DateTime.Now.Millisecond.ToString());
Thread.Sleep(50);
}
4:两个之间相互顺序执行,间隔50MS,和3 之间的区别是,线程1,2交替执行。
private void thread1Func()
{
for (int count = 0; count < 10; count++)
{
lock (this)
{
mutex.WaitOne();
TestFunc("Thread1 have run " + count.ToString() + " times");
mutex.ReleaseMutex();
}
}
}
private void thread2Func()
{
for (int count = 0; count < 10; count++)
{
lock (this)
{
mutex.WaitOne();
TestFunc("Thread2 have run " + count.ToString() + " times");
mutex.ReleaseMutex();
}
}
}
private void TestFunc(string str)
{
Console.WriteLine("{0} {1}", str, System.DateTime.Now.Millisecond.ToString());
Thread.Sleep(50);
}
五:ReaderWriterLock
1:两个原则
当一个线程正在写入数据时,其他线程不能写,也不能读。
当一个线程正在读入数据时,其他线程不能写,但能够读。
Reader-Reader,第二个不需等待,直接获得读控制权;
Reader-Writer,第二个需要等待第一个调用ReleaseReaderLock()释放读控制权后,才能获得写控制权;
Writer-Writer,第二个需要等待第一个调用ReleaseWriterLock()释放写控制权后,才能获得写控制权;
Writer-Reader,第二个需要等待第一个调用ReleaseWriterLock()释放写控制权后,才能获得读控制权。
六:SynchronizationAttribute
当我们确定某个类的实例在同一时刻只能被一个线程访问时,我们可以直接将类标识成Synchronization的。
该类实例无法被多个线程同时访问,我们说,这样的类是线程安全的。
[Synchronization(SynchronizationAttribute.REQUIRED)]
class SynchronizedClass : System.ContextBoundObject
{
public void DisplayThreadId()
{
Thread.Sleep(1000);
}
}
public class program
{
static SynchronizedClass sc = new SynchronizedClass();
static void Main()
{
Thread t0 = new Thread(ThreadProc);
Thread t1 = new Thread(ThreadProc);
t0.Start();
t1.Start();
t0.Join();
t1.Join();
}
static void ThreadProc()
{
for (int i = 0; i < 10; i++)
{
sc.DisplayThreadId();
}
}
}
七:同步事件和等待句柄
用lock和Monitor可以很好地起到线程同步的作用,但它们无法实现线程之间传递事件。如果要实现线程同步的同时,线程之间还要有交互,就要用到同步事件。同步事件是有两个状态(终止和非终止)的对象,它可以用来激活和挂起线程。
AutoResetEvent 和 ManualResetEvent。它们之间唯一不同的地方就是在激活线程之后,状态是否自动由终止变为非终止。AutoResetEvent自动变 为非终止,就是说一个AutoResetEvent只能激活一个线程。而ManualResetEvent要等到它的Reset方法被调用,状态才变为非 终止,在这之前,ManualResetEvent可以激活任意多个线程。 可以调用WaitOne、WaitAny或WaitAll来使线程等待事件。
如下的生产者,消费者操作流程
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
//三个线程操作队列,生产者,消费者,主线程显示
public class SyncEvents
{
public SyncEvents()
{
_newItemEvent = new AutoResetEvent(false);
_exitThreadEvent = new ManualResetEvent(false);
_eventArray = new WaitHandle[2];
_eventArray[0] = _newItemEvent;
_eventArray[1] = _exitThreadEvent;
}
public EventWaitHandle ExitThreadEvent
{
get { return _exitThreadEvent; }
}
public EventWaitHandle NewItemEvent
{
get { return _newItemEvent; }
}
public WaitHandle[] EventArray
{
get { return _eventArray; }
}
//添加产品后,自动设置
private EventWaitHandle _newItemEvent;
//退出,手动设置,需要退出时设置。如果手动设置的话,当一个线程相应后,立即重置,导致另一个线程退不出来。
private EventWaitHandle _exitThreadEvent;
private WaitHandle[] _eventArray;
}
public class Producer
{
public Producer(Queue<int> q, SyncEvents e)
{
_queue = q;
_syncEvents = e;
}
public void ThreadRun()
{
int count = 0;
Random r = new Random();
//等待退出信号,实时循环,如果设置了退出信号,则返回TRUE
while (!_syncEvents.ExitThreadEvent.WaitOne(0, false))
{
lock (((ICollection)_queue).SyncRoot)
{
while (_queue.Count < 20)
{
_queue.Enqueue(r.Next(0, 100));
//生产后,重置
_syncEvents.NewItemEvent.Set();
count++;
}
}
}
Console.WriteLine("Producer thread: produced {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class Consumer
{
public Consumer(Queue<int> q, SyncEvents e)
{
_queue = q;
_syncEvents = e;
}
public void ThreadRun()
{
int count = 0;
//如果事件1被激活,说明需要退出程序,如果0被激活,说明有产品了。
while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
{
lock (((ICollection)_queue).SyncRoot)
{
int item = _queue.Dequeue();
}
count++;
}
Console.WriteLine("Consumer Thread: consumed {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class ThreadSyncSample
{
private static void ShowQueueContents(Queue<int> q)
{
lock (((ICollection)q).SyncRoot)
{
foreach (int item in q)
{
Console.Write("{0} ", item);
}
}
Console.WriteLine();
}
static void Main()
{
Queue<int> queue = new Queue<int>();
SyncEvents syncEvents = new SyncEvents();
Console.WriteLine("Configuring worker threads...");
Producer producer = new Producer(queue, syncEvents);
Consumer consumer = new Consumer(queue, syncEvents);
Thread producerThread = new Thread(producer.ThreadRun);
Thread consumerThread = new Thread(consumer.ThreadRun);
Console.WriteLine("Launching producer and consumer threads...");
producerThread.Start();
consumerThread.Start();
for (int i = 0; i < 4; i++)
{
Thread.Sleep(2500);
ShowQueueContents(queue);
}
Console.WriteLine("Signaling threads to terminate...");
//手动设置,线程退出
syncEvents.ExitThreadEvent.Set();
producerThread.Join();
consumerThread.Join();
}
}