在C#的多线程开发场景中,非线程安全的代码指的是没有内置同步机制、多个线程同时执行时会导致内部状态被破坏的方法或类。这类代码如果直接被多个线程并发调用,大概率会出现数据异常、逻辑错误甚至程序崩溃的问题,因此需要额外的同步手段来保证调用安全。

为什么非线程安全代码不能直接多线程调用
非线程安全的代码通常没有对共享资源的访问做互斥控制,比如下面的示例类,它的Add方法操作的是实例内部的共享计数器,多个线程同时调用时就会出现计数错误:
using System;
using System.Threading;
// 非线程安全的计数器类
public class UnsafeCounter
{
private int count = 0;
// 非线程安全的累加方法
public void Add(int value)
{
int temp = count;
// 模拟耗时操作,放大并发问题
Thread.Sleep(10);
count = temp + value;
}
public int GetCount()
{
return count;
}
}
class Program
{
static void Main()
{
UnsafeCounter counter = new UnsafeCounter();
// 创建两个线程同时调用Add方法
Thread t1 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
counter.Add(1);
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
counter.Add(1);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
// 预期结果是10,实际结果大概率小于10
Console.WriteLine($"最终计数: {counter.GetCount()}");
}
}
使用lock语句实现互斥调用
lock语句是C#中最常用的同步方式,它会保证同一时间只有一个线程能执行被锁定的代码块,适合保护非线程安全的代码调用。lock的对象需要是引用类型,且所有调用非线程安全代码的线程都要使用同一个锁对象。
using System;
using System.Threading;
public class UnsafeCounter
{
private int count = 0;
public void Add(int value)
{
int temp = count;
Thread.Sleep(10);
count = temp + value;
}
public int GetCount()
{
return count;
}
}
class Program
{
static void Main()
{
UnsafeCounter counter = new UnsafeCounter();
// 定义统一的锁对象
object lockObj = new object();
Thread t1 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
// 使用lock包裹非线程安全代码的调用
lock (lockObj)
{
counter.Add(1);
}
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
lock (lockObj)
{
counter.Add(1);
}
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
// 此时结果一定是10
Console.WriteLine($"最终计数: {counter.GetCount()}");
}
}
lock语句的注意事项
- 不要使用
this、字符串、值类型作为锁对象,避免锁冲突或无效锁 - 锁定的代码块要尽量小,只包裹非线程安全代码的调用部分,减少线程阻塞时间
- 不要在被锁定的代码块中调用外部未知的方法,避免死锁风险
使用Monitor类实现更灵活的同步
lock语句本质是Monitor类的语法糖,如果需要更灵活的控制,比如尝试获取锁、设置锁超时时间,可以直接使用Monitor类。这种方式适合需要判断锁获取状态或者避免长期阻塞的场景。
using System;
using System.Threading;
public class UnsafeCounter
{
private int count = 0;
public void Add(int value)
{
int temp = count;
Thread.Sleep(10);
count = temp + value;
}
public int GetCount()
{
return count;
}
}
class Program
{
static void Main()
{
UnsafeCounter counter = new UnsafeCounter();
object lockObj = new object();
Thread t1 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
// 尝试获取锁,最多等待100毫秒
bool lockTaken = false;
try
{
Monitor.TryEnter(lockObj, 100, ref lockTaken);
if (lockTaken)
{
counter.Add(1);
}
else
{
Console.WriteLine("获取锁失败,跳过本次调用");
}
}
finally
{
if (lockTaken)
{
Monitor.Exit(lockObj);
}
}
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
bool lockTaken = false;
try
{
Monitor.TryEnter(lockObj, 100, ref lockTaken);
if (lockTaken)
{
counter.Add(1);
}
}
finally
{
if (lockTaken)
{
Monitor.Exit(lockObj);
}
}
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"最终计数: {counter.GetCount()}");
}
}
使用线程本地存储隔离调用
如果非线程安全的代码不需要共享状态,每个线程只需要自己的独立实例,那么可以使用线程本地存储(ThreadLocal)来给每个线程分配独立的对象实例,从根源上避免并发冲突。这种方式没有锁的开销,性能更好。
using System;
using System.Threading;
public class UnsafeCounter
{
private int count = 0;
public void Add(int value)
{
int temp = count;
Thread.Sleep(10);
count = temp + value;
}
public int GetCount()
{
return count;
}
}
class Program
{
static void Main()
{
// 每个线程拥有独立的UnsafeCounter实例
ThreadLocal<UnsafeCounter> localCounter = new ThreadLocal<UnsafeCounter>(() => new UnsafeCounter());
Thread t1 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
// 直接调用当前线程的实例,无需加锁
localCounter.Value.Add(1);
}
Console.WriteLine($"线程1计数: {localCounter.Value.GetCount()}");
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 5; i++)
{
localCounter.Value.Add(1);
}
Console.WriteLine($"线程2计数: {localCounter.Value.GetCount()}");
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
localCounter.Dispose();
}
}
不同方案的适用场景对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| lock语句 | 需要共享非线程安全对象,且调用频率不高的场景 | 使用简单,不易出错 | 有锁开销,高并发下可能阻塞 |
| Monitor类 | 需要灵活控制锁获取逻辑,避免长期阻塞的场景 | 控制灵活,支持超时和尝试获取锁 | 使用比lock复杂,容易遗漏释放锁 |
| 线程本地存储 | 非线程安全对象不需要共享状态,每个线程独立使用的场景 | 无锁开销,性能好 | 无法共享对象状态,内存占用更高 |
其他注意事项
如果非线程安全的代码是静态方法,那么锁对象需要是静态的,保证所有线程使用的是同一个锁。另外,如果调用的是第三方库的非线程安全代码,优先查看官方文档是否有推荐的同步方式,避免自行加锁后出现兼容性问题。如果非线程安全代码涉及IO操作,还要考虑IO本身的异步特性,结合异步同步机制做处理,不要单纯依赖线程同步锁。