文章目录
- 引言
- 正文
- 代码的执行和线程
- 使用std::mutex进行编程
- mutex基本用法
- std::lock_guard的使用
- std::unique_lock的使用
- condition_variable的使用
- wait函数的使用
- condition_variable的整体代码
- 多线程编程的基本语句
- 总结
- 引用
引言
-
在学习老师给的目标检测的代码过程中,接触到了串口通信、相机控制以及多线程通信。在前两个文章已经介绍了串口通信使用的Pcomm库,大恒相机的具体控制,具体链接如下:
- 串口通信使用Pcomm库,链接
- 大恒相机控制程序,链接
-
在本文中,将具体介绍多线程的信息传递的具体实现。因为在最终的程序中,需要同时控制四个相机,并且要同时完成图片处理和控制的多个任务,所以必不可少,要使用多线程。程序中对于消息通信机制,是借鉴了Sogou C++ Workflow这个项目。
-
这里就结合这个程序简单分析一下。
-
这里主要是偏向文字的分析比较多,有一片有图的博客,可以参考一下
- 基本信号量的解释
- 读者写者问题的具体分析
正文
- 这里使用了两个队列,一个是生产者放入消息的队列,一个是消费者取走消息的队列,两个队列进行互换,实现消息的传递。
- 之前虽然接触过进程锁之类的编程,但是都是使用自己定义的结构体来实现进程的互斥访问,并没有使用过std::mutex进行编程,而且线程也是自己定义的一些类的实体。
- 所以,这里首先介绍一下线程的基本要素,然后介绍一下std::mutex信号量类,接着在介绍一下std::condition_variable同步线程类的使用,最后在介绍具体的实现过程。
代码的执行和线程
-
程序的编译执行过程
- 编写源代码
- 替换源代码中的预处理
- 将源代码编译为汇编代码
- 将汇编代码编译为目标代码
- 将多个文件的目标代码进行链接
- 将链接后的目标代码加载到内存中
- CPU执行内存中的机器代码
- 结束和清理分配资源
-
同一个进程的多个线程是共享代码段、数据段、堆空间的,每一个线程有自己的栈空间,寄存器。
-
多个线程之间共享和不共享的存储设备
- 代码段:所有线程共用一个代码段,所有的线程都执行相同的程序
- 数据段:所有线程共享数据段中的全局变量和静态变量,一般控制线程通信的都是使用全局变量
- 堆空间 :所有线程的空间分配都是来自于同一个堆空间,线程可以释放和分配堆上的内存。对于堆空间的合理管理,是避免线程冲突的一个主要的方面。
- 栈空间:线程都有自己私有的栈空间,所以局部变量是线程安全的,
- 栈空间:用于存储局部变量和函数调用的返回地址。
- 寄存器:每一个线程都有自己的寄存器集合的副本。
-
控制进程互斥:
- 所以,如果要控制进程互斥,就得对他们共享的空间中声明变量,也就是将互斥信号量声明为全局变量,或者静态变量,因为这二者是共享。不能在函数内部声明互斥锁变量,因为每一个线程都有自己的栈空间,意味着每一个都有一个互斥锁副本,彼此并不会有任何影响。
使用std::mutex进行编程
- std::mutex是C++标准库中的一个类,用于同步线程访问共享资源。是一个同步原语,用来保护共享数据免受多个线程同时访问。
- 注意,如果要通过mutex互斥锁来控制线程同步,一定要声明为全局变量。
mutex基本用法
-
锁定:
- 当一个线程锁定互斥锁时,其他试图锁定该互斥锁的进程将会被阻塞,知道拥有互斥锁的线程解锁。
-
解锁
- 拥有互斥锁的线程可以解锁他,其他线程锁定
-
mutex有两种方式实现对于互斥锁的使用,分别如下
- 使用std::lock_guard:一旦锁定,就不能解锁,除非当前作用域的变量被销毁
- 使用std::unique_lock:一旦锁定,除了等待这个作用域的变量自动销毁,还可以自己加上unlock解锁,实现在作用域内解锁
std::lock_guard的使用
- 在下述代码中,碎语mutex进行声明对象时,会自动落锁,然后在下面的地方编辑代码,当结束方法时,会自动解锁。
- 具体样例代码如下
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
int shared_data = 0; // 共享资源
void increment() {
std::lock_guard<std::mutex> lock(mtx); // 自动锁定互斥锁
++shared_data;
std::cout << "Thread " << std::this_thread::get_id() << " incremented shared_data to " << shared_data << '\n';
} // 锁定的互斥锁在lock对象离开作用域时自动解锁
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return 0;
}
- 运行结果
std::unique_lock的使用
- 不同于std::guard,std::unique_lock是需要手动落锁和解锁的,支持更加复杂的操作,支持多线程的通信操作。
- 通过lock()落锁,通过unlock()解锁
- 具体使用代码如下
std::mutex mtx; // 必然是全局变量
std::unique_lock<std::mutex> lock(mtx); // 构造时自动锁定mtx
// 在此处访问受保护的共享资源
lock.unlock(); // 显式解锁
// ...
lock.lock(); // 显式重新锁定
// 在此处再次访问受保护的共享资源
// 析构时自动解锁mtx
condition_variable的使用
- std::condition_variable是C++标准库中的一个类,用于同步线程,是的线程能够相互之间进行通信。这个是实现读这些这问题的根本,当信号量发生变化,要及时通知相关进程进行操作。
- 当线程需要等待某个条件成立(或某个事件发生)时,它可以使用条件变量进入睡眠状态。当条件成立时,另一个线程可以使用条件变量通知等待的线程,使其醒来并继续执行。
- 基本用法:
- wait():通知进程进入睡眠状态,直到另外一个线程调用notify_one()或notify_all()通知它醒来,同时释放传入的互斥锁,并且下次被唤醒之后,会从wait之后的语句开始执行。
- notify_one() 通知一个正在等待的线程,唤醒某一个线程
- notify_all() 通知所有正在等待的线程,唤醒所有线程
wait函数的使用
- 功能描述:
- 释放传入的互斥线程锁
- 使执行函数的线程陷入阻塞
- 被唤醒的线程,将会冲被阻塞的地方继续执行
- 被唤醒的线程将重新获得互斥锁
- 参数
- std::unique_lockstd::mutex ,传入的是进程锁的落锁语句。
- 具体使用
- 执行流程:
- 申请了一个线程t1,执行函数waitForReady
- 线程t1执行到第一句lock,会对互斥锁变量mtx落锁
- 执行到wait,因为ready为false,就陷入阻塞
- 主线程沉睡10秒钟,莫放在执行别的任务
- 主线程执行setReady函数,将ready设置为true,并且唤醒等待的线程
- 线程t1醒来了,重新从wait开始往下执行,直到结束
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx; // 声明全局变量,互斥线程锁
std::condition_variable cv; //声明全局条件变量
bool ready = false; // 声明共有资源
void waitForReady() {
// 获取线程互斥锁,落锁,其余线程并不能访问
std::unique_lock<std::mutex> lock(mtx);
// 判定是否满足执行条件,默认为false,会陷入阻塞
while (!ready) {
// 当前线程陷入阻塞,并且释放互斥锁
// 线程被唤醒之后,会重新落锁,从此出开始执行
cv.wait(lock);
}
std::cout << "Ready is true, continuing execution.\n";
}
void setReady() {
std::unique_lock<std::mutex> lock(mtx);
ready = true;
std::cout << "everything is ready \n";
cv.notify_one(); // 唤醒等待的线程
}
int main() {
std::thread t1(waitForReady);
std::cout<<"allocate the task to thread 1"<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(10)); // 模拟一些工作
setReady();
t1.join();
return 0;
}
- 执行效果
condition_variable的整体代码
-
多个线程进程互斥操作具体运行程序,主要是显出一点,那就是进程执行的随机性
-
具体使用代码如下
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
// 输出并打印编号
std::unique_lock<std::mutex> lock(mtx);
while (!ready) { // 如果条件不满足,则等待
cv.wait(lock);
}
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lock(mtx);
ready = true; // 改变条件
cv.notify_all(); // 通知所有等待的线程
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // 开始比赛
for (auto &th : threads) th.join();
return 0;
}
- 执行效果如下
- 结果分析
- 这个程序和上面那个程序差不多,唯一的差异就是线程多了,由原先的一个线程变成了10个线程,而且执行的顺序是随机的,说明一个问题,那就是同时唤醒所有的进程,但是进程获取互斥锁的顺序并不是按照阻塞的顺序获取的,是随机获取的。
多线程编程的基本语句
-
通过学习上面的样例程序,仅仅知道多线程的互斥访问如何实现,但是并没有学习过多线程的基本变成,但是或多或少用到了,这里做一下总结。
-
在C++中一般使用std::thread库进行创建和管理线程,通过创建thread对象,来实现对于线程的操作
1. 构造函数
- std::thread有多个构造函数,允许你以不同的方式创建线程。最常用的构造函数接受一个函数指针或可调用对象,并将其作为新线程的入口点。
void myFunction(int x) {
// 代码
}
int main() {
std::thread myThread(myFunction, 42); // 传递参数给线程函数
}
2. 成员函数
-
std::thread提供了一些成员函数来管理线程的生命周期和行为。以下是一些常用的成员函数:
- join(): 等待线程完成执行。如果线程已经完成,则立即返回。
- detach(): 允许线程独立运行。调用后,线程对象不再代表实际的线程执行。
- joinable(): 检查线程是否可以被join或detach。
- get_id(): 返回线程的ID。
- hardware_concurrency(): 返回可用的并发线程数。
总结
- 对于多线程的编程,之前仅仅是在数据结构的课程设计上接触过,并没有真切接受过,这次算是有一个初步的接触了。
- 之前专门写过读者写者问题,写过哲学家进餐问题,但是都没有具体实现过,实际应用起来,还是听不一样的。
- chatGPT搜索能力还是很强的。
引用
- chatGPT-plus
- std::mutex的参考文档
- std::condition_variable的参考文档