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 对比
| 对比项 | Thread | ThreadPool | Task |
|---|---|---|---|
| 创建速度 | 慢 | 快 | 快 |
| 控制粒度 | 细(状态、优先级) | 粗 | 中等 |
| 返回值 | 不能直接返回 | 不能直接返回 | 可以通过 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 | 超时、用户中断 |
最佳实践
- 全程异步——不要用
.Result或.Wait()阻塞异步方法 - ConfigureAwait(false)——库代码中使用,避免死锁
- 避免 async void——只在事件处理中使用
- 命名约定——异步方法以
Async结尾 - 使用 CancellationToken——支持取消的异步操作更健壮
- 避免共享状态——尽量减少可变共享数据
- 使用不可变数据结构——天然线程安全
- 选择合适的同步机制——
lock够用时不用Mutex
注意事项
- 死锁——避免嵌套锁,统一锁顺序,使用超时机制
- 竞态条件——多个线程同时读写共享变量需要同步
- 线程安全集合——优先使用
ConcurrentDictionary、BlockingCollection、ConcurrentQueue等 - UI 线程——WPF/WinForms 中更新 UI 需要使用
Dispatcher/Control.BeginInvoke - 异常处理——线程中的未处理异常会导致进程崩溃(.NET Core 5+ 默认行为)
- 资源泄漏——使用
using确保CancellationTokenSource、SemaphoreSlim等释放 - 不要过度并行——并行度超过 CPU 核心数不一定更快,会有上下文切换开销


