一、多线程编程的常见坑点
多线程编程就像在厨房里同时开多个灶台做饭,效率确实高了,但稍不注意就会把厨房搞得一团糟。在C#中,最常见的坑点大概有这几个:
- 竞态条件(Race Condition):多个线程同时修改同一个变量,结果难以预测。
- 死锁(Deadlock):两个线程互相等待对方释放锁,导致程序卡死。
- 线程安全集合的使用:
List<T>不是线程安全的,直接并发操作会出问题。 - 线程阻塞(Blocking):不合理的锁或同步机制导致性能下降。
下面用代码演示一个典型的竞态条件问题(技术栈:C#/.NET 6):
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Main()
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"最终结果: {counter}"); // 预期是20000,实际可能小于
}
static void IncrementCounter()
{
for (int i = 0; i < 10000; i++)
{
counter++; // 非原子操作,可能被其他线程打断
}
}
}
注释:
counter++实际上分为“读取→修改→写入”三步,线程切换可能导致丢失更新。- 最终结果可能小于20000,这就是典型的竞态条件。
二、解决竞态条件的几种武器
1. 用lock关键字加锁
最简单的解决方案是加锁,保证同一时间只有一个线程能执行关键代码:
static object lockObj = new object();
static void IncrementCounter()
{
for (int i = 0; i < 10000; i++)
{
lock (lockObj) // 用lock保护共享变量
{
counter++;
}
}
}
注意事项:
- 锁对象建议用
private readonly object,避免外部误用。 - 锁的粒度太大会降低性能,太小可能起不到保护作用。
2. 使用Interlocked原子操作
对于简单的数值操作,Interlocked类性能更高:
static void IncrementCounter()
{
for (int i = 0; i < 10000; i++)
{
Interlocked.Increment(ref counter); // 原子自增
}
}
适用场景:
- 适合计数器等简单操作。
- 比lock轻量,但不能用于复杂逻辑。
三、死锁:如何避免线程“互相掐架”
死锁就像两个人过独木桥,谁也不让谁,最后谁都过不去。下面是一个典型死锁示例:
object lockA = new object();
object lockB = new object();
void Method1()
{
lock (lockA)
{
Thread.Sleep(100); // 故意制造切换时机
lock (lockB) // 等待lockB释放
{
Console.WriteLine("Method1执行完成");
}
}
}
void Method2()
{
lock (lockB)
{
lock (lockA) // 等待lockA释放
{
Console.WriteLine("Method2执行完成");
}
}
}
解决方案:
- 按固定顺序获取锁:所有线程都先获取
lockA再获取lockB。 - 设置超时:用
Monitor.TryEnter(lockObj, timeout)避免无限等待。
四、高级玩法:Task和async/await
现代C#推荐用Task和async/await代替直接操作线程。例如:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task task1 = DoWorkAsync("任务1");
Task task2 = DoWorkAsync("任务2");
await Task.WhenAll(task1, task2); // 等待所有任务完成
Console.WriteLine("所有任务完成");
}
static async Task DoWorkAsync(string name)
{
await Task.Run(() =>
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"{name} 执行第{i}步");
Thread.Sleep(100); // 模拟耗时操作
}
});
}
}
优点:
- 避免手动管理线程。
- 配合
CancellationToken可以轻松实现取消操作。
五、实战:生产者-消费者模式
多线程编程中,生产者-消费者是一个经典场景。用BlockingCollection可以优雅实现:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class ProducerConsumerDemo
{
static BlockingCollection<int> queue = new BlockingCollection<int>(boundedCapacity: 10);
static void Main()
{
Task producer = Task.Run(Produce);
Task consumer = Task.Run(Consume);
Task.WaitAll(producer, consumer);
}
static void Produce()
{
for (int i = 0; i < 20; i++)
{
queue.Add(i); // 如果队列满,会自动阻塞
Console.WriteLine($"生产: {i}");
Thread.Sleep(200);
}
queue.CompleteAdding(); // 通知消费者结束
}
static void Consume()
{
foreach (int item in queue.GetConsumingEnumerable())
{
Console.WriteLine($"消费: {item}");
Thread.Sleep(300);
}
}
}
技术栈:C#/.NET 6
注释:
BlockingCollection自带线程安全特性和阻塞功能。CompleteAdding()用于优雅终止消费者。
六、总结与最佳实践
- 优先用高级API:如
Task、Parallel、async/await,减少直接操作Thread。 - 锁的注意事项:避免嵌套锁、控制锁粒度、考虑用
ReaderWriterLockSlim优化读多写少场景。 - 性能监控:用
ConcurrentQueue、MemoryCache等线程安全集合减少竞争。 - 调试工具:VS的并行堆栈窗口和
System.Diagnostics.ProcessThread能帮大忙。
多线程编程既考验技术,也考验耐心。就像开车,既要快,又要稳,还得随时注意路上的坑。
评论