linux 之线程

线程

为什么有线程

  • 线程的引入:60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
  • 因此在80年代,出现了能独立运行的基本单位——线程(Threads)。

线程的定义

  • 线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
  • 线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

进程与线程之间的关系

linux 之线程

线程的特点

  • 在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
  1. 轻型实体
    线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
- 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
  1. 独立调度和分派的基本单位。
    在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
  2. 可并发执行。
    在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
  3. 共享进程资源。
    在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

与进程的比较

  • 进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。
  • 另外,进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
  • 与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。
  • 线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其他线程的相关变量。
  • 通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
    线程与进程的区别可以归纳为以下4点:
    1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
    2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
    3)调度和切换:线程上下文切换比进程上下文切换要快得多。
    4)在多线程OS中,线程不是一个可执行的实体。
    linux 之线程

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小的多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将 I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

  • 性能损失

    • 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如
      果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低

    • 编写多线程需要更全面更深入的考虑,在一个多线程程序中,因时间分配上的细微偏差
      或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制

    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高

    • 编写与调试一个多线程程序比单线程程序困难得多

线程的相关函数

  • 获取线程ID:
函数原型:pthread_self();

形参列表 无参数,

返回值为当前的线程ID号,并且返回值数据类型为pthread_t ,该类型在系统中可能为不同的类型.

ps:编译时需要链接第三方库    -l pthread
  • 创建一个进程:
函数原型:int ptherad_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

形参列表: thread为创建进程的ID号,定义一个pthread_t 类型的变量,在实参中加取址符,回写到变量中。

第二个参数 attr 为新线程的属性,默认类型NULL

第三个参数为新进程的入口函数,为函数名

第四个参数arg为传递给新线程的参数。

返回值: 成功返回0,失败返回-1.
  • 退出线程:
函数原型:void pthread_exit(void *retval);

形参列表: retval全称return value,可以在其他线程调用pthread_join取得该返回值。

返回值: 无返回值。
  • 线程等待以及资源的释放:
函数原型:int pthread_join(pthread_t tid, void **rval);

形参列表: tid 为需要等待的线程ID

rval为等待的线程的返回值。也就是pthread_exit函数的形参。

返回值:成功返回0,失败返回错误码。
  • 线程的取消:
函数原型:int pthread_cancel(pthread_t thread);

形参列表: thread为取消的线程的ID号,搭配int pthread_setcancelstate(int state, int *oldstate) 函数可以修改线程对于cancel信号的处理方式,
state有两种值:
	PTHREAD_CANCEL_ENABLE(缺省:响应)、PTHREAD_CANCEL_DISABLE(忽略)

oldstate 如果不为NULL,则存入原来的cancel状态以便恢复。搭配 int pthread_setcanceltype(int type, int *oldtype),可以设置取消的执行时机,
type有两种取值:PTHREAD_CANCEL_DEFFERED(下个取消点)、
PTHREAD_CANCEL_ASYCHRONOUS(立即取消)

第二个参数oldtype如果不为NULL,则存入原来的取消动作类型值

返回值: 成功返回0,失败返回-1
  • 线程终止清理函数:
函数原型:void pthread_cleanup_push(void (*routine) (void *), void *arg)
void pthread_cleanup_pop(int execute)

必须成对出现,否则编译不会通过,原因是有一个宏定义,一个函数有左大括号,另一个右大括号。

参数列表: 第一个函数:routune为函数名,arg为传入函数的参数,执行后代表一个函数入栈。

第二个函数:execute可以为0和非0值 ,当为0是,仅仅在本线程调用pthread_exit以及其他函数对本线程

调用cancel函数时弹出清理函数并执行。

若为非0值,执行后都会弹出函数并执行。

无返回值
  • 互斥锁函数:

互斥锁初始化有两种方式,一种是静态宏设置,一种是动态函数设置

静态锁设置  :

快速静态锁:pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;普通锁,在嵌套锁时会出现死锁

递归锁:pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;可以嵌套上锁

检错锁:pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;在出现嵌套锁时返回一个错误信息,不会死锁。


动态锁设置:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)

第一个参数为初始化的锁名称,第二个参数为锁的属性,对应快速锁,嵌套锁,检错锁。
可以定义一个pthread_mutexattr_t mutexattr; 通过修改mutexattr.__mutexkind = PTHREAD_MUTEX_RECURSIVE_NP修改为嵌套锁,

修改mutexattr.__mutexkind = PTHREAD_MUTEX_ERRORCHECK_NP修改为检错锁,使用NULL则缺省为快速锁。

对于锁的操作:加锁、解锁、测试加锁、销毁锁

加锁:int pthread_mutex_lock(pthread_mutex_t *mutex)   不管是哪种类型的锁,都不可能被两个线程

同时得到,必须等解锁。普通锁可以是同进程的任何线程,检错锁必须加锁者解锁,嵌套锁由加锁者解锁。


解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex)    。对于快速锁,则解除锁定。对于嵌套锁,使锁上的技术减一,表示上了两层。对于检错锁,如果锁是本线程加的,则解除锁,否则啥也不干。

测试加锁: int pthread_mutex_trylock(pthread_mutex_t *mutex)   使用测试加锁,则不会挂起阻塞。

销毁锁:int pthread_mutex_destroy(pthread_mutex_t *mutex);  销毁锁,意味着释放所占用的资源,而且要求锁处于开放状态。
  • 线程同步函数:条件变量和互斥锁结合,可以实现线程同步。
条件变量:静态创建和动态创建

静态创建:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态创建:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

其属性在Linux Threads内没有实现,所以属性为NULL。

注销条件变量:int pthread_cond_destroy(pthread_cond_t *cond);  只有在没有线程在该条件变量等待时才可以注销该变量。

等待条件变量: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

该函数执行时会上锁并访问,假如没有收到信号,则阻塞,直至有信号到来,则解锁继续执行后续动作。

参数一:等待的条件变量        参数二:用于上锁的互斥锁。

还有一种计时等待,int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec
*abstime);   表示经历abstime时间后,即使没有信号到来,阻塞也被解除。

激发有两种:pthread_cond_signal(cond)**一个等待该条件的线程。而pthread_cond_broadcast()则**所有等待线程。