多线程开发(1)

在 C++ 里,有四个基本线程处理的工具:仅调用一次、线程局部存储、原子变量和线程对象。

仅调用1次

先声明一个 once_flag 类型的变量,最好是静态、全局的(线程可见),作为初始化的标志:

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

然后调用专门的 call_once() 函数,以函数式编程的方式,传递这个标志和初始化函数。这样 C++ 就会保证,即使多个线程重入 call_once(),也只能有一个线程会成功运行初始化。


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

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

线程局部存储

线程局部存储这个功能在 C++ 里由关键字 thread_local 实现,它是一个和 static、extern 同级的变量存储说明,有 thread_local 标记的变量在每个线程里都会有一个独立的副本,是“线程独占”的,所以就不会有竞争读写的问题。

示例:

thread_local int n = 0; // 线程局部存储变量
auto f = [&](int x) // 在线程里运行的lambda表达式,捕获引用
{
	n += x; // 使用线程局部变量,互不影响
	cout << n; // 输出,验证结果
};
thread t1(f, 10); // 启动两个线程,运行函数f
thread t2(f, 20);

先定义了一个线程独占变量,然后用 lambda 表达式捕获引用,再放进多个线程里运行。

原子变量

对于那些非独占、必须共享的数据.要想保证多线程读写共享数据的一致性,关键是要解决同步问题,不能让两个线程同时写,也就是“互斥”。
这在多线程编程里早就有解决方案了,就是互斥量(Mutex)。但它的成本太高,所以,对于小数据,应该采用“原子化”这个更好的方案。

所谓原子(atomic),在多线程领域里的意思就是不可分的。操作要么完成,要么未完成,不能被任何外部操作打断,总是有一个确定的、完整的状态。所以也就不会存在竞争读写的问题,不需要使用互斥量来同步,成本也就更低。但不是所有的操作都可以原子化的,否则多线程编程就太轻松了。目前,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); // 比较运算

原子变量的主要应用场景:

  • 当作线程安全的全局计数器或者标志位
  • 实现高效的无锁数据结构(lock-free)。

线程对象

C++ 标准库的std::this_thread 里,有 yield()get_id()sleep_for()sleep_until() 等线程管理函数。
用法示例:

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();

建议使用异步运行的思路,调用函数 async(),“异步运行”一个任务,隐含的动作是启动一个线程去执行,但不绝对保证立即启动(也可以在第一个参数传递 std::launch::async,要求立即启动线程)。大多数 thread 能做的事情也可以用 async() 来实现,但不会看到明显的线程:

其实,这还是函数式编程的思路,在更高的抽象级别上去看待问题,异步并发多个任务,让底层去自动管理线程,要比我们自己手动控制更好(比如内部使用线程池或者其他机制)。sync() 会返回一个 future 变量,可以认为是代表了执行结果的“期货”,如果任务有返回值,就可以用成员函数 et() 获取。
不过要特别注意,get() 只能调一次,再次获取结果会发生错误,抛出异常std::future_error。(至于为什么这么设计我也不太清楚,没找到官方的解释)另外,这里还有一个很隐蔽的“坑”,如果你不显式获取 async() 的返回值(即 future 对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数),于是async就变成了sync。所以,即使我们不关心返回值,也总要用auto来配合 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; // 获取任务的执行结果
原文地址:https://www.cnblogs.com/shijiashuai/p/14409343.html