新手学C++多线程编程(3)线程

xiaoxiao2021-02-28  11

新手学C++多线程编程(3)线程

  转载

C++多线程编程(3)

线程

 

1.线程(thread): 一种轻量级进程。与进程相比,线程给操作系统带来的创建、维护和管理负担要轻,因为与线程相关信息非常少。 线程也有上下文(只包含一个堆栈、一个寄存器组<程序或指令指针以及堆栈指针>和一个优先权),当线程被抢先时,必定发生线程间的上下文切换。线程没有地址空间,线程包含在进程的地址空间中。线程文本包含在它的进程文本片断中。进程拥有的所有资源都属于线程。其他信息如规划计数等都由进程所定义。

 进程的数据片断被它的线程共享。线程可以读和写进程的内存地址,而且可以访问数据。当进程写入内存时,线程可以访问这些数据。   线程可以在它的进程中创建另一个线程。   同位体(peer):一个进程中的所有线程。他们共享进程的资源和内存。   线程也可以在进程中挂起、恢复和终止其他线程。   线程是进程的租借者。线程发挥作用所需的一切由进程提供和定义。   线程:线程id,定义线程状态的寄存器组、优先权、堆栈。

  2.多线程处理:   多线程进程拥有的线程多于一个,每个线程都执行进程的程序代码。指令序列称作执行线程。如果进程只有一个线程,则称作主线程,它只有一个控制流程。  线程可以执行单个函数或一组函数。每个线程的程序计数器将跟踪执行下一条指令。每个线程允许进程的函数独立执行,而与进程的主控制流程无关。   多线程应用程序指那些可以受益于子任务异步执行的应用程序。如“命令解析器”由以下子任务组成:验证字符串、提取噪音、符号化字符串以及执行。每个子任务可以表示一个线程。一些线程(解析器与进程的其他线程)同步执行,一些线程(符号化字符串中获取命令符号、获取设备符号、获取文件名符号)可以异步执行。 如下图:

3.线程与进程异同点:   (1)共同点:     a.线程和进程都有id,寄存器组,状态及优先权。     b.进程和线程都有信息块。     c.线程和子进程共享父进程的资源     d.进程与线程在创建后都是独立的实体。     e.进程与线程创建者对它们施加控制。     f.进程与线程都可以在创建后更改属性,创建新的资源。     g.进程与线程都不能直接访问其他无关进程或线程的资源。   (2)不同点:     a.进程有一个地址空间,线程没有地址空间。     b.父和子进程必须使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。     c.子进程对任何其他子进程不施加控制。进程的线程被看做同位体,并对进程的其他线程施加控制。     d.子进程不能对父进程施加控制。进程的所有线程都可以对主线程施加控制,并因此影响到整个进程。   4.线程优点: (1)多线程可以提供子任务的异步执行,这种方式对上下文切换所花的开销和资源较少,而并发多进程(一个进程一个线程)开销较大。 (2)多线程可以增加应用程序的吞吐能力。整个应用程序不能等待每个I/O请求的完成,其他不依赖于阻塞线程的任务可以执行。 (3)线程不需要子任务间的特殊通信机制(进程间通信方式),线程可以轻易直接的在任务间传递与接收数据,线程通过使用进程内所有线程共享的内存(变量)来通信,而进程通过共享内存来通信,有单独地址空间,共享内存位于两个进程地址空间之外。

 

5.线程缺点: (1)线程需要同步并发访问内存,则必须进行线程同步化。线程A试图写内存地址,线程B正写入同样地址,线程C负责读取保存在该内存地址的值,并用于计算中。此时线程需要同步化,在线程B覆盖值之前,让线程C可以读取线程A保存在内存地址中的值。线程同步化:当发生写时,必须发生读。 (2)线程可能产生影响所有线程的进程(内存空间)的数据错误,而进程的数据错误只限制于单个进程,其他进程将继续执行。 (3)线程依赖于它所属的进程并且不能退出到创建它的进程之外;而进程更独立可以保护资源不被其他进程随意访问,也可以打包成模块供其他应用程序来使用。

