实战总结-C++并发应用

实战总结-C++并发应用

[TOC]
基于前几天学习的C++并发实战《C++ Concurrency in Action》进行自己的实战.
场景为拟合B样条曲线后等长分段过程中的分点求一阶导数的计算.结果显示较大计算量时并发节省时间效果明显.

代码模型搭建

拟合B样条曲线后需要对其进行等长分段,用弦长近似求弧长中需要进行分点求一阶导数,点数按照需求精度决定,精度高时需要的点数很多,如果能多线程求解能取得精度与效率的平衡.参照《C++ Concurrency in Action》的第三章内容得到如下执行模型.

model.JPG

踩过的坑

Thread构造函数

初始化构造函数声明如下:

1
2
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

因为我是在类里初始化thread,需要调用类内的成员函数,所以参数一直匹配不上,找到如下参考,解决.

1
2
3
4
5
6
7
8
9
Define INVOKE (f, t1, t2, ..., tN) as follows:

(t1.*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;
((*t1).*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is not one of the types described in the previous item;
t1.*f when N == 1 and f is a pointer to member data of a class T and t 1 is an object of type T or a
reference to an object of type T or a reference to an object of a
type derived from T;
(*t1).*f when N == 1 and f is a pointer to member data of a class T and t 1 is not one of the types described in the previous item;
f(t1, t2, ..., tN) in all other cases.

注意对于f函数中引用形式的参数需要用std::ref()强制转换成引用.因为为了保证参数能outlive thread的生命周期,所以一般都是把参数copy到thread实例里,如果要保持引用的话需要强制引用.
同时如果有些值只对某个thread必要,也可以用std::move传入,不用copy.

一样的错误,我尝试把sharedData Interface类放入std::unique_ptr中然后传入f函数中,报错error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr.unique_ptr(const unique_ptr&) = delete;
因为多个threads都会去调用各自的f函数,因此需要把参数copy多份分给各个thread,但是std::unique_ptr是没有复制操作符的.
把std::unique_ptr改为std::shared_ptr解决.

同理地,对于std::asyncstd::bind的构造函数也需要注意到上面.

mutex的误嵌套

真的是纸上的来终觉浅啊,书中强调过如果要使用嵌套锁,需使用std::recursive_mutex,我根本没这个意识,所以子函数里mutex一下,母函数里有mutex了,导致一直阻塞,程序停在那里.

1
2
3
4
5
6
7
8
9
10
void FuncA(){
std::lock_guard<std::mutex> l(mx);
do_some_thing;
}

void FuncB{
std::lock_guard<std::mutex> l(mx);
FuncA; //嵌套mutex了...
do_some_other_things;
}

涉及到sharedData接口类里的嵌套成员函数时注意使用std::recursive_mutex.

join之前判断joinable

把分段的数据中的一段交给main thread执行后报错:terminate called after throwing an instance of 'std::system_error' what(): Invalid argument.
原来是有线程已经join了,后面还是不加判断地全部join导致的问题.

1
2
3
4
5
for (auto &thread : threads){
if (thread.joinable()){
thread.join();
}
}

sharedData数据结构

因为要求的数据需要考虑顺序,所以先想到的是2维数组,基于2维vector实现,每个thread的输出分别为一个1维vector,为2维vector的元素.最后再首位相接拼成一个1维vector.
跑通后省略2维vector,直接用迭代器访问修改1维vector.

GDB调试

下面是常用的几个多线程调试命令:

1
2
3
4
5
info threads #线程信息汇总
thread [ID] #跳到某个线程
thread apply [ID……] [command] #对某个线程运行命令
set scheduler-locking [mode] #当调试一个线程时其他的线程是否继续执行,off,默认值不锁定,on锁定只执行当前线程,step单步执行,只有被调试的程序执行
break <linespec> thread [ID] if ... #对某线程打断点

tip:多线程下不支持倒退调试.

结果

改变计算点数目,10万点时,多开2个线程平均速度提升近60%,快与线程数的增加成正比了.
1万点时,多开2个线程平均速度提升近20%,并不稳定偶尔多线程变现还不如单线程.
1千点时,多开2个线程平均速度提升近10%,表现也不稳定,偶尔多线程变现还不如单线程.
因此单次计算量太小时,也许单线程就够了.但是当计算量很大的时候,多线程能显著降低运行时间,这对RTOS下程序很重要.

ps.计算平台Intel Core i7-6600U CPU @ 2.60GHz × 4

2

作者

cx

发布于

2021-11-14

更新于

2022-07-16

许可协议