C++并发编程实战笔记(一)线程概念与基本控制

2023-05-16


tags: C++ Concurrency

写在前面

在C++ 中实现多线程还是很容易的, 不像C的pthreads接口, 下面来总结一下C++多线程的一些基本操作, 包括线程的创建, 合并, 分离, 获取ID等操作, 主要参考了**C++并发编程实战(第二版)**的第一二章, 这本书应该是C++并发必看的经典了.

另外参考:

std::thread;

一些有用的程序

用于辅助

#include <iostream>
#include <cassert> // 断言
#include <chrono>  // 计时
#include <thread>  // 线程

using namespace std;
using namespace std::chrono;   // 计时
using namespace std::literals; // 秒数字面量, C++14

睡眠

测试多线程, 不加睡眠系统实在是太容易假死了.

this_thread::sleep_for(1s); // 睡眠1s

计时

auto start = system_clock::now();
// ... 待计时的程序
auto end = system_clock::now();
auto duration = duration_cast<microseconds>(end - start);
cout << "Time spent: "
     << double(duration.count()) * microseconds::period::num /
            microseconds::period::den
     << "s" << endl;

线程基础

头文件thread

查看硬件支持

我的是8核CPU.

#include <iostream>
#include <thread>

int main(int argc, char const *argv[]) {
    // static method
    std::cout << std::thread::hardware_concurrency(); // 8
    return 0;
}

如果是1核, 那就只能实现并发而不能实现并行了.

创建与合并(join)

构造函数: 直接传入函数名(函数指针), 以及对应的参数(如果有), 需要注意线程的join(), 否则主线程不会等待子线程结束.

void fun() { cout << "Hello t1!\n"; }

void t1() {
    thread t1(&fun); // 传入函数指针
    if (t1.joinable()) cout << "t1 is joinable\n", t1.join();
    // 令主线程等待子线程
    // t1 is joinable
    // Hello t1!
}

其他创建方法:

  1. 传入函数对象: 临时对象, 即右值
  2. 传入函数对象: 具名对象, 即左值
  3. 传入lambda表达式

void t2() {
    thread t2([] { cout << "Hello t2!\n"; }); // lambda 表达式
    t2.join();                                // Hello t2!
}

struct Foo {
    void operator()() const { cout << "Hello t3!\n"; }
};

void t3() { // 传入临时函数对象
    // 二义性(烦人的分析机制), 参见Effective STL,
    // 即`只要C++语句有可能被解释成函数声明, 编译器就肯定将其解释为函数声明`
    // thread t3((foo())); // 由于存在函数指针二义性, 这里必须用圆括号包裹
    thread t3{Foo()}; // 同理, 这里用一致性初始化{}, 推荐这种方法
    t3.join();        // Hello t3!
}

struct Foo1 {
    void operator()() const { cout << "Hello t4!\n"; }
};

void t4() {
    Foo1 f;
    thread t4(f);
    t4.join(); // Hello t4!
}

void t5() {
    auto t5 = thread([] { cout << "Hello t5!\n"; });
    t5.join(); // Hello t5!
}

事实上使用join()方法等待线程是一刀切式的, 即要么不等待, 要么一直等待, 之后会采用期值(future)或者条件变量(condition_variable)来做.

并且线程只能被join一次.

int main() {
    //
    thread t1([] { cout << "AA\n"; });
    t1.join();                     //"AA"
    cout << t1.joinable() << endl; // 0
    t1.join(); // libc++abi: terminating with uncaught exception of type
               // std::__1::system_error: thread::join failed: Invalid argument
}

线程分离: detach

分离的线程不受主线程(即main函数)的管理, 而是由C++runtime库管理(成为daemon守护/后台进程).

但是分离线程之后就无法等待线程结束了

void t1() {
    thread t([] {
        cout << "detached thread\n";
        this_thread::sleep_for(1s);
    });
    t.detach();
    assert(!t.joinable());
    this_thread::sleep_for(1s);
    cout << "Main thread\n";
}


int main(int argc, char const* argv[]) {
    auto start = system_clock::now();
    t1();
    auto end = system_clock::now();
    auto duration = duration_cast<microseconds>(end - start);
    cout << "Time spent: "
         << double(duration.count()) * microseconds::period::num /
                microseconds::period::den
         << "s" << endl;
    // detached thread
    // Main thread
    // Time spent: 1.00508s
    return 0;
}

