一、多线程编程的常见坑点

多线程编程就像在厨房里同时开多个灶台做饭,效率确实高了,但稍不注意就会把厨房搞得一团糟。在C#中,最常见的坑点大概有这几个:

  1. 竞态条件(Race Condition):多个线程同时修改同一个变量,结果难以预测。
  2. 死锁(Deadlock):两个线程互相等待对方释放锁,导致程序卡死。
  3. 线程安全集合的使用List<T>不是线程安全的,直接并发操作会出问题。
  4. 线程阻塞(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执行完成");
        }
    }
}

解决方案

  1. 按固定顺序获取锁:所有线程都先获取lockA再获取lockB
  2. 设置超时:用Monitor.TryEnter(lockObj, timeout)避免无限等待。

四、高级玩法:Task和async/await

现代C#推荐用Taskasync/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()用于优雅终止消费者。

六、总结与最佳实践

  1. 优先用高级API:如TaskParallelasync/await,减少直接操作Thread
  2. 锁的注意事项:避免嵌套锁、控制锁粒度、考虑用ReaderWriterLockSlim优化读多写少场景。
  3. 性能监控:用ConcurrentQueueMemoryCache等线程安全集合减少竞争。
  4. 调试工具:VS的并行堆栈窗口和System.Diagnostics.ProcessThread能帮大忙。

多线程编程既考验技术,也考验耐心。就像开车,既要快,又要稳,还得随时注意路上的坑。