【C# 线程】开篇 线程
阅读原文时间:2023年07月09日阅读:3

线程主要学习什么,通过一个月的摸索、终于总结出来了:
学习Thread类涉及到
学习Thread类涉及到
线程单元状态: ApartmentState、GetApartmentState\SetApartmentState
内存屏障:VolatileWrite()、VolatileRead()、MemoryBarrier
线程共享变量原子操作:interLoacked() 具有原子性、可见性、有序性。
线程本地变量的存储:LocalThread、ThreadStatic、Lazy、数据曹(AllocateDataSlot()、AllocateNamedDataSlot()、GetData()、SetData()、LocalDataStoreSlot类)
可变内存操作:Volatile 类 \VolatileRead()\VolatileWrite() 具有可见性和原子性、CAS机制
线程同步问题:同步锁(spinlock、interlocked原子锁、)、临界区、临界资源(BeginCriticalRegion() 。 EndCriticalRegion())

内核模式和用户模式

阻塞
线程自旋 :spinwait
线程时间片转让: sleep(0)、sleep(1)、yeild ()
线程状态:ThreadState枚举
线程类型:前后台线程
线程状态的操作:jion()\sleep()\Interrupt()
线程优先级:程序员可控制的5个Priority ThreadPriority枚举

线程亲和力:BeginThreadAffinity ()和 EndThreadAffinity()
应用域:GetDomain()、GetDomainID()
进程:GetCurrentProcessorId()
线程上下文:ExecutionContext()、SynchronizationContext类
线程安全:原子性、有序性、可见性
线程区域文化:CurrentUICulture\CurrentCulture
线程池:IsThreadPoolThread

我们首先回顾进程的两个基本属性:

  • 进程使一个可拥有资源的独立单位
  • 进程同时又是一个可以独立调度和分派的基本单位。

正是由于这两个基本属性,才使进程成为一个能独立运行的基本单位,从而构成了进程并发执行的基础。

由于进程是一个资源的拥有者,因而在进程的创建、撤销、和切换的过程中,系统必须为之付出较大的时空开销。为了解决这个问题,不少操作系统的学者们想到:将进程的两个属性分开,由操作系统分开处理。即对作为调度和分派的基本单位,不同时作为独立分配资源的单位,以使之轻装运行;而对拥有资源的基本单位,又不频繁地对之进行切换,在这种思想的指导下,产生了线程的概念。

线程引入的原因: 为了减少程序并发执行所付出的时空开销,使os具有更好的并发性。

线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。

CLR线程完全等价于windows线程。System.Environment类公开了CurrentMangagedThreadID属性,返回的是线程的CLR ID。

而System.Diagnostics.ProcessThread类公开了Id属性,返回同一个线程的Windows ID。

备注:如果想P/Invoke本地代码,而且代码必须使用当前物理操作系统的线程来执行,那么应该调用System.Threading.Thread的静态BeginThreadAffinity方法。BeginThreadAffinity就是告诉CLR不要切换线程。线程不再需要使用物理操作系统线程运行时,可调用Thread的EndThreadAffinity方法来通知CLR。

当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程,它是前台线程。

前台线程默认是MTA

1、在CLR中,线程要么是前台线程,要么就是后台线程。当一个进程的所有前台线程停止运行时,CLR将强制终止仍在运行的任何后台线程,不会抛出异常。

2、在C#中可通过Thread类中的IsBackground属性来指定是否为后台线程。在线程生命周期中,任何时候都可从前台线程变为后台线程。线程池中的线程默认为后台线程

3、在C#中,Thread类默认创建的是前台线程,通过线程池(后面会讲到)创建的线程都是后台线程。

4、前台线程与后台线程唯一的区别是后台线程不会阻止进程终止。

5、尽量避免使用前台线程。

Windows支持6个进程优先级类(priority class) :Idle,Below NormalAbove NormalHight(非必要不用)Realtime(最好不要用)。默认的Normal是最常用的优先级类。

Windows支持7个相对线程优先级IdleLowestBelow NormalNormalAbove NormalHighestTime-Critical

优先级类和优先级合并构成一个线程的“基础优先级”(base priority)。注意,每个线程都有一个动态优先级(dynamic priority)。线程调度器根据这个优先级来决定要执行哪个线程。最初线程的动态优先级和它的基础优先级是相同的。

线程优先级:windows 操作系统的线程优先级分为 0~31级。应用程序开发人员永远不直接处理这些优先级。只要使用相对优先级就够了。

