当前位置:网站首页>15.线程同步的几种方法
15.线程同步的几种方法
2022-06-24 20:28:00 【anieoo】
一、为什么需要线程同步
线程同步通常是出现在多线程环境下的问题,对于多个线程同时访问的共享内存中的变量,如果不进行保护,就会导致一些列数据出错问题。以下图为例:

假设线程A在第一次读取变量的值为10,每次写周期会将变量A加5,理论上当线程A完成其任务的时候,变量的值变为20,但是由于线程B是在两个写周期间读取的变量,结果为15,因此会导致数据出错。
二、互斥锁
互斥锁,是进行线程同步的一种方式。顾名思义,当线程A对共享内存进行访问的时候,对其进行上锁,在访问结束后解锁。在加锁期间,如果线程B想要申请访问共享内存资源的话,会被阻塞,直到线程A释放互斥锁。
1、互斥锁初始化
pthread_mutex_init()函数
初始化互斥锁,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
使用该函数需要包含头文件<pthread.h>。
mutex: 参数 mutex 是一个 pthread_mutex_t 类型指针, 指向需要进行初始化操作的互斥锁对象;
attr: 参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性
返回值: 成功返回 0;失败将返回一个非 0 的错误码调用函数 pthread_mutex_lock()可以对互斥锁加锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);而调用函数pthread_mutex_unlock()可以对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);当不需要使用互斥锁的时候需要对其进行销毁:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
不能销毁还没有解锁的互斥锁,否则将会出现错误;
没有初始化的互斥锁也不能销毁测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex; //定义自旋锁
static int count = 0;
static int loops;
static void *new_pthread(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
//pthread_mutex_lock(&mutex); //互斥锁上锁
l_count = count;
l_count++;
count = l_count;
//pthread_mutex_unlock(&mutex);//互斥锁解锁
}
return (void *)0;
}
int main(int argc, char *argv[])
{
pthread_t tid1, tid2; //定义两个线程
int ret;
loops = atoi(argv[1]); //保存循环次数
/* 初始化互斥锁 */
//pthread_mutex_init(&mutex, NULL);
//创建第一个线程
ret = pthread_create(&tid1, NULL, new_pthread, &loops);
if(ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
//创建第二个线程
ret = pthread_create(&tid2, NULL, new_pthread, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("count = %d\n", count);
pthread_mutex_destroy(&mutex);
exit(0);
}创建两个线程分别对变量count加上loops次数,使用互斥锁保护变量count,防止数据读取错误。运行结果如下:

如果把互斥锁注释点,可以发现运行结果如下,会导致数据不一致:

pthread_mutex_trylock()函数
在线程A持有互斥锁期间,如果线程B调用pthread_mutex_lock()函数获取互斥锁,会持续阻塞,直到线程A释放。
针对不想被阻塞的情况,Linux提供了pthread_mutex_trylock()函数,其函数原型如下:
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数 mutex 指向目标互斥锁,成功返回 0,失败返回一个非 0 值的错误码,如果目标互斥锁已经被其它
线程锁住,则调用失败返回 EBUSY。函数测试如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int count = 0;
static int loops;
static void *new_pthread(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁
l_count = count;
l_count++;
count = l_count;
pthread_mutex_unlock(&mutex);//互斥锁解锁
}
return (void *)0;
}
int main(int argc, char *argv[])
{
pthread_t tid1, tid2; //定义两个线程
int ret;
loops = atoi(argv[1]); //保存循环次数
/* 初始化互斥锁 */
//pthread_mutex_init(&mutex, NULL);
//创建第一个线程
ret = pthread_create(&tid1, NULL, new_pthread, &loops);
if(ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
//创建第二个线程
ret = pthread_create(&tid2, NULL, new_pthread, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("count = %d\n", count);
pthread_mutex_destroy(&mutex);
exit(0);
}运行结果如下,和使用 pthread_mutex_lock()效果一样:

三、条件变量
条件变量和互斥锁很相似,不过条件变量通常和互斥锁搭配使用。
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止。条件变量是和互斥锁一起搭配使用的。 使用条件变量主要包括两个动作:
① 一个线程等待某个条件满足而被阻塞;
②另一个线程中,条件满足时发出“信号”。
条件变量的初始化:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
参数 cond 指向 pthread_cond_t 条件变量对象
函数调用成功返回 0,失败将返回一个非 0 值的错误码通知条件变量:
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal()和 pthread_cond_broadcast()的区别在于:二者对阻塞于 pthread_cond_wait()
的多个线程对应的处理方式不同, pthread_cond_signal()函数至少能唤醒一个线程,而
pthread_cond_broadcast()函数则能唤醒所有线程。 等待条件变量:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
cond: 指向需要等待的条件变量,目标条件变量;
mutex: 参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;前面开头便给大家介绍
了,条件变量通常是和互斥锁一起使用。
返回值: 调用成功返回 0;失败将返回一个非 0 值的错误码测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
for ( ; ; ) {
pthread_mutex_lock(&mutex);//上锁
while (0 >= g_avail)
pthread_cond_wait(&cond, &mutex);//等待条件满足
while (0 < g_avail)
g_avail--; //消费
pthread_mutex_unlock(&mutex);//解锁
}
return (void *)0;
}
/* 主线程(生产者) */
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
/* 初始化互斥锁和条件变量 */
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
/* 创建新线程 */
ret = pthread_create(&tid, NULL, consumer_thread, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
for ( ; ; ) {
pthread_mutex_lock(&mutex);//上锁
g_avail++; //生产
pthread_mutex_unlock(&mutex);//解锁
pthread_cond_signal(&cond);//向条件变量发送信号
}
exit(0);
}全局变量 g_avail 作为主线程和新线程之间的共享资源,两个线程在访问它们之间首先会对互斥锁进行上锁,消费者线程中,当判断没有产品可被消费时(g_avail <= 0),调用pthread_cond_wait()使得线程陷入等待状态,等待条件变量,等待生产者制造产品;调用pthread_cond_wait()后线程阻塞并解锁互斥锁;而在生产者线程中,它的任务是生产产品(使用g_avail++来模拟),产品生产完成之后,调用pthread_mutex_unlock()将互斥锁解锁,并调用 pthread_cond_signal()向条件变量发送信号;这将会唤醒处于等待该条件变量的消费者线程,唤醒之后再次自动获取互斥锁,然后再对产品进行消费(g_avai--模拟)。
四、自旋锁
自旋锁的使用方法和互斥锁很相似。
自旋锁与互斥锁之间的区别:
实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠) ,直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁; 休眠与唤醒开销是很大的, 所以互斥锁的开销要远高于自旋锁、 自旋锁的效率远高于互斥锁; 但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
使用场景的区别: 自旋锁在用户态应用程序中使用的比较少, 通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占) , 一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁。
自旋锁初始化
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
参数 lock 指向了需要进行初始化或销毁的自旋锁对象
参数 pshared 表示自旋锁的进程共享属性,可以
取值如下:
PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
PTHREAD_PROCESS_PRIVATE: 私有自旋锁。只有本进程内的线程才能够使用该自旋锁自旋锁加锁和解锁
pthread_spin_lock()函数或 pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);五、信号量
信号量本质上是一个计数器,用于多进程对共享数据对象的读取,,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
信号量有关函数:
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> 创建信号量:
int semget(key_t key,int nsems,int flags);
(1)第一个参数key是长整型(唯一非零)。
(2)第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1。
(3)第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。一般我们会还或上一个文件权限删除和初始化信号量 :
int semctl(int semid, int semnum, int cmd, ...);
(1)sem_id是由semget返回的信号量标识符
(2)semnum当前信号量集的哪一个信号量
(3)cmd通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。
第四个参数一般设置为union semnu arg;定义如下:
union semun
{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET 使用的缓存区
unsigned short *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};改变信号量的值 :
int semop(int semid, struct sembuf *sops, size_t nops);
(1)nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
(2)sembuf的定义如下:
struct sembuf{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
}; 边栏推荐
- Rich text tables, lists, pictures
- [microservices sentinel] cluster link | microservices cluster environment construction
- Lenovo tongfuyao: 11 times the general trend, we attacked the city and pulled out the stronghold all the way
- adb shell getevent
- Scala object blending trait
- A website for programmers with a monthly salary of 30K
- Introduction to smart contract security audit delegatecall (2)
- Scala IO reads by lexical units and numbers
- Golang示例续期锁:Redis+Channel+sync.Mutex
- Which securities company should I choose to open an account online? Is it safe to open an account online?
猜你喜欢

