第22章 高级线程处理

第22章 高级线程处理

22.1 同步概述

多线程我们常需要一些同步结构进行线程间通讯。同步结构可以分为三类:

  1. 互斥锁

一次只允许一个线程执行特定的活动或一段代码。主要目的是令线程访问共享的写状态而不互相影响。互斥锁包括 lock​、Mutex​ 和 SpinLock​。
2. 非互斥锁

实现了有限的并发性。非互斥锁包括 Semaphore(Slim)​ 和 ReaderWriterLock(Slim)​。
3. 信号发送结构

信号发送结构允许线程在接到一个或者多个其他线程的通知之前保持阻塞状态。信号发送结构包括 ManualResetEvent(Slim)​、AutoResetEvent​、CountdownEvent​ 和 Barrier​。前三者就是所谓的事件等待句柄(event wait handles)。

22.2 互斥锁

22.2.1 lock 语句

22.2.2 Monitor.Enter​ 方法和 Monitor.Exit​ 方法

C# 的 lock​ 语句是包裹在 try-finally 语句块中的 Monitor.Enter​ 和 Monitor.Exit​ 语法糖,以下两段代码等价(有简化):

lock (_locker)
{if (_val2 != 0) Console.WriteLine (_val1 / _val2);val2 = 0;
}
bool lockTaken = false;
try {Monitor.Enter(_locker, ref lockTaken);if(_val2 != 0)Console.WriteLine(_val1 / _val2);_val2 = 0;
}
finally {if (lockTaken)Monitor.Exit(_locker);
}

又见 lock & Monitor

22.2.2.1 lockTaken 重载

bool lockTaken = false;
try {Monitor.Enter(_locker, ref lockTaken);if(_val2 != 0)Console.WriteLine(_val1 / _val2);_val2 = 0;
}
finally {if (lockTaken)Monitor.Exit(_locker);
}

这段代码中,lockTaken​ 的存在是为了防止这种特殊情况: **== ** ​Monitor.Enter​ ** ** 之前有其他动作抛出了异常,导致 ** ** ​Monitor.Enter​ ** ** 并未实际执行, Monitor.Exit​ ** ** 却会释放锁==。

22.2.2.2 TryEnter

Monitor.TryEnter​ 方法可以指定超时时间。使用分三种情况:

  1. 在指定时间内获得锁, 返回 true
  2. 在指定时间内未获得锁, 返回 false
  3. 未指定时间,且未获得锁, 立即返回 false

22.2.3 选择同步对象

同步对象(locker 对象)需遵循如下原则:

  1. 必须是 引用 类型
  2. 访问修饰符通常private ,也可以是 protected
  3. 通常是 字段 成员

除了我们定义的 object 对象外,还可以用如下对象作为 locker:

  • this 实例
lock (this) { ... }
  • 类型 实例
lock (typeof(Widget)) { ... }

Warn

上述锁定方式有一个缺点:无法封装锁逻辑,因此难以避免死锁或者长时间阻塞。而类型上的锁甚至可以跨越(同一进程中的)应用程序域的边界。

  • Lambda 表达式或匿名方法中捕获的局部变量

C7.0 核心技术指南 第7版.pdf - p907 - C7.0 核心技术指南 第 7 版-P907-20240310161421

22.2.4 使用锁的时机

使用锁的基本原则是:若需要 修改共享字段 ,则需要在其周围加锁。即便对于最简单的情况(例如对某个字段进行赋值),也必须考虑进行同步。

锁会在变量前后创建内存栅障(memory barrier),内存栅障就像是操作变量的围栏,而编译器优化指令执行顺序(进行重排)、变量缓存是无法跨越这个围栏的。

22.2.5 锁与原子性

如果使用同一个锁对一组变量的读写操作进行保护,那么可以认为这些变量的读写操作是原子的(atomically)。

C7.0 核心技术指南 第7版.pdf - p909 - C7.0 核心技术指南 第 7 版-P909-20240310162357

22.2.6 嵌套锁

线程可以用嵌套(重入)的方式重复锁住同一个对象:

