C#多线程总结
目录
6、Task中专门的异常处理AggregateException
?
前言
Q1:为什么要使用线程?
在多CPU和多核时代,使用线程能够充分利用硬件资源,提升软件的运行效率。但是没有章法的乱用线程会适得其反。
Q2:线程和进程关系?
一个程序运行,通常在任务管理器中看到一个进程。这个进程占用多少资源,并不是由进程本身决定。而是由这个进程分配 的线程决定。也就是说操作系统是通过线程来管理程序资源的。
一、异步线程
????????异步线程是一种用于处理耗时操作的机制,它允许应用程序在执行某些操作时不被阻塞,以提高性能和响应性.
使用async和await关键字
using System;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
Console.WriteLine("开始执行异步操作");
await ExecuteAsyncOperation(); // 调用异步方法,并在此等待其完成
Console.WriteLine("异步操作完成");
}
public static async Task ExecuteAsyncOperation()
{
Console.WriteLine("开始执行异步操作");
// 模拟一个耗时的操作,这里使用 Task.Delay 方法来模拟
// 这个操作会暂停方法的执行,但不会阻塞主线程
await Task.Delay(1000); // 暂停 1 秒钟
Console.WriteLine("异步操作完成");
}
}
基于委托实现
调用BeginInvoke()方法
利用返回结果状态做进度条
异步等待WaitOne
异步返回值EndInvoke
二、同步线程
????????同步线程是指线程的执行是按照顺序的,每个操作都必须等待前一个操作完成后才能继续执行,这种线程模型被称为同步。
三、Thread线程
开启线程
设置线程优先级
thread.Priority=ThreadPriority.Highest;//设置高优先级,只是一个概率的提升
?Thread拓展封装
场景一:开启新线程执行任务后紧接着需要执行的一个任务
场景二:开启一个线程,既要不卡界面,又要返回结果
Func<int> func=()=>
{
Thread.Sleep(2000);
return 6;
};
Fun<int> funcResult=ThreadWithReturn(func);
int iResult=funcResult.Invoke();//如果需要执行结果,等待是必须的
四、ThreadPool线程池
线程池是一个用于管理和重用线程的机制,它提供了一种轻量级的方式来处理并发执行的任务。线程池在应用程序启动时会创建一组预分配的线程,这些线程被放置在线程池中,并准备好处理工作项。当应用程序需要执行某个任务时,可以将任务提交给线程池,并由线程池中的线程来执行任务。使用线程池的好处之一是可以减少线程创建和销毁的开销,因为线程池会重用已创建的线程,从而提高性能和资源利用率。
注:线程池是一个共享的资源,因此需要注意使用线程池的任务应尽量是短期的,以避免阻塞线程池中的其他任务。如果需要执行长时间运行的任务,可以考虑使用Async/Await或其他异步编程模型。
常规使用
1)ThreadPool.QueueUserWorkItem
运行结果:方法1、2中的任务是由线程池自动调度的
2)一些使用示例
设置线程数
线程等待
Thread和ThreadPool比较
【1】Thread:只要我们需要异步任务,都会开启一个Thread,比然有时间和空间开销。
【2】TheadPool:是一个线程池,由系统维护和缓存,因为默认都是初始化好的,所以使用方便,退回容易。用的时候,只需要请求即可。
Thread开启10个线程
ThreadPool开启10个线程
总结:
【1】我们的线程池可以根据电脑的实际CPU情况开启一定个数的线程,放到线程池中。
【2】10个异步任务,但是我们通过4个线程就完成了,节省时间和空间开销。
通过线程池做一些扩展(定时器类)
五、Task线程(推荐使用)
????????Task是一种用于多线程编程的高级概念。Task可以把工作项分配给线程执行,使得你可以轻松地编写异步代码。
????????在C# 5.0以前,编写异步代码需要使用回调函数或Thread类等底层API,这使得代码复杂难以维护。而在C# 5.0引入async/await语法之后,使用Task来编写异步代码变得更加简单和直观,使用Task来编写异步代码的好处之一是可以避免使用回调函数,使得代码更加简洁和易于维护。同时,Task还提供了其他高级的功能,如取消异步操作和处理多个异步操作的结果等。
?1、常规使用
new一个新的Task
例子
运行结果
Task.Run
Task.Factory
例子
RunSynchronously
通常用于同步地执行一个异步操作。这个方法通常是在异步操作的Task对象上调用的,以便立即在当前线程上执行异步操作并等待其完成。
Parallel
Parallel类提供了一种简单而有效的方式来并行执行任务,以充分利用多核处理器的能力。Parallel类通常用于对集合进行并行操作,如并行循环、并行LINQ查询等。
注意:Parallel类会自动控制线程的创建和任务的分配,以充分利用可用的处理器核心。它提供了一种方便的方式来实现并行计算,避免了手动管理线程和同步的复杂性。
使用Parallel类可以使得在多核处理器上执行任务变得更加简单和高效,但需要注意避免在并行任务中使用共享的可变状态,以避免数据竞争和其它并发问题。
使用Parallel.Invoke
使用 Parallel.For 并行处理循环
使用 Parallel.ForEach 并行处理数组中的元素
Parallel.ForEach(data, (item) =>
{
Console.WriteLine("处理元素 " + item);
// 在这里可以执行一些耗时的操作
});
2、Task中6种阻塞方式和任务延续
Join阻塞
?Delay延迟
创建一个在指定时间间隔后完成的异步操作的静态方法
Wait等待
通过调用 task.Wait(),程序会等待任务完成。在任务完成后,才会继续执行下面的代码
????????task.Wait() 和 task.Join() 都用于等待异步任务的完成。它们的主要区别在于,Wait() 是 Task 类的成员方法,而 Join() 是 Thread 类的成员方法。
????????task.Wait() 会阻塞当前线程,直到任务完成。如果任务异常退出,Wait() 方法会抛出相应的异常,可以使用 try-catch 块来处理。
????????task.Join() 会阻塞当前线程,直到对应线程执行完成。如果对应的线程异常退出,Join() 方法不会抛出异常,需要通过其他机制来处理异常。
????????与 Wait() 不同,Join() 方法是用在多线程编程中的,因为 Join() 方法只能等待线程完成,而不能等待任务完成。因此,如果使用 Join() 方法等待异步任务的完成,必须先使用 Task.Run() 方法将异步任务转换为线程,然后才能使用 Join() 方法。
WaitAll等待所有
Task.WaitAll(task1,task2)
Task.WaitAll(taskList.ToArray());
WaitAny等待任一
Task.WaitAny(task1,task2)
Task.WaitAny(taskList.ToArray());
ContinueWhenAll
主线程不等待,子线程依次进行
不卡UI界面
WhenAll和ContinueWith结合使用
ContinueWhenAny
一堆子线程中,执行某一任务后,去执行另外一个动作
WhenAny和ContinueWith结合使用
3、TaskCreationOptions枚举
父子任务
长时间运行的任务处理
4、线程取消
抛出异常
线程无法从外面取消的,他只能自己取消自己(其实就是抛出一个异常)。
CacellationTokenSoure
Task中的取消功能:使用的是CacellationTokenSoure解决多任务中协作取消、任务清理、和超时取消方法。
取消当前线程
Token注入到线程中,可取消当前线程后的未启动的所有线程
注:若在当前线程中间调用Cancel(),当前线程会执行完毕,后续线程终止
Task任务取消时,额外工作处理
//Task任务延时自动取消:特别适合于一定时间内容取消任务
//比如我们请求一个任务(可以是远程的接口,或者是某些数据的接收)在一定的时间内,没有返回数据,或者数据没有接收完毕
//这时候可以自己设置延时时间,超时自动取消。
5、Task中的返回值获取
单任务返回结果
关联任务返回结果
多任务集中返回结果
6、Task中专门的异常处理AggregateException
7、多线程情况下的异常捕获
使用Task.WaitAll
六、线程的生命周期
Start(开始)
线程开始
Suspend(挂起)
挂起(suspend),就是我们说的暂停。挂起是用户主动发起的行为,所以,可以恢复。线程被挂起的时候,CPU资源部不被释放。如果当前执行的任务优先级高,其他任务靠边站。挂起一般是程序调试中,为了观察某些数据,而使用,方便调试。
Interrupt(中断)
中断(Interrupt), 通过调用Thread实例的Interrupt方法,可以向线程发送一个中断信号,从而引发ThreadInterruptedException,以便中断线程的执行。需要注意的是,中断并不会终止线程的执行,而是将中断标志位置为true,并在适当的时机抛出ThreadInterruptedException。在工作线程中需要使用try...catch块来捕获中断异常,并在捕获到中断信号时执行清理工作并中止线程。
Sleep(休眠)和Wait(等待)
Sleep()和Wait()都可以让程序等待多少毫秒。Sleep()方法没有是放锁。线程调用的时候,CPU资源一直占有。所以称为“占着CPU睡觉”。Wait()方法释放锁。其他线程可以使用资源。
Sleep(2000)表示:占用CPU,程序休眠2秒。
Wait(2000)表示:不占用CPU,程序等待2秒。
Resume(恢复)
恢复(Resume), 不再推荐使用Resume方法来恢复已暂停的线程。在早期版本的.NET Framework中,Thread类提供了Resume方法来恢复线程的执行,但是在现代的.NET版本中,Resume方法已经过时,并且不推荐使用它。在替代方案中,可以使用Monitor类或ManualResetEvent来控制线程的暂停和恢复。
Abort(取消)
取消(Abort), 可以使用Abort()方法来取消(终止)一个线程的执行。但是,Abort()方法并不是一种建议的线程取消方式,因为它可能导致一些不可预测的结果和资源泄漏。当调用Abort()方法时,线程会立即引发ThreadAbortException,这个异常可以在线程的代码中捕获和处理。如果该异常未被捕获,线程会立即终止并且不会执行任何清理工作。这可能会导致资源未正确释放,例如未关闭的文件或未释放的锁等。此外,取消线程的过程中,线程的堆栈可能被破坏,导致应用程序变得不稳定。通常建议使用一种更安全和优雅的方式来取消线程的执行,例如使用CancellationToenSource类来实现取消操作。
Join(阻塞)
阻塞(Join), 可以使用Join()方法来等待一个线程执行完成。Join()方法将阻塞调用线程,直到目标线程完成执行并终止。如果目标线程已经完成执行了,那么Join()方法会立即返回。Join()方法是一种比较简单直接的方式来等待线程执行完成,但需要注意的是,在执行Join()方法之前,必须先调用Start()方法来启动线程。
thread.Join(2000)//阻塞2s
thread.Join()//阻塞直到任务完成
ResetAbort(再次启用)
把终结的线程再次启用
示例:
多线程的优点:
【1】计算机中一般会运行很多程序,会有很多对应的进程,进程的数量都会超过CPU的个数,如果所有的任务都通过进程来切换,会非常的耗时,将每个进程,分成若干线程,通过线程切换代价更小。
【2】提升CPU的利用率。比如某些线程在等待资源的时候(等待输入、监听数据等)多线程允许其他线程继续执行,从而避免整个进程被阻塞。
从而提高了CPU的利用率。这就解释了,为什么有时候CPU的使用率很低,但是发现内存占用还很高的原因。
【3】在多CPU和多核情况下,真正实现并行运行。
多线程的缺点:
【1】线程越多,上下文切换的开销也越大。CPU的效率就会降低。
【2】线程间的同步容易出错,且不易调试。
【3】多线程是需要代价的。
七、应用程序域
定义:一个应用程序对应一个进程,每个进程会映射对应的物理内存,从而隔离程序。
特殊情况:在一个进程中,我们通常会调用另一个应用程序,比如在VS中,对应devenv进程,创建一个记事本进程。如果单独开一个进程,性能开销是比较大的。为了解决这个问题,.NET中引入应用程序域(AppDomain),并且将它设置在进程和线程之间。每个进程至少包括一个应用程序域,在托管代码运行时,CLR还会额外的创建《系统域》和《共享域》,存放应用程序需要的资源这样的话,就能够减少进程的总数,提高系统性能,减轻进度调度的压力。应用程序域可以看成是程序集的“容器”。应用程序域可以被主动创建,也可以被卸载。并且很快被GC回收。
加载应用程序域前后,进程数没有改变
八、WinDbg的使用
WinDbg的使用:底层内存查看工具,通过它可以观察CLR这一级别的信息,底层看的更清楚。启动后,首先要载入.NET调试扩展包SOS? ,是.NET框架的一部分,无需下来,在操作系统对应目录中。
注意:在使用老版本的时候,需要手动添加符号文件,并且注意你项目的编译位数一定要和windbg的位数一致。
常用的命令:
【1】.loadby sos clr???? 加载sos.dll (以便支持更多的命令)
【2】!clrstack????????? 打印当前线程调用的Stacktrace
【3】!dumpheap?????? 打印堆中所有对象的地址,大小和方法表
PS:sos所有的命令都以“!”开拓,通过!help查看所有的命令。
九、多线程原理研究
目的:增加对线程空间和时间上的开销理解,才能更加合理的使用线程。
内容:
(1)线程Thread在内存空间上和时间的消耗研究
(2)线程Thread相关的实例方法
(3)线程Thread相关的静态方法
线程在内存空间上的开销
【1】Thread内核数据结果:主要有osid(线程的ID)和Context(存放CPU寄存器相关的变量)
寄存器的状态会被随时的保存到Contex中,以便下次使用,多线程时间片切换中是有帮助。
时间片切换理解:计算机基于时间片把当前线程中的资源放到CPU的Context中,然后线程休眠,开始其他线程的调度。
一个时间片应该大约30ms左右(xp和vista时代)
【2】Thead环境块(Thread Environment Block,TEB)
包括:thread本地存储,exceptionList信息等。
0:007> .loadby sos clr
0:007> !threads
ThreadCount:????? 3
UnstartedThread:? 0
BackgroundThread: 1
PendingThread:??? 0
DeadThread:?????? 1
Hosted Runtime:?? no
??????????????????????????????????????????????????????????????????????????????????????????????????????? Lock?
?????? ID OSID ThreadOBJ?????????? State GC Mode???? GC Alloc Context????????????????? Domain?????????? Count Apt Exception
?? 0??? 1 294c0 0000018436123130??? 2a020 Preemptive? 0000018437BC7E38:0000018437BC7FD0 00000184360fa5c0 1???? MTA
?? 6??? 2 8454 000001843614d520??? 2b220 Preemptive? 0000000000000000:0000000000000000 00000184360fa5c0 0???? MTA (Finalizer)
XXXX??? 3??? 0 000001843617cf20??? 39820 Preemptive? 0000000000000000:0000000000000000 00000184360fa5c0 0???? Ukn
第1个线程是主线程
第2个线程是终结器(也就是GC用来回收资源的)
第3个线程是我们启动的。
以上内容都会占用资源,所以我们需要研究。
【3】用户堆栈模式:用户程序的“局部变量”和“参数传递”所使用的堆栈。
经常见到StackOverFlowException,内存溢出的基本原因其实就是《堆栈溢出》
默认情况:windows会分配1M的空间用于用户模式堆栈(换句话说,一个线程分配1M堆栈空间,主要用途参数,局部变量)
【4】内核堆栈模式:就是我们的程序要访问操作系统使用的堆栈。
理解:在CLR的线程操作的时候,通常最后会调用底层win32函数,用户模式中的参数需要传递到内核模式中。
线程在时间上的开销
【1】资源使用通知的开销:我们运行一个程序,通常会加载很多的托管的,和非托管的dll,exe,资源,元数据... 我们通过windbg可以观察到。
? 使用windbg加载一个进程的时候,会有很多ModLoad列表,这些就是加载的模块...请在windbg上看那个进程列表...
? 这些dll有的是托管的,也有的是非托管的。
? 思考:程序运行的时候,如何查看应用程序域?
? 《常用命令3》!DumpDomain?? 输入后看到3个应用程序域,这个是进程启动的时候,默认的。
(1)System Domain: 系统程序域(由CLR创建的)
(2)Shared Domain: 共享程序域,加载了一个叫做mscorlib.dll模块,这个是系统模块,很多程序需要(如果感兴趣,可以自己研究)
???? 比如我们程序中用的那些int、long等都会放到这里面。
(3)Domain 1:加载了当前exe文件,还有mscorlib.dll模块,也就是我们运行的这个程序最终在这个里面。这个可以理解成私有的程序域。
?开启一个Thread,销毁一个Thread,都会通知进程中的相关dll。通过相关的attach、detach等标志位。目的就是为了给线程做资源清理。
【2】时间片切换开销:请大家自己打开任务管理器,看看CPU个数。? 通过观察,当前我的计算机是4核四线程,如果超过4个线程,比如5个,必然会有一个thread休眠30ms,也就是时间片切换,来实现调度。
以上是我们开启一个线程,所带来的开销,请大家权衡后,适当的开启我们需要的线程,不要任意开启,否则CPU也难以承受。
? 我们看到的CPU中线程很多。但是本质上会有很多线程都是休眠的。CPU使用很低。
十、多线程资源竞用问题
1. 使用《 Thread.AllocateDataSlot()未命名的数据槽位》和《Thread.AllocateNamedDataSlot? 命名的数据槽位》解决资源竞用。
使用命名槽位
使用未命名槽位
2.在主线程中使用 ThreadStatic特性标注在变量上面,则只有主线程有权访问该变量
3. ThreadLocal线程的本地存储(TLS: thread local storage),解决资源竞用问题
十一、Debug和Release区别
在实际项目中,我们一般都用Release版本,而不是Debug发布。因为Release中做了一些代码和缓存的优化,
比如说将一些数据从memory中读取到cpu高速缓存中。我们观察一下CPU任务管理器中,L1、L2、L3缓存,速度远高于内存。
编写冒泡排序程序,并测试10次,比较Release和Debug】
Debug版本
Release版本
从结果中可以看到,大概有2-3倍的差距!release做了性能提升!
上面这段代码在release环境下出现问题了:主线程不能执行结束。
【问题分析】
从代码中可以发现,有两个线程在共同一个stop变量。
就是thread这个线程会将stop加载到Cpu Cache中,而主线程中,又修改了stop的数据,所以thread是无法知道的,
这样while就会一直执行!而主线程又在Join子线程,所以,无法输出!
【注意问题】以上情况,不是绝对的,也就是说主线程和子线程公用变量的情况,是否会出现上面的情况,是不完全确定的。
通常的解决方法:
【1】 不要让多个线程去操作一个共享变量,否则容易出问题。从根本上解决问题。
【2】如果一定要这么做,那就需要使用本节课所讲到的两个方法:??
?? MemoryBarrier()
?? VolatileRead/Write()
?? 也就是:不要进行缓存,每次读取数据都是从memory中读取数据。就不存在上面的问题。
十二、应用场景
跨线程访问控件
1)UI控件获取其他线程中的数据
2)跨线程读取控件的值
跨线程访问数据库
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!