多线程以及线程锁的学习笔记

C++ 多线程 Thread(C++11)和线程锁的知识


参考文章:
https://blog.csdn.net/ouyangfushu/article/details/80199140
https://zhuanlan.zhihu.com/p/91062516

1.普通函数多线程调用:

无参数函数

    #include <thread>
    #include <iostream>
    #include <mutex>
    
    void hello_world(){
        std::cout << "This is thread t1" << std::endl;
    }
    int main(){
        std::thread t1(hello_world);
        t1.join();
        std::cout << "This is Main" << std::endl;
    }
    // 可以看到在线程的构造函数中,引入子线程的函数名

带参数函数

    #include <thread>
    #include <iostream>
    #include <mutex>
    
    void hello_world(int a, int b){
        std::cout << "This is thread t1" << std::endl;
        std::cout << " a + b = " << a+b << std::endl;
    }
    int main(){
        std::thread t1(hello_world, a, b);
        t1.join();
        std::cout << "This is Main" << std::endl;
    }
    // 可以看到在线程的构造函数中,参数也是一并加入的

2.join() & detach() 的区别

join()的作用前面已经提到,主线程等待子线程结束方可执行下一步(串行),detach()是的子线程放飞自我,独立于主线程并发执行,主线程后续代码段无需等待。看看效果:

join( ):

在这里插入图片描述
在这里插入图片描述

join()会阻塞主线程,直到子线程的结束。

detach( )

在这里插入图片描述
在这里插入图片描述
可以看到detach()使得主线程并行运行,意味着会像图中这样,输出不对头。

这里需要特别说明一点,也是一开始我迷糊的地方:无论是join还是detach,线程开始的地方都是在线程的构造函数处,一旦生成了子线程的对象,子线程就已经开始了,join & detach只是决定了主线程是否并行运行而已

3.数据同步,线程锁

无锁的尴尬

线程锁的作用是防止出现多个线程同时处理同一个共享数据,比如下面的情况:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
void increase(int time) {
    for (int i = 0; i < time; i++) {
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}
int main(int argc, char** argv) {
    std::thread t1(increase, 10000);
    std::thread t2(increase, 10000);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

如果没有线程的相关概念,很容易理解为,increase两次,所以counter=20000,输出的结果是:

[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19997
[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19996

之所以出现这种情况是因为t1t2可能在某一个时刻同时处理了counter,比如:假定counter当前值为10,线程1读取到了10,线程2也读取到了10,分别执行自增操作,线程1和线程2分别将自增的结果写回counter,不管写入的顺序如何,counter都会是11,但是线程1和线程2分别执行了一次自增操作,我们期望的结果是12!!

加锁的情况

为了避免上述尴尬的情况,我们定义一个std::mutex对象用于保护counter变量。对于任意一个线程,如果想访问counter,首先要进行"加锁"操作,如果加锁成功,则进行counter的读写,读写操作完成后释放锁(重要!!!); 如果“加锁”不成功,则线程阻塞,直到加锁成功。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter
void increase(int time) {
    for (int i = 0; i < time; i++) {
        mtx.lock();
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        mtx.unlock();
    }
}
int main(int argc, char** argv) {
    std::thread t1(increase, 10000);
    std::thread t2(increase, 10000);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

这里再次说明:子线程并不是从join开始的,所以不会出现t1处理完了再处理t2的情况。也就是说当线程对象一建立,线程就开始了
简单总结一些std::mutex:

  1. 对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
  2. mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
  3. mtx.unlock():释放锁
  4. std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。

死锁了怎么解决

虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        mtx.lock();
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        mtx.unlock();
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

运行结果是:

[root@2d129aac5cc5 demo]# ./mutex_demo3_dead_lock
id:1, throw excption....

程序并没有退出,而是永远的“卡”在那里了,也就是发生了死锁。

那么这种情况该怎么避免呢? 这个时候就需要std::lock_guard登场了。std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(这就是所谓的RAII,读者可自行搜索)。

为了避免死锁,加入lock_guard:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        // std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
        // std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
        std::lock_guard<std::mutex> lk(mtx);
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}
int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

巧妙之处在于,当发生异常的时候,lock_guard会自动调用析构函数,而释放锁的操作就在析构函数中,就很好解决了发生异常后不会释放锁的问题。

未完待续

这就是多线程和线程锁的基本知识,还有很多关于锁的知识,以后慢慢补充

补充1

没有真正运用过锁,很多概念都还存在于理所当然之中,其实理解是有错误的。
看下面的一个例子,猜一猜会不会输出:counter:2?

int counter_1 = 0;
std::mutex mtx; // 保护counter
void increase_1(int time) {
    mtx.lock();
    counter_1++;
    //mtx.unlock();
}

void increase_2(int time) {
	// mtx.lock();
    counter_1++;
    std::cout << "counter:" << counter_1 << std::endl;
}
int main(int argc, char** argv) {
    std::thread t1(increase_1, 10000);
    std::thread t2(increase_2, 10000);
    t1.join();
    t2.join();

    return 0;

counter:2

Process finished with exit code 0

答案是不会,我错误的以为,当上锁之后,获得锁之后,被该线程使用的变量都不能再被其他线程获得。但其实不是,锁的作用更像是一个标志位,作用在线程之间,当一个线程获得锁A之后,当另外一个线程也想获得这把锁时候,发现获得不了于是一直阻塞着。因为上面线程t2中并没有用注释掉的那行,所以正常可以使用counter变量。当去掉注释之后,t2将一直停在mtx.lock();那行。


再看这样的一个例子:

int counter_1 = 0;
boost::recursive_mutex Mutex;

void increase_1(int time) {
    boost::recursive_mutex::scoped_lock scopedLock_test(Mutex);
    counter_1++;
}

void increase_2(int time) {
    boost::recursive_mutex::scoped_lock scopedLock_test(Mutex);
    counter_1++;
    std::cout << "counter:" << counter_1 << std::endl;
}
int main(int argc, char** argv) {
    std::thread t1(increase_1, 10000);
    std::thread t2(increase_2, 10000);
    t1.join();
    t2.join();
    return 0;
}
counter:2

Process finished with exit code 0

使用不同的锁机制,这里t2明明也上锁了,t1也没有解锁,但是还是获得了counter变量,这是因为这个boost::recursive_mutex::scoped_lock锁机制不同与前面例子的简单的std::mutexscoped_lock可以做到程序线程结束后自动释放锁。一般来说,越复杂的锁功能丰富但开销大。
如果上述increase_1()中加入while(1)则会发现始终无法输出counter :2


总结:
锁并不是对变量做了什么,而是在线程之间做了一个标志位:它说明了当其他线程使用某一把锁后,其他线程就只能乖乖等这把锁,线程的程序暂停直到这把锁被释放。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值