可见主线程和分离的线程(几乎)同时结束. 耗时1s.

上面代码中, 如果用join而不是detach, 那么用时就是2s, 大家可以测试一下.

获取id

两种获取方法:

  1. 直接对thread对象调用成员函数.get_id();
  2. 通过在对应线程中(即传入线程的函数中)调用this_thread::get_id().
int main() {
    cout << "null thread id: " << thread().get_id() << endl;
    cout << "null thread id: " << thread::id() << endl; // static func
    thread t1([] {
        cout << "Hello t1!\n";
        cout << "t1 thread id(use this_thread::get_id): "
             << this_thread::get_id() << endl;
    });
    cout << "main thread id: " << this_thread::get_id() << endl;
    cout << "t1 id(use t1.get_id): " << t1.get_id() << endl;
    t1.join();
    // null thread id: 0x0
    // null thread id: 0x0
    // main thread id: 0x1046d0580
    // t1 id(use t1.get_id): 0x16bc43000
    // Hello t1!
    // t1 thread id(use this_thread::get_id): 0x16bc43000
}

线程实战

参数传递的小问题

case 1: 常量引用

线程具有内部存储空间, 参数会按照默认方式先复制到该处, 新创建的线程才能直接访问它们.

然后, 这些副本被当成临时变量, 以右值形式传给新线程上的函数或者可调用对象.

即便函数的相关参数是引用, 上述过程依然会发生.

void oops() {
    //
    auto f = [](int i, string const& s) { cout << i << s << endl; };
    char buf[1024]; // 局部变量(自动变量 )
    snprintf(buf, 10, "%i", 100);
    // thread t(f, 3, buf); // buf 可能已销毁
    // 直接传入 buf 可能会出现安全性问题, 原因是参数传递本意是将 buf 隐式转换为
    // String, 再将其作为函数参数, 但转换不一定能及时开始(由于 thread
    // 的工作机制, 其构造函数需要原样复制所有传入的参数)
    thread t(f, 3, string(buf)); // 这样可以解决, 直接在传入之前进行构造
    t.detach();
}

自动变量: 代码块内声明或者定义的局部变量, 位于程序的栈区.

case 2: 非常量引用

// 传入一个非常量引用
class Widget {};
void oops_again() {
    auto f = [](int id, Widget& w) {};
    Widget w1;
    // 此时传入的 w1 是右值形式, move-only 型别, 因为非常量引用不能向其传递右值
    // thread t(f, 10, w1);
    thread t(f, 10, std::ref(w1));
    t.join();
}

针对非常量引用, 由于这种形参不能接受右值变量, 所以一定要加上std::ref修饰(配接器)

case 3: 成员函数

class X {
public:
    void do_something() { cout << "do_something\n"; }
};
void t2() {
    X my_x;
    // 向某个类的成员函数设定为线程函数, 需要传入函数指针, 指向该成员函数
    thread t(&X::do_something, &my_x);
    // 若考虑到对象指针, 成员函数的第一个形参实际上是其第二个实参
    // 向线程函数 传入的第三个参数就是成员函数的第一个参数
}

针对成员函数的参数传递, 需要考虑形参的顺序(将成员的地址作为成员函数的第一个参数, 然后才传入成员函数的参数)

case 4: 智能指针的控制权转移

void process(unique_ptr<X>){} // X 定义在 case 3
void t3(){
    unique_ptr<X> p(new X);
    p->do_something();
    thread t(process, std::move(p));// 通过 move 移交智能指针所指对象的控制权
}

通过 std::move() 移交控制权

移动语义支持

通过移动语义, thread可以实现控制权移交.

void f() { cout << "f()\n"; }
void g() { cout << "g()\n"; }

void test1() {
    thread t1(f);         // t1:f
    t1.join();
    thread t2 = move(t1); // t2:f
    t1 = thread(g);       // t1:g
    t1.join();
    thread t3;
    t3 = move(t2); // t1:g t2:∅ t3:f
    // 运行f的线程归属权转移到t1, 该线程最初由t1启动, 但是在转移时,
    // t1已经关联到g的线程, 因此terminate()会被调用, 终止程序.
    t1 = move(t3); // 终止整个程序
    // f()
    // g()
}

