返回题库高级 .NET 刷题C# 选择题 · 第 36 / 50 篇

C# 试题 36:线程同步

0701 关于 lock 语句的语义,哪项正确?

难度: 进阶

  • A. lock 只防止同一线程重入,不阻止其他线程
  • B. 异常时锁不会释放,必须手动 try/finally
  • C. lock 编译为 Monitor.Enter/Exit 并保证异常路径也释放;Monitor 在同一线程可重入
  • D. lock 使临界区内的所有操作原子
查看答案与解析

正确答案C

解析: lock 展开为 Monitor.Enter/Monitor.ExitExit 放在 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.WaitPulsePulseAll 都要求调用线程已经持有目标对象的锁,否则抛 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 及对应枚举)。doublelong(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 读之后的操作不会被越过它重排。它比全栅栏弱,不能代替 InterlockedThread.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 管理其生命周期。

关键点: 信号量释放后不可再用,注意生命周期。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 34C# 试题 34:取消、异常与上下文20 题
  2. 35C# 试题 35:ValueTask 与异步流20 题
  3. 36C# 试题 36:线程同步20 题
  4. 37C# 试题 37:并行、PLINQ 与 Channel20 题
  5. 38C# 试题 38:GC 与分配20 题
ESC

输入关键词开始搜索