【Linux应用编程】Day12线程

进程与线程:
【Linux应用编程】Day10_进程 一文详细剖析进程,从基本概念到创建再到进程操作直至消亡
【进程间通信机制】管道和 FIFO、信号、消息队列、信号量、共享内存、套接字(Socket)
【Linux应用编程】Day12线程
【线程同步机制】Day13线程同步:互斥锁、条件变量、自旋锁、读写锁

线程

  • 与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度;

  • 事实上,系统调度的最小单元是线程、而并非进程。

⚫ 线程的基本概念,线程 VS 进程;

⚫ 线程标识;

⚫ 线程创建与回收;

⚫ 线程取消;

⚫ 线程终止;

⚫ 线程分离;

⚫ 线程同步技术;

⚫ 线程安全。


线程概述

线程定义

在这里插入图片描述

线程创建

在这里插入图片描述

线程特点

在这里插入图片描述

线程与进程

在这里插入图片描述

并发和并行

个人理解侧重点:

串行:顺序
并行:同时运行
并发:时间片轮转 
串行
  • 指的是一种顺序执行。

串行运行:依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元;

在这里插入图片描述

并行
  • 指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行;

在这里插入图片描述

  • 并行不一定同时开始运行、同时结束运行**,只需在某一个时间段上存在多个任务被多个执行单元同时运行;

在这里插入图片描述

并发
  • 相比于串行和并行,并发强调的是一种时分复用

  • 与串行的区别

    不必等待上一个任务完成再做下一个任务,可打断当前执行的任务切换执行下一个任务(时分复用)

  • 定义:在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行)

在这里插入图片描述

典例

在这里插入图片描述


线程 ID

  • 每个线程也有其对应的标识,称为线程 ID;
  • 进程与线程ID
    • 进程 ID 在整个系统中是唯一的
    • 但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义
    • 进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数;
    • 线程 ID 使用 pthread_t 数据类型来表示;
作用

线程 ID 在应用程序中非常有用,原因如下:

⚫ 很多线程相关函数,譬如后面将要学习的 pthread_cancel()、pthread_detach()、pthread_join()等,它
们都是利用线程 ID 来标识要操作的目标线程;
⚫ 在一些应用程序中,以特定线程的线程 ID 作为动态数据结构的标签,这某些应用场合颇为有用,
既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具
体线程。
pthread_self()
  • 一个线程可通过库函数 pthread_self()获取自己的线程 ID

原型

#include <pthread.h>
pthread_t pthread_self(void);
pthread_equal()
  • 可通过库函数检查两个线程 ID 是否相等

原型

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
/*
返回值:
	如果两个线程 ID t1 和 t2 相等,则 pthread_equal()返回一个非零值;
	否则返回 0。

重要性

Linux 系统使用无符号长整型(unsigned long int)来表示 pthread_t 数据类型,

其它系统当中,不一定无符号长整型,所以**必须将 pthread_t 作为一种不透明的数据类型加以对待**;
所以 pthread_equal()函数用于比较两个线程 ID 是否相等是有用的:

创建线程

  • 启动程序时,创建的进程只是一个单线程的进程,称之为初始线程或主线程;
  • 创建成功后的了调度
    • 线程创建成功,新线程就会加入到系统调度队列中
    • 获取到 CPU 之后就会立马从 start_routine()函数开始运行该线程的任务;
  • 调度问题☆☆☆☆☆
pthread_create()
  • 库函数 **pthread_create()**负责创建一个新的线程,创建出来的新线程被称为主线程的子线程

原型

#include <pthread.h>
int pthread_create
(
		pthread_t *thread, 
		const pthread_attr_t *attr, 
		void *(*start_routine) (void *), 
		void *arg
);
/*
参数:
	thread:pthread_t 类型指针,当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数	 thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
	
	attr:pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区,
	pthread_attr_t 数据类型定义了线程的各种属性,关于线程属性将会在 11.8 小节介绍。
	如果将参数 attr 设置为 NULL,那么表示将线程的所有属性设置为默认值,以此创建新线程。
	
	start_routine:参数 start_routine 是一个函数指针,指向一个函数,
	新创建线程从 start_routine()函数开始运行,该函数返回值类型为void *,
	并且该函数的参数只有一个void *,其实这个参数就是pthread_create()函数的第四个参数 arg。
	如果需要向 start_routine()传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构
	体对象的地址作为 arg 参数传入。
	
	arg:传递给 start_routine()函数的参数。
	一般情况下,需要将 arg 指向一个全局或堆变量,
	意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。
	当然也可将参数 arg 设置为 NULL,表示不需要传入参数给 start_routine()函数。

返回值:
	成功返回 0;
	失败时将返回一个错误号,并且参数 thread 指向的内容是不确定的。
注意: 
	pthread_create()在调用失败时通常会返回错误码,它并不像其它库函数或系统调用一样设置 errno,
	每个线程都提供了全局变量 errno 的副本,这只是为了与使用 errno 到的函数进行兼容,
	在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局变量,这样可以把错误		的范围限制在引起出错的函数中。	

典例

将 pthread_t 作为一种不透明的数据类型加以对待;

但是在示例代码中需要打印线程 ID,所以要明确其数据类型

示例代码中使用了 printf()函数打印线程 ID 时,将其作为 unsigned long int 数据类型

在 Linux系统下,确实是使用 unsigned long int 来表示 pthread_t,所以这样做没有问题!

				/*示例代码 11.3.1 pthread_create()创建线程使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
     printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
     return (void *)0;
}

int main(void)
{
     pthread_t tid;
     int ret;
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    
     if (ret) {
         fprintf(stderr, "Error: %s\n", strerror(ret));
         exit(-1);
     }
    
     printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
     sleep(1);
     exit(0);
}

主线程休眠了 1 秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创

建的线程还没有机会运行,整个进程就结束了。

在主线程和新线程中,分别通过 getpid()和 pthread_self()来获取进程 ID 和线程 ID,将结果打印出来!

在这里插入图片描述

链接库的文件问题:

gcc -o testApp testApp.c -lpthread
/*使用-l 选项指定链接库 pthread,原因在于 pthread 不在 gcc 的默认链接库中,所以需要手动指定*/

在这里插入图片描述


终止线程

  • 新线程的启动函数(线程 start 函数)new_thread_start()通过 return 返回之后,意味着该线程已经终止;

  • 终止线程的方式

    • 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
    • 线程调用 pthread_exit()函数(进程中的任意线程调用 exit()、_exit()、Exit(),会导致整个进程终止);
    • 调用 pthread_cancel()取消线程(后文介绍);
