0%

Linux线程(二)

Linux线程(二)

分离线程

  • 默认情况下,当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源,有 时,程序员并不关心线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这 种情况下,可以调用 pthread_detach()将指定线程进行分离,也就是分离线程
#include <pthread.h>
int pthread_detach(pthread_t thread);
  • 线程分离自己
  • pthread_detach(pthread_self());
  • 一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态,此过程是不可逆的,一旦处于 分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源

注册线程清理处理函数

  • 当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数, 我们把这个称为线程清理函数
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

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

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

线程属性

  • 在 Linux 下,使用 pthread_attr_t 数据类型定义线程的所有属性

线程栈属性

#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

分离状态属性

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

​ 具体略

线程安全

  • 当我们编写的程序是一个多线程应用程序时,就不得不考虑到线程安全的问题,确保我们编写的程序是 一个线程安全(thread-safe)的多线程应用程序,什么是线程安全以及如何保证线程安全?带着这些问题, 本小节将讨论线程安全相关的话题。

线程栈

  • 进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈。譬如主线程调用 pthread_create()创建 了一个新的线程,那么这个新的线程有它自己独立的栈地址空间、而主线程也有它自己独立的栈地址空间。 在创建一个新的线程时,可以配置线程栈的大小以及起始地址,当然在大部分情况 下,保持默认即可!
  • 然每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量)都是分 配在自己的线程栈中的,它们不会相互干扰

可重入函数

  • 单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;而对于 多线程程序而言,同一进程却存在多条独立、并发的执行流。
  • 进程中执行流的数量除了与线程有关之外,与信号处理也有关联。因为信号是异步的,进程可能会在其 运行过程中的任何时间点收到信号,进而跳转、执行信号处理函数,从而在一个单线程进程(包含信号处理) 中形成了两条(即主程序和信号处理函数)独立的执行流。
  • 如果一个函数被同一进程的多个不同的执行流同时调用,每次函数 调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数。实质上也就是该函数被多个执行流并发/并行调用

绝对可重入函数的特点

  • 函数内所使用到的变量均为局部变量,换句话说,该函数内的操作的内存地址均为本地栈地址
  • 函数参数和返回值均是值类型
  • 函数内调用的其它函数也均是绝对可重入函数

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

  • 一个函数具有引用类型的函数,传入了一个指针,并在函数内部读写该指针所指向的内存地址,该函 数是一个可重入函数,但同样需要满足一定的条件;如果多个执行流同时调用该函数时,所传入的指针是共 享变量的地址,那么在这种情况,最终可能得不到预期的结果;因为在这种情况下,函数 func()所读写的便是多个执行流的共享数据,会出现数据不一致的情况,所以是不安全的。
  • 但如果每个执行流所传入的指针是其本地变量(局部变量)对应的地址,那就是没有问题的,所以呢, 这个函数就是一个带条件的可重入函数。

线程安全

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

用来保证线程安全的函数

  • 在多线程编程环境下,有些代码段只需要执行一次
  • pthread_once()函数保证函数只执行一次
#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 指向该变量。通常在定义变量时会使用 PTHREAD_ONCE_INIT 宏对其进行初始化
pthread_once_t once_control = PTHREAD_ONCE_INIT;
  • init_routine:一个函数指针,参数init_routine所指向的函数就是要求只能被执行一次的代码段, pthread_once()函数内部会调用 init_routine(),即使 pthread_once()函数会被多次执行,但它能保证 init_routine() 仅被执行一次。

  • 返回值:调用成功返回 0;失败则返回错误编码以指示错误原因。

  • 如果在一个线程调用 pthread_once()时,另外一个线程也调用了 pthread_once,则该线程将会被阻塞等待,直到第一个完成初始化后返回。换言之,当调用 pthread_once 成功返回时,调用总是能够肯定所有的状态已经初始化完成了。

线程特有数据

  • 线程特有数据也称为线程私有数据,简单点说,就是为每个调用线程分别维护一份变量的副本(copy), 每个线程通过特有数据键(key)访问时,这个特有数据键都会获取到本线程绑定的变量副本。这样就可以 避免变量成为多个线程间的共享数据

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

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

#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_key_create()函数创建特有数据键(key)后通常需要为调用线程分配私有数据缓冲区,譬 如通过 malloc()(或类似函数)申请堆内存,每个调用线程分配一次,且只会在线程初次调用此函数时分配。为线程分配私有数据缓冲区之后,通常需要调用 pthread_setspecific()函数,pthread_setspecific()函数其实完成 了这样的操作:首先保存指向线程私有数据缓冲区的指针,并将其与特有数据键以及当前调用线程关联起 来

  • pthread_setspecific()函数
  • 调用 pthread_key_create() 函数创建特有数据键(key)后通常需要为调用线程分配私有数据缓冲区,譬如通过malloc()(或类似函数)申请堆内存,每个调用线程分配一次,且只会在线程初次调用此函数时分配。 为线程分配私有数据缓冲区之后,通常需要调用 pthread_setspecific()函数,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_setspecific()函数将线程私有数据缓冲区与调用线程以及特有数据键关联之后,便可以使用
