IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> VINS-Mono中的多线程锁 -> 正文阅读

[C++知识库]VINS-Mono中的多线程锁

0.参考资料

std::lock_guard 引起的思考 : 强烈推荐看,写的非常好。

vins-mono使用的多线程代码解读:对VINS中使用的线程锁都进行了介绍,但是写的感觉不是特别明白

C++11 std::unique_lock与std::lock_guard区别及多线程应用实例 :这个讲解了std::unique_lockstd::lock_guard的区别,其实他俩基本功能是一样的。

1.std::lock_guard实现自动加锁

1.1.std::lock_guard实现机理

简单来说,在需要使用线程锁保证多线程数据访问的安全性的时候,如果使用手动加锁,那么经常会出现忘记释放锁而导致线程死锁的现象。

std::lock_guard是C++的一个标准模板类,见如下自己写的类似的类,其模板参数就是传入的std::mutex类,然后引用传入的实参需要是一个具体的mutex对象,这样声明一个std::lock_guard局部对象后,就可以在其生命周期内利用构造函数和析构函数进行自动加锁和解锁。

namespace myspace {
    template<typename T> class my_lock_guard {
    public:
        // 在 std::mutex 的定义中,下面两个函数被删除了
        // mutex(const mutex&) = delete;
        // mutex& operator=(const mutex&) = delete;
        // 因此这里必须传递引用
        my_lock_guard(T& mutex) :mutex_(mutex){
            // 构造加锁
            mutex_.lock();
        }

        ~my_lock_guard() {
            // 析构解锁
            mutex_.unlock();
        }
    private:
        // 不可赋值,不可拷贝
        my_lock_guard(my_lock_guard const&);
        my_lock_guard& operator=(my_lock_guard const&);
    private:
        T& mutex_;
    };
};

1.2.工程测试

  • main.cpp
#include <iostream>
#include <mutex>
#include <thread>

// 两个子线程共享的全局变量
int kData = 0;

// std::mutex 提供了一种防止共享数据被多个线程并发访问的简单同步方法
// 调用线程可以通过 lock 和 try_lock 来获取互斥量,使用 unlock() 释放互斥量
std::mutex kMutex;

namespace myspace {
    template<typename T> class my_lock_guard {
    public:
        // 在 std::mutex 的定义中,下面两个函数被删除了
        // mutex(const mutex&) = delete;
        // mutex& operator=(const mutex&) = delete;
        // 因此这里必须传递引用
        my_lock_guard(T& mutex) :mutex_(mutex){
            // 构造加锁
            mutex_.lock();
        }
        ~my_lock_guard() {
            // 析构解锁
            mutex_.unlock();
        }
    private:
        // 不可赋值,不可拷贝
        my_lock_guard(my_lock_guard const&);
        my_lock_guard& operator=(my_lock_guard const&);
    private:
        T& mutex_;
    };
};

void increment() {
    // 1.创建一个互斥量的包装类,用来自动管理互斥量的获取和释放
    // std::lock_guard<std::mutex> lock(kMutex);
    // 2.原生加锁
    // kMutex.lock();
    // 3.自己实现的 std::mutex 的包装类
    // myspace::my_lock_guard<std::mutex> lock(kMutex);
    for (int i = 0; i < 10; i++) {
        // 打印当前线程的 id : kData
        std::cout << std::this_thread::get_id() 
                  << ":" << kData++ << std::endl;
    }
    // 2. 原生解锁  
    //kMutex.unlock(); 
    // 离开局部作用域,局部锁解锁,释放互斥量
}
int main()
{
    // 打印当前函数名
    // std::cout << __FUNCTION__ << ":" << kData << std::endl;
    // 开启两个线程
    std::thread t1(increment);
    std::thread t2(increment);
    // 主线程等待这两个线程完成操作之后再退出
    t1.join();
    t2.join();
    // 防止立刻退出
    getchar();
    return 0;
}
  • CMakeLists.txt:这里需要注意的是由于thread使用了pthread库,所以之类必须手动链接到这个库上,否则报错。
cmake_minimum_required(VERSION 2.8.3)
project(mutex_test)
add_executable(mutex_test main.cpp)
target_link_libraries(mutex_test pthread)   # 注意这个地方必须链接到pthread库,因为thread编程用到了这个库,如果不链接会出错

执行结果

  • 不加锁:每次执行结果都不一样,比如其中一次出错的结果如下:
    在这里插入图片描述
  • 加锁:程序正常执行,在一个线程访问数据的时候,另一个线程并不能访问这个数据。
    在这里插入图片描述

2.std::unique_lock与std::lock_guard区别

他们两个的区别不是很大,都可以实现自动加锁和解锁的功能。主要区别可以记住以下几点:

  • unique_lock有手动的unlock()方法,可以手动解锁,而lock_guard没有,只能等待对象的生命周期结束后自动解锁,所以相对来说不是很灵活;
  • unique_lock可以配合环境变量condition_variable实现多线程之间的同步和通信,而lock_guard不行。

3.condition_variable条件变量

3.1.讲解例1

参考:C++11(六) 条件变量(condition_variable)

