【C#多线程面试必备】锁机制与异步陷阱,你真的会用Lock吗?
引言:一次尴尬的面试经历
“我因为不知道C#锁的工作原理而在一次面试中失败了。读完这篇文章,保证你不会重蹈覆辙。”
——一位开发者的自白
多线程编程对于许多软件开发者来说,既是晋升高阶的必经之路,也是每次面试绕不开的考点。特别是当你面对涉及资源争用、并发控制的问题时,一个小小的锁机制理解不到位,可能就会导致“当场翻车”。
今天,我们就从一个真实的面试场景出发,聊聊C#中的锁与异步操作,拆解那些让人头疼的陷阱,并带你走出误区!
为什么要使用锁?保护共享资源&并发控制
在多线程环境下,如果多个线程同时访问某个资源(比如全局变量、文件、数据库连接等),就有可能出现数据错乱或竞争条件(Race Condition)。因此,“加锁”成了并发编程绕不开的话题。
常见需求:
-
保护昂贵资源
比如防止同一时间多次写入数据库。 -
实现并发控制
限制某段代码同一时刻只能有固定数量的线程访问。
面试现场:lock语句失灵了!
假设你遇到了这样一道题:
- 你知道需要一个锁来保护资源;
- 但是这个方法是
async
异步的; - 于是你顺手用了
lock
关键字。
此时,bug就悄然埋下了……💥
lock语句为啥和async不兼容?
lock本质上是对一个对象加独占锁,保证一段代码块在同一时刻只被一个线程访问。但在async/await异步方法中,代码执行会被挂起,然后切换到其他线程继续执行,这样lock保护的作用范围就被打破了!
也就是说:
lock
只能保护同步代码块;- 一旦遇到
await
,任务可能在任何线程恢复; - lock失效,临界区保护形同虚设。
正确姿势:用异步同步原语解决
那么,在C#中怎么优雅地处理异步方法中的并发问题?
常见解决方案
1️⃣ SemaphoreSlim —— 最推荐的异步同步工具
private static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public async Task MyAsyncMethod()
{
// 等待进入临界区,有timeout防止死锁
await _semaphore.WaitAsync(TimeSpan.FromSeconds(5));
try
{
// 临界区代码
await DoSomethingAsync();
}
finally
{
// 一定要释放,否则会死锁!
_semaphore.Release();
}
}
要点:
- 使用
WaitAsync()
支持异步等待; - 建议设置timeout参数,防止某些情况下卡死;
- 用
try-finally
保证释放锁,即使发生异常也不会死锁。
2️⃣ 其他常见同步原语
- Mutex:可以用于跨进程,但重量级,一般不推荐做高频操作;
- Monitor:底层API,通常配合lock使用,不适合异步;
- Semaphore:经典信号量,但不支持await异步操作。
结论:在异步场景下推荐用SemaphoreSlim!
总结与实用建议
- 牢记:lock不能用于async/await方法!
- 遇到并发资源保护,优先考虑SemaphoreSlim。
- 写多线程/异步代码时,养成try-finally释放锁的好习惯。
- 如有必要,为WaitAsync设置合理的超时时间。