pthread_exit()

在这里插入图片描述

原型

#include <pthread.h>
void pthread_exit(void *retval);
/*
参数:
	参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,
	该返回值可由另一个线程通过调用 pthread_join()来获取;
	
	如果线程是在 start 函数中执行 return 语句终止,
	那么 return 的返回值也是可以通过 pthread_join()来获取的。
	
	参数 retval 所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效;
	出于同样的理由,也不应在线程栈中分配线程 start 函数的返回值。

典例

新线程中调用 sleep()休眠,保证主线程先调用 pthread_exit()终止,休眠结束后新线程也调用pthread_exit()终止

					/*示例代码 11.4.1 pthread_exit()终止线程使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
     printf("新线程 start\n");
     sleep(1);
     printf("新线程 end\n");
     pthread_exit(NULL);
}
int main(void)
{
     pthread_t tid;
     int ret;
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    
     if (ret) 
     {
         fprintf(stderr, "Error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("主线程 end\n");
     pthread_exit(NULL);
     exit(0);
}

在这里插入图片描述


回收线程

  • 父、子进程中,父进程通过 wait()(或waitpid)阻塞等待子进程退出并获取其终止状态,回收子进程资源;

  • 在线程当中,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;

pthread_join()

原型

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
/*
参数:
	thread:pthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程;
	retval:如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过
	pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到		*retval 所指向的内存区域;
	如果目标线程被 pthread_cancel()取消,则将 PTHREAD_CANCELED 放在*retval 中。
	如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL。

返回值:
	成功返回 0;
	失败将返回错误码。
	
调用 pthread_join()函数将会以阻塞的形式等待指定的线程终止;
如果该线程已经终止,则 pthread_join()立刻返回;
如果多个线程同时尝试调用 pthread_join()等待指定线程的终止,那么结果将是不确定的。

典例

pthread_create()创建新线程之后,新线程执行 new_thread_start()函数,而在主线程中调用pthread_join()阻塞等待新线程终止

新线程终止后,pthread_join()返回,将目标线程的退出码保存在*tret 所指向的内存中.

					/*示例代码 11.5.1 pthread_join()等待线程终止*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void *new_thread_start(void *arg)
{
     printf("新线程 start\n");
     sleep(2);
     printf("新线程 end\n");
     pthread_exit((void *)10);
}
int main(void)
{
     pthread_t tid;
     void *tret;
     int ret;
    
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }
    
    //阻塞等待新线程终止
     ret = pthread_join(tid, &tret);
     if (ret) 
     {
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("新线程终止, code=%ld\n", (long)tret);
    
     exit(0);
}
pthread_join()与 waitpid()
  • 线程之间关系是对等的

    • 进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止

      譬如,如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join()等待
      线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止;
      
    • 进程间层次关系不同

      父进程如果使用 fork()创建了子进程,那么它也是**唯一能够对子进程调用 wait()的进程**,线程之间
      不存在这样的关系。
      
  • 阻塞与非阻塞调用

    • 不能以非阻塞的方式调用 pthread_join();

    • 对于进程,调用 waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待;

僵尸线程

若线程并未分离(detached,后文介绍),则必须使用 pthread_join()来等待线程终止,回收线程资源;

	·如果线程终止后,其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;
	·同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。
	·当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸
线程同样也会被回收。

取消线程

取消线程:向指定的线程发送一个请求,要求其立刻终止、退出

  • 通常情况:

    进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出;
    
  • 程序设计需求:

    需要向一个线程发送一个请求,要求它立刻退出,把这种操作称为取消线程;
    譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出
    
pthread_cancel()
  • 通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求

  • 发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出;

    行为表现为如同:
    	调用参数为 PTHREAD_CANCELED(其实就是(void *)-1)的pthread_exit()函数;
    注意:
    	线程可以设置不被取消或者控制如何被取消,所以pthread_cancel()并不会等待线程终止,仅是提出请求
    

原型

#include <pthread.h>
int pthread_cancel(pthread_t thread);
/*
参数 
	thread 指定需要取消的目标线程;
返回值:
	成功返回 0;
	失败将返回错误码。

典例

主线程创建新线程,新线程 new_thread_start()函数直接运行 for 死循环;

主线程休眠一段时间后,调用pthread_cancel()向新线程发送取消请求,接着再调用 pthread_join()等待新线程终止、获取其终止状态,将线程退出码打印出来。

				/*示例代码 11.6.1 pthread_cancel()取消线程使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
     printf("新线程--running\n");
     for ( ; ; )
     	sleep(1);
     return (void *)0;
}

int main(void)
{
     pthread_t tid;
     void *tret;
     int ret;
    
     /* 创建新线程 */
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }
     sleep(1);
    
     /* 向新线程发送取消请求 */
     ret = pthread_cancel(tid);
     if (ret) 
     {
         fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
         exit(-1);
     }
    
     /* 等待新线程终止 */
     ret = pthread_join(tid, &tret);
     if (ret) 
     {
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("新线程终止, code=%ld\n", (long)tret);
     
     exit(0);
}

在这里插入图片描述

取消状态以及类型
  • 默认情况:线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程;

  • 可择情况(包括默认情况):可设置线程的取消性状态和类型

    • pthread_setcancelstate():状态,可取消和不可取消,不可取消则挂起!!!
    • **pthread_setcanceltype():**类型,状态为可取消,则取决于类型
  • fork继承,exec重置

    • 某个线程调用 fork()创建子进程时,子进程会继承调用线程的取消性状态和取消性类型;
    • 当某线程调用exec函数时,会将新程序主线程的取消性状态和类型重置为默认值,也就是PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED。
pthread_setcancelstate()
  • 将调用线程的取消性状态设置为参数 state 中给定的值,并将线程之前的取消性状态保存于 oldstate 指定缓冲区

  • pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作

