异步编程:线程概述及使用
从此图中我们会发现 .NET 与C# 的每个版本发布都是有一个“主题”。即:C#1.0托管代码→C#2.0泛型→C#3.0LINQ→C#4.0动态语言→C#5.0异步编程。现在我为最新版本的“异步编程”主题写系列分享,期待你的查看及点评。
开始:《异步编程:线程概述及使用》
做交互式客户端应用程序,用户总希望程序能时刻响应UI操作;做高性能服务器开发,使用者总希望服务器能同时处理多个请求……等等,这时我们可以使用多线程技术来保证UI线程可响应、提高服务器吞吐量、提升程序处理速度,设置任务优先级进行调度……
多线程技术只是多个线程在操作系统分配的不同时间片里执行,并不是程序开12个线程12个线程都在同一个 “时间点”执行,同一“时间点”能执行多少线程由CPU决定,各个执行线程的衔接由操作系统进行调度。即,在线程数量超出用于处理它们的处理器数量的情况下,操作系统将定期为每个线程调度一个时间片来控制处理器,以此来模拟同时并发。
在认识线程前,我们需要了解下CPU,了解下进程。
多核心CPU超线程CPU
- 多核心处理器(CPU)
指在一块处理器(CPU)中含有多个处理单元,每一个处理单元它就相当于一个单核处理器(CPU)。因此,多核处理器的功能就相当于多台单核处理器电脑联机作战。
- 超线程处理器(CPU)
指在一块CPU中,用虚拟的方法将一个物理核心模拟成多个核心(一般情况是一个单物理核心,模拟成二个核心,也即所谓的二线程。只有当线程数比物理核心数多才能叫超线程。如四核四线程并不是超线程,而四核八线程才能叫超线程)。
- 优缺点:
1) 多核心是真正的物理核心,一块多核心的处理器(CPU),就相当于多块单核心的处理器(CPU)相互协作。因此,从理论上说,多核心比超线程具有更高运算能力。虽然多核心比超线程的运算速度快很多,但多核心也有一个明显的缺点,那就是多核心的使用效率比超线程处理器(CPU)低。因为,多核心在处理数据时,它们相互“合作”的并不是很完美,常常某个核心需要等待其他核心的计算数据,从而耽误时间,被迫怠工。另外,由于目前多核心都是采用共享缓存,这更使多核心的CPU运算速度减慢不少(因为:CPU读取Cache时是以行为单位读取的,如果两个硬件线程的两块不同内存位于同一Cache行里,那么当两个硬件线程同时在对各自的内存进行写操作时,将会造成两个硬件线程写同一Cache行的问题,它会引起竞争)。
2) 超线程是用虚拟的方法将一个物理核心虚拟成多个核心,它能够最大限度地利用现有的核心资源,具有较高性价比。
操作系统对多核处理器的支持
主要体现在调度和中断上:
- 对任务的分配进行优化。使同一应用程序的任务尽量在同一个核上执行。
- 对任务的共享数据优化。由于多核处理器(Chip Multi-Processor,CMP)体系结构共享缓存(目前),可以考虑改变任务在内存中的数据分布,使任务在执行时尽量增加缓存的命中率。
- 对任务的负载均衡优化。当任务在调度时,出现了负载不均衡,考虑将较忙处理器中与其他任务最不相关的任务迁移,以达到数据的冲突最小。
- 支持抢先多任务处理的操作系统可以创建多个进程中的多个线程同时执行的效果。它通过以下方式实现这一点:在需要处理器时间的线程之间分割可用处理器时间,并轮流为每个线程分配处理器时间片。当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文。
进程和线程
- 进程
进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。
- 线程
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程。主执行线程终止了,进程也就随之终止。
每个线程都维护异常处理程序、调度优先级和线程上下文。(线程上下文,当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文)
- 关系
操作系统使用进程将它们正在执行的不同应用程序分开,.NET Framework 将操作系统进程进一步细分为System.AppDomain (应用程序域)的轻量托管子进程。
线程是CPU的调度单元,是进程中的执行单位,一个进程中可以有多个线程同时执行代码。
线程Thread类详解
静态属性 | CurrentThread ,CurrentContext,CurrentPrincipal(负责人) |
静态方法 | AllocateDataSlot(),AllocateNamedDataSlot(),FreeNamedDataSlot(),GetNamedDataSlot(),GetData(),SetData(),BeginCriticalRegion()[关键的],EndCriticalRegion(),BeginThreadAffinity(),EndThreadAffinity(), GetDomain(),GetDomainID(), ResetAbort(),Sleep(),SpinWait(),MemoryBarrier(),VolatileRead(),VolatileWrite(),Yield() |
实例属性 | Priority,ThreadState ,IsAlive,IsBackground,IsThreadPoolThread,ManagedThreadId,ApartmentState,CurrentCulture,CurrentUICulture,ExecutionContext,Name |
实例方法 | GetHashCode(),Start(),Abort(), Resume(),Suspend(),Join(),Interrupt(),GetApartmentState(),SetApartmentState(),TrySetApartmentState(),GetCompressedStack(),SetCompressedStack(),DisableComObjectEagerCleanup() |
- 常用属性
1) CurrentContext 获取线程正在其中执行的当前上下文。主要用于线程内部存储数据。
2) ExecutionContext 获取一个System.Threading.ExecutionContext对象,该对象包含有关当前线程的各种上下文的信息。主要用于线程间数据共享。
3) IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池。
4) ManagedThreadId 获取一个整数,表示此托管线程的唯一标识符。
5) IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程。
前台线程和后台线程并不等同于主线程和工作线程,如果所有的前台线程终止,那所有的后台线程也会被自动终止。所以,要特别注意前台线程的使用,会造成应用程序终止不了。
默认情况下:通过Thread.Start()方法开启的线程都默认为前台线程。可以设置IsBackground属性将线程配置为后台线程。
属于托管线程池的线程(即其 IsThreadPoolThread 属性为 true 的线程)是后台线程。从非托管代码进入托管执行环境的所有线程都被标记为后台线程。
6) IsAlive 判断此线程是否还存活。经测试只有 Unstarted、Stopped 返回false;其他线程状态都返回true。
- 创建线程
1
2
3
4
|
public
Thread(ParameterizedThreadStart start);
public
Thread(ThreadStart start);
public
Thread(ParameterizedThreadStart start,
int
maxStackSize);
public
Thread(ThreadStart start,
int
maxStackSize);
|
Thread包含使用ThreadStart或ParameterizedThreadStart委托做参数的构造函数,这些委托包装调用Start()时由新线程执行的方法。
线程一旦启动,就不必保留对Thread对象的引用。线程会继续执行直到线程所调用委托执行完毕。
1) 向线程传递数据(见示例)
我们可以直接使用接收ParameterizedThreadStart参数Thread构造函数创建新线程,再通过Start(object parameter)传入参数并启动线程。由于Start方法接收任何对象,所以这并不是一种类型安全的实现。
所以我们可以使用一种替代方案:将线程执行的方法和待传递数据封装在帮助器类中,使用无参的Start()启动线程。必要的时候需在帮助器类中使用同步基元对象避免线程共享数据的死锁和资源争用。
2) 使用回调方法检索数据(见示例)
Thread构造函数接收的ThreadStart或ParameterizedThreadStart委托参数,这两个委托的声明都是返回void,即线程执行完后不会有数据返回(实际上主线程也不会等待Thread创建的新线程返回,否则创建新线程就无意义了)。那么如何在异步执行完时做出响应呢?使用回调方法。
示例----关键代码(详见Simple4CallBackWithParam()):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// 包装异步方法的委托
public
delegate
void
ExampleCallback(
int
lineCount);
// 帮助器类
public
class
ThreadWithState
{
private
string
boilerplate;
private
int
value;
private
ExampleCallback callback;
public
ThreadWithState(
string
text,
int
number,
ExampleCallback callbackDelegate)
{
boilerplate = text;
value = number;
callback = callbackDelegate;
}
public
void
ThreadProc()
{
Console.WriteLine(boilerplate, value);
// 异步执行完时调用回调
if
(callback !=
null
)
callback(1);
}
}
// 异步调用
// 将需传递给异步执行方法数据及委托传递给帮助器类
ThreadWithState tws =
new
ThreadWithState(
"This report displays the number {0}."
,
42,
new
ExampleCallback(ResultCallback)
);
Thread t =
new
Thread(
new
ThreadStart(tws.ThreadProc));
t.Start();
|
- 调度线程
使用Thread.Priority属性获取或设置任何线程的优先级。优先级:Lowest <BelowNormal< Normal <AboveNormal< Highest
1
2
3
4
5
6
7
8
9
|
public
enum
ThreadPriority
{
Lowest = 0,
BelowNormal = 1,
// 默认情况下,线程具有 Normal 优先级。
Normal = 2,
AboveNormal = 3,
Highest = 4,
}
|
每个线程都具有分配给它的线程优先级。在公共语言运行库中创建的线程最初分配的优先级为ThreadPriority.Normal。在运行库外创建的线程会保留它们在进入托管环境之前所具有的优先级。
线程是根据其优先级而调度执行的。所有线程都是由操作系统分配处理器时间片的,如果具有相同优先级的多个线程都可用,则计划程序将遍历处于该优先级的线程,并为每个线程提供一个“固定的时间片”来执行,执行完“固定的时间片”后就切换线程,若当前任务还未执行完,则必须等待下一次的调度。
低优先级的线程并不是被阻塞直到较高优先级的线程完成,低优先级的线程只是在相同时间间隔被CPU调度的次数相对较少。
重要提示:
最好是降低一个线程的优先级,而不是提升另一个线程的优先级。如果线程要执行一个长时间运行的计算限制任务,比如编译代码、拼写检查、电子表格重新计算等,一般应降低该线程的优先级。如果线程要快速响应某个事件,然后运行非常短暂的时间,再恢复为等待状态,则应提高该线程的优先级。高优先级线程在其生命中的大多数时间里都应处于等待状态,这样才不至于影响系统的总体响应能力。
- 线程状态
Thread.ThreadState属性提供一个位掩码,用它指示线程的当前状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
[Flags]
public
enum
ThreadState
{
//线程已启动,它未被阻塞,并且没有挂起的 ThreadAbortException。
Running = 0,
// 正在请求线程停止。 这仅用于内部。
StopRequested = 1,
// 正在请求线程挂起。
SuspendRequested = 2,
// 线程正作为后台线程执行(相对于前台线程而言)。 此状态可以通过设置 Thread.IsBackground 属性来控制。
Background = 4,
// 尚未对线程调用 Thread.Start() 方法。
Unstarted = 8,
// 线程已停止。
Stopped = 16,
// 线程已被阻止。 这可能是因为:调用 Thread.Sleep(System.Int32) 或 Thread.Join()、请求锁定(例如通过调用Monitor.Enter(System.Object) 或 Monitor.Wait(System.Object,System.Int32,System.Boolean))或等待线程同步对象(例如Threading.ManualResetEvent)。
WaitSleepJoin = 32,
// 线程已挂起。
Suspended = 64,
// 已对线程调用了 Thread.Abort(System.Object) 方法,但线程尚未收到试图终止它的挂起的ThreadAbortException。
AbortRequested = 128,
// 线程状态包括 ThreadState.AbortRequested 并且该线程现在已死,但其状态尚未更改为 ThreadState.Stopped。
Aborted = 256,
}
|
由于 Running 状态的值为 0 (枚举的默认值),因此不可能执行位测试来发现此状态。但可以使用此测试(以伪代码表示):if ((state & (Unstarted | Stopped)) == 0){}
线程可以同时处于多个状态中。例如,如果某个线程在 Monitor.Wait 调用被阻止,并且另一个线程对同一个线程调用 Abort,则该线程将同时处于 WaitSleepJoin 和 AbortRequested 状态。在这种情况下,一旦该线程从对 Wait 的调用返回或该线程中断,它就会收到 ThreadAbortException。
- 线程状态操作方法
操作:Start(),Abort(),Suspend(),Resume(), Join(),Interrupt()以及静态方法Sleep()和ResetAbort()
线程操作与线程状态对应的表和图如下:
操作 | 所得到的新状态 |
调用 Thread 类的构造函数。 | Unstarted |
另一个线程调用 Thread.Start。 | Unstarted |
线程响应 Thread.Start 并开始运行。 | Running |
线程调用 Thread.Sleep。 |
WaitSleepJoin |
线程对另一个对象调用 Monitor.Wait。 | |
线程对另一个线程调用 Thread.Join。 | |
另一个线程调用 Thread.Suspend。 | SuspendRequested |
线程返回到托管代码时,线程响应 Thread.Suspend 请求。 | Suspended |
另一个线程调用 Thread.Resume。 | Running |
另一个线程调用 Thread.Abort。 | AbortRequested |
线程返回到托管代码时,线程响应 Thread.Abort。 | Aborted ,然后 Stopped |
1) 开始线程
调用Start()开始一个线程。一旦线程由于调用 Start 而离开 Unstarted 状态,那么它将无法再返回到 Unstarted 状态(最后被销毁)。
2) 线程销毁及取消销毁
调用线程的Abort()实例方法可以销毁目标线程实例,调用Thread.ResetAbort() 来取消线程销毁。()
请注意:
a) 异常是在目标线程捕获,而不是主线程的try-catch-finally。
b) 是“可以”销毁目标线程实例,不能保证线程会结束。因为
l 目标线程可捕捉 ThreadAbortException 异常并在此catch块中调用Thread.ResetAbort() 来取消线程销毁,取消后try块外面的代码可正常运行。
l 在finally块中可以执行任意数量的代码(在finally中调用Thread.ResetAbort()不能取消线程的销毁),若不给予超时设置也无法保证线程会结束。
c) 注意Abort()后要在catch或finally中清理对象。
d) 如果您希望一直等到被终止的线程结束,可以调用Thread.Join()方法。Join 是一个模块化调用,它直到线程实际停止执行时才返回。
e) 如果调用线程的 Abort 方法时线程正在执行非托管代码,则运行库将其标记为ThreadState.AbortRequested。待线程返回到托管代码时引发ThreadAbortException异常。
f) 一旦线程被中止ThreadState.Stoped,它将无法重新启动。
示例----关键代码(详见Simple4Abort())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
Thread t =
new
Thread(
() =>
{
try
{
Console.WriteLine(
"try内部,调用Abort前。"
);
// ……等待其他线程调用该线程的Abort()
Console.WriteLine(
"try内部,调用Abort后。"
);
}
catch
(ThreadAbortException abortEx)
{
Console.WriteLine(
"catch:"
+ abortEx.GetType());
Thread.ResetAbort();
Console.WriteLine(
"catch:调用ResetAbort()。"
);
}
catch
(Exception ex)
{
Console.WriteLine(
"catch:"
+ ex.GetType());
}
finally
{
Console.WriteLine(
"finally"
);
// 在finally中调用Thread.ResetAbort()不能取消线程的销毁
//Thread.ResetAbort();
//Console.WriteLine("调用ResetAbort()。");
}
Console.WriteLine(
"try外面,调用Abort后(若再catch中调用了ResetAbort,则try块外面的代码依旧执行,即:线程没有终止)。"
);
}
// 其他线程调用该线程的Abort()
t.Abort();
Console.WriteLine(
"主线程,调用Abort。"
);
|
输出:
若在catch中没有调用Thread.ResetAbort(),哪么try块外面的代码就不会输出(详见输出截图的两处红线)。
3) 阻塞线程
调用Sleep()方法使当前线程放弃剩余时间片,立即挂起(阻塞)并且在指定时间内不被调度。
Sleep(timeout),会有条件地将调用线程从当前处理器上移除,并且有可能将它从线程调度器的可运行队列中移除。这个条件取决于调用 Sleep 时timeout 参数。
a) 当 timeout = 0, 即 Sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。一个时间片结束时,如果Windows决定再次调度同一个线程(而不是切换到另一个线程),那么Windows不会执行上下文切换。
b) 当 timeout > 0 时,如:Sleep(1),可能会引发线程上下文切换(如果发生线程切换):调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。
调用Thread.Sleep(Timeout.Infinite)将使线程休眠,直到其他运行线程调用 Interrupt ()中断处于WaitSleepJoin线程状态的线程,或调用Abort()中止线程。
应用实例:轮询休眠
while (!proceed) Thread.Sleep (x); // "轮询休眠!"
4) 线程的挂起和唤醒
可结合Suspend()与Resume()来挂起和唤醒线程,这两方法已过时。
当对某线程调用Suspend()时,系统会让该线程执行到一个安全点,然后才实际挂起该线程(与Thread.Sleep()不同, Suspend()不会导致线程立即停止执行)。无论调用了多少次 Suspend(),调用Resume()均会使另一个线程脱离挂起状态,并导致该线程继续执行。
注意:由于Suspend()和Resume()不依赖于受控制线程的协作,因此,它们极具侵犯性并且会导致严重的应用程序问题,如死锁(例如,如果您在安全权限评估期间挂起持有锁的线程,则AppDomain中的其他线程可能被阻止。如果您在线程正在执行类构造函数时挂起它,则AppDomain中试图使用该类的其他线程将被阻止。很容易发生死锁)。
线程的安全点:
是线程执行过程中可执行垃圾回收的一个点。垃圾回收器在执行垃圾回收时,运行库必须挂起除正在执行回收的线程以外的所有线程。每个线程在可以挂起之前都必须置于安全点。
5) Join()
在线程A中调用线程B的Join()实例方法。在继续执行标准的 COM 和 SendMessage 消息泵处理期间,线程A将被阻塞,直到线程B终止为止。
6) Interrupt()
中断处于WaitSleepJoin线程状态的线程。如果此线程当前未阻塞在等待、休眠或联接状态中,则下次开始阻塞时它将被中断并引发ThreadInterruptedException异常。
线程应该捕获ThreadInterruptedException并执行任何适当的操作以继续运行。如果线程忽略该异常,则运行库将捕获该异常并停止该线程。
如果调用线程的 Interrupt()方法时线程正在执行非托管代码,则运行库将其标记为ThreadState.SuspendRequested。待线程返回到托管代码时引发ThreadInterruptedException异常。
- SpinWait(int iterations)
SpinWait实质上会将处理器置于十分紧密的自旋转中,当前线程一直占用CPU,其循环计数由 iterations 参数指定。
SpinWait并不是一个阻止的方法:一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,并且也不会被其它的线程过早的中断(Interrupt)。SpinWait的作用是等待一个在极短时间(可能小于一微秒)内可准备好的可预期的资源,而避免调用Sleep()方法阻止线程而浪费CPU时间(上下文切换)。
优点:避免线程上下文切换的耗时操作。
缺点:CPU不能很好的调度CPU利用率。这种技术的优势只能在多处理器计算机上体现,对单一处理器的电脑,直到轮询的线程结束了它的时间片之前,别的资源无法获得cpu调度执行。
- 设置和获取线程的单元状态
1
2
3
4
5
6
7
8
9
10
|
// System.Threading.Thread 的单元状态。
public
enum
ApartmentState
{
// System.Threading.Thread 将创建并进入一个单线程单元。
STA = 0,
// System.Threading.Thread 将创建并进入一个多线程单元。
MTA = 1,
// 尚未设置 System.Threading.Thread.ApartmentState 属性。
Unknown = 2,
}
|
1) 可使用ApartmentState获取和设置线程的单元状态,次属性已经过时
2) SetApartmentState()+TrySetApartmentState()+GetApartentState()
可以标记一个托管线程以指示它将承载一个单线程或多线程单元。如果未设置该状态,则GetApartmentState返回ApartmentState.Unknown。只有当线程处于ThreadState.Unstarted状态时(即线程还未调用Start()时)才可以设置该属性;一个线程只能设置一次。
如果在启动线程之前未设置单元状态,则该线程被初始化为默认多线程单元 (MTA)。(终结器线程和由ThreadPool控制的所有线程都是 MTA)
要将主应用程序线程的单元状态设置为ApartmentState.STA的唯一方法是将STAThreadAttribute属性应用到入口点方法。(eg:Main()方法)
- 设置和检索线程数据(数据槽)
线程使用托管线程本地存储区 (TLS,Thread-Local Storage)来存储线程特定的数据,托管 TLS 中的数据都是线程和应用程序域组合所独有的,其他任何线程(即使是子线程)都无法获取这些数据。
公共语言运行库在创建每个进程时给它分配一个多槽数据存储区数组,数据槽包括两种类型:命名槽和未命名槽。
1) 若要创建命名数据槽,使用 Thread.AllocateNamedDataSlot() 或 Thread.GetNamedDataSlot() 方法。命名数据槽数据必须使用Thread.FreeNamedDataSlot()来释放。
在任何线程调用Thread.FreeNamedDataSlot()之后,后面任何线程使用相同名称调用Thread.GetNamedDataSlot()都将返回新槽。但是,任何仍具有以前通过调用Thread.GetNamedDataSlot()返回的System.LocalDataStoreSlot引用的线程可以继续使用旧槽。
只有当调用Thread.FreeNamedDataSlot()之前获取的所有LocalDataStoreSlot已被释放并进行垃圾回收之后,与名称关联的槽才会被释放。
2) 若要获取对某个现有命名槽的引用,将其名称传递给 Thread.GetNamedDataSlot() 方法。
3) 若要创建未命名数据槽,使用 Thread.AllocateDataSlot() 方法。未命名数据槽数据在线程终止后释放。
4) 对于命名槽和未命名槽,使用 Thread.SetData() 和 Thread.GetData() 方法设置和检索槽中的信息。
命名槽可能很方便,因为您可以在需要它时通过将其名称传递给 GetNamedDataSlot 方法来检索该槽,而不是维护对未命名槽的引用。但是,如果另一个组件使用相同的名称来命名其线程相关的存储区,并且有一个线程同时执行来自您的组件和该组件的代码,则这两个组件可能会破坏彼此的数据。(本方案假定这两个组件在同一应用程序域内运行,并且它们并不用于共享相同数据。)
为了获得更好的性能,请改用以 System.ThreadStaticAttribute特性标记的线程相关的静态字段。
- 原子操作
由于编译器,或者CPU的优化,可能导致程序执行的时候并不是真正的按照代码顺序执行。在多线程开发的时候可能会引起错误。
在debug模式下,编译器不会做任何优化,而当Release后,编译器做了优化,此时就会出现问题。
1) Thread.MemoryBarrier()
按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 Thread.MemoryBarrier()调用之后的内存存取,再执行 Thread.MemoryBarrier() 调用之前的内存存取的方式。
2) Thread.VolatileRead()+Thread.VolatileWrite() (内部使用MemoryBarrier()内存屏障)
a) VolatileRead() 读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。
b) VolatileWrite () 立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。
3) 关键字Volatile:
为了简化编程,C#编译器提供了volatile关键字。确保JIT编译器对易失字段都以易失读取或者易失写入的方法执行,不用显示调用Thread的VolatileRead()和VolatileWrite()。
- BeginCriticalRegion()+EndCriticalRegion() (Critical:关键性的)
若要通知宿主代码进入关键区域,调用BeginCriticalRegion。当执行返回到非关键代码区域时,调用EndCriticalRegion。
公共语言运行库 (CLR) 的宿主可在关键代码区域和非关键代码区域建立不同的失败策略。关键区域是指线程中止或未处理异常的影响可能不限于当前任务的区域。相反,非关键代码区域中的中止或失败只对出现错误的任务有影响。
当关键区域中出现失败时,宿主可能决定卸载整个AppDomain,而不是冒险在可能不稳定的状态下继续执行。
例如,假设有一个尝试在占有锁时分配内存的任务。如果内存分配失败,则中止当前任务并不足以确保AppDomain的稳定性,原因是域中可能存在其他等待同一个锁的任务。如果终止当前任务,则可能导致其他任务死锁。
- BeginThreadAffinity()+EndThreadAffinity() (Affinity:喜爱,密切关系)
使用BeginThreadAffinity和EndThreadAffinity方法通知宿主代码块依赖于物理操作系统线程的标识。
公共语言运行库的某些宿主提供其自己的线程管理。提供其自己的线程管理的宿主可以在任何时候将正在执行的任务从一个物理操作系统线程移至另一个物理操作系统线程。大多数任务不会受此切换影响。但是,某些任务具有【线程关联】 -- 即它们依赖于物理操作系统线程的标识。这些任务在其执行“不应被切换的代码”时必须通知宿主。
例如,如果应用程序调用系统 API 以获取具有【线程关联】的操作系统锁(如 Win32 CRITICAL_SECTION),则必须在获取该锁之前调用BeginThreadAffinity,并在释放该锁之后调用EndThreadAffinity。
还必须在从WaitHandle继承的任何 .NET Framework 类型上发生阻止之前调用BeginThreadAffinity,因为这些类型依赖于操作系统对象。
线程本地存储区和线程相关的静态字段
可以使用托管线程本地存储区 (TLS,Thread-Local Storage) 和线程相关的静态字段来存储某一线程和应用程序域所独有的数据。
a) 如果可以在编译时预料到确切需要,请使用线程相关的静态字段。
b) 如果只能在运行时发现实际需要,请使用数据槽。
为了获得更好的性能,请尽量改用以 System.ThreadStaticAttribute特性标记的线程相关的静态字段。
无论是使用线程相关的静态字段还是使用数据槽,托管 TLS 中的数据都是线程和应用程序域组合所独有的。
a) 在应用程序域内部,一个线程不能修改另一个线程中的数据,即使这两个线程使用同一个字段或槽时也不能。
b) 当线程从多个应用程序域中访问同一个字段或槽时,会在每个应用程序域中维护一个单独的值。
1) 线程相关的静态字段(编译时)
如果您知道某类型的字段【总是某个线程和应用程序域组合】所独有的(即不是共享的),则使用ThreadStaticAttribute修饰静态字段(static)。
需要注意的是,任何类构造函数代码都将在访问该字段的第一个上下文中的第一个线程上运行。在所有其他线程或上下文中,如果这些字段是引用类型,将被初始化为 null;如果这些字段是值类型,将被初始化为它们的默认值。因此,不要依赖于类构造函数来初始化线程相关的静态字段[ThreadStatic]。相反,应总是假定与线程相关的静态字段被初始化为 null 或它们的默认值。
2) 数据槽(运行时)
示例:托管TSL中数据的唯一性(数据槽|线程相关静态字段)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
/// <summary>
/// 数据槽 的使用示例
/// </summary>
private
static
void
TLS4DataSlot()
{
LocalDataStoreSlot slot = Thread.AllocateNamedDataSlot(
"Name"
);
Console.WriteLine(String.Format(
"ID为{0}的线程,命名为\"Name\"的数据槽,开始设置数据。"
, Thread.CurrentThread.ManagedThreadId));
Thread.SetData(slot,
"小丽"
);
Console.WriteLine(String.Format(
"ID为{0}的线程,命名为\"Name\"的数据槽,数据是\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot)));
Thread newThread =
new
Thread(
() =>
{
LocalDataStoreSlot storeSlot = Thread.GetNamedDataSlot(
"Name"
);
Console.WriteLine(String.Format(
"ID为{0}的线程,命名为\"Name\"的数据槽,在新线程为其设置数据 前 为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, Thread.GetData(storeSlot)));
Console.WriteLine(String.Format(
"ID为{0}的线程,命名为\"Name\"的数据槽,开始设置数据。"
, Thread.CurrentThread.ManagedThreadId));
Thread.SetData(storeSlot,
"小红"
);
Console.WriteLine(String.Format(
"ID为{0}的线程,命名为\"Name\"的数据槽,在新线程为其设置数据 后 为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, Thread.GetData(storeSlot)));
// 命名数据槽中分配的数据必须用 FreeNamedDataSlot() 释放。未命名的数据槽数据随线程的销毁而释放
Thread.FreeNamedDataSlot(
"Name"
);
}
);
newThread.Start();
newThread.Join();
Console.WriteLine(String.Format(
"执行完新线程后,ID为{0}的线程,命名为\"Name\"的数据槽,在新线程为其设置数据 后 为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot)));
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 不应依赖于类构造函数来初始化线程相关的静态字段[ThreadStatic]
[ThreadStatic]
static
string
name = String.Empty;
/// <summary>
/// 线程相关静态字段 的使用示例
/// </summary>
private
static
void
TLS4StaticField()
{
Console.WriteLine(String.Format(
"ID为{0}的线程,开始为name静态字段设置数据。"
, Thread.CurrentThread.ManagedThreadId));
name =
"小丽"
;
Console.WriteLine(String.Format(
"ID为{0}的线程,name静态字段数据为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, name));
Thread newThread =
new
Thread(
() =>
{
Console.WriteLine(String.Format(
"ID为{0}的线程,为name静态字段设置数据 前 为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, name));
Console.WriteLine(String.Format(
"ID为{0}的线程,开始为name静态字段设置数据。"
, Thread.CurrentThread.ManagedThreadId));
name =
"小红"
;
Console.WriteLine(String.Format(
"ID为{0}的线程,为name静态字段设置数据 后 为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, name));
}
);
newThread.Start();
newThread.Join();
Console.WriteLine(String.Format(
"执行完新线程后,ID为{0}的线程,name静态字段数据为\"{1}\"。"
, Thread.CurrentThread.ManagedThreadId, name));
}
|
结果截图:
.NET下未捕获异常的处理
- 控制台应用程序
通过为当前AppDomain添加 UnhandledException 事件处理程序。
1
2
3
|
AppDomain.CurrentDomain.UnhandledException +=
new
UnhandledExceptionEventHandler(UnhandledExceptionEventHandler);
static
void
UnhandledExceptionEventHandler(
object
sender, UnhandledExceptionEventArgs e) { …… }
|
- WinForm窗体应用程序
未处理的异常将引发Application.ThreadException事件。
a) 如果异常发生在主线程中,默认行为是未经处理的异常不终止该应用程序。在这种情况下,不会引发 UnhandledException 事件。但可以在在挂钩 ThreadException 事件处理程序之前,使用应用程序配置文件或者使用 Application.SetUnhandledExceptionMode() 方法将模式设置为 UnhandledExceptionMode.ThrowException 来更改此默认行为。
b) 如果异常发生在其它线程中,将引发 UnhandledException 事件。
1
2
|
Application.ThreadException +=
new
ThreadExceptionEventHandler(Application_ThreadException)
static
void
Application_ThreadException(
object
sender, ThreadExceptionEventArgs e) { …… }
|
- ASP.NET应用程序
要截获ASP.NET 的未捕获异常,我们需要为每个应用程序域安装事件钩子。这个过程需要分两步完成:
a) 首先创建一个实现IHttpModule接口的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
class
UnhandledExceptionModule : IHttpModule
{
……
static
object
_initLock =
new
object
();
static
bool
_initialized =
false
;
public
void
Init(HttpApplication context)
{
// Do this one time for each AppDomain.
lock
(_initLock)
{
if
(!_initialized)
{
AppDomain.CurrentDomain.UnhandledException +=
new
UnhandledExceptionEventHandler(OnUnhandledException);
_initialized =
true
;
}
}
}
}
|
b) 第二步:修改web.config,在 system.web 段中加入
1
2
3
|
<httpModules>
<add name=
"UnhandledExceptionModule"
type=
"WebMonitor.UnhandledExceptionModule"
/>
</httpModules>
|
判断多个线程是否都结束的几种方法
有园友问到此问题,所以提供下面几种方法。若还有其他方法请告知。
1. 线程计数器
线程也可以采用计数器的方法,即为所有需要监视的线程设一个线程计数器,每开始一个线程,在线程的执行方法中为这个计数器加1,如果某个线程结束(在线程执行方法的最后),为这个计数器减1。使用这种方法需要使用原子操作(eg:Volatile、InterLocked)同步这个计数器变量。
2. 使用Thread.join方法
join方法只有在线程结束时才继续执行下面的语句。可以对每一个线程调用它的join方法,但要注意,这个调用要在一个专门线程里做,而不要在主线程,否则程序会被阻塞。
3. 轮询Thread的IsAlive属性
IsAlive判断此线程是否还存活。经测试只有 Unstarted、Stopped 返回false;其他线程状态都返回true。
我们通过轮询检查此属性来判断线程是否结束。但要注意,这个调用要在一个专门线程里做,而不要在主线程,否则程序会被阻塞。
EG:while(true) { foreach(多个线程){ if(thread1.IsAlive) { } } }
4. 使用回调函数进行通知
5. 使用同步基元对象
Eg:WaitHandle。在后续章节中再说明
本博文主要为大家介绍了进程和线程的差别,计算机对多线程的支持,Thread类的详解,线程状态及影响线程状态的各种线程操作,托管线程本地存储区,线程中未处理异常的捕获等等……
看完后你会发现如果程序任务小而多会造成不断的创建和销毁线程不便于线程管理;你可能还会发现当线程操作共享资源的时候没有控制资源的同步问题……在后续章节中会陆续引入线程池和同步基元对象解决相应问题,敬请查看。
本节就此结束,谢谢大家查看,一起学习一起进步。
参考资料
扩展知识: