2022-07-25 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)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值