原型

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
/*
参数:
	state:		线程的取消性状态设置为参数 state 中给定的值
	oldstate:	线程之前的取消性状态保存**于 oldstate 指定缓冲区
				 如果对之前的状态不感兴趣,Linux 允许将参数 oldstate 设置为 NULL;
返回值:
	成功将返回 0;
	失败返回非 0 值的错误码。
参数 state 必须是以下值之一:
	⚫ PTHREAD_CANCEL_ENABLE: 线程可以取消,这是新创建的线程取消性状态的默认值,
							   所以新建线程以及主线程默认都是可以取消的。
	⚫ PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,
							   则会将请求挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE。

典例

新线程 new_thread_start调用 pthread_setcancelstate()将线程取消性状态设为PTHREAD_CANCEL_DISABLE

				/*示例代码 11.6.2 pthread_setcancelstate()使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void *new_thread_start(void *arg)
{
     /* 设置为不可被取消 */
     pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    
     for ( ; ; ) 
     {
         printf("新线程--running\n");
         sleep(2);
 	 }
    
 	 return (void *)0;
}

int main(void)
{
     pthread_t tid;
     void *tret;
     int ret;
    
     /* 创建新线程 */
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }
    
     sleep(1);
    
     /* 向新线程发送取消请求 */
     ret = pthread_cancel(tid);
     if (ret) 
     {
         fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
         exit(-1);
     }
    
     /* 等待新线程终止 */
     ret = pthread_join(tid, &tret);
     if (ret) 
     {
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("新线程终止, code=%ld\n", (long)tret);
    
     exit(0);
}

新线程 new_thread_start()函数中调用 pthread_setcancelstate()将自己设置为不可被取消,主线程延时 1 秒钟之后调用 pthread_cancel()向新线程发送取消请求,那么此时新线程是不会终止的,pthread_cancel()立刻返回之后进入到 pthread_join()函数,那么此时会被阻塞等待新线程终止。

在这里插入图片描述

pthread_setcanceltype()
  • 状态为可取消,处理则取决于类型,由参数 type 指定,之前的取消性类型保存在参数 oldtype 指定缓冲区;
  • pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);

/*
参数:
	type:	状态为可取消,处理则取决于类型,由参数 type 指定;
	oldtype:之前的取消性类型保存在参数 oldtype 指定缓冲区;
返回值:
	成功将返回 0;
	失败返回非 0 值
的错误码。
参数 type 必须是以下值之一:
		⚫ PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程继续运行,取消请求被挂起,直到线程到达某个取									  消点(cancellation point,将在 11.6.3 小节介绍)为止,
									这是所有新建线程包括主线程默认的取消性类型。
		⚫ PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)取消线程,
										这种取消性类型应用场景很少;
取消点
  • 基于线程可取消状态下且取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时有效;

  • 当线程抵达某个取消点,取消请求才会起作用;

    ⚫所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;
    ⚫在没有出现取消点时,取消请求是无法得到处理的;
    ⚫究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,	    此时终止线程将可能会导致出现意想不到的异常发生。
    
取消点函数
  • 表列外,还有大量函数,系统实现可将其作为取消点: man 手册可进行查询“man 7 pthreads

在这里插入图片描述

  • 线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;

  • 除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)。

典例

在这里插入图片描述

线程可取消性检测
问题:
假设线程执行的是一个不含取消点的循环(譬如 for 循环、while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它;
解决:
该线程必须可以被其它线程通过发送取消请求的方式终止,pthread_testcancel()产生一个取消点;
pthread_testcancel()

产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。

原型

#include <pthread.h>
void pthread_testcancel(void);

典例

在new_thread_start 函数的 for 循环体中执行 pthread_testcancel()函数;

			 		/*示例代码 11.6.4 使用 pthread_testcancel()产生取消点*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void *new_thread_start(void *arg)
{
     printf("新线程--start run\n");
     for ( ; ; ) 
     {
         pthread_testcancel();
     }
     return (void *)0;
}

int main(void)
{
     pthread_t tid;
     void *tret;
     int ret;
    
     /* 创建新线程 */
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }
     sleep(1);
    
     /* 向新线程发送取消请求 */
     ret = pthread_cancel(tid);
     if (ret) 
     {
         fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
         exit(-1);
     }
    
     /* 等待新线程终止 */
     ret = pthread_join(tid, &tret);
     if (ret) 
     {
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("新线程终止, code=%ld\n", (long)tret);
     
     exit(0);
}

在这里插入图片描述


分离线程

  • 默认情况:当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源

  • 线程分离:程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并移除

pthread_detach()
  • 将指定线程进行分离;

  • 一个线程既可以将另一个线程分离,同时也可以将自己分离;

  • 一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态,此过程是不可逆的;

  • 处于分离状态的线程,当其终止后,能够自动回收线程资源。

原型

#include <pthread.h>
int pthread_detach(pthread_t thread);
/*
参数:
	thread 指定需要分离的线程;
返回值:
	成功将返回 0;
	失败将返回一个错误码。

典例

			/*示例代码 11.7.1 pthread_detach()分离线程使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 int ret;
 /* 自行分离 */
 ret = pthread_detach(pthread_self());
 if (ret) {
     fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
 return NULL;
 }
 printf("新线程 start\n");
 sleep(2); //休眠 2 秒钟
 printf("新线程 end\n");
 pthread_exit(NULL);
}
int main(void)
{
 pthread_t tid;
 int ret;
 /* 创建新线程 */
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
 exit(-1);
 }
 sleep(1); //休眠 1 秒钟
 /* 等待新线程终止 */
 ret = pthread_join(tid, NULL);
 if (ret)
 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
 pthread_exit(NULL);
}

主线程创建新的线程之后,休眠 1 秒钟,调用 pthread_join()等待新线程终止;新线程调用pthread_detach(pthread_self())将自己分离;

休眠 2 秒钟之后 pthread_exit()退出线程;主线程休眠 1 秒钟是能够确保调用 pthread_join()函数时新线程已经将自己分离了,所以按照上面的介绍可知,此时主线程调用pthread_join()必然会失败:

在这里插入图片描述


注册线程清理处理函数

  • 终止处理函数

    • atexit()函数注册进程终止处理函数,当进程调用 exit()退出时就会执行进程终止处理函数;

    • 线程终止退出时,也可去执行这样的处理函数,把这个称为线程清理函数(thread cleanup handler);

  • 与进程不同,一个线程可以注册多个清理函数,清理函数记录在栈中,每个线程都可以拥有一个清理函数栈;

  • 栈是一种先进后出的数据结构,执行顺序与注册(添加)顺序相反!!

pthread_cleanup_push()

向调用线程的清理函数栈中添加清理函数

pthread_cleanup_pop()

向调用线程的清理函数栈中移除清理函数

清理函数执行条件

线程执行以下动作时,清理函数栈中的清理函数才会被执行:

