C++并发入门(一)
前言
C++ 11 之后,并发库比较完善,可较为简单的进行并发编程,而不用直接调用系统api。
使得并发编程较为方便,但并发编程的逻辑已经变了,已经由单线程的顺序逻辑转换为乱序逻辑,执行顺序需要自己把控。
同时并发编程使得debug难度飙升,顺序逻辑为基础的 gdb 受到较大限制。
并发编程的功用也并非只是追求高速度,对于程序的逻辑梳理,方便进行任务的并行处理也是不可或缺。
并发编程的陷阱有时需要注意,需要并发的场合可能并非那么多,需要比较单线程和多线程的投入产出比。
一、Hello Concurrent World
通过简单的示例,了解一下多线程:
#include <iostream>
#include <thread>
void hello()
{
std::cout << "Hello Concurrent World\n" << std::endl;
}
auto main(int /*unused*/, char * /*argv*/[]) -> int
{
std::thread t(hello);
t.join();
return 0;
}
thread 是C++的线程库,用于开启一个新线程,对于无需控制并发逻辑的程序,直接开启线程,设置线程等待模式即可,非常简单。
二、线程的发起等待
1.线程发起
线程发起是由线程对象初始化引入函数完成的,为了发起线程,需要准备相应的函数或仿函数:
struct background_task
{
public:
void operator()() const
{
std::cout << "q" << std::endl;
std::cout << "r" << std::endl;
}
};
用函数或仿函数对象初始化线程对象:
background_task f;
std::thread my_thread(f);
2.线程等待方式
线程有两种等待方式,在主线程等待其他线程结束,或不等待。
在主线程等待其他线程完成:
my_thread.join();
不等待线程完成,将线程分离
my_thread.detach();
等待线程完成后退出,比较容易理解,除非程序崩溃,你会得到线程函数计算的结果或产生的副作用。
而不等待线程完成,将线程分离,则较为晦涩,同时由于需要利用主线程的全局对象将线程函数计算结果返回,而主线程结束会导致所有对象的析构,使得多线程的函数达不到目的。所以,对于逻辑不清晰的使用者,基本不会用对。
三、参数传递
参数的传递并不复杂,对于一般函数或仿函数对象,只需要在thread的第二个参数顺序给出即可。
1.传指针陷阱
这其中有一点需注意,就是如果传入的是局部变量指针,要确保变量的生命周期长到可以在线程初始化过程中存在。
void funcVariable(int i, const std::string &s)
{
std::cout << i << s << std::endl;
}
void testVariable(int someParam)
{
char buffer[] = "hello";
std::thread test(funcVariable, someParam, buffer);
test.detach();
}
以上代码的问题在于char指针在线程初始化时被带入到线程中,之后才会隐式转换为string,但buffer的生命周期随着线程分离的语句结束后就结束,基本不能支持到线程初始化完成。
有一个比较取巧的方法,是传入一个带状态的仿函数对象,但效率可能堪忧。
struct func
{
explicit func(const std::string &str_)
: str(str_)
{}
void operator()() const
{
std::cout << i << std::endl;
}
private:
std::string str;
};
void f()
{
char someLocalState[] = "hello";
func my_func(someLocalState);
std::thread t(my_func);
}
更实际的办法,是传参时就不用指针,直接构造std::string(buffer).
2.传递引用
线程传参还有个问题,线程构造的函数形参如果涉及引用,除了考虑生命周期,还要考虑如何实现,由于线程传参的过程会分成两部分,1,传给线程,2,在线程中进行拷贝,导致引用不能直接传过去,编译器不可通过,需要std::ref(引用)方法
void funcVariable(int i, std::string &s)
{
std::cout << i << s << std::endl;
}
void testVariable(int someParam)
{
std::string buffer = "hello";
std::thread test(funcVariable, someParam, std::ref(buffer));
test.join();
}
3.单一所有权变量的转移
C++中有一类变量是有单一所有权的,比如 std::unique_ptr 只可移动,不可拷贝,这种变量的传递,需要用 std::move(单一所有权变量)的方法。
void funcUniquePtr(std::unique_ptr<int> someParam)
{
std::cout << *someParam << std::endl;
}
void testUniquePtr()
{
std::unique_ptr<int> uPtr(new int(10));
std::thread thr(funcUniquePtr, std::move(uPtr));
thr.join();
}
4.类成员函数传递this指针
类成员函数和普通函数不同, 其省略了this指针, 但当将类成员函数用于thread线程时, 则必须将this指针进行传递, 因为this指针包含类成员函数的状态.
可以通过&对类对象取地址, 将成员函数的第一个参数传递此地址, 或者直接传递类对象,.
#include <iostream>
#include <thread>
struct func
{
func(int rhs)
: other(rhs)
{}
auto add(int lhs, int rhs) -> int
{
printf("%d\n", lhs + rhs + other);
return lhs + rhs;
}
private:
int other = 0;
};
auto main() -> int
{
func funcTest(5);
std::thread testPtr(&func::add, &funcTest, 1, 2);
std::thread testIns(&func::add, func(4), 1, 2);
testPtr.join();
testIns.join();
return 0;
}
总结
以上内容简述了线程的发起和参数的传递,不难理解,但极易出错。
对于多线程,比较考验编程者的逻辑,以上例子只是考虑线程传参的对象的生命周期和所有权转移,还未引入数据共享,线程间相互调用,数据竞争等问题,所以深入下去还需费些脑细胞的。
参考文献:C++并发编程实战(第2版)[英] 安东尼•威廉姆斯(Anthony Williams)