一、线程的概念
1.1 什么是线程
线程是一个实体,是CPU调度和分派的基本单元,有时我们也可以称它为轻量级的进程。每个线程都包含有表示执行环境所必须的信息,其中包括标识线程
的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、error变量以及线程私有数据(下面我们会讨论)。除此之外,我们知道线程还包括所在
进程的所以信息,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。
1.2 多线程的好处
在典型的Unix进程可以看做只有一个控制线程,在某一时刻只能做一件事情,当我们有了多线程以后进程在某一时候就不止做一件事情,每个线程可以做一
个独立的任务。
1. 将不同事件类型分配给不同的线程,可以简化处理异步事件的代码
2. 多进程必须使用的操作系统复杂的机制才能实现内存和文件描述符的共享,而多线程可以很容易的实现储存地址和文件描述符的访问。
3. 可以提高程序的吞吐量。
1.3 关于多线程和多处理器的理解
有些童鞋认为多线程需要多处理器才可以执行,其实这是错误的认识,我们即使在单处理器上也可以使用多线程。在单处理器中多线程程序在执行串行任
务时,一旦某个线程阻塞其他线程任然可以继续执行,所以即使是单处理器我们也可以使用多线程编程,同样可以提高我们程序的吞吐量和改善我们程序的结构。
二、线程的基操(基本操作)本节主要讲线程的基础知识,线程的创建以及同步。
2.1 线程标识
所谓的标识其实就是线程ID,我们知道每个进程都已唯一的一个ID,同样线程也有自己的ID,进程的ID是在整个系统中唯一的,那么线程ID也是唯一的吗?
答案是 No!!!,切记线程ID不是唯一的,它只在它所属的进程上下文有意义。
我们知道进程是有函数getpid()可以获取进程ID的,并由pid_t的数据类型来表示,而且还是一个非负数。那么在线程中线程ID是如何获取呢?
#include <pthread.h>
pthread_t pthread_self(void);
返回值:调用线程的线程ID。
注意:我们知道pid其实是一个int类型的数,当我们第一次调用pthread_self()拿到线程ID时一看懵逼了,这啥呀?怎么和pid一点都不一样呢
(差不多是这样的:140693894424320)?哈哈哈 其实我第一次用的时候就是一脸懵逼,大家稍安勿躁,在我们linux系统中线程ID是用无符号
长整型(%lu)来表示的,它看起来像一个指针。
2.2 线程的创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
功能:创建线程
参数:thread 用来存放线程的id号
attr 线程的属性,一般用null
start_routine 函数指针,指向一个函数,函数的功能就是此线程要干的事
arg:给函数传的参数,没有为null
返回值:成功为0 失败返回 errno
当创建成功时,新线程的线程ID会被设置成 thread指向的内存单元(这也说明了为什么线程ID像一个指针了),新创建的线程就从我们的start_routine
函数地址开始运行。新创建的线程不会保证哪个线程会先运行:是新创建的线程呢?还是调用的线程呢? 这个依赖操作系统的调度算法。
2.3 线程的终止
2.3.1 如果进程中任意线程调用了exit、_Exit 或者 _exit函数,那么整个程序终止。下面列出单个线程退出的三种方式,可以在整个进程不退出情况下停止进程
1. 线程简单地从启动程序返回,返回值是线程的退出码。
2. 线程被同一进程中其他线程取消
3. 线程调用pthread_exit
2.3.2 #include <pthread.h>
void pthread_exit(void *retval);
功能:退出线程
参数:传递给接收函数的参数,标识退出状态
2.3.3 int pthread_join(pthread_t thread,void **retval)
功能:阻塞等待接收线程的退出状态,回收其资源
参数:thread 线程号
retval 存放线程退出的状态,如果不关心退出状态,传NULL
返回值:成功0 ,失败 errno的值
调用pthread_join线程将一直阻塞,与直到线程调用pthread_exit、从启动程序中返回或者被取消。pthread_join会将线程置于分离状态(下面我们讨论,
同时讨论pthread_join与pthread_detach的区别),这样资源就可以恢复,如果已经处于分离状态,则返回错误。
2.3.4 #include <pthread.h>
int pthread_cancel(pthread_t tid);
功能:取消进程中其他线程。
参数:tid 线程号
返回值:成功0 ,失败 errno的值
2.3.5 #include <pthread.h>
int pthread_detach(pthread_t tid);
功能:分离线程,主线程与子线程分离,子线程结束后,资源自动回收。
参数:tid 线程号
返回值:成功0 ,失败 errno的值
2.3.6 pthread_join与pthread_detach的区别:
一旦线程处于分离状态,该线程终止时底 层资源立即被回收;否则终止子线程的状态会一直保存(占用系统资源)直到主线程调用pthread_join获取线程的退出状态。
通常是主线程使用pthread_create()创建子线程以后,一般可以调用pthread_detach(threadid)分离刚刚创建的子线程,这里的threadid是指子线程的threadid;如此以来,该子线程止时底层资源立即被回收。
2.4 线程的同步
当多个控制线程共享相同内存时候,需要确保每个线程看到的数据一致。当一个线程在修改一个变量时候另一个线程读取到的变量可能会看到不一致的结果。
为此线程不得不使用锁来解决这个问题,在同一时间只允许一个线程访问该变量。对此我们可以使用互斥锁(互斥量),信号量、条件变量、自旋锁来对数据进行控制实现线程的同步。
2.4.1. 互斥锁:在访问共享资源前对互斥量进行加锁,访问完之后解锁。如果加锁之后其他线程试图对互斥量再次加锁则会被阻塞,如果解锁时有一个以上线程阻塞,则这些阻塞状态
变为可运行状态,第一个变为可运行状态的线程获得锁。如果某个线程在没有得到锁的前提下也可以访问共享数据,那么及时其他线程使用锁也是会出现数据不一致的情况。
互斥变量是用 pthread_mutex_t 数据类型表示的,在使用之前必须对其初始化,使用完之后销毁锁。
2.4.1.1.初始化互斥锁
#include <pthread.h>
动态 int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数:mutex:锁标识
attr:锁的属性,一般用NULL;
返回值:成功返回0 失败返回错误值
静态:static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; 初始化定义一气呵成
2.4.1.2.上锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数:锁的标识
返回值:成功0
2.4.1.3.开锁
#include <pthread.h>是
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:锁的标识
返回值:成功0
2.4.1.4.销毁锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:锁的标识
返回值: 0
2.4.1.5 设置锁超时
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timesec *restrict tsptr);
功能:到达超时时间值时不会再对互斥量加锁,返回错误码ETIMEDOUT
参数:mutex: 锁的标识
tsptr: 超时等待的绝对时间,这个时间用timespec结构体表示的。
返回值:成功0
2.4.1.6 如果我们不希望线程阻塞,我们可以使用pthread_mutex_trylock尝试对互斥量进行加锁,如果调用该函数时候互斥量处于未锁状态,那么将锁住互斥量,
不会出现阻塞状态并返回0,否则失败返回EBUSY。
2.4.1.7 死锁的产生与避免
产生:1. 当线程试图对一个互斥量加锁两次。
2. 当程序使用一个以上互斥量时,一个线程一直占有一个互斥量,并试图锁住第二个互斥量,但是拥有第二个互斥量的线程也在尝试锁住第一个互斥量,
两个线程都在相互请求对方的资源导致程序无法继续向前所以导致死锁。
避免:尽量使用 pthread_mutex_trylock()和pthread_mutex_timedlock()函数。
2.4.1.8 互斥锁的扩展:读写锁
读写锁与互斥量类似,但是读写锁拥有更高的并行性,读写锁有三种状态:读模式加锁、写模式加锁和不加锁,对于写模式一次只能有一个线程加锁,但是读模式
可以有多个线程同时占有。当锁处于写加锁时,任何试图对这个锁加锁的线程都会阻塞,当为读加锁时,所有试图读模式加锁都可以得到访问,但是写模式加锁会
阻塞。所以读写锁很适合对数据结构读远远大于写的模式下使用。
用法和互斥量一样使用之前也需要初始化。
2.4.1.8.1 初始化读写锁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
功能:初始化读写锁
参数:如果使用默认属性 attr为null
返回值:成功0
2.4.1.8.2 去初始化读写锁
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:去初始化读写锁
参数:
返回值:成功0
2.4.1.8.3 读模式加锁
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
功能:读模式加锁
参数:
返回值:成功0
2.4.1.8.4 写模式加锁
#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:写模式加锁
参数:
返回值:成功0
2.4.1.8.5 解锁
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:解锁
参数:
返回值:成功0
2.4.1.8.6 带超时的读锁
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict abs_timeout);
功能:带超时的读锁
参数:
返回值:成功0
2.4.1.8.7 带超时的写锁
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict abs_timeout);
功能:带超时的写锁
参数:
返回值:成功0
2.4.2 信号量
也就是操作系统中的PV原子操作,用于进程或线程间的同步与互斥。本质是一个非负整数计数器,被用来控制对公共资源的访问。
初始化:P操作(申请资源)如果有可用资源(信号量值大于0),则占用一个资源(信号量值-1,进入临界区代码),如果没有可用资源(信号量值等于0),则被阻塞,知道系统将资源分配给该任务,(进入等待队列,直到有资源被唤醒)
V操作(释放资源)如果在该信号量的等待队列中有任务在等待资源,则唤醒一个阻塞任务,如果没有任务等待它,则释放一个资源(信号量值+1)。
2.4.2.1.初始化:#include <semaphore.h> Link with -pthread.
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
参数:sem 信号量的标识
pshsred: 0 用于线程之间, 非0 用于进程之间(目前Linux还没有实现进程间共享信号量)
value:信号量初始化值 表示可用的资源的数目,若 为0则表示当前没有可用资源, 一般用0 和1
返回值:成功0 失败-1
2.4.2.2.申请资源:
#include <semaphore.h>
int sem_wait(sem_t *sem); Link with -pthread.
功能:申请资源
参数:sem‘’
返回值:成功0 失败-1
2.4.2.3.释放资源:
#include <semaphore.h>
int sem_post(sem_t *sem); Link with -pthread.
功能:释放资源
参数:sem‘’
返回值:成功0 失败-1
2.4.2.4.销毁信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem); Link with r -pthread.
功能:释放资源
参数:sem‘’
返回值:成功0 失败-1
2.4.3 条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;
另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
在使用条件变量之前需要对其进行初始化,由pthread_cond_t数据类型表示条件变量。和互斥量一样分为静态初始化和动态初始化,同样只有动态初始化才需要去初始化。
2.4.3.1 初始化
#include <pthread.h>
静态:pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr) cond_attr一般为null
2.4.3.2 去初始化
int pthread_cond_destory(pthread_cond_t *cond);
2.4.3.3 等待条件变量
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
功能:等待条件变量为真,pthread_cond_timedwait如果在给定时间内没有满足条件则返回错误码,通过timespec结构体指定,这里的时间需要注意了,
如果我们想要超时时间为五分钟,那么我们需要在当前时间上加上五分钟。
参数:cond:条件变量
mutex:互斥量
bstime:设置超时时间
返回值:成功0
2.4.3.4 唤醒条件变量
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:通知线程条件已满足,二者区别是signal函数可以唤醒一个等待该条件变量的线程,而broadcast是唤醒全部等待该条件变得的线程
参数:cond 条件变量
返回值:成功0
2.4.4 自旋锁
自旋锁与互斥量类似,但它不是通过休眠让线程处于阻塞状态,而是在得到锁之前一直处于忙等状态阻塞线程。自旋锁一般在底层驱动使用,锁的持有时间短
而且不希望线程在重新调度上花费太多时间,自旋锁在非抢占式内核中非常有用,因为他们除了提供互斥机制外还可以阻塞中断,这样中断处理程序不会让系
统陷入死锁状态。在用户层自旋锁很少用。
2.自旋锁(重点):在访问共享资源之前,先要获得自旋锁,接着对资源上锁,访问完之后再解锁,其它内核路径(进程)如果没有得到锁,但想要访问资源,只能等待。(同线程互斥锁原理相同)
2.4.4.1 特点:
1.针对多核CPU。
2.如果获取不到锁处于自旋状态。(消耗CPU)
3.自旋锁处在中断上下文(临界资源小),临界代码执行时间不可太长。
4.在获得锁期间,不可调用会引起进程切换的函数,如果新调度的进程需要自旋锁,会导致死锁。
5.在上锁前将中断禁止使用 spin_lock_irqsave函数。
6.一个进程只能拥有一个锁,否则导致死锁,内核崩溃(亲测)
2.4.4.1 API:
#include<linux/spinlock.h>
spinlock_t lock; //定义一把锁
spin_lock_init(&lock)//初始化锁
spin_lock(&lock) //上锁
spin_unlock(&lock) //解锁
spin_lock、spin_lock_irq、spin_lock_irqsave区别
void spin_lock(spinlock_t *lock);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。
举个例子:进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),
该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)
试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,
中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就
无法再调度进程A运行,这样就导致了死锁!但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。 因为在不同的CPU上出现中断不会导致
进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。
如果自旋锁在中断处理函数中被用到,那么在获取该锁之前需要关闭本地中断,spin_lock_irqsave 只是下列动作的一个便利接口:
解锁时通过 spin_unlock_irqrestore完成释放锁、恢复本地中断到之前的状态等工作
2.4.5 屏障
屏障是用户协调多个线程并行工作的同步机制,屏障允许每个线程等待,直到所有合作的新村都到达某一点,然后从改点继续执行。其实我们前面看到过一种屏障了
没错,就是pthread_join()函数,屏障允许任意数量等待,直到所有线程完成工作不用退出继续工作。
2.4.5.1 初始化屏障
#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);
功能:初始化屏障
参数:count:允许所有线程继续运行之前,必须达到屏障的最大线程数目
attr:可以设置为null
返回值:成功0
2.4.5.2 去初始化
#include <pthread.h>
int pthread_barrier_destroy(pthread_barrier_t *barrier);
功能:去初始化
返回值:成功0
2.4.5.3 设置等待屏障
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
功能:线程在屏障技术未满足条件时进入休眠状态,直到最后一个线程满足计数则唤醒所以线程。
三、线程的控制:本节主要讲控制线程的属性和控制线程行为的内容
3.1 线程属性
在前面我们pthread_create函数对于参数pthread_attr_t结构指针一直传参为null,现在我们可以使用pthread_attr_t来修改线程默认属性。
我们可以使用pthread_attr_init()函数初始化pthread_attr_t结构体,这样pthread_attr_t结构体包含了操作系统实现支持所有线程的默认属性。
3.1.1 Linux系统支持线程属性
detachstate 线程分离状态属性
guardsize 线程栈末尾的警戒缓冲区大小
stackaddr 线程栈的最低地址
stacksize 线程栈的最小长度
3.1.2 pthread_attr_t结构体初始化
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
返回值:成功0
3.1.3 pthread_attr_t结构体去初始化
#include <pthread.h>
int pthread_attr_destroy(pthread_attr_t *attr);
返回值:成功0
3.1.4 设置/获取线程detachstate属性
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
参数:attr:pthread_attr_t结构体指针
detachstate:设置/获取值为 PTHREAD_CREATE_DETACHED(分离状态) 或 PTHREAD_CREATE_JOINABLE(正常状态)
3.1.5 设置/获取线程栈
对于进程来说虚拟空间是大小不变的,对于线程来说许多个线共享一个进程的虚拟空间,但是每个线程都有自己的线程栈,一旦线程过多所有栈加起来大于
虚拟空间的大小,这时候就需要减小默认线程栈的大小。如果线程分配了很多自动变量,那么这时候就需要增大默认的线程栈。
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr,
void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr,
void **stackaddr, size_t *stacksize);
参数:stackaddr 指定线程栈内存范围中最低可寻址地址
stacksize 线程栈大小
3.1.6 设置/获取stacksize属性
stacksize为线程栈最低内存段地址,但这并不一定是开始位置,有些处理器栈地址是从高向低开始的,所以也可能是结尾位置。
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
3.1.7 设置/获取guardsize属性
guardsize属性是用来避免栈溢出的扩展内存大小,guardsize属性被修改之后线程指针如果溢出到警戒区域,那么线程就可以通过信号接收到错误信息
#include <pthread.h>
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);
3.2 同步属性
同步属性讨论的是互斥量属性、读写锁属性、条件变量属性和屏障属性
3.2.1 互斥量属性
互斥量属性使用pthread_mutexattr_t结构体表示的,对于非默认属性我们使用仍然需要初始化以及使用完之后去初始化。
pthread_mutexattr_init函数将用默认的属性初始化互斥量,其中包括三个属性:进程共享属性、健壮属性以及类型属性。
3.2.1.1 互斥量属性初始化
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
3.2.1.1 互斥量属性去初始化
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
3.2.1.2 进程共享属性
可以通过检查系统的 _POSIX_THREAD_PROCESS_SHAERED符号来判断系统是否支持进程共享属性,也可以使用sysconf()函数通过传递_SC_THREAD_PROCESS_SHAERED参数来检查
默认情况下在进程中多个线程可以访问同一个同步对象,进程共享属性设置为THREAD_PROCESS_PRIVATE,但是当多个进程需要共享互斥量时候就需要设置为THREAD_PROCESS_SHAERED
让多个线程之间共享内存数据块中分配的互斥量达到进程同步的功能。
3.2.1.3 进程共享属性获取/设置
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
int pshared);
参数:pshared:PTHREAD_PROCESS_SHARED 和 PTHREAD_PROCESS_PRIVAT
返回值:成功0
3.2.1.4 互斥量健壮属性
健壮性属性与进程共享属性有关,当持有互斥量进程终止时需要解决恢复互斥量状态,当互斥量处于锁状态时,恢复起来很困难,其他进程将阻塞
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *
restrict attr, int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr,
int robust);
参数:robust:PTHREAD_MUTEX_STALLED(默认动作,不会做任何动作) 和 PTHREAD_MUTEX_ROBUST
返回值:成功0
3.2.1.4 互斥量类型属性
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,
int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
3.2.2 读写锁属性
读写锁唯一支持的属性是进程共享属性
3.2.2.1 去/初始化
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
3.2.2.2 读取/设置读写锁属性
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *
restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
int pshared);
3.2.3 条件变量属性
目前条件变量属性有两个:进程共享属性和时钟属性,进程共享属性和其他同步属性一样。时钟属性可以使用pthread_condattr_getclock()函数获取
用于被pthread_cond_timedwait函数的时钟id。
3.2.3.1 去/初始化
#include <pthread.h>
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);
3.2.3.2 获取/设置条件变量进程共享属性
#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,
int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,
int pshared);
3.2.3.3 获取/设置时钟属性
int pthread_condattr_getclock(const pthread_condattr_t *restrict attr,
clockid_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr,
clockid_t clock_id);
3.2.4 屏障属性
屏障属性只有进程共享属性,它控制屏障可以被多个进程的线程使用还是只能背初始化屏障的进程内多线程使用。
3.2.4.1 去/初始化
#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destory(pthread_barrierattr_t *attr);
3.2.4.2 获取/设置条件变量进程共享属性
#include <pthread.h>
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,
int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
int pshared);
3.3 线程特定数据
特定数据也叫私有数据,每个线程都会有的私有数据,是存储和查询某个线程相关数据的一种机制。在我们分配特定数据之前,需要创建与该数据相关连的键,
这个键用于获取对线程特定数据的访问。这个键可以被进程中所有线程访问
3.3.1 创建键
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
参数:key创建的键存在key:创建的键存在key
3.3.2 取消键与线程特定数据值得关联
#include <pthread.h>
int pthread_key_delete(pthread_key_t *key);
3.3.3 防止在初始化阶段发送竞争,确保线程特定数据只创建一个键
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control,
void (*init_routine)(void));
3.3.4 将键和线程特定数据关联
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
参数:value:pthread_getspecific()返回的值,如果返回值为null,则先分配内存,再与该内存缓冲区关联
3.3.5 获取特定数据地址
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
返回值:线程特定数据值,若没有关联返回null
3.4 线程和信号
每个线程都有自己的信号屏蔽字,但是信号处理是所以线程共享进程的,这意味着如果一个线程修改或者忽略某个信号,那么其他信号也必须共享这个处理行为的
改变,但是其他线程可以取消其他线程的信号选择。两种方式:1、恢复信号默认处理行为,2、为信号设置一个新的信号处理。
3.4.1 阻止信号发送
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
参数:how:取下面三个值:SIG_BLOCK:把信号集添加到信号屏蔽字中
SIG_SETMASK:把信号集替换线程的信号屏蔽字
SIG_UNBLOCK:把信号集从信号屏蔽字中移除
set:信号屏蔽字的信号集
oldset:可以设为null
返回值:成功0
3.4.2 等待信号
如果信号集中某个信号在sigwit调用时候处于挂起状态,那么sigwait返回之后会将挂起状态的信号移除,如果多个线程在sigwait调用中因等待同一个
信号而阻塞,那么在信号递送时候只有一个线程可以从sigwait中返回,
#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict sig);
参数:set:指定线程等待的信号集
sig: 发送信号的数量
返回值:成功0
3.4.3 把信号发送给进程
可以将sig值为0来检查线程是否存在
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
3.5 线程和fork
当我们线程调用fork时,就位子进程创建了整个进程地址空间的副本,子进程继承整个地址空间之外还继承了互斥量、读写锁、条件变量的状态,当子进程在fork返回
后如果不是马上调用exec的话需要清理锁状态。但是子进程并没有继承父进程占有锁的线程副本,所以根本不知道它占有了哪些锁以及释放哪些锁。
3.5.1 清除锁状态
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void),
void (*child)(void));
参数:prepare 由父进程在fork子进程之前调用
parent fork创建子进程之后返回之前父进程上下文调用
child fork返回之前在子进程上下文调用
3.5.2 pthread_atfork函数存在问题
虽然pthread_atfork机制使得fork之后锁保持一致,但是还存在一些不足之处
1.对更复杂的同步对象进行状态的重新初始化,比如条件变量和屏障
2.递归互斥量无法清理
3.如果进程运行异步信号安全函数,则处理程序不可能清除同步对象
四、总结
到此为止我们已经把线程百分之九十的内容都学完了,我们学习了创建和销毁线程以及线程的同步机制,(互斥量、读写锁、条件变量、自旋锁、屏障),
了解了如何使用它们来保护共享资源,还有线程内部的一些属性那剩下的百分之十是什么呢?有我刚刚提到的递归锁、线程的可重入函数和取消选项(可取消状态和可取消类型)。这百分之十的内容有感兴趣的童鞋可以自己去学习一下。