600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > C++多线程编程实战01:std::thread

C++多线程编程实战01:std::thread

时间:2019-12-10 10:10:47

相关推荐

C++多线程编程实战01:std::thread

C++多线程:std::thread

文章目录

C++多线程:std::thread定义构造函数析构函数赋值操作函数join与datch例子例子其它基本用法线程参数等待线程完成(Join)特殊情况下的等待转移所有权线程标识查看硬件支持的线程数量例子 运行时决定线程数量

最近这段时间在学习C++多线程相关的知识,打算将学习的内容记录下来,加深理解和记忆。

C++11 新标准中引入了五个头文件来支持多线程编程,他们分别是<atomic>,<thread>,<mutex>,<condition_variable><future>

<atomic>:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套C风格的原子类型和与C兼容的原子操作的函数。<thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。<mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。<condition_variable>:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。<future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

定义

C++是在C++11之后才有了线程库:std::thread。编译时需要添加选项:-std=c++11。

使用std::thread创建线程比较简单,thread实例化一个线程对象就创建完成了,示例:

#include <iostream>#include <thread>void f() { std::cout << "hello world"; }int main() {std::thread t{f};t.join(); // 等待新起的线程退出}复制代码

main() 函数的是主线程,将函数f()添加为std::thread的参数即可启动另一个线程,两个线程会同时运行。

构造函数

默认构造函数,创建一个空的std::thread执行对象。初始化构造函数,创建一个std::thread对象,该std::thread对象可被joinable,新产生的线程会调用fn函数,该函数的参数由args给出。拷贝构造函数(被禁用),意味着std::thread对象不可拷贝构造。Move构造函数,move构造函数(move语义是C++11新出现的概念),调用成功之后x不代表任何std::thread执行对象。

析构函数

~thread();复制代码

销毁thread对象。如果它还拥有关联线程(joinable() == true),则会调用std::terminate()结束程序。一般需要下列操作后,thread对象无关联的线程才可以安全销毁:

被默认构造被移动(转移所有权)已调用join()已调用detach()

赋值操作函数

如果该对象还拥有关联的运行中进程(即joinable() == true),则调用std::terminate()终止程序。否则,赋值other的状态给该对象并设置other为默认构造的状态(空状态,不再执行线程)。

thread& operator=( thread&& other ) noexcept;复制代码

注意:该操作与move构造函数一样,属于“剪切”而非“拷贝”。

join与datch

join:阻塞当前线程直至thread对象所标识的线程结束其执行。

void join();复制代码

例子

// TAG 演示join()namespace detail1{void func(){// 创建线程// todo 默认的构造函数创建线程// thread t();// if (true == t.joinable()) error// {// }// t.join(); //error//当前这个线程是否可以joinable的。通常不能被joinable有以下几种情况:// 1)由thread的缺省构造函数而造成的(thread()没有参数)。// 2)该thread被move过(包括move构造和move赋值)。// 3)该线程被join或者detach过。}}namespace detail2{void func(){cout << "func 子线程开始" << endl;cout << "func 子线程处理任务............ " << endl;for (int i = 0; i < 5; i++) // 处理5s{std::this_thread::sleep_for(std::chrono::seconds(1));cout << i << endl;}cout << "func 子线程结束" << endl;}// join()函数的作用是让主线程的等待该子线程完成,// 然后主线程再继续执行。这种情况下,子线程可以安全的访问主线程中的资源。子线程结束后由主线程负责回收子线程资源。}int main(int argc, char **argv){cout << "主线程开始执行" << endl;thread t2(detail2::func);t2.join();cout << "主线程已经等待子线程执行5s, 后继续执行其他任务" << endl;for (int i = 0; i < 3; i++) // 处理3s{std::this_thread::sleep_for(std::chrono::seconds(1));cout << i << endl;}cout << "主线程继续执行3s后结束" << endl;}

detach:从thread对象分离执行线程,允许独立地执行线程,主调线程无法再取得该线程的控制权。detach调用后不需要再调用join等待线程结束释放资源。一旦该线程退出,则自动释放所有分配的资源(它的资源会被init进程回收)。

void detach();复制代码

例子