⚫ 线程调用 pthread_exit()退出时;
⚫ 线程响应取消请求时;
⚫ 用非 0 参数调用 pthread_cleanup_pop()

除了以上三种情况,其它方式终止线程将不会执行线程清理函数

譬如在线程 start 函数中执行return 语句退出时不会执行清理函数。

原型

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

/*
参数:
	routine:一个函数指针,指向一个需要添加的清理函数,routine()函数无返回值,只一 void *类型参数;
	arg:	当调用清理函数 routine()时,将 arg 作为 routine()函数的参数;

pthread_cleanup_pop()
	对应入栈和出栈,将清理函数栈中最顶层(也就是最后添加的函数,最后入栈)的函数移除;
参数
	execute:可以取值为 0,也可以为非 0;
			 如果为 0,清理函数不会被调用,只是将清理函数栈中最顶层的函数移除;
			 如果参数 execute 为非 0,则除了将清理函数栈中最顶层的函数移除之外,还会该清理函数。

典例1

通过宏来实现,可展开为分别由{和}所包裹的语句序列,所以必须在与线程相同的作用域中以匹配对的形式使用,必须一一对应着来使用:

pthread_cleanup_push(cleanup, NULL);
pthread_cleanup_push(cleanup, NULL);
pthread_cleanup_push(cleanup, NULL);
......
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);

否则会编译报错:

在这里插入图片描述

典例2

使用线程清理函数的例子,描述了其中所涉及到的清理机制

				/*示例代码 11.8.1 pthread_cleanup_push()注册线程清理函数*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void cleanup(void *arg)
{
     printf("cleanup: %s\n", (char *)arg);
}

static void *new_thread_start(void *arg)
{
     printf("新线程--start run\n");
     pthread_cleanup_push(cleanup, "第 1 次调用");
     pthread_cleanup_push(cleanup, "第 2 次调用");
     pthread_cleanup_push(cleanup, "第 3 次调用");
     sleep(2);
     pthread_exit((void *)0); //线程终止
     /* 为了与 pthread_cleanup_push 配对,不添加程序编译会通不过 */
     pthread_cleanup_pop(0);
     pthread_cleanup_pop(0);
     pthread_cleanup_pop(0);
}

int main(void)
{
     pthread_t tid;
     void *tret;
     int ret;
    
     /* 创建新线程 */
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }
    
     /* 等待新线程终止 */
     ret = pthread_join(tid, &tret);
     if (ret) 
         {
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("新线程终止, code=%ld\n", (long)tret);
    
     exit(0);
}
  1. 主线程创建新线程之后,调用 pthread_join()等待新线程终止;

  2. 新线程调用 pthread_cleanup_push()函数添加线程清理函数,调用了三次,但每次添加的都是同一个函数,只是传入的参数不同;

  3. 清理函数添加完成,休眠一段时间后,调用 pthread_exit()退出。

    之后还调用 3 次pthread_cleanup_pop(),在这里的目的仅仅只是为了与 pthread_cleanup_push()配对使用,否则编译不通过

在这里插入图片描述

可见:先添加到线程清理函数栈中的函数会后被执行,添加顺序与执行顺序相反

​ 将新线程中调用的 pthread_exit()替换为 return,在进行测试,发现并不会执行清理函数

典例3

在线程功能设计中,线程清理函数并不一定需要在线程退出时才执行,譬如当完成某一个步骤之后,就需要执行线程清理函数;

此时我们可以调用 pthread_cleanup_pop()并传入非 0 参数,来手动执行线程清理函数

示例代码 11.8.2 手动执行线程清理函数
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void cleanup(void *arg)
{
 	printf("cleanup: %s\n", (char *)arg);
}

static void *new_thread_start(void *arg)
{
     printf("新线程--start run\n");
     pthread_cleanup_push(cleanup, "第 1 次调用");
     pthread_cleanup_push(cleanup, "第 2 次调用");
     pthread_cleanup_push(cleanup, "第 3 次调用");
    
     pthread_cleanup_pop(1); //执行最顶层的清理函数
     printf("~~~~~~~~~~~~~~~~~\n");
     sleep(2);
     pthread_exit((void *)0); //线程终止
    
     /* 为了与 pthread_cleanup_push 配对 */
     pthread_cleanup_pop(0);
     pthread_cleanup_pop(0);
}

int main(void)
{
     pthread_t tid;
     void *tret;
     int ret;

     /* 创建新线程 */
     ret = pthread_create(&tid, NULL, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }

     /* 等待新线程终止 */
     ret = pthread_join(tid, &tret);
     if (ret) 
     {
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }
     printf("新线程终止, code=%ld\n", (long)tret);
    
     exit(0);
}

新线程调用 pthread_exit()前,先用 pthread_cleanup_pop(1)手动运行最顶层的清理函数,并将其从栈中移除:

在这里插入图片描述

  1. 调用 pthread_cleanup_pop(1)执行了最后一次注册的清理函数,

  2. 调用 pthread_exit()退出线程时执行2 次清理函数,

  3. 因为前面调用 pthread_cleanup_pop()已经将顶层的清理函数移除栈中,自然在退出时就不会再执行;


线程属性

  • Linux 为 pthread_attr_t 对象的每种属性提供了设置属性的接口以及获取属性的接口
  • pthread_attr_t 数据结构中包含的属性比较多,可能比较关注属性包括:线程栈的位置和大小线程调度策略和优先级以及线程的分离状态属性

在这里插入图片描述

熟悉初始化与销毁
pthread_attr_destroy()
pthread_attr_init()
  • 功能

    • 定义pthread_attr_t 对象之后 ,需要使用 pthread_attr_init()函数 对该对象进行初始化操作 ,

      将指定的 pthread_attr_t 对象中定义的各种线程属性初始化为它们各自对应的默认值

    • 当对象不再使用时, 需要使用pthread_attr_destroy()函数将其销毁

