目录git
线程基础主要包括线程建立、挂起、等待和终止线程。关于更多的线程的底层实现,CPU时间片轮转等等的知识,能够参考《深刻理解计算机系统》
一书中关于进程和线程的章节,本文不过多赘述。算法
在C#语言中,建立线程是一件很是简单的事情;它只须要用到 System.Threading
命名空间,其中主要使用Thread
类来建立线程。编程
演示代码以下所示:c#
using System; using System.Threading; // 建立线程须要用到的命名空间 namespace Recipe1 { class Program { static void Main(string[] args) { // 1.建立一个线程 PrintNumbers为该线程所须要执行的方法 Thread t = new Thread(PrintNumbers); // 2.启动线程 t.Start(); // 主线程也运行PrintNumbers方法,方便对照 PrintNumbers(); // 暂停一下 Console.ReadKey(); } static void PrintNumbers() { // 使用Thread.CurrentThread.ManagedThreadId 能够获取当前运行线程的惟一标识,经过它来区别线程 Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印..."); for (int i = 0; i < 10; i++) { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i}"); } } } }
运行结果以下图所示,咱们能够经过运行结果得知上面的代码建立了一个线程,而后主线程和建立的线程交叉输出结果,这说明PrintNumbers
方法同时运行在主线程和另一个线程中。安全
暂停线程这里使用的方式是经过Thread.Sleep
方法,若是线程执行Thread.Sleep
方法,那么操做系统将在指定的时间内不为该线程分配任什么时候间片。若是Sleep时间100ms那么操做系统将至少让该线程睡眠100ms或者更长时间,因此Thread.Sleep
方法不能做为高精度的计时器使用。数据结构
演示代码以下所示:多线程
using System; using System.Threading; // 建立线程须要用到的命名空间 namespace Recipe2 { class Program { static void Main(string[] args) { // 1.建立一个线程 PrintNumbers为该线程所须要执行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.启动线程 t.Start(); // 暂停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { //3. 使用Thread.Sleep方法来使当前线程睡眠,TimeSpan.FromSeconds(2)表示时间为 2秒 Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } } }
运行结果以下图所示,经过下图能够肯定上面的代码是有效的,经过Thread.Sleep
方法,使线程休眠了2秒左右,可是并非特别精确的2秒。验证了上面的说法,它的睡眠是至少让线程睡眠多长时间,而不是必定多长时间。闭包
在本章中,线程等待使用的是Join
方法,该方法将暂停执行当前线程,直到所等待的另外一个线程终止。在简单的线程同步中会使用到,但它比较简单,不做过多介绍。ide
演示代码以下所示:函数
class Program { static void Main(string[] args) { Console.WriteLine($"-------开始执行 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.建立一个线程 PrintNumbersWithDelay为该线程所须要执行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.启动线程 t.Start(); // 3.等待线程结束 t.Join(); Console.WriteLine($"-------执行完毕 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暂停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } }
运行结果以下图所示,开始执行和执行完毕两条信息由主线程打印;根据其输出的顺序可见主线程是等待另外的线程结束后才输出执行完毕这条信息。
终止线程使用的方法是Abort
方法,当该方法被执行时,将尝试销毁该线程。经过引起ThreadAbortException
异常使线程被销毁。但通常不推荐使用该方法,缘由有如下几点。
- 使用
Abort
方法只是尝试销毁该线程,但不必定能终止线程。- 若是被终止的线程在执行lock内的代码,那么终止线程会形成线程不安全。
- 线程终止时,CLR会保证本身内部的数据结构不会损坏,可是BCL不能保证。
基于以上缘由不推荐使用Abort
方法,在实际项目中通常使用CancellationToken
来终止线程。
演示代码以下所示:
static void Main(string[] args) { Console.WriteLine($"-------开始执行 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.建立一个线程 PrintNumbersWithDelay为该线程所须要执行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.启动线程 t.Start(); // 3.主线程休眠6秒 Thread.Sleep(TimeSpan.FromSeconds(6)); // 4.终止线程 t.Abort(); Console.WriteLine($"-------执行完毕 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暂停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 如今时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } }
运行结果以下图所示,启动所建立的线程3后,6秒钟主线程调用了Abort
方法,线程3没有继续执行便结束了;与预期的结果一致。
线程的状态可经过访问ThreadState
属性来检测,ThreadState
是一个枚举类型,一共有10种状态,状态具体含义以下表所示。
成员名称 | 说明 |
---|---|
Aborted | 线程处于 Stopped 状态中。 |
AbortRequested | 已对线程调用了 Thread.Abort 方法,但线程还没有收到试图终止它的挂起的 System.Threading.ThreadAbortException。 |
Background | 线程正做为后台线程执行(相对于前台线程而言)。此状态能够经过设置 Thread.IsBackground 属性来控制。 |
Running | 线程已启动,它未被阻塞,而且没有挂起的 ThreadAbortException。 |
Stopped | 线程已中止。 |
StopRequested | 正在请求线程中止。这仅用于内部。 |
Suspended | 线程已挂起。 |
SuspendRequested | 正在请求线程挂起。 |
Unstarted | 还没有对线程调用 Thread.Start 方法。 |
WaitSleepJoin | 因为调用 Wait、Sleep 或 Join,线程已被阻止。 |
下表列出致使状态更改的操做。
操做 | ThreadState |
---|---|
在公共语言运行库中建立线程。 | Unstarted |
线程调用 Start | Unstarted |
线程开始运行。 | Running |
线程调用 Sleep | WaitSleepJoin |
线程对其余对象调用 Wait。 | WaitSleepJoin |
线程对其余线程调用 Join。 | WaitSleepJoin |
另外一个线程调用 Interrupt | Running |
另外一个线程调用 Suspend | SuspendRequested |
线程响应 Suspend 请求。 | Suspended |
另外一个线程调用 Resume | Running |
另外一个线程调用 Abort | AbortRequested |
线程响应 Abort 请求。 | Stopped |
线程被终止。 | Stopped |
演示代码以下所示:
static void Main(string[] args) { Console.WriteLine("开始执行..."); Thread t = new Thread(PrintNumbersWithStatus); Thread t2 = new Thread(DoNothing); // 使用ThreadState查看线程状态 此时线程未启动,应为Unstarted Console.WriteLine($"Check 1 :{t.ThreadState}"); t2.Start(); t.Start(); // 线程启动, 状态应为 Running Console.WriteLine($"Check 2 :{t.ThreadState}"); // 因为PrintNumberWithStatus方法开始执行,状态为Running // 可是经接着会执行Thread.Sleep方法 状态会转为 WaitSleepJoin for (int i = 1; i < 30; i++) { Console.WriteLine($"Check 3 : {t.ThreadState}"); } // 延时一段时间,方便查看状态 Thread.Sleep(TimeSpan.FromSeconds(6)); // 终止线程 t.Abort(); Console.WriteLine("t线程被终止"); // 因为该线程是被Abort方法终止 因此状态为 Aborted或AbortRequested Console.WriteLine($"Check 4 : {t.ThreadState}"); // 该线程正常执行结束 因此状态为Stopped Console.WriteLine($"Check 5 : {t2.ThreadState}"); Console.ReadKey(); } static void DoNothing() { Thread.Sleep(TimeSpan.FromSeconds(2)); } static void PrintNumbersWithStatus() { Console.WriteLine("t线程开始执行..."); // 在线程内部,可经过Thread.CurrentThread拿到当前线程Thread对象 Console.WriteLine($"Check 6 : {Thread.CurrentThread.ThreadState}"); for (int i = 1; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"t线程输出 :{i}"); } }
运行结果以下图所示,与预期的结果一致。
Windows操做系统为抢占式多线程(Preemptive multithreaded)操做系统,是由于线程可在任什么时候间中止(被枪占)并调度另外一个线程。
Windows操做系统中线程有0(最低) ~ 31(最高)
的优先级,而优先级越高所能占用的CPU时间就越多,肯定某个线程所处的优先级须要考虑进程优先级和相对线程优先级两个优先级。
- 进程优先级:Windows支持6个进程优先级,分别是
Idle、Below Normal、Normal、Above normal、High 和Realtime
。默认为Normal
。- 相对线程优先级:相对线程优先级是相对于进程优先级的,由于进程包含了线程。Windows支持7个相对线程优先级,分别是
Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和 Time-Critical
.默认为Normal
。
下表总结了进程的优先级和线程的相对优先级与优先级(0~31)的映射关系。粗体为相对线程优先级,斜体为进程优先级。
Idle | Below Normal | Normal | Above Normal | High | Realtime | |
---|---|---|---|---|---|---|
Time-Critical | 15 | 15 | 15 | 15 | 15 | 31 |
Highest | 6 | 8 | 10 | 12 | 15 | 26 |
Above Normal | 5 | 7 | 9 | 11 | 14 | 25 |
Normal | 4 | 6 | 8 | 10 | 13 | 24 |
Below Normal | 3 | 5 | 7 | 9 | 12 | 23 |
Lowest | 2 | 4 | 6 | 8 | 11 | 22 |
Idle | 1 | 1 | 1 | 1 | 1 | 16 |
而在C#程序中,可更改线程的相对优先级,须要设置Thread
的Priority
属性,可设置为ThreadPriority
枚举类型的五个值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest
。CLR为本身保留了Idle
和Time-Critical
优先级,程序中不可设置。
演示代码以下所示。
static void Main(string[] args) { Console.WriteLine($"当前线程优先级: {Thread.CurrentThread.Priority} \r\n"); // 第一次测试,在全部核心上运行 Console.WriteLine("运行在全部空闲的核心上"); RunThreads(); Thread.Sleep(TimeSpan.FromSeconds(2)); // 第二次测试,在单个核心上运行 Console.WriteLine("\r\n运行在单个核心上"); // 设置在单个核心上运行 System.Diagnostics.Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1); RunThreads(); Console.ReadLine(); } static void RunThreads() { var sample = new ThreadSample(); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "线程一"; var threadTwo = new Thread(sample.CountNumbers); threadTwo.Name = "线程二"; // 设置优先级和启动线程 threadOne.Priority = ThreadPriority.Highest; threadTwo.Priority = ThreadPriority.Lowest; threadOne.Start(); threadTwo.Start(); // 延时2秒 查看结果 Thread.Sleep(TimeSpan.FromSeconds(2)); sample.Stop(); } class ThreadSample { private bool _isStopped = false; public void Stop() { _isStopped = true; } public void CountNumbers() { long counter = 0; while (!_isStopped) { counter++; } Console.WriteLine($"{Thread.CurrentThread.Name} 优先级为 {Thread.CurrentThread.Priority,11} 计数为 = {counter,13:N0}"); } }
运行结果以下图所示。Highest
占用的CPU时间明显多于Lowest
。当程序运行在全部核心上时,线程能够在不一样核心同时运行,因此Highest
和Lowest
差距会小一些。
在CLR中,线程要么是前台线程,要么就是后台线程。当一个进程的全部前台线程中止运行时,CLR将强制终止仍在运行的任何后台线程,不会抛出异常。
在C#中可经过Thread
类中的IsBackground
属性来指定是否为后台线程。在线程生命周期中,任什么时候候均可从前台线程变为后台线程。线程池中的线程默认为后台线程。
演示代码以下所示。
static void Main(string[] args) { var sampleForeground = new ThreadSample(10); var sampleBackground = new ThreadSample(20); var threadPoolBackground = new ThreadSample(20); // 默认建立为前台线程 var threadOne = new Thread(sampleForeground.CountNumbers); threadOne.Name = "前台线程"; var threadTwo = new Thread(sampleBackground.CountNumbers); threadTwo.Name = "后台线程"; // 设置IsBackground属性为 true 表示后台线程 threadTwo.IsBackground = true; // 线程池内的线程默认为 后台线程 ThreadPool.QueueUserWorkItem((obj) => { Thread.CurrentThread.Name = "线程池线程"; threadPoolBackground.CountNumbers(); }); // 启动线程 threadOne.Start(); threadTwo.Start(); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 0; i < _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }
运行结果以下图所示。当前台线程10次循环结束之后,建立的后台线程和线程池线程都会被CLR强制结束。
向线程中传递参数经常使用的有三种方法,构造函数传值、Start方法传值和Lambda表达式传值,通常经常使用Start方法来传值。
演示代码以下所示,经过三种方式来传递参数,告诉线程中的循环最终须要循环几回。
static void Main(string[] args) { // 第一种方法 经过构造函数传值 var sample = new ThreadSample(10); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "ThreadOne"; threadOne.Start(); threadOne.Join(); Console.WriteLine("--------------------------"); // 第二种方法 使用Start方法传值 // Count方法 接收一个Object类型参数 var threadTwo = new Thread(Count); threadTwo.Name = "ThreadTwo"; // Start方法中传入的值 会传递到 Count方法 Object参数上 threadTwo.Start(8); threadTwo.Join(); Console.WriteLine("--------------------------"); // 第三种方法 Lambda表达式传值 // 其实是构建了一个匿名函数 经过函数闭包来传值 var threadThree = new Thread(() => CountNumbers(12)); threadThree.Name = "ThreadThree"; threadThree.Start(); threadThree.Join(); Console.WriteLine("--------------------------"); // Lambda表达式传值 会共享变量值 int i = 10; var threadFour = new Thread(() => PrintNumber(i)); i = 20; var threadFive = new Thread(() => PrintNumber(i)); threadFour.Start(); threadFive.Start(); } static void Count(object iterations) { CountNumbers((int)iterations); } static void CountNumbers(int iterations) { for (int i = 1; i <= iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } static void PrintNumber(int number) { Console.WriteLine(number); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 1; i <= _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }
运行结果以下图所示,与预期结果相符。
在多线程的系统中,因为CPU的时间片轮转等线程调度算法的使用,容易出现线程安全问题。具体可参考《深刻理解计算机系统》
一书相关的章节。
在C#中lock
关键字是一个语法糖,它将Monitor
封装,给object加上一个互斥锁,从而实现代码的线程安全,Monitor
会在下一节中介绍。
对于lock
关键字仍是Monitor
锁定的对象,都必须当心选择,不恰当的选择可能会形成严重的性能问题甚至发生死锁。如下有几条关于选择锁定对象的建议。
- 同步锁定的对象不能是值类型。由于使用值类型时会有装箱的问题,装箱后的就成了一个新的实例,会致使
Monitor.Enter()
和Monitor.Exit()
接收到不一样的实例而失去关联性- 避免锁定
this、typeof(type)和string
。this
和typeof(type)
锁定可能在其它不相干的代码中会有相同的定义,致使多个同步块互相阻塞。string
须要考虑字符串拘留的问题,若是同一个字符串常量在多个地方出现,可能引用的会是同一个实例。- 对象的选择做用域尽量恰好达到要求,使用静态的、私有的变量。
如下演示代码实现了多线程状况下的计数功能,一种实现是线程不安全的,会致使结果与预期不相符,但也有可能正确。另一种使用了lock
关键字进行线程同步,因此它结果是必定的。
static void Main(string[] args) { Console.WriteLine("错误的多线程计数方式"); var c = new Counter(); // 开启3个线程,使用没有同步块的计数方式对其进行计数 var t1 = new Thread(() => TestCounter(c)); var t2 = new Thread(() => TestCounter(c)); var t3 = new Thread(() => TestCounter(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 由于多线程 线程抢占等缘由 其结果是不必定的 碰巧可能为0 Console.WriteLine($"Total count: {c.Count}"); Console.WriteLine("--------------------------"); Console.WriteLine("正确的多线程计数方式"); var c1 = new CounterWithLock(); // 开启3个线程,使用带有lock同步块的方式对其进行计数 t1 = new Thread(() => TestCounter(c1)); t2 = new Thread(() => TestCounter(c1)); t3 = new Thread(() => TestCounter(c1)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 其结果是必定的 为0 Console.WriteLine($"Total count: {c1.Count}"); Console.ReadLine(); } static void TestCounter(CounterBase c) { for (int i = 0; i < 100000; i++) { c.Increment(); c.Decrement(); } } // 线程不安全的计数 class Counter : CounterBase { public int Count { get; private set; } public override void Increment() { Count++; } public override void Decrement() { Count--; } } // 线程安全的计数 class CounterWithLock : CounterBase { private readonly object _syncRoot = new Object(); public int Count { get; private set; } public override void Increment() { // 使用Lock关键字 锁定私有变量 lock (_syncRoot) { // 同步块 Count++; } } public override void Decrement() { lock (_syncRoot) { Count--; } } } abstract class CounterBase { public abstract void Increment(); public abstract void Decrement(); }
运行结果以下图所示,与预期结果相符。
Monitor
类主要用于线程同步中, lock
关键字是对Monitor
类的一个封装,其封装结构以下代码所示。
try { Monitor.Enter(obj); dosomething(); } catch(Exception ex) { } finally { Monitor.Exit(obj); }
如下代码演示了使用Monitor.TyeEnter()
方法避免资源死锁和使用lock
发生资源死锁的场景。
static void Main(string[] args) { object lock1 = new object(); object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2) { Thread.Sleep(1000); Console.WriteLine("Monitor.TryEnter能够不被阻塞, 在超过指定时间后返回false"); // 若是5S不能进入同步块,那么返回。 // 由于前面的lock锁定了 lock2变量 而LockTooMuch()一开始锁定了lock1 因此这个同步块没法获取 lock1 而LockTooMuch方法内也不能获取lock2 // 只能等待TryEnter超时 释放 lock2 LockTooMuch()才会是释放 lock1 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) { Console.WriteLine("获取保护资源成功"); } else { Console.WriteLine("获取资源超时"); } } new Thread(() => LockTooMuch(lock1, lock2)).Start(); Console.WriteLine("----------------------------------"); lock (lock2) { Console.WriteLine("这里会发生资源死锁"); Thread.Sleep(1000); // 这里必然会发生死锁 // 本同步块 锁定了 lock2 没法获得 lock1 // 而 LockTooMuch 锁定了 lock1 没法获得 lock2 lock (lock1) { // 该语句永远都不会执行 Console.WriteLine("获取保护资源成功"); } } } static void LockTooMuch(object lock1, object lock2) { lock (lock1) { Thread.Sleep(1000); lock (lock2) ; } }
运行结果以下图所示,由于使用Monitor.TryEnter()
方法在超时之后会返回,不会阻塞线程,因此没有发生死锁。而第二段代码中lock
没有超时返回的功能,致使资源死锁,同步块中的代码永远不会被执行。
在多线程中处理异常应当使用就近原则,在哪一个线程发生异常那么所在的代码块必定要有相应的异常处理。不然可能会致使程序崩溃、数据丢失。
主线程中使用try/catch
语句是不能捕获建立线程中的异常。可是万一遇到不可预料的异常,可经过监听AppDomain.CurrentDomain.UnhandledException
事件来进行捕获和异常处理。
演示代码以下所示,异常处理 1 和 异常处理 2 能正常被执行,而异常处理 3 是无效的。
static void Main(string[] args) { // 启动线程,线程代码中进行异常处理 var t = new Thread(FaultyThread); t.Start(); t.Join(); // 捕获全局异常 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; t = new Thread(BadFaultyThread); t.Start(); t.Join(); // 线程代码中不进行异常处理,尝试在主线程中捕获 AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; try { t = new Thread(BadFaultyThread); t.Start(); } catch (Exception ex) { // 永远不会运行 Console.WriteLine($"异常处理 3 : {ex.Message}"); } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine($"异常处理 2 :{(e.ExceptionObject as Exception).Message}"); } static void BadFaultyThread() { Console.WriteLine("有异常的线程已启动..."); Thread.Sleep(TimeSpan.FromSeconds(2)); throw new Exception("Boom!"); } static void FaultyThread() { try { Console.WriteLine("有异常的线程已启动..."); Thread.Sleep(TimeSpan.FromSeconds(1)); throw new Exception("Boom!"); } catch (Exception ex) { Console.WriteLine($"异常处理 1 : {ex.Message}"); } }
运行结果以下图所示,与预期结果一致。
本文主要参考了如下几本书,在此对这些做者表示由衷的感谢大家提供了这么好的资料。
- 《CLR via C#》
- 《C# in Depth Third Edition》
- 《Essential C# 6.0》
- 《Multithreading with C# Cookbook Second Edition》
线程基础这一章节终于整理完了,是笔者学习过程当中的笔记和思考。计划按照《Multithreading with C# Cookbook Second Edition》这本书的结构,一共更新十二个章节,先立个Flag。
源码下载点击连接 示例源码下载