Skip to content

05-线程和异步编程 ​

C# 支持多线程和异步编程,让程序能够同时执行多个任务,提高响应速度和资源利用率。


一、进程、线程与任务的概念 ​

概念说明类比
进程(Process)正在运行的程序实例,拥有独立的内存空间一家工厂
线程(Thread)进程内的执行单元,共享进程内存工厂里的工人
任务(Task)抽象的工作单元,由线程调度执行工人要完成的一项工作

二、线程基础(Thread) ​

System.Threading.Thread 是 C# 中创建和操作线程的基本类。

1. 创建和启动线程 ​

csharp
using System.Threading;

// 创建线程
Thread thread = new Thread(WorkerMethod);
thread.Start();  // 启动线程

static void WorkerMethod()
{
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId}:{i}");
        Thread.Sleep(500);  // 暂停 500ms
    }
}

带参数的线程 ​

csharp
// 方式一:ParameterizedThreadStart
Thread thread = new Thread(obj =>
{
    string? message = obj as string;
    Console.WriteLine($"收到消息:{message}");
});
thread.Start("Hello from thread!");

// 方式二:Lambda 捕获变量(推荐)
string data = "线程数据";
Thread thread2 = new Thread(() =>
{
    Console.WriteLine($"处理:{data}");
});
thread2.Start();

2. 线程控制方法 ​

方法/属性说明注意事项
thread.Start()启动线程每个线程只能启动一次
thread.Join()等待线程完成阻塞当前线程直到目标线程结束
Thread.Sleep(ms)当前线程暂停单位毫秒,Sleep(0) 让出时间片
thread.IsAlive线程是否存活只读属性
thread.Name设置线程名称便于调试,只能设置一次
thread.Priority线程优先级Normal、High、Lowest 等
thread.IsBackground是否为后台线程默认为前台线程
csharp
Thread thread = new Thread(() =>
{
    Thread.Sleep(1000);
    Console.WriteLine("线程完成");
});

thread.Name = "WorkerThread";
thread.Priority = ThreadPriority.Highest;  // 设置优先级
thread.Start();

Console.WriteLine($"线程是否存活:{thread.IsAlive}");  // True
thread.Join();  // 阻塞等待线程完成(最多等 2 秒)
thread.Join(2000);  // 超时版本
Console.WriteLine("主线程继续");

3. 前台线程 vs 后台线程 ​

csharp
// 前台线程(默认):应用程序必须等待所有前台线程退出
Thread foreground = new Thread(() =>
{
    Thread.Sleep(5000);
    Console.WriteLine("前台线程完成");
});
foreground.IsBackground = false;
foreground.Start();
// 程序会等待 5 秒才退出

// 后台线程:应用程序退出时自动终止
Thread background = new Thread(() =>
{
    Thread.Sleep(5000);
    Console.WriteLine("后台线程完成");  // 主程序退出时可能不会执行到这里
});
background.IsBackground = true;
background.Start();
// 程序不会等待后台线程

4. 线程安全问题 ​

csharp
private static int counter = 0;
private static readonly object lockObj = new object();

// ❌ 线程不安全:多个线程同时修改共享变量
static void UnsafeIncrement()
{
    for (int i = 0; i < 100000; i++)
        counter++;  // 非原子操作,数据竞争
}

// ✅ 线程安全:使用 lock 保护临界区
static void SafeIncrement()
{
    for (int i = 0; i < 100000; i++)
    {
        lock (lockObj)
        {
            counter++;
        }
    }
}

三、ThreadPool(线程池) ​

线程池复用线程,避免频繁创建和销毁线程的开销。

csharp
// 将任务添加到线程池
ThreadPool.QueueUserWorkItem(state =>
{
    Console.WriteLine($"线程池线程:{Thread.CurrentThread.ManagedThreadId}");
});

// 使用 Task 更方便(推荐)
Task.Run(() =>
{
    Console.WriteLine("使用 Task 在线程池执行");
});

// 设置线程池大小
ThreadPool.SetMinThreads(4, 4);
ThreadPool.SetMaxThreads(100, 100);

Thread vs ThreadPool vs Task 对比 ​

