Interlocked类中的每个方法都执行一次原子性的读取以及写入操作,其中public static Int32 Increment(ref Int32 location)方法是最常用到的方法,后面我在自定一个混合结构锁的时候就会用到。
public static class Interlocked { // return (++location) public static Int32 Increment(ref Int32 location); // return (--location) public static Int32 Decrement(ref Int32 location); // return (location1 += value) public static Int32 Add(ref Int32 location1, Int32 value); // Int32 old = location1; location1 = value; return old; public static Int32 Exchange(ref Int32 location1, Int32 value); // Int32 old = location1; // if (location1 == comparand) location1 = value; // return old; public static Int32 CompareExchange(ref Int32 location1, Int32 value, Int32 comparand); ... }
假如有多个线程调用了Enter方法,那么只有一个线程能满足条件进入while的内部,其他线程都因为不满足条件而在不断的判断while条件。
exchange方法确保第一个调用的线程将m_ResourceInUse变为1,并且原始值为0.而其他线程将会使得m_ResourceInUse从1变为1,也就是原始值为1,不满足条件。
class SimpleSpinLock { private Int32 m_ResourceInUse; // 0=false (default), 1=true public void Enter() { // Set the resource to in-use and if this thread while (Interlocked.Exchange(ref m_ResourceInUse, 1) != 0) { } } public void Leave() { Thread.VolatileWrite(ref m_ResourceInUse, 0); } }
public sealed class SomeResource { private SimpleSpinLock m_sl = new SimpleSpinLock(); public void AccessResource() { m_sl.Enter(); // Only one thread at a time can get in here to access the resource... m_sl.Leave(); } }
exchange是原子判断true和false的一个常用办法。
int a = 0; a++;
当编译器把这行C#语句编译成汇编代码的时候,将会包含多条指令,如:
INC EAX
MOV [a], EAX
int a = 0; int tmp = a; tmp++; a = tmp;
注意,纵向是时间线,#n表示当前时候a的值。
我们的原本想法应该是这样执行:
但是由于抢占式操作系统线程的推进是不可预测的,真正执行的时候可能是这样
在上面的执行流程中,t1首先更新为1,然后t2更新为2.此时,从系统中其他线程的角度来看,似乎一切都正常。
然后,此时t3被唤醒继续执行,它将覆盖t1和t2的执行结果,重新将a的值设置为1.
这是一个典型的数据竞争问题,之所以称为“竞争”,是因为代码执行的正确性完全依赖于多个线程之间的竞争结果。每个线程都试图最先执行完代码,并且根据哪个线程最先执行完成的不同,会导致不同的结果。也就是相同的源代码,不同的执行结果。
class SimpleHybridLock : IDisposable { private Int32 m_waiters = 0;
// The AutoResetEvent is the primitive kernel-mode construct private AutoResetEvent m_waiterLock = new AutoResetEvent(false); public void Enter() { if (Interlocked.Increment(ref m_waiters) == 1) //what will happen if we use m_waiters++ in this place? return; //return means we enter critical region// Another thread is waiting. There is contention, block this thread m_waiterLock.WaitOne(); // Bad performance hit here // When WaitOne returns, this thread now has the lock } public void Leave() { // This thread is releasing the lock if (Interlocked.Decrement(ref m_waiters) == 0) return; // No other threads are blocked, just return // Other threads are blocked, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } public void Dispose() { m_waiterLock.Dispose(); } }
我们用一个int私有字段来计数,确保只有一个线程调用该方法的时候不会调用到非常影响性能的内核对象。只有多个线程并发的访问这个方法的时候,才会初始化内核对象,阻塞线程。
我们可以给这个锁加入更多的功能,这时我们需要保存更多的信息,也就需要更多的字段,比如说保存哪个线程拥有这个锁,以及它拥有了多少次。在多个线程并发访问的时候,我们也可以推迟一段时间再创建内核对象,可以加入spin lock先自旋一段时间。
internal sealed class AnotherHybridLock : IDisposable { // The Int32 is used by the primitive user-mode constructs (Interlocked methods) private Int32 m_waiters = 0; // The AutoResetEvent is the primitive kernel-mode construct private AutoResetEvent m_waiterLock = new AutoResetEvent(false); // This field controls spinning in an effort to improve performance private Int32 m_spincount = 4000; // Arbitrarily chosen count // These fields indicate which thread owns the lock and how many times it owns it private Int32 m_owningThreadId = 0, m_recursion = 0; public void Enter() { // If calling thread already owns the lock, increment recursion count and return Int32 threadId = Thread.CurrentThread.ManagedThreadId; if (threadId == m_owningThreadId) { m_recursion++; return; } // The calling thread doesn't own the lock, try to get it SpinWait spinwait = new SpinWait(); for (Int32 spinCount = 0; spinCount < m_spincount; spinCount++) { // If the lock was free, this thread got it; set some state and return if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0) goto GotLock; // Black magic: give other threads a chance to run // in hopes that the lock will be released spinwait.SpinOnce(); } // Spinning is over and the lock was still not obtained, try one more time if (Interlocked.Increment(ref m_waiters) > 1) { // Other threads are blocked and this thread must block too m_waiterLock.WaitOne(); // Wait for the lock; performance hit // When this thread wakes, it owns the lock; set some state and return } GotLock: // When a thread gets the lock, we record its ID and // indicate that the thread owns the lock once m_owningThreadId = threadId; m_recursion = 1; } public void Leave() { // If the calling thread doesn't own the lock, there is a bug Int32 threadId = Thread.CurrentThread.ManagedThreadId; if (threadId != m_owningThreadId) throw new SynchronizationLockException("Lock not owned by calling thread"); // Decrement the recursion count. If this thread still owns the lock, just return if (--m_recursion > 0) return; m_owningThreadId = 0; // No thread owns the lock now // If no other threads are blocked, just return if (Interlocked.Decrement(ref m_waiters) == 0) return; // Other threads are blocked, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } public void Dispose() { m_waiterLock.Dispose(); } }
当然锁变复杂了,性能也会有相应的降低。有所得有所失去。
Sync block
堆上的每个对象都可以关联一个叫做Sync block(同步块)的数据结构。同步块包含字段,这些字段和上面我们实现的锁中的字段的作用是差不多的。具体地说,它为一个内核对象、拥有线程的ID、递归计数器、等待线程的计数提供了保存的地方。
照例配图一张,要不光看我文字描述不太容易懂:
因此同步块干啥子的?用来保存数据的呗……
当然,同步块也不是一开始就上的,上面这张图隐藏了点信息。就是其实那个指向同步块的指针有2个指针大小的内存,还保存着hashcode的值还有一些其他 东西。如果块内存不足以保存这些信息,那么才会为这个对象分配一个共享内存池中的同步块。这就是Object Header Inflation现象。
懂得相同之处了,再来理解为什么锁type类型危险的,究其原因就是type能被很多地方访问,甚至能跨appdomain,这就很有可能你莫名其妙就和另一 个appdomain中的锁用到同一个同步块了。同样情况的类型还有于AppDomain无关的反射类型,比如说啥子MemberInfo之类的。
class Program { static void Main(string[] args) { var syncTest = new SyncTest(); Thread t1 = new Thread(syncTest.LongSyncMethod); // critical region 1 t1.Start(); Thread t2 = new Thread(syncTest.NoSyncMethod); t2.Start(); Thread t3 = new Thread(syncTest.LongSyncMethod);// critical region 1 t3.Start(); Thread t4 = new Thread(syncTest.NoSyncMethod); t4.Start(); Thread t5 = new Thread(syncTest.NoSyncMethod); t5.Start(); Thread t6 = new Thread(syncTest.SyncMethodUsingPrivateObject);// critical region 2 t6.Start(); Thread t7 = new Thread(syncTest.SyncMethodUsingPrivateObject);// critical region 2 t7.Start(); } } class SyncTest { private object _lock = new object(); [MethodImplAttribute(MethodImplOptions.Synchronized)] public void LongSyncMethod() { Console.WriteLine("being asleep"); Thread.Sleep(10000); } public void NoSyncMethod() { Console.WriteLine("do sth"); } public void SyncMethodUsingPrivateObject() { lock (_lock) { Console.WriteLine("another critical section"); Thread.Sleep(5000); } } }
bool acquired = false; object tmp = listLock; try { Monitor.Enter(tmp, ref acquired); list.Add("item"); } finally { if (acquired) { Monitor.Release(tmp); } }
public class BlockingQueue<T> { private Queue<T> m_queue = new Queue<T>(); private int m_waitingConsumers = 0; public int Count { get { lock (m_queue) return m_queue.Count; } } public void Clear() { lock (m_queue) m_queue.Clear(); } public bool Contains(T item) { lock (m_queue) return m_queue.Contains(item); } public void Enqueue(T item) { lock (m_queue) { m_queue.Enqueue(item); // Wake consumers waiting for a new element. if (m_waitingConsumers > 0) Monitor.Pulse(m_queue); } } public T Dequeue() { lock (m_queue) { while (m_queue.Count == 0) { //Queue is empty, wait until en element arrives. 644 Chapter 12: Parallel Containers m_waitingConsumers++; try { Monitor.Wait(m_queue); } finally { m_waitingConsumers--; } } return m_queue.Dequeue(); } } public T Peek() { lock (m_queue) return m_queue.Peek(); } }
4.多线程之旅之四——浅谈内存模型和用户态同步机制
最后,如果你觉得文章还不错,请点击右下角的推荐,谢谢!