线程(thread)

线程概述

线程是一个独立处理的执行路径。每一个线程都运行在一个操做系统进程中,这个进程是程序执行的独立环境。在单线程中进程的独立环境内只有一个线程运行,因此该线程具备独立使用进程资源的权利。在多线程程序中,在进程中有多个线程运行,因此它们共享同一个执行环境。html

 

基础线程(thread)

使用Thread类能够建立和控制线程,定义在System.Threading命名空间中:node

 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毫秒),用于模拟并发性。而在多核/多处理器主机上线程却可以真正实现并行执行(分别由计算机上其它激活处理器完成)。windows

 

线程经常使用方法

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;
线程优先级经过 Priority属性设置, Priority属性是一个 ThreadPriority枚举
AboveNormal 高于正常
BelowNormal 低于正常
Highest 最高
Lowest 最低
Normal 正常
普通线程的优先级默认为Normal,主线程和其它工做线程(默认优先级)优先级相同,交替进行。
注意:线程优先级跟线程执行的前后顺序无关,而是肯定其激活线程在操做系统中的相对执行时间的长短(肯定分配”时间片“的长短,即线程执行时间长短)。

ThreadStart&ParameterizedThreadStart

Thread重载的其它四种构造函数须要带入特殊对象,分别是ThreadStartParameterizedThreadStart类。

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     }

执行结果(执行结果不固定):

由于ThreadStartParameterizedThreadStart委托,因此咱们也能够把符合要求的自定义委托或者内置委托进行转换带入构造函数。例如:

 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     }

上述案例对比DemoOneDemoTwo的执行结果咱们能够获得:

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.EnterMonitor.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的信号量与Mutexlock类似,可是信号量与线程无关,任何线程均可以释放,而Mutexlock,只有得到锁的线程才能够释放。

下面示例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     }

信号量可限制并发处理,防止太多线程同时执行特定代码。这个类有两个功能类似的版本:SemaphoreSemaphoreSlim。后者是.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也提供了相应的方法为TryEnterReadLockTryEnterWriteLock

 

发送信号(ManualResetEvent、AutoResetEvent、CountdownEvent和Barrier)

发送信号包括ManualResetEvent(Slim)AutoResetEventCountdownEventBarrier

前三个就是所谓的事件等待处理器(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类的两个方法GetDataSetData。这两个方法会将数据存储在线程独有的“插槽”中。须要使用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寄存器集合的内存块(x8六、x6四、ARM CPU架构,线程上下文分别使用约700、1240、350字节的内存)。

2.线程环境块(thread environment block,TEB)

TEB消耗一个内存页(x8六、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(不知道目前有没有卖纸质的?),哪位网友可否分享下,好人一辈子平安在此表示感谢!

                                

相关文章
相关标签/搜索