14.十面埋伏的并发:多线程真的很难吗
并发与多线程
并发有很多实现方式,而多线程只是其中最常用的手段
认识多线程
在C++中,线程就是一个能够独立运行的函数.你可以写一个lambda表达式让它在多线程中跑起来
- auto f = // 定义一个lambda表达式
{
cout << "tid=" <<
this_thread::get_id() << endl;
};
thread t(f); // 启动一个线程,运行函数f
好处:任务并行,避免IO阻塞,充分利用CPU,提高用户界面响应速度
基本常识:读而不写就不会有数据竞争
- 读取const变量,对类调用const成员函数,
对容器调用只读算法都是线程安全的
开发原则:最好的并发就是没有并发,最好的多线程就是没有线程
- 在微观层面"看不到"线程,减少死锁,同步的恶性问题出现
多线程开发实践
基本工具:仅调用一次,线程局部存储,原子变量和线程对象
1.仅调用一次
- 让初始化操作只调用一次,防止多线程没有同步时调用多次
- 声明once_flag类的变量,最好是静态,全局的(线程可见);然后利用call_once()来进行调用
- auto f = // 在线程里运行的lambda表达式
{
std::call_once(flag, // 仅一次调用,注意要传flag
{ // 匿名lambda,初始化函数,只会执行一次
cout << "only once" << endl;
} // 匿名lambda结束
); // 在线程里运行的lambda表达式结束
};
thread t1(f); // 启动两个线程,运行函数f
thread t2(f);
2.线程局部存储
-
读写全局(或局部静态)变量是常见的数据竞争场景
-
有时候全局变量并不一定是必须共享的,
可能是为了方便线程传入传出数据
-
有thread_local(和static,extern同级的关键字)标记的变量
在每个线程里都会有一个独立的副本,是线程独占的
- thread_local int n = 0;
auto f2 = [&](int x) {
cout << n << endl; //0
n += x;
cout << n << endl; //10 或者 100
};
thread t3(f2, 10);
thread t4(f2, 100);
t3.join();
t4.join();
cout << n << endl; //此处的n是主线程中的n,因此仍然是0.
- 而如果把例子中的thread_local改为static,
则变成所有的线程共享了
3.原子变量
- 原子指在线程领域中不可分,操作要么完成,要么未完成.
- 互斥量成本太高,对于小数据,原子化(atomic)是更好的方案
- C++只能让一些基本的类型原子化,比如atomic_int,atomic_long等
- 原子变量禁用了拷贝构造函数,因此初始化时不能用=,需要用()或{}
- 最基本用法是作为线程安全的全局计数器或者标志位;别的用法是实现无锁数据结构
4.线程对象
-
上面三个工具,都不与线程直接相关,但可以用于多线程编程,消除显式使用线程
-
std::thread成员与方法
-
std::this_thread方法:yield,get_id,sleep_for,sleep_until
async()
-
异步运行一个任务,隐含动作是启动一个线程去执行
- 不绝对保证立即启动,可在第一参数传递std::launch::async,立即启动现场
-
隐蔽的"坑"
- 不显式获取async()的返回值,就会同步阻塞值任务完成
- std::async(task, ...); // 没有显式获取future,被同步阻塞
auto f = std::async(task, ...); // 只有上一个任务完成后才能被执行
XMind - Trial Version
14.十面埋伏的并发:多线程真的很难吗<罗剑锋的C++实战笔记 >
原文:https://www.cnblogs.com/Stephen-Qin/p/13058217.html