C# 多线程
闲得蛋疼,记录一下线程的基础概念和常规用法
基础概念
**进程:**应用程序(typerpa、word、IDEA)运行的时候进入到内存,程序在内存中占用的内存空间(进程)。
- 是程序(任务)的一个执行过程,是动态的。
- 持有资源(共享内存,共享文件)和线程。
线程:进程中负责程序运行的执行单元,也叫执行路径。
- 可以将进程看作一个班级,线程看作班级里的学生,学生通过使用共享资源(教师、桌椅、黑板)进行学习。
- 多线程技术解决了多部分代码同时执行的需求,能够更好的利用cpu的资源。
线程任务:线程执行的代码内容。
并行:多个cpu实例或者多台机器同时执行处理逻辑,实现真正的同时。
- 如果电脑的CPU是多核处理器(4核),那就可以做到同时处理4个线程,从真正意义上做到多线程并行。线程的最大并行数量上限是CPU核心的数量,但是,往往电脑运行的线程的数量远大于CPU核心的数量,所以 还是需要CPU时间片的切换。
并发:通过cpu的调度算法,快速的切换线程执行线程任务,并不是真正意义上的同时。
线程安全:同一段代码在多线程使用下,不会因为线程的调度顺序而影响任何结果。在出现共享资源的情况下容易出现线程安全问题。确保线程的安全要优先于性能。
争用条件:当多个线程共享访问同一数据时,每个线程都尝试操作该数据,从而导致数据被破坏,这种现象称为争用条件。
注意,尽管多线程的使用可以提升程序的运行效率,充分发挥CPU的作用,但多线程并不是所有时候都可以随便使用的,多线程在时间和空间上都是有开销的,当超过一定限度之后,多线程开得越多,性能反而会下降。因此使用时应该根据实际需求,谨慎使用。
空间上的开销
- Thread内核数据结构:线程的ID和线程环境都在CPU寄存器中占用空间。例如在CPU进行时间片切换的时候,没有处理完的数据会存储在CPU的相关空间中。
- 用户堆栈模式:用户程序中的局部变量和参数传递所使用的堆栈。常见的异常:StackOverFlowException(堆栈溢出)就是内存占存过多,导致溢出异常。
时间上的开销
- 资源使用通知的开销:因为一个程序会调用很多的托管和非托管的dll或exe资源,使用前需要先进行通知确认,这都是需要时间的。
- 时间片切换开销。
Thread
一、线程的简单使用
在C#中启动多线程需要实例化一个 Thread
对象,然后在构造方法中传入委托对象,为了方便一般会直接传入lambda表达式, 这个lambda表达式中执行的代码片段就是新的线程中所需要执行的任务。
最后,调用Thread
对象Start()
方法来启动新线程。
static void Main(string[] args)
{
var thread = new Thread(() =>
{
Print();
});
thread.Start();
Console.WriteLine($"主线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("主线程结束!");
}
private static void Print()
{
for (var i = 0; i< 100; i++)
{
Console.WriteLine($"子线程ID:{Thread.CurrentThread.ManagedThreadId}------{i}");
Thread.Sleep(1000);
}
}
另外提一下,因为线程的构造函数Thread()
中所需要传入的参数是一个委托对象,所以如果任务函数不需要传入任何参数,可以直接将任务函数的名称作为参数传给构造函数。
var thread = new Thread(Print);
此外,如果需要提前终结线程,可以使用Thread
对象的Abort()
方法。
Abort()
:在调用此方法的线程上引发 System.Threading.ThreadAbortException
,通过处理异常,去决定程序的终止与否(例如直接不理会异常导致整个程序终止),并不能直接终止线程。
- 该方法已经过时弃用,不建议使用。
二、前台线程&后台线程
从上面的例子中可以看到,当程序运行起来后,主线程已经运行结束了,但是程序却没有停止,子线程依然在运行,依然每隔1秒输出一次。
这种情况下,持续运行的子线程就被称作前台线程(Foreground Thread)。
一般来说,在前台线程中执行的任务都比较重要,所以,只有等待所有的前台线程结束以后程序才能关闭。
相对而言,后台线程执行的任务就不是那么重要了,只要主线程和所有的前台线程执行结束,就算后台线程的任务还没完成,也会强行打断直接退出。
在 .Net中,除了主线程以外,任何一个线程都可以在任何时候在前台和后台之间随意切换。代码也非常简单,只需要将它的IsBackground
属性设置为true
,那么这个线程就可以被切换为后台线程了。
var thread = new Thread(() =>
{
Print();
});
thread.IsBackground = true;
thread.Start();
特点:
后台线程不会影响进程的终止,属于某个进程的所有前台线程都终止后,该进程就会被终止,所有剩余的后台线程都会被强制停止。
可以在任何时候(线程开始前或开始后都可以)将前台线程修改为后台线程,方式是设置IsBackground
属性。
不管是前台线程还是后台线程,如果线程内出现了异常,都会导致进程的终止。
托管线程池中的线程都是后台线程,使用new Thread
方式创建的线程默认都是前台线程。
三、线程池
线程是非常消耗资源的,如果我们每次需要子线程来执行任务,就去创建一个新的线程,那当我们执行1千次1万次甚至是100万次的时候,那么估计电脑硬件也受不了这么大的资源消耗。
对于这种情况我们就需要使用线程池来管理线程了。只要是通过线程池创建的线程,那么它的所有的生命周期都会托管在线程池内。所有线程池中的线程都是后台线程,线程池会检查程序当前的运行状态,当程序启动时,线程池内的线程也会开始工作;而当Main方法结束以后,线程池会自动关闭,同时中断所有正在运行的线程。
线程池的使用方式很简单,直接调用线程池ThreadPool
类中的静态方法QueueUserWorkItem()
来创建线程池内的线程就可以了。
static bool QueueUserWorkItem(WaitCallback callBack)
:参数为一个带一个object
类型参数的委托,最后返回bool
值成功则返回true
。
static void