6.线程类型(策略):休眠(sleeper)和单步(one-shot)、先占工作(anticipating work)、延迟工作(deferring work)。

(1)休眠线程: 直到某事件发生前线程一直挂起,执行任务后再次挂起,等待事件再次发生。 (2)单步线程:直到某事件发生前线程一直挂起,执行任务一次后就终止。(实际上就是休眠线程)

 监视线程:休眠线程/单步线程。他们监视应用程序的某些方面,等待事件的发生,然后作为反映采取一些相应行为。

如监视COM端口:当端口激活时,线程一直挂起,但当COM端口非激活时,线程苏醒并监视停止工作的过程。如果停止工作超过5000ms时,线程关闭通信端口,此时该监视线程不再监视任何对象,于是可以终止了。 如监视打印机:当某个任务放入队列中时,线程将此任务发送到打印机。队列中可能有多个任务,所以线程被挂起,直到它必须再次执行为止。

(3)先占工作:线程在被请求前已在执行某任务。这个动作可能被请求了,也可能没有被请求。   对情况进行估计,并先占执行一些可能没有得到请求的动作,如果被请求,它已经完成了执行。遵循永不等待规则目的是阻止非激活。这一规则表明预先执行一个可能不被使用的任务要比处于空闲要好。例如:线程A、线程B、线程C执行如下三条语句:  线程A:d=a+a;  线程B:if(d>10) a=5;  线程c: x=5*a; //先占工作  线程A和线程C异步执行,然后执行线程B。如果线程B改变了a的值,则重新执行线程C,反之则该计算已经执行,这就节省了计算时间。

(3)延迟工作:线程将(非关键性低优先级)任务推迟到将来某个点(分离线程)执行该任务。    线程可以创建另一个线程来执行任务,或者将任务交给一些已存在的线程。

 

7.线程相关信息:   线程信息块:指向线程异常处理器头的指针、线程的堆栈基和大小、线程ID以及调用线程的优先权。

 

8.线程创建:   环境提供两种创建线程的方式:(1)调用由线程库提供的函数 (2)调用操作系统提供的函数   POSIX: in pthread_create(pthread_t *new_thread_id,const pthread_atr_t *attr,void *(*start_func)(void*),void *arg)   OS/2: APIRET DosCreateThread(pptid ThreadID,ppThreadAddr,ulThreadArg,ulThreadFlags,ulStackSize)   Win32:HANDLE CreateThread(LPSECURITY_ATTRIB_UTE lpsa DWORD ccStack,LPTHREAD_ START_ ROUTINE lpStartAddr,LPVOID lpvThreadParm,DWORD fdwCreate LPDWORD lpIDThread)

 

9.每个线程维护一个在进程内存中自动分配的堆栈。堆栈的大小必须足够大,用于调用即将创建线程的所有函数、函数的参数以及由每个函数创建的局部变量。在线程终止时释放为堆栈分配的内存。

 

10.线程的终止:(1)当线程返回到创建它的函数时自动终止。线程的资源被释放。(2)显式调用一个线程终止函数,则任何等待该线程的终止的线程都将受到终止进程ID或线程终止信号。(3)使用一个终止线程堆栈的指针,指针将由于堆栈的销毁而变得无用,并导致整个进程的终止。(4)进程内一个线程可以强迫另一个线程终止,但不能强迫自己终止。强迫终止如果没有反应或执行不正确,则必须销毁该线程。 POSIX: void pthread_exit(void *status); //线程退出        int pthread_detach(pthread_tthreadID); //线程分离        int pthread_cancel(pthread_tthreadID);//线程取消 OS/2:  APIRET DosExit(ULONG terminate_action,ULONG exit_code);//线程终止        APIRET DosKillThread(TID ThreadID);//将被销毁线程 Win32: BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);//终止线程        VOID ExitThread(DWORD dwExitcode);//线程退出

 