lock (locker)lock (locker)lock (locker){// Do something ...}

嵌套后,最内层方法 可以 正常执行,锁将在 最外 层释放。如下代码将 输出“Another method”

static readonly object _locker = new object();static void Main() {lock (_locker) {AnotherMethod();}
}static void AnotherMethod(){lock (_locker) { Console.WriteLine ("Another method"); }
}

22.2.7 死锁

两个线程互相等待对方占用的资源,会使双方都无法继续执行,从而形成死锁。如下代码演示了死锁:

object locker1 = new object();
object locker2 = new object();new Thread (() => {lock (locker1) {Thread.Sleep (1000);lock (locker2) { }      // Deadlock}
}).Start();lock (locker2) {Thread.Sleep (1000);lock (locker1) { }          // Deadlock
}

22.2.8 性能

锁的操作是很快的:2015 年生产的计算机在没有出现竞争的情况下一般可以在 50 纳秒内获取或者释放锁。如果在竞争的情况下,则相应的上下文切换开销将增加到微秒级但即便如此,这个时间也可能小于线程实际的调度时间。

22.2.9 Mutex

Mutex​ 和 C# 的 lock​ 类似,但是它可以支持多个 进程 。换言之,Mutex​ 不但可以用于应用程序范围,还可以用于 计算机 范围。

在非竞争的情况下获得或者释放 Mutex​ 需要大约一微秒的时间,大概比 lock​ 要慢 20 倍。

C7.0 核心技术指南 第7版.pdf - p912 - C7.0 核心技术指南 第 7 版-P912-20240310165017

Mutex​ 常用于防止程序多开,实现方式如下:

static void Main() {// 通过给 Mutex 命名可以在计算机范围内使用该 Mutex。// Mutex 的名称在你的计算机和应用程序中应该是独一无二的(比如说用 URL)using (var mutex = new Mutex (false, "oreilly.com OneAtATimeDemo")) {// 如果 Mutex 在三秒内一直处于占用状态,退出程序。if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false)) {Console.WriteLine ("Another instance of the app is running. Bye!");return;}RunProgram();}
}static void RunProgram() {Console.WriteLine ("Running. Press Enter to exit");Console.ReadLine();
}

C7.0 核心技术指南 第7版.pdf - p912 - C7.0 核心技术指南 第 7 版-P912-20240310165718

22.3 锁和线程安全性

.NET 中自带的类型很少是线程安全的,保证线程安全性是开发者的责任。常用的线程安全实现方式有四:

  1. 牺牲粒度,将一大部分代码(甚至整个对象)都包裹在互斥锁中;
  2. 降低线程间交互,减少共享数据;
  3. 对于富文本客户端,可以在 UI 线程上访问共享状态(使用异步编程);
  4. 使用自动锁(ContextBoundObject​ 基类 + Synchronization​ 特性)

22.3.1 线程安全和 .NET Framework 类型

锁可以将非线程安全的代码变为线程安全的代码。对于集合,我们在遍历前可以将它 拷贝到数组中 ,再进行遍历,避免枚举过程中一直占用互斥锁。

22.3.1.1 在线程安全的对象上使用锁

22.3.1.2 静态成员

在实现线程安全时需遵循如下原则:

  1. 静态成员 必须 是线程安全的
  2. 实例成员 可以是 线程安全的

静态成员的使用范围更广,更容易发生多线程交互、出现线程不安全的情况。

C7.0 核心技术指南 第7版.pdf - p915 - C7.0 核心技术指南 第 7 版-P915-20240310174937

22.3.1.3 只读线程安全性

C7.0 核心技术指南 第7版.pdf - p916 - C7.0 核心技术指南 第 7 版-P916-20240310175531

22.3.2 应用服务器的线程安全性

22.3.3 不可变对象

22.4 非互斥锁

22.4.1 信号量(Semaphore​)

Semaphore​ 就像一个俱乐部:它有特定的容量,还有门卫保护。一旦满员之后,就不允许其他人进入了,人们只能在外面排队。每当有人离开时,才准许另外一个人进入。

Semaphore ​的构造器需要至少两个参数:俱乐部当前的 空闲容量 ,以及俱乐部的 总容量

Semaphore​ 有如下特点:

  1. Semaphore​​ 是线程无关的

    容量为 1 的 Semaphore​​ 类似于 Mutex​​、lock​​,但是它没有持有者这个概念,它是线程无关的。 任何 线程都可以调用 Semaphore​​ 的 Release​​ 方法。Mutex​​ 和 lock​​ 则不然,只有 持有锁 的线程才能够释放锁。

  2. 它用于 限制并发性 ,防止太多线程同时执行特定的代码。

    当我们发现 cpu 占用过高,或爬虫时不想过于频繁,可以通过这种方式限制线程的使用数量。

var inputs = Enumerable.Range(1, 10).ToArray();
var tasks = new List<Task<int>>();
// 限制最多有两个线程可以使用。
var sem = new SemaphoreSlim(2, 2);foreach (var input in inputs){tasks.Add(HeavyJob(input));
}await Task.WhenAll(tasks);var outputs = tasks.Select(x => x.Result).ToArray();
outputs.Dump();async Task<int> HeavyJob(int input){await sem.WaitAsync();await Task.Delay(1000);// 使用完需要释放。sem.Release();return input * input;
}

22.4.2 读写锁

通常来说,一个对象(可以是文件、可以是集合)可以同时“读”,却不能同时“读+写”、“写+写”。读写锁便是为该场景服务的。

读写锁有两个:

  • ReaderWriterLockSlim
  • ReaderWriterLock

C7.0 核心技术指南 第7版.pdf - p919 - C7.0 核心技术指南 第 7 版-P919-20240310231325

它们有两种基本的锁:读锁和写锁:

  • 锁是全局排它锁
  • 锁可以兼容其他的
ReaderWriterLockSlim​ 的使用

ReaderWriterLockSlim​ 定义了如下方法来获得、释放锁:

public void EnterReadLock();
public void ExitReadLock();
public void EnterWriteLock();
public void ExitWriteLock();

此外,每个 EnterXXX​ 方法都有相应的 TryEnterXXX ​ 方法,可以像 Monitor.TryEnter​ 接受超时参数。

ReaderWriterLockSlim​ 还提供了若干属性用于监视锁的状态:

public bool IsReadLockHeld            { get; }
public bool IsUpgradeableReadLockHeld { get; }
public bool IsWriteLockHeld           { get; }
public int  WaitingReadCount          { get; }
public int  WaitingUpgradeCount       { get; }
public int  WaitingWriteCount         { get; }
public int  RecursiveReadCount        { get; }
public int  RecursiveUpgradeCount     { get; }
public int  RecursiveWriteCount       { get; }

以下示例演示了 ReaderWriterLockSlim ​的用法。

  1. 读:三个线程将持续枚举列表中的元素。
  2. 写:两个线程每隔 100 毫秒生成一个随机数,并试图将该数字加入列表中。
static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
static List<int> _items = new List<int>();
static Random _rand = new Random();static void Main() {new Thread (Read).Start();new Thread (Read).Start();new Thread (Read).Start();new Thread (Write).Start ("A");new Thread (Write).Start ("B");
}static void Read() {while (true) {_rw.EnterReadLock();foreach (int i in _items) Thread.Sleep (10);_rw.ExitReadLock();}
}static void Write (object threadID) {while (true) {int newNumber = GetRandNum (100);_rw.EnterWriteLock();_items.Add (newNumber);_rw.ExitWriteLock();Console.WriteLine ("Thread " + threadID + " added " + newNumber);Thread.Sleep (100);}
}static int GetRandNum (int max) { lock (_rand) return _rand.Next(max); }

C7.0 核心技术指南 第7版.pdf - p921 - C7.0 核心技术指南 第 7 版-P921-20240311123626

ReaderWriterLock

ReaderWriterLock​ 中的锁方法名为 AcquireXXX ​ 和 ReleaseXXX ​。 AcquireXXX ​ 方法需要传入超时时间,超时后将 抛出 ApplicationException异常

22.4.2.1 可升级锁

以如下代码为例,读锁查询 → 写锁 Add​ 的中间,可能出现 其他线程将数据插入 的问题:

int newNumber = GetRandNum(100);
_rw.EnterReadLock();
if (!_items.Contains(newNumber))
{_rw.ExitReadLock();_rw.EnterWriteLock();_items.Add(newNumber);_rw.ExitWriteLock();Console.WriteLine("Thread " + threadID + " added " + newNumber);
}

为避免此问题,我们可以使用 UpgradeableReadLock ​:

int newNumber = GetRandNum (100);
_rw.EnterUpgradeableReadLock();
if (!_items.Contains (newNumber))
{_rw.EnterWriteLock();_items.Add (newNumber);_rw.ExitWriteLock();Console.WriteLine ("Thread " + threadID + " added " + newNumber);
}
_rw.ExitUpgradeableReadLock();

UpgradeableReadLock​ 锁具有 排它性 ,一次只能有 UpgradeableReadLock​。但是在 UpgradeableReadLock​ 被占用时,其他 ReadLock ​ 可以正常进入。

22.4.2.2 锁递归

通常,ReaderWriterLockSlim ​禁止使用嵌套锁或者递归锁。因此,下面的操作会 抛出异常

var rw = new ReaderWriterLockSlim ();
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();

可以在 构造器 中传入 LockRecursionPolicy.SupportsRecursion​,可以确保只有在真正需要时才支持递归锁。

递归锁的基本原则是:

  1. 写锁可升级锁可以内嵌 三种锁
  2. 读锁仅能内嵌 读锁
var rw = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();rw.EnterReadLock();
rw.EnterWriteLock();
Console.WriteLine(rw.IsReadLockHeld);     // True
Console.WriteLine(rw.IsWriteLockHeld);    // True
rw.ExitWriteLock();
rw.ExitReadLock();

22.5 使用 EventWaitHandle 发送信号

EventWaitHandle(事件等待句柄)有三种实现:

  1. AutoResetEvent
  2. ManualResetEvent(Slim)
  3. CountdownEvent

22.5.1 AutoResetEvent

AutoResetEvent​ 类似于闸机,一次仅允许 个线程通过(Set()​ 后仅能通过 1 个 WaitOne()​),通过后闸机 自动关闭WaitOne()​ 后自动 Reset()​)。创建 AutoResetEvent​ 方式有二:

  1. 使用 AutoResetEvent​ 的构造器

    var auto = new AutoResetEvent (false);
    
  2. 使用 EventWaitHandle ​ 的构造器

    var auto = new EventWaitHandle (false, EventResetMode.AutoReset);
    

AutoResetEvent​ 使用方式如下:

static EventWaitHandle _waitHandle = new AutoResetEvent (false);static void Main() {new Thread (Waiter).Start();Thread.Sleep (1000);        // Pause for a second..._waitHandle.Set();          // Wake up the Waiter.
}static void Waiter() {Console.WriteLine ("Waiting...");_waitHandle.WaitOne();      // Wait for notificationConsole.WriteLine ("Notified");
}

C7.0 核心技术指南 第7版.pdf - p925 - C7.0 核心技术指南 第 7 版-P925-20240311171723

AutoResetEvent​ 常用的方法有:

  1. Set

    开放闸机

  2. WaitOne

    等待闸机开放,使用后自动调用 Reset ​​。可以接收超时时间,在指定时间内没有收到信号,返回 false。

  3. Reset

    关闭闸机

双向信号

假设主线程需要向工作线程连续发送三次信号。如果主线程单纯地连续调用 Set ​方法若干次,那么第二次或者第三次发送的信号就有可能 丢失 ,因为工作线程需要时间来处理每一次的信号。

我们可以 使用两个 AutoResetEvent ​,主线程和子线程 互相 SetWaitOne ​:

C7.0 核心技术指南 第7版.pdf - p927 - C7.0 核心技术指南 第 7 版-P927-20240311173724

static EventWaitHandle _ready = new AutoResetEvent (false);
static EventWaitHandle _go = new AutoResetEvent (false);
static readonly object _locker = new object();
static string _message;static void Main() {new Thread (Work).Start();_ready.WaitOne();                  // First wait until worker is readylock (_locker) _message = "ooo";_go.Set();                         // Tell worker to go_ready.WaitOne();lock (_locker) _message = "ahhh";  // Give the worker another message_go.Set();_ready.WaitOne();lock (_locker) _message = null;    // Signal the worker to exit_go.Set();
}static void Work() {while (true) {_ready.Set();                          // Indicate that we're ready_go.WaitOne();                         // Wait to be kicked off...lock (_locker) {if (_message == null) return;        // Gracefully exitConsole.WriteLine (_message);}}
}

22.5.2 ManualResetEvent

ManualResetEvent​ 类似于红绿灯,调用 Set ​ 方法亮绿灯,任意 WaitOne​ 线程均可通过;调用 Reset ​ 方法亮红灯,WaitOne​ 线程将阻塞。

AutoResetEvent​ 相同,创建 ManualResetEvent​ 方式有二:

  1. 使用 ManualResetEvent​ 的构造器

    var manual1 = new ManualResetEvent (false);
    
  2. 使用 EventWaitHandle ​ 的构造器

    var manual2 = new EventWaitHandle (false, EventResetMode.ManualReset);
    

C7.0 核心技术指南 第7版.pdf - p928 - C7.0 核心技术指南 第 7 版-P928-20240311175842

C7.0 核心技术指南 第7版.pdf - p928 - C7.0 核心技术指南 第 7 版-P928-20240311175917

22.5.3 CountdownEvent

CountdownEvent​ 类似于坐满即走的班车。通过 构造器 设置座位数量,调用 Signal ​ 方法占用一个座位, Wait ​ 会阻塞线程,直至 剩余座位为 0

使用方式如下:

static CountdownEvent _countdown = new CountdownEvent (3);static void Main()
{new Thread (SaySomething).Start ("I am thread 1");new Thread (SaySomething).Start ("I am thread 2");new Thread (SaySomething).Start ("I am thread 3");_countdown.Wait();   // Blocks until Signal has been called 3 timesConsole.WriteLine ("All threads have finished speaking!");
}static void SaySomething (object thing)
{Thread.Sleep (1000);Console.WriteLine (thing);_countdown.Signal();
}

CountdownEvent​ 常用的方法有:

  1. AddCount

    添加座位,如果座位已满(计数为 0),调用该方法将 抛出异常 。为了避免异常,可以使用它的 Try​ 方法。

  2. Signal

    占用座位

  3. Wait

    等待满员发车

  4. Reset

    清空乘客

22.5.4 创建跨进程的 EventWaitHandle

AutoResetEvent​ 和 ManualResetEvent​ 派生自 EventWaitHandle​,EventWaitHandle​ 支持跨进程访问。

通过向 构造器 传递 名称 ,可以在进程间共用该 EventWaitHandle​。使用方式如下:

var wh = new EventWaitHandle(false, EventResetMode.AutoReset, "MyCompany.MyApp.SomeName");

22.5.5 等待句柄和延续操作

线程池(ThreadPool​)的 RegisterWaitForSingleObject​ 方法支持 EventWaitHandle.Set ​ 后,自动 运行回调方法 。该方法签名如下:

public static RegisteredWaitHandle RegisterWaitForSingleObject(WaitHandle waitObject, WaitOrTimerCallback callBack, object? state, int millisecondsTimeOutInterval, bool executeOnlyOnce)

WaitHandle​​ 接到信号时(或者超时后),委托就会在一个线程池线程中执行。之后,还需要调用 Unregister ​​ 解除非托管的句柄和回调之间的关系:

static ManualResetEvent _starter = new ManualResetEvent (false);public static void Main() {RegisteredWaitHandle reg = ThreadPool.RegisterWaitForSingleObject (_starter, Go, "Some Data", -1, true);Thread.Sleep (5000);Console.WriteLine ("Signaling worker...");_starter.Set();Console.ReadLine();reg.Unregister (_starter);    // Clean up when we’re done.
}public static void Go (object data, bool timedOut) {Console.WriteLine ("Started - " + data);// Perform task...
}

22.5.6 将等待句柄转换为任务

ThreadPool.RegisterWaitForSingleObject​​ 的使用较为不便,我们可以通过 TaskCompletionSource ​​、 ManualResetEventSlim ​​ 将该功能包装为 Task​​:

public static Task<bool> ToTask (this WaitHandle waitHandle, int timeout = -1) {var tcs = new TaskCompletionSource<bool>();RegisteredWaitHandle token = null;var tokenReady = new ManualResetEventSlim();token = ThreadPool.RegisterWaitForSingleObject (waitHandle, (state, timedOut) => {tokenReady.Wait();tokenReady.Dispose();token.Unregister (waitHandle);tcs.SetResult (!timedOut); },null,timeout, true);tokenReady.Set();return tcs.Task;
}

此时我们可以在等待句柄上附加延续操作、进行 await、设置超时时间:

myWaitHandle.ToTask().ContinueWith(...)await myWaitHandle.ToTask();if(!await (myWaitHandle.ToTask(5000)))Console.WriteLine ("Timed out");

上述代码的 ManualResetEventSlim​ 用于避免 token.Unregister ​ 发生在 token 赋值之前

22.5.7 WaitAny​、WaitAll​ 和 SignalAndWait

这三个方法是 WaitHandle​ 的静态方法,作用如下:

  1. WaitAny

    可以等待一组句柄中的任意一个句柄

  2. WaitAll

    等待所有给定的句柄

  3. SignalAndWait

    需传入两个 WaitHandle​ 参数(令参数名为 wh1​ 和 wh2​),该方法自动调用 wh1.Set ​, wh2.WaitOne

Notice

WaitAll​ 和 SignalAndWait​ 不支持在 STA 线程上运行。

Eureka

使用 WaitAll​ 时,其实可以考虑使用 22.5.3 CountdownEvent。

22.6 Barrier​ 类

Barrier​ 类称为“线程屏障”,它和 22.5.3 CountdownEvent 相似,却更像欹器,待水接满后(阻塞结束后),将 清空容器,继续接水 。它常用于汇合线程。以下代码将输出:“ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ”:

static Barrier _barrier = new Barrier (3);static void Main() {new Thread (Speak).Start();new Thread (Speak).Start();new Thread (Speak).Start();
}static void Speak() {for (int i = 0; i < 5; i++) {Console.Write (i + " ");_barrier.SignalAndWait();}
}

Barrier​ 对象还支持容器清空后 回调(post-phase) ,上述代码构造器改为如下方式,将输出“ 0 0 0 | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | ”:

static Barrier _barrier = new Barrier (3, barrier => Console.Write(" | "));

22.7 延迟初始化

22.7.1 Lazy<T>

该类实现了 延迟初始化 功能。当 Lazy​ ​构造器传入 false,它是线程 不安全 的,如下两段代码等价:

class Foo {Expensive _expensive;public Expensive Expensive {get {if (_expensive == null) _expensive = new Expensive();return _expensive;}}
}
class Foo
{Lazy<Expensive> _expensive = new Lazy<Expensive> (() => new Expensive(), false);public Expensive Expensive { get { return _expensive.Value; } }
}

Lazy​ 构造器传入 true,它是线程 安全 的,如下两段代码等价:

class Foo {Expensive _expensive;readonly object _expenseLock = new object();public Expensive Expensive {get {lock (_expenseLock) {if (_expensive == null) _expensive = new Expensive();return _expensive;}}}
}
class Foo
{Lazy<Expensive> _expensive = new Lazy<Expensive> (() => new Expensive(), true);public Expensive Expensive { get { return _expensive.Value; } }
}

22.7.2 LazyInitializer ​类

LazyInitializer​ 为静态类,与 Lazy<T>​ 工作方式相似,有以下不同点:

  1. 直接使用静态方法操作自定义类型的字段,避免引入间接层次
  2. 允许多个线程同时实例化,最终会使用 第一个完成实例化的成员

用法如下:

class Foo {Expensive _expensive;public Expensive Expensive {get { LazyInitializer.EnsureInitialized (ref _expensive, () => new Expensive());return _expensive;}}
}

这是一种极致的优化方式,但很少使用。它也会带来相应的开销:

  • 如果竞争实例化的线程数量大于内核数量,速度会变慢。
  • 由于它执行了多余的初始化,因此潜在地浪费了 CPU 资源。
  • 初始化逻辑必须是线程安全的(因此,如果 Expensive​ 的构造器在静态字段上执行写操作,它就不具备线程安全性)。
  • 如果初始化器实例化一个需要销毁(disposal​ 的对象),则必须编写额外的逻辑才能将“多创建的”对象销毁。

22.8 线程本地存储

不同的线程可以使用线程本地存储用存放自己的数据。实现线程本地存储的方法有三种。

22.8.1 [ThreadStatic] ​ 特性

可以在 静态 字段上附加 ThreadStatic ​ 特性:

[ThreadStatic] static int _x;

这样,每一个线程都会得到一个 _x ​的独立副本。

需要注意的是:

  1. 不支持 实例 字段

  2. 无法通过 字段初始化 器为它赋值

    它只在调用静态构造器的线程上执行一次。如下代码将输出:“ 20 10 1 1 1 ”:

    void Main() {new Thread (() => { Thread.Sleep(1000); Foo._x++; Foo._x.Dump(); }).Start();new Thread (() => { Thread.Sleep(2000); Foo._x++; Foo._x.Dump(); }).Start();new Thread (() => { Thread.Sleep(3000); Foo._x++; Foo._x.Dump(); }).Start();Foo._x.Dump();Foo._y.Dump();
    }public class Foo {[ThreadStatic]public static int _x = 10;public static int _y = 10;static Foo() {_x = 20;}
    }
    

22.8.2 ThreadLocal<T>​ 类

ThreadLocal<T>​ 使用方式如下,_x​ 的默认值为 3

static ThreadLocal<int> _x = new ThreadLocal<int> (() => 3);
// 如下写成下面这种形式,第一个线程输出的是 6 而非 4,剩余线程仍输出4
// static ThreadLocal<int> _x = new ThreadLocal<int>(() => 3) { Value = 5 };
void Main() {new Thread (() => { Thread.Sleep(1000); _x.Value++; _x.Dump(); }).Start();new Thread (() => { Thread.Sleep(2000); _x.Value++; _x.Dump(); }).Start();new Thread (() => { Thread.Sleep(3000); _x.Value++; _x.Dump(); }).Start();
}

可以调用 _x​ 的 Value ​ 属性来访问线程本地值。ThreadLocal​ 的值是延迟计算的:其中的工厂函数会在(每一个线程)第一次调用时计算实际的值。

Summary

可以看到,ThreadStatic​ 和 ThreadLocal​ 的区别在于, ThreadLocal ​ 可以对参数进行初始化,而 ThreadStatic ​ 只能使用参数的默认值。

使用 ThreadLocal<T>​ 定义线程安全的 Random

Random​ 类不是线程安全的,我们可以通过 ThreadLocal<T>​ 为每一个线程生成一个独立的 Random​ 对象,使其线程安全:

var localRandom = new ThreadLocal<Random> (()=> new Random(Guid.NewGuid().GetHashCode())
);

22.8.3 GetData​ 方法和 SetData​ 方法

Thread ​ 类的 GetData​、SetData​ 为每一个线程开辟了一个空间(插槽),可以从中存取数据。使用前需获取 LocalDataStoreSlot ​ 插槽对象,用法如下:

void Main() {var test = new Test();new Thread (() => { Thread.Sleep(1000); test.SecurityLevel++; test.SecurityLevel.Dump(); }).Start();new Thread (() => { Thread.Sleep(2000); test.SecurityLevel++; test.SecurityLevel.Dump(); }).Start();new Thread (() => { Thread.Sleep(3000); test.SecurityLevel++; test.SecurityLevel.Dump(); }).Start();
}class Test {LocalDataStoreSlot _secSlot = Thread.GetNamedDataSlot ("securityLevel");public int SecurityLevel {get {object data = Thread.GetData (_secSlot);return data == null ? 0 : (int) data;    // null == uninitialized}set  {Thread.SetData (_secSlot, value);}}
}

插槽对象可以用如下方法获取:

  1. Thread.GetNamedDataSlot

    获取 命名 插槽

  2. Thread.AllocateDataSlot

    获取 匿名 插槽

Thread.FreeNamedDataSlot​ 方法可以 释放所有线程中的命名插槽 ,前提是它们在线程中的引用已被垃圾回收。这确保了当线程需要特定数据插槽时,只要它保留了正确的 LocalDataStoreSlot​ 对象的引用,那么相应的数据插槽就不会丢失。

22.9 Interrupt​ 和 Abort​ 方法

Interrupt​ 用于强制释放处于阻塞状态的线程,并抛出 ThreadInterruptedException ​ 异常。详见 Thread.Interrupt

Abort​ 用于强行中止其他线程,并抛出 ThreadAbortException ​ 异常。详见 Abort[弃用]ThreadAbortException ​ 异常较为特殊,会在 catch 后 继续抛出(以中止线程) 。若要阻止该行为,可以在 catch 块中调用 Thread.ResetAbort ​ 静态方法,它会将线程状态(ThreadState​)从 AbortRequested​ 恢复至 Running​。

如下代码将 catch 次异常:

void Func() {try {try {while (true) {Thread.Sleep(20);}}catch (ThreadAbortException ex) {ex.Dump("1");}}catch(ThreadAbortException ex){ex.Dump("2");}
}

如下代码将 catch 次异常:

void Func() {try {try {while (true) {Thread.Sleep(20);}}catch (ThreadAbortException ex) {Thread.CurrentThread.ThreadState.Dump();Thread.ResetAbort();Thread.CurrentThread.ThreadState.Dump();ex.Dump("1");}}catch(ThreadAbortException ex){ex.Dump("2");}
}

C7.0 核心技术指南 第7版.pdf - p939 - C7.0 核心技术指南 第 7 版-P939-20240313123340

22.9.1 Abort 的使用陷阱

C# 中的 finally​ 块无论如何都会执行,静态构造器也不会因 Abort​ 中止,这都保证了应用程序域的完整性。不过仍有例外,这种例外会导致被中止线程遭到污染,并可能进一步污染应用程序域,甚至污染进程。因此 Abort​ 不是一个取消执行的通用手段。

例如,假设类型的实例构造器获得了一个非托管资源(例如一个文件句柄),而该资源需要调用 Dispose​ 方法进行释放。如果一个线程在 构造器 结束前中止,则部分构造的对象将 无法销毁 ,从而导致非托管句柄的泄露。(如果该类型有终结器,则终结器仍然会执行,但也需等到 GC 捕获到该对象时才会执行。)。.NET Framework 中的许多基础类型(包括 FileStream​)都有这样的风险,因此 Abort​ 在大多数情况下都不适用。中止 .NET Framework 的代码是不安全的。

22.10 Suspend​ 和 Resume​ 方法

Suspend​​ 和 Resume​​ 用于 冻结解冻 线程,详见 Thread.Suspend[弃用]和 Thread.Resume[弃用]。如果线程持有锁,用 Suspend​​ 挂起线程很容易造成 死锁

它们的应用场景很少,不过可以用来追踪线程的 调用栈信息

StackTrace stackTrace = null;
targetThread.Suspend();
try { stackTrace = new StackTrace (targetThread, true); }
finally { targetThread.Resume(); }

上述代码可能造成死锁,解决方案之一是:如果指定线程在 200 毫秒后仍然处于挂起状态,则在另外一个线程中调用 Resume​ 方法。虽然这会导致调用栈跟踪信息失效,但肯定比造成应用程序死锁要好得多:

StackTrace stackTrace = null;
var ready = new ManualResetEventSlim();new Thread (() =>
{// Backstop to release thread in case of deadlock:ready.Set();Thread.Sleep (200);try { targetThread.Resume(); } catch { }
}).Start();ready.Wait();
targetThread.Suspend();
try { stackTrace = new StackTrace (targetThread, true); }
catch { /* Deadlock */ }
finally
{try { targetThread.Resume(); } catch { stackTrace = null;  /* Deadlock */  }
}

22.11 定时器

.NET Framework 提供了四种定时器,两种定时器是通用 线程定时器:

  • System.Threading.Timer
  • System.Timers.Timer

另外两种则是特殊用途的 线程定时器:

  • System.Windows.Forms.Timer​(Windows Forms 应用的定时器)
  • System.Windows.Threading.DispatcherTimer​(WPF 的定时器)

多线程定时器更加强大,定时精确,使用灵活;需要更新界面元素(UI)的简单任务来说,单线程定时器更加安全方便。

22.11.1 多线程定时器

System.Threading.Timer

System.Threading.Timer​ 是最简单的多线程定时器,通过构造器便可使用。在构造器中传入如下参数即可启用:

  1. TimerCallback 委托
  2. 委托参数 (可选)
  3. 时间间隔 (可选)

使用完成后通过 Timer.Dispose ​ 停止。如下定时器 5s 后启动,之后每隔 1s 输出一次 tick...

Timer tmr = new Timer (Tick, "tick...", 5000, 1000);
Console.WriteLine ("Press Enter to stop");
Console.ReadLine();
tmr.Dispose();         // This both stops the timer and cleans up.static void Tick (object data) {Console.WriteLine (data);
}

在创建定时器之后仍然可以调用 Change ​方法修改定时器的定时间隔。如果希望定时器只触发一次,则可以用 Timeout.Infinite ​作为构造器的最后一个参数。

System.Timers.Timer

System.Timers.Timer​ 对 System.Threading.Timer​ 进行了简单的包装,提高了易用性。它的附加功能如下:

  • 实现了 IComponent ​接口,允许 嵌入到 Visual Studio 设计器的组件托盘中
  • 提供了 Interval ​ ​属性替代 Change​ ​方法。
  • 提供了 Elapsed ​ ​事件取代回调委托。
  • 提供了 Enabled ​ ​属性来开始和停止计时器(默认值为 false)。
  • 如果不习惯使用 Enabled ​ ​属性还可以使用 Start ​ ​和 Stop ​ ​方法。
  • 提供了 AutoReset ​ ​标志,用于指示重复的事件(默认值为 true)。
  • 提供了 SynchronizingObject​ 属性。可调用该对象的 Invoke ​ 和 BeginInvoke ​ 方法安全地调用 WPF 元素和 Windows Forms 控件的方法。

用法如下:

var tmr = new System.Timers.Timer();  // 无需参数
tmr.Interval = 500;
tmr.Elapsed += tmr_Elapsed;    // 使用事件而非委托
tmr.Start();                   // 开始
Console.ReadLine();
tmr.Stop();                    // 停止
Console.ReadLine();
tmr.Start();                   // 重新开始
Console.ReadLine();
tmr.Dispose();                 // 永久释放static void tmr_Elapsed (object sender, EventArgs e) {Console.WriteLine ("Tick");
}

该定时器有如下特点:

  1. 事件线程来自 线程池 ,事件每一次都可能在不同的线程上触发;
  2. 事件几乎能保证触发的时效性,即使上一次事件未执行完毕。因此事件处理函数必须是线程安全的。

C7.0 核心技术指南 第7版.pdf - p943 - C7.0 核心技术指南 第 7 版-P943-20240313174534

22.11.2 单线程定时器

单线程计时器有两个:

  • System.Windows.Forms.Timer​(Windows Forms 应用的定时器)
  • System.Windows.Threading.DispatcherTimer​(WPF 的定时器)

两种定时器的成员和 System.Timers.Timer ​ 的成员非常相似,不同的是它们将事件发送到消息循环中执行。它有如下好处:

  1. 可以忽略线程安全性。
  2. 如果前一次的 Tick 没有完成处理,则新的 Tick 事件 绝不会 触发。
  3. 可以无须调用 Control.BeginInvoke ​ 或者 Dispatcher.BeginInvoke ​ 方法,可以直接在 Tick​ 事件处理代码中操作元素或控件。

同样的,Tick 事件处理器必须非常快地执行完毕,否则将会导致用户界面失去响应。

C7.0 核心技术指南 第7版.pdf - p943 - C7.0 核心技术指南 第 7 版-P943-20240313174719

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/865089.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DINO-X环境搭建推理测试

​引子 开放世界检测,前文也写OV-DINO(感兴趣的童鞋,请移步OV-DINO开放词检测环境安装与推理-CSDN博客)。这不,DINO系列又更新了。OK,那就让我们开始吧。 一、模型介绍 IDEA 开发了一个通用物体提示来支持无提示的开放世界检测,从而无需用户提供任何提示即可检测图像中…

一文说透汇编语言中的各种地址

本文讨论了学习汇编语言时一些易混淆的、关于地址的概念前言 由于笔者水平有限,随笔中难免有些许纰漏和错误,希望广大读者能指正。 一、各种地址之间的区分 笔者在刚开始学习汇编语言时,不是很能分清楚汇编地址、逻辑地址、物理地址、段地址、偏移地址、线性地址等概念,这对…

什么是自动化测试?为什么要做自动化测试?如何学习自动化测试?

自动化测试是指使用软件工具和脚本来执行测试任务的过程。它可以自动化执行测试用例、生成测试报告和进行结果分析。自动化测试可以减少人工测试的工作量,并提高测试的效率和准确性。它在软件开发过程中起到了重要的作用,可以帮助开发团队快速发现和修复软件中的缺陷,确保软…

o3 发布了,摔碎了码农的饭碗

大家好,我是汤师爷~ 在 2024 年底,OpenAI 发布了最新推理模型 o3。o3模型相当炸裂,在世界级编程比赛中拿下第 175 名,打败 99.9% 的参赛者。AI 写代码都赶上顶级程序员了,程序员是不是要失业?最近不少读者反馈,像 GitHub Copilot、Claude Sonnet 3.5、Cursor 等 AI 辅助…

Diary - 2025.01.06

回到正轨了,感觉今天好像不太摆了,但还是在小摸阿发现昨天日期写成 2024 了。明天计划来说应该是主要写题解了!!! 上午还有个模拟赛,但是说不定又是像之前那样拉个 USACO 来(?)。 仍记那时 USACO 金组没 ak,t3 被卡常了,6。 明天要写的题解:Luogu P11513 [ROIR 201…

前端必备基础系列(七)原型/原型链/this

对象的原型: JavaScript中每个对象都有一个特殊的内置属性[[prototype]],这个特殊属性指向另外一个对象。 当我们访问一个对象上的某个属性时,首先会检查这个对象自身有没有这个属性,如果没有就会去[[prototype]]指向的对象查找。 那么这个特性就可以帮助我们实现继承了。 …

cv2.imwrite保存的图像是全黑的

1.保存,全黑的图像cv2.imwrite(img/test.jpg, imutils.resize(enhancedImg, height=151,width=240))2.原因分析 3.原本image是0-255的数值,现在标准化了,全都是0-1之间的数值,还原,乘以255,图片输出正常。cv2.imwrite(img/test1.jpg, imutils.resize(enhancedImg, height…

SaltStack快速入门

Saltstack快速入门 saltstack介绍 Salt,一种全新的基础设施管理方式,部署轻松,在几分钟内可运行起来,扩展性好,很容易管理上万台服务器,速度够快,服务器之间秒级通讯 主要功能:远程执行 配置管理,参考官方文档: http://docs.saltstack.cn/ 安装说明: https://docs.s…

计数问题选讲做题记录

从 $1+1$ 到 $\exp(\sum\limits_{i=1}^k\ln(1+ix))$。计数杂题。calc 考虑先不管数字之间的顺序,最后给答案乘上一个 \(n!\)。 记 \(dp_{i,j}\) 表示前 \(i\) 个数在 \([1,j]\) 之间选,所产生的总贡献,显然有 \(dp_{i,j}=dp_{i,j-1}+j\times dp_{i-1,j-1}\),最后的答案是 \…

如何构建高效的智能体

简单才是王道:构建高效 AI 智能体的秘诀!工作流为简单任务提供可预测性,而智能体在复杂场景中展现灵活性。本指南深入解析如何优化工具设计、选择框架,并平衡复杂性与性能,助你构建可靠且高效的 AI 系统。 如何构建高效的智能体Anthropic 刚刚发布了一份关于“如何构建高…