对比项ThreadThreadPoolTask
创建速度慢快快
控制粒度细(状态、优先级)粗中等
返回值不能直接返回不能直接返回可以通过 Task<T> 返回
异常处理需在线程方法内部需在回调内部自动捕获,由 await 抛出
取消支持不原生支持不原生支持支持 CancellationToken
延续操作需手动 Join不原生支持ContinueWith / await
推荐场景长时间运行的任务短时间批量任务通用异步操作(推荐)

四、Task(任务) ​

Task 是 .NET 推荐的异步编程模型,比 Thread 更高级、更易用。

1. 创建和启动 Task ​

csharp
// 方式一:Task.Run(推荐)
Task task1 = Task.Run(() =>
{
    Console.WriteLine("任务执行中");
});

// 方式二:Task.Factory.StartNew(更灵活)
Task task2 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("通过工厂创建");
}, TaskCreationOptions.LongRunning);  // 适合长时间运行

// 方式三:直接 new 然后 Start(不常见)
Task task3 = new Task(() => Console.WriteLine("new + Start"));
task3.Start();

// 等待任务
task1.Wait();                          // 阻塞直到完成
Task.WaitAll(task1, task2);            // 等待所有完成
Task.WaitAny(task1, task2);            // 等待任意一个完成

2. Task 带返回值 ​

csharp
Task<int> task = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 100; i++)
        sum += i;
    return sum;
});

// 获取结果(会阻塞)
int result = task.Result;
Console.WriteLine($"1+2+...+100 = {result}");  // 5050

// 或通过 await 获取(不阻塞)
async Task PrintResultAsync()
{
    int value = await task;  // 不阻塞
    Console.WriteLine(value);
}

3. 任务延续(ContinueWith) ​

csharp
Task<int> task = Task.Run(() => 42);

Task continuation = task.ContinueWith(previous =>
{
    int result = previous.Result;
    Console.WriteLine($"前一个任务的结果:{result}");
});

// 条件延续
task.ContinueWith(t => Console.WriteLine("成功"), TaskContinuationOptions.OnlyOnRanToCompletion);
task.ContinueWith(t => Console.WriteLine("失败"), TaskContinuationOptions.OnlyOnFaulted);

五、async/await 异步编程 ​

async 和 await 是 C# 5.0 引入的异步编程模型,让异步代码看起来像同步代码。

1. 基本用法 ​

csharp
using System.Net.Http;

public static async Task<int> DownloadAndCountAsync(string url)
{
    using HttpClient client = new HttpClient();

    // await 不会阻塞线程,而是异步等待
    string content = await client.GetStringAsync(url);

    return content.Length;
}

// 调用
static async Task Main()
{
    Console.WriteLine("开始下载...");
    int length = await DownloadAndCountAsync("https://example.com");
    Console.WriteLine($"下载完成,字符数:{length}");
}

2. async/await 工作原理 ​

调用 async 方法
    │
    ▼
┌─────────────────────┐
│ 同步执行到第一个 await │
└─────────┬───────────┘
          │
    ┌─────▼──────┐
    │await 是否完成?│
    └─────┬──────┘
    是◄───┴───► 否
     │          │
     │     ┌────▼──────┐
     │     │挂起方法     │
     │     │线程返回调用方 │
     │     │等待操作完成  │
     │     └────┬──────┘
     │          │ 操作完成
     │     ┌────▼──────┐
     │     │恢复方法执行 │
     │     │(从线程池取)│
     │     └────┬──────┘
     │          │
     └──────────┘
          │
     ┌────▼────┐
     │返回结果   │
     └─────────┘

3. async/await 最佳实践 ​

csharp
// ✅ 正确:返回 Task 或 Task<T>
public async Task DoSomethingAsync()
{
    await Task.Delay(1000);
}

public async Task<int> CalculateAsync()
{
    await Task.Delay(500);
    return 42;
}

// ⚠️ 避免 async void(无法被 await,异常难捕获)
// 只在事件处理中使用
public async void Button_Click(object sender, EventArgs e)
{
    await DoSomethingAsync();
}

// ✅ 异步方法命名以 Async 结尾
public Task<string> FetchDataAsync() { ... }

