在C#的开发过程中,多线程是绕不开的一道坎,今天我想从线程开始讲起,希望对初学者能略有所助。
首先讲线程,线程是操作系统执行的最小单位,一个进程可以包含多个线程。这里举个简单例子,如果一个应用打开那就是一个进程,如果这个应用只能显示图片,那显示图片就是一个线程,如果还能同时播放音乐,那就是另一个线程。(执行图片显和播放音乐可能不止需要一个线程的执行,这里仅为快速理解特意举例)。
诶,相信现在你明白为啥需要多线程了。首先就是我需要多线程来帮我同时做多件事情,毕竟电脑的CPU性能有空闲,如果多个任务能够同时跑就最好同时跑,提升效率;其次就是响应性,就如同日常使用电脑软件,你点击播放音乐,然后又去点击另一个页面去添加到播放列表。如果是单线程的话,那么在歌曲没播放完成前你的添加都会一直卡着,直到歌曲放完才能按照顺序去执行添加操作。好了,经过简单介绍我们也大致理解了多线程在日常开发中的作用是什么了,一个是并发性,一个是响应性。接下来我们学习如何使用多线程。
生产者-消费者模式
在正式讲解多线程之前,我们先简单介绍下生产者-消费者模式。这是一种软件设计模式,简单来说就是在软件开发中,加入有一个发送数据的类A,一个接收数据的类B,如果A发送数据过快,B接收不过来,那必然造成阻塞或者数据缺失;反之如果B接收过快,那就需要一直等着A发送,也不合理。所以在中间创建一个数据缓存队列,用来平衡并传输数据,这就是生产者消费者模式。举个简单例子,就像快递公司,站点和顾客一样,如果每个顾客都去公司领快递,一旦公司出货过慢,顾客就得一直等;如果过快,快递堆积,顾客有可能拿错。所以就需要一个站点,一个站点存2000个快递,顾客按号领取,一旦领取一个,快递公司就会收到通知,再补一个快递到达站点。这样就能保证了快递的收发平衡与准确率。这里推荐一篇文章:https://www.cnblogs.com/luego/p/12048857.html (只需要大致理解生产者消费者模式即可)
多线程的应用
好了,已经明了多线程的用途了,我们来看看如何使用?这里有关线程的用法总结推荐: https://www.cnblogs.com/wyt007/p/9486752.html
//这里我们简单搭个案例,在一个窗口上设置两个文本框,两个单独线程分别在对应文本框内循环打印数字
主窗口MainWindow.axmal

主窗口ViewModel MainWindow.axmal.cs代码:
点击查看代码//创建两个线程与线程方法,并在命令方法内执行即可
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MultiThreading.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
//两个独立的线程
public Thread thread1;
public Thread thread2;
//循环显示在文本框中的数字
[ObservableProperty] private string _thread1Txt="线程一";
[ObservableProperty] private string _thread2Txt="线程二";
public void DoSomething()
{
for (int i = 0; i < 20; i++)
{
Thread.Sleep(100);
Thread1Txt = i.ToString();
}
}
public void DoSomething2()
{
for (int i = 0; i < 30; i++)
{
Thread.Sleep(150);
Thread2Txt = i.ToString();
}
}
[RelayCommand]
public void TwoThread()
{
thread1=new Thread(DoSomething);
thread2=new Thread(DoSomething2);
thread1.Start();
thread2.Start();
// 不使用 Join(),避免阻塞 UI 线程
}
}
以上就完成了,这就是多线程的应用,运行效果如下图:

可以看见线程一和线程二是同时执行且独立于UI线程的,也就是说两个文本框运行的同时,UI页面的按钮还可以进行点击,如果添加文本框在其上还可以在线程一二执行时编辑文本框。
线程池的引入
当然以上只是多线程的一个简单应用,随着c#的不断发展,为了便于多线程的使用(创建销毁线程,自行管理其生命流程是一件很麻烦的事,也容易出错),出现了更便于使用线程的方法,线程池就诞生了。接下来我们看看线程池的应用,依旧是上面的例子做扩展,逐步加深理解为什么要用线程池。先看要实现的运行效果如图:

//增加一个文本框执行线程三,真实开发中往往并发线程会更多