// TAG 演示deatach// detach:从thread对象分离执行线程,允许独立地执行线程,主调线程无法再取得该线程的控制权。// detach调用后不需要再调用join等待线程结束释放资源。// 一旦该线程退出,则自动释放所有分配的资源(它的资源会被init进程回收)。namespace detail2{void func(){cout << "func 子线程开始" << endl;cout << "func 子线程处理任务............ " << endl;for (int i = 0; i < 5; i++) // 处理5s{std::this_thread::sleep_for(std::chrono::seconds(1));cout << "son:: " << i << endl;}cout << "func 子线程结束" << endl;}// join()函数的作用是让主线程的等待该子线程完成,// 然后主线程再继续执行。这种情况下,子线程可以安全的访问主线程中的资源。子线程结束后由主线程负责回收子线程资源。}int main(int argc, char **argv){cout << "主线程开始" << endl;thread t2(detail2::func);t2.detach();cout << "主线程不会等待子线程执行,子线程分离,主线程和子线程同步执行" << endl;for (int i = 0; i < 3; i++) // 处理3s{std::this_thread::sleep_for(std::chrono::seconds(1));cout << "main:: " << i << endl;}std::this_thread::sleep_for(std::chrono::milliseconds(200));cout << "主线程3.2s后结束,子线程在3s时被线程库接管,成为守护线程" << endl;}

其它

joinable:检查对象是否还标识活跃的执行线程。具体就是,若get_id() != std::thread::id()则返回true,否则false 。默认构造的thread因为没有执行线程所以返回false。 结束执行代码,但仍未调用join函数的线程仍被当作活跃的执行线程,从而返回true。

bool joinable() const noexcept;复制代码

get_id:返回标识与当前thread对象关联的线程的std::thread::id。也就是返回线程的唯一标识。类std::thread::id是轻量的可频繁复制类,它是std::thread对象的唯一标识符。此类的实例也保留有不表示任何线程的特殊值。一旦线程结束,则std::thread::id的值可为另一线程复用。此类也可以用作有序和无序的关联容器的键值。

std::thread::id get_id() const noexcept;复制代码

native_handle:返回实现定义的底层线程句柄。允许通过使用平台相关API直接操作底层实现。具体值要依赖具体平台,对于Linux而言,即返回pthread的句柄。

native_handle_type native_handle();复制代码

hardware_concurrency:返回实现支持的并发线程数。应该只把该值当做提示。当无法获取时,函数返回0。

static unsigned int hardware_concurrency() noexcept;复制代码

基本用法

上面已经给出实例,传递一个函数指针就可以实例化一个线程,而std::thread的参数也可以使用有函数操作符类型的对象实例或者Lambda表达式进行构造:

#include <iostream>#include <thread>struct A {// 函数操作符重载void operator()() const {std::cout << 1; }};int main() {A a;std::thread t1(a); // 1 会调用 A 的操作符()函数std::thread t2(A()); // 2 most vexing parse,声明名为t2参数类型为A的函数std::thread t3{A()}; // 3std::thread t4((A())); // 4std::thread t5{[] {std::cout << 1; }}; // 5t1.join();t3.join();t4.join();t5.join();}复制代码

对于上面语句①,通过有函数操作符类型的实例进行构造,也就是类型A的实例a,thread会将对象a复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。

语句②,传递了一个临时变量A(),而不是一个命名的变量。C++编译器会将其解析为函数声明,而不是类型对象的定义。即t2是一个函数,原型是std::thread t2(A (*)()),这个函数带有一个参数(函数指针指向没有参数并返回A对象的函数),返回一个std::thread对象的函数。

为了避免出现类似语句②那样的语法失误,可以使用多组括号③,或使用统一的初始化语法④都可以避免。

语句⑤,Lambda表达式也能避免这个问题。Lambda表达式是C++11的一个新特性,允许使用一个可以捕获局部变量的局部函数。

线程参数

向可调用对象或函数传递参数很简单,只需要将这些参数作为std::thread构造函数的附加参数即可。需要注意的是,这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。来看一个例子:

#include <thread>#include <string>#include <iostream>void f(int i, std::string const& s) {std::cout << s << i << std::endl;}int main() {std::thread t(f, 3, "hello");t.join();}复制代码

上述代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型,线程的上下文完成字面值向std::string的转化。

