Linux(十二) 线程同步

目录

为什么要线程同步

线程同步有哪几种方式?

条件变量

同步概念与竞态条件

条件变量函数

初始化

销毁

等待条件满足

唤醒等待

为什么pthread_ cond_ wait 需要互斥量?


为什么要线程同步

引入线程同步,主要是为了解决访问临界资源合理性的问题,多个线程可以同时访问共享资源。线程同步是为了保证多个线程之间对共享资源的访问顺序和结果的正确性。当多个线程同时访问共享资源时,如果没有进行适当的同步措施,可能会导致数据不一致、竞争条件和死锁等问题。

例如,规定抢完一张票后要重新排队,不要一直抢,而且要到队尾排队,锁被释放时,其他线程不要一起抢,要有规定的顺序使用锁

线程同步有哪几种方式?

互斥锁(Mutex):互斥锁保证在同一时刻只有一个线程可以访问共享资源,其他线程需要等待锁的释放才能继续执行。

信号量(Semaphore):信号量是一个计数器,用于控制对共享资源的访问数量。通过对信号量进行P(等待)和V(释放)操作,可以限制同时访问共享资源的线程数量。

条件变量(Condition):条件变量用于在多个线程之间进行通信和同步。一个线程可以通过条件变量等待某个条件的发生,并在其他线程满足条件时被唤醒继续执行。

读写锁(ReadWriteLock):读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这样可以提高读取操作的并发性能。

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

条件变量函数

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);// 唤醒全部线程
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒特定线程
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <string.h>
#define THREAD_NUM 4
typedef void (*func_t)(const std::string &name,pthread_mutex_t* pmtx,pthread_cond_t* pcon);
volatile bool quit = false;
class ThreadData
{
public:
    ThreadData(const std::string &name, func_t func,pthread_mutex_t* pmtx,pthread_cond_t* pcon)
        : name_(name), func_(func), pmtx_(pmtx), pcon_(pcon)
    {
    }

public:
    const std::string name_;
    func_t func_;
    pthread_mutex_t* pmtx_;
    pthread_cond_t* pcon_;
};

void func1(const std::string &name,pthread_mutex_t* pmtx,pthread_cond_t* pcon)
{
    while (!quit)
    {
        // wait一定要在加锁和解锁之间wait,因为我么是要在临界资源没就位时等,而检查临界资源是否就位
        // 之前要先访问临界资源,只有拥有锁才可以访问临界资源,所以wait要在加锁和解锁之间
        //pthread_mutex_lock(pmtx);
        // if(临界资源是否就绪)
        pthread_cond_wait(pcon,pmtx);// 默认在执行该代码时,wait代码在执行,当前进程会被立即阻塞
        while(1);
        std::cout << name << "running -a" << std::endl;
        pthread_mutex_unlock(pmtx);
        // std::cout.flush();
        // sleep(1);
    }
}
void func2(const std::string &name,pthread_mutex_t* pmtx,pthread_cond_t* pcon)
{
    while (!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcon,pmtx);
        std::cout << name << "running -b" << std::endl;
        pthread_mutex_unlock(pmtx);
        // std::cout.flush();

        // sleep(1);
    }
}
void func3(const std::string &name,pthread_mutex_t* pmtx,pthread_cond_t* pcon)
{
    while (!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcon,pmtx);
        std::cout << name << "running -c" << std::endl;
        pthread_mutex_unlock(pmtx);
        // std::cout.flush();

        // sleep(1);
    }
}
void func4(const std::string &name,pthread_mutex_t* pmtx,pthread_cond_t* pcon)
{
    while (!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcon,pmtx);
        std::cout << name << "running -d" << std::endl;
        pthread_mutex_unlock(pmtx);
        // std::cout.flush();

        // sleep(1);
    }
}
void *Entry(void *args)
{
    ThreadData *td = (ThreadData *)args;
    td->func_(td->name_,td->pmtx_,td->pcon_);
    delete td;
    return nullptr;
}

int main()
{
    pthread_mutex_t mtx;
    pthread_cond_t con;
    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&con,nullptr);
    pthread_t tids[THREAD_NUM];
    func_t func[THREAD_NUM] = {func1, func2, func3, func4};
    for (int i = 0; i < THREAD_NUM; i++)
    {
        std::string name("thread ");
        name += std::to_string(i + 1);

        ThreadData *td = new ThreadData(name, func[i],&mtx,&con);
        pthread_create(tids + i, nullptr, Entry, (void *)td);
    }
    sleep(1);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "thread running code cnt:" << cnt << std::endl;
        //pthread_cond_signal(&con);
        pthread_cond_broadcast(&con);
        sleep(1);
    }
    
    std::cout << "control over" << std::endl;
    quit = true;
    pthread_cond_broadcast(&con);

    for (int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(tids[i], nullptr);
        std::cout << "thread:" << tids[i] << "quit" << std::endl;
    }

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&con);
    return 0;
}

为什么pthread_ cond_ wait 需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。得先访问临界资源才知道条件满不满足,要访问临界资源就需要带锁
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
     

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

由于解锁和等待不是原子操作。调用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1(即解锁),直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样(加锁)。

条件变量—虚假唤醒(放到while循环的原因)

在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应成为”虚假唤醒。

所以通常的标准解决办法是这样的:
将条件的判断从if 改为while,举例子说明:
下面为线程处理函数:

static void *thread_func(void *arg)
{
    while (1) {
    pthread_mutex_lock(&mtx);           //这个mutex主要是用来保证pthread_cond_wait的并发性
    while (msg_list.empty())   {     //pthread_cond_wait里的线程可能会被意外唤醒(虚假唤醒),如果这个时候,则不是我们想要的情况。这个时候,应该让线程继续进入pthread_cond_wait
        pthread_cond_wait(&cond, &mtx);
    }
        msg = msg_list.pop();
        pthread_mutex_unlock(&mtx);             //临界区数据操作完毕,释放互斥锁
        // handle msg
    }
    return 0;
}

如果不存在虚假唤醒的情况,那么下面代码:

 while (msg_list.empty())   {
        pthread_cond_wait(&cond, &mtx);
    }

可以为

if (msg_list.empty())   {
        pthread_cond_wait(&cond, &mtx);
    }

但是存在虚假唤醒的时候,如果用if,而不用while,那么但被虚假唤醒的时候,不会再次while判断,而是继续下面执行msg = msg_list.pop();这其实是逻辑上有问题的。因为下面的代码已经假定了msg_list不是空的。

举例:
多核环境下,线程1、2都在thread_func函数中阻塞等待,某刻这两个线程被pthread_cond_signal同时唤醒并竞争互斥锁想要向下运行,线程1成功获取锁,并向下运行从消息队列取出消息,此刻线程2仍因为获取不到锁而阻塞,线程1取完消息后释放锁后,线程2继续向下运行,但此时消息队列中已没有数据,所以对于线程2来说,此次唤醒是虚假唤醒。需要在被唤醒后在次判断条件是否成立来避免这种情况

注:调用pthread_cond_broadcast会唤醒所有等待线程,也会出现虚拟唤醒情况,同多核处理器的情况一样,所以调用pthread_cond_broadcast的程序中也需要做虚拟唤醒处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值