首先,肯定的一点:Microsoft的Framework Class Library(FCL)保证了所有静态方法都是线程安全的。
FCL不保证实例方法是线程安全的。因为假如全部添加锁定,会造成性能的巨大损失。另外,假如每个实例方法都需要获取和释放一个锁,事实上会造成最终在任何给定的时刻,你的应用程序只有一个线程在运行,这对性能的影响显而易见。
下面介绍基元线程同步构造。
基元:是指可以在代码中使用的最简单的构造。有两种基元构造:用户模式(user-mode)和内核模式(kernel-mode)。
用户模式
使用了特殊的CPU指令来协调线程。
技术:volatile关键字、Interlocked类(互锁)、spinlock(自旋锁)
常见锁①:volatile 关键字指示一个字段可以由多个同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。
Interlocked类: 为多个线程共享的变量提供原子操作。。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
常见锁②:SpinLock 结构是一个低级别的互斥同步基元,它在等待获取锁时进行旋转。在多核计算机上,当等待时间预计较短且极少出现争用情况时,SpinLock 的性能将高于其他类型的锁。即使 SpinLock 未获取锁,它也会产生线程的时间片。 它这样做是为了避免线程优先级别反转,并使垃圾回收器能够继续执行。 在使用 SpinLock 时,请确保任何线程持有锁的时间不会超过一个非常短的时间段,并确保任何线程在持有锁时不会阻塞。
优点:
应尽量使用基元用户模式构造,它们的速度要显著快于内核模式的构造。
协调线程的在硬件中发生的(所以才这么快)。
但是Microsoft Windows操作系统永远检测不到一个线程在基元用户模式的构造上阻塞了。
由于在用户模式的基元构造上阻塞的线程池永远不认为已堵塞,所以线程池不会创建新线程来替换这种临时的线程。
这些CPU指令只阻塞线程相当短的时间。
缺点:
只有Windows操作系统内核才能停止一个线程的运行(防止它浪费CPU的时间)。
在用户模式中运行的线程可能被系统抢占,但线程会以最快的速度再次调度。
想要取得资源但暂时取不到的线程会一直在用户模式中“自旋”,这可能浪费大量的CPU时间。线程一直在一个CPU上运行,我们称为“活锁”(livelock)。
实例:
using System;using System.Threading;public class Worker { // This method is called when the thread is started. public void DoWork() { while (!_shouldStop) { Console.WriteLine("Worker thread: working..."); } Console.WriteLine("Worker thread: terminating gracefully."); } public void RequestStop() { _shouldStop = true; } // Keyword volatile is used as a hint to the compiler that this data // member is accessed by multiple threads. private volatile bool _shouldStop; }public class WorkerThreadExample { static void Main() { // Create the worker thread object. This does not start the thread. Worker workerObject = new Worker(); Thread workerThread = new Thread(workerObject.DoWork); // Start the worker thread. workerThread.Start(); Console.WriteLine("Main thread: starting worker thread..."); // Loop until the worker thread activates. while (!workerThread.IsAlive) ; // Put the main thread to sleep for 1 millisecond to // allow the worker thread to do some work. Thread.Sleep(1); // Request that the worker thread stop itself. workerObject.RequestStop(); // Use the Thread.Join method to block the current thread // until the object's thread terminates. workerThread.Join(); Console.WriteLine("Main thread: worker thread has terminated."); } // Sample output: // Main thread: starting worker thread... // Worker thread: working... // Worker thread: working... // Worker thread: working... // Worker thread: working... // Worker thread: working... // Worker thread: working... // Worker thread: terminating gracefully. // Main thread: worker thread has terminated.}
内核模式
由Windows操作系统自身提供的。它们要求在应用程序的线程中调用有操作系统内核实现的函数。
技术:EventWaitHandle(事件)、Semaphore(信号量)、Mutex(互斥体)
System.Object System.MarshalByRefObject System.Threading.WaitHandle System.Threading.EventWaitHandle System.Threading.Mutex System.Threading.Semaphore
常见锁③:Mutex 类是 Win32 构造的包装,它可以跨应用程序域边界进行封送处理,可用于多个等待,并且可用于同步不同进程中的线程。
优点:
线程通过内核模式的构造获取其他线程拥有的资源时,Windows会阻塞线程以避免它浪费CPU时间。当资源变得可用时,Windows会恢复线程,允许它访问资源。它不会占着一个CPU“自旋”。
可实现本机和托管线程相互之间的同步。
可同步在同一台机器的不同进程中运行的线程。
可应用安全性设置,防止未经授权的账户访问它们。
线程可一直阻塞,直到及合作的所有内核模式构造都可用,或者直到集合中的任何内核模式构造可用。
在内核模式的构造上阻塞的线程可指定超时值:指定时间内访问不到希望的资源,线程就可以解除阻塞并执行其他任务。
缺点:
将线程从用户模式切换为内核模式(或者相反)会招致巨大的性能损失,这正是为什么要避免使用内核构造的原因。另外,线程一直阻塞,会导致“死锁“(deadlock)。
死锁总是由于活锁,因为活锁即浪费CPU时间,有浪费内存(线程栈等),而死锁只浪费内存。
混合构造
兼具上面两者的长处。在没有竞争的情况下,快而且不会阻塞(就像用户模式)。在有竞争的情况,希望它被操作系统内核阻塞。
技术:ManualResetEventSlim类、SemaphoreSlim类、Monitor类、Lock类、ReaderWriterLockSlim类、CountdownEvent类、Barrier类、双检锁.
常见锁④:Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它比Mutex可以更好地利用资源。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
常见锁⑤:使用 lock (C#) 或 SyncLock (Visual Basic) 关键字是Monitor的封装。通常比直接使用 Monitor 类更可取,一方面是因为 lock 或 SyncLock 更简洁,另一方面是因为lock 或 SyncLock 确保了即使受保护的代码引发异常,也可以释放基础监视器。
常见锁⑥:ReaderWriterLock 锁,在某些情况下,可能希望只在写入数据时锁定资源,在不更新数据时允许多个客户端同时读取数据。ReaderWriterLock 类在线程修改资源时将强制其独占访问资源,但在读取资源时则允许非独占访问。 ReaderWriter 锁可用于代替排它锁。使用排它锁时,即使其他线程不需要更新数据,也会让这些线程等待。
双检锁
常见锁⑦:双重检查锁定模式(也被称为”双重检查加锁优化”,”锁暗示”(Lock hint)) 是一种软件设计模式用来减少并发系统中竞争和同步的开销。
双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。
它通常用于减少加锁开销,尤其是为多线程环境中的单例模式实现“惰性初始化”。惰性初始化的意思是直到第一次访问时才初始化它的值。
public sealed class Singleton { private static volatile Singleton instance = null; private static object syncRoot = new Object(); private static int count = 100; private Singleton() { } public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } public static Singleton GetSingleton() { if (instance == null) { lock (syncRoot) { if (instance == null) { instance = new Singleton(); } } } return instance; } public override string ToString() { lock (syncRoot) { int buf = --count; Thread.Sleep(20); return buf + "_" + count.ToString(); } } }static void Main(string[] args) { Task<string>[] tasks = new Task<string>[10]; Stopwatch watch = new Stopwatch(); object syncRoot = new Object(); watch.Start(); for (int i = 0; i < tasks.Length; i++) { tasks[i] = Task.Factory.StartNew<string>(() =>(Singleton.GetSingleton().ToString())); } Task.WaitAll(tasks, 5000);//设置超时5s watch.Stop(); Console.WriteLine("Tasks running need " + watch.ElapsedMilliseconds + " ms." + "\n"); for (int i = 0; i < tasks.Length; i++) { //超时处理 if (tasks[i].Status != TaskStatus.RanToCompletion) { Console.WriteLine("Task {0} Error!", i + 1); } else { //save result Console.WriteLine("Tick ID is " + tasks[i].Result); Console.WriteLine(); } } for (int i = 0; i < 3; i++) { Console.WriteLine("Main thread do work!"); Thread.Sleep(200); } Console.ReadKey(); }
输出:
Tasks running need 298 ms. Tick ID is 96_96 Tick ID is 99_99 Tick ID is 97_97 Tick ID is 98_98 Tick ID is 95_95 Tick ID is 94_94 Tick ID is 93_93 Tick ID is 92_92 Tick ID is 91_91 Tick ID is 90_90 Main thread do work! Main thread do work! Main thread do work!
以上就是从0自学C#12--线程同步解决方法汇总以及优缺点的内容,更多相关内容请关注PHP中文网(www.php.cn)!