线程同步
- 1.线程同步的概念
- 2.锁
-
- 2.1互斥锁(互斥量)
- 2.2 死锁
- 2.3 读写锁
- 2.4 条件变量
- 2.5 信号量
- 2.6 自旋锁
- 3.原子操作:
1.线程同步的概念
所谓线程同步,就是多个线程同时访问同一资源,多个线程协同步调,先后处理某件事情。那么如何实现线程同步呢?就是利用"锁"来实现的。下图为实现线程同步的基本操作:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL41EROJzYq1kMNpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0czN2UzMyAjMwITNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
如上图,线程在访问数据前必须加锁,加锁的目的是为了对线程进行阻塞,若锁已经被另外的线程锁上了,那么当前线程进入阻塞态,直到锁被打开,解除阻塞。若没有上锁,当前进行直接加锁。
上图中,资源被线程1直接上锁,那么线程2就进入阻塞状态。直到线程1解锁,然后线程2解除阻塞,加锁。如此往复。
2.锁
综上所述,我们要使用线程同步,就必须使用锁,那么锁怎么加又是怎么打开呢?锁的种类呢?下面将一一介绍:
2.1互斥锁(互斥量)
互斥锁:pthread_mutex_t mutext;
互斥锁提供一个可以在同一时间,只让一个线程访问临界资源的的操作接口。互斥锁让多个线程访问共享数据的时候是串行的。互斥锁的值,要么是0要么是1,1表示解锁,0表示加锁
互斥锁的使用步骤:
- 创建互斥锁:pthread_mutex_t mutex;
- 初始化这把锁:pthread_mutex_init(&mutex,NULL)
-
如何对资源加锁:在操作共享资源的代码之前加锁,之后解锁
使用示例:
pthread_mutex_lock(&mutex);
//共享资源代码//
pthread_mutex_unlock(&mutex);
互斥锁的相关函数
<1> 初始化互斥锁
pthread_mutex_init(
pthread_mutex_t *restrict mutex, //用其他的指针指向此地址是不管用的
const pthread_mutexattr_t *restrict attr//一般为NULL
);
<2> 销毁互斥锁
pthread_mutex_destroy(pthread_mutex_t *mutex);
<3> 加锁
pthread_mutex_lock(pthread_mutex_t *mutex);
-参数:mutex:
没有被上锁:当前线程会将这把锁锁上
被锁上了:当前线程阻塞
锁被打开之后:线程解除阻塞
<4>尝试加锁, 失败返回, 不阻塞
pthread_mutex_trylock(pthread_mutex_t *mutex);
- 没有锁上:当前线程会给这把锁加锁
- 如果锁上了:不会阻塞,返回
-
使用示例:
if(pthread_mutex_trylock(&mutex)==0)
{
// 则说明尝试加锁,并且成功了
// 接下来进行访问共享资源操作
}
else
{
//表示 加锁失败
//则进行错误处理
// 或者 等一会,再次尝试加锁
}
<6> 解锁
pthread_mutex_unlock(pthread_mutex_t *mutex);
2.2 死锁
死锁不是一种锁,而是一种现象。
造成死锁的原因1:自己锁自己,如下代码:
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
//资源代码块
pthread_mutex_unlock(&mutex);
显然上面代码使用同意吧锁,锁了两次,但是只解锁了一次。说明该资源依旧是被锁的。这是由于不规范的编程,忘了加锁后进行解锁。因此造成了死锁。
解决方法:加锁后一定要解锁
** 造成死锁的原因2:两个线程都被阻塞了**
如图:
上图所示:
首先:
线程1对共享资源A进行加锁:A锁
线程2对共享资源B进行加锁:B锁
接着:
线程1访问共享资源B,对B加锁。此时由于线程2对B加锁了,因此线程1被阻塞在B锁上,加锁失败。
线程2访问共享资源A,对A加锁。此时由于线程1对A加锁了,因此线程2被阻塞在A锁上,加锁失败。
这样就导致两个线程都被分别阻塞在AB锁上了。那么如何解决这件事呢?
- 方法1:让线程按照一定的顺序去访问共享资源。
- 方法2:在访问其他锁的时候,需要先将自己的锁打开。
2.3 读写锁
读写锁:pthread_rwlock_t lock
读写锁将对资源代码的访问分为读写两种模式,这大大的提高了并发效率。读写锁相较于互斥锁具有更好的性能,因为假如以读模式加锁后,当有多个线程对资源以读模式加锁时,并不会造成这些线程阻塞在等待解锁。
读写锁只有一把锁,不过是将资源的分文分为了读写两种模式。
读写锁类型:
- 读锁:对内存读操作
- 写锁:对内存写操作
读写锁特性
<1>线程A加读锁成功,此时又来了三个线程想要进行读操作,那么这三个线程可以加锁成功
- 读共享—读是并行处理的
<2> 线程A加写锁成功,此时又来了三个线程想要进行读操作,那么这三个线程被阻塞
- 写独占
<3> 线程A加读锁成功,此时又来了线程B想要进行写操作,那么线程B被阻塞。此时又来了线程C想要进行读操作,那么线程C被阻塞
- 读写不能同时进行
- 写的优先级更高
根据读写锁特性的一些场景练习
<1> 线程A持有写锁,线程B请求加读锁
- 线程B被阻塞:写锁优先级更高
<2> 线程A持有读锁,线程B请求加写锁
- 线程B阻塞:读写不能同时进行
<3> 线程A拥有读锁,线程B请求读锁
- 线程B加锁成功:读共享
<4> 线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
- 线程B被阻塞,线程C被阻塞:读写不能同时进行,写的优先级高
- 若线程A解锁,线程B加写锁成功,C继续阻塞
- B解锁,C加读锁成功
<5> 线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁
- 线程B被阻塞,线程C被阻塞
- 若线程A解锁,线程C加写锁成功,B继续阻塞:写的优先级更高
- C解锁,B加读锁成功
读写锁的使用场景
- 互斥锁的使用场景为:读写串行时
-
读写锁:
由读写锁的读写特性,我们知道,读是并行的,写是串行的。那么读写锁的使用场景应该是程序中的读操作的数量大于写操作的数量时。
读写锁的相关函数
<1> 初始化读写锁
pthread_rwlock_init(
pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr
);
<2> 销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
<3>加读锁
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- 上一次加写锁,但是还没解锁,则阻塞
<4> 尝试加读锁
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
-
返回值:
加锁成功:0
失败:错误号
<5> 加写锁
pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
- 上一次加写锁,但是还没有解锁,则阻塞
- 上一次加读锁,但是还没有解锁,则阻塞
<6>尝试加写锁
pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
<7>解锁
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
读写锁使用示例:
对同一资源,使用5次读线程,3次写线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//定义全局变量共享资源
int num = 0;
//定义读写锁
pthread_rwlock_t lock;
//read_thread函数的实现
void* read_thread(void *arg)
{
while(1)
{
//加读锁
pthread_rwlock_rdlock(&lock);
printf("read number = %d\n",num);
//解读锁
pthread_rwlock_unlock(&lock);
usleep(500);
}
return NULL;
}
//write_thread函数的实现
void *write_thread(void *arg)
{
while(1)
{
//加写锁
pthread_rwlock_wrlock(&lock);
num++;
printf("wrirte number = %d\n",num);
//解锁
pthread_rwlock_unlock(&lock);
usleep(500);
}
return NULL;
}
int main(void)
{
//创建八个子线程
pthread_t thread[8];
//初始化读写锁
pthread_rwlock_init(&lock,NULL);
//创建5个读线程
for(int i = 0;i < 5;++i)
{
int ret = pthread_create(&thread[i],NULL,read_thread,NULL);
if(ret != 0)
{
printf("第 %d 个线程创建失败\n",i);
printf("error:%s\n",strerror(ret));
}
}
//创建3个写线程
for(int i = 5;i < 8;++i)
{
int ret = pthread_create(&thread[i],NULL,write_thread,NULL);
if(ret != 0)
{
printf("第%d个线程创建失败\n",i);
printf("error:%s\n",strerror(ret));
}
}
//线程回收
for(int i = 0;i < 8;++i)
{
pthread_join(thread[i],NULL);
}
//读写锁的销毁
pthread_rwlock_destroy(&lock);
return 0;
}
运行结果:
显然,数据是按照升序逐个增加的
2.4 条件变量
条件变量:pthread_cond_t cond;
条件变量不是锁,但是能是够实现阻塞线程的功能。条件变量一般与互斥锁联合使用。当条件不满足时,条件变量阻塞线程;当条件满足时,会通知相应被阻塞的一个或多个线程开始工作。
一个例子:生产者-消费者模型
所谓生产者-消费者模型,就是生产者生产"数据",消费者消费“数据”。如生产者线程用于给链表中插入结点,消费者线程打印尾结点数据,并删除尾结点。
若我们直接使用互斥锁,那么生产者和多个消费者之间,及消费者之间都会去“抢”锁。但是,若生产者没有抢到,那么就没有“数据"让消费者消费,那么消费者之间“抢”锁就是做无用功,因为就算他们抢到了,也没有进行消费。这就造成了资源浪费。
但是我们使用了条件变量,我们就可以使用条件变量让消费者线程阻塞,让生产者通知被阻塞的线程开始工作。这样的话,就不会出现上面资源浪费的情况。
条件变量相关函数:
<1> 初始化一个条件变量
pthread_cond_init(
pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
<2> 销毁一个条件变量
pthread_cond_destroy(pthread_cond_t *cond);
<3> 阻塞等待一个条件变量
pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
-
函数作用:阻塞线程
若该函数阻塞线程,则将已经上锁的mutex解锁
若该函数解除阻塞,会对互斥锁加锁
<4> 限时等待一个条件变量-----------阻塞一定的时长
pthread_cond_timedwait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime//阻塞时长);
<5> 唤醒至少一个阻塞在条件变量上的线程
pthread_cond_signal(pthread_cond_t *cond);
<6> 唤醒全部阻塞在条件变量上的线程
pthread_cond_broadcast(pthread_cond_t *cond);
生产者-消费者模型实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//创建互斥锁
pthread_mutex_t mutex;
//创建条件变量
pthread_cond_t cond;
//定义生产出的结点
typedef struct node
{
int data;
struct node *next;
}NODE;
//定义指向链表头的头指针
NODE *header = NULL;
//生产者线程
void *producer(void *arg)
{
while(1)
{
//创建节点
NODE *node = (NODE*)malloc(sizeof(NODE));
//设置节点数据
node->data = (rand() % 1000);//0-1000的随机数
//加锁
pthread_mutex_lock(&mutex);
//使用头插法将结点插入链表
node ->next = header;
header = node;
//打印生产出来的结点数据
printf("producer data = %d\n",node ->data);
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者锁的状态为解除阻塞,
pthread_cond_signal(&cond);//若生产者没有生产结点,说明这句话执行并没有执行,那么消费者锁的状态为阻塞
sleep(rand()%3);//睡1-3s
}
return NULL;
}
//消费者线程
void *customer(void *arg)
{
while(1)
{
//删除结点
if(header == NULL)//首先判断结点是否为空
{
//若为空则执行以下操作
//等待阻塞
pthread_cond_wait(&cond,&mutex);//若结点为空,说明生产者那边没有解除阻塞状态,此时该函数阻塞线程。互斥锁为解锁状态。直到生产者那么有了数据,那么就会解除阻塞,那么互斥锁就会上锁,保证其他消费者不会访问到下面代码数据
}
//结点非空
//头删法删除结点
//定义代删除结点指针
NODE *del_node = header;
//重建结点关系
header = del_node ->next;
//打印删除结点的数据
printf("customer data = %d\n",del_node ->data);
//释放已删除结点的资源
free(del_node);
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);//睡1-3s
}
return NULL;
}
int main(void)
{
//创建两个子线程
pthread_t thread_producer;
pthread_t thread_cstm;
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//初始化条件变量
pthread_cond_init(&cond,NULL);
//创建生产者线程
pthread_create(&thread_producer,NULL,producer,NULL);
//创建消费者线程
pthread_create(&thread_cstm,NULL,customer,NULL);
//线程资源回收
pthread_join(thread_producer,NULL);
pthread_join(thread_cstm,NULL);
//销毁互斥锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
2.5 信号量
信号量:sem_t sem
信号量就是加强版的互斥锁,使用互斥锁对多线程加锁,使同一时间只允许一个线程对资源进行访问,具有唯一性和排他性。但是互斥锁无法限制对资源的访问顺序,但是使用信号量,它通过两个原子操作来访问资源,实现了对资源的访问是有序的。(有关原子操作,下一节介绍)
互斥锁的值只能为0或1,但是信号量可以为负数,0表示解锁,非零加锁。
互斥锁的加锁和解锁只能由同一个线程中,但是信号量可以由一个线程加锁另一个线程解锁
信号量的相关函数
<1> 初始化信号量:sem_init(sem_t *sem, int pshared, unsigned int value);
-
pshared
0 - 线程同步
1 - 进程同步
-
value
最多有几个线程操作共享数据
<2> 销毁信号量:sem_destroy(sem_t *sem);
<3> 加锁 sem–:对sem–会加锁
sem_wait(sem_t *sem);
- 调用一次相当于对sem做了–操作
- 如果sem值为0, 线程会阻塞
<4>尝试加锁 :sem_trywait(sem_t *sem);
- sem == 0, 加锁失败, 不阻塞, 直接返回
<5> 限时尝试加锁:sem_timedwait(sem_t *sem, xxxxx);
<6> 解锁 sem++:对sem++会解锁
sem_post(sem_t *sem);
- 对sem做了++操作
使用信号量实现生产者-消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
//定义链表结点
typedef struct node
{
int data;
struct node * next;
}NODE;
//定义指向链表头的指针
NODE *header = NULL;
//定义信号灯
sem_t sem_producer;
sem_t sem_custmoer;
void *producer_thread(void*arg)
{
//生产者生产链表结点
while(1)
{
//使用信号量加锁,使sen--
sem_wait(&sem_producer);//只有sem不为0时,线程才会解除阻塞,解除阻塞后又对sem--,sem加锁,保证没有数据被产生时,消费者不能消费数据。只有一下代码执行后,产生了数据,同时sem解锁了,消费者才能消费资源。
//给新建的结点开辟空间
NODE *node = (NODE*)malloc(sizeof(NODE));
//给结点数据赋值
node->data = rand()%1000;//1000以内的随机整数
//使用头插法将结点插入链表
node->next = header;
header = node;
//打印结点数据
printf("product data = %d\n",node ->data);
//解锁,使其sem++
sem_post(&sem_custmoer);
sleep(rand()%3);
}
return NULL;
}
void *customer_thread(void*arg)
{
while(1)
{
//使用信号量进行消费者判断,判断结果将决定该函数是否阻塞
sem_wait(&sem_custmoer);//只有sem不为0时,线程才会解除阻塞,解除阻塞后又对sem--,加锁,使其他消费者线程不能访问下面资源
//解除阻塞后的操作
//删除头结点数据
//首先保存头结点指针
NODE *del_node = header;
//重建结点关系
header = del_node->next;
//打印删除结点的数据
printf("custome data = %d\n",del_node ->data);
//释放删除结点的内存
free(del_node);
//解锁使其sem++
sem_post(&sem_producer);
sleep(rand()%3);
}
return NULL;
}
int main(void)
{
//创建两个线程
pthread_ t thread_p;
pthread_t thread_c;
//初始化信号量
sem_init(&sem_producer,0,4);
sem_init(&sem_custmoer,0,0);
//chuangjianxiancheng
pthread_create(&thread_p,NULL,producer_thread,NULL);//生产者线程
pthread_create(&thread_c,NULL,customer_thread,NULL);//消费者线程
//线程资源的回收
pthread_join(thread_p,NULL);
pthread_join(thread_c,NULL);
//销毁信号量
sem_destroy(&sem_producer);
sem_destroy(&sem_custmoer);
return 0;
}
2.6 自旋锁
参考:https://www.cnblogs.com/cxuanBlog/p/11679883.html
3.原子操作:
cpu处理一个命令时,线程/进程在处理完这个指令之前不会丢失cpu的。如下:
非原子操作:
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 创建一个链表的节点
Node* newNode = (Node*)malloc(sizeof(Node));
// init
newNode->data = rand() % 1000;
//****************************************//
newNode->next = head;
head = newNode;
printf("+++ producer: %d\n", head->data);
//****************************************//
sleep(rand()%3);
}
return NULL;
}
上面这段代码,中间被括起来的代码,由于head被放于全局区,那么执行这段代码时,可能会失去cpu,那么结果就会出错。
我们可以使用锁来模拟原子操作
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 创建一个链表的节点
Node* newNode = (Node*)malloc(sizeof(Node));
// init
newNode->data = rand() % 1000;
pthread_mutex_lock(&mutex);
newNode->next = head;
head = newNode;
printf("+++ producer: %d\n", head->data);
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
return NULL;
}
使用互斥锁,不可能让线程失去cpu的。故结果是对的。