一、为什么需要线程同步

线程同步通常是出现在多线程环境下的问题,对于多个线程同时访问的共享内存中的变量,如果不进行保护,就会导致一些列数据出错问题。以下图为例:

假设线程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.线程同步的几种方法相关推荐

  1. C#线程同步的几种方法

    在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的.它只能在变量一级做 ...

  2. 归纳一下:C#线程同步的几种方法

    我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程 ...

  3. unix c线程同步的三种方法:互斥量、读写锁以及条件变-xhb8413-ChinaUnix博客

    unix c线程同步的三种方法:互斥量.读写锁以及条件变-xhb8413-ChinaUnix博客 unix c线程同步的三种方法:互斥量.读写锁以及条件变 2012-03-30 14:42:38 分类 ...

  4. linux:线程同步的5种方法

    linux:线程同步的5种方法 一.为什么要使用线程: 二.线程同步的5种方法 2.1 互斥量 2.2 读写锁 2.3 条件变量 2.4 自旋锁 2.5 屏障 一.为什么要使用线程: <1> ...

  5. C++中线程同步的四种方法(Win32平台)

    1.同步和互斥 互质是一种特殊的同步.线程同步一般指线程之间的执行存在某种程度上的相互依赖关系. 2.C++中线程同步的四种方法 (1)事件(Event); (2)信号量(semaphore); (3 ...

  6. 关于线程同步的几种方法

    java允许多线程,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突. 比方说,我们在买火车票的时候,如何能确定余票数据准确而无误差,这个时候就需要 ...

  7. java线程同步的7种方法

    为何要使用同步?      java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),      将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有 ...

  8. java线程同步的五种方法

    2019独角兽企业重金招聘Python工程师标准>>> 1.同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, ...

  9. 实现线程同步的几种方法

    在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了.可能造成数据不一致.因此引入多线程同步,也就是说多个 ...

最新文章

  1. 常见的表死锁情况及解决方法
  2. MySQL5.6多实例部署
  3. matlab 着色算法,colorization_matlab着色 - 源码下载|图形图象|图形图像处理(光照,映射..)|源代码 - 源码中国...
  4. LIVE555再学习 -- OpenRTSP 源码分析
  5. Appcelerator Titanium 3.x Win7 64位平台安装步骤
  6. 如何快速入手一个JavaWeb项目
  7. android中弹出窗口,如何在Android中创建弹出窗口(PopupWindow)
  8. 蜂鸣器音乐代码 天空之城_潮玩 | 艺术展览,乐队live现场,网红小黑泥,贩卖“美好”的市集……一场未来公共生活,天空之城和你一起探索!...
  9. 详解3D物体检测模型: Voxel Transformer for 3D Object Detection
  10. php管理员权限表,权限表的建立
  11. ERP开发分享 1 数据库表设计
  12. 优秀的产品管理促进了IBM的成功转型--和谐生产方式百题03
  13. python输入三个数形成各种三角形
  14. python能开发微信公众号吗_用python如何开发微信公共帐号?
  15. QT美化使用字体图标
  16. 我要吐槽各大自媒体平台的权重问题
  17. ORA-3136错误分析——WARNING Inbound Connection Timed Out
  18. .NetCore之AutoMapper进阶篇
  19. 统一身份认证,企业实现统一身份认证有什么好处?
  20. UI自动化测试经验之谈(一)

热门文章

  1. 背景图片的精灵图的使用
  2. NBD Network Block Device
  3. 【论文】医疗大数据方面的资料
  4. docker——Ubuntu镜像操作和apache web容器操作小实训
  5. 【Nios II】以SOPC开发流程完成流水灯以及串口输出实验
  6. 如何判断过拟合和欠拟合,解决手段有哪些?
  7. 机柜风扇 的组成及如何正确安装 机柜散热风扇
  8. 【技术】基于angularJS的前端自动化测试工具Protractor快速入门
  9. [转]一个IT人才的精彩故事
  10. 3ds max 2014 启动出现 error while registering plugins 怎么修复