void f3(thread t) {}
void g3() {
    // 线程归属权可以转移到函数内部, 函数能够接收thread实例作为按右值传递的参数.
    f3(thread(f));
    thread t(f);
    f3(std::move(t));
}

std::move() 仅仅将左值强制类型转换为右值, 但是不进行其他操作, 真正移交控制权的时刻是 t2 的move构造调用时(初始化)

并行版的accumulate

template <typename Iterator, typename T>
struct accmuluate_block {
    void operator()(Iterator first, Iterator last, T& result) {
        result = accumulate(first, last, result);
    }
};

template <typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
    // 设置常量
    unsigned long const length = distance(first, last);
    if (!length) return init; // 如果计算区间为空, 返回初值
    unsigned long const min_per_thread = 25; // 每一个线程计算的数量
    unsigned long const max_threads =        // 最大线程数
        (length + min_per_thread - 1) / min_per_thread;
    unsigned long const hardeare_threads = thread::hardware_concurrency(); // 8
    unsigned long const num_threads = // 实际线程数
        min(hardeare_threads != 0 ? hardeare_threads : 2, max_threads);
    unsigned long const block_size = length / num_threads;
    // 存放计算结果,
    vector<T> results(num_threads);
    // 设置线程存储
    vector<thread> threads(num_threads - 1);
    Iterator block_start = first;

    for (unsigned long i{}; i < num_threads - 1; ++i) {
        Iterator block_end = block_start;
        advance(block_end, block_size);
        threads[i] = thread(accmuluate_block<Iterator, T>(), block_start,
                            block_end, ref(results[i])); // 这里使用ref适配器
        block_start = block_end;
    }
    accmuluate_block<Iterator, T>()(block_start, last,
                                    results[num_threads - 1]);

    for (auto& entry : threads) entry.join();
    // 汇总每一个线程分块的结果, 累加得到最终结果, 所以结果需要满足结合律
    // (double/float不满足, 所以可能与串行版accumulate结果有出入)
    return accumulate(results.begin(), results.end(), init);
}


vector<int> get_vec() { // 生成测试数据
    vector<int> v;
    for (int i{}; i < 10000000; ++i) v.emplace_back(i);
    return v;
}


void t1() {
    auto v = get_vec();
    auto start = system_clock::now();
    int ans = parallel_accumulate(v.begin(), v.end(), 0);
    // int ans = accumulate(v.begin(), v.end(), 0);
    auto end = system_clock::now();
    auto duration = duration_cast<microseconds>(end - start);
    cout << "Time spent: "
         << double(duration.count()) * microseconds::period::num /
                microseconds::period::den
         << "s" << endl;

    cout << ans;

    // with parallel:
    // Time spent: 0.014697s
    // -2014260032

    // without parallel:
    // Time spent: 0.083763s
    // -2014260032
}

