Part.3 重修C++之并发实战1 您所在的位置:网站首页 solidworks文件名称自动关联 Part.3 重修C++之并发实战1

Part.3 重修C++之并发实战1

2023-06-13 22:06| 来源: 网络整理| 查看: 265

1 开始入门

让我们开始学习C++的多线程编写,C++!C++!C++!不是C,想看C的盆友请转到哦之前的文章 二、多线程_Linux C ,准确来说在 C++ 中使用 C 的多线程方法写也是没有问题的,而其本质上C++的多线程也是用到C的多线程技术。但是为了更好的学习C++,还是尽量在C++中使用C++标准的多线程模型。(PS:看书上说以前C++还不支持多线程模型。。。)

你好并发世界

首先先使用一个多线程写一个 “Hello World!”

HelloConcurrentWorld.cpp

#include #include ? void hello() { ? ?std::cout std::cout 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 #include #include void do_something(int& i) { std::cout } void operator()() const { for (int j = 0; j int stat = 0; func myfunc(stat); std::thread my_thread(myfunc); my_thread.detach(); //分离线程,即不等待线程完成 usleep(10); //加休眠是为了方便观察循环次序和变量值的变化 }// 当oops()退出时,线程仍然可能运行,1出就会访问一个被销毁的变量 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 #include #include //用来承载线程的类 class ThreadGuard { std::thread& t; public: //构造函数 要用引用而不是值传递! ThreadGuard(std::thread &t_):t(t_) {} //析构函数 保证对象销毁前等待线程退出 ~ThreadGuard() { if (t.joinable()) { t.join(); } } //销毁const的拷贝函数和赋值函数 ThreadGuard(ThreadGuard const &) = delete; ThreadGuard &operator=(ThreadGuard const &) = delete; private: //私有化拷贝函数和赋值函数 保证外部不会调用 ThreadGuard(ThreadGuard &&) = default; ThreadGuard &operator=(ThreadGuard &&) = default; }; //打印函数 void do_something(int& i) { std::cout } void operator()() const //()运算符函数 线程执行函数 { std::cout int stat = 0; //初始化func类 func myfunc(stat); //启动线程 std::thread my_thread(myfunc); //将线程添加到 ThreadGuard 类中 ThreadGuard g(my_thread); std::cout char buffer[1024]; sprintf(buffer, "%i", some_param); //有可能出现 在buffer转换成string类型之前函数oops退出,导致未定义的情况 //大概率会发上述情况,解决方法是在将buffer传递给线程构造函数之前就完成类型转换 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; // 在 my_x 对象上运行 X::do_lengthy_work() std::thread t(&X::do_lengthy_work, &my_x); // 这段代码将在新线程上调用 my_x.do_lengthy_work() // 而且第三个参数之后的参数将会作为 do_lengthy_work() 的参数

除此之外,还有另一种传递参数的方式,这里的参数只能够被**移动(一个对象内保存的数据被转移到另一个对象,使原来的对象变为空壳)**而不能被复制。

这种类型的一个例子是 std::unique_ptr 它提供了动态分配对象的自动内存管理。只有一个 std::unique_ptr 实例可以在某一时刻指向一个给定的对象,当该实例销毁时,其指向的对象将被删除。移动构造函数和移动赋值运算符允许一个对象的所有权在 std::unique_ptr 实例之间进行转移,这种转移会给源对象留下一个空指针。 所以当线程使用这种对象为参数时只能选择移动。

void f3(std::unique_ptr b); std::unique_ptr 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(); // t1关联执行线程some_function() std::thread t1(some_function); // 当t2构建完成时some_function()线程所有权由t1转移到t2 std::thread t2 = std::move(t1); // 启动一个新线程并与临时的std::thread对象相关联,并将所有权转移给t1 t1 = std::thread(some_other_function); // 创建一个std::thread对象t3不关联任何线程 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(...)); //or func_in(std::move(t));

std::thread 支持移动的好处之一就是可以实际获取线程的所有权。这可以避免引用它的线程结束后继续存在造成不良影响,同时也意味着一旦所有权转移到了该对象,那么其他对象都不可以结合或分离该线程。因为这主要是为了确保在退出一个作用域之前线程都已完成,这种类称为 scoped_thread。

#include #include 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 f(); return 0; }

std::thread 对移动的支持同样考虑了 std::thread 对象的容器,如果那些容器是移动感知的,就可像下面的例子一样,生成一批线程,然后等待完成。

#include #include #include #include #include #include #include void do_work(unsigned int id) { std::cout threads.push_back(std::thread(do_work, i)); } // 对每个线程调用join等待线程完成 std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); /*************************************** 这一条语句作用同下这段代码 std::vector::iterator it = threads.begin(); for ( ;it != threads.end(); it++) { it->join(); } ****************************************/ } int main(int argc, const char** argv) { f(); return 0; } 2.6 在运行时选择线程数量

C++库中对此有帮助的特性是 std::thread::hardware_concurrency()。这个函数是一个静态方法返回支持的并发线程数。若该信息不可用返回0。这个值仅仅是作为一个提示,为了避免运行比硬件所能支持的更多线程数(超额订阅),以为上下文切换将意味着更多的线程会降低性能。

#include #include int main(int argc, const char** argv) { std::cout 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


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有