系统将进程的优先级类和其中的一个线程的相对优先级映射成一个优先级(0~31)。下表总结了进程的优先级类和线程的相对优先级与优先级(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

例如,Normal进程中的一个Normal线程的优先级是8。由于大多数进程都是Normal优先级,大多数线程也是Normal优先级,所以系统中大多数线程的优先级都是8。

注意,表中没有值为0的线程优先级。这是因为0优先级保留给零页线程了,系统启动时会创建一个特殊的零页线程(zero page thread)。该线程的优先级是0,而且是整个系统唯一优先级为0的线程。在没有其他线程需要“干活儿”的时候,零页线程将系统RAM的所有空闲页清零。系统不允许其他线程的优先级为0。而且,以下优先级也不可获得:17,18,19,20,21,27,28,29或者30。以内核模式运行的设备驱动程序才能获得这些优先级;用户模式的应用程序不能。还要注意,Realtime优先级类中的线程优先级不能低于16。类似地,非Realtime的优先级类中的线程优先级不能高于15。

而在C#程序中,可更改线程的相对优先级,需要设置ThreadPriority属性,可设置为ThreadPriority枚举类型的五个值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest。CLR为自己保留了IdleTime-Critical优先级,程序中不可设置。

windows使用32个线程优先级,分成三类:

线程共包括以下 5 种状态:

1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。

2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
  • (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

一般认为,线程有五种状态:

新建(new 对象 ) 、就绪(等待CPU调度)、运行(CPU正在运行 Running)、阻塞、死亡(对象释放)。

System.Threading.Thread.ThreadState属性定义了执行时线程的状态。

Unstarted:

状态在公共语言运行时中创建的线程最初处于 Unstarted 状态, 线程 Unstarted 通过调用从状态转换到 Running 状态 Thread.Start 。 线程由于调用 Unstarted 而离开 Start状态后,它将无法再返回到 Unstarted 状态。也就是说线程只能start一次

Running状态:

一个线程执行新的一个线程的thread.Start();方法直到新线程开始运行,方法才会返回。

WaitSleepJoin状态::

(1)线程调用 Sleep()     
(2)线程对另一个对象调用 Monitor.Wait、autoResetEvent.waitone()、ManualResetEvent.waitone()。     
(3)A线程执行B. Join(),A者线程处于WaitSleepJoin状态,B线程处于running。

(4)另一个线程调用 Interrupt 线程如果不处理线程,线程就会结束  。当线程处于进入WaitSleepJoin状态或解除WaitSleepJoin状态时,会发生上下文切换,这就带来了昂贵的消耗。

StopRequested状态:正在请求线程停止。 这仅用于内部。

Stopped状态

线程终止。

线程除抛出的异常未处理。取消异常 、中断异常等

Background状态:它指示线程是在后台运行还是在前台运行

过时:

SuspendRequested状态:另一个线程调用该线程的 Suspend()

Suspended状态:线程响应 Suspend 请求。

AbortRequested状态:另一个线程调用 Abort

Aborted状态:线程状态包括 AbortRequested 并且该线程现在已死,但其状态尚未更改为 Stopped

另一个线程调用 Resume  过时Running状态

简单概述以下:

1、线程从创建到线程终止,它一定处于其中某一个状态。当线程被创建时,它处在Unstarted状态.

2、Thread类的Start() 方法将使线程状态变为Running状态,线程将一直处于这样的状态,除非我们调用了相应的方法使其挂起、阻塞、销毁或者自然终止。

3、如果线程被挂起,它将处于Suspended状态,除非我们调用resume()方法使其重新执行,这时候线程将重新变为Running状态。

4、一旦线程被销毁或者终止,线程处于Stopped状态。处于这个状态的线程将不复存在,正如线程开始启动,线程将不可能回到Unstarted状态。

5、线程还有一个Background状态,它表明线程运行在前台还是后台。

6、在一个确定的时间,线程可能处于多个状态。据例子来说,一个线程被调用了Sleep而处于阻塞,而接着另外一个线程调用Abort方法于这个阻塞的线程,这时候线程将同时处于WaitSleepJoin和AbortRequested状态。一旦线程响应转为阻塞或者中止,当销毁时会抛出ThreadAbortException异常。

以上状态对应Thread线程的操作方法:

Start()方法:此时的线程状态为running。开始运行,调用该方法,知道线程开始运行,才返回。

Join()方法:阻塞直到某个线程终止时为止,此时的线程状态为:WaitSleepJoin。
Sleep()方法:将当前线程阻塞指定的毫秒数,Sleep()使得线程立即停止执行,线程将不再得到CPU时间。此时的线程状态为:WaitSleepJoin

线程的暂停与恢复:此时的线程状态为WaitSleepJoin。 C#已经弃用了不安全的Suspend()和Resume(),现在实现线程的暂停与恢复可以通过AutoResetEvent和ManualResetEvent这两个阻塞事件类来实现。
Interrupt()方法:其他线程中对目标线程调用interrupt()方法,目标线程将自身的中断状态位为true ,线程会不时地检测这个中断标示位,以判断线程是否应该被中断。并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。如果该异常被捕获,线程就不会终止。如果线程未阻塞就可能在不中断的情况下运行完成线程。

Thread thread = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
object o = new();
try
{

            Thread.Sleep(500);//如果该线程内没有阻塞语句例如 Thread.Sleep(500);那么 thread.Interrupt();将不影响线程执行  
        Console.WriteLine(Thread.CurrentThread.ThreadState);

        //如果处于

    }      ///如果捕获 Thread.Sleep(1000); 那么其他线程运行thread.Interrupt();将起不到终止线程的效果。所以不要什么异常都捕获  
           ///将会设置该线程的中断状态位为true ,线程会不时地检测这个中断标示位,以判断线程是否应该被中断。并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

    catch (ThreadInterruptedException ex)  
    {

        Console.WriteLine($"第{i}次中断{Thread.CurrentThread.ThreadState}");  
    }

}

});
Console.WriteLine(thread.ThreadState);
thread.Start();
Console.WriteLine(thread.ThreadState);
thread.Interrupt();

