01、什么是线程?
要深刻理解什么是线程,就需要了解计算机的发展史,需要了解多任务概念,需要了解进程概念,然后才是线程概念。因为我们主要还是讲解线程,因此这里就不进行展开说其他概念了,有兴趣的可以自行了解下。
简单来说,线程就是操作系统中能够单独执行任务的最小单元。
对于大多数编程语言来说,都有一个或者类似的功能的Main()方法,而该方法中的所有代码也都是按照顺序一行一行的执行,如果要想执行下一行代码那么就必须等待上一行代码执行完成。而线程作为能够单独执行任务的最小单元,因此线程可以使得应用程序的一部分独立于另外一部分而单独运行,这也就意味着我们可以改变程序的常规代码执行顺序,从而达到更复杂的程序控制。
对于一个应用程序来说,要想正常运行至少要有一个线程,通常为主线程,也就是上文中提到的Main()方法,当调用此方法时系统就会自动创建一个主线程。
02、后台线程和前台线程
线程可以分为前台线程和后台线程,两者基本完全相同,唯一区别是前台线程可以在托管执行环境中一直运行,而后台线程不可以。简单来说就是当进程中所有前台进程都停止后,系统会自动停止并关闭该进程内的所有后台线程。
在C#中可以通过Thread.IsBackground查看当前线程类型,也可以通过该属性修改线程类型。默认情况下,通过Thread对象新建并启动的所有线程都是前台线程。
看如下简单示例:
csharp
public static void CreateThread()
{
Console.WriteLine($"主线程 是否为后台线程:{Thread.CurrentThread.IsBackground}");
var thread1 = new Thread(()=> Console.WriteLine("Hello World"));
Console.WriteLine($" 线程1 默认为后台线程:{thread1.IsBackground}");
thread1.IsBackground = true;
Console.WriteLine($" 线程1 设置为后台线程:{thread1.IsBackground}");
thread1.Start();
}
执行效果如下:
03、线程的优先级
线程作为操作系统中能够单独执行任务的最小单元,那么当一个进程中有多个线程时,应该先执行那个线程呢?因此线程需要一个标记其执行优先级的属性。
在C#中Thread可以通过Priority来设置线程的优先级,告诉系统应该先执行谁。ThreadPriority有以下5种类型:
- Lowest: 最低优先级,在所有优先级中最低,在所有线程中可位于最后执行。
- BelowNormal: 低于正常优先级,在Normal优先级之后,在Lowest优先级之前。
- Normal: 默认优先级,线程默认的优先级
- AboveNormal: 高于正常优先级,在Highest优先级的线程之后,在Normal优先级之前。
- Highest: 最高优先级,在所有优先级中最高,在所有线程中可优先执行。
下面我们做个简单的测试,用来验证优先级不同的导致差异。
csharp
class ThreadPriorityTest
{
//是否执行,确保一个线程修改此值后,其他线程立刻查看到最新值
static volatile bool isRun = true;
//确保每个线程都有独立的副本存储计数统计值
[ThreadStatic]
static long threadCount;
//停止运行
public void Stop()
{
isRun = false;
}
//打印线程名称对应优先级以及计数总数
public void Print()
{
threadCount = 0;
while (isRun)
{
threadCount++;
}
Console.WriteLine($"{Thread.CurrentThread.Name} 优先级为{Thread.CurrentThread.Priority,8} 总执行计数为:{threadCount,-13:N0}");
}
}
public static void PriorityTest()
{
var threadPriorityTest = new ThreadPriorityTest();
//创建3个线程,并设置优先级
var thread1 = new Thread(threadPriorityTest.Print)
{
Name = "线程1"
};
var thread2 = new Thread(threadPriorityTest.Print)
{
Name = "线程2",
Priority = ThreadPriority.Lowest
};
var thread3 = new Thread(threadPriorityTest.Print)
{
Name = "线程3",
Priority = ThreadPriority.Highest
};
//启动3个线程
thread1.Start();
thread2.Start();
thread3.Start();
//休眠3秒
Thread.Sleep(10000);
//停止运行
threadPriorityTest.Stop();
//等待所有线程完成
thread1.Join();
thread2.Join();
thread3.Join();
}
执行效果如下:
可以发现优先级越高,其执行计数值越大。
其中需要注意的是volatile和ThreadStatic的用法。
在这个多线程示例中我们需要准确的统计不同的线程执行计数,因此正常来说可能需要设置多个变量用来对应存储各自线程的统计计数,很显然这样会导致代码臃肿。因此我们选用了另一种办法,使用ThreadStatic标记一个字段,使得该字段对每个线程都有独立的副本。这样可以做到线程之间不会共享这个字段的值,同时还可以做到多个线程只用这一个字段。
另外对于多线程共享的变量,很可能因为CPU缓存导致多个线程共享的变量不一致问题,因此通过volatile告诉编译器和运行时每次访问该字段时都要直接从内存中读取最新值,以此来保证线程之间的可见性。
04、线程的生命周期
当一个线程被创建后,会经历多个状态,包括未启动、已启动、执行、睡眠、挂起等十个状态,同时Thread类也提供了一些方法,用来控制当前线程状态,比如启动、停止、恢复、中止、挂起以及等待线程等方法。
我们先来看看线程具体有哪些状态:
- Running(运行)------ 线程已启动,而且没有停止;
- StopRequested(请求停止) ------ 请求停止线程;
- SuspendRequested(请求挂起) ------ 请求线程挂起;
- Background(后台) ------ 线程在后台执行;
- Unstarted(未启动) ------ 还没有在线程上调用 Start()方法;
- Stopped(停止) ------ 线程已完成了其所有的指令,而且已经停止;
- WaitSleepJoin(等待睡眠连接) ------ 通过调用 Wait()、Sleep()或 Join()方法,来暂停线程;
- Suspended(挂起) ------ 线程处于挂起状态;
- AbortRequested(请求中止) ------ Abort()方法已调用,但是线程还没有收到试图终止自己的 System.Threading.ThreadAbortexception,也就是说,线程还没有停止但不久就会停止;
- Aborted****(中止) ------ 线程处于停止状态,但不一定已执行完毕;
下面我们再来看看线程的常用方法。
- Start(): 启动线程,使其状态变更为Running。
- Sleep(): 把正在运行的线程暂停一段时间后自动恢复,线程状态保持活跃。
- Suspend() :[已弃用]暂停当前线程的执行,直到调用 Thread.Resume 显式恢复。
- Resume() :[已弃用]恢复一个已被暂停的线程。
- Interrupt(): 中断处于 WaitSleepJoin 线程状态的线程。
- Join(): 阻塞调用线程,直到某个线程终止时为止。
- Abort() :[已弃用]终止当前线程。
通过源码可以看到Resume和Suspend方法被弃用的原因。这是因为它们有很多问题和缺陷。使用它可能会导致程序的不稳定、死锁或者资源竞争问题。因此,它已经被标记为废弃,不推荐再使用。
在多线程编程中,通常可以通过合理的同步机制来控制线程的执行。比如,使用上述的 Monitor、Mutex、Event 和 Semaphore 来协调多个线程的行为,确保资源访问的安全和正确性。
注 :测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner