【Linux应用编程】Day12线程

news2024/9/20 5:33:56

线程

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

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

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

⚫ 线程标识;

⚫ 线程创建与回收;

⚫ 线程取消;

⚫ 线程终止;

⚫ 线程分离;

⚫ 线程同步技术;

⚫ 线程安全。

线程同步移步:【线程同步机制】Day13线程同步:互斥锁、条件变量、自旋锁、读写锁


线程概述

线程概念
线程定义

在这里插入图片描述

线程创建

在这里插入图片描述

线程特点

在这里插入图片描述

线程与进程

在这里插入图片描述

并发和并行

个人理解侧重点:

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

  • 与串行的区别

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

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

在这里插入图片描述

典例

在这里插入图片描述


线程 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()函数;

**原型**

#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**:

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

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

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

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

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

pthread_setspecific()
pthread_getspecific()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1970734.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电脑上有什么好用的记笔记软件吗?试试这3款笔记软件,功能丰富又实用

笔记软件千千万&#xff0c;日常使用方便最关键&#xff01;&#xff01; 推荐3个各有亮点的笔记软件&#xff0c;不止是记笔记这么简单&#xff1a; 1、FlowUs 推荐指数&#xff1a;☆☆☆☆☆ 关键词&#xff1a;文档笔记软件 下载链接>>flowus.cn FlowUs是一款在…

ADI - 通过5 V至24 V输入提供双极性、双向DC-DC流入和流出电流

大部分电子系统都依赖于正电压轨或负电压轨&#xff0c;但是有些应用要求单电压轨同时为正负电压轨。在这种情况下&#xff0c;正电源或负电源由同一端子提供&#xff0c;也就是说&#xff0c;电源的输出电压可以在整个电压范围内调节&#xff0c;并且可以平稳转换极性。例如&a…

【mars3d】实现线面内插值计算效果

面插值计算效果展示&#xff1a; &#xff08;离屏渲染方式&#xff09;面插值效果展示&#xff1a; 面内插值计算插点效果展示&#xff1a; 线插值效果展示&#xff1a; &#xff08;离屏渲染方式&#xff09;高密度线内插值计算效果展示&#xff1a; 相关代码&#xff1a; i…

docker二进制包部署(带arm版自动部署包)

文章目录 1.概述2.Docker二进制包下载3.安装脚本制作4.安装5.卸载6.注意事项7.分享一个arm版自动部署安装包8.懒人 X86 版安装包 1.概述 最近需要在Linux上部署docker&#xff0c;于是自己做了一个自动部署包。脚本的写法不区分X86或arm&#xff0c;通用的。 2.Docker二进制包…

网络安全和数据安全到底有什么区别?(非常详细)零基础入门到精通,收藏这一篇就够了

随着信息技术的迅猛发展&#xff0c;网络安全和数据安全已经成为当今社会不可忽视的重要议题。两者在保障信息系统安全、防范数据泄露和保障用户权益方面起着至关重要的作用。然而&#xff0c;尽管网络安全与数据安全在某些方面有着密切的联系&#xff0c;但它们在定义、目标和…

“八股文”:程序员的福音还是梦魇?

——一场关于面试题的“代码战争” 在程序员的世界里&#xff0c;“八股文”这个词儿可谓是“如雷贯耳”。不&#xff0c;咱们可不是说古代科举考试中的那种八股文&#xff0c;而是指程序员面试中的那些固定套路的题目。如今&#xff0c;各大中小企业在招聘程序员时&#xff0…

11.2.0.4 ADG故障 LGWR (ospid: 30945):terminating the instance due to error 4021

11.2.0.4 ADG无法连接&#xff0c;查看数据库为关闭状态&#xff0c;重新启动实例&#xff0c;应用日志后即可正常同步数据并打开到只读模式。 查看alert日志发现有以下报错&#xff1a; 0RA-04021:timeout occurred while waiting to lock obiectLGWR (ospid: 30945):termi…

矩阵、向量、张量 一文彻底理清!

矩阵&#xff1a;可理解为二维数组、二维张量 向量Vector&#xff1a;是只有一列的矩阵 张量&#xff1a;是矩阵向任意维度的推广。 机器学习经常会用到张量做变换&#xff0c;所以下文重点介绍张量。 可以通过.ndim查看numpy数据的张量维度。张量的维度&#xff08;dimens…

【熊猫派对】

