一、并发编程基础
并发编程是指让程序可以同时执行多个任务,提高资源利用率和程序响应速度。在 C# 中,常见的并发编程技术包括:
- 线程(Thread)
- 线程池(ThreadPool)
- 任务(Task)
- 异步(async/await)
- 并行(Parallel 类)
- 锁和同步机制
二、线程(Thread)
最基础的并发单位,直接操作系统线程。
using System.Threading;
Thread t = new Thread(() =>
{
Console.WriteLine("线程工作中...");
});
t.Start();
**注意:**直接使用 Thread 灵活但开销大,不推荐频繁创建销毁。
三、线程池(ThreadPool)
.NET 提供了线程池,复用线程,适合短小任务。
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine("线程池任务");
});
四、任务(Task)
Task 是 .NET 并发编程的主流方式,支持任务调度、取消、异常处理等。
using System.Threading.Tasks;
Task.Run(() =>
{
Console.WriteLine("通过 Task 执行任务");
});
等待任务完成
Task t = Task.Run(() => { /* ... */ });
t.Wait(); // 阻塞等待
获取返回值
Task<int> t = Task.Run(() => 42);
int result = t.Result;
五、异步编程(async/await)
让异步代码像同步一样易于编写和理解。
public async Task<int> GetDataAsync()
{
await Task.Delay(1000); // 模拟异步操作
return 123;
}
int data = await GetDataAsync();
优点:不会阻塞主线程,适合 IO 密集型场景。
六、并行编程(Parallel 类)
用于数据或任务的并行处理。
并行循环
using System.Threading.Tasks;
Parallel.For(0, 10, i =>
{
Console.WriteLine($"并行处理:{i}");
});
并行集合处理
string[] arr = { "A", "B", "C" };
Parallel.ForEach(arr, item =>
{
Console.WriteLine(item);
});
七、同步与锁
多线程访问共享资源时需要同步,避免数据竞争。
lock 关键字
private object _lockObj = new object();
void SafeMethod()
{
lock (_lockObj)
{
// 线程安全代码块
}
}
其他同步机制
MonitorMutexSemaphoreReaderWriterLockSlim
八、线程安全集合
.NET 提供了线程安全的集合类,如:
ConcurrentDictionary<TKey, TValue>ConcurrentQueue<T>ConcurrentBag<T>BlockingCollection<T>
var dict = new ConcurrentDictionary<int, string>();
dict.TryAdd(1, "A");
九、取消任务(CancellationToken)
可以优雅地取消正在执行的任务。
var cts = new CancellationTokenSource();
Task.Run(() =>
{
while (!cts.Token.IsCancellationRequested)
{
// 执行任务
}
}, cts.Token);
cts.Cancel(); // 请求取消
十、死锁与竞态条件
死锁:多个线程互相等待对方释放资源,导致程序卡死。
竞态条件:多个线程同时修改数据,导致结果不可预测。
避免方法:
- 只锁必要代码
- 保持锁顺序一致
- 使用高层并发原语(如线程安全集合)
十一、异步与并发的区别
- 异步:强调非阻塞,通常用于 IO 操作。
- 并发:强调同时运行,通常用于多核计算。
十二、并发编程最佳实践
- 优先使用高层 API(如 Task、Parallel、线程安全集合)
- 保持代码简单,避免复杂锁逻辑
- 只在必要时用低层 API(Thread、Monitor)
- 注意异常处理和任务取消
- 合理拆分任务,避免过度并发
十三、实际应用场景举例
- Web 服务器异步处理请求
- 批量数据并行处理
- 多线程爬虫/下载器
- 多任务图像处理
- 实时监控/数据采集
十四、综合示例
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 并行计算
Parallel.For(0, 5, i =>
{
Console.WriteLine($"并行任务 {i} 由线程 {Thread.CurrentThread.ManagedThreadId} 执行");
});
// 异步任务
int result = await GetNumberAsync();
Console.WriteLine($"异步结果:{result}");
// 线程安全集合
var bag = new System.Collections.Concurrent.ConcurrentBag<int>();
Parallel.For(0, 100, bag.Add);
Console.WriteLine($"集合元素数量:{bag.Count}");
}
static async Task<int> GetNumberAsync()
{
await Task.Delay(500);
return 42;
}
}
15. 并发编程中的常见问题与解决方法
1. 死锁(Deadlock)
死锁发生在两个或多个线程相互等待对方释放资源,永远无法继续执行。
避免方法:
- 保证所有线程获取锁的顺序一致。
- 尽量减少锁的粒度,锁定最小必要范围。
- 使用超时锁
Monitor.TryEnter或Mutex.WaitOne(timeout)。
示例:
object lockA = new object();
object lockB = new object();
void Thread1()
{
lock(lockA)
{
Thread.Sleep(100); // 模拟业务处理
lock(lockB)
{
// ...
}
}
}
void Thread2()
{
lock(lockB)
{
Thread.Sleep(100);
lock(lockA)
{
// ...
}
}
}
上例可能导致死锁。
2. 竞态条件(Race Condition)
多个线程并发访问和修改同一资源,导致结果不可预测。
解决方法:
- 使用
lock或线程安全集合。 - 尽量减少共享资源。
- 原子操作(如
Interlocked类)。
示例:
int count = 0;
void Increase()
{
Interlocked.Increment(ref count);
}
16. 线程同步机制补充
1. Monitor
比 lock 更灵活,可以实现等待/通知机制。
Monitor.Enter(lockObj);
try
{
// ...
}
finally
{
Monitor.Exit(lockObj);
}
2. Mutex
用于跨进程同步,开销较大。
Mutex mutex = new Mutex();
mutex.WaitOne();
try
{
// ...
}
finally
{
mutex.ReleaseMutex();
}
3. Semaphore
限制同时访问资源的线程数量。
Semaphore sem = new Semaphore(2, 2); // 最多2个线程同时进入
sem.WaitOne();
try
{
// ...
}
finally
{
sem.Release();
}
4. ReaderWriterLockSlim
适合读多写少场景,允许多个线程并发读取,但写入时独占。
ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
void Read()
{
rwLock.EnterReadLock();
try { /* 读操作 */ }
finally { rwLock.ExitReadLock(); }
}
void Write()
{
rwLock.EnterWriteLock();
try { /* 写操作 */ }
finally { rwLock.ExitWriteLock(); }
}
17. 并发与异步的结合
异步方法本身不一定并发,但可以结合并发技术提升性能。
示例:批量异步处理
async Task ProcessBatchAsync(IEnumerable<string> urls)
{
var tasks = urls.Select(url => DownloadAsync(url));
await Task.WhenAll(tasks);
}
async Task<string> DownloadAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
18. 并发限流(限制并发量)
通过 SemaphoreSlim 控制最大并发数,防止资源耗尽。
SemaphoreSlim semaphore = new SemaphoreSlim(5); // 最多5个并发
async Task DownloadAsync(string url)
{
await semaphore.WaitAsync();
try
{
// 下载逻辑
}
finally
{
semaphore.Release();
}
}
19. 线程安全单例模式
单例模式在并发场景下要保证线程安全。
双重检查锁定:
class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new Singleton();
}
}
return _instance;
}
}
}
20. 并发编程的调试与分析
- 使用 Visual Studio 的“并行堆栈”、“线程窗口”调试线程状态。
- 利用性能分析工具(如 PerfView、dotTrace)分析并发瓶颈。
- 使用日志和事件追踪定位并发问题。
21. 生产环境中的并发建议
- 优先用高层 API(Task、Parallel、线程安全集合)。
- 控制并发量,防止资源耗尽。
- 关注异常处理和任务取消,避免僵尸线程或死锁。
- 定期回顾并发代码,防止隐患。
22. 综合并发示例:并发下载器
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Downloader
{
private SemaphoreSlim _semaphore = new SemaphoreSlim(3); // 最多3个并发
public async Task DownloadAllAsync(IEnumerable<string> urls)
{
var tasks = new List<Task>();
foreach (var url in urls)
{
tasks.Add(DownloadAsync(url));
}
await Task.WhenAll(tasks);
}
private async Task DownloadAsync(string url)
{
await _semaphore.WaitAsync();
try
{
using var client = new HttpClient();
var content = await client.GetStringAsync(url);
Console.WriteLine($"下载完成: {url}, 长度: {content.Length}");
}
catch (Exception ex)
{
Console.WriteLine($"下载失败: {url}, 错误: {ex.Message}");
}
finally
{
_semaphore.Release();
}
}
}
23. 并发数据结构与集合
.NET 提供了一系列线程安全的数据结构,适合高并发场景:
- ConcurrentQueue:线程安全队列,适合生产者-消费者模型。
- ConcurrentStack:线程安全栈。
- ConcurrentBag:适合多线程收集数据。
- ConcurrentDictionary<TKey, TValue>:线程安全字典,支持高效并发读写。
- BlockingCollection:包装了线程安全集合,支持阻塞和限流。
示例:生产者-消费者模型
using System.Collections.Concurrent;
using System.Threading.Tasks;
ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
// 生产者
Task.Run(() => {
for (int i = 0; i < 100; i++)
{
queue.Enqueue(i);
}
});
// 消费者
Task.Run(() => {
int item;
while (true)
{
if (queue.TryDequeue(out item))
{
Console.WriteLine($"消费:{item}");
}
}
});
24. 并发中的异常处理
并发任务出现异常时,要统一处理,避免异常丢失。
Task 异常处理
var task = Task.Run(() => throw new Exception("出错了!"));
try
{
task.Wait();
}
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions)
Console.WriteLine(inner.Message);
}
异步方法异常处理
try
{
await SomeAsyncMethod();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
25. 并发中的任务取消与超时
利用 CancellationTokenSource 实现任务取消和超时控制。
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); // 3秒超时
try
{
await Task.Run(() =>
{
while (!cts.Token.IsCancellationRequested)
{
// 模拟长时间任务
}
}, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("任务已取消或超时");
}
26. 线程局部存储(ThreadLocal)
有些数据只需在当前线程内维护,使用 ThreadLocal<T>。
ThreadLocal<int> threadId = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId);
Task.Run(() => Console.WriteLine(threadId.Value));
Task.Run(() => Console.WriteLine(threadId.Value));
27. 并发编程中的内存可见性(volatile)
volatile 关键字保证变量的修改对所有线程立即可见,适合简单标志。
volatile bool _running = true;
void Worker()
{
while (_running)
{
// ...
}
}
28. 并发与异步的性能对比与选择
- CPU密集型任务:推荐用
Parallel.For、Task.Run,充分利用多核。 - IO密集型任务:推荐用
async/await,减少线程阻塞。 - 高并发数据处理:用线程安全集合,减少锁冲突。
29. 并发编程的常见设计模式
- 生产者-消费者:BlockingCollection、ConcurrentQueue
- 线程池模式:Task、ThreadPool
- 异步回调模式:Task.ContinueWith、事件
- 限流模式:SemaphoreSlim
30. 并发编程的测试与调试
- 使用
Thread.Sleep、ManualResetEvent等模拟并发环境。 - 利用 Visual Studio 的并行调试工具分析线程状态。
- 利用
ConcurrentQueue、日志等记录并发行为,便于问题定位。
31. 并发编程的常见面试题
- 如何实现线程安全的单例?
- 说说 lock 和 Monitor 的区别。
- 如何避免死锁?
- 如何让多个任务并发执行,并处理所有结果?
- 并发队列和普通队列的区别?
- async/await 的底层原理?
32. 真实项目中的并发优化建议
- 业务数据分片,减少共享资源。
- 用线程安全集合替代锁。
- 控制并发量,防止资源耗尽(如数据库连接池、限流)。
- 合理拆分任务,避免“线程爆炸”。
- 关注内存泄漏和资源释放。
- 定期回顾并发代码,利用代码审查发现隐患。
33. 综合并发实战案例:高并发爬虫
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Threading.Tasks;
class Spider
{
private SemaphoreSlim _semaphore = new SemaphoreSlim(10); // 最大并发10
private ConcurrentBag<string> _results = new ConcurrentBag<string>();
public async Task CrawlAsync(string[] urls)
{
var tasks = new Task[urls.Length];
for (int i = 0; i < urls.Length; i++)
{
tasks[i] = CrawlOneAsync(urls[i]);
}
await Task.WhenAll(tasks);
Console.WriteLine($"抓取完成,结果数:{_results.Count}");
}
private async Task CrawlOneAsync(string url)
{
await _semaphore.WaitAsync();
try
{
using var client = new HttpClient();
var html = await client.GetStringAsync(url);
_results.Add(html);
Console.WriteLine($"抓取:{url},长度:{html.Length}");
}
catch (Exception ex)
{
Console.WriteLine($"抓取失败:{url},错误:{ex.Message}");
}
finally
{
_semaphore.Release();
}
}
}

1451

被折叠的 条评论
为什么被折叠?



