C++ 多线程 用条件变量确定线程的执行顺序而不是使用 sleep(1)

使用 sleep(1)

这几天学习多线程里的互斥锁和条件变量,想要实现一个功能,线程 A 对一个全局变量进行递增操作,当变量符合某个要求的时候,用线程 B 输出。

代码内容:线程 1 使用 add 方法对 point 进行递增到 100,point 可以被 5 整除时线程 B 对其进行输出。

实现方法1:

// compiler g++ 4.8
mutex mt;
condition_variable cv_1;
static int point = 0;
int status = 0;

void add(){							//线程1
    int times = 100;
    while(times--){
        unique_lock<mutex> lk(mt);
        while(status != 0){
            cv_1.wait(lk);
        }
        point++;
        if(point%5 == 0){
            lk.unlock();
            sleep(1);				//(1)
        }
    }
}

void print(){						//线程2
    int times = 100 / 5;
    while(times--){
        unique_lock<mutex> lk(mt);
        cout << "point: " << point << endl;
        status = 0;
        cv_1.notify_one();
        lk.unlock();
        sleep(1);					//(2)
    }
}

int main(){

    thread t1 = thread(add);
    thread t2 = thread(print);

    t1.join();
    t2.join();

    return 0;
}

代码的 (1) (2) 部分使用了sleep函数,这个sleep发挥了至关重要的作用。
(1) 的上一条语句解锁了互斥锁,此时线程 1 和线程 2 都在竞争这个锁,到底谁会动作快一点拿到锁呢?我们的目的是线程 2 拿到锁。但是实践表明线程 2 比较拉给,抢不过线程 1。不过在加上了sleep(1)之后,线程 2 就能成功拿到锁了,符合我们的要求。

个人思考

这个代码实现是没有问题的,我的问题在于使用sleep(1)这个函数。线程 1 会比线程 2 快多少?0.5s?0.1s?这些都是不确定的,而直接让线程休眠 1 秒,经过试验,OK 可以成功运行了,那就 1 秒。我觉得这是很“模糊”,很“浪费”的一个行为。1 秒可以运行多少代码了。。。
我看网上有挺多文章写条件变量的代码时都是直接用的sleep。应该用其他更好的方法实现才对,而不是因为sleep可以使代码运行符合要求就让线程每次都强迫休眠 1 秒。可以使用条件变量来控制线程的执行顺序。

使用条件变量确定

mutex mt;
condition_variable cv_1, cv_2;
static int point = 0;
int status = 0;

void add(){
    int times = 100;
    while(times--){
        unique_lock<mutex> lk(mt);
        while(status != 0){
            cv_1.wait(lk);
        }
        point++;
        if(point%5 == 0){
            status = 1;
            cv_1.notify_one();
        }								
    }									//(1)
}

void print(){
    int times = 100 / 5;
    while(times--){
        unique_lock<mutex> lk(mt);				//(2)
        while(status != 1){
            cv_1.wait(lk);						//(3)
        }
        cout << "point: " << point << endl;
        status = 0;
        cv_1.notify_one();
        
    }
}

int main(){

    thread t1 = thread(add);
    thread t2 = thread(print);

    t1.join();
    t2.join();

    return 0;
}

代码分析

使用 wait 方法可以使线程阻塞,释放互斥锁,这样就可以 100% 让其他线程拿到锁了。
在上面的代码中,当point符合要求时,线程 1 进行notify 。等到循环一次结束,运行到 (1) 时,unique_lock 析构会释放锁,但我们仍然不知道哪个线程先拿到锁。

此时有两种情况:
(1)如果是线程 1 unique_lock lk(mt)拿到锁,就会进入cv_1.wait(lk)的阻塞状态,释放锁。然后线程 2 就可以拿到锁运行了。
(2)线程 2 直接拿到锁运行,和 (1) 一样的结果。

线程 2 拿到锁时有两种情况:
(1)正阻塞在代码中 (2) 的位置,这时候因为线程 1 已经执行过cv_1.notify_one()了,所以线程 2 中的 while(status != 1) 肯定为否,会跳过cv_1.wait(lk)直接执行后面的代码。(符合要求)
(2)正阻塞在代码中 (3) 的位置,cv_1.wait(lk)在被线程 1 notify后就一直请求着锁,现在拿到了锁,可以执行cv_1.wait(lk)语句后面的代码了。和 (1) 一样的结果。

这里对这种情况加以解释
如果线程 1 notify 的时候线程 2 没有进入wait 阻塞状态怎么办?不就信号丢失了吗?
这正是while判断的魅力,如果 1 已经 notify 过了,说明 2 中的 while 判断里面的语句为假,2 此时运行就不会进入 waitwhile 循环了。会直接执行后面的代码。这和线程 2 处于wait 阻塞状态时收到线程 1 的通知是一样的。(同样的,也适用于线程 2 通知的时候线程 1 没有进入 wait阻塞状态的情况)。

while 判断的另一个作用是防止虚假唤醒。总之就是 wait 在被唤醒的时候数据不一定是符合要求的,所以要用 while 循环,直到数据符合条件才跳出。

改进
把 add 和 print 函数中的 unique_lock lk(mt) 放到函数体的第一句,这样在 while 循环过程中lk 就不会析构,就会一直占有着锁,直到运行 cv_1.wait(lk) 才会进入阻塞状态,释放锁。
这样线程的执行流程就很清楚了:在线程 1 满足条件 notify 线程 2 后,线程 2 第一时间并没有拿到锁,而是等到线程 1 阻塞之后才拿到锁,然后线程 2 执行代码后,notify 线程 1,线程 1 也是等到线程 2 阻塞之后才拿到锁,解除阻塞状态,继续运行。

void add(){
    unique_lock<mutex> lk(mt);
    int times = 100;
    while(times--){
        //unique_lock lk(mt); //本来在这
        while(status != 0){
            cv_1.wait(lk);
        }
        point++;
        if(point%5 == 0){
            status = 1;
            cv_1.notify_one();
        }								
    }									//(1)
}

反思

我在上面代码分析里的分几种情况讨论太过复杂,没有必要。其实只要关注哪个线程的条件变量符合要求,可以运行代码就行了。其他不符合要求的线程,不管是没有抢到锁阻塞在unique_lock lk(mt),还是抢到了阻塞在cv_1.wait(lk),他们都没有占有锁,从而让那些符合要求的线程可以运行,并且这些的阻塞在符合条件变量的要求后,都会执行相同的逻辑代码,是一样的。

要是代码的逻辑都能像上面改进后的代码的逻辑这样简单粗暴就好了。

总结

对于不确定锁的争夺的情况,可以用condition_variable.wait(mutex) 方法,使线程阻塞并且释放锁,这样就确保其他线程可以拿到锁了。要注意的是不同线程之间的“条件变量”的逻辑关系。

在分析类似的多线程使用条件变量确定执行顺序的代码时,不用太过于执着分析锁的竞争情况,把重点放在符合条件不会阻塞的线程上,然后根据条件变量的改变去判断接下来执行的线程。

有错请指正,感谢

你可能感兴趣的:(C++,多线程,c++,多线程)