原型

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
/*
参数:
	attr:指向一个 pthread_attr_t 对象,即需要进行初始化的线程属性对象。
返回值:
	在调用成功时返回 0;
	失败将返回一个非 0 值的错误码。
线程栈属性

每个线程都有自己的栈空间,pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小;

pthread_attr_getstack()

栈的起始地址以及栈大小

pthread_attr_setstack()

对栈起始地址和栈大小进行设置

原型

#include <pthread.h>
int pthread_attr_setstack
    (
    pthread_attr_t *attr, 
     void *stackaddr, 
     size_t stacksize
    );
/*
参数:
	attr:参数 attr 指向线程属性对象。
	stackaddr:调用 pthread_attr_getstack()获取栈起始地址,并将起始地址信息保存于*stackaddr;
	stacksize:调用 pthread_attr_getstack()获取栈大小,并将栈大小信息保存于stacksize指向的内存;
返回值:
	成功返回 0;
	失败将返回一个非 0 值的错误码。
*/
int pthread_attr_getstack
    (
    const pthread_attr_t *attr, 
    void **stackaddr, 
    size_t *stacksize
	);
/*
参数:
	attr:参数 attr 指向线程属性对象。
	stackaddr:设置栈起始地址为指定值。
	stacksize:设置栈大小为指定值;
返回值:
	成功返回 0;
	失败将返回一个非 0 值的错误码
*/

想单独获取或设置栈大小、栈起始地址

#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);

典例

创建新的线程,将线程的栈大小设置为 4Kbyte

				/*示例代码 11.9.1 设置线程栈大小 pthread_attr_getstack()*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
    
static void *new_thread_start(void *arg)
{
 puts("Hello World!");
 return (void *)0;
}

int main(int argc, char *argv[])
{
 pthread_attr_t attr;
 size_t stacksize;
 pthread_t tid;
 int ret;
    
 /* 对 attr 对象进行初始化 */
 pthread_attr_init(&attr);
    
 /* 设置栈大小为 4K */
 pthread_attr_setstacksize(&attr, 4096);
    
 /* 创建新线程 */
 ret = pthread_create(&tid, &attr, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
 exit(-1);
 }
    
 /* 等待新线程终止 */
 ret = pthread_join(tid, NULL);
 if (ret) {
 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
 exit(-1);
 }
    
 /* 销毁 attr 对象 */
 pthread_attr_destroy(&attr);
 exit(0);
}
分离状态属性
  • 线程分离:

    如果对现已创建的某个线程的终止状态不感兴趣,
    可以使用pthread_detach()函数将其分离,那么该线程在退出时,操作系统会自动回收它所占用的资源;
    
  • 可以修改 pthread_attr_t 结构中的 detachstate 线程属性,让线程一开始运行就处于分离状态

    • 调用函数 pthread_attr_setdetachstate()设置 detachstate 线程属性
    • 调用函数pthread_attr_getdetachstate()获取 detachstate 线程属性
pthread_attr_setdetachstate()

参数 attr 指向 pthread_attr_t 对象,将detachstate 线程属性设置为参数 detachstate 所指定的值

pthread_attr_getdetachstate()

用于获取 detachstate 线程属性,将 detachstate 线程属性保存在参数detachstate 所指定的内存中;

原型

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
/*
参数 attr 指向 pthread_attr_t 对象;
参数 detachstate 取值如下:
	⚫ PTHREAD_CREATE_DETACHED:新建线程一开始运行便处于分离状态,以分离状态启动线程,
			无法被其它线程调用 pthread_join()回收,线程结束后由操作系统收回其所占用的资源;
	⚫ PTHREAD_CREATE_JOINABLE:这是 detachstate 线程属性的默认值,正常启动线程,
			可以被其它线程获取终止状态信息。

典例

分离状态启动线程

					/*示例代码 11.9.2 以分离状态启动线程*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

static void *new_thread_start(void *arg)
{
     puts("Hello World!");
     return (void *)0;
}

int main(int argc, char *argv[])
{
     pthread_attr_t attr;
     pthread_t tid;
     int ret;
    
     /* 对 attr 对象进行初始化 */
     pthread_attr_init(&attr);
    
     /* 设置以分离状态启动线程 */
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
     /* 创建新线程 */
     ret = pthread_create(&tid, &attr, new_thread_start, NULL);
     if (ret) 
     {
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }
     sleep(1);
    
     /* 销毁 attr 对象 */
     pthread_attr_destroy(&attr);
     exit(0);
}

线程安全hread-safe

编写多线程应用程序时,需要考虑到线程安全,确保编写的程序是一个线程安全的多线程应用程序;

线程栈

创建一个新的线程时,可以配置线程栈的大小以及起始地址;

每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量)都是分配在自己的线程栈中的,它们不会相互干扰

典例

主线程创建了 5 个新的线程,这 5 个线程使用同一个 start 函数 new_thread,该函数中定义了局部变量 number 和 tid 以及 arg 参数,意味着这 5个线程的线程栈中都各自为这些变量分配了内存空间,任何一个线程修改了 number 或 tid 都不会影响其它线程。

						/*示例代码 11.10.1 线程栈示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static void *new_thread(void *arg)
{
     int number = *((int *)arg);
     unsigned long int tid = pthread_self();
     printf("当前为<%d>号线程, 线程 ID<%lu>\n", number, tid);
     return (void *)0;
}

static int nums[5] = {0, 1, 2, 3, 4};

int main(int argc, char *argv[])
{
     pthread_t tid[5];
     int j;
    
     /* 创建 5 个线程 */
     for (j = 0; j < 5; j++)
     	pthread_create(&tid[j], NULL, new_thread, &nums[j]);
    
     /* 等待线程结束 */
     for (j = 0; j < 5; j++)
     	pthread_join(tid[j], NULL);//回收线程
     
    exit(0);
}

在这里插入图片描述

可重入函数
执行流与可重入函数定义
  • 先需要区分单线程程序和多线程程序

    • 单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;
    • 对于多线程程序而言,同一进程却存在多条独立、并发的执行流
    进程中执行流的数量除了与线程有关之外,与信号处理也有关联:
    因为信号是异步的,进程可能会在其运行过程中的任何时间点收到信号,进而跳转、执行信号处理函数,从而在一个单线程进程(包含信号处理)中形成了两条(即主程序和信号处理函数)独立的执行流。
    
  • 可重入函数

    如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数

    Tips:上面所说的同时指的是宏观上同时调用,实质上也就是该函数被多个执行流并发/并行调用,无特别说明,本章内容所提到的同时均指宏观上的概念。
    重入指的是同一个函数被不同执行流调用,前一个执行流还没有执行完该函数、另一个执行流又开始调用该函数了,其实就是同一个函数被多个执行流并发/并行调用,在宏观角度上理解指的就是被多个执行流同时调用。
    

典例

一个单线程与信号处理关联的程序:

main()函数中调用 signal()函数为 SIGINT 信号注册了一个信号处理函数 sig_handler,信号处理函数 sig_handler 会调用 func 函数;main()函数最终会进入到一个循环中,循环调用 func()。

						/*示例代码 11.10.2 信号与可重入问题*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void func(void)
{
 /*...... */
}

static void sig_handler(int sig)
{
 	func();
}

int main(int argc, char *argv[])
{
     sig_t ret = NULL;
    
     ret = signal(SIGINT, (sig_t)sig_handler);
     if (SIG_ERR == ret) 
     {
         perror("signal error");
         exit(-1);
     }
    
     /* 死循环 */
     for ( ; ; )
     	func();
    
     exit(0);
}

在这里插入图片描述

举例说明了函数被多个执行流同时调用的两种情况:

⚫ 在一个含有信号处理的程序当中,主程序正执行函数 func(),此时进程接收到信号,主程序被打断,跳转到信号处理函数中执行,信号处理函数中也调用了 func()。
⚫ 在多线程环境下,多个线程并发调用同一个函数。
可重入函数的分类

(正点原子作者自创)

  • 绝对的可重入函数:所谓绝对,指的是该函数不管如何调用,都刚断言它是可重入的,都能得到预期的结果。
  • 带条件的可重入函数:指满足某个/某些条件情况下,可断言该函数可重入,如何调用都能得到预期的结果。
总结

⚫很多的 C 库函数有两个版本:可重入版本和不可重入版本,可重入版本函数其名称后面加上了**“-r”**,用于表明该函数是一个可重入函数;而不可重入版本函数其名称后面没有“_r”,前面章节内容中也已经遇到过很多次了,譬如 asctime()/asctime_r()、ctime()/ctime_r()、localtime()/localtime_r()等。

⚫通过 man 手册可以查询到它们“ATTRIBUTES”信息;

譬如执行"man 3 ctime",在帮助页面上往下翻便可以找到,如下所示:
在这里插入图片描述

线程安全函数
  • 一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。

  • 线程安全函数包括可重入函数,可重入函数是线程安全函数的一个真子集

在这里插入图片描述

在这里插入图片描述

一次性初始化
  • 在多线程编程环境下,有些代码段只需要执行一次;

    譬如一些初始化相关的代码段,通常比较容易想到的就是将其放在 main()主函数进行初始化,这样也就是意味着该段代码只在主线程中被调用,只执行过一次。
    
  • 问题:当你写了一个 C 函数 func(),该函数可能会被多个线程调用,并且该函数中有一段初始化代码,该段代码只能被执行一次(无论哪个线程执行都可以)、如果执行多次会出现问题

    譬如下小节将要介绍的线程特有数据就需要有这样的需求,那我们如何去保证这段代码只能被执行一次呢(被进程中的任一线程执行都可以)?
    
pthread_once()函数
  • 保证这段代码只能被执行一次

  • 在多线程编程环境下,尽管 pthread_once()调用会出现在多个线程中,但该函数会保证 init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。

  • 如果在一个线程调用 pthread_once()时,另外一个线程也调用了 pthread_once,则该线程将会被阻塞等

    待,直到第一个完成初始化后返回

原型

  • 如果参数 once_control 指向的 pthread_once_t 类型变量,其初值不是 PTHREAD_ONCE_INIT,pthread_once()的行为将是不正常的;PTHREAD_ONCE_INIT 宏在<pthread.h>头文件中定义;

  • 通常在定义变量时会使用 PTHREAD_ONCE_INIT 宏对其进行初始化,譬如:

    pthread_once_t once_control = PTHREAD_ONCE_INIT;
    
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
/*
参数:
	once_control:这是一个 pthread_once_t 类型指针,在调用 pthread_once()函数之前,我们需要定义了一个 pthread_once_t 类型的静态变量,调用 pthread_once()时参数 once_control 指向该变量。
	init_routine:一个函数指针,参数 init_routine 所指向的函数就是要求只能被执行一次的代码段,
pthread_once()函数内部会调用 init_routine(),即使 pthread_once()函数会被多次执行,但它能保证 init_routine()仅被执行一次。
返回值:
	调用成功返回 0;
	失败则返回错误编码以指示错误原因。

典例

测试当 pthread_once()被多次调用时,init_routine()函数是不是只会被执行一次;

				/*示例代码 11.10.3 pthread_once()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static pthread_once_t once = PTHREAD_ONCE_INIT;

static void initialize_once(void)
{
	 printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
}

static void func(void)
{
     pthread_once(&once, initialize_once);//执行一次性初始化函数
     printf("函数 func 执行完毕.\n");
}

static void *thread_start(void *arg)
{
     printf("线程%d 被创建: 线程 ID<%lu>\n", *((int *)arg), pthread_self());
     func(); //调用函数 func
     pthread_exit(NULL); //线程终止
}

static int nums[5] = {0, 1, 2, 3, 4};
int main(void)
{
     pthread_t tid[5];
     int j;
    
     /* 创建 5 个线程 */
     for (j = 0; j < 5; j++)
     	pthread_create(&tid[j], NULL, thread_start, &nums[j]);
    
     /* 等待线程结束 */
     for (j = 0; j < 5; j++)
     	pthread_join(tid[j], NULL);//回收线程
    
     exit(0);
}

在这里插入图片描述

线程特有数据
  • 线程特有数据也称为线程私有数据,用于避免变量成为多个线程间的共享数据

  • 为每个调用线程分别维护一份变量的副本(copy),

    每个线程通过特有数据键(key)访问时,这个特有数据键都会获取到本线程绑定的变量副本;

    为每一个调用线程(调用某函数的线程,该函数就是我们要通过线程特有数据将其实现为线程安全的函数)分配属于该线程的私有数据区,为每个调用线程分别维护一份变量的副本。
    
pthread_key_create()
  • 在为线程分配私有数据区之前,需要调用 pthread_key_create()函数创建一个特有数据键(key);
  • 只需要在首个调用的线程中创建一次即可,所以通常会使用到上小节所学习的 pthread_once()函数;

原型

#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

参数:

key

调用该函数会创建一个特有数据键,并通过参数 key 所指向的缓冲区返回给调用者,参数 key 是一个 pthread_key_t 类型的指针,可以把 pthread_key_t 称为 key 类型。调用 pthread_key_create()之前,需要定义一个 pthread_key_t 类型变量,调用 pthread_key_create()时参数 key 指向 pthread_key_t 类型变量。

destructor