Task.Run(() =>
{

Thread.Sleep(1000); thread.Interrupt();

});
thread.Join();
Console.ReadKey();

Suspend()方法:

过时  因为容易造成死锁,调用Suspend()方法 停止线程运行,不是及时的,它要求公共语言运行时必须到达一个安全点,线程将不再得到CPU时间。
但是可以调用Suspend()方法使得另外一个线程暂停执行。对已经挂起的线程调用Thread.Resume()方法会使其继续执行。不管使用多少次Suspend()方法来阻塞一个线程,只需一次调用Resume()方法就可以使得线程继续执行。
尽可能的不要用Suspend()方法来挂起阻塞线程,因为这样很容易造成死锁。假设你挂起了一个线程,而这个线程的资源是其他线程所需要的,会发生什么后果。
因此,我们尽可能的给重要性不同的线程以不同的优先级,用Thread.Priority()方法来代替使用Thread.Suspend()方法。
Resume()方法:过时 恢复挂起 配合suspend一起使用的
Abort()方法:

过时,因为它会破坏同步锁中代码的原子逻辑,破坏锁的作用。.NET 5(包括 .NET Core)及更高版本不支持 Thread.Abort 方法,CancellationToken 已成为一个安全且被广泛接受的 Thread.Abort 替代者。如果线程已经在终止 。Thread.Abort()方法使得系统悄悄的销毁了线程而且不通知用户。一旦实施Thread.Abort()操作,该线程不能被重新启动。调用了这个方法并不是意味着线程立即销毁,因此为了确定线程是否被销毁,我们可以调用Thread.Join()来确定其销毁。对于A和B两个线程,A线程可以正确的使用Thread.Abort()方法作用于B线程,但是B线程却不能调用Thread.ResetAbort()来取消Thread.Abort()操作。
ResetAbort()方法:

只有具有适当权限的代码才能调用此方法。

当调用 Abort 终止线程时,系统将引发 ThreadAbortException 。 ThreadAbortException 是一个特殊的异常,可由应用程序代码捕获,但会在 catch 块结束时重新引发,除非 ResetAbort 调用。 ResetAbort 取消要中止的请求,并阻止 ThreadAbortException 终止线程。

ThreadAbortException有关演示如何调用方法的示例,请参阅 ResetAbort

阻塞状态指线程处于等待状态。当线程处于阻塞状态时,会尽可能少占用 CPU 时间。

当线程从运行状态(Runing)变为阻塞状态时(WaitSleepJoin),操作系统就会将此线程占用的 CPU 时间片分配给别的线程。当线程恢复运行状态时(Runing),操作系统会重新分配 CPU 时间片。

分配 CPU 时间片时,会出现上下文切换。

有两个线程状态枚举: System.Diagnostics.ThreadStateSystem.Threading.ThreadState 。 只有几个调试方案才会对线程状态枚举感兴趣。 因此,始终不应在代码中使用线程状态来同步线程活动。

前面说过,一个应用程序域中可能包括多个上下文,而通过CurrentContext可以获取线程当前的上下文。

CurrentThread是最常用的一个属性,它是用于获取当前运行的线程。

只有操作系统才能切换线程、挂起线程,因此阻塞线程是由操作系统处理的,这种方式被称为内核模式(kernel-mode)。

Sleep()Join() 等,都是使用内核模式来阻塞线程,实现线程同步(等待)。

内核模式实现线程等待时,出现上下文切换。这适合等待时间比较长的操作,这样会减少大量的 CPU 时间损耗。

如果线程只需要等待非常微小的时间,阻塞线程带来的上下文切换代价会比较大,这时我们可以使用自旋,来实现线程同步,这一方法称为用户模式(user-mode)。

cpu加载当前线程上下文》执行计算》保存当前线程上下文》运行另外一个线程

ManagedThreadId是确认线程的唯一标识符,程序在大部分情况下都是通过Thread.ManagedThreadId来辨别线程的。而Name是一个可变值,在默认时候,Name为一个空值 Null,开发人员可以通过程序设置线程的名称,但这只是一个辅助功能。

1.如果线程要一直运行(如Word的拼写检查器线程),就应使用Thread类创建一个线程。入池的线程只能用于时间较短的任务。

什么是线程安全?线程安全是怎么完成的(原理)?
线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。

在并发编程中,线程安全问题的本质其实就是 原子性、有序性、可见性;接下来主要围绕这三个问题进行展开分析其本质,彻底了解可见性的特性。

  • 原子性 和数据库事务中的原子性一样,满足原子性特性的操作是不可中断的,要么全部执行成功要么全部执行失败。C#中解决原子性用C#中解决原子性用 volatile关键字、volatile类。1、Interlocked.Increment(ref a)取代a++、Interlocked.Decrement(ref a)取代i--、给代码加锁,如果在 lock 锁内抛出异常,将会影响锁的原子性,这个时候就需要结合回滚机制来进行实现。

  • 有序性(同步性) 编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的,重排序可能会导致多线程程序出现内存可见性问题。C#中解决有序性  内存屏障、 加锁。

  • 可见性 多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值。C#中解决可见性用 volatile类。

为了彻底了解这三个特性,我们从两个层面来分析,第一个层面是硬件层面、第二个层面是JMM层面

详细请查看:https://www.cnblogs.com/barrywxx/p/10135068.html

为了实现线程对组件对象的安全访问。com组件提供了两种模式并且要求线程提供对应的线程模型来访问,这个就是套间(单元)的由来。

1.线程模型是干嘛用的?
解决”多个线程”“同时”调用你的COM组件的并发控制。客户没有你的COM的源代码,它不知道你的组件是怎么写的,是不是线程安全的(是否用CriticalSection或Mutex保护了临界资源),所以要有一种机制来声明组件的线程安全性,你开发时指定了组件的线程模型,客户端一看,哦,它就知道该怎么写调用的代码。
2.啥时候用操心线程模型?
全看客户端是单线程还是多线程,单线程不用操心,怎么调都没事,多线程就来事了,跨线程调用时就要考虑。
3.线程模型的一堆概念都是哪跟哪啊?
首先一分为二:客户端和组件。客户端用CoInitializeEx进入一种套间,Apartment、Free是指的客户端进入的套间种类;组件要向注册表写入自己兼容什么样的客户端套间,是Apartment,还是Free,还是两种都兼容(Both)。
4.客户端的线程套间和组件的不一致了咋办,难道我调个COM组件还得查注册表看看兼容什么线程模型?
不一致时以组件为主。客户端建的是Apartment,组件兼容Free,那COM背地里会在客户端建一个Free套间,把组件放进去。反之,会建一个Apartment套间把组件放进去。总之以组件为主,这是关键,只有这样,你才不用关心组件的线程安全性,COM服务替你在后台办妥了。
5.听说过列集这个名词,是什么啊,啥时候用?
记死了,跨线程调用组件就得列集,没的商量。传出接口的线程列集,使用接口的线程散列。列集说白了就是不让你直接调用组件的接口,而是调用接口的代理。COM服务在中间插一杠子,干啥,实现COM线程安全那一揽子事呗,它不拦截你的调用它怎么实现啊,所以就给你个代理,所以就列集了呗。