// ✅ ConfigureAwait(false) —— 不需要返回到原同步上下文时
public async Task ReadFileAsync()
{
    using var reader = new StreamReader("file.txt");
    string content = await reader.ReadToEndAsync().ConfigureAwait(false);
    // 后续代码不在原 SynchronizationContext 上执行
}

4. 多任务并行 ​

csharp
// 顺序执行(每个 await 等待上一个完成)
async Task ProcessSequentialAsync()
{
    var result1 = await Task1Async();
    var result2 = await Task2Async();  // 等 Task1 完成后
}

// 并行执行(同时启动,同时等待)
async Task ProcessParallelAsync()
{
    Task<int> task1 = Task1Async();
    Task<int> task2 = Task2Async();

    // 同时等待两个任务
    await Task.WhenAll(task1, task2);

    // 安全获取结果
    int r1 = task1.Result;
    int r2 = task2.Result;
}

// 任意一个完成就继续
async Task<string> FastestResponseAsync()
{
    Task<string> task1 = FetchFromServer1Async();
    Task<string> task2 = FetchFromServer2Async();

    Task<string> fastest = await Task.WhenAny(task1, task2);
    return await fastest;
}

// 批量并行
async Task ProcessAllAsync(IEnumerable<int> items)
{
    var tasks = items.Select(item => ProcessItemAsync(item));
    await Task.WhenAll(tasks);  // 所有任务并行执行
}

5. 取消异步操作 ​

csharp
async Task LongRunningOperation(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();  // 检查取消

        Console.WriteLine($"进度:{i}%");
        await Task.Delay(500, cancellationToken);  // 带取消的延迟
    }
}

// 使用
var cts = new CancellationTokenSource();
Task task = LongRunningOperation(cts.Token);

// 5 秒后取消
await Task.Delay(5000);
cts.Cancel();  // 发送取消请求

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("操作已被取消");
}

6. ValueTask vs Task ​

对比Task / Task<T>ValueTask / ValueTask<T>
堆分配总是分配在堆上值类型,可避免堆分配
性能通用,适用于大多数场景同步完成或频繁调用时更快
限制可多次 await只能 await 一次
推荐场景通用异步方法结果可能同步返回的热路径
csharp
// ValueTask 示例:结果可能缓存
private int? _cachedResult;

public async ValueTask<int> GetResultAsync()
{
    if (_cachedResult.HasValue)
        return _cachedResult.Value;  // 同步返回,无堆分配

    int result = await FetchFromDatabaseAsync();
    _cachedResult = result;
    return result;
}

六、同步机制 ​

1. lock 语句 ​

csharp
private static readonly object _lock = new object();
private static int sharedCounter = 0;

public static void Increment()
{
    lock (_lock)  // 同一时间只有一个线程能进入
    {
        sharedCounter++;
    }
}

2. Monitor ​

csharp
// lock 实际上是 Monitor 的语法糖
Monitor.Enter(_lock);
try
{
    sharedCounter++;
}
finally
{
    Monitor.Exit(_lock);  // 确保释放锁
}

// Monitor.TryEnter 可以设置超时
if (Monitor.TryEnter(_lock, TimeSpan.FromSeconds(1)))
{
    try { /* ... */ }
    finally { Monitor.Exit(_lock); }
}
else
{
    Console.WriteLine("获取锁超时");
}

3. ReaderWriterLockSlim ​

适合读多写少的场景——多个线程可以同时读,但写时必须独占。

csharp
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
private Dictionary<string, string> cache = new Dictionary<string, string>();

public string? Read(string key)
{
    rwLock.EnterReadLock();
    try
    {
        return cache.GetValueOrDefault(key);
    }
    finally
    {
        rwLock.ExitReadLock();
    }
}

public void Write(string key, string value)
{
    rwLock.EnterWriteLock();
    try
    {
        cache[key] = value;
    }
    finally
    {
        rwLock.ExitWriteLock();
    }
}

4. SemaphoreSlim(信号量) ​

限制同时访问的线程数量。

csharp
SemaphoreSlim semaphore = new SemaphoreSlim(3);  // 最多 3 个线程同时访问

