异步编程线程概述及使用.docx
《异步编程线程概述及使用.docx》由会员分享,可在线阅读,更多相关《异步编程线程概述及使用.docx(25页珍藏版)》请在冰豆网上搜索。
![异步编程线程概述及使用.docx](https://file1.bdocx.com/fileroot1/2022-11/29/a6936bac-382b-466f-8eda-232d0814a5be/a6936bac-382b-466f-8eda-232d0814a5be1.gif)
异步编程线程概述及使用
异步编程:
线程概述及使用
从此图中我们会发现.NET与C#的每个版本发布都是有一个“主题”。
即:
C#1.0托管代码→C#2.0泛型→C#3.0LINQ→C#4.0动态语言→C#5.0异步编程。
现在我为最新版本的“异步编程”主题写系列分享,期待你的查看及点评。
传送门:
异步编程系列目录……
开始:
《异步编程:
线程概述及使用》
示例:
异步编程:
线程概述及使用.rar
做交互式客户端应用程序,用户总希望程序能时刻响应UI操作;做高性能服务器开发,使用者总希望服务器能同时处理多个请求……等等,这时我们可以使用多线程技术来保证UI线程可响应、提高服务器吞吐量、提升程序处理速度,设置任务优先级进行调度……
多线程技术只是多个线程在操作系统分配的不同时间片里执行,并不是程序开12个线程12个线程都在同一个“时间点”执行,同一“时间点”能执行多少线程由CPU决定,各个执行线程的衔接由操作系统进行调度。
即,在线程数量超出用于处理它们的处理器数量的情况下,操作系统将定期为每个线程调度一个时间片来控制处理器,以此来模拟同时并发。
在认识线程前,我们需要了解下CPU,了解下进程。
多核心CPU超线程CPU
1. 多核心处理器(CPU)
指在一块处理器(CPU)中含有多个处理单元,每一个处理单元它就相当于一个单核处理器(CPU)。
因此,多核处理器的功能就相当于多台单核处理器电脑联机作战。
2. 超线程处理器(CPU)
指在一块CPU中,用虚拟的方法将一个物理核心模拟成多个核心(一般情况是一个单物理核心,模拟成二个核心,也即所谓的二线程。
只有当线程数比物理核心数多才能叫超线程。
如四核四线程并不是超线程,而四核八线程才能叫超线程)。
3. 优缺点:
1) 多核心是真正的物理核心,一块多核心的处理器(CPU),就相当于多块单核心的处理器(CPU)相互协作。
因此,从理论上说,多核心比超线程具有更高运算能力。
虽然多核心比超线程的运算速度快很多,但多核心也有一个明显的缺点,那就是多核心的使用效率比超线程处理器(CPU)低。
因为,多核心在处理数据时,它们相互“合作”的并不是很完美,常常某个核心需要等待其他核心的计算数据,从而耽误时间,被迫怠工。
另外,由于目前多核心都是采用共享缓存,这更使多核心的CPU运算速度减慢不少(因为:
CPU读取Cache时是以行为单位读取的,如果两个硬件线程的两块不同内存位于同一Cache行里,那么当两个硬件线程同时在对各自的内存进行写操作时,将会造成两个硬件线程写同一Cache行的问题,它会引起竞争)。
2) 超线程是用虚拟的方法将一个物理核心虚拟成多个核心,它能够最大限度地利用现有的核心资源,具有较高性价比。
操作系统对多核处理器的支持
主要体现在调度和中断上:
1. 对任务的分配进行优化。
使同一应用程序的任务尽量在同一个核上执行。
2. 对任务的共享数据优化。
由于多核处理器(ChipMulti-Processor,CMP)体系结构共享缓存(目前),可以考虑改变任务在内存中的数据分布,使任务在执行时尽量增加缓存的命中率。
3. 对任务的负载均衡优化。
当任务在调度时,出现了负载不均衡,考虑将较忙处理器中与其他任务最不相关的任务迁移,以达到数据的冲突最小。
4. 支持抢先多任务处理的操作系统可以创建多个进程中的多个线程同时执行的效果。
它通过以下方式实现这一点:
在需要处理器时间的线程之间分割可用处理器时间,并轮流为每个线程分配处理器时间片。
当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。
当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文。
进程和线程
1. 进程
进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。
2. 线程
线程是进程内部的一个执行单元。
系统创建好进程后,实际上就启动执行了该进程的主执行线程。
主执行线程终止了,进程也就随之终止。
每个线程都维护异常处理程序、调度优先级和线程上下文。
(线程上下文,当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。
当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文)
3. 关系
操作系统使用进程将它们正在执行的不同应用程序分开,.NETFramework将操作系统进程进一步细分为System.AppDomain(应用程序域)的轻量托管子进程。
线程是CPU的调度单元,是进程中的执行单位,一个进程中可以有多个线程同时执行代码。
操作系统中,CPU的两种竞争策略
操作系统中,CPU竞争有很多种策略。
Unix系统使用的是时间片算法,而Windows则属于抢占式的。
1. 在时间片算法中,所有的进程排成一个队列。
操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。
如果在 时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。
如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。
调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
2. 所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。
因此可以看出,在抢占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。
在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一个总的优先级来。
操作系统就会把 CPU 交给总优先级最高的这个进程。
当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一次所有进程的总优先级,然后再挑一个优先级最高的把 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. 常用属性
1) CurrentContext 获取线程正在其中执行的当前上下文。
主要用于线程内部存储数据。
2) ExecutionContext 获取一个System.Threading.ExecutionContext对象,该对象包含有关当前线程的各种上下文的信息。
主要用于线程间数据共享。
3) IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池。
4) ManagedThreadId 获取一个整数,表示此托管线程的唯一标识符。
5) IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程。
前台线程和后台线程并不等同于主线程和工作线程,如果所有的前台线程终止,那所有的后台线程也会被自动终止。
应用程序必须运行完所有的前台线程才可以退出,所以,要特别注意前台线程的使用,会造成应用程序终止不了。
默认情况下:
通过Thread.Start()方法开启的线程都默认为前台线程。
可以设置IsBackground属性将线程配置为后台线程。
属于托管线程池的线程(即其IsThreadPoolThread属性为true的线程)是后台线程。
从非托管代码进入托管执行环境的所有线程都被标记为后台线程。
6) IsAlive 判断此线程是否还存活。
经测试只有Unstarted、Stopped返回false;其他线程状态都返回true。
2. 创建线程
1
2
3
4
public Thread(ParameterizedThreadStartstart);
public Thread(ThreadStartstart);
public Thread(ParameterizedThreadStartstart,int maxStackSize);
public Thread(ThreadStartstart,int maxStackSize);
Thread包含使用ThreadStart或ParameterizedThreadStart委托做参数的构造函数,这些委托包装调用Start()时由新线程执行的方法。
线程一旦启动,就不必保留对Thread对象的引用。
线程会继续执行直到线程所调用委托执行完毕。
1) 向线程传递数据(见示例)
我们可以直接使用接收ParameterizedThreadStart参数Thread构造函数创建新线程,再通过Start(objectparameter)传入参数并启动线程。
由于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 ExampleCallbackcallback;
public ThreadWithState(string text,int number,
ExampleCallbackcallbackDelegate)
{
boilerplate=text;
value=number;
callback=callbackDelegate;
}
public void ThreadProc()
{
Console.WriteLine(boilerplate,value);
//异步执行完时调用回调
if (callback!
=null)
callback
(1);
}
}
//异步调用
//将需传递给异步执行方法数据及委托传递给帮助器类
ThreadWithStatetws=new ThreadWithState(
"Thisreportdisplaysthenumber{0}.",
42,
new ExampleCallback(ResultCallback)
);
Threadt=new Thread(new ThreadStart(tws.ThreadProc));
t.Start();
3. 调度线程
使用Thread.Priority属性获取或设置任何线程的优先级。
优先级:
Lowest1
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调度的次数相对较少。
重要提示:
最好是降低一个线程的优先级,而不是提升另一个线程的优先级。
如果线程要执行一个长时间运行的计算限制任务,比如编译代码、拼写检查、电子表格重新计算等,一般应降低该线程的优先级。
如果线程要快速响应某个事件,然后运行非常短暂的时间,再恢复为等待状态,则应提高该线程的优先级。
高优先级线程在其生命中的大多数时间里都应处于等待状态,这样才不至于影响系统的总体响应能力。
4. 线程状态
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。
5. 线程状态操作方法
操作:
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
Threadt=new Thread(
()=>
{
try
{
Console.WriteLine("try内部,调用Abort前。
");
//……等待其他线程调用该线程的Abort()
Console.WriteLine("try内部,调用Abort后。
");
}
catch (ThreadAbortExceptionabortEx)
{
Console.WriteLine("catch:
" +abortEx.GetType());
T