互斥锁 条件变量 信号量 读写锁 自旋锁
互斥锁: 资源只能一个线程用
// 初始化一个互斥锁。 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,直到互斥锁解锁后再上锁。 int pthread_mutex_lock(pthread_mutex_t *mutex); // 调用该函数时,若互斥锁未加锁,则上锁,返回 0; // 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。 int pthread_mutex_trylock(pthread_mutex_t *mutex); // 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量 // 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout); // 对指定的互斥锁解锁。 int pthread_mutex_unlock(pthread_mutex_t *mutex); // 销毁指定的一个互斥锁。互斥锁在使用完毕后, // 必须要对互斥锁进行销毁,以释放资源。 int pthread_mutex_destroy(pthread_mutex_t *mutex);
条件变量【问题多多】
初始化:
int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr)或者pthread_cond_tcond=PTHREAD_COND_INITIALIER
;属性置为NULL;
等待条件成立:
int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex)或int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp, const structtimespec * abstime);
释放mutex锁,并阻塞等待条件变量为真,该函数内部将mutex加锁,继续执行。条件为真+加锁 为一个原子操作。
pthread_cond_timedwait设置等待时间,在规定的时间内,未收到唤醒信号,则返回ETIMEDOUT,收到信号,该函数内部将mutex加锁,继续执行。条件为真+加锁 为一个原子操作。
激活条件变量:
int pthread_cond_signal(pthread_cond_t *cv); 激活一个线程
int pthread_cond_broadcast(pthread_cond_t *cv);激活所有等待线程
清除条件变量:
int pthread_cond_destroy(pthread_cond_t *cv);返回值:函数成功返回0;任何其他返回值都表示错误:有线程等待,返回EBUSY
释放条件变量。
注意:条件变量占用的空间并未被释放。
当资源条件满足后,它能够唤醒一个线程pthread_cond_signal(cond) 或者多个线程pthread_cond_broadcast(cond),但是存在 虚假唤醒与唤醒丢失的问题。
虚假唤醒:
https://blog.csdn.net/just_kong/article/details/98871393
唤醒操作(SetEvent和pthread_cond_signal)原本意图是唤醒一个等待的线程,但是在多核处理器下,可能会激活多个等待的线程,这种效应为“虚假唤醒”。linux帮助文档中提到:虽然虚假唤醒在pthread_cond_wait函数中可以解决,为了发生概率很低的情况而降低边缘条件(fringe condition)效率是不值得的,纠正这个问题会降低对所有基于它的所有更高级的同步操作的并发度。所以pthread_cond_wait的实现上没有去解决它。所以通常的解决办法是在线程被激活后还需要检测等待的条件是否满足,例如下图所示。
pthread_cond_wait中的while()不仅仅在等待条件变量前检查条件,实际上在等待条件变量后也检查条件。
唤醒丢失:
如果在等待条件变量(pthread_cond_wait)前,条件变量就被唤醒激活(pthread_cond_signal),那么这次唤醒就会丢失。
例如客户端向服务端发送同步消息时,客户端需要等到服务的回应再返回发送接口,这时需要在发送接口内部等待回应。
唤醒丢失问题可以采用信号量来解决。
信号量:
【计数信号量 POSIX信号量】
#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 信号量 P 操作(减 1)
int sem_wait(sem_t *sem);
// 以非阻塞的方式来对信号量进行减 1 操作
int sem_trywait(sem_t *sem);
// 信号量 V 操作(加 1)
int sem_post(sem_t *sem);
// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
// 销毁信号量
int sem_destroy(sem_t *sem);
【二值信号量 == System V信号量】用作二值
System V 信号量在内核中维护,其中包括二值信号量 、计数信号量、计数信号量集。
二值信号量 : 其值只有0、1 两种选择,0表示资源被锁,1表示资源可用;
计数信号量:其值在0 和某个限定值之间,不限定资源数只在0 1 之间;
计数信号量集 :多个信号量的集合组成信号量集
互斥锁和二值信号量在使用上非常相似,二值信号量维护的是资源顺序的问题,锁可以理解为是无序的。
System V 信号量函数头文件及相关函数原型: #include <sys/sem.h> int semget(key_t key, int nsems, int oflag); 功能:创建一个信号量集或访问一个已经存在的信号量集 返回值:成功返回非负的标识符,出错返回-1 参数:key是信号量的键值,多个进程可以通过这个键值访问同一个信号量;nsems参数指定信号量集合中的信号量数,一般设为1,如果不创建新的信号量集,只是访问一个已经存在的集合,可以把该参数设为0,一旦创建完一个信号量集,就不能改变其中的信号量数;oflag同open()权限位,IPC_CREAT标示创建新的信号量,如果或上IPC_EXCL,若信号量已存在则出错,如果没有或上IPC_EXCL,若信号量存在也不会出错。 int semctl(int semid, int semnum, int cmd, ... /*union semun arg */); 功能: 信号量控制操作。 返回值:若成功,根据cmd不同返回不同的值,IPC_STAT,IPC_SETVAL,IPC_RMID返回0,IPC_GETVAL返回信号量当前值;出错返回-1. 参数:semid标示操作的信号量集;semnum标示该信号量集内的某个成员(0,1等,直到nsems-1),semnum值仅仅用于GETVAL,SETVAL,GETNCNT,GETZCNT,GETPID,通常取值0,也就是第一个信号量;cmd:指定对单个信号量的各种操作,IPC_STAT,IPC_GETVAL,IPC_SETVAL,IPC_RMID;arg: 可选参数,取决了第三个参数cmd。
int semop(int semid, struct sembuf *opstr, size_t nops); 功能:操作信号量,P,V 操作 返回值:成功返回信号量标识符,出错返回-1 参数:semid:信号量集标识符;nops是opstr数组中元素数目,通常取值为1;opstr指向一个结构数组 struct sembuf{ short sem_num; short sem_op; short sem_flg; } sem_num 操作信号的下标,其值可以为0 到nops sem_flg为该信号操作的标志:其值可以为0、IPC_NOWAIT 、 SEM_UNDO 0 在对信号量的操作不能执行的情况下,该操作阻塞到可以执行为止; IPC_NOWAIT 在对信号量的操作不能执行的情况下,该操作立即返回; SEM_UNDO当操作的进程推出后,该进程对sem进行的操作将被取消; sem_op取值 >0 则信号量加上它的值,等价于进程释放信号量控制的资源 sem_op取值 =0若没有设置IPC_NOWAIT, 那么调用进程将进入睡眠状态,直到信号量的值为0,否则进程直接返回 sem_op取值 <0则信号量加上它的值,等价于进程申请信号量控制的资源,若进程设置IPC_NOWAIT则进程再没有可用资源情况下,进程阻塞,否则直接返回。 一般编程步骤: 1. 创建信号量或获得在系统中已存在的信号量 1). 调用semget(). 2). 不同进程使用同一个信号量键值来获得同个信号量 2. 初始化信号量 1).使用semctl()函数的SETVAL操作 2).当使用二维信号量时,通常将信号量初始化为1 3.进行信号量PV操作 1). 调用semop()函数 2). 实现进程之间的同步和互斥 4.如果不需要该信号量,从系统中删除 1).使用semctl()函数的IPC_RMID操作
关于PV操作
p操作和v操作是不可中断的程序段,称为原语。P,V原语中P是荷兰语的Passeren,相当于英文的pass, 意为通过,V是荷兰语的Verhoog,相当于英文中的incremnet,意为释放。
#include<unistd.h> #include<stdio.h> #include<sys/sem.h> //仓库货架空位 10 定为0号信号量 //产品数目 0 定为 1 号信号量 int main(int argc,char * argv[]) { srand(100*1000*1000); int i = rand() % 100*1000; int sems_id; sems_id=semget(1000,2,IPC_CREAT|0600); unsigned short arr[2]={10,0};//第一个为仓库货架空位个数,第二个为产品个数 int ret=semctl(sems_id,0,SETALL,arr); //令父进程为生产者,子进程为消费者。 struct sembuf sopp,sopv; if(!fork()) {//子进程为出库,空位+1,产品数-1 sopp.sem_num=1;//1号信号量为产品数 sopp.sem_op=-1;//对产品数进行+1 sopp.sem_flg=SEM_UNDO;//防止程序死锁 sopv.sem_num=0;//0号信号量为空位 sopv.sem_op=1;//对空位数进行+1 sopv.sem_flg=SEM_UNDO; while(1) { semop(sems_id,&sopp,1);//产品出库,产品数目-1 semop(sems_id,&sopv,1);//货架空位+1 printf("customer Mark1,stack pos num=%d,product num=%d\n",semctl(sems_id,0,GETVAL),semctl(sems_id,1,GETVAL)); printf("Now starring produce\n"); printf("customer Mark1,stack pos num=%d,product num=%d\n",semctl(sems_id,0,GETVAL),semctl(sems_id,1,GETVAL)); printf("-------------------------------------------------------\n"); i = rand() % 100*1000; usleep(i); } }else {//父进程入库,空位-1.产品数+1 sopp.sem_num=0;//0号信号量为空位 sopp.sem_op=-1;//对空位-1 sopp.sem_flg=SEM_UNDO; //sopp.sem_flg=0; sopv.sem_num=1;//1号信号量为产品数 sopv.sem_op=1;//对产品数进行+1 sopv.sem_flg=SEM_UNDO; while(1) { semop(sems_id,&sopp,1);//产品入库,空位-1,p操作 semop(sems_id,&sopv,1);//产品个数+1 printf("customer Mark1,stack pos num=%d,product num=%d\n",semctl(sems_id,0,GETVAL),semctl(sems_id,1,GETVAL)); printf("Now starring produce\n"); printf("customer Mark1,stack pos num=%d,product num=%d\n",semctl(sems_id,0,GETVAL),semctl(sems_id,1,GETVAL)); printf("-------------------------------------------------------\n"); i = rand() % 100*1000; usleep(i); } } return 0; }
(1)使用条件变量可以一次唤醒所有等待者,而这个信号量没有的功能,感觉是最大区别。
(2)信号量是有一个值(状态的),而条件变量是没有的,没有地方记录唤醒(发送信号)过多少次,也没有地方记录唤醒线程(wait返回)过多少次。从实现上来说一个信号量可以是用mutex + counter + condition variable实现的。因为信号量有一个状态,如果想精准的同步,那么信号量可能会有特殊的地方。信号量可以解决条件变量中存在的唤醒丢失问题。
(3)在Posix.1基本原理一文声称,有了互斥锁和条件变量还提供信号量的原因是:“本标准提供信号量的而主要目的是提供一种进程间同步的方式;这些进程可能共享也可能不共享内存区。互斥锁和条件变量是作为线程间的同步机制说明的;这些线程总是共享(某个)内存区。这两者都是已广泛使用了多年的同步方式。每组原语都特别适合于特定的问题”。尽管信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。应当根据实际的情况进行决定。信号量最有用的场景是用以指明可用资源的数量。
读写锁
读写锁特性:
-
读写锁是"写模式加锁"时, 解锁前,所有对该锁加锁的线程都会被阻塞。
-
读写锁是"读模式加锁"时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
-
读写锁是"读模式加锁"时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况, 如 数据库。
API:
1,读写锁的初始化与销毁,静态初始化的话,可以直接使用PTHREAD_RWLOCK_INITIALIZER。
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
2,用读的方式加锁和尝试(没锁上就立即返回)加锁。
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
3,用写的方式加锁和尝试(没锁上就立即返回)加锁。
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
4,解锁
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
自旋锁(应该等价于windows 下的临界区CRITICAL_SECTION)
https://blog.csdn.net/hhhanpan/article/details/80624244
多用于Linux内核同步
自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被已经持有(争用)的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用要是锁未被争用,请求锁的执行线程就可以立即得到它,继续执行。
自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。
在单处理机器上,编译的时候不会加入自旋锁,仅会被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁就会被剔除出内核。