for (int i = 0; i < 10; i++)
{
    int id = i;
    Task.Run(async () =>
    {
        await semaphore.WaitAsync();
        try
        {
            Console.WriteLine($"线程 {id} 开始工作");
            await Task.Delay(1000);
        }
        finally
        {
            semaphore.Release();
        }
    });
}

5. Mutex ​

可用于跨进程同步。

csharp
using (Mutex mutex = new Mutex(false, "Global\\MyMutex"))
{
    if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
    {
        try { /* 访问共享资源 */ }
        finally { mutex.ReleaseMutex(); }
    }
}

6. 同步机制对比 ​

机制用途跨进程异步支持性能
lock / Monitor保护共享资源否否快
ReaderWriterLockSlim读多写少场景否否中等
SemaphoreSlim限制并发数否是(WaitAsync)快
Mutex跨进程同步是否慢
SpinLock极短临界区否否极快(自旋等待)

七、并行编程(Parallel) ​

用于数据并行和任务并行,自动利用多核 CPU。

csharp
using System.Threading.Tasks;

// Parallel.For:并行 for 循环
Parallel.For(0, 10, i =>
{
    Console.WriteLine($"处理 {i}(线程 {Thread.CurrentThread.ManagedThreadId})");
});

// Parallel.ForEach:并行遍历
var items = new[] { "A", "B", "C", "D", "E" };
Parallel.ForEach(items, item =>
{
    Console.WriteLine($"处理 {item}");
});

// Parallel.Invoke:并行执行多个操作
Parallel.Invoke(
    () => Console.WriteLine("任务1"),
    () => Console.WriteLine("任务2"),
    () => Console.WriteLine("任务3")
);

// 控制并行度
var options = new ParallelOptions { MaxDegreeOfParallelism = 2 };
Parallel.ForEach(items, options, item =>
{
    Console.WriteLine($"处理 {item}(最大并行度 2)");
});

注意: Parallel 类会阻塞调用线程,不适合 UI 线程。在 UI 应用中使用 Task 和 async/await。


八、死锁和常见问题 ​

1. 死锁示例 ​

csharp
static readonly object lockA = new object();
static readonly object lockB = new object();

static void Method1()
{
    lock (lockA)
    {
        Thread.Sleep(100);
        lock (lockB)  // 等待 lockB
        {
            Console.WriteLine("Method1");
        }
    }
}

static void Method2()
{
    lock (lockB)
    {
        Thread.Sleep(100);
        lock (lockA)  // 等待 lockA → 死锁!
        {
            Console.WriteLine("Method2");
        }
    }
}

// 两个线程互相等待对方释放锁 → 永远阻塞

2. 避免死锁的方法 ​

csharp
// 方法1:统一锁的顺序
static void SafeMethod1()
{
    lock (lockA) { lock (lockB) { /* ... */ } }
}
static void SafeMethod2()
{
    lock (lockA) { lock (lockB) { /* ... */ } }  // 和 Method1 顺序一致
}

// 方法2:使用 Monitor.TryEnter 设置超时
if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1)))
{
    try
    {
        if (Monitor.TryEnter(lockB, TimeSpan.FromSeconds(1)))
        {
            try { /* ... */ }
            finally { Monitor.Exit(lockB); }
        }
    }
    finally { Monitor.Exit(lockA); }
}

// 方法3:避免嵌套锁(最推荐)
// 尽量不在一个锁内获取另一个锁

3. async/await 死锁 ​

csharp
// ❌ 死锁原因:在同步上下文中阻塞异步方法
public void Deadlock()
{
    // .Result 或 .Wait() 阻塞当前线程
    var result = GetDataAsync().Result;  // 可能死锁!
}

public async Task<string> GetDataAsync()
{
    await Task.Delay(1000);  // 完成后需要回到原同步上下文
    return "data";
}

// ✅ 正确做法:全程异步
public async Task<string> CorrectWay()
{
    return await GetDataAsync();
}

// ✅ 或使用 ConfigureAwait(false)
public async Task<string> GetDataAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);  // 不回到原上下文
    return "data";
}

九、综合案例 ​

案例一:生产者-消费者模式 ​

csharp
using System.Collections.Concurrent;

public class ProducerConsumer<T>
{
    private BlockingCollection<T> queue = new BlockingCollection<T>(boundedCapacity: 10);
    private CancellationTokenSource cts = new CancellationTokenSource();

