多线程编程笔记

1、线程概念

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

线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

2、创建线程

成功返回0,失败返回一个正数即errno的值。

3、终止线程

pthread_exit函数类似于在start函数中调用return作用,不同的是在start函数调用的任意函数中调用pthread_exit函数。如果主线程调用了pthread_exit函数,而非调用了exit函数或者执行return,那么其他线程将会正常运行。参数retval用来设置线程退出时的返回值。 

pthread_cancel 在默认情况下,只是通知线程,至于线程什么时候会取消,只有在遇到了取消点(把它想象成某个函数)的时候才会停止。

可以在线程函数中禁止取消。下面的函数可以实现此功能:

int pthread_setcancelstate(int state, int *oldstate);

其中,state 可以为下面的值:

  • PTHREAD_CANCEL_ENABLE
  • PTHREAD_CANCEL_DISABLE

默认情况下,state 的值为 PTHREAD_CANCEL_ENABLE,即可以被取消。如果将 state 设置为 PTHREAD_CANCEL_DISABLE,则线程是无法被取消的。

有一种办法,可以让线程还没到达取消点的时候直接退出。这时候需要将线程设置为异步取消的方式。具体方法为在线程内部调用下面的函数:

int pthread_setcanceltype(int type, int *oldtype);

其中,type 的值如下:

  • PTHREAD_CANCEL_ASYNCHRONOUS
  • PTHREAD_CANCEL_DEFERRED

默认情况下,线程取消方式为默认值——PTHREAD_CANCEL_DEFERRED。要想让线程收到“取消信号”后立即退出,需要将 type 设置为 PTHREAD_CANCEL_ASYNCHRONOUS. 

4、线程ID

进程内部每个线程都有一个唯一标识,称为线程ID。线程ID会返回给pthread_create的调用者,一个线程可以通过pthread_self()函数来获取自己的线程ID。

线程ID的类型为pthread_t  Linux将pthread_t 定义为无符号长整型(unsigned long),但在其他实现中则有可能是一个指针或者结构。 当需要比较两个线程ID是否相同时,会用到函数pthread_equal函数。 

int pthread_equal(pthread_t t1, pthread_t t2)

返回非0值,表示相等。

如上图,我们看到的SPID并非我们上面所说的线程ID,线程ID是由线程库实现来负责分配和维护的。SPID是由内核分配的,类似进程ID,可以通过系统调用gettid()获得。

5、连接(joining)已终止的线程

int pthread_join(pthread_t thread, void **retval)

函数pthread_join()等待由thread标识的线程终止。(如果线程已终止,pthread_join()会立即返回)。这种操作称为连接(joining)。若retval为已非空指针,将会保存线程终止时返回值的拷贝,该返回值亦即线程return或pthread_exit()时所指定的值。

若线程没有分离(pthread_detach()),则必须使用pthread_join()来进行连接。如果未能连接,那么线程终止时将产生僵尸线程,与僵尸进程概念相似,除了浪费系统资源之外,僵尸线程若积累过多,应用将再也无法创建新的线程。

pthread_join()类似于针对进程的waitpid()调用,不过线程之间的关系是对等的,进程中的任意线程均可以调用pthread_join与该进程中任意其他线程连接起来。

6、线程分离

int pthread_detach(pthread_t thread)

如果不关心线程的返回状态,只是希望线程终止时系统能够自动清理并移除之。可以调用pthread_detach(),将线程标记为处于分离状态。一旦线程处于分离状态,就不能再使用pthread_join()来获取其状态,也无法返回“可连接”的状态。其他线程调用exit(),或是主线程执行return语句时,不管是线程时处于连接状态还是分离状态,进程的所有线程都会被终止。

7、线程清理函数

void pthread_cleanup_push(void (*rtn)(void*), void *arg);
void pthread_cleanup_pop(int execute);

在 Linux 中,pthread_cleanup_push 和 pthread_cleanup_pop 这两个函数是通过宏来做的,pthread_cleanup_push 被替换成以左花括号 { 为开头的一段代码,而 pthread_cleanup_pop 被替换成以右花括号 } 结尾的一段代码,这就意味着这两个函数必须要成对出现才能将左右花括号匹配上,否则就出现编译错误。

有三种情况线程清理函数会被调用:

  • 线程还未执行 pthread_cleanup_pop 前,被 pthread_cancel 取消
  • 线程还未执行 pthread_cleanup_pop 前,主动执行 pthread_exit 终止
  • 线程执行 pthread_cleanup_pop,且 pthread_cleanup_pop 的参数不为 0.

注意:如果线程还未执行 pthread_cleanup_pop 前通过 return 返回,是不会执行清理函数的。
参考https://blog.csdn.net/q1007729991/article/details/60751394

7、互斥量的属性