如果函数参数定义了默认实参则会被忽略,也就是必须要指定一个函数实参,即使有默认参数。

#include <iostream>#include <thread>void f(int i = 1) {}int main() {// std::thread t{f}; // 出错,因为默认实参会被忽略std::thread t{f, 42};t.join();}复制代码

如果参数是引用类型也会被忽略,如下面的代码,std::thread的构造函数①并不知晓函数f需要传入引用参数,直接无视函数参数类型,盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用f()。但因为函数期望的是一个非常量引用作为参数(而非右值),所以会在编译时出错。

#include <iostream>#include <thread>void f(int& x) {++x; }int main() {int i = 1;std::thread t{f, i}; // 1 compile errort.join();std::cout << i << std::endl;}复制代码

问题的解决办法很简单:如果参数是引用类型要使用std::ref,使用std::ref将参数转换成引用的形式,这样函数f()就会收到i的引用,而非i的拷贝副本,例子如下:

#include <iostream>#include <thread>void f(int& x) {++x; }int main() {int i = 1;std::thread t{f, std::ref(i)};t.join();std::cout << i << std::endl; // 输出 2}复制代码

thread构建也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:

#include <iostream>#include <thread>struct A {public:void do_work() {std::cout << "A::do_work\n";}};int main() {A a;std::thread t(&A::do_work, &a); // 1 t.join();}复制代码

上面这段代码中,新线程将会调用a.do_work(),其中a的地址作为对象指针提供给函数。

这种情况也可以为成员函数提供参数:std::thread构造函数的第三个参数就是成员函数的第一个参数,以此类推:

#include <iostream>#include <thread>struct A {public:void do_work(int num) {std::cout << "A::do_work\n";}};int main() {A a;int i = 1;std::thread t(&A::do_work, &a, i); // 1 t.join();}复制代码

另一种情况,为线程提供的入口函数的参数仅支持移动(move),不能拷贝。“移动”是指原始对象中的数据所有权转移给另一对象,从而这些数据就不再在原始对象中保存(类似文本编辑时的剪切操作)。std::unique_ptr就是这样一种类型(C++11中的智能指针),这种类型为动态分配的对象提供内存自动管理机制。同一时间内,只允许一个std::unique_ptr实例指向一个对象,并且当这个实例销毁时,指向的对象也将被删除。移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权在多个std::unique_ptr实例中传递。使用“std::move”转移对象所有权后,就会留下一个空指针。使用移动操作可以将对象转换成函数可接受的实参类型,或满足函数返回值类型要求。当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用std::move()进行显示移动。下面的代码展示了std::move的用法,展示了std::move是如何转移动态对象的所有权到线程中去的:

#include <iostream>#include <thread>struct big_object {public:void prepare_data(int num) {std::cout << "big_object::prepare_data\n";}};void process_big_object(std::unique_ptr<big_object> up) {std::cout << "process_big_object\n";}int main() {std::unique_ptr<big_object> p(new big_object);p->prepare_data(42);std::thread t(process_big_object, std::move(p)); // 1 t.join();}复制代码

通过在std::thread构造函数中执行std::move§,big_object对象的所有权首先被转移到新创建线程的的内部存储中,之后再传递给process_big_object函数。

等待线程完成(Join)

在线程销毁前要对其调用join等待线程退出或detach将线程分离,以下程序属于使用join正常等待线程退出,join属于阻塞式接口:

#include <iostream>#include <thread>#include <chrono>std::time_t now() {auto t0 = std::chrono::system_clock::now();std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);return time_t_today; // seconds}void foo(){// simulate expensive operationstd::this_thread::sleep_for(std::chrono::seconds(5));std::cout << now() << "-ending first helper...\n";}void bar(){// simulate expensive operationstd::this_thread::sleep_for(std::chrono::seconds(1));std::cout << now() << "-ending second helper...\n";}int main(){std::cout << "starting first helper...\n";std::thread helper1(foo);std::cout << "starting second helper...\n";std::thread helper2(bar);std::cout << now() << "-waiting for helpers to finish..." << std::endl;helper1.join();std::cout << now() << "-join return first helper...\n";helper2.join();std::cout << now() << "-join return second helper...\n";std::cout << "finish!\n";}复制代码

输出