//对应的ViewModel业务层代码(增加了线程三及线程三任务)
点击查看代码using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MultiThreading.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
//两个独立的线程
public Thread thread1;
public Thread thread2;
public Thread thread3;
//循环显示在文本框中的数字
[ObservableProperty] private string _thread1Txt="线程一";
[ObservableProperty] private string _thread2Txt="线程二";
[ObservableProperty] private string _thread3Txt = "线程三";
public void DoSomething()
{
for (int i = 0; i < 20; i++)
{
Thread.Sleep(100);
Thread1Txt = i.ToString();
}
}
public void DoSomething2()
{
for (int i = 0; i < 30; i++)
{
Thread.Sleep(150);
Thread2Txt = i.ToString();
}
}
public void DoSomething3()
{
for (int i = 0; i < 100; i++)
{
string work=i.ToString();
Thread.Sleep(500);
Thread3Txt = "线程" + work;
}
}
[RelayCommand]
public void TwoThread()
{
thread1=new Thread(DoSomething);
thread2=new Thread(DoSomething2);
thread3=new Thread(DoSomething3);
thread1.Start();
thread2.Start();
thread3.Start();
// 不使用 Join(),避免阻塞 UI 线程
}
}
从上看出,如果线程增加过多,创建线程,启动线程,销毁线程都是一件很重复性甚至麻烦易出错的事情,所以我们引入了线程池,只修改ViewModel层,应用如下:点击查看代码
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MultiThreading.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty] private string _thread1Txt = "线程一";
[ObservableProperty] private string _thread2Txt = "线程二";
[ObservableProperty] private string _thread3Txt = "线程三";
// 添加取消支持
private CancellationTokenSource _cancellationTokenSource = new();
// 修改方法,添加null检查
public void DoSomething(object? state)
{
// 安全的null检查和处理
if (state is not ManualResetEvent handle)
{
// 如果state不是ManualResetEvent,直接返回或记录日志
return;
}
try
{
for (int i = 0; i < 20; i++)
{
// 检查取消请求
if (_cancellationTokenSource.Token.IsCancellationRequested)
{
Dispatcher.UIThread.InvokeAsync(() => Thread1Txt = "已取消");
return;
}
string work = i.ToString();
Thread.Sleep(100);
// UI更新使用Dispatcher
Dispatcher.UIThread.InvokeAsync(() => Thread1Txt = work);
}
}
finally
{
handle.Set(); // 确保总是会调用Set()
}
}
public void DoSomething2(object? state)
{
if (state is not ManualResetEvent handle) return;
try
{
for (int i = 0; i < 30; i++)
{
if (_cancellationTokenSource.Token.IsCancellationRequested)
{
Dispatcher.UIThread.InvokeAsync(() => Thread2Txt = "已取消");
return;
}
string work = i.ToString();
Thread.Sleep(150);
Dispatcher.UIThread.InvokeAsync(() => Thread2Txt = work);
}
}
finally
{
handle.Set();
}
}
public void DoSomething3(object? state)
{
if (state is not ManualResetEvent handle) return;
try
{
for (int i = 0; i < 100; i++)
{
if (_cancellationTokenSource.Token.IsCancellationRequested)
{
Dispatcher.UIThread.InvokeAsync(() => Thread3Txt = "已取消");
return;
}
string work = i.ToString();
Thread.Sleep(500);
Dispatcher.UIThread.InvokeAsync(() => Thread3Txt = "线程" + work);
}
}
finally
{
handle.Set();
}
}
[RelayCommand]
public async Task ThreadPoolEx()
{
// 重置取消令牌
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
// 创建同步事件
ManualResetEvent[] waitHandles = new ManualResetEvent[3];
for (int i = 0; i < 3; i++)
{
waitHandles[i] = new ManualResetEvent(false);
}
// 提交任务到线程池
ThreadPool.QueueUserWorkItem(DoSomething, waitHandles[0]);
ThreadPool.QueueUserWorkItem(DoSomething2, waitHandles[1]);
ThreadPool.QueueUserWorkItem(DoSomething3, waitHandles[2]);
try
{
// 异步等待所有任务完成(不阻塞UI线程)
await Task.Run(() =>
{
WaitHandle.WaitAll(waitHandles);
}, _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
// 任务被取消
Thread1Txt = "线程一:已取消";
Thread2Txt = "线程二:已取消";
Thread3Txt = "线程三:已取消";
}
finally
{
// 清理资源
foreach (var handle in waitHandles)
{
handle.Dispose();
}
}
}
}
从以上实现可以看出,无论对于线程创建,还是线程的同步实现等,ThreadPool都有更简洁的实现方式。但是请注意,线程池的出现只是为了简化实现多且小的异步任务(毕竟为每个任务创建线程和管理线程实在十分吃力且占用内存),线程池也有劣势,譬如它只是个可复用线程队列(其实细细体会,它就是一种线程的生产者消费者模式的体现),无法对内部单个线程进行严格管理,Thread就不一样,可以设置优先级,生命流程也可以覆盖整个进程,更加自由。
Task,async和await的发展
不过以上ThreadPool的应用随着发展,佬们还是觉得很麻烦,所以更优的异步实现——增强版线程池它来了————Task&async,await相结合。直接看基于以上ThreadPool的Task&async/await实现例码:
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MultiThreading.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty] private string _thread1Txt = "线程一";
[ObservableProperty] private string _thread2Txt = "线程二";
[ObservableProperty] private string _thread3Txt = "线程三";
public void DoSomething()
{
for (int i = 0; i < 20; i++)
{
string work = i.ToString();
Thread.Sleep(100);
// UI更新使用Dispatcher
Dispatcher.UIThread.InvokeAsync(() => Thread1Txt = work);
}
}
public void DoSomething2()
{
for (int i = 0; i < 30; i++)
{
string work = i.ToString();
Thread.Sleep(150);
// UI更新使用Dispatcher
Dispatcher.UIThread.InvokeAsync(() => Thread2Txt = work);
}
}
public void DoSomething3()
{
for (int i = 0; i < 100; i++)
{
string work = i.ToString();
Thread.Sleep(500);
Dispatcher.UIThread.InvokeAsync(() => Thread3Txt = "线程" + work);
}
}
[RelayCommand]
public async Task ThreadPoolEx()
{
try
{
Task task1 = Task.Run(DoSomething);
Task task2 = Task.Run(DoSomething2);
Task task3 = Task.Run(DoSomething3);
await Task.WhenAll(task1, task2, task3);
}
catch (AggregateException ae)
{
//统一处理所有异常
foreach (var e in ae.Flatten().InnerExceptions)
Console.WriteLine(e.Message);
}
}
}
由此可以看出,用Task&async/await实现更为优雅,同时也让代码可读性更强。这种异步实现是当前首选的现代方式。其本质还是与Thread是一样的,都是为了实现异步,但是减少了许多重复代码,可读性也更高,所以,其实多线程并没有多复杂,从Thread到ThreadPool再到Task,无非就是使得实现方式更便捷,根本的目的都是一样为实现并行任务。但是关于线程之间的死锁以及资源冲突等相关问题,都是我们在实际开发中需要避免,至于这一部分,我推荐一篇文章:https://www.cnblogs.com/willick/p/13967346.html ,希望能帮助您解决不惑。
浙公网安备 33010602011771号