Linux C++实现多线程同步的四种方式

https://www.bilibili.com/read/cv4323139

背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例:

#include 
#include
#include
#include
#include
#include
using namespace std;
int ticket_sum=20;
void *sell_ticket(void *arg)
{
    for(int i=0; i<20; i++)
    {
        if(ticket_sum>0)
        {
            sleep(1);
            cout<<"sell the "<<20-ticket_sum+1<<"th"<Linux C++实现多线程同步的四种方式_第1张图片

分析:总票数只有20张,却卖出了23张,是非常明显的超买超卖问题,而造成这个问题的根 本原因就是同时发生的各个线程都可以对ticket_sum进行读取和写入!

ps:

  1. 在并发情况下,指令执行的先后顺序由内核决定,同一个线程内部,指令按照先后顺序执 行,但不同线程之间的指令很难说清楚是哪一个先执行,如果运行的结果依赖于不同线程执行 的先后的话,那么就会形成竞争条件,在这样的情况下,计算的结果很难预知,所以应该尽量 避免竞争条件的形成
  2. 最常见的解决竞争条件的方法是将原先分离的两个指令构成一个不可分割的原子操作,而其 他任务不能插入到原子操作中!
  3. 对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内, 不允许其他线程访问该资源!
  4. 线程同步的常见方法:互斥锁,条件变量,读写锁,信号量

一.互斥锁

本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线 程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他 线程想要获得互斥锁,必须得到互斥锁再次被打开之后 采用互斥锁来同步资源:

#include 
#include
#include
#include
#include
#include
using namespace std;
int ticket_sum=20;
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;//static init mutex
void *sell_ticket(void *arg)
{
    for(int i=0; i<20; i++)
    {
        pthread_mutex_lock(&mutex_x);//atomic opreation through mutex lock
        if(ticket_sum>0)
        {
            sleep(1);
            cout<<"sell the "<<20-ticket_sum+1<<"th"<#include 
#include
#include
#include
#include
#include
#include
using namespace std;
int ticket_sum=20;
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;//static init mutex
void *sell_ticket_1(void *arg)
{
    for(int i=0; i<20; i++)
    {
        pthread_mutex_lock(&mutex_x);
        if(ticket_sum>0)
        {
            sleep(1);
            cout<<"thread_1 sell the "<<20-ticket_sum+1<<"th ticket"<0)
            {
                sleep(1);
                cout<<"thread_2 sell the "<<20-ticket_sum+1<<"th tickets"<#include 
#include
#include
#include
#include
#include
#include
using namespace std;
pthread_cond_t qready=PTHREAD_COND_INITIALIZER; //cond
pthread_mutex_t qlock=PTHREAD_MUTEX_INITIALIZER; //mutex
int x=10,y=20;
void *f1(void *arg)
{
    cout<<"f1 start"<

分析:线程1不满足条件被阻塞,然后线程2运行,改变了条件,线程2发行条件改变了通知线 程1运行,然线程1不满足条件被阻塞,然后线程2运行,改变了条件,线程2发行条件改变了 通知线程1运行,然后线程2结束,然后线程1继续运行,然后线程1结束,为了确保线程1先执 行,在创建线程2之前我们sleep了2秒

ps:

1.条件变量通过运行线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,常常和互 斥锁一起使用,使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开响应 的互斥锁并等待条件发生变化,一旦其他的某个线程改变了条件变量,它将通知响应的条件变 量换线一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并且重新测试条件 是否满足

1.条件变量的相关函数

1)创建

静态方式:pthread_cond_t cond PTHREAD_COND_INITIALIZER

动态方式:int pthread_cond_init(&cond,NULL)

Linux thread 实现的条件变量不支持属性,所以NULL(cond_attr参数)

2)注销

int pthread_cond_destory(&cond)

只有没有线程在该条件变量上,该条件变量才能注销,否则返回EBUSY

因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程!(请参 考条件变量的底层实现)

3)等待

条件等待:int pthread_cond_wait(&cond,&mutex)

计时等待:int pthread_cond_timewait(&cond,&mutex,time)

  1. 其中计时等待如果在给定时刻前条件没有被满足,则返回ETIMEOUT,结束等待
  2. 无论那种等待方式,都必须有一个互斥锁配合,以防止多个线程同时请求 pthread_cond_wait形成竞争条件!
  3. 在调用pthread_cond_wait前必须由本线程加锁

4)激发

激发一个等待线程:pthread_cond_signal(&cond)

激发所有等待线程:pthread_cond_broadcast(&cond)

重要的是,pthread_cond_signal不会存在惊群效应,也就是是它最多给一个等待线程发信 号,不会给所有线程发信号唤醒提他们,然后要求他们自己去争抢资源!

pthread_cond_signal会根据等待线程的优先级和等待时间来确定激发哪一个等待线程

下面看一个程序,找到程序存在的问题

#include 
#include
#include
#include
#include
#include
#include
using namespace std;
pthread_cond_t taxi_cond=PTHREAD_COND_INITIALIZER; //taix arrive cond
pthread_mutex_t taxi_mutex=PTHREAD_MUTEX_INITIALIZER;// sync mutex
void *traveler_arrive(void *name)
{
    cout<<"Traveler:"<<(char*)name<<" needs a taxi now!"<#include 
#include
#include
#include
#include
#include
#include
using namespace std;
pthread_cond_t taxi_cond=PTHREAD_COND_INITIALIZER; //taix arrive cond
pthread_mutex_t taxi_mutex=PTHREAD_MUTEX_INITIALIZER;// sync mutex
void *traveler_arrive(void *name)
{
    cout<<"Traveler:"<<(char*)name<<" needs a taxi now!"<#include 
#include
#include
#include
#include
#include
#include
using namespace std;
int num=5;
pthread_rwlock_t rwlock;
void *reader(void *arg)
{
    pthread_rwlock_rdlock(&rwlock);
    cout<<"reader "<<(long)arg<<" got the lock"<

分析:3个读线程,2个写线程,读线程比写线程多

当读写锁是写状态时,在锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞

当读写锁是读状态时,在锁被解锁之前,所有视图以读模式对它进行加锁的线程都可以得到访 问权,但是以写模式对它进行加锁的线程会被阻塞

所以读写锁默认是强读模式!

四.信号量

信号量(sem)和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程 进入临界区

1)信号量初始化

int sem_init(&sem,pshared,v)

pshared为0表示这个信号量是当前进程的局部信号量

pshared为1表示这个信号量可以在多个进程之间共享

v为信号量的初始值 成功返回0,失败返回-1

2)信号量值的加减

int sem_wait(&sem):以原子操作的方式将信号量的值减去1

int sem_post(&sem):以原子操作的方式将信号量的值加上1

3)对信号量进行清理

int sem_destory(&sem) 通过信号量模拟2个窗口,10个客人进行服务的过程

样例:

#include 
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int num=10;
sem_t sem;
void *get_service(void *cid)
{
    int id=*((int*)cid);
    if(sem_wait(&sem)==0)
    {
        sleep(5);
        cout<<"customer "<

分析:信号量的值代表空闲的服务窗口,每个窗口一次只能服务一个人,有空闲窗口,开始服 务前,信号量-1,服务完成后信号量+1

你可能感兴趣的:(Linux,线程与进程)