JS Chapter 1 Summary

汇编语言(3)16位汇编基础框架与加减循环

Xcode preview displays a bug in the content of the list view and its solution

Using tcp/udp tools to debug the yeelight ribbon

4 years of working experience, and you can't tell the five communication modes between multithreads. Can you believe it?

The interview questions and answers for the high-frequency software test of Dachang help you prepare for the golden nine silver ten

Danish Technical University pioneered the application of quantum computing to power flow modeling of energy system

【Redis实现秒杀业务③】超卖问题之乐观锁具体实现

Heavyweight: the domestic ide was released, developed by Alibaba, and is completely open source! (high performance + high customization)

图书馆管理系统代码源码(php+css+js+mysql) 完整的代码源码
随机推荐
网上开户选哪个证券公司?网上开户安全么?
重磅:国产IDE发布,由阿里研发,完全开源!(高性能+高定制性)
卷积与反卷积关系超详细说明及推导(反卷积又称转置卷积、分数步长卷积)
2022 crane driver (limited to bridge crane) examination question bank simulated examination platform operation
生成订单30分钟未支付,则自动取消,该怎么实现?
VB 学习笔记
MySQL common basic statements (collation)
Text editor of QT project practice ---------- episode 11
Scala adapter pattern
Use of file class filenamefilter & filefilter in io
I'd like to ask how to open an account at industrial securities? Is it safe to open a stock account through the link
Mobile security tool apktool
扎克伯格上手演示四款VR头显原型机,Meta透露元宇宙「家底」
2022R1快开门式压力容器操作考题及答案
Add information on the left and add parts on the right of the status bar
丹麦技术大学首创将量子计算应用于能源系统潮流建模
Which securities company should I choose to open an account online? Is it safe to open an account online?
Golang示例续期锁:Redis+Channel+sync.Mutex
【实用系列】家内wifi全覆盖
Can communication experiment between C and C