在多线程编程要用到线程局部变量的存储。相当于线程级别的static

为了确保在线程中声明特定类型的变量,在每个线程中的值都是唯一的,不受到其他线程对该变量读写的影响。 也就是俗称的线程本地存储 (TLS),可用于存储对线程和应用程序域唯一的数据。

例如:主线程中声明了变量A ,只能由主线程进行读取和写入。子线程虽然可以使用变量A(相当于复制一个A,可以对该变量进行读写),却无法读取和写入主线变量中A的值。

.net提供了3种方式:

1、线程相对静态字段 相对于线程的静态字段(ThreadStatic

2、数据槽  LocalDataStoreSlot

3、ThreadLocal

在多线程编程要用到内存模型

1、内存操作重新排序
2、可变字段 volatile,可以限制内存重排,从内存直接读取数据。
3、原子性:在
C# 中,值不一定以原子方式写入内存。支持原子性数据类型有:
reference,bool,char,byte,sbyte,short,ushort,unit,int,float。不知此类型有long\double
因为他们是64位的,所有要看芯片类型,如果64位芯片那么久就支持原子性,32位不支持。
4、不可重新排序优化

同步基元分为用户模式和内核模式

用户模式:Iterlocked.Exchange(互锁)、SpinLocked(自旋锁)、易变构造(volatile关键字、`volatile`类、Thread.VolatitleRead|`Thread.VolatitleWrite`)、MemoryBarrier。

内核模式:event()、Semaphore、Mutex

同步又分为进程内线程同步,和进程之间同步。在进程内线程同步,轻量级锁(用户态)和重量级锁(内核锁)都可以使用。进程之间同步必须要用重量级锁。

轻量级锁:SpinLock, SpinWait, CountdownEvent, SemaphoreSlim, ManualResetEventSlim, Barrier

重量级锁:lock、mutex、Semaphores、event、Monitor

同步机制的四个准则:

  1. 空闲让进:屁话
  2. 忙则等待:屁话
  3. 有限等待:要防止饥饿现象
  4. 让权等待:进程不能进入临界区的时候要释放CPU

.NET 中线程同步的方式多的让人看了眼花缭乱,究竟该怎么理解?其实,抛开.NET 环境看线程同步,无非执行两种操作:      1. 互斥/加锁,目的是保证临界区代码操作的"原子性";


2. 信号灯操作,目的是保证多个线程按照一定顺序执行,如生产者线程要先于消费者线程执行。


.NET 中线程同步的类无非是对这两种方式的封装,目的归根结底都可以归结为实现互斥/加锁或者是信号灯这两种方式,只是它们的适用场合有所不同。

实现线程间同步4种常用方法:

1)临界区(Critical Section)

指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源(如打印机)又无法同时被多个 线程 访问的特性 。临界区只限制与同一进程的各个线程之间使用,C#中使用以下锁实现同进程里不同线程的同步。

  • lock 同步锁: 使用比较简单 lock(obj){ Synchronize part  };  只能传递对象,无法设置等待超时;通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则
    最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。

entry section 进去区 该区检查是否可以进去临界区
critical section  访问临界资源那段代码
exit section 退出区     将访问临界区的标志清除
remainder  section 剩余区。代码中的其余部分

  • InterLocked原子同步锁:  原子操作,提供了以线程安全的方式递增,递减,交换和读取值的方法;
  • Monitor同步锁: lock语句等同于Monitor.Enter() ,同样只能传递对象,无法设置等待超时,

2)互斥量(Mutex)

为协调共同对一个共享资源的单独访问而设计的。

3)信号量(Semaphores)

为控制一个具有有限数量用户资源而设计。

4)通知事件(Event)

通知事件对象可以通过通知操作的方式来保持线程的同步。c#中的AutoResetEvent 类ManualResetEvent 类属于通知事件。

线程内部同步

1)自旋锁 (SpinLock)

2)内存屏障

线程的空间开销主要来自:
1)线程内核对象(Thread Kernel Object)。每个线程都会创建一个这样的对象,它主要包含线程上下文信息,占用的内存在700字节左右。
2)线程环境
块(Thread Environment Block)。占用4KB内存。
3)用户模式栈(User Mode Stack),即线程栈。线程栈用于保存方法的参数、局部变量和返回值。每个线程栈占用1MB的内存。要用完这些内存很简单,写一个不能结束的递归方法
,让方法参数和返回值不停地消耗内存,很快就会发生OutOfMemoryException。
4)内核模式栈(Kernel Mode Stack)。当调用操作系统的内核模式函数
时,系统会将函数参数从用户模式栈复制到内核模式栈。会占用12KB内存。

线程的时间开销来自:
1)线程创建的时候,系统相继初始化以上这些内存空间。
2)接着CLR会调用所有加载DLL的DLLMain方法,并传递连接标志(线程终止的时候,也会调用DLL的DLLMain方法,并传递分离标志)。
3)线程上下文切换。一个系统中会加载很多的进程,而一个进程又包含若干个线程。但是一个CPU在任何时候都只能有一个线程在执行。为了让每个线程看上去都在运行,系统会不断地切换“线程上下文”:每个线程大概得到几十毫秒的执行时间片,然后就会切换到下一个线程了。

这个过程大概又分为以下5个步骤:
步骤1 进入内核模式。
步骤2 将上下文信息(主要是一些CPU 寄存器信息
)保存到正在执行的线程内核对象上。
步骤3 系统获取一个 Spinlock,并确定下一个要执行的线程,然后释放 Spinlock。如果下一个线程不在同一个进程内,则需要进行虚拟地址交换。
步骤4 从将被执行的线程内核对象上载入上下文信息。
步骤5 离开内核模式。
所以线程的创建和销毁是需要付出时间和空间的代价的,而微软为了防止我们开发者无节制的使用线程,就封装了线程池这种技术,简单说就是帮助我们开发者来管理线程,随着工作的完成,线程不会被销毁,而是回到线程池中,看别的工作会不会继续使用线程,而具体何时被销毁或者创建,由CLR自己的算法来决定,所以真实项目中,我们更多的应该考虑使用线程池来替代Thread,C#中线程池主要有ThreadPool和BackgroundWorker这两个类,使用也蛮简单的:

ThreadPool.QueueUserWorkItem(state =>
{
//todo
});
var bw = new BackgroundWorker();
bw.DoWork += (sender, e) =>
{
//todo
};

bw.RunWorkerAsync();

而ThreadPool和BackgroundWorker的区别在于:BackgroundWorker在WinForm和WPF中还提供了和UI线程交互的能力,而ThreadPool没有这种能力,BackgroundWorker的能力还包括:通知进度、完成回调、取消任务、暂停任务等功能。

Task是.NET4.5之后提供的线程的更高级的一种技术,虽然前面刚说了ThreadPool和BackgroundWorker比Thread更有优势,那么Task更是超越ThreadPool和BackgroundWorker更强大的概念。为线程池提供了更多的API可以调用,管理一个线程简直颠覆传统了

1、每一个进程都有一个CLR线程池,当该进程销毁的时候,该线程池就会注销。

2、 对于COM对象,入池的所有线程都是多线程单元(Multi-threaded apartment,MTA)线程。许多COM对象都需要单线程单元(Single -threaded apartment,STA)线程。

3、单个任务处理的时间比较短

4、需要处理的任务的数量大

5、不能给入池的线程设置优先级或名称。

详细请看 线程池章节

在 .NET Framework 4 之前,.NET 不提供内置方法在线程启动后以协作方式取消线程。 不过,从 .NET Framework 4 开始,可以使用 System.Threading.CancellationToken 来取消线程,就像使用它们取消 System.Threading.Tasks.Task 对象或 PLINQ 查询一样。 虽然 System.Threading.Thread 类不提供对取消标记的内置支持,但可以使用采用 ParameterizedThreadStart 委托的 Thread 构造函数将一个标记传递给一个线程过程。 下面的示例演示如何执行此操作

using System;
using System.Threading;

public class ServerClass
{
public static void StaticMethod(object obj)
{
CancellationToken ct = (CancellationToken)obj;
Console.WriteLine("ServerClass.StaticMethod is running on another thread.");

  // Simulate work that can be canceled.  
  while (!ct.IsCancellationRequested) {  
     Thread.SpinWait(50000);  
  }  
  Console.WriteLine("The worker thread has been canceled. Press any key to exit.");  
  Console.ReadKey(true);  

}
}

