首先,肯定的一点: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!