线程概述
线程是一个独立处理的执行路径。每个线程都运行在一个操作系统进程中,这个进程是程序执行的独立环境。在单线程中进程的独立环境内只有一个线程运行,所以该线程具有独立使用进程资源的权利。在多线程程序中,在进程中有多个线程运行,所以它们共享同一个执行环境。
基础线程(thread)
使用Thread类可以创建和控制线程,定义在System.Threading命名空间中:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int mainId = Thread.CurrentThread.ManagedThreadId; 6 Console.WriteLine("主线程Id为:{0}", mainId); 7 //定义线程 8 Thread thread = new Thread(() => 9 { 10 Test("Demo-ok"); 11 }); 12 //启动线程 13 thread.Start(); 14 Console.WriteLine("主线程Id为:{0}", mainId); 15 Console.ReadKey(); 16 } 17 static void Test(string o) 18 { 19 Console.WriteLine("工作者线程Id为:{0}", Thread.CurrentThread.ManagedThreadId); 20 Console.WriteLine("执行方法:{0}", o); 21 } 22 /* 23 * 作者:Jonins 24 * 出处:http://www.cnblogs.com/jonins/ 25 */ 26 }
执行结果(执行结果并不固定):
主线程创建一个新线程thread在上面运行一个方法Test。同时主线程也会继续执行。在单核计算机上,操作系统会给每一个线程分配一些"时间片"(winodws一般为20毫秒),用于模拟并发性。而在多核/多处理器主机上线程却能够真正实现并行执行(分别由计算机上其它激活处理器完成)。
线程常用方法
Thread在.NET Framework 1.1起引入是最早的多线程处理方式,他包含了几种最常用的方法如下,
Start | 开启线程(停止后的线程无法再次启用) |
Suspend | 暂停(挂起)线程(已过时,不推荐使用) |
Resume | 恢复暂停(挂起)的线程(已过时,不推荐使用) |
Intterupt | 中断线程 |
Abort | 销毁线程 |
IsAlive | 获取当前线程的执行状态(True-运行,False-停止) |
Join |
方法是非静态方法,使得在系统调用此方法时只有这个线程执行完后,才能执行其他线程,包括主线程的终止! 或者给它制定时间,即最多过了这么多时间后,如果还是没有执行完,下面的线程可以继续执行而不必再理会当前线程是否执行完。 |
Thread.Sleep |
方式是Thread类静态方法,在调用出使得该线程暂停一段时间 |
注意:
不要使用Suspend和Resume方法来同步线程的活动。当你Suspend线程时,您无法知道线程正在执行什么代码。如果在安全权限评估期间线程持有锁时挂起线程,则AppDomain中的其他线程可能会被阻塞。如果线程在执行类构造函数时Suspend,则试图使用该类的AppDomain中的其他线程将被阻塞。死锁很容易发生。
后台/前台线程 &阻塞
前台进程和后台进程使用IsBackground属性设置。此状态与线程的优先级(执行时间分配)无关。
前台进程:Thread默认为前台线程,程序关闭后,线程仍然继续,直到计算完为止。
后台进程:将IsBackground属性设置为true,即为后台进程,主线程关闭,所有子线程无论运行完否,都马上关闭。
线程阻塞是指线程由于特定原因暂停执行,如Sleeping或执行Join后等待另一个线程停止。阻塞的线程会立刻交出”时间片“, 并从此时开始不再消耗处理器的时间,直至阻塞条件结束。使用线程的ThreadState属性,可以测试线程的阻塞状态。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Thread thread = new Thread(() => 6 { 7 Test("Demo-ok"); 8 }); 9 var state = thread.ThreadState; 10 Console.WriteLine("子线程开启前ThreadState:{0}", state); 11 //开启线程 12 thread.Start(); 13 state = thread.ThreadState; 14 Console.WriteLine("子线程开启后ThreadState:{0}", state); 15 //阻塞主线程1秒 16 Thread.Sleep(1000); 17 state = thread.ThreadState; 18 Console.WriteLine("子线程阻塞时ThreadState:{0}", state); 19 //主线程等待子线程执行完成 20 thread.Join(); 21 state = thread.ThreadState; 22 Console.WriteLine("子线程执行完成ThreadState:{0}", state); 23 Console.ReadKey(); 24 } 25 static void Test(string o) 26 { 27 //阻塞子线程2秒 28 Thread.Sleep(2000); 29 Console.WriteLine("方法执行完成!返回值:{0}", o); 30 } 31 /* 32 * 作者:Jonins 33 * 出处:http://www.cnblogs.com/jonins/ 34 */ 35 }
结果如下:
ThreadState是一个标记枚举量,我们只大约常用的记住这四个状态即可,其它因为API中弃用了一部分如挂起等不必考虑:
Running | 启动线程 |
Stopped | 该线程已停止 |
Unstarted | 未开启 |
WaitSleepJoin | 线程受阻 |
注意:
1.当线程阻塞时,操作系统执行环境(线程上下文)切换,会增加负载,幅度一般在1-2毫秒左右。
2.ThreadState属性只是用于调试程序,绝对不要用ThreadState来同步线程活动,因为线程状态可能在测试ThreadState和获取这个信息的时间段内发生变化。
线程优先级
1 xxx.Priority = ThreadPriority.Normal;
AboveNormal | 高于正常 |
BelowNormal | 低于正常 |
Highest | 最高 |
Lowest | 最低 |
Normal | 正常 |
ThreadStart&ParameterizedThreadStart
Thread重载的其它四种构造函数需要带入特殊对象,分别是ThreadStart和ParameterizedThreadStart类。
ThreadStart类本质是一个无参数无返回值的委托。
1 public delegate void ThreadStart();
ParameterizedThreadStart类本质是有一个object类型参数无返回值的委托。
1 public delegate void ParameterizedThreadStart(object obj);
使用方式如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int mainId = Thread.CurrentThread.ManagedThreadId; 6 Console.WriteLine("主线程Id为:{0}", mainId); 7 //ThreadStart构造函数创建线程 8 { 9 ThreadStart threadStart = new ThreadStart(TestOne); 10 Thread threadOne = new Thread(threadStart); 11 threadOne.Start(); 12 } 13 //ParameterizedThreadStart构造函数创建线程 14 { 15 ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(TestTwo); 16 Thread threadTwo = new Thread(parameterizedThreadStart); 17 threadTwo.Start("DemoTwo-ok"); 18 } 19 Console.WriteLine("主线程Id为:{0}", mainId); 20 Console.ReadKey(); 21 } 22 private static void TestOne() 23 { 24 Console.WriteLine("执行方法:DemoOne-ok,工作者线程Id为:{0}", Thread.CurrentThread.ManagedThreadId); 25 } 26 private static void TestTwo(object o) 27 { 28 Console.WriteLine("执行方法:{0},工作者线程Id为:{1}", o, Thread.CurrentThread.ManagedThreadId); 29 } 30 /* 31 * 作者:Jonins 32 * 出处:http://www.cnblogs.com/jonins/ 33 */ 34 }
执行结果(执行结果不固定):
因为ThreadStart和ParameterizedThreadStart为委托,所以我们也可以把符合要求的自定义委托或者内置委托进行转换带入构造函数。例如:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Action action = Test; 6 Thread thread = new Thread(new ThreadStart(action)); 7 thread.Start(); 8 Console.ReadKey(); 9 } 10 private static void Test() 11 { 12 Console.WriteLine("执行方法:Demo-ok"); 13 } 14 }
注意:
在需要传递参数时ParameterizedThreadStart构造线程和使用lambda表达式构建线程有着极大的区别:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //DemoOne();//数据被主线程修改 6 //DemoTwo(); 7 Console.ReadKey(); 8 } 9 static void DemoOne() 10 { 11 string message = "XXXXXX"; 12 Thread thread = new Thread(() => Test(message)); 13 thread.Start(); 14 message = "YYYYYY"; 15 } 16 static void DemoTwo() 17 { 18 19 string message = "XXXXXX"; 20 ParameterizedThreadStart parameterizedThreadStart = Test; 21 Thread thread = new Thread(parameterizedThreadStart); 22 thread.Start(message); 23 message = "YYYYYY"; 24 } 25 private static void Test(object o) 26 { 27 for (int i = 0; i < 1000; i++) 28 { 29 Console.WriteLine( o); 30 } 31 } 32 /* 33 * 作者:Jonins 34 * 出处:http://www.cnblogs.com/jonins/ 35 */ 36 }
上述案例对比DemoOne和DemoTwo的执行结果我们可以得到:
1.使用lamdba表达式构建线程时,变量由引用捕获,父线程中的任何更改都将影响子线程内的值。且lamda是在实际执行时捕获变量而不是在线程开始时捕获变量,如果在父线程中修改参数值子线程内的值也会受到影响。
2.而ParameterizedThreadStart则是在线程启动是捕获变量,启动后父线程修改变量值子线程内的值不会受到影响。
本地/共享状态
CLR会给每一个线程分配独立的内存堆,从而保证本地变量的隔离。而多个线程访问相同的对象,并对共享状态的访问没有同步,此时就会出现数据争用的问题从而引发程序间歇性错误,这也是多线程经常被诟病的缘由。
局部(本地)变量每个线程的内存堆都会创建变量副本。
如果线程拥有同一个对象实例的通用引用,那么这些线程就会共享数据。
1 public class ThreadInstance 2 { 3 //共享变量 4 bool flag; 5 public void Demo() 6 { 7 new Thread(Test).Start();//子线程执行一次方法 8 Thread.Sleep(1000); 9 Test();//主线程执行一次方法 10 Console.ReadKey(); 11 } 12 void Test() 13 { 14 //线程内局部变量 15 bool localFlag=true; 16 Console.WriteLine("localFlag:{0}", localFlag); 17 localFlag = !localFlag; 18 if (!flag) 19 { 20 Console.WriteLine("flag:{0}", flag); 21 flag = !flag; 22 } 23 } 24 }
执行Demo方法结果:
因为两个线程都在同一个ThreadInstance实例上调用方法,所以它们共享flag,因此flag变量只会打印一次。而localFlag为局部变量所以两个线程内变量相互不影响。
注意:
1.编译器会将lambda表达式或匿名代理捕获的局部变量转换为域,它们会共享数据。
2.静态域线程之间也会共享数据。
线程同步
在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
线程同步:一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
同步结构可以分三大类:
排他锁:排他锁结构只允许一个线程执行特定的活动,它们的主要目标是允许线程访问共享的写状态,但不会互相影响。包括(lock、Mutex、SpinLock)。
非排他锁:非排他锁只能实现有限的并发性。包括(Semaphore、ReaderWriterLock)。
发送信号:允许线程保持阻塞,直到从其它线程接受到通知。包括(ManualResetEvent、AutoResetEvent、CountdownEvent和Barrier)
排他锁 lock&Mutex&SpinLock
1.内核锁 Lock&Monitor
Lock:保证当多个线程同时争夺同一个锁时,每次只有一个线程可以锁定同步对象,其他线程会等待(或阻塞)在加锁位置,直到锁释放,其它线程才可以继续访问。如果多个线程争夺同一个锁,那么它们会在一个准备队列中排队,以先到先得的方式分配锁。排他锁有时候也称为对锁保护的对象添加序列化访问权限,因为一个线程的访问不会与其他线程的访问重叠。
lock使用的示例如下,Demo未加锁,DemoTwo加锁:
1 public class ThreadInstance 2 { 3 //--------------Demo---------------- 4 public void Demo() 5 { 6 new Thread(Test).Start(); 7 Test(); 8 } 9 private bool Flag { get; set; } 10 void Test() 11 { 12 Console.WriteLine("Demo-Flag:{0}", Flag); 13 Thread.Sleep(1000);//阻塞子线程,让主线程运行下来 14 Flag = true; 15 } 16 //--------------DemoTwo---------------- 17 public void DemoTwo() 18 { 19 new Thread(TestTow).Start(); 20 TestTow(); 21 } 22 private bool FlagTow { get; set; } 23 readonly object Locker = new object(); 24 void TestTow() 25 { 26 //加锁,阻塞主线程直至子线程执行完毕 27 lock (Locker) 28 { 29 Console.WriteLine("TestTow-FlagTow:{0}", FlagTow); 30 Thread.Sleep(1000);//阻塞子线程,让主线程运行下来 31 FlagTow = true; 32 } 33 } 34 /* 35 * 作者:Jonins 36 * 出处:http://www.cnblogs.com/jonins/ 37 */ 38 }
执行结果如下:
Demo:不具有线程安全性,两个线程同时调用Test,会出现两次False,因为主线程执行时子线程变量还没有改变。
DemoTwo:保证每次只有一个线程可以锁定同步对象(Locker),其他竞争线程(本例即主线程)都会阻塞在这个位置,直至锁释放,所以会打印一次False和一次True。
lock语句是Monitor.Enter和Monitor.Exit方法调用try/finally语句块的简写语法。
1 lock (Locker) 2 { 3 ... 4 } 5 //-------两者等价------- 6 Monitor.Enter(Locker); 7 try 8 { 9 ... 10 } 11 finally 12 { 13 Monitor.Exit(Locker); 14 }
但此写法在方法调用和语句块之间若抛出异常,锁将无法释放,因为执行过程无法再进入try/finally语句块,导致锁泄露,优化方法是使用Monitor.Enter重载,同时可以使用Monitor.TryEnter方法指定一个超时时间。
1 bool lockTaken = false; 2 Monitor.Enter(Locker, ref lockTaken); 3 try 4 { 5 ... 6 } 7 finally 8 { 9 if (lockTaken) 10 Monitor.Exit(Locker); 11 }
2.互斥锁 Mutex
Mutex:类似于C#的Lock,但是它可以支持多个进程。所以Mutex可用于计算机范围或应用范围。使用Mutex类,就可以调用WaitOne方法获得锁,ReleaseMutex释放锁,关闭或去掉一个Mutex会自动释放互斥锁。
示例来自https://msdn.microsoft.com/zh-cn/library/system.threading.mutex(v=vs.110).aspx ,如需更详细请访问MSDN。
1 class Program 2 { 3 //创建一个新的互斥。创建线程不拥有互斥对象。 4 private static Mutex mut = new Mutex(); 5 private const int numThreads = 3; 6 static void Main(string[] args) 7 { 8 //创建将使用受保护资源的线程 9 for (int i = 0; i < numThreads; i++) 10 { 11 Thread newThread = new Thread(new ThreadStart(ThreadProc)); 12 newThread.Name = String.Format("Thread{0} :", i + 1); 13 newThread.Start(); 14 } 15 Console.ReadKey(); 16 } 17 private static void ThreadProc() 18 { 19 Console.WriteLine("{0}请求互斥锁", Thread.CurrentThread.Name); 20 // 等待,直到安全进入,如果请求超时,不会获得互斥量 21 if (mut.WaitOne(3000)) 22 { 23 Console.WriteLine("{0}进入保护区了", Thread.CurrentThread.Name); 24 { 25 //模拟一些工作 26 Thread.Sleep(2000); 27 Console.WriteLine("{0}执行了工作 ", Thread.CurrentThread.Name); 28 } 29 // 释放互斥锁。 30 mut.ReleaseMutex(); 31 Console.WriteLine("{0}释放了互斥锁 ", Thread.CurrentThread.Name); 32 } 33 else 34 { 35 Console.WriteLine("{0}不会获得互斥量", Thread.CurrentThread.Name); 36 } 37 } 38 }
注意:
1.给Mutex命名,使之整个计算机范围有效,这个名称应该在公司和应用程序中保持唯一。
2.获得和释放一个无争夺的Mutex需要几毫秒,时间比lock操作慢50倍。
3.自旋锁 SpinLock
SpinLock 在.NET 4.0引入,内部实现了微优化,可以减少高度并发场景的上下文切换。示例如下:
1 class ThreadInstance 2 { 3 public void Demo() 4 { 5 Thread thread = new Thread(() => Test()); 6 thread.Start(); 7 Test(); 8 Console.ReadKey(); 9 } 10 SpinLock spinLock = new SpinLock(); 11 bool Flag; 12 void Test() 13 { 14 bool gotLock = false; //释放成功 15 //进入锁 16 spinLock.Enter(ref gotLock); 17 { 18 Console.WriteLine(Flag); 19 Flag = !Flag; 20 } 21 if (gotLock) spinLock.Exit();//释放锁 22 } 23 }
执行结果如下,若注释掉代码行spinLock.Enter(ref gotLock);这段程序就会出现问题会打印两次False:
排他锁总结:
lock(内核锁) | |
本质 | 基于内核对象构造的锁机制,它发现资源被锁住时,请求进入排队等待,直到锁释放再继续访问资源 |
优点 | CPU利用最大化。 |
缺点 | 线程上下文切换损耗性能。 |
Mutex(互斥锁) | |
本质 | 多线程共享资源时,当一个线程占用Mutex对象时,其它需要占用Mutex的线程将处于挂起状态,直到Mutex被释放。 |
优点 |
可以跨应用程序边界对资源进行独占访问,即可以用同步不同进程中的线程。 |
缺点 | 牺牲更多的系统资源。 |
SpinLock(自旋锁) | |
本质 | 不会让线程休眠,而是一直循环尝试对资源的访问,直到锁释放资源得到访问。 |
优点 | 被阻塞时,不进行上下文切换,而是空转等待。对多核CPU而言,减少了切换线程上下文的开销。 |
缺点 | 长时间的循环导致CPU的浪费,高并发竞争下,CPU的损耗严重。 |
非排他锁 SemaphoreSlim&ReaderWriterLockSlim
1.信号量 SemaphoreSlim
信号量(SemaphoreSlim)类似于一个阀门,只允许特定容量的线程进入,超出容量的线程则不允许再进入只能在后面排队(先到先进)。容量为1的信号量与Mutex或lock相似,但是信号量与线程无关,任何线程都可以释放,而Mutex和lock,只有获得锁的线程才可以释放。
下面示例5个线程同时请求但只有3个线程可以同时访问:
1 class Program 2 { 3 /// <summary> 4 /// 声明信号量,容量3 5 /// </summary> 6 static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3); 7 static void Main(string[] args) 8 { 9 for (int i = 0; i < 5; i++) 10 { 11 new Thread(Enter).Start(i); 12 } 13 Console.ReadKey(); 14 } 15 static void Enter(object id) 16 { 17 Console.WriteLine("准备访问:{0}", id); 18 semaphoreSlim.Wait(); 19 //只有3个线程可以同时访问 20 { 21 Console.WriteLine("开始访问:{0}", id); 22 Thread.Sleep(1000 * (int)id); 23 Console.WriteLine("已经离开:{0}", id); 24 } 25 semaphoreSlim.Release(); 26 } 27 }
信号量可限制并发处理,防止太多线程同时执行特定代码。这个类有两个功能相似的版本:Semaphore和SemaphoreSlim。后者是.NET 4.0引入的,进行了一些优化,以满足并行编程的低延迟要求。SemaphoreSlim适用于传统多线程编程,因为它可以再等待时指定一个取消令牌。然而它并不适用于进程间通信。Semaphore在调用WaitOne或Release时需要消耗约1毫秒时间,而SemaphoreSlim的延迟时间只有前者1/4。
2.读/写锁 ReaderWriterLockSlim
一些资源访问,当读操作很多而写操作很少时,限制并发访问并不合理,这种情况可能发生在业务应用服务器,它会将常用的数据缓存在静态域中,用以加块访问速度。使用ReaderWriterLockSlim类,可以在这种情况中实现锁的最大可用性。
ReaderWriterLockSlim在.NET 3.5引入,目的是替换ReaderWriterLock类。两者功能相似,但后者执行速度要慢好几倍,且本身存在一些锁升级处理的设计缺陷。与常规锁(lock)相比,ReaderWriterLockSlim执行速度仍然要慢一倍。
下面示例,有3个线程不停的获取链表内元素总个数,同时有2个线程每个1秒钟向链表添加随机数:
1 class Program 2 { 3 static ReaderWriterLockSlim readerWriter = new ReaderWriterLockSlim(); 4 static List<int> Items = new List<int>(); 5 static void Main(string[] args) 6 { 7 for (int i = 0; i < 3; i++) 8 { 9 new Thread(Read).Start(); 10 } 11 new Thread(Write).Start("A"); 12 new Thread(Write).Start("B"); 13 Console.ReadKey(); 14 } 15 static void Read() 16 { 17 while (true) 18 { 19 readerWriter.EnterReadLock();//进入读取模式锁定状态 20 { 21 Console.WriteLine("Items总数:{0}", Items.Count); 22 Thread.Sleep(2000); 23 } 24 readerWriter.ExitReadLock();//推出读取模式 25 } 26 } 27 static void Write(object id) 28 { 29 while (true) 30 { 31 int newNumber = GetRandNum(100); 32 readerWriter.EnterWriteLock();//进入写入模式锁定状态 33 { 34 Items.Add(newNumber); 35 } 36 readerWriter.ExitWriteLock();//推出写入模式 37 Console.WriteLine("线程:{0},已随机数:{1}", id, newNumber); 38 Thread.Sleep(1000); 39 } 40 } 41 static Random random = new Random(); 42 static int GetRandNum(int max) 43 { 44 lock (random) 45 return random.Next(max); 46 } 47 }
ReaderWriterLockSlim类可以实现2种基本锁(读锁和写锁)。写锁是全局排他锁,读锁兼容其它的读锁。所以获得写锁的线程会阻塞其它试图获得读锁或写锁的线程。但是如果没有线程获得写锁,那么任意数量的线程可以同时获得读锁。
所有EnterXXX方法都有相应的TreXXX,它们可以接受Monitor.TryEnter风格的超时参数(如果资源争夺严重,那么很容易出现超时情况),ReaderWriterLockSlim也提供了相应的方法为TryEnterReadLock,TryEnterWriteLock。
发送信号(ManualResetEvent、AutoResetEvent、CountdownEvent和Barrier)
发送信号包括ManualResetEvent(Slim)、AutoResetEvent、CountdownEvent和Barrier。
前三个就是所谓的事件等待处理器(event wait handles,于C#事件无关)。同时ManualResetEvent(Slim)和AutoResetEvent继承自EventWaitHandle类,它们从基类继承了所有的功能。
1.AutoResetEvent
AutoResetEvent就像验票口,插入一张票据则只允许一人通过,当一个线程调用WaitOne会在验票口等待或阻塞,调用Set方法则插入一张票据。如果多个线程调用WaitOne则会在验票口进行排队,票据可以来自于任意线程。
1 class Program 2 { 3 static AutoResetEvent autoReset = new AutoResetEvent(false);//声明一个验票口 4 static void Main(string[] args) 5 { 6 new Thread(Waiter).Start(); 7 Thread.Sleep(1000); 8 autoReset.Set();//生成票据 9 Console.ReadKey(); 10 } 11 static void Waiter() 12 { 13 Console.WriteLine("等待");//线程在此等待,直到票据产生 14 autoReset.WaitOne(); 15 Console.WriteLine("通知"); 16 } 17 }
2.ManualResetEvent
ManualResetEvent的作用像是一扇大门,调用Set可以打开大门,使任意线程可以调用WaitOne,然后获得允许进入大门的权限。调用Reset,则可以关闭大门。在已经关闭的大门上调用WaitOne的线程会进入阻塞状态,当大门再次打开时这些线程会释放。
1 class Program 2 { 3 4 static ManualResetEvent manualReset = new ManualResetEvent(false);//声明一个闸门 5 static void Main(string[] args) 6 { 7 new Thread(Waiter).Start(); 8 new Thread(Waiter).Start(); 9 Thread.Sleep(2000); 10 manualReset.Set();//打开门 11 manualReset.Reset();//关闭门 12 new Thread(Waiter).Start(); 13 Thread.Sleep(2000); 14 manualReset.Set();//打开门 15 Console.ReadKey(); 16 } 17 static void Waiter() 18 { 19 Console.WriteLine("等待");//线程在此等待,直到大门打开 20 manualReset.WaitOne(); 21 Console.WriteLine("通知"); 22 } 23 }
3.CountdownEvent
CountdownEvent允许等待多个线程。它的作用像是计数器,计数器设置一个计数总量,多个线程调用Signal的次数达到计数总量时,调用WaitOne的线程将被释放(不依赖于操作系统且优化了自旋结构,速度要比前两者快50倍)。
1 class Program 2 { 3 static CountdownEvent countdownEvent = new CountdownEvent(3);//声明一个计数器,总量3 4 static void Main(string[] args) 5 { 6 new Thread(Demo).Start("A"); 7 new Thread(Demo).Start("B"); 8 new Thread(Demo).Start("C"); 9 countdownEvent.Wait();//阻塞,直至Signal调用了3次 10 Console.WriteLine("所有子线程都经过了登记"); 11 Console.ReadKey(); 12 } 13 static void Demo(object o) 14 { 15 Console.WriteLine("线程:{0},已登记", o); 16 Thread.Sleep(2000); 17 countdownEvent.Signal(); 18 } 19 }
4.Barriet
Barriet类可以实现一个线程执行屏障,允许多个线程在同一时刻会合(如下图所示),这个类执行速度很快非常高效,基于Wait,Pulse和自旋锁实现。
Barriet类使用步骤:
1.创建它的实例,指定参与会合的线程数量,通过调用AddParticipants和RemoveParticipants修改此值。
2.当需要会合时,在每个线程上调用SignalAndWait。
1 class Program 2 { 3 static Barrier barrier = new Barrier(3);//初始化为3 4 static void Main(string[] args) 5 { 6 new Thread(Speak).Start(); 7 new Thread(Speak).Start(); 8 new Thread(Speak).Start(); 9 Console.ReadKey(); 10 } 11 static void Speak() 12 { 13 for (int i = 0; i < 5; i++) 14 { 15 Console.Write(i + " "); 16 //进入阻塞状态,当调用3次后,”会合统一“执行,然后重新开始计数,这样可以让各个线程步调一致执行。 17 barrier.SignalAndWait(); 18 } 19 } 20 }
线程本地存储
上面主要是解决线程并发访问数据的问题。但有时候也需要保持数据隔离,以保证每个线程都拥有自己的副本。本地变量就可以实现这个目标,但是它们只适用于保存临时数据。解决方案是使用线程本地存储。
线程本地存储有三种方式:ThreadStatic、ThreadLocal<T>和LocalDataStoreSlot(线程槽)
1.ThreadStatic
实现线程本地存储的最简单的方法时使用ThreadStatic静态修饰符,是每个线程都可以使用独立的变量副本,但是ThreadStatic不适用于实力域,也不适用于域的对象初始化。它们只能在调用静态高走方法的线程上执行一次。如果需要处理实例域,那么更适合适用ThreadLocal<T>
1 class Program 2 { 3 [ThreadStatic] 4 private static string code = "string"; 5 static void Main(string[] args) 6 { 7 //在主线程设置只能被主线程读取,其它线程无法访问 8 //若在子线程中设置,则只有子线程可以访问,其他线程无法访问 9 Thread thread = new Thread(() => 10 { 11 code = "object"; 12 Console.WriteLine("子线程中读取数据:{0}", code); 13 }); 14 thread.Start(); 15 Console.WriteLine("主线程中读取数据:{0}", code); 16 Console.ReadKey(); 17 } 18 }
2.ThreadLocal<T>
ThreadLocal<T>支持创建静态域和实例域的线程本地存储,并且允许默认值。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ThreadLocal<string> threadLocal = new ThreadLocal<string>(()=>"string"); 6 //在主线程设置只能被主线程读取,其它线程无法访问 7 //若在子线程中设置,则只有子线程可以访问,其他线程无法访问 8 //threadLocal.Value = "object"; 9 Thread thread = new Thread(() => 10 { 11 threadLocal.Value = "object"; 12 Console.WriteLine("子线程中读取数据:{0}", threadLocal.Value); 13 }); 14 15 thread.Start(); 16 //主线程中读取数据 17 Console.WriteLine("主线程中读取数据:{0}", threadLocal.Value); 18 Console.ReadKey(); 19 } 20 }
3.LocalDataStoreSlot
使用Thred类的两个方法GetData和SetData。这两个方法会将数据存储在线程独有的“插槽”中。需要使用LocalDataStoreSlot对象来获得这个存储插槽。所有线程都可以使用相同的插槽。而创建一个命名插槽,整个应用程序将共享这个插槽。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //在主线程封装内存槽,2种情况 6 //1.命名槽位 7 LocalDataStoreSlot localDataStoreSlot = Thread.AllocateNamedDataSlot("demo"); 8 //2.未命名槽位 9 //LocalDataStoreSlot localDataStoreSlot = Thread.AllocateDataSlot(); 10 //在主线程设置槽位,使此objcet类型数据智能被主线程读取,其它线程无法访问 11 //若在子线程中设置,则只有子线程可以访问,其他线程无法访问 12 Thread.SetData(localDataStoreSlot, "object"); 13 Thread thread = new Thread(() => 14 { 15 Console.WriteLine("子线程中读取数据:{0}", Thread.GetData(localDataStoreSlot)); 16 }); 17 Console.WriteLine("主线程中读取数据:{0}", Thread.GetData(localDataStoreSlot)); 18 thread.Start(); 19 Console.ReadKey(); 20 } 21 }
线程回调模拟
线程模拟回调函数的方式如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Action callback = () => 6 { 7 Console.WriteLine("回调方法-ok"); 8 }; 9 ThreadBeginInvoke(Test, callback); 10 Console.ReadKey(); 11 } 12 static void Test() 13 { 14 Console.WriteLine("执行方法-ok"); 15 } 16 static void ThreadBeginInvoke(ThreadStart method, Action callback) 17 { 18 ThreadStart threadStart = new ThreadStart(() => 19 { 20 method.Invoke(); 21 callback.Invoke(); 22 }); 23 Thread thread = new Thread(threadStart); 24 thread.Start(); 25 } 26 }
大致的思路如此,根据所需自行封装。
线程异常处理
在线程创建时任何生效的try/catch/finally语句块在线程开始执行后都与线程无关,线程的异常处理要在线程调用方法内部。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 try 6 { 7 new Thread(Test).Start(); 8 } 9 catch (Exception ex) 10 { 11 //代码永远不会运行到这里 12 Console.WriteLine(ex.Message); 13 } 14 Console.ReadKey(); 15 } 16 static void Test() 17 { 18 //要在方法内部捕获异常 19 try 20 { 21 throw null; 22 } 23 catch (Exception ex) 24 { 25 //记录日志等... 26 } 27 } 28 }
定时器&MemoryBarrier
1.定时器
.NET 提供了4种定时器
多线程计时器:
1:System.Threading.Timer
2:System.Timers.Timer
特殊目的的单线程计时器:
3:System.Windows.Forms.Timer(Windows Forms Timer)
4:System.Windows.Threading.DispatcherTimer(WPF timer);
有关定时器详细介绍(个人觉得不错):
https://www.cnblogs.com/LoveJenny/archive/2011/05/28/2053697.html
2.内存屏障 MemoryBarrier
编译器、CLR或者CPU可能重新排序了程序指令,以此提高效率。同时引入缓存优化导致其他的线程不能马上看到变量值的更改。lock可以满足需要,但是竞争锁会导致阻塞并且带来上下文切换和调度等开销,为此.NET 提供了非阻塞同步构造内存栅栏的概念。
有关MemoryBarrie详细介绍(个人觉得不错):
https://www.cnblogs.com/LoveJenny/archive/2011/05/29/2060718.html
允许我偷个懒 - -、 反正别人写的也不错。
线程组成要素
1.线程内核对象(thread kernel object)
包含对线程描述的属性。还包含线程上下文(thread context)。上下文还包含CPU寄存器集合的内存块(x86、x64、ARM CPU架构,线程上下文分别使用约700、1240、350字节的内存)。
2.线程环境块(thread environment block,TEB)
TEB消耗一个内存页(x86、x64和ARM CPU中是4KB)。包含线程异常处理链首(head)。线程进入的每个try块都在链首插入一个节点(node);线程退出try块时在链首中删除对应节点。
3.用户模式栈(user mode stack)
用户模式栈存储传给方法的局部变量和实参。还包含一个地址用于指出当前方法返回时线程继续执行位置(Winodws默认为用户模式栈保留1MB地址空间,在线程实际需要时才会提交物理内存)。
4.DLL线程连接(attach)和线程分离(detach)通知
进程中线程在创建和终止时,都会调用线程中加载的所有非托管DLL的DllMain方法,并向该方法传递标记(DLL_THREAD_ATTACH或DLL_THREAD_DETACH)。有的DLL需要获取这些通知,为进程中创建/销毁的每个线程执行特殊的初始化或资源清理工作。
(C#和大多数托管编程语言生成的DLL没有DllMain函数。所以托管DLL不会受到标志通知,非托管DLL可以调用Win32 DisableThreadLibraryCalls函数来决定不理会这些通知)
线程性能开销
1.DLL线程链接与分离:目前随便一个进程就可能加载几百个DLL,每次开启和销毁一个线程这些函数都要调用一边,严重影响了进程中创建和销毁线程的性能。
2.线程上下文切换:单CPU计算机一次只做一件事情,所以Windwos必须在系统中的所有线程(逻辑CPU)之间共享物理CPU。
3.时间片切换:Windws只将一个线程分配给CPU.这个线程能运行一个“时间片”(量””,quantum)。时间片到期,winodws就上下文切换到另一个线程。每次上下文切换都要求Windws执行以下操作:
1.将CPU寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。
2.从现有线程集合中选出一个线程供调度。如果线程由另一个进程拥有,windows在开始执行任何代码或者接触任何数据之前,还必须切换CPU获取到虚拟地址空间。
3.将所选上下文结构中的值加载到CPU的寄存器中。上下文切换完成后,CPU执行所选的线程,直到它的时间片到期。然后发生上下文切换。Windows大约每30米毫秒执行一次上下文切换。上下文切换是纯开销;不会换取任何内存和性能上的收益。
注意:
1.执行上下文切换所需的时间取决于CPU架构和速度。而填充CPU缓存所需的时间取决于系统中运行的应用程序、CPU缓存大小及其它因素。要构建高性能应用程序和组件,尽量避免上下文切换。
2.外垃圾回收时,CLR必须挂起所有线程,遍历他们的栈来查找跟以便对堆中的对象进行标记,有的对象在压缩期间发生了移动,所以要更新它的根,再回复所有线程。所以减少线程数量会提升垃圾回收的性能。
3.Winodws为每个进程提供了该进程专用的线程来增强系统的可靠性和影响力。在Winodws中,进程十分昂贵,创建一个进程通常需要花几秒时间,必须分配大量内存,这些内存必须初始化,EXE和DLL文件必须从磁盘加载。相反在Winodws中创建线程则十分廉价。
结语
关于线程(Thread)你想知道应该都在这里了。
一个字:好累!
线程是一个很复杂的概念,延伸出来的知识点都需要有所了解,否则写出的程序会出大问题(维护成本很高)。
参考文献
CLR via C#(第4版) Jeffrey Richter
C#高级编程(第7版) Christian Nagel
C#高级编程(第10版) C# 6 & .NET Core 1.0 Christian Nagel
C# 经典实例 C# 6.0 &.NET Framework 4.6 Jay Hilyard
果壳中的C# C#5.0权威指南 Joseph Albahari
------------------------------------江湖救急 分割线----------------------------------------
求两本书要中文版PDF(不知道目前有没有卖纸质的?),哪位网友能否分享下,好人一生平安在此表示感谢!