pthread_getspecific()函数来获取调用线程的私有数据区了。

  • pthread_getspecific()函数
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
  • pthread_getspecific()函数应返回当前调用线程关联到特有数据键的私有数据缓冲区,返回值是一个指针, 指向该缓冲区。如果当前调用线程并没有设置线程私有数据缓冲区与特有数据键进行关联,则返回值应为 NULL,函数中可以利用这一点来判断当前调用线程是否为初次调用该函数,如果是初次调用,则必须为该 线程分配私有数据缓冲区。

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

  • pthread_key_delete()函数
  • 需要删除一个特有数据键(key)可以使用函数 pthread_key_delete(), pthread_key_delete()函数删除先前由 pthread_key_create()创建的键
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
  • 调用 pthread_key_delete()函数将释放参数 key 指定的特有数据键,可以供下一次调用 pthread_key_create() 时使用;调用 pthread_key_delete()时,它并不将查当前是否有线程正在使用该键所关联的线程私有数据缓冲 区,所以它并不会触发键的解构函数,也就不会释放键关联的线程私有数据区占用的内存资源,并且调用 pthread_key_delete()后,当线程终止时也不再执行键的解构函数。
  • 调用的条件
    • 所有线程已经释放了私有数据区(显式调用解构函数或线程终止)。
    • 参数 key 指定的特有数据键将不再使用。
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX_ERROR_LEN 256
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerror_key;
static void destructor(void *buf)
{
free(buf); //释放内存
}
static void create_key(void)
{
/* 创建一个键(key),并且绑定键的解构函数 */
if (pthread_key_create(&strerror_key, destructor))
pthread_exit(NULL);
}
/******************************
* 对 strerror 函数重写
* 使其变成为一个线程安全函数
******************************/
static char *strerror(int errnum)
{
char *buf;
/* 创建一个键(只执行一次 create_key) */
if (pthread_once(&once, create_key))
pthread_exit(NULL);
/* 获取 */
buf = pthread_getspecific(strerror_key);
if (NULL == buf)
{ //首次调用 my_strerror 函数,则需给调用线程分配线程私有数据
buf = malloc(MAX_ERROR_LEN); //分配内存
if (NULL == buf)
pthread_exit(NULL);
/* 保存缓冲区地址,与键、线程关联起来 */
if (pthread_setspecific(strerror_key, buf))
pthread_exit(NULL);
}
if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);
else
{
strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);
buf[MAX_ERROR_LEN - 1] = '\0'; //终止字符
}
return buf;
}
  • 第一步是调用 pthread_once(),以确保只会执行一次 create_key()函数,而在 create_key()函数中便是调用 pthread_key_create()创建了一个键、并绑定了相应的解构函数 destructor(),解构 函数用于释放与键关联的所有线程私有数据所占的内存空间。
  • 函数 strerror()调用 pthread_getspecific()以获取该调用线程与键相关联的私有数据缓冲区地址,如 果返回为 NULL,则表明该线程是首次调用 strerror()函数,因为函数会调用 malloc()为其分配一个新的私有 数据缓冲区,并调用 pthread_setspecific()来保存缓冲区地址、并与键与该调用线程建立关联。如果 pthread_getspecific()函数的返回值并不等于 NULL,那么该值将指向以存在的私有数据缓冲区,此缓冲区由之前对 strerror()的调用所分配。

使用例

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

pthread_key_t p_key;
int a = 0;
void freeBuf(void * p)
{

}

static void *func(void * str)
{
pthread_setspecific(p_key, &a);
int* ptr = (int*)pthread_getspecific(p_key);
//*ptr += 1;
printf("%s %d\n", str, p_key);

sleep(2);
*ptr += 1;
printf("%s %d\n", str, *ptr);
return NULL;
}

int main()
{
pthread_t pa, pb;

pthread_key_create(&p_key, NULL);

pthread_create(&pa, NULL, func, "thread1:");
sleep(1);
pthread_create(&pb, NULL, func, "thread2:");
pthread_join(pa, NULL);
pthread_join(pb, NULL);

return 0;
}

两个线程使用同一个key访问同一个全局变量,出现了线程不安全情况

image-20220123144524240

假如是使用两个不同的key:

代码更改为

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>


int a = 0;
void freeBuf(void * p)
{

}

static void *func(void * str)
{
// pthread_once_t val = PTHREAD_ONCE_INIT;
pthread_key_t p_key;
pthread_key_create(&p_key, NULL);
pthread_setspecific(p_key, &a);
int* ptr = (int*)pthread_getspecific(p_key);
//*ptr += 1;
printf("%s(key) %d\n", str, p_key);

sleep(2);
*ptr += 1;
printf("%s %d\n", str, *ptr);
return NULL;
}