3.1.1.原例

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;		// 注意这里的线程锁是全局变量,同一个线程锁才能实现不同线程之间的切换
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    std::cout << "First go to Worker thread\n";
    //子进程的中wait函数对互斥量进行解锁,同时线程进入阻塞或者等待状态。
    cv.wait(lk, []{return ready;});
 
    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    //; 注意这里要先手动释放线程锁,然后另一个线程才能获取线程锁执行。
    //; 其实这里不手动释放也可以,因为这个线程马上就结束了。但是下一个例子中就可以看到区别
    lk.unlock();	
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // send data to the worker thread
    {
        //主线程堵塞在这里,等待子线程的wait()函数释放互斥量。
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();		// 为什么在这里才调用?但是好像线程在调用join之前就已经执行了?
}

程序执行结果:
在这里插入图片描述

上述函数执行的步骤是:

  1. 在主线程中新开一个worker线程,其实此时worker线程到底何时运行不是很确定,有可能在主线程std::lock_guard<std::mutex> lk(m);之前就运行了,也有可能在这之后才运行,但是从上面的执行结果来看是在主线程抢得线程锁执行之后,worker线程才执行;
  2. 主线程执行完43-48行之后(在此过程中worker线程无法获得线程锁,一直阻塞),条件变量ready变为true,主线程离开lock_guard的作用域后释放了线程锁。此时49行手动调用cv.notify_one();唤醒worker线程;
  3. worker线程首先获得线程锁,在wait函数的地方判断ready条件变量为真,此时继续持有线程锁而不释放,也就是worker线程继续跑(在此过程中主线程无法获得线程锁,一直阻塞);
  4. worker线程中将条件变量process变为true,最后释放线程锁,并在第34行手动调用cv.notify_one();唤醒主线程;
  5. 再次回到主线程,此时主线程获得线程锁,在wait函数的地方判断process条件变量为真,此时继续持有线程锁而不释放,也就是主线程继续跑(在此过程中由于worker线程中没有死循环,上面已经整个worker线程已经执行完毕,所以这里不会再执行worker线程)

3.1.2.修改测试

在原来的main函数第40行增加一句主函数的休眠语句,为了让worker先执行,代码和程序执行结果如下:

int main()
{
    std::thread worker(worker_thread);
    std::this_thread::sleep_for(std::chrono::seconds(2));	//; 主线程休眠2s
    data = "Example data";
    .........
}

在这里插入图片描述

3.2.讲解例2

参考:vins-mono使用的多线程代码解读

3.2.1.原例

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
 
std::condition_variable cv;
std::mutex m;
long balance = 0;
//add money will always start first
void addMoney(int money) {
    std::lock_guard<std::mutex> lg(m);//haveto work to the end of the scope
    balance+=money;
    cout<<"Amount added current balance: "<<balance<<endl;
    cv.notify_one();//let wait awake, enbale the lock of the next part
    std::this_thread::sleep_for(std::chrono::seconds(2));
 
    std::cout<<"HHHH"<<endl;
}
 
void withdrowMoney(int money) {
    std::unique_lock<mutex> ul(m);//acquire the lock
    cout << "First got to withdrowMoney thread" << endl;
    cv.wait(ul/*unique lock*/,[]{return (balance!=0)?true:false;});//if true , go further, false, release the lock and wait  
    if (balance>=money) {
        balance -=money;
        cout<<"Amount deducted: "<<money<<endl;
    }
    else{
        cout<<"Amount can't be deducted, current balance is less than "<<money<<endl;
    }
    cout<<"Current Balance is: "<<balance<<endl;
}
 
int main()
{
    std::thread t1(withdrowMoney,500);
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::thread t2(addMoney,500);
    t1.join();
    t2.join();
    return 0;
}

程序执行结果:
在这里插入图片描述

上述程序执行步骤:

  1. withdrowMoney先执行,获得线程锁之后,在wait函数处判断条件变量balance不满足条件,因此释放线程锁,该线程阻塞挂起;
  2. 2s后addMoney线程执行,获得线程锁后修改balance的值,然后手动调用cv.notify_one();唤起withdrowMoney线程。但是注意:由于此时lock_guard还未失效,所以线程锁仍然在addMoney线程手中,所以即使通知了另一个线程也没用,本线程会继续执行,休眠2s,然后输出"HHHH";
  3. addMoney线程结束释放线程锁,withdrowMoney被唤醒获得线程锁之后,在wait函数处判断条件变量balance满足条件,继续往下执行,知道执行完整个线程。注意由于addMoney线程中没有死循环,执行完毕之后就不再执行了

3.2.2.修改测试

addMoney线程中修改lock_guard线程锁的作用范围,在调用cv.notify_one();之前就释放线程锁,这样才能成功调用另一个withdrowMoney线程。修改函数如下:

void addMoney(int money) {
    {
	   std::lock_guard<std::mutex> lg(m);//haveto work to the end of the scope
	    balance+=money;
	    cout<<"Amount added current balance: "<<balance<<endl;
	}
    cv.notify_one();//let wait awake, enbale the lock of the next part
    std::this_thread::sleep_for(std::chrono::seconds(2));
 
    std::cout<<"HHHH"<<endl;
}

程序执行结果如下,可见“HHHH”是最后才输出的。
在这里插入图片描述

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 15:50:34  更:2022-03-03 15:51:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:18:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码