10分钟搞定Linux多线程同步(互斥量,死锁、读写锁,条件变量,信号量,文件锁)

在这里插入图片描述

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 多线程出现数据混乱(数据竞争)的原因

  1. 资源共享(独享资源则不会)
  2. 调度随机(意味着数据访问会出现竞争者)
  3. 线程间缺乏必要的同步机制

  以上三点中前两点不能改变,想提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
  所以只能从第三点着手解决。使多个线程在访问共享资源的时候出现互斥。

2. 互斥量mutex

  1. Linux提供一把互斥锁mutex(也称之为互斥量)
  2. 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束后解锁。
  3. 资源还是共享的,线程间也还是竞争的,但通过锁将资源的访问变为互斥操作,而后与时间有关的错误也不会在产生了。

在这里插入图片描述

但是应该注意:同一个时刻,只能有一个线程持有该锁

  当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

  所以,互斥锁实质上是是操作系统提供的一把“建议锁”(又称“协同所”),建议程序中有多线程访问共享资源的时候使用该机制,但是,并没有强制限定。

2.1 mutex相关的函数和使用步骤

pthread_mutex_t 类型,其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex:变量mutex只有两种取值01

pthread_mutex_init		初始化	
pthread_mutex_destroy	摧毁	
pthread_mutex_lock		加锁
pthread_mutex_unlock	解锁

互斥量的使用步骤:

  1. 初始化
  2. 加锁
  3. 执行逻辑——操作共享数据
  4. 解锁

注意事项
加锁需要最小粒度,不要一直占用临界区(加锁到解锁之间的执行逻辑即为临界区)

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. 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
  2. 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功,如果线程以写模式加锁会阻塞。

场景例子:
(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 条件变量解决生产着消费者模式

用链表存储数据
两个线程,一个线程想把所有的内容都写成0,另一个线程想把所有的内容都写成1

#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

  1. 信号量大于0,对信号量--
  2. 信号量等于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_SETSEEK_CURSEEK_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;
}

在这里插入图片描述
开启另外一个终端
在这里插入图片描述

  • 8
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值