目录
一、C11线程
C++11
中提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作等类,比起pthread更加灵活,不易出错。但是并发执行需要消耗时间代价, 系统从一个任务切换到另一个任务需要执行一次上下文切换
多进程并发 缺点:
A: 进程间通信较为复杂,速度相对线程间的通信更慢。 B: 启动进程的开销比线程大,使用的系统资源也更多。
多进程并发 优点:
A: 进程间通信的机制相对于线程更加安全。 B: 能够很容易的将一台机器上的多进程程序部署在不同的机器上。
多线程并发优点:
A: 由于可以共享数据,多线程间的通信开销比进程小的多。B: 线程启动的比进程快,占用的资源更少。
多线程并发缺点:
A: 共享数据太过于灵活,为了维护正确的共享,代码写起来比较复杂。B: 无法部署在分布式系统上。
二、类抽象
1.头文件
类使用的声明模块, 线程相关部分:锁住共享数据互斥锁mutex, 管理互斥量的lock_guard, 线程同步的condition_variable。子类具体实现的图像处理模块process()。
/*
*** Data: 2021.03.01
*** Author: xiaoqi.wang
*** Email: xiaoqiwang@tusvisiontech.com
*/
#ifndef ALGORITHMBASE_H
#define ALGORITHMBASE_H
enum ALG_RESULT_TYPE {
ALG_RESULT_TYPE_SUCCESS = 0, /* 算法处理结果正常 */
ALG_RESULT_TYPE_ONE_FAIL = 1, /* ONE 工位检测失败 */
};
/* 算法处理结果上报 */
struct AlgoResult {
int algorithm_id; /* 算法的id */
std::map<int, cv::Mat> res_imgs; /* 处理后的图像 */
void* reserved; /* 预留 */
};
using SrcFrame = std::pair<int64_t, cv::Mat>;
//算法callback
class AlgoListener
{
public:
virtual ~AlgoListener() {}
virtual bool onResultFromAlgo(uint64_t time_stamp, AlgoResult& algo_result) = 0;
};
//算法基础类,线程同步相关
class AlgorithmBase
{
public:
explicit AlgorithmBase(): process_running_(false) {}
virtual ~AlgorithmBase() { }
//... ...
//camera下发frame,唤醒线程
bool setFrameFromCamera(const cv::Mat& src_image, int64_t frame_id);
//初始化
virtual bool init() { return true;}
//开始
void start();
//停止
void stop();
protected:
//受保护的方法和属性,子类可访问
void process_loop();
virtual bool process() = 0;
private:
//类内和友元可访问,子类不可访问
AlgoListener* listener_;
std::mutex mutex_;
std::condition_variable process_cond_;
std::unique_ptr<std::thread> process_thread_;
std::atomic<bool> process_running_;
};
#endif // ALGORITHMBASE_H
2.cpp文件
2.1 创建thread,绑定loopFun
绑定线程执行的函数
//创建thread
void AlgorithmBase::start()
{
process_running_ = true;
process_thread_ = std::unique_ptr<std::thread> (new std::thread(&AlgorithmBase::process_loop, this));
}
2.2 setFrame给模块
上层主动的调用, 发送数据
//camera下发frame,唤醒线程
bool AlgorithmBase::setFrameFromCamera(const cv::Mat& image, int64_t frame_id) {
std::lock_guard<std::mutex> guard(mutex_);
//process的list
SrcFrame src_frame = std::pair<int64_t, cv::Mat>(frame_id, image);
src_frames_.push_back(std::move(src_frame));
//唤醒thread
process_cond_.notify_one();
return true;
}
2.3 threadFun 方法
被动的唤醒, 接收数据
//phread run 函数
void AlgorithmBase::process_loop()
{
while (process_running_) {
auto pred_func = [this] { return (!process_running_) || (src_frames_.size() > 0); };
{
std::unique_lock<std::mutex> lock(mutex_);
while(true) {
//pred_flag为False,阻塞该线程
process_cond_.wait(lock, pred_func);
bool pred = true;
//唤醒跳出循环
if(!process_running_) {
return;
} else if (pred) {
break;
}
}
src_frame_ = src_frames_.front();
src_frames_.pop_front();
}
//pipe line的处理, 子类具体实现
process();
//发送observer的时间戳
struct timeval current_time;
gettimeofday(¤t_time, NULL);
listener_->onResultFromAlgo((current_time.tv_sec*1000 +current_time.tv_usec/1000), algo_result_);
}
}
2.4 线程的退出
//停止stop thread
void AlgorithmBase::stop()
{
if (process_thread_) {
process_running_ = false;
process_cond_.notify_one();
if(process_thread_->joinable()) {
process_thread_->join();
}
}
}
3.code解析
3.1 std::mutex
C11最基本的互斥量,独占所有权的特性。lock()原语锁住该互斥量:
A: 没有被锁住,调用线程将该互斥量锁住,直道调用unlock。
B:被其它的线程锁住,当前的调用线程被阻塞。
C:被当前的线程锁住,产生死锁。
mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();
3.2 std::lock_guard
mutex保证关键部分是顺序执行的, 如上代码: 关键部分异常或者忘记解互斥锁, 则会出现死锁。{ }之外即生命周期离开临界区时,调用析构函数简单的说: lock_guard在构造时加锁, 在离开时析构释放锁 。
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}
3.3 std::unique_lock
比lock_guard更加灵活, 功能更加强大, 如延时try_lock等, 但是要付出更多的性能成本, 同时可以进行unlock和lock的操作。
void print_block (int n, char c)
{
//unique_lock有多组构造函数, 这里std::defer_lock不设置锁状态
std::unique_lock<std::mutex> my_lock (mtx, std::defer_lock);
//尝试加锁, 如果加锁成功则执行
//(适合定时执行一个job的场景, 一个线程执行就可以, 可以用更新时间戳辅助)
if(my_lock.try_lock()) {
for (int i=0; i<n; ++i) {
std::cout << c; std::cout << '\n';
}
}
}
void shared_print(std::string msg, int id) {
std::unique_lock<std::mutex> guard(_mu);
//do something 1
guard.unlock(); //临时解锁
//do something 2
guard.lock(); //继续上锁
// do something 3
// 结束时析构guard会临时解锁
// guard.ulock(); // 这句话可要可不要,不写,析构的时候也会自动执行
}
3.4 std::condition_variable
条件变量允许使用通知而实现线程的同步, 可以作为发送者和使用者的角色。作为发送者, 可以通知一个或者多个接受者。
std::mutex mutex_;
std::condition_variable condVar;
void waitingForWork(){
std::cout << "Worker: Waiting for work." << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck);
//doTheWork();
std::cout << "Work done." << std::endl;
}
void setDataReady(){
std::cout << "Sender: Data is ready." << std::endl;
condVar.notify_one();
}
接收方在发送方发出通知之前完成了任务。接收方对虚假的唤醒很敏感所以即使没有通知发生,接收方也有可能会醒来,为了保护它,可等待方法添加一个判断。它接受一个判定。判定是个callable,它返回true或false。
void waitingForWork(){
std::cout << "Worker: Waiting for work." << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck,[]{return dataReady;});
//doTheWork();
std::cout << "Work done." << std::endl;
}
3.5 std::pair
将两个数据合并到一个数据, 可以是不同的数据类型, 两个变量是first和second, 初始化时可以使构造函数也可以是std::make_pari() , make_pari() 在pari做参数位置, 不同是make_pari有隐式的类型转化。
std::pair<int, float>(1, 1.2); std::make_pair(1, 1.2); //double类型
1.namespece std{
template<typename T1, typename T2>
struct pair{
T1 first;
T2 second;
...
}
}
3.6 std::thread
A: 线程创建成功后立即启动,并没有一个类似start
的函数来显式的启动线程。
B: 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。注意:只需要在std::thread对象销毁前
做出这个决定。
C: join()
,主线程会一直阻塞着,直到子线程完成,join()
函数的另一个任务是回收该线程中使用的资源。
void function_1() {
std::cout << "I'm function_1()" << std::endl;
}
std::thread t1(function_1);
调用detach()
,从而将t1
线程放在后台运行,所有权和控制权被转交给C++
运行时库,以确保与线程相关联的资源在线程退出后能被正确的回收。
void test() {
std::thread t1(function_1);
t1.detach();
// t1.join();
std::cout << "test() finished" << std::endl;
}
// 使用 t1.detach()时
// test() finished
// I'm function_1()
// 使用 t1.join()时
// I'm function_1()
// test() finished
一旦一个线程被分离了,就不能够再被join
了。如果非要调用,程序就会崩溃,可以使用joinable()
函数判断一个线程对象能否调用join()
。
void test() {
std::thread t1(function_1);
t1.detach();
if(t1.joinable())
t1.join();
assert(!t1.joinable());
}
3.7 std::move
A: 使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了。
B: 避免不必要的拷贝操作,为性能而生。std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
C: 实现 移动语义意味着两点, 原对象不再被使用,如果对其使用会造成不可预知的后果。所有权转移,资源的所有权被转移给新的对象。
D: std::move除了能实现右值引用,同时也能实现对左值的引用。在左值上使用移动语义。
template<typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
对于右值 首先模板类型推导确定T的类型为string,得remove_reference::type为string,故返回值和static的模板参数类型都为string &&;而move的参数就是string &&,于是不需要进行类型转换直接返回。
std::move(string("haha"));
对于左值 当将一个左值传递给一个参数是右值引用的函数,且此右值引用指向模板类型参数(T&&)时,编译器推断模板参数类型为实参的左值引用。
string str("xixixi");
std::move(str);
此时明显str是一个左值,首先模板类型推导确定T的类型为string &,得remove_reference::type为string。故返回值和static的模板参数类型都为string &&;而move的参数类型为string& &&,即为sting &。所以结果就为将string &通过static_cast转为string &&。返回string &&
总结
C11 线程类的抽象