int main()
{
pthread_t pa, pb;



pthread_create(&pa, NULL, func, "thread1:");
sleep(1);
pthread_create(&pb, NULL, func, "thread2:");
pthread_join(pa, NULL);
pthread_join(pb, NULL);

return 0;
}

image-20220123144850517

效果类似

但是对于同一个key,不同的线程set不同的内存位置,再在不同的线程中用同样的key调用get得到的是各自的变量空间

如下

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>


int a = 1;
int b = 5;
void freeBuf(void * p)
{

}

static void *func(void * str)
{
pthread_once_t val = PTHREAD_ONCE_INIT;
pthread_key_t p_key;
pthread_key_create(&p_key, NULL);
pthread_setspecific(p_key, str);
char* ptr = pthread_getspecific(p_key);
//*ptr += 1;
printf("%s(key) %d\n", str, p_key);

sleep(2);
int i = ptr[6]-'0';
i += 1;
printf("%s %d\n", ptr, i);
return NULL;
}

int main()
{
pthread_t pa, pb;



pthread_create(&pa, NULL, func, "thread1:");
sleep(1);
pthread_create(&pb, NULL, func, "thread2:");
pthread_join(pa, NULL);
pthread_join(pb, NULL);

return 0;
}

image-20220123145927573

线程局部变量

  • 通常情况下,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量;而 线程局部存储在定义全局或静态变量时,使用__thread 修饰符修饰变量,此时,每个线程都会拥有一份对该 变量的拷贝。线程局部存储中的变量将一直存在,直至线程终止,届时会自动释放这一存储

  • 要创建线程局部变量,只需简单地在全 局或静态变量的声明中包含__thread 修饰符即可!

static __thread char buf[512];

注意:

  • 如果变量声明中使用了关键字 static 或 extern,那么关键字__thread 必须紧随其后。
  • 与一般的全局或静态变量申明一眼,线程局部变量在申明时可设置一个初始值。
  • 可以使用 C 语言取值操作符(&)来获取线程局部变量的地址。
#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;
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);
}

image-20220123185312283

可见主线程和子线程的buf不是同一个东西

多线程信号处理

信号模型在一些方面是属于进程层面(由进程中的所有线程线程共享)的,而在另一些方面是属于单个 线程层面的

  • 信号的系统默认行为是属于进程层面。每一个信号都有其对应的系统默认动作, 当进程中的任一线程收到任何一个未经处理(忽略或捕获)的信号时,会执行该信号的默认操作, 信号的默认操作通常是停止或终止进程。

  • 信号处理函数属于进程层面。进程中的所有线程共享程序中所注册的信号处理函数;

  • 信号的发送既可针对整个进程,也可针对某个特定的线程。在满足以下三个条件中的任意一个时, 信号的发送针对的是某个线程

    • 产生了硬件异常相关信号,譬如 SIGBUS、SIGFPE、SIGILL 和 SIGSEGV 信号;这些硬件异 常信号在某个线程执行指令的过程中产生,也就是说这些硬件异常信号是由某个线程所引起; 那么在这种情况下,系统会将信号发送给该线程。
    • 当线程试图对已断开的管道进行写操作时所产生的 SIGPIPE 信号;
    • 由函数 pthread_kill()或 pthread_sigqueue()所发出的信号,稍后介绍这两个函数;这些函数允许 线程向同一进程下的其它线程发送一个指定的信号。
  • 当一个多线程进程接收到一个信号时,且该信号绑定了信号处理函数时,内核会任选一个线程来接 收这个信号,意味着由该线程接收信号并调用信号处理函数对其进行处理,并不是每个线程都会接 收到该信号并调用信号处理函数

  • 信号掩码其实是属于线程层面的,也就是说信号掩码是针对每个线程而言。8.9 小节向大家介绍了 信号掩码的概念,并介绍了 sigprocmask()函数,通过 sigprocmask()可以设置进程的信号掩码,事实 上,信号掩码是并不是针对整个进程来说,而是针对线程,对于一个多线程应用程序来说,并不存 在一个作用于整个进程范围内的信号掩码(管理进程中的所有线程);那么在多线程环境下,各个 线程可以调用 pthread_sigmask()函数来设置它们各自的信号掩码,譬如设置线程可以接收哪些信号、 不接收哪些信号,各线程可独立阻止或放行各种信号。

  • 针对整个进程所挂起的信号,以及针对每个线程所挂起的信号,内核都会分别进行维护、记录。 8.11.1 小节介绍到,调用 sigpending()会返回进程中所有被挂起的信号,事实上,sigpending()会返 回针对整个进程所挂起的信号,以及针对每个线程所挂起的信号的并集。

  • 其他内容略