15.线程同步的几种方法
一、为什么需要线程同步
线程同步通常是出现在多线程环境下的问题,对于多个线程同时访问的共享内存中的变量,如果不进行保护,就会导致一些列数据出错问题。以下图为例:
假设线程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,使操作系统跟踪信号量, //并在进程没有释放该信号量而终止时,操作系统释放信号量
};
15.线程同步的几种方法相关推荐
- C#线程同步的几种方法
在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的.它只能在变量一级做 ...
- 归纳一下:C#线程同步的几种方法
我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程 ...
- unix c线程同步的三种方法:互斥量、读写锁以及条件变-xhb8413-ChinaUnix博客
unix c线程同步的三种方法:互斥量.读写锁以及条件变-xhb8413-ChinaUnix博客 unix c线程同步的三种方法:互斥量.读写锁以及条件变 2012-03-30 14:42:38 分类 ...
- linux:线程同步的5种方法
linux:线程同步的5种方法 一.为什么要使用线程: 二.线程同步的5种方法 2.1 互斥量 2.2 读写锁 2.3 条件变量 2.4 自旋锁 2.5 屏障 一.为什么要使用线程: <1> ...
- C++中线程同步的四种方法(Win32平台)
1.同步和互斥 互质是一种特殊的同步.线程同步一般指线程之间的执行存在某种程度上的相互依赖关系. 2.C++中线程同步的四种方法 (1)事件(Event); (2)信号量(semaphore); (3 ...
- 关于线程同步的几种方法
java允许多线程,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突. 比方说,我们在买火车票的时候,如何能确定余票数据准确而无误差,这个时候就需要 ...
- java线程同步的7种方法
为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有 ...
- java线程同步的五种方法
2019独角兽企业重金招聘Python工程师标准>>> 1.同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, ...
- 实现线程同步的几种方法
在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了.可能造成数据不一致.因此引入多线程同步,也就是说多个 ...
最新文章
- 常见的表死锁情况及解决方法
- MySQL5.6多实例部署
- matlab 着色算法,colorization_matlab着色 - 源码下载|图形图象|图形图像处理(光照,映射..)|源代码 - 源码中国...
- LIVE555再学习 -- OpenRTSP 源码分析
- Appcelerator Titanium 3.x Win7 64位平台安装步骤
- 如何快速入手一个JavaWeb项目
- android中弹出窗口,如何在Android中创建弹出窗口(PopupWindow)
- 蜂鸣器音乐代码 天空之城_潮玩 | 艺术展览,乐队live现场,网红小黑泥,贩卖“美好”的市集……一场未来公共生活,天空之城和你一起探索!...
- 详解3D物体检测模型: Voxel Transformer for 3D Object Detection
- php管理员权限表,权限表的建立
- ERP开发分享 1 数据库表设计
- 优秀的产品管理促进了IBM的成功转型--和谐生产方式百题03
- python输入三个数形成各种三角形
- python能开发微信公众号吗_用python如何开发微信公共帐号?
- QT美化使用字体图标
- 我要吐槽各大自媒体平台的权重问题
- ORA-3136错误分析——WARNING Inbound Connection Timed Out
- .NetCore之AutoMapper进阶篇
- 统一身份认证,企业实现统一身份认证有什么好处?
- UI自动化测试经验之谈(一)