starting first helper...starting second helper...1645140155-waiting for helpers to finish...1645140156-ending second helper...1645140160-ending first helper...1645140160-join return first helper...1645140160-join return second helper...finish!复制代码

使用detach分离线程,注意分离线程可能出现空悬引用的隐患:

#include <iostream>#include <thread>#include <chrono>class A {public:A(int& x) : x_(x) {}void operator()() {std::cout << "before sleep" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "after sleep" << std::endl;call(x_); // 存在对象析构后引用空悬的隐患}private:void call(int& x) {std::cout << x << std::endl;}private:int& x_;};void f() {int x = 0;A a{x}; // 1 x的引用传递给A.0std::cout << "before t" << std::endl;std::thread t{a}; // 2std::this_thread::sleep_for(std::chrono::seconds(1));t.detach(); // 3 不等待 t 结束std::cout << "after t.detach" << std::endl;} // 4 函数结束后 t 可能还在运行,而 x 已经销毁,a.x_ 为空悬引用int main() {std::thread t{f}; // 5 导致空悬引用t.join();std::cout << "finish" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5)); // 6}复制代码

输出:

before tbefore sleepafter t.detachfinishafter sleep0复制代码

如果std::thread线程结束后没有调用join释放资源,thread的析构函数会调用std::terminate终止程序,如下程序就会运行出错:

#include <thread>int main() {{std::thread t([] {});// t.join();}}// terminate called without an active exception复制代码

join会在线程结束后清理std::thread所有资源,使其与完成的线程不再关联,因此对一个线程只能进行一次 join,如果调用多次会抛出异常:

#include <thread>int main() {std::thread t([] {});t.join();t.join(); // 错误throw excaption}// 抛出的异常错误如下:// minate called after throwing an instance of 'std::system_error'// What(): Invalid argument复制代码

如果线程运行过程中发生异常(通常抛出异常要么会终止程序,要么跳转到捕获异常的位置),之后的join会被忽略,为此需要捕获异常,并在抛出异常前join:

#include <iostream>#include <thread>int main() {std::thread t([] {});try {std::cout << "throw 0" << std::endl;throw 1; // 1} catch (int x) {std::cout << "catch: " << x << std::endl;t.join(); // 2 处理异常前先 join()throw x; // 3 再将异常抛出}std::cout << "last join" << std::endl;t.join(); // 4 之前抛异常,不会执行到此处}复制代码

输出:

terminate called after throwing an instance of 'int'throw 0catch: 1复制代码

从上面的输出来看,根本不会执行到最后语句④,因为上面抛出异常程序就直接结束了,这个例子就是要说明thread创建的线程需要使用join()确保在线程完成后清理线程相关的资源,否则会引起程序异常。

特殊情况下的等待

对于上面发生异常导致程序终止的情况,很容易就忘记调用join,为了避免应用被抛出的异常所终止,通常我们使用另外一种方式解决该问题,封装一个类,在析构函数中使用join()。如同下面代码:

class thread_guard{std::thread& t;public:explicit thread_guard(std::thread& t_):t(t_){}~thread_guard(){if(t.joinable()) // 1{t.join();// 2}}thread_guard(thread_guard const&)=delete; // 3thread_guard& operator=(thread_guard const&)=delete;};void th_func() {std::cout << "th_func" << std::endl;}void f(){std::thread t(th_func);thread_guard g(t);do_something_in_current_thread();} // 4int main() {f();}复制代码

线程执行到④处时,局部对象就要被逆序销毁了。因此,thread_guard对象g是第一个被销毁的,这时线程在析构函数中被加入②到原始线程中。即使do_something_in_current_thread抛出一个异常,这个销毁依旧会发生。

在thread_guard析构函数的测试中,首先判断线程是否可汇入①。如果可汇入,会调用join()②进行汇入。

拷贝构造函数和拷贝赋值操作标记为=delete③,是为了不让编译器自动生成。直接对对象进行拷贝或赋值是很危险的,因为这可能会弄丢已汇入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。

如果不想等待线程结束,可以分离线程,从而避免异常。不过,分离操作即使能让线程仍然在后台运行着,也能确保在std::thread对象销毁时不调用std::terminate()。但这会打破了线程与std::thread对象的联系,也就是外部无法通过thread实例对象操作控制线程了。

转移所有权

std::thread是move-only类型,不能拷贝,只能通过移动转移所有权(复制构造函数已被删除),但不能转移所有权到joinable的线程,因为每个线程thread实例都是唯一的,没有两个std::thread对象会表示同一执行线程。

#include <thread>#include <cassert>#include <utility>void f() {}void g() {}int main() {std::thread t1{f}; // 1std::thread t2 = std::move(t1); // 2assert(!t1.joinable());assert(t2.joinable());t1 = std::thread{g}; // 3assert(t1.joinable());assert(t2.joinable());// t1 = std::move(t2); // 4 错误,不能转移所有权到 joinable 的线程t1.join();t1 = std::move(t2); // 5 assert(t1.joinable());assert(!t2.joinable());t1.join();}复制代码

首先,新线程与t1相关联①。当显式使用std::move()创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行f的函数线程与t2关联。

然后,临时std::thread对象相关的线程启动了③。为什么不显式调用std::move()转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用

将t2线程的所有权转移④给t1。不过,t1已经有了一个关联的线程(执行g的线程),所以这里系统直接调用std::terminate()终止程序继续运行。这样做(不抛出异常,std::terminate()是noexcept函数)是为了保证与std::thread的析构函数的行为一致。需要在线程对象析构前,显式的等待线程完成,或者分离它,进行赋值时也需要满足这些条件(说明:不能通过赋新值给std::thread对象的方式来"丢弃"一个线程)。

可以看到调用join释放线程资源后就可以使用move转移所有权了⑤。

移动操作同样适用于支持移动的容器

#include <algorithm>#include <thread>#include <vector>int main() {std::vector<std::thread> v;for (int i = 0; i < 10; ++i) {v.emplace_back([] {});}std::for_each(std::begin(v), std::end(v), std::mem_fn(&std::thread::join));}复制代码

将std::thread放入std::vector是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,而是把它们当做一个组。创建一组线程(数量可以在运行时确定)。

std::thread 可以作为函数返回值

#include <thread>std::thread f() {return std::thread{[] {}};}int main() {std::thread t{f()}; // f函数返回的thread移交给了tt.join();}复制代码

std::thread 也可以作为函数参数

#include <thread>#include <utility>void f(std::thread t) {t.join(); }int main() {f(std::thread([] {}));std::thread t([] {});f(std::move(t));}

实现一个可以直接用std::thread构造的自动清理线程的类

#include <stdexcept>#include <thread>#include <utility>class scoped_thread {public:explicit scoped_thread(std::thread x) : // 1t_(std::move(x)) {if (!t_.joinable()) {// 2throw std::logic_error("no thread");}}~scoped_thread() {t_.join(); // 3}scoped_thread(const scoped_thread&) = delete;scoped_thread& operator=(const scoped_thread&) = delete;private:std::thread t_;};void f() {scoped_thread t{std::thread{[] {}}}; // 4} // 5int main() {f();}

与上面实现的thread_guard相似,不过新线程会直接传递到scoped_thread中④,而非创建一个独立变量。当主线程到达f()末尾时⑤,scoped_thread对象就会销毁,然后在析构函数中完成汇入③。上面的thread_guard类,需要在析构中检查线程是否“可汇入”。这里把检查放在了构造函数中②,并且当线程不可汇入时抛出异常。

线程标识

线程标识为std::thread::id类型,可以通过两种方式进行检索。

可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值(一般是0),这个值表示“无线程”。在当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可以获得线程标识。

std::thread::id对象可以自由的拷贝和对比,因为标识符是唯一的。如果两个对象的std::thread::id相等,那就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。

C++线程库不会限制你去检查线程标识是否一样,std::thread::id类型对象提供了相当丰富的对比操作。比如,为不同的值进行排序。这意味着开发者可以将其当做为容器的键值做排序,或做其他比较。按默认顺序比较不同的std::thread::id:当a<bb<c时,得a<c,等等,标准库也提供std::hash<std::thread::id>容器,std::thread::id也可以作为无序容器的键值。

#include <iostream>#include <thread>#include <vector>void th_func() {std::cout << "sub_thread id2: " << std::this_thread::get_id() << std::endl;}void f(){std::thread t_null;std::cout << "null_thread id: " << t_null.get_id() << std::endl;std::thread t(th_func);std::cout << "sub_thread id1: " << t.get_id() << std::endl;t.join();} // 4int main() {std::thread::id main_thread = std::this_thread::get_id();std::cout << "main_thread id: " << main_thread << std::endl;f();std::vector<std::thread> threads(3); // 5for(int i=0; i < 3; ++i){threads[i]=std::thread([i] () {std::cout << i << std::endl;});}for (auto& it : threads) {it.join();}}

输出:

main_thread id: 140672360355648null_thread id: thread::id of a non-executing threadsub_thread id1: 140672360351488sub_thread id2: 140672360351488021

查看硬件支持的线程数量

std::thread::hardware_concurrency()在新版C++中非常有用,其会返回并发线程的数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个标识,当无法获取时,函数返回0。

#include <iostream>#include <thread>int main() {unsigned int n = std::thread::hardware_concurrency();std::cout << n << " concurrent threads are supported.\n";}

附录一个《C++并发编程》的例子

例子 运行时决定线程数量

std::thread::hardware_concurrency() 在新版C++标准库中是一个很有用的函数。这个函数会

返回能并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。

返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无法掩盖这 个函数对启动线程数量的帮助。清单2.9实现了一个并行版的 std::accumulate 。代码中将整体工作拆分成小任务交给每个线

程去做,其中设置最小任务数,是为了避免产生太多的线程。程序可能会在操作数量为0的时 候抛出异常。比如, std::thread

构造函数无法启动一个执行线程,就会抛出一个异常。

std::thread到这里基本完成了,接口不是很多,但是用法细节挺多的。学习时最好都能用一个简单例子跑一遍。

// std::accumulate的并行版本,非异常安全版本#include <numeric>#include <iterator>#include <thread>#include <vector>#include <functional>template<typename Iterator, typename T>struct accumulate_block{void operator()(Iterator first, Iterator last, T &result){// 计算范围和result = std::accumulate(first, last, result);}};template<typename Iterator, typename T>T parallel_accumulate(Iterator first, Iterator last, T init){// 计算迭代器的跳数const unsigned long length = std::distance(first, last);if (!length) // 如果长度为0,直接返回{return init;}// 最小理论线程数量const unsigned long min_per_thread = 25;// 最大理论线程数量const unsigned long max_threads = (length + min_per_thread - 1) / min_per_thread;// 硬件核心数量const unsigned long hardware_threads = std::thread::hardware_concurrency();// 真正运行的线程数量const unsigned long num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);// 每个线程分到的任务块的大小const unsigned long block_size = length / num_threads; // 结果结合 - 每个线程对应一个对应的结果集合std::vector<T> results(num_threads);// 线程池std::vector<std::thread> threads(num_threads);Iterator block_start = first;for (unsigned long i = 0; i < (num_threads - 1); ++i){Iterator block_end = block_start;// block_end 前进 block_size 个单位// 即block_start 到 block_end 是一个线程的工作块std::advance(block_end, block_size);threads.at(i) = \std::thread(accumulate_block<Iterator, T>(), \block_start, block_end, \std::ref(results.at(i))); // 绝对引用// 重置下一次block_startblock_start = block_end;}// 当前线程也要做任务,做最后一波任务accumulate_block()(block_start, last, results.at(num_threads - 1));// mem_fn把函数转换为函数对象std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));// 返回结果return std::accumulate(results.begin(), results.end(), init);}

在上面的示例中,Anthony Williams试图并行化标准累加函数。我的问题是他为什么要这样做:

unsigned long const max_threads=(length+min_per_thread-1)/min_per_thread;

为什么加上长度并减去1?

为什么不只是:

unsigned long const max_threads=length/min_per_thread;

原因

使用的问题

unsigned long const max_threads=length/min_per_thread;

是由整数除法中使用的截断舍入引起的

如果

length = 7min_per_thread = 5

然后

max_threads = length / min_per_thread = 1

而最大线程数实际上应该是2

length + min_per_thread - 1 = 11max_threads = (length + min_per_thread - 1) / min_per_thread = 2

参考:

《C++ Concurrency In Action》

std::thread - C++中文 - API参考文档 ()

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。