public class Simple
{
public static void Main()
{
// The Simple class controls access to the token source.
CancellationTokenSource cts = new CancellationTokenSource();

  Console.WriteLine("Press 'C' to terminate the application...\\n");  
  // Allow the UI thread to capture the token source, so that it  
  // can issue the cancel command.  
  Thread t1 = new Thread(() => { if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")  
                                 cts.Cancel(); } );

  // ServerClass sees only the token, not the token source.  
  Thread t2 = new Thread(new ParameterizedThreadStart(ServerClass.StaticMethod));  
  // Start the UI thread.

  t1.Start();

  // Start the worker thread and pass it the token.  
  t2.Start(cts.Token);

  t2.Join();  
  cts.Dispose();  

}
}
// The example displays the following output:
// Press 'C' to terminate the application…
//
// ServerClass.StaticMethod is running on another thread.
// The worker thread has been canceled. Press any key to exit.

什么是临界资源和临界区

1.临界资源
  临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。

2.临界区:

每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问。多个进程涉及到同一个临界资源的的临界区称为相关临界区。使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。

=======================================其他概念=============================================================================

缓存一致性:cache一致性关注的是多个CPU看到一个地址的数据是否一致。为了解决缓存一致性,提出缓存一致性协议例如MOSI MESI 等协议
内存一致:内存一致性关注的是多个CPU看到多个地址数据读写的次序。何为一致由内存模型决定。
内存模型:针对内存一致性问题,我们提出内存模型的概念。内存模型可以分为软件层面的内存模型和硬件层面的内存模型
软件层面的内存模型:由各个编程语义制定的规则来规范。
硬件层面的内存模型:各个cpu厂商决定的 例如intel芯片的TSO内存模型

CPU0的store-load操作,在别的CPU看来乱序执行了,变成了load-store次序。这种内存模型,我们称之为完全存储定序(Total Store Order),简称TSO。
store和load的组合有4种。分别是store-store,store-load,load-load和load-store。TSO模型中,只存在store-load存在乱序,另外3种内存操作不存在乱序。
当我们知道了一个CPU的内存模型,就可以根据具体的模型考虑问题。而不用纠结硬件实现的机制,也不用关心硬件做了什么操作导致的乱序。我们只关心内存模型。所以内存够模型更像是一种理论,一种标准,CPU设计之初就需要遵循的法则。当我们知道一款CPU的内存模型,在编写并发代码时就需要时刻考虑乱序带来的影响。我们常见的PC处理器x86-64就是常见的TSO模型。

什么是指令重排?
适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
指令重排序是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段,重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。
从源代码到最终运行的指令,会经过如下两个阶段的重排序。
第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少CPU和内存的交互,重排序之后尽可能保证CPU从寄存器或缓存行中读取数据。
在前面分析JIT优化中提到的循环表达式外提(Loop Expression Hoisting)就是编译器层面的重排序,从CPU层面来说,避免了处理器每次都去内存中加载stop,减少了处理器和内存的交互开销。

第二阶段,处理器重排序,处理器重排序分为两个部分。
并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。
内存系统重排序,这是处理器引入Store Buffer缓冲区延时写入产生的指令执行顺序不一致的问题。

内存屏障
防止指令指令集过度优化,控制指令集重排顺序,的乱序执行的问题。
内存屏障就是将 store bufferes中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。
Store Memory Barrier(写屏障) 告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的
Load Memory Barrier(读屏障) 处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
Full Memory Barrier(全屏障) 确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作

store barrier乱序执行
store barrier存在的意义就是将store buffer中的数据,刷入cache。
1、store buffer存在于cpu核与cache之间,对于x86架构来说,store buffer是FIFO,因此不会存在乱序,写入顺序就是刷入cache的顺序。但是对于ARM/Power架构来说,store buffer并未保证FIFO,因此先写入store buffer的数据,是有可能比后写入store buffer的数据晚刷入cache的。从这点上来说,store buffer的存在会让ARM/Power架构出现乱序的可能。store barrier存在的意义就是将store buffer中的数据,刷入cache。"
2、CPU0的store-load操作,在别的CPU看来乱序执行了,变成了load-store次序。这种内存模型,我们称之为完全存储定序(Total Store Order),简称TSO。

多线程可见性
可见性是指当一个线程修改了共享变量的值,其它线程能够适时得知这个修改

数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性
简单来说,下一步的数据操作依赖上一步操作的数据,即为数据依赖性
编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序