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线程优先级NormalHighLowest
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 异步编程

asyncawait 是 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 应用中使用 Taskasync/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. 线程安全集合——优先使用 ConcurrentDictionaryBlockingCollectionConcurrentQueue
  4. UI 线程——WPF/WinForms 中更新 UI 需要使用 Dispatcher / Control.BeginInvoke
  5. 异常处理——线程中的未处理异常会导致进程崩溃(.NET Core 5+ 默认行为)
  6. 资源泄漏——使用 using 确保 CancellationTokenSourceSemaphoreSlim 等释放
  7. 不要过度并行——并行度超过 CPU 核心数不一定更快,会有上下文切换开销

Released under the MIT License.