游戏简介 熊猫派对是一款滑稽打闹游戏&#xff0c;玩法容易上手简单&#xff0c;游戏中玩家将操控自己的熊猫人&#xff0c;与其他对手对战&#xff0c;重拳、飞脚甚至还有各种各样的武器都可用来击败你的对手。 游戏特色 1、滑稽角色 网络超火的滑稽角色&#xff0c;从表情包…

JNI原理是什么?JNI在DDS binding JAVA中/DDS移植android平台中有什么作用?

1 JNI是什么2 如何在JAVA中调用C/C方法&#xff08;通过JNI调用的demo&#xff09;java中声明一个本地native方法生成JNI头文件Java native方法转换成C的规则与语法说明C实现的native方法本地实现以及.o .dll库的生成查看hello.dll库中的函数运行一下HelloJNI JNI在DDS移植andr…

微信小程序 - 自定义计数器

微信小程序通过自定义组件&#xff0c;实现计数器值的增加、减少、清零、最大最小值限定、禁用等操作。通过按钮事件触发方式&#xff0c;更新计数器的值&#xff0c;并修改相关联的其它变量。通过提升用户体验&#xff0c;对计数器进行优化设计&#xff0c;使用户操作更加便捷…

PHP教育培训小程序系统源码

&#x1f680;【学习新纪元】解锁教育培训小程序的无限可能✨ &#x1f4da; 引言&#xff1a;教育培训新风尚&#xff0c;小程序来引领&#xff01; Hey小伙伴们&#xff0c;是不是还在为找不到合适的学习资源而烦恼&#xff1f;或是厌倦了传统教育模式的单调&#xff1f;今…

Monaco 使用 SignatureHelpProvider

Monaco 中 SignatureHelpProvider 是方法提示说明&#xff0c;当敲入方法名时&#xff0c;系统会提示方法名称和对应的参数信息。效果如下&#xff1a; 通过 registerSignatureHelpProvider 实现 SignatureHelpProvider 处理函数。 实现 signatureHelpTriggerCharacters 和 pro…

我们如何优化 Elasticsearch Serverless 中的刷新成本

作者&#xff1a;来自 Elastic Francisco Fernndez Castao, Henning Andersen 最近&#xff0c;我们推出了 Elastic Cloud Serverless 产品&#xff0c;旨在提供在云中运行搜索工作负载的无缝体验。为了推出该产品&#xff0c;我们重新设计了 Elasticsearch&#xff0c;将存储与…

深入了解下 Markdown 的原理

前面讲了 Markdown 的基本语法&#xff0c;常见的 Markdown 编辑器&#xff0c;在继续讲解其他知识之前&#xff0c;有必要稍微深入了解一下 Markdown 与 HTML 的关系。 ‍ ‍ HTML 简介 什么是 HTML&#xff1f;其实它也是标记语言的一种&#xff0c;但是比 Markdown 更重…

Java面试题--JVM大厂篇之深入分析Parallel GC:从原理到优化

目录 引言: 正文&#xff1a; 1. Parallel GC原理解析 2. Parallel GC关键参数配置 3. 常见调优场景与技巧 4. 监控与日志分析 结束语&#xff1a; 引言: 在Java应用程序中&#xff0c;垃圾回收&#xff08;Garbage Collection, GC&#xff09;扮演着至关重要的角色。对…

【学术会议征稿】第三届图像处理、计算机视觉与机器学习国际学术会议(ICICML 2024)

第三届图像处理、计算机视觉与机器学习国际学术会议(ICICML 2024) 2024 3rd International Conference on Image Processing, Computer Vision and Machine Learning 重要信息 大会官网&#xff1a;参会投稿/了解会议详情 大会时间&#xff1a;2024年11月22日-24日 大会地…

大米cms支付逻辑漏洞

1.打开环境 注册账户 随机选择一个产品 修改数据

什么是折叠幼儿床?该如何认证?

​折叠幼儿床具有如下特征&#xff1a; 轻巧便携 用于睡眠的幼儿床&#xff0c;不使用时会折叠 有幼儿护栏&#xff0c;且必须带底板&#xff08;不包括没有地板的婴儿围栏&#xff09; 图片 亚马逊政策规定&#xff0c;通过亚马逊网站销售的折叠幼儿床必须符合特定标准的测…

视频转文字在线提取怎么弄?5款软件帮你解决

三伏天炎炎&#xff0c;阳光炽热&#xff0c;视频内容分享正当时。然而&#xff0c;在海量视频信息中快速提取关键信息却成了难题。试想&#xff0c;如果能一键将视频中的精彩讲解或会议要点转换成文字&#xff0c;岂不省时又高效&#xff1f; 那么问题来了&#xff0c;面对市…