参数 destructor 是一个**函数指针**,指向一个自定义的函数,其格式如下:
void destructor(void *value)
{
/* code */
}

调用 pthread_key_create()函数允许调用者指定一个自定义的解构函数(类似于 C++中的析构函数),使用参数 destructor 指向该函数;

该函数通常用于释放与特有数据键关联的线程私有数据区占用的内存空间,当使用线程特有数据的线程终止时,destructor()函数会被自动调用。

**返回值:**成功返回 0;失败将返回一个错误编号以指示错误原因,返回的错误编号其实就是全局变量

errno,可以使用诸如 strerror()函数查看其错误字符串信息。

pthread_setspecific()
pthread_getspecific()

, pthread_self());
func(); //调用函数 func
pthread_exit(NULL); //线程终止
}

static int nums[5] = {0, 1, 2, 3, 4};
int main(void)
{
pthread_t tid[5];
int j;

 /* 创建 5 个线程 */
 for (j = 0; j < 5; j++)
 	pthread_create(&tid[j], NULL, thread_start, &nums[j]);

 /* 等待线程结束 */
 for (j = 0; j < 5; j++)
 	pthread_join(tid[j], NULL);//回收线程

 exit(0);

}


[外链图片转存中...(img-g0v0bBRe-1722535321734)]

#### 线程特有数据
- **创建数据键、分配缓冲区、调用缓冲区、删除数据键;**

- 线程特有数据也称为线程**私有数据**,用于**避免变量成为多个线程间的共享数据**;

- 为每个调用线程分别维护一份变量的副本(copy),

  每个线程通过特有数据键(key)访问时,这个**特有数据键都会获取到本线程绑定的变量副本**;

为每一个调用线程(调用某函数的线程,该函数就是我们要通过线程特有数据将其实现为线程安全的函数)分配属于该线程的私有数据区,为每个调用线程分别维护一份变量的副本。


##### pthread_key_create()

- 在为线程分配私有数据区之前,需要调用 pthread_key_create()函数**创建一个特有数据键(key)**;

- 只需要在首个调用的线程中创建一次即可,所以通常会使用到上小节所学习的 pthread_once()函数;

调用 pthread_key_create()函数允许调用者指定一个**自定义的解构函数**(类似于 C++中的析构函数),使用参数 **destructor** 指向该函数;
该函数通常用于**释放与特有数据键关联的线程私有数据区占用的内存空间**,当使用线程特有数据的线程终止时,destructor()函数会被自动调用。

**原型**

调用 pthread_key_create()之前,需要定义一个 pthread_key_t 类型变量,调用 pthread_key_create()时参数 key 指向 pthread_key_t 类型变量。

