线程同步的问题
1) 我们必须找到代码中所有可能被多个线程同时访问的资源,然后使用线程同步来保护资源,并且我们没有办法来验证是不是正确进行了线程同步,包括是否有遗漏和是否对不需要同步的资源进行同步。
2) 线程同步是有损性能的,如果某个操作大量执行,并且这个操作原先的执行时间非常短,那么如果我们对这段操作前后进行锁的申请和释放的话性能可能下降一个数量级。
3) 线程同步可能导致更频繁的线程创建和上下文的切换。
当然,只有在下面的情况下才需要线程同步,换句话说我们尽量不要使用下面的方案来导致线程同步:
1) 只有写操作, 如果只是访问只读对象,即使多个线程同时访问也不会改变资源,因此不需要同步,当然如果有写操作的话,即使是读操作也要考虑是不是需要同步。
2) 引用类型或者是可变的类型,如果访问的都是值类型(这里不说比如寄存器缓存的特殊情况)或者说类型是不可变的(比如string),那么我们对类型的获取和改写都是基于新的类型而不是多个线程的共享资源,因此不需要同步。
3) static静态资源,这也就是资源访问域的问题,如果这个资源并不会被多个线程访问到,也就是这个资源属于每个线程本身的,那么当然不需要同步。
最后,微软确保FCL所有静态方法线程安全,但由于性能问题FCL不保证所有实例方法线程安全,我们需要自己实现线程同步。对于我们自己的类库最好也遵从这个标准。
两种同步结构
1) 用户模式,硬件提供支持,速度非常快,但是在block的时候消耗CPU资源,在未争用的时候不损失性能
2) 内核模式,操作系统提供支持,速度比较慢,但是很灵活(比如可以设定超时时间,可以等待一组同步结构都可用的时候继续)并且和用户模式想法的是在block的时候可以不消耗CPU
作者进行了一个性能测试:
class Program { static void Main(string[] args) { Int32 x = 0; const Int32 iterations = 5000000; Stopwatch sw = Stopwatch.StartNew(); SimpleSpinLock ssl = new SimpleSpinLock(); for (Int32 i = 0; i < iterations; i++) { ssl.Enter(); x++; ssl.Leave(); } Console.WriteLine("Incrementing x in SimpleSpinLock: {0:N0}", sw.ElapsedMilliseconds); using (SimpleWaitLock swl = new SimpleWaitLock()) { sw = Stopwatch.StartNew(); for (Int32 i = 0; i < iterations; i++) { swl.Enter(); x++; swl.Leave(); } Console.WriteLine("Incrementing x in SimpleWaitLock: {0:N0}", sw.ElapsedMilliseconds); } } } struct SimpleSpinLock { private Int32 m_ResourceInUse; public void Enter() { while (Interlocked.Exchange(ref m_ResourceInUse, 1) != 0) { } } public void Leave() { Thread.VolatileWrite(ref m_ResourceInUse, 0); } } class SimpleWaitLock : IDisposable { private AutoResetEvent m_ResourceFree = new AutoResetEvent(true); public void Enter() { m_ResourceFree.WaitOne(); } public void Leave() { m_ResourceFree.Set(); } public void Dispose() { m_ResourceFree.Close(); } }
结果表明基于内核模式的同步比基于用户模式的同步慢了数十倍:
有关内核模式的各种结构我们在之前的文章中介绍了很多,在这里再补充一下两种用户模式的同步结构:
1) Volatile
两个作用一是防止编译器JIT甚至CPU对代码和指令等进行优化(比如跳过判断,调整代码次序),二是防止多核CPU分别在自己的寄存器中缓存了值导致的不一致性,让值始终从内存中读取。
2) Interlocked
Interlocked.Exchange和Interlocked.CompareExchange分别用于确保赋值操作和比较赋值操作的原子性,值得一提的是,Interlocked.Exchange并没有提供bool重载,我们可以使用int的0和1替代。还有,Interlocked.Read只提供了long重载,那是因为读一个long值的任何操作在32位操作系统上不是原子行为,而在64位操作系统上long的读操作是原子的。