11.分离线程:

  分离进程为异步子进程,它不继承父进程的任何属性。分离进程在终止时不返回到父进程。分离线程在这一点与分离进程相似。   当线程终止时,终止线程的ID和状态由系统保存。如果进程不需要知道线程何时终止,就可以创建分离线程。在创建的同时设置分离标志。运行线程也可以分离。   守护进程(daemon thread)一直保持阻塞状态直到系统中发生了某事件,才被激活(如打印线程).

 

12.远程线程:一个进程中的线程可创建另一个进程中的线程。创建过程与在同一进程中创建线程相似。

 

13.线程堆栈:堆栈必须足够大,适应于线程的任何函数调用、进程外部的任何代码(如库代码)以及局部变量存储。进程的地址空间必须足以容纳文本、数据片段以及它的所有线程堆栈。    阻止线程使进程内存崩溃方法:在现场堆栈外部指派一个危险区(danger area)、危险页(danger page)或危险地带(danger zone),当受到侵入时,系统就得到通知。一但侵入这些区域,那么采取一些行动,这些行动取决于系统以及它是如何分配堆栈上的提交页的。    防止系统移动危险区让堆栈增长:警戒页(guard page)实现,它位于堆栈顶部,下方是堆栈的提交页。如果侵入警戒页,就下移到另一个提交页,允许堆栈的增长。这一过程重复进行,直到耗尽堆栈。下面的页为未提交页。

 

14.线程控制:进程内的线程被看做是同位体,对资源有同样的访问权限,相互施加同等的控制。线程间可以相互通信,不必创建特殊的通信机制。线程既可以销毁(包括主线程)也可以创建其他线程。    临界区:线程的一个代码部分,用于控制线程访问内存的权限,可用于读与写线程的问题。当线程进入一个临界区,它访问与进程中所有其他线程共享的内存,但是当共享、可修改的数据正被进入临界区的线程使用时,它阻止其他任何线程的访问,所有试图使用这一数据的线程都将被阻塞。当该线程退出后,才允许另一个线程来访问这些数据。

 

15.挂起和恢复线程:线程可以挂起进程内另一个线程的执行。挂起线程直到调用一个恢复它的函数时才会执行。进程内的任何线程都可以恢复挂起线程,除了挂起线程自己。线程可以在指定时间段内挂起自身。

 

16.线程优先权:在抢占式操作系统中,操作系统保持对处理器的控制权。如果较高优先权的线程可用、运行线程发出了一个I/O请求或被挂起。 线程饥饿:较高优先权的线程将主宰处理器,剥夺了其他线程有用的处理器时间。 使用动态优先权机制改变线程优先权。

 

17.优先权(priority)包括一个优先类(priority class)和一个优先级(priority level)。  在Win32中,最高优先类有16~31个级,其他类有0~15个级。    实时类(16~31级):对于应当立即执行的短小任务的最高优先权。等同于系统函数。    普通类(1~15级):默认优先权。大部分用户线路使用这个优先类。    空闲类(1~15级):最低优先权。仅在无其他优先权类任务时执行。

线程优先权:基优先权(base prority)。改变优先权是给基优先权添加一个值。如果级别提升,则在基中添加一个正值。如果级别降低,则添加一个负值。 主线程可以更改它的任何线程的优先权。如果更改优先类,将同时改变进程中所有线程的优先权,同时更改子进程的线程还没有更改的默认优先权。

 

18.提升线程优先权   前台进程:与用户交互的进程。应得到充足的处理器时间。   后台进程:其他所有进程。 【注意】后台进程的优先权应比前台进程的优先权设置的低一些。前台进程的线程先于后台进程的线程使用处理器。当后台进程变成前台进程时,该进程的所有线程都将被推进或提升(即允许前台线程有能力对用户迅速做出反应)。   19.优先权倒置现象:当一个较低优先权线程锁定一个资源,而该资源正是另一个较高优先权线程所需要的,那么较低优先权线程将不能释放资源,较高优先权的线程(但比需要该资源的线程优先权低)阻塞了低优先权线程。  

  优先权倒置现象解析:线程2增加资源计数Count,而线程3减少Count。线程3优先权最高,线程2优先权最低。线程3首先执行。线程3和线程1阻塞允许线程2来执行。线程2锁定并增加Count。在它能够取消锁定Count前,线程3和线程1变成非阻塞。因为线程3具有较高优先权,所以它被分配给处理器。线程2被抢占但没有释放资源。线程3试图锁定Count,但不能实现,因为线程2没有被释放。线程3再次变成阻塞。线程1被分配给处理器,这将阻止线程2重新获得处理器以释放资源。而线程3则继续处于阻塞状态,直到资源释放为止。线程1阻塞时,线程2能够释放资源。

 

