03 重修C++之并发实战
【持续更新中】
1 开始入门
让我们开始学习C++的多线程编写,C++!C++!C++!不是C,想看C的盆友请转到哦之前的文章 二、多线程_Linux C ,准确来说在 C++ 中使用 C 的多线程方法写也是没有问题的,而其本质上C++的多线程也是用到C的多线程技术。但是为了更好的学习C++,还是尽量在C++中使用C++标准的多线程模型。(PS:看书上说以前C++还不支持多线程模型。。。)
你好并发世界
首先先使用一个多线程写一个 “Hello World!”
HelloConcurrentWorld.cpp
#include <iostream>
#include <thread>
void hello()
{
std::cout << "Hello Concurrent World!" << std::endl;
}
int main(int argc, const char** argv)
{
std::thread t(hello);
t.join();
return 0;
}
打开Terminal
[wangs7@localhost HelloConcurrentWorld]$ g++ -o HelloConcurrentWorld HelloConcurrentWorld.cpp -pthread
[wangs7@localhost HelloConcurrentWorld]$ ./HelloConcurrentWorld
Hello Concurrent World!
[wangs7@localhost HelloConcurrentWorld]$
好了,这就入门了。
2 管理线程
线程是通过构造 std::thread 对象开始,该对象指定了线程上需要运行的任务(函数hello)。当然C++的多线程模型需要引入 <thread> 来支持,并且允许将一个带有函数调用操作符的类的实例传递给 std::thread 的构造函数进行代替。下面是几种构造形式。
2.1 启动线程
#include <iostream>
#include <thread>
void do_something()
{
std::cout << "do_something()" << std::endl;
}
void do_something_else()
{
std::cout << "do_something_else()" << std::endl;
}
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
int main(int argc, const char** argv) {
std::thread thread1( (background_task()) );
std::thread thread2{background_task()};
thread1.join();
thread2.join();
return 0;
}
#include <iostream>
#include <thread>
#include <unistd.h>
void do_something(int& i)
{
std::cout << i++ << "::" << "do_something()" << std::endl;
}
class func
{
public:
int& i;
func(int& i_):i(i_) {}
void operator()() const
{
for (int j = 0; j < 1000000; j++)
{
std::cout << j << "::";
do_something(i);
}
}
};
void oops()
{
int stat = 0;
func myfunc(stat);
std::thread my_thread(myfunc);
my_thread.detach();
usleep(10);
}
int main(int argc, const char** argv) {
oops();
usleep(10);
return 0;
}
运行后发现最后打印的 j 和 i 的值不同,向上找记录,找到130和131行发现:
126::126::do_something()
127::127::do_something()
128::128::do_something()
129::129::do_something()
130::130::do_something() //j 和 i 一致
131::0::do_something() //j 和 i 开始不一样
132::1::do_something()
133::2::do_something()
134::3::do_something()
135::4::do_something()
所以这里就是发生错误引用的地方
2.2 等待线程完成
等待线程完成需要调用join() 这样就能确保在函数结束前等待该线程结束,join() 的方式简单暴力,要么就等一个线程完成,要么就不等。如果需要对线程进行更细粒度的控制,例如检查线程是否完成,或只是在一段特定时间内进行等待,就必须使用替代机制,例如条件变量和future。调用join() 的行为会清理所有与该线程相关联的存储器,这样std::thread 对象不再与现在已完成的线程相关联,它也不与任何线程相关联,这就意味着,你只能对一个给定的线程调用一次join() ,一旦调用了join() ,此std::thread 对象就不再是可连接的,并且joinable() 将返回false 。
在使用多线程的时候,我们需要保证在线程对象销毁前调用join() 或detach() 函数。如果要分离线程,通常在线程启动后即可分离,但是如果打算等待该线程就需要仔细地选择在代码地那个位置调用join() 。如果在join() 前发生异常,就很可能跳过join() ,所以就需要在异常处理中也添加join() 调用,使用try/catch 是很麻烦的事情。
所以,有一种标准地资源获取即初始化(RAII)惯用语法,并提供一个类,在它的析构函数中进行join() ,如下:
#include <iostream>
#include <thread>
#include <unistd.h>
class ThreadGuard
{
std::thread& t;
public:
ThreadGuard(std::thread &t_):t(t_) {}
~ThreadGuard()
{
if (t.joinable())
{
t.join();
}
}
ThreadGuard(ThreadGuard const &) = delete;
ThreadGuard &operator=(ThreadGuard const &) = delete;
private:
ThreadGuard(ThreadGuard &&) = default;
ThreadGuard &operator=(ThreadGuard &&) = default;
};
void do_something(int& i)
{
std::cout << i++ << "::" << "do_something()" << std::endl;
}
class func
{
public:
int& i;
func(int& i_):i(i_)
{
}
void operator()() const
{
std::cout << "thread start--------------------." << std::endl;
for (int j = 0; j < 100; j++)
{
std::cout << j << "::";
do_something(i);
usleep(2);
}
}
};
void oops()
{
int stat = 0;
func myfunc(stat);
std::thread my_thread(myfunc);
ThreadGuard g(my_thread);
std::cout << "start usleep in thread." << std::endl;
usleep(5);
std::cout << "stop usleep in thread." << std::endl;
}
int main(int argc, const char** argv) {
oops();
std::cout << "main-------------------." << std::endl;
usleep(10);
return 0;
}
执行情况
[wangs7@localhost 2nd_chapter]$ ./a
start usleep in thread.
thread start--------------------.
0::0::do_something()
stop usleep in thread.
1::1::do_something()
2::2::do_something()
3::3::do_something()
4::4::do_something()
... ... ...
... ... ...
98::98::do_something()
99::99::do_something()
main-------------------.
[wangs7@localhost 2nd_chapter]$
首先可以看出在打印 “stop usleep in thread.” 之后(不是立刻)应该就会退出 oop() 线程应该会直接挂掉,但是由于 ThreadGuard 类中析构的时候会等待线程,是所以最后会一直等到线程退出,才进入主函数。
2.3 在后台运行线程
在 std::thread 对象上调用 detach() 会把线程丢到后台运行,没有直接的方法与之通信。也不再可能等待该线程完成;如果一个线程成为分离的,获取一个引用它的 std::thread 对象也是不可能的,所以它也不能够再次被结合。分离的线程会在后台运行;所有权和控制权会被转交给C++运行时库,以确保与线程相关联的资源在线程退出后能够被正确地回收。
2.4 传递参数给线程函数
对于在线程中传递参数给调用的函数或对象,基本上就是简单地将额外复制的参数传递给线程对象的构造函数。但重要的是,参数会以默认的方式被复制到内部存储空间,在哪里新创建的执行线程可以访问它们,即便函数中的相应参数期待着引用,下面提供一个简单的例子
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
这里创建一个与 t 相关联的执行线程,称为 f(3, "hello") 。即使 f 的第二个参数接受一个 std::string const& 类型的参数,但是字符串字面值仅在新线程的上下文中才会作为 char const* 传送,并作为 std::string 。尤其重要的是当提供的参数是一个自动变量指针时,如下。
void f(int i, std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, buffer);
t.detach();
}
std::thread t(f, 3, std::string(buffer));
上述方法完成的仅是**“复制”**即使参数中带有引用的符号,在线程构造过程中也是仅仅复制出一个对象放到线程中而非引用,如果期望使用引用,希望改变传入的参数,就需要使用 std::ref 来包装需要被引用的参数。下面的n将正确地被传入引用,而非n的副本。
void f2(int& n);
std::thread t2(f2, std::ref(n));
除了前面的几种构造方法之外,还有下面这种形式
class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x);
除此之外,还有另一种传递参数的方式,这里的参数只能够被**移动(一个对象内保存的数据被转移到另一个对象,使原来的对象变为空壳)**而不能被复制。
这种类型的一个例子是 std::unique_ptr 它提供了动态分配对象的自动内存管理。只有一个 std::unique_ptr 实例可以在某一时刻指向一个给定的对象,当该实例销毁时,其指向的对象将被删除。移动构造函数和移动赋值运算符允许一个对象的所有权在 std::unique_ptr 实例之间进行转移,这种转移会给源对象留下一个空指针。 所以当线程使用这种对象为参数时只能选择移动。
void f3(std::unique_ptr<big_object> b);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t3(f3, std::move(p));
2.5 转移线程所有权
std::thread 和 std::unique_ptr 是一样的,都是可移动的,而非可复制的。这意味着线程的所有权可以转移但是不能够被复制。
void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2 = std::move(t1);
t1 = std::thread(some_other_function);
std::thread t3;
t3 = std::mve(t2);
t1 = std::mve(t3);
因为 std::thread 支持移动,所以线程的所有权很容易从一个函数中被转出,或被转入。
std::thread func_out()
{
std::thread t(...);
... ...
return t;
}
void func_in(std::thread t);
func_in(std::thread(...));
func_in(std::move(t));
std::thread 支持移动的好处之一就是可以实际获取线程的所有权。这可以避免引用它的线程结束后继续存在造成不良影响,同时也意味着一旦所有权转移到了该对象,那么其他对象都不可以结合或分离该线程。因为这主要是为了确保在退出一个作用域之前线程都已完成,这种类称为 scoped_thread 。
#include <iostream>
#include <thread>
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_):t(std::move(t_))
{
if (!t.joinable())
{
throw std::logic_error("No thread");
}
}
virtual ~scoped_thread()
{
t.join();
}
scoped_thread(scoped_thread &&) = delete;
scoped_thread(const scoped_thread &) = delete;
scoped_thread &operator=(scoped_thread &&) = delete;
scoped_thread &operator=(const scoped_thread &) = delete;
};
void fun(int state)
{
for (int i = 0; i < 1000; i++)
std::cout << "Now in fun, state is " << i << "::" << state << std::endl;
}
void f()
{
int some_local_state = 666;
scoped_thread t(std::thread(fun, some_local_state));
}
int main(int argc, const char** argv) {
f();
return 0;
}
std::thread 对移动的支持同样考虑了 std::thread 对象的容器,如果那些容器是移动感知的,就可像下面的例子一样,生成一批线程,然后等待完成。
#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
#include <functional>
#include <string>
#include <unistd.h>
void do_work(unsigned int id)
{
std::cout << "thread::" + std::to_string(id) + " start!++++++++++++++\n" << std::endl;
sleep(3);
std::cout << "thread::" + std::to_string(id) + " stop!---------------\n" << std::endl;
}
void f()
{
std::vector<std::thread> threads;
for (unsigned int i = 0; i < 20 ;i++)
{
threads.push_back(std::thread(do_work, i));
}
std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));
}
int main(int argc, const char** argv) {
f();
return 0;
}
2.6 在运行时选择线程数量
C++库中对此有帮助的特性是 std::thread::hardware_concurrency() 。这个函数是一个静态方法返回支持的并发线程数。若该信息不可用返回0。这个值仅仅是作为一个提示,为了避免运行比硬件所能支持的更多线程数(超额订阅),以为上下文切换将意味着更多的线程会降低性能。
#include <iostream>
#include <thread>
int main(int argc, const char** argv) {
std::cout << "hardware_concurrency is " << std::thread::hardware_concurrency() << std::endl;
return 0;
}
通过这个静态方法能够获取到系统的最大同时运行线程数量,我们可以根据这个值在程序中动态调整我们的线程数量以适配不同的运行环境。
2.7 线程标识
线程标识符是 std::thread::id 。类 thread::id 是轻量的可频繁复制类,它作为 std::thread 对象的唯一标识符工作。此类的实例亦可保有不表示任何线程的特殊辨别值。一旦线程结束,则 std::thread::id 的值可为另一线程复用。此类为用作包括有序和无序的关联容器的关键而设计。
获取方式有两种:
一、从与之相关联的 std::thread 对象中通过调用 get_id() 成员函数来获得。如果无关联的线程,则返回默认构造的 std::thread::id 。
二、当前线程的线程标识可以通过 std::this_thread::get() 来获取。
#include <iostream>
#include <thread>
#include <chrono>
void foo()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << std::hex << "id of thread foo() is " << std::this_thread::get_id() << std::endl;
}
int main(int argc, char** argv)
{
std::thread t1(foo);
std::thread::id t1_id = t1.get_id();
std::thread t2;
std::thread::id t2_id = t2.get_id();
std::cout << std::hex << "t1's id: " << t1_id << '\n';
std::cout << "t2's id: " << t2_id << '\n';
if(t2_id == std::thread::id())
{
std::cout << "message" << std::endl;
}
t1.join();
}
线程库不限制用户检查线程的标识符是否相同,std::thread::id 类型的对象提供了一套完整的比较运算符,提供了不同值的总排序。这就允许它们在关系型容器中被用作主键,或排序。比较运算符为 std::thread::id 所有不相等的值提供了一个总排序。标准库还提供了 std::hash<std::thread::id> ,使得 std::thread::id 类型的值可在新的无序关系型容器中作为主键来用。
【2021/10/26】
3 线程间共享数据
|