C++多线程,另辟蹊径的并发编程方式

以下内容均是从另一个角度来看待多线程编程的方式,并不是用常规的共享变量或者锁来解析多线程编程

1、多用const关键字

C++多线程编程注意读而不写”就不会有数据竞争,在 C++ 多线程编程里读取 const 变量总是安全的,对类调用 const 成员函数、对容器调用只读算法也总是线程安全的。

2、C++4个用于多线程工具实践

多线程下要防止多次初始化数据,如果不控制,会导致初始化函数多次运行。

call_once()仅调用一次

意义:仅调用一次

static std::once_flag flag;        // 全局的初始化标志

使用方式:
1. 先声明一个 once_flag 类型的变量,最好是静态、全局的(线程可见),作为初始化的标志;
2. 调用专门的 call_once() 函数,以函数式编程的方式,传递这个标志和初始化函数。
效果:即使多个线程重入 call_once(),保证只会有一个线程会成功初始化。


auto f = []()                // 在线程里运行的lambda表达式
{   
    std::call_once(flag,      // 仅一次调用,注意要传flag
        [](){                // 匿名lambda,初始化函数,只会执行一次
            cout << "only once" << endl;
        }                  // 匿名lambda结束
    );                     // 在线程里运行的lambda表达式结束
};

thread t1(f);            // 启动两个线程,运行函数f
thread t2(f);

熟悉单例模式的可能会想到一个“双重检查锁定”的问题,双重检查锁定其实是有安全隐患的 , 具体的安全隐患等以后补充,先自己想想。
使用call_once()就消除了这个问题。

thread_local线程局部存储

多线程下经常会用读写全局变量或者局部静态变量,通过一个共享数据来达到一个数据同步的目的。

首先要明白,为什么要使用全局变量或者局部静态变量,如果仅仅是为了数据单独的方便线程传入传出数据,而不是真正用来共享使用这个数据,可以把他换成线程局部存储来使用,这样叫做线程独占所有权。

意义:有 thread_local 标记的变量在每个线程里都会有一个独立的副本,是“线程独占”的,所以就不会有竞争读写的问题。


thread_local int n = 0;        // 线程局部存储变量
//static int n = 0; // 静态全局变量,如果使用静态,结果为30
auto f = [&](int x)           // 在线程里运行的lambda表达式,捕获引用
{   
    n += x;                   // 使用线程局部变量,互不影响
    cout << n;                // 输出,验证结果
};  

thread t1(f, 10);           // 启动两个线程,运行函数f
thread t2(f, 20);

程序结果为10和20.

注意:必须清楚的知道数据的使用方式。

原子变量

线程同步:一种方式就是互斥量,但是开销大。所以,如果是比较小的数据,可以使用原子化方案。
目前,C++ 只能让一些最基本的类型原子化,比如 atomic_int、atomic_long,等等:

using atomic_bool = std::atomic<bool>;    // 原子化的bool
using atomic_int  = std::atomic<int>;      // 原子化的int
using atomic_long = std::atomic<long>;    // 原子化的long

这些使用多线程读写一般不会出错。
注意:
原子变量禁用了拷贝构造函数,所以在初始化的时候不能用“=”的赋值形式,只能用圆括号或者花括号


atomic_int  x {0};          // 初始化,不能用=
atomic_long y {1000L};      // 初始化,只能用圆括号或者花括号

assert(++x == 1);           // 自增运算

y += 200;                   // 加法运算
assert(y < 2000);           // 比较运算 

static atomic_flag flag {false};    // 原子化的标志量
static atomic_int  n;               // 原子化的int

auto f = [&]()              // 在线程里运行的lambda表达式,捕获引用
{
    auto value = flag.test_and_set();  // TAS检查原子标志量

    if (value) {
        cout << "flag has been set." << endl;
    } else {
        cout << "set flag by " <<
            this_thread::get_id() << endl;  // 输出线程id
    }

    n += 100;                    // 原子变量加法运算

    this_thread::sleep_for(      // 线程睡眠
        n.load() * 10ms);        // 使用时间字面量
    cout << n << endl;
};                        // 在线程里运行的lambda表达式结束

thread t1(f);                // 启动两个线程,运行函数f
thread t2(f);

t1.join();                   // 等待线程结束    
t2.join();

上述的例子还是看到了使用thread,最好的方法是隐藏到底层使用,如何隐藏到底层,具体如下。

async()

含义:“异步运行”一个任务,隐含的动作是启动一个线程去执行,但不绝对保证立即启动(也可以在第一个参数传递 std::launch::async,要求立即启动线程)。

这种调用线程的方式是把线程封装到底部使用,不会看到明显的线程。


auto task = [](auto x)                  // 在线程里运行的lambda表达式
{
    this_thread::sleep_for( x * 1ms);  // 线程睡眠
    cout << "sleep for " << x << endl;
    return x;
};

auto f = std::async(task, 10);         // 启动一个异步任务
f.wait();                              // 等待任务完成

assert(f.valid());                    // 确实已经完成了任务
cout << f.get() << endl;              // 获取任务的执行结果
这种不调用thread

线程的方式其实还是函数式编程的思路,在更高的抽象级别上去看待问题,异步并发多个任务,让底层去自动管理线程,要比我们自己手动控制更好(比如内部使用线程池或者其他机制)。

C++ 标准库里有专门的线程类 thread,使用它就可以简单地创建线程,在名字空间 std::this_thread 里,还有 yield()、get_id()、sleep_for()、sleep_until() 等几个方便的管理函数。可以直接百度查以下他们的用法。

async() 会返回一个 future 变量,可以认为是代表了执行结果的“期货”,如果任务有返回值,就可以用成员函数 get() 获取。
注意:get()只能调用一次。再次获取结果会发生错误,抛出异常 std::future_error。

另一个注意点:如果你不显式获取 async() 的返回值(即 future 对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数),于是“async”就变成了“sync”。

std::async(task, ...);            // 没有显式获取future,被同步阻塞
auto f = std::async(task, ...);   // 只有上一个任务完成后才能被执行

所以尽量用 auto 来配合 async(),避免同步阻塞,就像上面的示例代码那样。

3、总结

上述的整体思想就是尽量不用线程,或者把线程尽量封装到底层去使用。

  1. 多线程是并发最常用的实现方式,好处是任务并行、避免阻塞,坏处是开发难度高,有数据竞争、死锁等很多“坑”;
  2. call_once() 实现了仅调用一次的功能,避免多线程初始化时的冲突;
  3. thread_local 实现了线程局部存储,让每个线程都独立访问数据,互不干扰;
  4. atomic 实现了原子化变量,可以用作线程安全的计数器,也可以实现无锁数据结构;
  5. async() 启动一个异步任务,相当于开了一个线程,但内部通常会有优化,比直接使用线程更好。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值