    public void StartProducer(int itemCount)
    {
        Task.Run(() =>
        {
            for (int i = 0; i < itemCount; i++)
            {
                queue.Add((T)Convert.ChangeType($"消息-{i}", typeof(T)));
                Console.WriteLine($"→ 生产:消息-{i}(队列:{queue.Count})");
                Thread.Sleep(200);  // 模拟生产耗时
            }
            queue.CompleteAdding();  // 标记生产完成
            Console.WriteLine("生产完成");
        });
    }

    public void StartConsumer(int consumerId)
    {
        Task.Run(() =>
        {
            foreach (var item in queue.GetConsumingEnumerable())
            {
                Console.WriteLine($"  ← 消费者{consumerId}处理:{item}");
                Thread.Sleep(500);  // 模拟处理耗时
            }
            Console.WriteLine($"消费者{consumerId}退出");
        });
    }

    public void Stop()
    {
        cts.Cancel();
    }
}

// 使用
var pc = new ProducerConsumer<string>();
pc.StartProducer(8);
pc.StartConsumer(1);
pc.StartConsumer(2);

Console.ReadLine();

案例二:异步下载器 ​

csharp
public class AsyncDownloader
{
    private readonly HttpClient client = new HttpClient();

    public async Task DownloadAsync(string[] urls)
    {
        var tasks = urls.Select(DownloadSingleAsync);
        var results = await Task.WhenAll(tasks);

        foreach (var (url, length) in results)
        {
            Console.WriteLine($"{url}: {length} 字节");
        }
    }

    private async Task<(string url, int length)> DownloadSingleAsync(string url)
    {
        try
        {
            byte[] data = await client.GetByteArrayAsync(url);
            return (url, data.Length);
        }
        catch (Exception ex)
        {
            return (url, -1);
        }
    }
}

案例三:并行数据统计 ​

csharp
public static class ParallelStats
{
    public static (int min, int max, double avg, long sum) Calculate(int[] data)
    {
        object lockObj = new object();
        int min = int.MaxValue, max = int.MinValue;
        long sum = 0;

        Parallel.ForEach(data, value =>
        {
            // 局部变量避免锁竞争
            int localMin = value, localMax = value;

            // Interlocked 或 lock 用于最终合并
            lock (lockObj)
            {
                if (value < min) min = value;
                if (value > max) max = value;
                Interlocked.Add(ref sum, value);
            }
        });

        return (min, max, (double)sum / data.Length, sum);
    }
}

核心知识点总结 ​

异步编程模型选择 ​

场景推荐技术示例
CPU 密集型计算Task.Run + Parallel图像处理、数据分析
IO 密集型操作async/await网络请求、文件读写、数据库
长时间运行Thread(LongRunning)后台服务、监控线程
短时间批量ThreadPool / Task.Run小型任务、并行处理
需要取消CancellationToken超时、用户中断

最佳实践 ​

  1. 全程异步——不要用 .Result 或 .Wait() 阻塞异步方法
  2. ConfigureAwait(false)——库代码中使用,避免死锁
  3. 避免 async void——只在事件处理中使用
  4. 命名约定——异步方法以 Async 结尾
  5. 使用 CancellationToken——支持取消的异步操作更健壮
  6. 避免共享状态——尽量减少可变共享数据
  7. 使用不可变数据结构——天然线程安全
  8. 选择合适的同步机制——lock 够用时不用 Mutex

注意事项 ​

  1. 死锁——避免嵌套锁,统一锁顺序,使用超时机制
  2. 竞态条件——多个线程同时读写共享变量需要同步
  3. 线程安全集合——优先使用 ConcurrentDictionary、BlockingCollection、ConcurrentQueue 等
  4. UI 线程——WPF/WinForms 中更新 UI 需要使用 Dispatcher / Control.BeginInvoke
  5. 异常处理——线程中的未处理异常会导致进程崩溃(.NET Core 5+ 默认行为)
  6. 资源泄漏——使用 using 确保 CancellationTokenSource、SemaphoreSlim 等释放
  7. 不要过度并行——并行度超过 CPU 核心数不一定更快,会有上下文切换开销

Released under the MIT License.