常用技术
原子操作
Mutex 类
SemaphoreSlim 类
AutoResetEvent 类
ManualResetEventSlim 类
CountDownEvent 类
Barrier 类
ReaderWriterLockSlim 类
SpinWait 类
相关概念
多线程
线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程。如果某个线程进行一次长延迟操作, 处理器就切换到另一个线程执行。这样,多个线程的并行(并发)执行隐藏了长延迟,提高了处理器资源利用率,从而提高了整体性能。
多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率
多线程的优势
进程有独立的地址空间,同一进程内的线程共享进程的地址空间。启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
进程
进程,是操作系统进行资源调度和分配的基本单位。是由进程控制块、程序段、数据段三部分组成。一个进程可以包含若干线程(Thread),线程可以帮助应用程序同时做几件事。
在程序被运行后中,系统首先要做的就是为该程序进程建立一个默认线程,然后程序可 以根据需要自行添加或删除相关的线程。它是可并发执行的程序。在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位,也是称活动、路径或任务,它有两方面性质:活动性、并发性。
进程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:就绪--运行,运行--阻塞,阻塞--就绪。
线程(thread)
线程是CPU调度和执行的最小单位。有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。
主线程
进程创建时,默认创建一个线程,这个线程就是主线程。主线程是产生其他子线程的线程,同时,主线程必须是最后一个结束执行的线程,它完成各种关闭其他子线程的操作。尽管主线程是程序开始时自动创建的,它也可以通过Thead类对象来控制,通过调用CurrentThread方法获得当前线程的引用
原子操作
在多线程环境中,不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(上下文切换)
System.Threading.Interlocked 静态类
context switch(上下文切换)
在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。上下文切换就是这样一个过程,它允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。
上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。即 “切换桢”(switchframe)。
在三种情况下可能会发生上下文切换:中断处理,多任务处理,用户态切换。
内核模式
CPU将线程置于阻塞状态,线程挂起时间比较长
用户模式
线程等待时间较短,不用将线程切换到阻塞状态
混合模式
先尝试用户模式,如果等待时间较长,则将线程切换到阻塞状态,以节省CPU资源
线程同步有:临界区、互斥区、事件、信号量四种方式
临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
2、互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享
3、信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目
4、事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
Interlocked 类
1 | public class CounterModule |
Interlocked 提供了 Increment、Decrement 和 Add 等基于数据操作的原子方法
- 共享打印机
1 | class PrinterWithInterlockTest |
- 共享打印机,使用 lock
1 | /// <summary> |
- 共享打印机,使用 Monitor “监视器”
1 | /// <summary> |
lock 关键字就是用 Monitor 类来实现的,使用 lock 关键字通常比直接使用 Monitor 类更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 关键字来实现的,无论是否引发异常它都执行关联的代码块。
Mutex 类
1 | public static void TestMutex() { |
Mutex 类,只对一个线程授予对共享资源的独占访问
具名的互斥量是全局的操作系统对象,务必正确关闭互斥量。最好使用 using 代码块包裹互斥量对象
跨进程,win32封装的,所以它所需要的互操作转换更耗资源。
SemaphoreSlim 类
信号量,可用于在一个预计等待时间会非常短的进程内进行等待。 SemaphoreSlim 会尽可能多地依赖由公共语言运行时 (CLR) 提供的同步基元。 但是,它也会根据需要提供延迟初始化的、基于内核的等待句柄,以支持等待多个信号量。 SemaphoreSlim 还支持使用取消标记,但它不支持命名信号量或使用等待句柄来进行同步。
1 | public static void TestSemaphoreSlim() |
SemaphoreSlim 类,限制并发线程数量,并不适用 Windows 内核信号量,也不支持进程间同步。因此在跨程序同步的场景下可以使用 Semaphore 类。
- 支付流程
1 | class PaymentWithSemaphore |
同步事件有两种:AutoResetEvent 和 ManualResetEvent。它们之间唯一的不同在于,无论何时,只要 AutoResetEvent 激活线程,它的状态将自动从终止变为非终止。相反,ManualResetEvent 允许它的终止状态激活任意多个线程,只有当它的 Reset 方法被调用时才还原到非终止状态。
AutoResetEvent 类
1 | public static void TestAutoResetEvent() { |
AutoResetEvent 类,从一个线程向另一个线程发送通知,可以通知等待的线程有某事件发生。
AutoResetEvent 构造函数中传入 false,定义了两个实例的初始状态为 unsignaled 意味着任何线程调用这两个对象中的任何一个 WaitOne 方法将会被阻塞,直到我们调用了 Set 方法;
如果构造函数中传入 true,定义了两个实例的初始状态为 signaled 线程调用 WaitOne 方法则会被立即执行,然后事件状态自动变成 unsignaled,需要对该实例调用 Set 方法,以便其他线程对该实例调用 WaitOne 方法从而继续执行。
类似旋转门,一次只能通过一个
- 熬粥、蒸鱼
1 | class CookResetEvent |
ManualResetEventSlim
1 | public static void TestManualResetEventSlim() { |
类似人群通过大门,set()大门打开,reset()大门关闭
CountDownEvent
1 | public static void TestCountdownEvent() { |
如果调用 _countdown.Signal() 没达到指定次数,_countdown.Wait() 将会一直等待,请确保使用 CountdownEvent 时,所有线程完成后都要调用 Signal()
Barrier
Barrier(a, b)
a -> 调用 SignalAndWait() 的次数
b -> 回调函数
SignalAndWait() 调用 a 次后,立即执行 b 回调函数
1 | public static void TestBarrier() { |
ReaderWriterLockSlim
读取器/编写器锁,允许多个线程同时读取一个资源,但在向该资源写入时要求线程等待以获得独占锁
1 | public static void TestReaderWriterLockSlim() { |
- 构造一个线程安全的缓存
1 | /// <summary> |
SpinWait 类
轻量同步类型,可以在低级别方案中使用它来避免内核事件所需的高开销的上下文切换和内核转换。 在多核计算机上,当预计资源不会保留很长一段时间时,如果让等待线程以用户模式旋转数十或数百个周期,然后重新尝试获取资源,则效率会更高。 如果在旋转后资源变为可用的,则可以节省数千个周期。 如果资源仍然不可用,则只花费了少量周期,并且仍然可以进行基于内核的等待。 这一旋转-等待的组合有时称为“两阶段等待操作”。
1 | public static void TestSpinWait() { |
- 无锁堆栈
1 | public class LockFreeStack<T> |