“发现问题的能力,运用技术解决问题的能力,是一个技术人成长的关键”
@图片故事:洋姜的花,拍摄于2022年7月23日,地点:北京奥林匹克森林公园 ,摄影师:刘先生
概要:使用C#发起多线程任务十分简单,本文旨在汇总多线程编程的注意事项,重点不在于如何发起多线程,主要内容如下:
控制线程并发数量
界定共享资源
加锁并控制锁范围
子线程异常处理
未完成任务取消
希望对小伙伴儿们有所帮助
01
—
控制线程并发数量
多线程以多任务并行的方式,加快业务处理速度,但如果线程数量超出了系统的承载能力,反倒会造成系统整体性能下降,如何合理地控制线程并发数量,是多线程开发的关键。
推荐采用信号量机制,可以在线程总数未知的情况下,有效地控制并发线程数量,并且以瀑布流的形式,连续执行后续线程,逻辑清晰可控,执行性能高效。
基础代码逻辑如下:
//semaphoreCount是设定的可并行运行的最大线程数量
//taskCount是需要发起的线程的数量
using (Semaphore semaphore = new Semaphore(semaphoreCount, semaphoreCount))
{
var woker = new Worker();
Task[] tasks = new Task[taskCount];
for (int step = 0; step < taskCount; step++)
{
//获取一个信号量,如果所有信号量都已使用,则等待直到一个被释放
semaphore.WaitOne();
//获得信号量之后,才能发起子线程
tasks[step] = Task.Factory.StartNew((data) => { woker.Work(data); }, innerData)
.ContinueWith((task) =>
{
//线程完成,释放信号量
semaphore.Release();
});
}
//...
}
简单来说,是由于分时操作系统,多任务之间存在线程上下文切换,有兴趣的同学可以尝试一下,一次性启动2000个以上线程,查看计算机的资源耗用情况,以便有更真切的体会。
02
—
界定共享资源
线程共享资源,一类是业务本身需要多个子线程共同处理的资源,另一类是从性能角度考虑,需要被多个子线程共享的资源。
以数据查询为例,数据库连接是一种昂贵的资源,如果每个子线程单独创建数据库连接,必然会造成浪费,多个线程共用一个数据库连接是更合理的选择,因此,数据库连接便是共享资源。
有兴趣的同学可以测试一下,同时启动50个以上线程,如果每个线程创建一个数据库连接,会造成数据库短时间内无法创建足够连接而报错。
03
—
加锁并控制锁范围
对共享资源进行访问时,需要加锁保护,防止并发错误。
对于业务本身处理的共享资源,加锁主要是防止数据处理错误;对于集合类型的共享资源,建议首选System.Collections.Concurrent 命名空间下的集合类型,以达到线程安全的目的;对于如数据库连接之类的资源,加锁是为了防止程序异常,如数据库连接、HttpClient对象,在一个请求处理完之前,是不能被其他线程访问的,因此需要加锁,确保串行访问是必须的。
对于锁对象,推荐的写法如下,至于是不是要加static ,要看具体业务场景,静态变量的作用域是整个应用程序,如果有两个以上请求同时到达,那么在访问到加锁代码块时,请求也是串行执行的,普通变量的作用域是当前对象,锁范围也是在当前对象内,请求间相互不影响。
readonly object locker = new object();
04
—
子线程异常处理
概括成一句话是:在明确异常处理要做什么的情况下,才进行异常处理,否则,让异常抛出,交由外层程序处理即可。参考我上一篇文章:异常处理,究竟是处理什么
多线程下异常处理的不同之处在于:子线程内的异常,不会直接抛出到主线程,而是保存在了Task对象的Exception属性中。因此,需要开发小伙伴判断线程状态,进行异常处理。
基础代码逻辑如下:
Task.Factory.StartNew((data) => { woker.Work(data); }, innerData)
.ContinueWith((task) =>
{
//判断线程处理状态,如果执行失败,则抛出异常
if (task.Status == TaskStatus.Faulted)
{
throw task.Exception;
}
});
05
—
未完成任务取消
当某个子线程发生异常之后,取消后续相关线程的执行,符合绝大多数业务逻辑。
取消线程操作需要用到 CancellationTokenSource 类,线程启动时,注册“取消凭证(Token)”,当某个子线程发生异常后,调用CancellationTokenSource的Cancel()方法,通知相关线程取消操作。以后会写一篇CancellationToken的详细介绍。
基础代码逻辑如下:
//声明 CancellationTokenSource
using (CancellationTokenSource cancellation = new CancellationTokenSource())
{
Task[] tasks = new Task[taskCount];
for (int step = 0; step < steps; step++)
{
semaphore.WaitOne();
//注册cancellation.Token
tasks[step] = Task.Factory.StartNew((data) => { woker.Work(data); }, innerData, cancellation.Token)
.ContinueWith((task) =>
{
if (task.Status == TaskStatus.Faulted)
{
//通知取消任务
cancellation.Cancel(true);
throw task.Exception;
}
semaphore.Release();
});
}
}
有多线程开发经历的小伙伴,可以看一下自己的代码,是否有对以上几点的处理。以上内容均来自于我个人的经验总结,如有疏漏,欢迎小伙伴补充指正。
最后,说一下对于多线程的认识,了解二次元的小伙伴应该知道一个词:“结界”,线程与结界有很多相似之处,一个子线程就相当于一个结界,结界内外虽处于同一空间,但却属于不同的世界,结界阻断了结界内外的联系,但又可以相互作用,更多相似处,小伙伴们自己体会。
您的反馈是我坚持的动力,欢迎点赞,转发,关注