<二>线程同步
一、简介
当多个线程访问同一个共享资源时,会造成竞争,死锁等问题,那么同步这些线程使得对共享对象的操作就尤为重要了。线程同步有很多方式,第一种是用lock锁定资源,避免资源被多个线程同时修改,第二种是使用原子操作,一个操作只占用一个量子的时间,只有当前的操作完成后,才能进行下一个操作。无须实现其他线程等待当前操作完成,这就避免了使用锁,也排除了死锁的情况。
- Interlocked(为多个线程提供原子操作)
lock(this) { if(class==null) { class=NewClass; } } 等价 Interlocked.CompareExchange<Class>(ref class, NewClass, null)
- Mutex(互斥锁),用于多线程中防止两条线程同时对一个公共资源进行读写的机制。当两个或两个以上的线程同时访问共享资源时,操作系统需要一个同步机制来确保每次只有一个线程使用资源。
如下例子,当主程序启动时,定义了一个指定名称的互斥量MutexName,Mutex(false,MutextNmae)第一个参数是线程是否拥有权,true:其他程序无法获取该互斥量,false其他程序可以获取此互斥量。WaitOne() 方法用于等待资源被释放, ReleaseMutex() 方法用于释放资源。
TimeSpan.FromSeconds表示等待5秒获取互斥量。启动第一个程序时,m.waitone获取不到互斥量被其他进程using,所以执行First is Running。再打开第二个程序,执行waitone时获取到互斥量别其他程序using,那么等待5秒钟,5秒钟内扫描互斥量是否被释放,如果5秒内第一个程序按下任何键后执行了ReleaseMutex,那么互斥量被释放,第二个程序Waitone会返回false,并执行First Is Runing。如果5秒内互斥量没有被释放,则Waitone 返回true,执行Second is running。
const string MutexName = "MutexThreedName"; using (var m = new Mutex(false, MutexName)) { if (!m.WaitOne(TimeSpan.FromSeconds(5), false)) { Console.WriteLine(" Second is running ! "); Console.ReadLine(); } else { Console.WriteLine("First is Running ! "); Console.ReadLine(); m.ReleaseMutex(); } }
-
Semaphore(信号量)
信号量是为了限制线程运行的数量,它内部有个计数器,如果一个线程调用了这个信号量,那么计数器会相应减一,直到为时,其他调用这个信号量的线程就会进入到阻塞状态。线程处理完后调用Release方法释放信号量,使得计数器加一。如下例子:定义信号量为4个,同时启用6个线程。当前4个进入waiting之后,第五个会等其他线程释放信号量即complete后才会执行wating。
SemaphoreSlim _semaphore = new SemaphoreSlim(4); for (int i = 1; i <= 6; i++) { string threadName ="Thread " + i; int secondsTowait =2 * i; var t = new Thread(()=> Work(threadName,secondsTowait)); t.Start(); } void Work(string name, int seconds) { Console.WriteLine("{0} begin", name); _semaphore.Wait(); Console.WriteLine("{0} waitting",name); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine("{0} completed", name); _semaphore.Release(); }
-
AutoResetEvent(线程通信类)
我们在线程编程的时候往往会涉及到线程的通信,通过信号的接受来进行线程是否阻塞的操作。AutoResetEvent 允许线程通过发信号互相通信。通常,此通信涉及线程需要独占访问的资源。最常用方法中就有Set()和WaitOne()。线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号。如果 AutoResetEvent 处于非终止状态,则该线程阻塞,并等待当前控制资源的线程通过调用 Set 发出资源可用的信号。
AutoResetEvent myEvent = new AutoResetEvent(false); 构造函数中的参数false就代表该状态为非终止状态,相反若为true则为终止状态。
如下例子:定义两个通信事件,再定义一个线程与主线程通信,当主线程启动子线程后,等待子线程执行完的信号,当子线程走执行完第一个任务时,发出资源可用信号,并等待主线程执行第一步操作。主线程执行第一步操作后发出主线程资源可用,触发子线程执行第二个任务的执行。
AutoResetEvent _workerEvent = new AutoResetEvent(false); AutoResetEvent _mainEvent = new AutoResetEvent (false) ; var t = new Thread(() => Work(10) ); t.Start() ; Console.WriteLine(" waiting for work thread to complete work"); _workerEvent.WaitOne(); Console.WriteLine("first step on main thread" ); Thread.Sleep(TimeSpan.FromSeconds(5)); _mainEvent.Set(); Console.WriteLine("second step on main thread" ); _workerEvent.WaitOne(); Console.WriteLine("second step completed! "); void Work(int seconds) { Console.WriteLine("starting first work"); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine("first work is done! "); _workerEvent.Set(); Console.WriteLine("waiting main thread to complete first step"); _mainEvent.WaitOne(); Console.WriteLine("starting second word" ); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine("second work is done! "); _workerEvent.Set(); }
- ManualResetEventSlim(线程通信类)
ManualResetEventSlim的整个工作方式有点像人群通过大门,之前的AutoResetEvent事件像一个旋转门,一次只允许一人通过。ManualResetEventSlim是大门敞开直到手动调用Reset方法。当调用Set时,相当于打开了大门从而允许准备好当线程接收信号并继续工作。当调用Reset相当于关闭了大门。
ManualResetEventSlim _mainEvent = new ManualResetEventSlim(false); var t1 = new Thread(() => Work("Thread 1", 5)); var t2 = new Thread(() => Work("Thread 2", 6)); var t3 = new Thread(() => Work("Thread 3", 12)); t1.Start(); t2.Start(); t3.Start(); Thread.Sleep(TimeSpan.FromSeconds(6)); Console.WriteLine("The gates are now open!"); _mainEvent.Set(); Thread.Sleep(TimeSpan.FromSeconds(2)); _mainEvent.Reset(); Console.WriteLine("The gates have been closed!"); Thread.Sleep(TimeSpan.FromSeconds(10)); Console.WriteLine("The gates are now open for the second time!"); _mainEvent.Set(); Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("The gates have been closed!"); _mainEvent.Reset(); void Work(string threadName, int seconds) { Console.WriteLine($"{threadName} falls to sleep"); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{threadName} waits for the gates to open!"); _mainEvent.Wait(); Console.WriteLine($"{threadName} enters the gates!"); }
- CountDownEvent(通信类)
针对需要等待多个异步操作完成后触发。如果调用Signal()没达到指定的次数,那么Wait()将一直等待。确保使用CountdownEvent时,所有线程完成后都要调用Signal方法。
CountdownEvent _countdown = new CountdownEvent(2); Console.WriteLine("Starting "); var t1 = new Thread(() => Work("thread 1 is completed", 4000)); var t2 = new Thread(() => Work("thread 2 is completed", 8000)); t1.Start(); t2.Start(); _countdown.Wait(); Console.WriteLine("2 thread have been completed."); _countdown.Dispose(); void Work(string message, int seconds) { Thread.Sleep(seconds); Console.WriteLine(message); _countdown.Signal(); }
- Barrier (通信类)
Barrier 类非常适用于其中工作有多个任务分支且以后又需要合并工作的情况。一组任务并行地运行一连串的阶段,但是每一个阶段都要等待所有其他任务都完成前一阶段之后才能开始.
Barrier _barrier = new Barrier(2,b=> Console.WriteLine ( "End of phase {0}",b.CurrentPhaseNumber + 1)); var t1 = new Thread(() => PlayMusic("the guitarist ", "play an amazing solo", 5)); var t2 = new Thread(() => PlayMusic("the singer","sing his song", 2)); t1.Start(); t2.Start(); void PlayMusic(string name, string message,int seconds) { for (int i = 1; i < 3; i++) { Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine("{0} starts to {1}", name, message); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine(" {0} finishes to {1} ", name, message); _barrier.SignalAndWait(); } }
- ReaderWriterLockSlim(读写锁)
当资源处于写入模式时,其他线程写入需要等待本次写入结束之后才能继续写入。允许多个线程同时获取读锁,但同一时间只允许一个线程获得写锁,因此也称作共享-独占锁。
ReaderWriterLockSlim _rw = new ReaderWriterLockSlim(); Dictionary<int, int> _items =new Dictionary<int, int>(); new Thread(Read) { IsBackground = true }.Start(); new Thread(Read) { IsBackground = true }.Start(); new Thread(Read) { IsBackground = true }.Start(); new Thread(() => write("Thread 1")) { IsBackground = true }.Start(); new Thread(() => write("Thread 2")) { IsBackground = true }.Start(); Thread.Sleep(TimeSpan.FromSeconds(30)); void Read() { while (true) { try { _rw.EnterReadLock(); Console.WriteLine("Reading contents of a dictionary({0})",_items.Keys.Count); Thread.Sleep(TimeSpan.FromSeconds(0.1)); //独占读取0.1秒 } finally { _rw.ExitReadLock(); } } } void write(string threadName) { while (true) { try { int newKey = new Random().Next(250); _rw.EnterUpgradeableReadLock(); if (!_items.ContainsKey(newKey)) { try { _rw.EnterWriteLock(); _items[newKey] = 1; Console.WriteLine(" New key {0} is added to a dictionary by a {1} ", newKey, threadName); Thread.Sleep(TimeSpan.FromSeconds(4));//独占4秒,不让读写 } finally { _rw.ExitWriteLock(); } } Thread.Sleep(TimeSpan.FromSeconds(2));//允许读,2秒内不让写 } finally { _rw.ExitUpgradeableReadLock(); } } }
- SpinWait(自旋等待)
我们经常会用到Thread.Sleep(timeout_ms)来等待或者腾出时间来让其他线程处理。调用Thread.Sleep时,是让内核暂停处理当前的线程,然后再看需要等待多久,当发现等待时间是timeout_ms时,就等待timeout_ms长的时间,然后内核继续运行该线程。即使timeout_ms为0,由于这个过程中内核已经执行了暂停和恢复的动作,所以会消耗时间。SpinWait.SpinUntil在执行等待时,会先进行自旋。自旋就是在CPU运转的周期内,如果条件满足了,就不会再进入内核等待(即暂停该线程,等待一段时间后,再继续运行该线程),如果条件不满足,才进入内核等待。
int _count = 1000; ThreadSleepInThread(0); SpinWaitInThread(0); ThreadSleepInThread(10); SpinWaitInThread(10); Console.ReadLine(); void ThreadSleepInThread(int second) { Thread thread = new Thread(() => { var sw = Stopwatch.StartNew(); for (int i = 0; i < _count; i++) { Thread.Sleep(second); } Console.WriteLine("Thread Sleep {0} Second Consume Time:{01}", second, sw.Elapsed.ToString()); }); thread.IsBackground = true; thread.Start(); } void SpinWaitInThread(int second) { Thread thread = new Thread(() => { var sw = Stopwatch.StartNew(); for (int i = 0; i < _count; i++) { SpinWait.SpinUntil(() => true, second); } Console.WriteLine("SpinWait {0} Second Consume Time:{1}", second, sw.Elapsed.ToString()); }); thread.IsBackground = true; thread.Start(); }