防止优先权倒置解决方法:协议(protocol)临时提升锁定资源的线程的优先权,防止被抢占,使得资源可以释放。该优先权必须高于或等于能够锁定该资源的所有线程的优先权。锁定资源意味着当使用该线程时,其他线程就不能访问它,直到使用它的线程完成。当资源被某个线程使用时,试图访问该资源的其他线程被阻塞。使用资源的该线程优先权被提升到高于阻塞线程,让这个线程使用和释放资源而不被抢占。

解决方法1:当线程2锁定Count时,优先权提升到高于线程3的优先权,允许线程2锁定、更新、取消锁定释放资源,而不被线程3或线程1抢占。在释放count后,线程2的优先权被减小到它的原始级别。

 

 

 解决方法2:直到另一个线程3试图锁定Count时,才提升线程2的优先权。当线程3试图锁定资源但失败时,线程2被抢占。它被阻塞。线程2的优先权被提升到高于线程3的优先权。

 

20.优先权原则:    给进程和线程分配优先权必须小心,会影响系统整个操作性能,它可能会抢占网络通信,导致数据丢失。一般来说,大部分用户进程和线程归入普通或常规优先权之列。    Win32系统中:    空闲(1~15)——系统监视(针对那些周期性检查系统或应用程序而不影响其他任务执行的任务)    普通(1~15)——管理前台交互任务和后台任务(默认优先级7~9)系统自动将它的所有线程优先级提升1。      高(1~15)——与任务管理器线程(处于同一优先权)竞争,仅在需要时使用。    实时(16~31)——与硬件直接通信的极高优先权。此级别极少使用。大部分系统线程在较低优先权上执行。如果应用程序与硬件直接对话或针对非中断短期执行的线程才使用此级别。

 

21.线程是处理器上可规划单元。

   系统中激活进程的所有线程都被放在就绪队列中,根据他们的优先权进行混合和排序。当一个线程正在执行,而另一个较高优先权线程变成可用时,运行线程被抢占。    同一个进程中的两个线程发生上下文切换,不同进程中的线程也可以进行上下文切换。    每个线程独立运行,同时竞争处理器时间。每个线程都有自己的上下文和线程状态。

22.具有多线程的进程状态:  (1)如果进程的所有线程阻塞,只有一个线程是激活的,则进程为激活。  (2)进程的所有线程为挂起,只有一个线程是激活的,则进程为激活。  即一个线程决定整个进程状态,让进程阻塞或挂起,进程的所有线程必须被阻塞或挂起。

 

23.线程与资源:对于内存、处理器和其他全局分配资源,线程都要与进程的其他线程以及其他进程的线程来竞争。一些操作系统允许用户设置单个线程的竞争域。

 

24.线程实现模型:用户级线程、核心级线程、混合级线程。    用户级线程:具有多对一线程关系(多个线程对应一个进程的线程)。不由操作系统管理,开销低。如C++任务库。缺点:不能通过多个处理器使用系统,不能分配给不同的处理器,只能分配给该进程的单处理器上。    核心级线程:一对一的线程关系(每创建一个线程,都存在一个创建核心线程的系统调用),由操作系统管理。开销大。只要资源足够,可以给它自己的处理器分配线程。    混合级线程:线程被看做一个用户级线程但被映射到核心级实体。只创建必须数量的核心线程。创建一个线程池(thread pool)从一个线程到另一个线程的模拟切换。    

转载请注明原文地址: https://www.6miu.com/read-1100330.html

最新回复(0)