Linux C/C++多线程同步(互斥量,死锁,读写锁,条件变量,信号量,文件锁)
1. 线程同步的一些概念
1.1 同步的概念
所谓同步,即同时起步,不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备间规定一个共同的时间参考;数据同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致。文件同步,是指让两个或多个文件夹里的内容保持一致,等等。
而编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。
1.2 什么是线程同步
同步即协同步调,按预定的先后次序运行。
线程同步,指一个县城发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。
举例1:银行存款5000。柜台,折:取3000;提款机,卡:取3000.剩余2000
举例2:内存中100字节,线程T1欲填入全1,线程T2欲填入全0.但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。
产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误,实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
1.3 多线程出现数据混乱(数据竞争)的原因
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争者)
- 线程间缺乏必要的同步机制
以上三点中前两点不能改变,想提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候出现互斥。
2. 互斥量mutex
- Linux提供一把互斥锁
mutex
(也称之为互斥量) - 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束后解锁。
- 资源还是共享的,线程间也还是竞争的,但通过锁将资源的访问变为互斥操作,而后与时间有关的错误也不会在产生了。
但是应该注意:同一个时刻,只能有一个线程持有该锁。
当A
线程对某个全局变量加锁访问,B
在访问前尝试加锁,拿不到锁,B
阻塞。C
线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是是操作系统提供的一把“建议锁”(又称“协同所”),建议程序中有多线程访问共享资源的时候使用该机制,但是,并没有强制限定。
2.1 mutex相关的函数和使用步骤
pthread_mutex_t
类型,其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex
:变量mutex
只有两种取值0
、1
;
pthread_mutex_init 初始化
pthread_mutex_destroy 摧毁
pthread_mutex_lock 加锁
pthread_mutex_unlock 解锁
互斥量的使用步骤:
- 初始化
- 加锁
- 执行逻辑——操作共享数据
- 解锁
注意事项:
加锁需要最小粒度,不要一直占用临界区(加锁到解锁之间的执行逻辑即为临界区)
2.1.1 初始化锁
功能:初始化一个互斥锁(互斥量);–>初值可看做1
int pthread_mutex_init( pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
返回值: 若成功,返回0,否则,返回错误编号
或者
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
参数:
mutex 传出参数,互斥量——锁
restruct 只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
attr 互斥属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享).
2.1.2 给共享资源加锁解锁
这些pthread_mutex_t *mutex
参数都是init
初始化的那个锁。
返回值: 若成功,返回0,否则,返回错误编号
pthread_mutex_lock(pthread_mutex_t *mutex)
功能:
如果当前未锁,成功,加锁,可理解为将mutex--(或-1)
如果当前已锁,阻塞等待,锁被打开之后,线程解除阻塞。
pthread_mutex_unlock(pthread_mutex_t *mutex)
功能:
解锁。可理解为将mtex++(或+1),同时将阻塞在该锁上的所有线程全部唤醒
pthread_mutex_trylock(pthread_mutex_t *mutex)
功能:
没有锁上:当前线程会给这把锁加锁
如果锁上了:不会阻塞,返回
lock与unlock:
- lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
- unlock主动解锁,同时将阻塞到该锁上所有线程全部唤醒,至于哪个线程先被唤醒取决于优先级,调度。默认:先阻塞、先唤醒。
例如:T1 T2 T3 T4 使用一把mutex锁,T1加锁成功,其他线程均阻塞,直至T1解锁,T1解锁后,T2 T3 T4均被唤醒,并自动再次尝试加锁。
可假想mutex锁init成功初值为1。lock功能是将mutex–。unlock将mutex++。
2.2.3 摧毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值: 若成功,返回0,否则,返回错误编号
参数也是init
初始化的那个锁。
2.2 互斥量使用的例子
通过互斥量,两个线程交替打印
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
//常量初始化锁——mutex(这样就不用init函数了),将其定义为全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int sum=0;
void *thr1(void *arg){
while(1){
//先上锁
pthread_mutex_lock(&mutex);//加锁,当有线程已经加锁的时候会阻塞
//加锁到解锁这一段为临界区
printf("hello");
sleep(rand()%3);
printf("world\n");
//释放锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
}
void *thr2(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("HELLO");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
}
int main(){
pthread_t tid[2];
pthread_create(&tid[0],NULL,thr1,NULL);
pthread_create(&tid[1],NULL,thr2,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
return 0;
}
运行结果:
如果不加锁:
2.3 pthread_mutex_trylock
pthread_mutex_unlock(pthread_mutex_t *mutex)
lock与trylock:
- lock加锁失败会阻塞,等待锁释放。
- trylock加锁失败直接返回错误号(如EBUSY),不阻塞。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
pthread_mutex_t mutex;
void *thr(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("hello world\n");
sleep(30);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
sleep(1);
while(1){
int ret = pthread_mutex_trylock(&mutex);
//加锁失败
if(ret > 0){
printf("ret = %d,srrmsg:%s\n",ret,strerror(ret));
}
sleep(1);
}
return 0;
}
运行结果:
返回值的错误码是
16
,我们来看一下16
代表什么意思。
错误码定义的地方:
/usr/include/asm-generic/errno-base.h
/usr/include/asm-generic/errno.h
/usr/include/errno.h
3. 死锁
产生条件
-
锁了又锁,自己加了锁,自己又加了一把锁。一般都是有分支或者写代码的时候忘了会出现这个问题
-
交叉锁(如下图所示)——解决方案:1、每个线程申请锁的顺序要一致;2、如果申请到一把锁,另一个申请不到,则释放已有资源
4. 读写锁
读共享,写互斥,写的优先级高。适合读的线程多的场景
读写锁仍然是一把锁,有不同的状态:未加锁、读锁、写锁。
4.1 读写锁特性
- 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
- 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功,如果线程以写模式加锁会阻塞。
场景例子:
(1)线程A加读锁成功,又来了三个线程,做读操作,可以加锁成功【读共享 - 并行处理】
(2)线程A加写锁成功,又来了三个线程,做读操作,三个线程阻塞 【写独占 】
(3)线程A加读锁成功,又来了B线程加写锁阻塞,又来了C线程加读锁阻塞【读写不能同时;写的优先级高(写锁都阻塞了那么之后的读锁肯定阻塞)】
4.2 读写锁使用场景
普遍读写锁在读的线程居多的时候使用。
读:并行
写:串行
读写锁练习场景:
- 线程A加写锁成功,线程B请求读锁
线程B阻塞 - 线程A持有读锁,线程B请求写锁
线程B阻塞 - 线程A持有读锁,线程B请求读锁
线程B加锁成功 - 线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
B阻塞,C阻塞 - 写的优先级高
A解锁,B线程加写锁成功
B解锁,C加读锁成功 - 线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁
BC阻塞
A解锁,C加写锁成功,B继续阻塞
C解锁,B加读锁
4.3 读写锁主要操作函数
初始化读写锁:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
或者:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
销毁读写锁:
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加读锁:
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
阻塞:之前对这把锁加的写锁的操作
尝试加读锁:
pthread_rwlock_trydlock(pthread_rwlock_t *rwlock);
返回值:
加锁成功 0
失败 错误号
加写锁:
pthread_rwlock_wrlock(pthread_rwlock_t *rwlocl);
上一次加锁写锁,还没有解锁的时候
上一次加读锁,没解锁
尝试加写锁:
pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
解锁:
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
4.4 读写锁例子
5个读,3个写
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int beginnum = 1000;
void *thr_write(void *arg) {
while(1){
//写锁加锁
pthread_rwlock_wrlock(&rwlock);
printf("---%s---self---%lu---beginnum---%d\n",__FUNCTION__,pthread_self(),++beginnum);
usleep(2000);//模拟占用时间
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(4000);
}
return NULL;
}
void *thr_read(void *arg) {
while(1){
//读锁加锁
pthread_rwlock_rdlock(&rwlock);
printf("---%s---self---%lu---beginnum---%d\n",__FUNCTION__,pthread_self(),beginnum);
usleep(2000);//模拟占用时间
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(){
//创建5个读锁和3个写锁
int n =8,i = 0;
pthread_t tid[8];//5-read ,3-write
for(i = 0; i < 5; i ++){
//参数依次是线程地址、线程属性、函数名、传入的参数
pthread_create(&tid[i],NULL,thr_read,NULL);
}
for(;i < 8; i ++){
//参数依次是线程地址、线程属性、函数名、传入的参数
pthread_create(&tid[i],NULL,thr_write,NULL);
}
for(i = 0; i < 8;i ++){
//线程回收
pthread_join(tid[i],NULL);
}
return 0;
}
执行结果:
5. 条件变量与消费者模式
条件变量概念是线程挂起直到共享数据的某些条件得到满足。
引入条件变量原因:mutex
会产生如下问题:
多个线程抢到锁之后,发现并没有资源,因此释放锁,然后继续多线程抢锁,然后释放锁,这户造成资源的浪费。
条件变量一般与互斥量协同作用
- 使用条件变量+互斥量
互斥量: 保护一块共享资源
条件变量:引起阻塞
生产者和消费者模型
条件变量的两个动作?
- 条件不满足,阻塞线程
- 当条件满足,通知阻塞的线程开始工作
条件变量的类型:pthread_cond_t
;
5.1 主要函数
pthread_cond_init 初始化
pthread_cond_destroy 销毁一个条件变量
pthread_cond_wait 阻塞等待一个条件变量
pthread_cond_timewait 限时等待一个条件变量
pthread_cond_signal 唤醒至少一个阻塞在条件变量上的线程
pthread_cond_broadcast 唤醒全部阻塞在条件变量上的线程
5.1.1 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
或者:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
5.1.2 销毁一个条件变量
pthread_cond_destroy(pthread_cond_t *cond);
5.1.3 阻塞等待一个条件变量
pthread_cond_wait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex
);
(1)阻塞线程
(2)将已经上锁的mutex解锁
(3)该函数解除阻塞,会对互斥锁加锁
5.1.4 限时等待一个条件变量
pthread_cond_timewait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime
);
参看 man sem_timedwait
函数,查看struct timespec
结构体
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
tv_sec绝对时间,填写的时候time(NULL)+600 ==>设置超时600s
5.1.5 唤醒至少一个阻塞在条件变量上的线程
pthread_cond_signal(pthread_cond_t *restrict cond);
5.1.6 唤醒全部阻塞在条件变量上的线程
pthread_cond_broadcast(pthread_cond_t *cond);
5.2 条件变量解决生产着消费者模式
用链表存储数据
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
int beginnum = 1000;
//初始化mutex
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
//初始化条件变量
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
typedef struct _ProdInfo{
int num;
struct _ProdInfo *next;
}ProdInfo;
ProdInfo *HEAD = NULL;
void *thr_producer(void *arg){
//负责向链表添加数据
while(1){
ProdInfo *prod=(ProdInfo *)malloc(sizeof(ProdInfo));
prod->num=beginnum++;
//上锁
pthread_mutex_lock(&mutex);
//add to list
prod->next=HEAD;
HEAD=prod;
printf("--------%s--------seld=%lu------------%d\n",__FUNCTION__,pthread_self(), prod->num);
pthread_mutex_unlock(&mutex);
//发起通知
pthread_cond_signal(&cond);
sleep(rand()%4);
}
return NULL;
}
void *thr_customer(void *arg){
ProdInfo *prod=NULL;
while(1){
//取链表的数据
pthread_mutex_lock(&mutex);
//判断有没有数据
if(HEAD==NULL){
//发送消息等待
pthread_cond_wait(&cond, &mutex);
}
//此时链表非空
prod=HEAD;
HEAD=HEAD->next;
printf("--------%s--------seld=%lu------------%d\n",__FUNCTION__,pthread_self(), prod->num);
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%4);
free(prod);
}
return NULL;
}
int main(){
pthread_t tid[2];
pthread_create(&tid[0], NULL, thr_producer, NULL);
pthread_create(&tid[1], NULL, thr_customer, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
运行结果:
一个生产者多个消费者时
把thr_customer
中的
if(HEAD==NULL)
改成
while(HEAD==NULL)
即可。
6. 信号量
加强版的互斥锁。适于多个资源多个线程访问的情况。
6.1 主要函数
sem_init
sem_destroy
sem_wait
sem_trywait
sem_timedwait
sem_post
以上六个函数的返回值都是:
成功返回0
,失败返回-1
,同时设置errno
。(注意,他们没有pthread前缀)
sem_t
类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem;
规定信号量sem
不能小于0,。头文件<semaphore.h>
6.1.1 初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem 定义的信号量,传出
pshared 非0则代表进程信号量, 0代表线程信号量
value 定义信号量的并发数(钥匙的个数)
6.1.2 摧毁
int sem_destroy(sem_t *sem);
摧毁一个信号量
6.1.3 申请信号量,申请成功,则value–
int sem_wait(sem_t *sem);
当信号量为0的时候,阻塞
6.1.4 释放信号量 value++
int sem_post(sem_t *sem);
6.2 信号量基本操作
sem_wait
:(类比pthread_mutex_lock
)
- 信号量大于
0
,对信号量--
- 信号量等于
0
,造成线程阻塞
sem_post
:将信号量++,
同时唤醒阻塞在信号量上的线程(类比pthread_mutex_unlock
)
但,由于sem_t
的实现对用户隐藏,所以所谓的++
,--
操作只能通过函数来实现,而不能直接++
,--
符号。
以上操作也成为PV操作
信号量的初值,决定了占用信号量的线程的个数。
6.3 信号量实现生产者消费者模式
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
//blank只有多少可以放生产者生产东西的地方,xfull是消费者可以消费的东西的数量
sem_t blank,xfull;
#define _SEM_CNT_ 5
// 模拟饼筐
int queue[_SEM_CNT_];
int beginnum = 100;
void *thr_producter(void *arg) {
int i = 0;
while(1){
//申请资源 blank--
//看看能不能生产,还有没有空间
sem_wait(&blank);
//打印函数名、线程名、数量
printf("-----%s-----self--%lu----num----%d\n",__FUNCTION__,pthread_self(),beginnum);
//生产数据
queue[(i++)%_SEM_CNT_] = beginnum++;
//xfull ++
sem_post(&xfull);
sleep(rand()%3);
}
return NULL;
}
void *thr_customer(void *arg) {
int i = 0;
int num = 0;
while(1){
//看看能不能消费
sem_wait(&xfull);
//通过取余来
num = queue[(i++)%_SEM_CNT_];
printf("-----%s-----self--%lu----num----%d\n",__FUNCTION__,pthread_self(),num);
//发送信号
sem_post(&blank);
sleep(rand()%3);
}
return NULL;
}
int main(){
//线程,所有第二个参数是0, 如果是进程,则第二个参数是非0
sem_init(&blank,0,_SEM_CNT_);
//消费者一开始的初始化默认没有产品
sem_init(&xfull,0,0);
pthread_t tid[2];
//线程没有设置属性,所有第二个参数为NULL
pthread_create(&tid[0],NULL,thr_producter,NULL);
pthread_create(&tid[1],NULL,thr_customer,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
sem_destroy(&blank);
sem_destroy(&xfull);
return 0;
}
运行结果:
6.4 信号量与互斥量的区别
No. | 区别 | 信号量 | 互斥量 |
---|---|---|---|
1 | 使用对象 | 线程和进程 | 线程 |
2 | 最值 | 非负整数 | 0或1 |
3 | 操作 | PV操作可由不同线程完成 | 加锁和解锁必须由同一线程使用 |
4 | 应用 | 用于线程的同步 | 用于线程的互斥 |
- 互斥:主要关注于资源访问的唯一性和排他性。
- 同步:主要关注于操作的顺序,同步以互斥为前提。
7. 文件锁
借助fcntl
函数来实现锁机制。操作文件的进程没有获得锁时,可以打开,但无法执行read、write操作。
适合环境——当前系统中该进程只能起一个。
实现原理——当一个进程打开了这个文件,另一个进程发现文件被打开了,就无法再打开这个文件了。
文件锁——读共享,写独占。
7.1 fcntl函数
int fcntl(int fd, int cmd, ... /* arg */ );
参1 文件描述符
参2
F_SETLK(struct flock *) 设置文件锁(trylock)
F_SETLKW(struct flock *) 设置文件锁(lock) W-->wait
F_GETTLK(struct flock *) 获取文件锁
参3
struct flock {
...
short l_type; 锁的类型:F_RDLCK、F_WRLCK、F_UNLCK
short l_whence; 偏移位置:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; 起始偏移:0
off_t l_len; 长度:0表示整个文件加锁
pid_t l_pid; 持有该所的进程ID:(F_GETLK only)
...
};
以上可以man fcntl
查看
7.2 文件锁举例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#define _FILE_NAME_ "/home/itheima/temp.lock"
int main() {
int fd = open(_FILE_NAME_,O_RDWR|O_CREAT,0666);
if(fd < 0){
//文件打开失败
perror("open err");
return -1;
}
struct flock lk;
lk.l_type = F_WRLCK;
lk.l_whence =SEEK_SET ;
lk.l_start = 0;
lk.l_len =0;
if(fcntl(fd,F_SETLK,&lk) < 0){
perror("get lock err");
exit(1);
}
// 核心逻辑
while(1){
printf("I am alive!\n");
sleep(1);
}
return 0;
}
开启另外一个终端