.NET 多线程任务的几种实现方式全解析

写软件的时候,要想让程序跑得快、响应及时,多线程是个绕不开的话题。.NET 给我们准备了一整套并发编程的工具,从最底层的 Thread 到高端的 async/await,想用哪种都行。不过多线程这玩意儿虽然好用,但也容易出幺蛾子:竞态条件、死锁、线程安全……一个不小心就踩坑。搞清楚每种并发机制适合干什么、怎么正确等待和同步,是写出靠谱并发程序的基本功。

.NET 多线程编程基础

线程是操作系统能调度的最小单位,一个进程里可以有好几个线程,它们共享进程的资源,但每个有自己的执行栈。在 .NET 里,线程分两种:前台线程会阻止进程退出,后台线程则随着主线程结束自动终止。用多线程的好处很明显:能把 CPU 用得更满、界面不卡、多核不浪费;但麻烦也不少:要操心线程安全、提防死锁、注意上下文切换的开销,调试起来也费劲①。

.NET 的线程模型是分层设计的:

  • Thread 类:直接操作线程,想怎么控制就怎么控制;

  • ThreadPool:线程池,省得老是创建销毁线程;

  • **Task Parallel Library (TPL)**:用任务(Task)来抽象并发操作,是现在的主流;

  • Parallel 类:专门用来并行处理数据,写起来简单;

  • BackgroundWorker:给 WinForms 和 WPF 准备的,做后台任务很方便;

  • async/await:基于状态机的异步编程范式,IO 密集型任务的首选②。

System.Threading.Thread是最原始的线程用法,适合那些需要长时间运行或者有特殊优先级要求的任务。用 Start启动,Join等着它干完,IsBackground可以设成后台线程。好处是啥都能管,缺点是每次创建线程开销不小,而且没有任务组合、异常传播这些高级功能③。

varthread = newThread( =>

{

for(inti = 0; i < 5; i++)

{

Console.WriteLine($"工作线程: {i}");

Thread.Sleep(200);

}

});

thread.IsBackground = true;

thread.Start;

thread.Join; // 等它干完再继续

ThreadPool:轻量级任务

ThreadPool.QueueUserWorkItem把任务丢给线程池,线程池自己会管线程的创建和销毁。特别适合那种短平快的活儿,比如写日志、更新缓存。但别把长时间运行的任务往里扔,不然会占着线程池的资源,而且线程池里的线程不能随便改名字、优先级这些属性④。

ThreadPool.QueueUserWorkItem(_ =>

{

Console.WriteLine("线程池任务执行");

Thread.Sleep(1000);

});

Task Parallel Library (TPL):现在的主流选择

Task代表一个异步操作,用 Task.Run就能快速启动一个任务。它支持 WaitAll/WaitAny来组合等待,可以用 ContinueWith串起后续操作,还能用 CancellationToken取消任务,出错了会给你一个 AggregateException告诉你一堆异常。跟 Thread比起来,Task更轻量,组合起来也更灵活,而且和 async/await配合得天衣无缝,已经是 .NET 并发编程的核心了⑤。

vartask1 = Task.Run( => { /* 干点活 */});

vartask2 = Task.Run( => { /* 干点别的 */});

awaitTask.WhenAll(task1, task2); // 异步等着,不堵线程

Parallel 类:数据并行好帮手

Parallel.ForParallel.ForEach会把循环里的活儿自动分到多个线程去干,内置了负载均衡和工作窃取。特别适合 CPU 密集型计算,比如处理图片、跑数值分析。但灵活度不高,不适合那种需要精细控制的任务流⑥。

Parallel.For(0, 100, i =>

{

ProcessData(i); // 自动分配给多个线程执行

});

BackgroundWorker:UI 场景专用

这个组件专门给 WinForms 和 WPF 设计的,通过 DoWork(后台干活)、ProgressChanged(更新 UI 进度)、RunWorkerCompleted(干完活通知)这些事件,简化了后台操作的写法。它会自动把事件封送到 UI 线程,省得你手动写 Invoke。不过自从 async/await普及以后,用它的越来越少了⑦。

varworker = newBackgroundWorker;

worker.DoWork += (_, e) => { /* 后台任务 */};

worker.RunWorkerCompleted += (_, e) => { /* 更新界面 */};

worker.RunWorkerAsync;

async/await:异步编程的终极范式

async/await让异步代码写起来就跟同步的一样,编译器在背后帮你生成状态机。特别适合 I/O 密集型操作,比如发 HTTP 请求、读写文件,用了它能大幅提升吞吐量。用的时候有几点要注意:用 ConfigureAwait(false)避免不必要的上下文捕获,考虑用 ValueTask减少内存分配,记得加上 CancellationToken支持取消操作⑧。

asyncTask<string> FetchDataAsync()

{

usingvarclient = newHttpClient;

returnawaitclient.GetStringAsync("https://api.example.com/data")

.ConfigureAwait(false);

}

线程等待与同步机制基本等待方式

  • Thread.Join:让当前线程等着,直到目标线程结束;

  • Task.Wait/ Task.WaitAll:同步等着任务完成;

  • await Task.WhenAll:异步等好几个任务,不堵线程⑨。

  • lock(Monitor):最简单的互斥锁,保证同一时间只有一个线程进临界区;

  • SemaphoreSlim:限制同时访问的线程数,支持异步等待;

  • ManualResetEvent:线程之间发信号用;

  • Barrier:让多个线程在某个阶段同步一下;

  • ReaderWriterLockSlim:允许多个线程同时读,但写的时候只能有一个,适合读多写少的场景⑩。