确实是快了将近8倍…

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++并发编程实战笔记(一)线程概念与基本控制 的相关文章

  • 逻辑回归案例

    应用案例 之前学习了逻辑回归 xff0c 我们现在来做一个案例 一个图片验证码识别的案例 xff1a 怎么从图片中准确的识别出正确的数字 我们分了三步 第一步 xff1a 先生成150验证码图片 xff0c 每个图片有5个数字 图片中有随机
  • CorelDRAW x4提示非法软件产品被禁用解决方法教程

    说起PS大部分人都有所耳闻 xff0c 甚至会一些简单的操作 但是CDR x4这名字相信有很多人就很陌生了 xff0c 所以在这里也很有必要先说一下CDR到底是个什么样的存在 CDR全名CorelDRAW xff0c 是加拿大Corel公司
  • Mybatis-Plus中分页插件PaginationInterceptor, MybatisPlusInterceptor在SpringBoot中的使用

    配置分页插件 span class token annotation punctuation 64 Configuration span span class token keyword public span span class tok
  • 矩阵连乘问题-构造最优解

    题目描述 使用动态规划算法求解矩阵连乘问题 输入 每组数据包括两行 xff0c 第一行为数组长度n xff0c 第二行为存储矩阵维数的一维数组 输出 矩阵连乘最优计算次序 样例输入 Copy 7 30 35 15 5 10 20 25 样例
  • 树莓派启动——安装+无显示器使用+自启动VNC

    目录 硬件准备软件准备写入系统启动树莓派换源VNC自启动 时隔一年多 xff0c 拿起树莓派却忘记如何使用了 本想用作自己搭建git服务器 xff0c 后续再完成了 在此记录一下使用流程 硬件准备 树莓派 3b 43 TF卡和读卡器 xff
  • Debain 10(Buster)换源

    Debain 10 Buster 换源的操作步骤 必要条件 xff1a 已经安装好的Debain 10 Buster 开始 Debain 10 Buster 换源的操作步骤步骤一 备份原始的源文件用户切换到root下 进行源文件备份 二 换
  • 使用nginx反向代理突然失灵

    之前使用nginx反向代理还好好的 xff0c 后来再启动项目时突然失灵 xff0c 浏览器显示如下 然后开始排查错误 xff0c 首先直接使用ip地址访问是正常的 xff0c 然后使用hosts中映射的域名访问是无效的 xff0c 这说明
  • win10 安装 Linux子系统(WSL)

    序 xff1a 前段时间字节不是发布了 modernJS 的开源项目吗 xff1f 大概看了一部分的内容 xff0c 这些的东西就不一一列出来了 xff0c 本来想尝一口的 xff0c 在环境准备的系统那里就先折了一下 xff08 目前支持
  • Java 集合

    ArrayList 默认长度为10 indexOf lastIndexOf 通过equals方法判断索引 span class token keyword public span span class token keyword int s
  • Java 多线程知识

    参考链接 xff1a https www cnblogs com kingsleylam p 6014441 html https blog csdn net ly0724ok article details 117030234 https
  • Java I/O

    参考链接 xff1a https blog csdn net m0 71563599 article details 125120982 https www cnblogs com shamo89 p 9860582 html https
  • 最小生成树 prim算法(附代码)

    prim算法是以一个根节点开始慢慢往下延伸 xff0c 不断寻找距生成树最短的距离的节点 xff0c 然后将该节点纳入生成树的集合中 xff0c 然后再将该节点影响的其他未纳入生成树节点的距离更新 xff08 缩小与生成树的距离 xff09
  • cdr x4检测显示软件产品已被禁用警告弹窗,如何解决教程分享

    偶尔翻开移动硬盘 xff0c 找到这货 xff0c CorelDraw X4简体中文正式版 网上现在比较难下载得到了 xff0c X4是我最常用的一个 现在把它分享出来 xff0c 有需要的可以去下载使用 orelDRAW X4打开显示被禁
  • 数据结构与算法题目集(中文) 6-1 单链表逆转 (20 分)

    本题要求实现一个函数 xff0c 将给定的单链表逆转 函数接口定义 xff1a List Reverse List L 其中List结构定义如下 xff1a typedef struct Node PtrToNode struct Node
  • HTML5 Table 布局实现 商品列表

    运行结果如上 下面说说设计过程 xff1a 一开始试探的做的时候 xff0c 是建立了一个table xff0c 这个table里面放一本图书的信息 然后建立了一个列 xff0c 然后建立了个td xff0c td里面放图片 xff0c t
  • POJ 1050 To the Max(动态规划)

    Given a two dimensional array of positive and negative integers a sub rectangle is any contiguous sub array of size 1 1
  • web前端 背景色属性bgcolor

    通过 lt body gt 元素中的bgcolor属性来设定网页的背景颜色 其语法格式如下 xff1a lt body bgcolor 61 34 value 34 gt 颜色是属性值的设定有三种方法 xff1a 1 颜色名称 规定颜色值为
  • java连接数据库步骤

    1 加载驱动 Class forname 数据库驱动名 2 建立数据库连接 使用DriverManager类的getConnection 静态方法来获取数据库连接对象 xff0c 其语法格式如下所示 Connection conn 61 D
  • 怎么从零开始运行github / 现成的项目

    这篇博客是作为非计软科班出身的我记录的一些经验 xff0c 希望得到交流和批评 目录 环境配置 通过文件命名了解项目 demo 代码运行的入口 设定参数的文件 build 通过代码了解项目 64 装饰器 一些交流时用到的术语 API 交流或
  • 生产环境中使用Kolla部署OpenStack-allinone云平台(红帽8版本)

    CentOS8系统中使用Kolla部署OpenStack allinone云平台 Kolla概述和openstack所有结点linux系统初始配置 kolla是openstack下面用于自动化部署的一个项目 xff0c 它基于docker和

随机推荐