23、线程基本概念

  与进程类似,线程是允许应用程序并发执行多个任务的一种机制。一个进程包含多个线程,同一进程中的所有县城均会独立执行相同程序,且共享一份全局内存区域。

1、线程的基本概念

  在一个进程中的多个执行路线叫做线程,更准确的定义是:线程是进程内部的一个控制序列。每个进程至少有一个执行线程(到目前为止,所涉及的所有进程都只有一个线程)。

  引入多线程之后,在程序设计时就可以把进程设计成在同一时刻能够执行多个任务,每个线程处理各自独立的任务,这样做有以下优点:

(1)通过为每种事件类型单独分配单独的处理线程,可以简化处理异步事件的代码。每个线程在进行任务处理时可以采用同步编程的模式,同步编程比异步编程模式简单的多。

(2)多个线程可以自动地访问相同的存储地址空间和文件描述符。

(3)有些问题可以分解,从而提高整个程序的吞吐量。在只有一个控制线程的情况下,一个单线进程要完成多个任务,只需要把这些任务串行化。但有多个控制线程时,相互独立任务的处理就可以交叉进行,此时只需要为每个任务分配一个单独的线程。只有在两个任务的处理过程互不依赖的情况下,两个任务才可以交叉执行。

(4)交互的程序同样可以通过使用多线程来改善响应时间,多线程可以把程序中处理用户输入和输出的部分与其他部分分开。

  有人把多线程的程序设计于多处理器和多核系统联系起来,但是即使运行在单个处理其上,也能得到多线程编程模型的好处。处理器的数量并不影响程序结构,所以不管处理器的数量有多少,程序都可以通过使用线程得到简化。而且,即使多个程序在执行串行话任务时会收到阻塞,但由于某些线程阻塞时另外一些线程仍然可以运行,因此在单处理器上运行多线程程序可以改善响应时间和吞吐量。

  同一程序中的所有线程均会独立执行相同的程序,且共享同一份全局内存区域,包括初始化的数据段、未初始化的数据段以及堆内存段。除全局内存之外,线程还共享了一些其他属性(对于进程而言,这些属性是全局性的,而非针对某个特定线程)

(1)进程ID和父进程ID。

(2)进程组ID和会话ID。

(3)控制终端。

(4)进程凭证(用户ID和组ID)。

(5)打开的文件描述符。

(6)使用函数fcntl()创建的记录锁。

(7)信号处理

(8)文件系统的相关信息,如文件权限掩码、当前工作目录及根目录。

(9)间隔定时器和POSIX定时器。

(10)System V信号量撤销值。

(11)资源限制。

(12)CPU时间消耗。

(13)资源消耗。

(14)nice值。

  各线程所独有的属性,包括线程ID、信号掩码、线程特有数据、备选信号线、errno变量、悬浮型环境、实时调度策略、CPU亲和力、能力和栈(本地变量和函数的调用链接信息)。

2、线程与进程

  将并发程序实现设计为多线程还是多进程,需要根据实际的需求。相对于多进程而言,多线程具有如下优势:

 (1)线程间的数据共享很简单,相比之下,进程间的数据共享需要更多的投入(例如,创建共享内存段或者使用管道)。

 (2)创建线程要快于创建进程,线程的上下文切换一般比进程短。

  相对于多进程而言,多线程的劣势如下:

(1)多线程编程时,需要确保调用线程安全的函数,或者以线程安全的方式来调用函数,多进程应用则无需关注这些。

(2)某个线程中的bug(例如,通过一个错误的指针来修改内存)可能会危及该进程的所有线程,因为他们共享者相同的地址空间和其他属性,相比之下,进程间的隔离更加彻底。

(3)每个进程都在争用宿主进程优先的虚拟地址空间,一旦每个线程栈及线程特有数据消耗掉进程虚拟地址空间的一部分,则后续线程都将无缘使用这些区域。尽管有效地址空间很大,但当进程分配大量现场,亦或线程使用大量内存时,这一因素的限制作用也就凸显出来了。与之相反,每个进程都可以使用全部的有效虚拟内存,仅受制于实际内存和交换空间。

  此外,对于选择多进程还是多线程,还应考虑以下因素:

(1)在多线程应用中处理信号时,需要格外小心作为通则,一般建议避免在多线程程序中处理信号0。

(2)在多线程应用中,所有线程必须运行同意程序(即使可能位于不同函数中;对于多进程应用,不同的进程则可以运行不同的程序。

(3)处理数据,线程还可以共享其他信息(例如,数据描述符、信号处理、当前工作目录,以及用户ID和组ID)。

  在多线程程序中,多线程并发执行同一程序。所有程序共享相同的全局和堆变量,但每个线程都配有用来存放局部变量的私有栈。同一进程中的线程还可共享其他属性,包括进程ID、打开的文件描述符、信号处理、当前工作目录以及资源限制等。

  线程与进程的关键区别在于:线程比进程更易于共享信息。这也是许多应用程序舍弃进程而取线程的主要原因。对余某些操作来说,线程可以提供更好的性能,但是,在程序设计的进程与线程之中,这并不起决定性作用。

PTHREADS API背景

  在20世纪80年代末90年代初,存在者数种不同的线程接口。1995年,POSIX.1.c对POSIX线程API做了标准化处理,该标准后来为SUSv3所接纳。

1、线程数据类型

  Pthreads API定义了一系列数据类型,如下表所示:

  SUSv3并未规定如何实现这些数据类型,可移植的程序应将其视为“不透明”数据,也就是说应避免对此类数据类型的变量的结构或内容产生依赖。尤其应该注意的是:不能使用C语言的比较操作去比较这些类型的变量

 2、线程和errno

  在传统的UNIX API中,errno虽是全局整型变量,却无法满足多线程程序的需求。如果线程调用的函数通过全局errno返回错误,则会与其他发起函数调用并检查errno的线程混淆在一起,这将引发竞争条件。因此,在多线程程序中,每个线程都有属于自己的errno。

  在Linux系统中,线程特有的errno实现方式与大多数UNIX系统实现errno的方式相类似:将errno定义为一个宏,可展开为函数调用,该函数返回一个可修改的左值,且为每个线程所独有。errno机制在保留传统UNIX API报错方式的同时,也适应了多线程的环境。

3、pthread函数返回值

  从系统调用和库函数中返回系统的传统做法是:返回0表示成功;返回-1表示失败,并设置errno以标识错误原因。pthread API则有所不同,所有Pthreads函数返回0表示成功,返回一正值表示失败。这一失败时的返回值与传统UNIX系统调用置于errno中的值的含义相同。

  由于多线程程序对errno的每次引用都会带来函数调用的开销,因此,后面的实例并不会直接将Pthreads赋给errno,而是使用一个中间变量,通过诊断函数errExitEN()对其的调用来实现,如下所示:

pthread_t *thread;
int s;

s=ppthread_creat(&thread,NULL,func,&arg);
if(s!=0)
    errExitEN(s;"pthread_creat()");

4、编译Pthreads程序

  Linux系统平台上,编译在调用Pthread API的程序时,需要设置cc -pthread的编译选项,使用该选项的效果如下:

(1)定义_REENTRANT预处理宏,会公开对少数可重入函数的声明。

(2)程序会于库libpthread进行链接(相当于-lpthread)

参考资料

POSIX线程

原文地址:https://www.cnblogs.com/noticeable/p/8540734.html