privatereadonlyobject_lock = new;

lock(_lock) { /* 临界区代码 */}

// SemaphoreSlim 的例子

privatereadonlySemaphoreSlim _semaphore = new(3);

await_semaphore.WaitAsync;

try{ /* 最多允许3个线程同时进来 */}

finally{ _semaphore.Release; }

超时处理

所有等待操作最好都设个超时,免得死锁:

  • Thread.Join(TimeSpan)

  • Task.Wait(TimeSpan)

  • await Task.WhenAny(task, Task.Delay(timeout))

  • Monitor.TryEnter(lockObj, TimeSpan)

  • 能不共享就不共享:优先设计成无状态组件;

  • 对象尽量不可变:共享的数据最好只读;

  • 锁的范围要小:只锁必要的代码,别锁一大片;

  • 选对集合:单线程用 List ,多线程用 ConcurrentQueue ConcurrentDictionary 这些线程安全的集合⑪。

死锁就是几个线程互相等对方手里的资源,结果都卡住了。预防方法有:

  • 固定的锁顺序:所有线程拿锁的顺序都一样;

  • 设超时:用 Monitor.TryEnter,等不到就算了;

  • 别嵌套锁:尽量重构代码,减少锁依赖⑫。

  • 减少锁竞争:临界区越小越好,用细粒度锁;

  • 并行度要合理MaxDegreeOfParallelism设成 Environment.ProcessorCount就行;

  • I/O 操作用异步:别让网络、磁盘读写占着线程池;

  • 先测量再优化:拿 Stopwatch或性能分析器跑一跑,别瞎猜⑬。

用生产者-消费者模式:主线程只管往里加日志,后台一个 Task不停地从队列里取出来写文件。用 BlockingCollection保证线程安全,用 CancellationToken实现优雅关闭⑭。

varqueue = newBlockingCollection<string>;

Task.Run(async=>

{

foreach(varmsg inqueue.GetConsumingEnumerable)

awaitFile.AppendAllTextAsync("log.txt", msg);

});

queue.Add("日志消息");

并行数据处理管道

可以混着用:PLINQ 负责加载数据,Task.WhenAll做 CPU 计算,Parallel.ForEach做验证过滤。每个阶段选最合适的并发模型,把资源利用率拉满⑮。

实时仪表板(WPF)

async/await从多个数据源拉数据,用 Dispatcher.InvokeAsync在 UI 线程上更新控件,用 Task.WhenAny配合 Task.Delay实现超时保护,保证界面一直能响应⑯。

结论

.NET 的并发工具链从 Threadasync/await一应俱全,怎么选看任务类型:

  • I/O 密集型async/await

  • CPU 密集型Parallel或 PLINQ;

  • 短期的后台任务Task.RunThreadPool

  • 长时间运行的高优先级任务Thread

不管用哪种模型,正确性永远比性能重要。同步机制要仔细设计,超时和取消必须考虑进去。把这些技术用好、组合好,才能写出既快又稳的并发系统。

并发模型关系图

这张图告诉我们:

  • 不管哪种并发模型,最后都得跑在操作系统线程上;

  • ThreadThreadPool是最底层的;

  • Task是现代并发的核心,Parallel是它的特化版本;

  • BackgroundWorkerasync/await是针对特定场景(UI、I/O)的高层封装;

  • 箭头方向表示依赖和演进关系,越靠右的越推荐在新项目里用。

① Microsoft. Threading in C#. https://learn.microsoft.com/en-us/dotnet/csharp/threading/

② Microsoft. Task Parallel Library (TPL). https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl

③ Microsoft. Thread Class. https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread

④ Microsoft. ThreadPool Class. https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool

⑤ Stephen Toub. The Task-Based Asynchronous Pattern. https://devblogs.microsoft.com/pfxteam/tag/tap/

⑥ Microsoft. Parallel Class. https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel

⑦ Microsoft. BackgroundWorker Component. https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.backgroundworker

⑧ Microsoft. Asynchronous Programming with async and await. https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

⑨ Microsoft. Task.WaitAll Method. https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitall

⑩ Microsoft. Synchronization Primitives. https://learn.microsoft.com/en-us/dotnet/standard/threading/overview-of-synchronization-primitives

⑪ Microsoft. Thread-Safe Collections. https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/

⑫ Joe Duffy. Concurrent Programming on Windows. Addison-Wesley, 2008.

⑬ Ben Watson. Writing High-Performance .NET Code. 2nd ed., 2018.

⑭ Microsoft. BlockingCollectionClass. https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1

⑮ Microsoft. PLINQ. https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/parallel-linq-plinq

课程简介:课程基于最新.NET 10与Microsoft Agent Framework (MAF)打造。覆盖.NET平台Al生态的核心知识点:M.E.Al、M.E.VD、MCP、Agent、A2A、Workflow、RAGAG-UI,全链路覆盖从能力搭建到多智能体编排与协作。返回搜狐,查看更多

阅读 ()
平台声明
该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。