线程同步复习
线程同步的方式
线程同步有:临界区、互斥区、事件、信号量四种方式 临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
2、互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享
3、信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目 4、事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
C#中常见线程同步方法
1、Interlocked 为多个线程共享的变量提供原子操作。
根据经验,那些需要在多线程情况下被保护的资源通常是整型值,且这些整型值在多线程下最常见的操作就是递增、递减或相加操作。Interlocked类提供了一个专门的机制用于完成这些特定的操作。这个类提供了Increment、Decrement、Add静态方法用于对int或long型变量的递增、递减或相加操作。此类的方法可以防止可能在下列情况发生的错误:计划程序在某个线程正在更新可由其他线程访问的变量时切换上下文;或者当两个线程在不同的处理器上并发执行时。 此类的成员不引发异常。
Increment 和 Decrement 方法递增或递减变量并将结果值存储在单个操作中。 在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
1)将实例变量中的值加载到寄存器中。 2)增加或减少该值。 3)在实例变量中存储该值。 如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。 然后由另一个线程执行所有三个步骤。 当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。
Exchange 方法自动交换指定变量的值。 CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。 比较和交换操作按原子操作执行。
案例:打印机
class Program { static void Main(string[] args) { PrinterWithInterlockTest.TestPrint(); } } class PrinterWithInterlockTest { /// <summary> /// 正在使用的打印机 /// 0代表未使用,1代表正在使用 /// </summary> public static int UsingPrinter = 0; /// <summary> /// 计算机数量 /// </summary> public static readonly int ComputerCount = 3; /// <summary> /// 测试 /// </summary> public static void TestPrint() { Thread thread; Random random = new Random(); for (int i = 0; i < ComputerCount; i++) { thread = new Thread(MyThreadProc); thread.Name = string.Format("Thread{0}", i); Thread.Sleep(random.Next(3)); thread.Start(); } } /// <summary> /// 线程执行操作 /// </summary> private static void MyThreadProc() { //使用打印机进行打印 UsePrinter(); //当前线程等待1秒 Thread.Sleep(1000); } /// <summary> /// 使用打印机进行打印 /// </summary> private static bool UsePrinter() { //检查打印机是否在使用,如果原始值为0,则为未使用,可以进行打印,否则不能打印,继续等待 if (0 == Interlocked.Exchange(ref UsingPrinter, 1)) { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //Code to access a resource that is not thread safe would go here. //Simulate some work Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); //释放打印机 Interlocked.Exchange(ref UsingPrinter, 0); return true; } else { Console.WriteLine(" {0} was denied the lock", Thread.CurrentThread.Name); return false; } } }
2、lock 关键字
lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。 lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
public void Function() { System.Object locker= new System.Object(); lock(locker) { // Access thread-sensitive resources. } }
案例:继续打印机
class Program { static void Main(string[] args) { PrinterWithInterlockTest.TestPrint(); } } class PrinterWithInterlockTest { private static object UsingPrinterLocker = new object(); /// <summary> /// 计算机数量 /// </summary> public static readonly int ComputerCount = 3; /// <summary> /// 测试 /// </summary> public static void TestPrint() { Thread thread; Random random = new Random(); for (int i = 0; i < ComputerCount; i++) { thread = new Thread(MyThreadProc); thread.Name = string.Format("Thread{0}", i); Thread.Sleep(random.Next(3)); thread.Start(); } } /// <summary> /// 线程执行操作 /// </summary> private static void MyThreadProc() { //使用打印机进行打印 UsePrinter(); //当前线程等待1秒 Thread.Sleep(1000); } /// <summary> /// 使用打印机进行打印 /// </summary> /// <summary> /// 使用打印机进行打印 /// </summary> private static void UsePrinter() { //临界区 lock (UsingPrinterLocker) { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模拟打印操作 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } } }
结果:
Thread0 acquired the lock
Thread0 exiting lock
Thread1 acquired the lock
Thread1 exiting lock
Thread2 acquired the lock
Thread2 exiting lock
请按任意键继续. . .
3、监视器
与 lock 关键字类似,监视器防止多个线程同时执行代码块。Enter 方法允许一个且仅一个线程继续执行后面的语句;其他所有线程都将被阻止,直到执行语句的线程调用 Exit。这与使用 lock 关键字一样。事实上,lock 关键字就是用 Monitor 类来实现的。例如:(继续修改共享打印机案例,增加方法UsePrinterWithMonitor)
/// <summary> /// 使用打印机进行打印 /// </summary> private static void UsePrinterWithMonitor() { System.Threading.Monitor.Enter(UsingPrinterLocker); try { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模拟打印操作 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } finally { System.Threading.Monitor.Exit(UsingPrinterLocker); } }
使用 lock 关键字通常比直接使用 Monitor 类更可取,一方面是因为 lock 更简洁,另一方面是因为 lock
确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 关键字来实现的,无论是否引发异常它都执行关联的代码块。
4、同步事件和等待句柄
使用锁或监视器对于防止同时执行区分线程的代码块很有用,但是这些构造不允许一个线程向另一个线程传达事件。这需要“同步事件”,它是有两个状态(终止和非终止)的对象,可以用来激活和挂起线程。让线程等待非终止的同步事件可以将线程挂起,将事件状态更改为终止可以将线程激活。如果线程试图等待已经终止的事件,则线程将继续执行,而不会延迟。
同步事件有两种:AutoResetEvent 和 ManualResetEvent。它们之间唯一的不同在于,无论何时,只要 AutoResetEvent 激活线程,它的状态将自动从终止变为非终止。相反,ManualResetEvent 允许它的终止状态激活任意多个线程,只有当它的 Reset 方法被调用时才还原到非终止状态。
等待句柄,可以通过调用一种等待方法,如 WaitOne、WaitAny 或 WaitAll,让线程等待事件。System.Threading.WaitHandle.WaitOne 使线程一直等待,直到单个事件变为终止状态;System.Threading.WaitHandle.WaitAny 阻止线程,直到一个或多个指示的事件变为终止状态;System.Threading.WaitHandle.WaitAll 阻止线程,直到所有指示的事件都变为终止状态。当调用事件的 Set 方法时,事件将变为终止状态。
AutoResetEvent 允许线程通过发信号互相通信。 通常,当线程需要独占访问资源时使用该类。线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号。 如果 AutoResetEvent 为非终止状态,则线程会被阻止,并等待当前控制资源的线程通过调用 Set 来通知资源可用。调用 Set 向 AutoResetEvent 发信号以释放等待线程。 AutoResetEvent 将保持终止状态,直到一个正在等待的线程被释放,然后自动返回非终止状态。 如果没有任何线程在等待,则状态将无限期地保持为终止状态。如果当 AutoResetEvent 为终止状态时线程调用 WaitOne,则线程不会被阻止。 AutoResetEvent 将立即释放线程并返回到非终止状态。 可以通过将一个布尔值传递给构造函数来控制 AutoResetEvent 的初始状态:如果初始状态为终止状态,则为 true;否则为 false。 AutoResetEvent 也可以同 staticWaitAll 和 WaitAny 方法一起使用。
案例:
class Example { private static AutoResetEvent event_1 = new AutoResetEvent(false); private static AutoResetEvent event_2 = new AutoResetEvent(false); static void Main() { Console.WriteLine("该示例将启动三个线程\n" + "这些线程等待终止状态中的已创建的 AutoResetEvent #1\n" + "分别等待按下enter来释放线程\n" + "其中每个线程执行完第一步都继续等待AutoResetEvent #2\n" + "分别再次等待按下enter来释放线程执行,完成后任何结束" ); Console.ReadLine(); for (int i = 1; i < 4; i++) { Thread t = new Thread(ThreadProc); t.Name = "Thread_" + i; t.Start(); } Thread.Sleep(250); for (int i = 0; i < 3; i++) { Console.WriteLine("Press Enter to release another thread."); Console.ReadLine(); event_1.Set(); Thread.Sleep(250); } Console.WriteLine("\r\nAll threads are now waiting on AutoResetEvent #2."); for (int i = 0; i < 3; i++) { Console.WriteLine("Press Enter to release a thread."); Console.ReadLine(); event_2.Set(); Thread.Sleep(250); } // Visual Studio: Uncomment the following line. //Console.Readline(); } static void ThreadProc() { string name = Thread.CurrentThread.Name; Console.WriteLine("{0} waits on AutoResetEvent #1.", name); event_1.WaitOne(); Console.WriteLine("{0} is released from AutoResetEvent #1.", name); Console.WriteLine("{0} waits on AutoResetEvent #2.", name); event_2.WaitOne(); Console.WriteLine("{0} is released from AutoResetEvent #2.", name); Console.WriteLine("{0} ends.", name); } }
结果:
5、Mutex对象
mutex 与监视器类似;它防止多个线程在某一时间同时执行某个代码块。事实上,名称“mutex”是术语“互相排斥 (mutually exclusive)”的简写形式。然而与监视器不同的是,mutex 可以用来使跨进程的线程同步。mutex 由 Mutex 类表示。当用于进程间同步时,mutex 称为“命名 mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个 mutex 对象。 尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
本地 mutex 和系统 mutex Mutex 分两种类型:本地 mutex 和命名系统 mutex。 如果使用接受名称的构造函数创建了 Mutex 对象,那么该对象将与具有该名称的操作系统对象相关联。 命名的系统 mutex 在整个操作系统中都可见,并且可用于同步进程活动。 您可以创建多个 Mutex 对象来表示同一命名系统 mutex,而且您可以使用 OpenExisting 方法打开现有的命名系统 mutex。 本地 mutex 仅存在于进程当中。 进程中引用本地 Mutex 对象的任意线程都可以使用本地 mutex。 每个 Mutex 对象都是一个单独的本地 mutex。
在本地Mutex中,用法与Monitor基本一致
继续修改前面的打印机案例:
声明Mutex对象:
/// <summary> /// mutex对象 /// </summary> private static Mutex mutex = new Mutex();
修改后的代码:
class PrinterWithLockTest { /// <summary> /// 正在使用的打印机 /// </summary> private static object UsingPrinterLocker = new object(); /// <summary> /// 计算机数量 /// </summary> public static readonly int ComputerCount = 3; /// <summary> /// mutex对象 /// </summary> private static Mutex mutex = new Mutex(); /// <summary> /// 测试 /// </summary> public static void TestPrint() { Thread thread; Random random = new Random(); for (int i = 0; i < ComputerCount; i++) { thread = new Thread(MyThreadProc); thread.Name = string.Format("Thread{0}", i); Thread.Sleep(random.Next(3)); thread.Start(); } } /// <summary> /// 线程执行操作 /// </summary> private static void MyThreadProc() { //使用打印机进行打印 //UsePrinter(); //monitor同步 //UsePrinterWithMonitor(); //用Mutex同步 UsePrinterWithMutex(); //当前线程等待1秒 Thread.Sleep(1000); } /// <summary> /// 使用打印机进行打印 /// </summary> private static void UsePrinter() { //临界区 lock (UsingPrinterLocker) { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模拟打印操作 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } } /// <summary> /// 使用打印机进行打印 /// </summary> private static void UsePrinterWithMonitor() { System.Threading.Monitor.Enter(UsingPrinterLocker); try { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模拟打印操作 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } finally { System.Threading.Monitor.Exit(UsingPrinterLocker); } } /// <summary> /// 使用打印机进行打印 /// </summary> private static void UsePrinterWithMutex() { mutex.WaitOne(); try { Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name); //模拟打印操作 Thread.Sleep(500); Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name); } finally { mutex.ReleaseMutex(); } } }
6、读取器/编写器
7、Semaphore 和 SemaphoreSlim
8、障碍(Barrier)4.0后技术
9、SpinLock(4.0后)
10、SpinWait(4.0后)
--后续补上