```c
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
/*
参数:
  key:调用该函数会创建一个特有数据键,并通过参数 key 所指向的缓冲区返回给调用者,参数 key 是一个 pthread_key_t 类型的指针,可以把 pthread_key_t 称为 key 类型。
  destructor:参数 destructor 是一个函数指针,指向一个自定义的函数,如下:
*/
  		void destructor(void *value)
  		{
  			/* code */
  		}
/*
返回值:
  成功返回 0;
  失败将返回一个错误编号以指示错误原因,返回的错误编号其实就是全局变量errno,可以使用诸如 strerror()函数查看其错误字符串信息。
pthread_setspecific()
  • 调用 pthread_key_create()函数创建特有数据键(key)后通常需要为调用线程分配私有数据缓冲区

    譬如通过 malloc()(或类似函数)申请堆内存,每个调用线程分配一次,且只会在线程初次调用此函数时分配;
    
  • 为线程分配私有数据缓冲区之后,通常需要调用 pthread_setspecific(),来首先保存指向线程私有数据缓冲区的指针,并将其与特有数据键以及当前调用线程关联起来

原型

#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
/*
参数:
	key:pthread_key_t 类型变量,参数 key 应赋值为调用 pthread_key_create()函数时创建的特有数据键,也就是 pthread_key_create()函数的参数 key 所指向的 pthread_key_t 变量;
	value:参数 value 是一个 void 类型的指针,指向由调用者分配的一块内存,作为线程的私有数据缓冲
区,当线程终止时,会自动调用参数 key 指定的特有数据键对应的解构函数来释放这一块动态申请的内存空间;
返回值:
	调用成功返回 0;
	失败将返回一个错误编码,可以使用诸如 strerror()函数查看其错误字符串信息;
pthread_getspecific()
  • 调用 pthread_setspecific()函数将线程私有数据缓冲区与调用线程以及特有数据键关联之后,便可以使用

    pthread_getspecific()函数来获取调用线程的私有数据区

原型

  • 函数中可以利用返回值来判断当前调用线程是否为初次调用该函数

  • 如果是初次调用,则必须为该线程分配私有数据缓冲区。

#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
/*
参数:
	key:赋值为调用 pthread_key_create()函数时创建的特有数据键,也就是 pthread_key_create()函数
的参数 key 指向的 pthread_key_t 变量。
	返回当前调用线程关联到特有数据键的私有数据缓冲区,返回值是一个指针,指向该缓冲区。
	如果当前调用线程并没有设置线程私有数据缓冲区与特有数据键进行关联,则返回值应为NULL;
pthread_key_delete()

如果需要删除一个特有数据键(key)可用pthread_key_delete(),删除先前 pthread_key_create()创建的键

原型

#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
/*参数 key 为要删除的键。函数调用成功返回 0,失败将返回一个错误编号*/

在这里插入图片描述

典例

线程局部存储
简介
  • 通常情况下,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量;

  • 线程局部存储定义全局或静态变量时,使用__thread 修饰符修饰变量,此时,每个线程都会拥有一份对该

    变量的拷贝。

  • 线程局部存储中的变量将一直存在,直至线程终止,届时会自动释放这一存储。

特性
  • 优点

    • 比线程特有数据的使用要简单只需简单地在全局或静态变量的声明中包含**__thread 修饰符**即可

      static __thread char buf[512];
      
  • 关于线程局部变量的声明和使用,需要注意以下几点:

    ⚫ 如果变量声明中使用了关键字 static 或 extern,那么关键字__thread 必须紧随其后。
    ⚫ 与一般的全局或静态变量申明一眼,线程局部变量在申明时可设置一个初始值。
    ⚫ 可以使用 C 语言取值操作符(&)来获取线程局部变量的地址。
    Tips:线程局部存储需要内核、Pthreads 以及 GCC 编译器的支持。
    
典例

来测试线程局部存储;

						/*示例代码 11.10.7 线程局部存储测试*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

static __thread char buf[100];
static void *thread_start(void *arg)
{
     strcpy(buf, "Child Thread\n");
     printf("子线程: buf (%p) = %s", buf, buf);
     pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
     pthread_t tid;
     int ret;
     //拷贝了字符串到 buf 缓冲区
     strcpy(buf, "Main Thread\n");
    
     /* 创建子线程 */
     if (ret = pthread_create(&tid, NULL, thread_start, NULL)) 
     {
         fprintf(stderr, "pthread_create error: %d\n", ret);
         exit(-1);
     }
    
     /* 等待回收子线程 */
     if (ret = pthread_join(tid, NULL)) 
     {
         fprintf(stderr, "pthread_join error: %d\n", ret);
         exit(-1);
     }
     printf("主线程: buf (%p) = %s", buf, buf);
     exit(0);
}
/*
程序中定义了一个全局变量 buf,使用__thread 修饰,使其变为线程局部变量;
主线程中首先调用 strcpy拷贝了字符串到 buf 缓冲区中,随后创建了一个子线程,子线程也调用了 strcpy()向 buf 缓冲区拷贝了数据;并调用 printf 打印 buf 缓冲区存储的字符串以及 buf 缓冲区的指针值。
*/
  • 从地址便可以看出来,主线程和子线程中使用的 buf 绝不是同一个变量

  • 这就是线程局部存储,使得每个线程都拥有一份对变量的拷贝,各个线程操作各自的变量不会影响其它线程;

在这里插入图片描述

信号与多线程模型

  • 主要包括线程与信号之间牵扯的问题、线程与进程控制(fork()、exec()、exit()等)之间的交互;

  • Linux 信号模型是基于进程模型而设计的,信号的问世远早于线程;

  • 信号既能够在传统单线程进程保持它原有功能、特性,同时,又要设计出能适用于多线程环境的新特性

信号如何映射到线程

在这里插入图片描述

线程的信号掩码
  • 对于一个单线程程序,**sigprocmask()**函数设置进程的信号掩码;

  • 在多线程环境下,使用**pthread_sigmask()**函数来设置各个线程的信号掩码;

  • 每个刚创建的线程,会从其创建者处继承信号掩码,新线程可调用 pthread_sigmask()函数改变信号掩码;

原型

  • pthread_sigmask()函数就像sigprocmask()一样,不同之处在于它在多线程程序中使用;
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
向线程发送信号
  • 调用 kill()或 sigqueue()所发送的信号都是针对整个进程来说,属于进程层面;

  • 具体该目标进程中的哪一个线程会去处理信号,由内核进行选择;

  • 事实上,在多线程程序中,可以通过 pthread_kill()向同一进程中的某个指定线程发送信号;

pthread_kill()
  • 向同一进程中的某个指定线程发送信号;

原型

#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
/*
参数:
	thread,也就是线程 ID,用于指定同一进程中的某个线程,调用 pthread_kill()将向参数 thread 指定
的线程发送信号 sig。
	如果参数 sig 为 0,则不发送信号,但仍会执行错误检查;
返回值:
	函数调用成功返回 0;
	失败将返回一个错误编号,不会发送信号。
pthread_sigqueue()
  • pthread_sigqueue()函数执行与 sigqueue类似的任务,但它不是向进程发送信号,而是向同一进程中的某个指定的线程发送信号
  • 区别:sigqueue()发送的信号针对的是整个进程,而 pthread_sigqueue()发送的信号针对的是某个线程

原型

#include <signal.h>
#include <pthread.h>
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
/*
参数:
	thread:线程 ID,指定接收信号的目标线程(目标线程与调用该函数的线程是属于同一个进程)!
	sig:指定要发送的信号,参数 value 指定伴随数据,与 sigqueue()函数中的 value 参数意义相同。
异步信号安全函数
  • 应用程序中涉及信号处理函数时必须要非常小心,因为信号处理函数可能会在程序执行的任意时间点被调用,从而打断主程序;

  • 线程安全函数,作为线程安全函数可以被多个线程同时调用,每次都能得到预期的结果;这里有前提条件,那就是没有信号处理函数参与

  • 线程安全函数不能在信号处理函数中被调用,否则不能保证它一定是安全的。所以出现异步信号安全函数。

在这里插入图片描述

典例

下面列举出来的一个函数是线程安全函数:

static pthread_mutex_t mutex;
static int glob = 0;

static void func(int loops)
{
    int local;
    int j;
    for (j = 0; j < loops; j++) 
    {
        pthread_mutex_lock(&mutex); //互斥锁上锁
        
        local = glob;
        local++;
        glob = local;
        
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
}
	该函数虽然对全局变量进行读写操作,但是在访问全局变量时进行了加锁,避免了引发竞争冒险;
	它是一个线程安全函数,假设线程 1 正在执行函数 func,刚刚获得锁(也就是刚刚对互斥锁上锁),而这时进程
收到信号,并分派给线程 1 处理,线程 1 接着跳转去执行信号处理函数,不巧的是,信号处理函数中也调用了 func()函数,同样它也去获取锁,由于此时锁处于锁住状态,所以信号处理函数中调用 func()获取锁将会陷入休眠、等待锁的释放。
	这时线程 1 就会陷入死锁状态,线程 1 无法执行,锁无法释放;如果其它线程也调用 func(),那它们也会陷入休眠、如此将会导致整个程序陷入死锁!
	通过上面的分析,可知,涉及到信号处理函数时要非常小心。

主要原因在以下两个方面:

⚫ 信号是异步的,信号可能会在任何时间点中断主程序的运行,跳转到信号处理函数处执行,从而形成一个新的执行流(信号处理函数执行流)。
⚫ 信号处理函数执行流与线程执行流存在一些区别,信号处理函数所产生的执行流是由执行信号处理函数的线程所触发的,它俩是在同一个线程中,属于同一个线程执行流。

线程安全、可重入、异步安全函数

  • 在异步信号安全函数、可重入函数以及线程安全函数三者中,可重入函数的要求是最严格的;
  • 所以通常会说可重入函数一定是线程安全函数、也一定是异步信号安全函数

Linux 标准 C 库和系统调用中以下函数被认为是异步信号安全函数,可以通过 man 手册查询,执行命令"man 7 signal":

在这里插入图片描述在这里插入图片描述

多线程环境下信号的处理

实战见!

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值