用于线程互斥的互斥量也有相应的属性 pthread_mutexattr_t,这里只讨论三个方面:

  • 共享属性
  • 鲁棒属性
  • 互斥量的递归类型

互斥量属性的数据类型是 pthread_mutexattr_t. 下面两个函数分别用于互斥量属性的初始化与回收。

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

1、共享属性

      除了互斥量有共享属性外,其它的线程互斥同步对象如读写锁、自旋锁、条件变量、屏障都有共享属性。

     该属性有两种情况:

  • PTHREAD_PROCESS_PRIVATE : 这种是默认的情况,表示互斥量只能在本进程内部使用。
  • PTHREAD_PROCESS_SHARED:表示互斥量可以在不同进程间使用。

共享设置和获取函数

int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared);
int pthread_mutexattr_getpshared(pthread_mutexattr_t *mattr, int pshared);

2、鲁棒属性

如果其中一个进程在未释放互斥量的情况下挂掉了,将会导致另一个线程永远无法获得锁,然后就死锁了。为了能够让进程在异常终止时,释放掉互斥锁,需要指定 ROBUST 属性,所谓的 ROBUST,指是的健壮的意思。

int pthread_mutexattr_getrobust(const pthread_mutexattr_t *attr, int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);

该属性有来两个值:

  • PTHREAD_MUTEX_STALLED :该值是默认值,这意味着持有互斥量的进程终止时不需要采取特别的动作。这种情况下如果锁没有释放,其他进程或者线程仍然不能获取锁
  • PTHREAD_MUTEX_ROBUST :这个情况下,如果锁没有释放,其他进程或者线程加锁时就会返回EOWNERDEAD的值。

在指定 robust 属性的情况下,如果其中某个进程在未释放锁的情况下退出了,另一个进程仍然可以获得锁,但是此时 pthread_mutex_lock 将返回 EOWNERDEAD,通知获得锁的线程,有一个其它进程的线程挂掉了,互斥量现在变成了 inconsistent 的状态。这时候,需要对互斥量做 consistent 处理,否则,一旦再次解锁后,互斥量将永久不可用。
翻译成代码就是这样的:

if (EOWNERDEAD == pthread_mutex_lock(&lock)) {
  pthread_mutex_consistent(&lock);
}

3、互斥量的递归类型

互斥量的类型属性通常有四种:

  • PTHREAD_MUTEX_NORMAL
  • PTHREAD_MUTEX_ERRORCHECK
  • PTHREAD_MUTEX_RECURSIVE
  • PTHREAD_MUTEX_DEFAULT

可以使用下面的函数对互斥量的类型属性进行设置和获取:

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

一般情况下,我们在同一个线程中对同一个互斥量加两次锁,就会死锁。如果将互斥量类型属性设置为递归类型 PTHREAD_MUTEX_RECURSIVE 就不会出现此问题。

递归互斥量内部维护着一个计数器,当互斥量未上锁时,计数器值为 0。只有计数器为 0 的情况下,线程才能够获得锁。只有获得锁的线程,才能持续对互斥量加锁,每加一次锁,计数器的值加 1,每解一次锁,计数器的值减 1.


8、pthread once

int phtread_once(pthread_once_t *initflag, void (*initfn)(void));
  • initflag 参数是一个 pthread_once_t 类型全局对象,将其初始化为 PTHREAD_ONCE_INIT
  • initfn是一个初始化函数, 可以在多个线程中调用但是只执行一次。

9、线程私有变量

系统为每一个线程提供了一个私有“容器”,该容器中保存的是一个个的键值对。通过键可以获取值,也可以向此容器中保存值。pthread 中,对键的类型和值的类型都是有要求的。

键的类型是 pthread_key_t,它只能通过函数 pthread_key_create 进行初始化。它的定义如下:

int pthread_key_create(pthread_key_t *key, void (*destructor(void*));

函数第一个参数,键可被任意一个线程使用。

函数第二个参数,它需要传递一个析构函数。当线程运行结束(return 或 pthread_exit)时,该函数会自动调用。析构函数的参数,是与该键关联的值(value).

通过 pthread_key_delete 函数,可删除指定的键,但是不会调用析构函数:

int pthread_key_delete(pthread_key_t key);

线程容器中的值类型必须是 void*。可以通过下面的函数将键值对保存到线程自己的容器中。

int pthread_setspecific(pthread_key_t key, const void *value);

不同线程调用上面的函数时,只会将键值对保存到自己(指线程自己)的容器中。 所以,我们把这种键值对称为线程私有数据。

可以通过下面的函数根据键来获取对应的值:

void* pthread_getspecific(pthread_key_t key);

注意,获取到的值类型仍然是 void* 类型。

10、errno 变量与多线程

errno 变量一直被用于调用系统函数时出错后,被赋上错误码的整数变量。errno 变量是线程安全的。

本质上 errno 并不是一个真正意义上的变量,而是通过宏定义扩展为语句,而这一行语句实际上是在调用函数,该函数返回保存了指向 errno 变量的指针。在 bits/errno.h 中的 errno 的实现:

这里写图片描述

我们也可以通过线程的私有变量和pthread_once实现errno:

// myerrno.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>

// 实际上 myerrno 就是一个宏定义
#define myerrno (*_myerrno())

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;

// 使用 pthread once 对键进行初始化
void thread_init() {
  puts("I'm thread_init");
  pthread_key_create(&key, free); // 这里注册了析构函数就是 free
}

// 该函数用来获取真正的 myerrno 的地址
int *_myerrno() {
  int *p; 
  pthread_once(&init_done, thread_init);
  // 如果根据键拿到的是一个空地址,说明之前还未分配内存
  p = (int*) pthread_getspecific(key);
  if (p == NULL) {
    p = (int*)malloc(sizeof(int));
    pthread_setspecific(key, (void*)p);
  }
  /**************************************/
  return p;
}

void* fun1() {
  errno = 5;
  myerrno = 5; // 这一行被扩展成 (*_myerrno()) = 5
  sleep(1);
  // printf 后面的 myerrno 会被扩展成 (*_myerrno())
  printf("fun1: errno = %d, myerrno = %d\n", errno, myerrno);
  return NULL;
}

void* fun2() {
  errno = 10; 
  myerrno = 10;  // 这一行被扩展成 (*_myerrno()) = 10
  printf("fun2: errno = %d, myerrno = %d\n", errno, myerrno);
  return NULL;
}

int main() {
  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, fun1, NULL);
  pthread_create(&tid2, NULL, fun2, NULL);
  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);
  return 0;
}

11、信号与多线程

在多线程中,每一个线程都有属于自己的阻塞信号集与未决信号集。当一个线程派生另一个线程的时候,会继承父线程的阻塞信号集,但是不会继承未决信号集,并且新线程会清空未决信号集。

在多线程程序中,如果要设置线程的阻塞信号集,不能再使用 sigprocmask 函数,而应该使用 pthread_sigmask,其定义如下:

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

这个函数的用法和 sigprocmask 是一样的。

how 参数

SIG_BLOCK 该选项表示将 set 参数指示的信号集中的信号添加到进程阻塞集中
SIG_UNBLOCK 该选项与功能 SIG_BLOCK 相反,表示将线程阻塞信号集中指定的信号删除
SIG_SETMASK 该选项表示将线程阻塞信号集直接设定为你指定的 set
set 参数

表示你指定的信号集合

oldset
返回旧的阻塞信号集

返回值 int
0 表示成功,-1 失败。

 获取未决信号的函数

int sigpending(sigset_t *set);

kill 函数只能给指定的进程发送函数,而是使用 pthread_kill 可以给指定的线程发送函数。它的原型如下:

int pthread_kill(pthread_t thread, int sig);

如果在线程中调用 sigwait,它会一直等待它指定的信号,直到未决信号集中出现指定的信号为止,同时 sigwait 还会从未决信号集中取出该信号返回,并将该信号从未决非信号集中删除。如果多线程中调用 sigwait 等待同一个信号,只会有一个线程可以从 sigwait 中返回。

int sigwait(const sigset_t *set, int *sig);

参数 set 表示要等待哪些信号,一旦 sigwait 函数返回,会从未决信号集中取出信号,放到参数 sig 指向的内存中。

12、多进程与多线程

在多线程程序中使用 fork,可能会导致一些意外:

  • 子进程中只剩下一个线程,它是父进程中调用 fork 的线程的副本构成。这意味着在多线程环境中,会导致“线程蒸发”,莫名奇妙的失踪!
  • 因为线程蒸发,它们所持有的锁也可能未释放,这将导致子进程在获取锁时进入死锁。

解决方法:

  1. 我们在fork()之前现获取锁,然后执行完fork之后再释放锁 ,代码类似这样
pthread_mutex_lock(&lock);
pid = fork();
pthread_mutex_unlock(&lock);

2、pthread_atfork  函数

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

上面三个回调函数,分别为 prepare 函数,parent 函数,child 函数。这些函数的调用时机如下:

  • prepare 函数是在 fork 还会产生子进程时调用
  • parent 和 child 是在产生子进程后调用
  • parent 在父进程中调用,child 在子进程中调用

用伪代码来说明调用时机:

prepare();
pid = fork();

if (pid > 0) {
  parent();
}
else if (pid == 0) {
  child();
}

参考https://blog.csdn.net/q1007729991 

猜你喜欢

转载自blog.csdn.net/u014608280/article/details/86064834
今日推荐