C# 试题 36:线程同步
0701 关于 lock 语句的语义,哪项正确?
难度: 进阶
- A. lock 只防止同一线程重入,不阻止其他线程
- B. 异常时锁不会释放,必须手动 try/finally
- C. lock 编译为 Monitor.Enter/Exit 并保证异常路径也释放;Monitor 在同一线程可重入
- D. lock 使临界区内的所有操作原子
查看答案与解析
正确答案C
解析: lock 展开为 Monitor.Enter/Monitor.Exit,Exit 放在 finally 中,因此异常时也会释放。Monitor 是可重入的:同一线程可以多次进入同一锁(按计数配对退出)。它提供互斥与内存可见性,但不保证“所有操作原子”。
关键点: lock 自带异常安全释放,Monitor 可重入。
0702 以下代码哪项正确?
难度: 实战
void A() { lock ("shared") { /* ... */ } }
void B() { lock (string.Intern("shared")) { /* ... */ } }
- A. 两处锁的是同一个 interned 字符串对象,任何引用同一字面量的代码都会互相阻塞;字符串不应作为锁对象
- B. 两者互不影响,锁的是不同对象
- C. 编译失败:lock 不能锁字符串
- D. 只有 B 能编译
查看答案与解析
正确答案A
解析: 字符串字面量会被驻留(intern),A 中的 "shared" 与 string.Intern("shared") 返回的是同一个实例,因此两段代码锁同一个对象。程序集中任何其他使用该字面量的代码都会与它们互相阻塞,这就是禁止用字符串当锁对象的原因。
关键点: 驻留字符串是全局共享的,锁它等于锁全局。
0703 以下代码的结果是什么?
难度: 实战
private readonly object _gate = new();
async Task Bad()
{
lock (_gate)
{
await Task.Delay(10);
}
}
- A. 编译通过,但运行时会死锁
- B. 编译失败:不能在 lock 语句体中 await(Monitor 不能跨异步持有,C# 编译器报 CS1996)
- C. 编译通过,且完全安全
- D. 运行抛 SynchronizationLockException
查看答案与解析
正确答案B
解析: C# 明确禁止在 lock 语句体内 await(CS1996)。Monitor 的持有与线程绑定,异步恢复可能切换到其他线程,无法保证释放;即使 C# 13+ 的 System.Threading.Lock 类型,其 EnterScope 返回 ref struct,同样不能跨 await。需要跨异步互斥应使用 SemaphoreSlim。
关键点: lock 与 await 互斥是语言级限制,跨异步持锁请用 SemaphoreSlim。
0704 以下代码输出什么?
难度: 实战
object gate = new();
void Outer()
{
lock (gate)
{
Inner();
}
}
void Inner()
{
lock (gate)
{
Console.WriteLine("inner");
}
}
Outer();
- A. 死锁,什么都不输出
- B. 抛 SynchronizationLockException
- C. 编译失败:同一对象不能嵌套 lock
- D. 输出
inner:Monitor 是可重入的,同一线程可再次进入同一锁
查看答案与解析
正确答案D
解析: Monitor 的进入计数是线程级的:同一线程再次 lock 同一对象只是把计数加一,内层正常执行,退出时计数递减,不会死锁。死锁发生在不同线程以相反顺序竞争多个锁时。
关键点: 同一线程重入同一 Monitor 是安全的,计数配对。
0705 以下代码哪项正确?
难度: 进阶
Monitor.Enter(_gate);
try
{
await Task.Delay(10);
}
finally
{
Monitor.Exit(_gate);
}
- A. 编译失败:Monitor.Enter 后不能 await
- B. 编译通过,但这是危险的跨 await 持锁:等待期间其他线程无法进入临界区,且 await 恢复线程可能不同,导致 Exit 抛 SynchronizationLockException;应改用 SemaphoreSlim
- C. 编译通过且完全安全
- D. await 会自动释放 Monitor
查看答案与解析
正确答案B
解析: 语言只禁止 lock 语句体内 await,手动 Monitor.Enter/Exit 仍可编译。但 Monitor 是线程亲和的:await 恢复可能发生在不同的线程池线程上,此时 Monitor.Exit 会抛 SynchronizationLockException;即便同线程,等待期间也阻塞所有竞争者。跨异步互斥应使用 SemaphoreSlim。
关键点: 编译通过不代表安全,Monitor 不能跨 await。
0706 以下代码的结果是什么?
难度: 实战
void Demo()
{
Monitor.Wait(_gate); // 当前线程未持有 _gate 锁
}
- A. 编译失败
- B. 正常运行,但立即超时返回
- C. 运行抛 SynchronizationLockException:Monitor.Wait/Pulse 要求当前线程先持有目标对象锁
- D. 死锁
查看答案与解析
正确答案C
解析: Monitor.Wait、Pulse、PulseAll 都要求调用线程已经持有目标对象的锁,否则抛 SynchronizationLockException。Wait 在等待期间会释放锁并挂起,被 Pulse 唤醒后重新获取锁。
关键点: Wait/Pulse 的前置条件是自己持有锁。
0707 以下代码哪项正确?
难度: 进阶
private readonly SemaphoreSlim _gate = new(1, 1);
async Task Critical()
{
await _gate.WaitAsync();
try
{
await Task.Delay(10);
}
finally
{
_gate.Release();
}
}
- A. 编译失败:SemaphoreSlim 不能跨 await
- B. SemaphoreSlim(1, 1) 模拟互斥锁,但严格互斥性不保证(可重入语义不同)
- C. 会死锁,因为 WaitAsync 与 Monitor 互斥
- D. 编译通过;初值 1、最大值 1 的 SemaphoreSlim 提供互斥语义且可跨 await,finally 中 Release 防止泄漏
查看答案与解析
正确答案D
解析: 信号量计数不绑定线程,WaitAsync 可以跨 await 持有,是异步临界区的标准实现。初值与最大值都为 1 时表现为互斥;但 SemaphoreSlim 不可重入,同一任务重复获取会自锁,因此必须用 try/finally 保证恰好一次 Release。
关键点: 跨 await 互斥用 SemaphoreSlim(1,1),Release 必须配对。
0708 以下代码的结果是什么?
难度: 进阶
int x = 0;
lock (x) { }
- A. 编译失败:lock 语句要求引用类型,值类型会被拒绝(装箱后每次都是新对象,无互斥意义)
- B. 编译通过
- C. 运行抛 InvalidOperationException
- D. 编译通过,且每次锁的都是同一个对象
查看答案与解析
正确答案A
解析: lock 的操作数必须是引用类型(CS0185)。值类型即使允许,也会被装箱成新对象,每个线程锁的是不同实例,互斥完全失效,因此编译器直接拒绝。
关键点: 不能 lock 值类型,锁对象必须是引用类型且私有稳定。
0709 以下代码输出什么?
难度: 实战
int count = 5;
int result = Interlocked.Increment(ref count);
Console.WriteLine($"{count} {result}");
- A.
5 5 - B.
6 6:Increment 原子递增并返回递增后的新值 - C.
6 5:Increment 返回旧值 - D.
5 6
查看答案与解析
正确答案B
解析: Interlocked.Increment 执行原子读-改-写并返回递增后的新值,与 count++ 返回旧值不同。这也是用它做计数器的常见注意点。
关键点: Interlocked.Increment 返回新值,前缀语义。
0710 以下代码输出什么?
难度: 实战
int current = 10;
int original = Interlocked.CompareExchange(ref current, 20, 10);
Console.WriteLine($"{original} {current}");
- A.
10 10 - B.
20 10 - C.
10 20:比较相等则替换,CompareExchange 返回原值 - D.
20 20
查看答案与解析
正确答案C
解析: Interlocked.CompareExchange(ref location, value, comparand):当 location == comparand(10 == 10)时把 location 替换为 value(20),无论是否替换都返回原值(10)。因此输出 10 20。
关键点: CompareExchange 返回旧值,这是 CAS 循环的基石。
0711 以下代码哪项正确?
难度: 实战
int counter = 0;
// 两个线程各自对 counter 执行 100_000 次 counter++
Console.WriteLine(counter);
- A. counter++ 是读-改-写,非原子;并发下结果可能小于 200_000,应使用 Interlocked.Increment 或锁
- B. counter++ 是原子的,结果必为 200_000
- C. 结果必为 0
- D. 编译失败:并发修改字段必须声明 volatile
查看答案与解析
正确答案A
解析: counter++ 编译为读取、加一、写回三步,两个线程交错会丢失更新,最终值通常小于期望。Interlocked.Increment 或锁才能保证每次递增原子完成;volatile 只解决可见性,不解决读-改-写。
关键点: 复合操作需要 Interlocked 或锁,volatile 不够。
0712 以下代码输出什么?
难度: 实战
double d = 1.5;
double old = Interlocked.Exchange(ref d, 2.5);
Console.WriteLine($"{old} {d}");
- A. 编译失败:Interlocked 不支持 double
- B.
1.5 2.5:Interlocked.Exchange 提供 double 的原子读写 - C. 运行抛 NotSupportedException
- D.
2.5 2.5
查看答案与解析
正确答案B
解析: Interlocked 除了 int/long,还提供 double 的 Exchange/CompareExchange(底层用 64 位原子指令或 CAS 循环实现)。交换返回原值 1.5,字段变为 2.5。
关键点: double 的原子读写可以用 Interlocked.Exchange/CompareExchange。
0713 以下代码哪项正确?
难度: 实战
private volatile bool _stop;
void Loop()
{
while (!_stop)
{
// 工作
}
}
void Stop() => _stop = true;
- A. volatile 使 _stop++ 原子
- B. 编译失败:bool 不能声明为 volatile
- C. volatile 保证循环整体原子
- D. volatile 让 _stop 的读写具备获取/释放语义,不被优化为寄存器缓存,跨线程能看到最新值;但复合操作仍不原子
查看答案与解析
正确答案D
解析: volatile bool 合法。volatile 读是获取语义、写是释放语义,并阻止编译器把字段缓存到寄存器,所以循环能观察到其他线程的写入。但它不提供原子读-改-写,也不等价于全内存栅栏。
关键点: volatile 解决可见性,不解决原子性。
0714 以下代码的结果是什么?
难度: 进阶
class Foo
{
private volatile double _d;
private volatile long _l;
}
- A. 都合法
- B. 都编译失败:volatile 只允许引用类型及 byte/sbyte/short/ushort/int/uint/char/float/bool 和基于它们的枚举,double 与 long 不允许(CS0677)
- C. 都合法,但只在 64 位进程有效
- D. long 合法,double 不合法
查看答案与解析
正确答案B
解析: volatile 字段的类型限制在引用类型、原生整数族(byte/sbyte/short/ushort/int/uint/char/float/bool 及对应枚举)。double 和 long(8 字节)不在允许列表,声明即报 CS0677。对 long/double 的并发访问应使用 Interlocked 或锁。
关键点: volatile 不是想加就加,double/long 直接编译失败。
0715 以下代码哪项正确?
难度: 进阶
private volatile int _counter;
// 多个线程并发执行 _counter++
- A. volatile 不提供原子性,_counter++ 仍是读-改-写,并发下可能丢失更新;需要 Interlocked.Increment 或锁
- B. volatile 使 _counter++ 原子
- C. 编译失败:volatile 字段不能参与运算
- D. 结果必等于线程数
查看答案与解析
正确答案A
解析: volatile 只约束字段读写本身的重排与缓存行为;_counter++ 是“读 volatile 字段、加一、写 volatile 字段”,中间步骤仍可交错,更新照样丢失。递增类操作必须用 Interlocked。
关键点: volatile + 复合操作仍然是数据竞争。
0716 关于 volatile 的内存语义,哪项正确?
难度: 进阶
- A. volatile 是全内存栅栏,等价于 Thread.MemoryBarrier
- B. volatile 只能用于值类型
- C. volatile 写是释放语义、读是获取语义,能阻止相关重排,但不是全栅栏,也不能让复合操作原子
- D. volatile 保证 long 的原子读写
查看答案与解析
正确答案C
解析: .NET 的 volatile 提供获取/释放(acquire/release)语义:volatile 写之前、volatile 读之后的操作不会被越过它重排。它比全栅栏弱,不能代替 Interlocked 或 Thread.MemoryBarrier 的全部场景,也不提供原子性。
关键点: volatile = 获取/释放语义,不是全栅栏。
0717 以下代码的结果是什么?
难度: 实战
var sem = new SemaphoreSlim(2, 2);
sem.Release();
- A. 正常运行,计数变为 3
- B. 运行抛 SemaphoreFullException:初始计数已等于最大计数,未先 Wait 就 Release 会超出上限
- C. 编译失败
- D. 计数不变
查看答案与解析
正确答案B
解析: 信号量计数不能超过构造时指定的最大值。初始计数已是 2(等于 max),此时 Release() 试图把计数加到 3,超出上限,抛 SemaphoreFullException。Release 次数必须与获得次数配对。
关键点: 过度 Release 抛 SemaphoreFullException,计数受最大值约束。
0718 关于 SemaphoreSlim 的公平性,哪项正确?
难度: 进阶
- A. 严格 FIFO:最先等待的线程一定先获得
- B. 最后一个调用 WaitAsync 的线程一定先获得
- C. 获得顺序随机,无法预测
- D. SemaphoreSlim 不保证严格 FIFO:释放后由哪个等待者获得不构成公平性契约,不应依赖等待顺序
查看答案与解析
正确答案D
解析: SemaphoreSlim 内部维护等待队列,但其文档明确不提供严格的先进先出保证(唤醒哪个等待者取决于内部实现,历史上甚至可能偏向新请求)。依赖等待顺序的代码属于假设未承诺的行为。
关键点: 信号量无公平性契约,不要依赖等待顺序。
0719 以下代码输出什么?
难度: 实战
var sem = new SemaphoreSlim(0, 1);
var cts = new CancellationTokenSource();
cts.Cancel();
try
{
await sem.WaitAsync(cts.Token);
Console.WriteLine("acquired");
}
catch (OperationCanceledException)
{
Console.WriteLine("canceled");
}
- A.
acquired - B.
canceled:WaitAsync 支持取消令牌,取消时抛 OperationCanceledException 且不会占用信号量 - C. 死锁
- D. 抛 SemaphoreFullException
查看答案与解析
正确答案B
解析: token 已取消,WaitAsync 直接抛 OperationCanceledException,且不会消耗信号量计数。信号量初值为 0 说明本应等待,但取消优先于获取。
关键点: WaitAsync 可取消,取消时不占名额。
0720 以下代码的结果是什么?
难度: 实战
var sem = new SemaphoreSlim(1, 1);
sem.Dispose();
await sem.WaitAsync();
- A. 抛 ObjectDisposedException:已释放的信号量不能再使用
- B. 正常运行
- C. 死锁
- D. 编译失败
查看答案与解析
正确答案A
解析: SemaphoreSlim 实现 IDisposable,Dispose 后任何 Wait/WaitAsync/Release 调用都抛 ObjectDisposedException。跨异步场景常配合 await using 管理其生命周期。
关键点: 信号量释放后不可再用,注意生命周期。