1.并发
1.1 并发形式
- 硬件并发
一个核心一个任务 - 单核多任务调度
1.2 并发的方式
- 多进程并发
信号、Socket、文件、管道、远程连接
2.线程基础
2.1.启动线程
- 观点:在线程传入函数中尽量以类传,因为涉及到变量都被被拷贝。尽量不要涉及到局部变量的指针和引用的要格外注意。这是多线程共享内存所带来的安全问题。
- 一个线程仅一次join,其后原始存储部分将清理。
- detach分离线程。比如打开一个文档时又新建一个文档。
2.2 传参
- 向线程传参。即使在线程的函数接收参数有&,但thread的构造函数会复制传入数据,再给到函数里,起不到引用作用。使用std::ref( )。
- 传类的函数初始化线程。先传类函数指针,再传类对象指针,以及参数。
- 转移
线程对象被析构前,要等待线程完成,或分离它,不能通过赋新值给thread对象来抛弃一个线程。
2.3 转移所有权
std::move()
2.4 识别线程
std::thread对象的get_id(),或std::this_thread::get_id()
3. 线程间的数据共享
3.1 条件竞争
- 软件事务内存;
- 使用标准库的互斥量;
3.2 互斥量
- std::mutex对象,lock(),unlock()
- 使用std::lock_guard()代替直接使用mutex,用mutex实例化lock_guard,在lock_guard被析构,例如退出函数时,就能自动解锁。例如访问前先上锁。
在锁下的对象都将访问限制?
3 . 在保护时不要将受保护数据的指针或引用传出去 - 死锁
std::lock() 可以同时给两个互斥量上锁。
建议:- 避免嵌套锁。 一个线程已获得一个锁时,就不去获取第二个;
- 避免在持有锁时调用用户提供的代码;
- 使用固定顺序获取锁;
- 使用锁的层次结构;
hierarchical_mutex 自定义的?
高层锁在外,低级锁在内。 - unique_lock
- 锁的粒度
描述一个锁保护的数据量的大小;
锁不仅要控制合适数据大小,还要控制锁的持有时间。
3.3 其他代替互斥量的方法
- 关于变量初始化
使用call_once函数和once_flag对象代替仅一次初始化的手动检查。
如static声明、定义可以放到一个函数中;
4.同步并发操作
4.1 等待
1.标志位持续查询;
2.std::this_thread::sleep_for()周期性;std::this_thread::until()
3.等待唤醒;
4.1.1 条件变量
条件变量是一个对象,可以阻塞线程,直到被通知恢复。当调用其等待功能之一时,它使用unique_lock(通过互斥锁)来锁定线程。该线程将保持阻塞状态,直到被另一个在同一个condition_variable对象上调用通知功能的线程唤醒为止。
std::condition_variable和std::condition_variable_any;
.notify_one(仅通知一个)或者.notify_all(多个等待一个时)和.wait()相互配合。
wait的条件变量中使用unique_lock()而不是look_guard();
有时条件变量不一定时最好的同步机制。
4.2 期望
一次性事件定义为“期望”future
有唯一futures<>和shared_futures<>,类似于unique_ptr和shared_ptr
- 要实现从异步运行的函数中接受结果,用std::async()启动任务;
- async()的std::launch::defered参数表调用wait()或.get()时才计算。std::launch::async必须在独立线程执行。
- async()返回一个future对象
- std::packaged_task()绑定一个期望到函数或可调用对象上。使用().get_future()获取期望。
- async返回的期望调用get或者wait才执行,而packaged_task封装一个可调用对象,可通过get_future返回绑定一个期望。在package_task对象调用后,就立即执行,结束时future就绪,期望get时就直接拿。
- std::promise 承诺 set_value,对一个数据做出承诺,并设值;
- 用future“存储”异常,若async或promise、packaged_task抛出异常,future在get时,就会抛出同样的异常;
- 多个线程获取期望,future对象只能有一个线程获取结果,可以使用shared_future在多个线程共享数据,为避免竞争可以加锁或者各自拷贝一份数据。
- promise().get_future().share() 期望的share()创建新的shared_future
4.3 限定等待时间
期望会阻塞调用,
处理持续时间: _for后缀
处理绝对时间:_until后缀
- 时钟
std::chrono空间,包含
- std::chrono::system::now() 系统时钟当前时间 是some_clock::time_point时间点类型
- std::chrono::high_resolution_clock::now()
- std::ratio<_, _> 时钟节拍
- std::chrono::system_clock可能不是稳定的
- std::chrono::steady_clock稳定时钟
- 时延
std::chrono::duration<>
std::chrono::duration<double,std::ratio<1,100>> 毫秒级计数存在double类型中
转化: std::chrono::duration_caststd::chrono::seconds(msdata)
- 使用时延表示超时等待:
if(future_obj.wait_for(std::chrono::milliseconds(35))==std::future_status::ready)
do_something(future_obj.get())
else if ******* ==std::future_status::timeout
else if ******* ==std::future_status::deferred //延时
- 时间点
std::chrono::time_point<>
用时间点的概念来表达超时等待:
auto const timeout = std::chrono::steady_clock::now()+std::chrono::milliseconds(500);
**
if(condition_variable_var.wait_until(unique_lock_var, timeout)==std::cv_status::timeout)
4.能接受超时功能的函数
4.4 同步操作
- 函数式编程概念FC,函数输出只和输入参数有关,不受外部状态影响。
在并发编程中引入FC概念,线程间不共享数据,信息在线程间传递。
CSP(communicating sequential processor) 通讯顺序进程
MPI(message passing interface) 消息传递接口 - 消息传递实现同步
CSP:没有共享数据,行为仅基于传入的信息;
参与者模式(Actor model): 系统中有许多独立参与者,彼此会发送信息,然后执行手头任务,不共享状态。
4.5 总结
同步操作:
- 条件变量
- 期望
- 承诺
- 打包任务
代替同步的方案: - 函数式编程;
- 消息传递模式;
5 C++内存模型和原子类型
原子类型和原子操作会直接接触硬件;
原子操作没有指定访问顺序,不会产生竞争。
原子操作去执行一个序列,可以避免一些数据竞争的未定义行为。
5.2 原子操作和原子类型
原子操作是一类不可分割的操作。
- std::atomic_flag 最简单的标准原子类型
设置/清楚两种状态;
必须被AUTOMIC_FLAG_INIT初始化。
三个操作:销毁、清除clear()、设置test_and_set()。
test_and_set()会检查atomic_flag的存储状态,若已经set就直接返回true,若没有set,就返回false。
基于atomic_flag实现自旋互斥锁例子:
// 主要实现mutex的lock(),和unlock()操作
class spinlock_mutex{
std::atomic_flag flag;
public:
spinlock_mutex():
flag(ATOMIC_FLAG_INIT){}
void lock(){
while(flag.test_and_set(std::memory_order_acquire));
}
void unlock(){
flag.clear(std::memory_order_release);
}
}
// spinlock相对于mutex的区别是:
// spinlock: 线程会等待,适合操作时间短的
// mutex: 线程不会等待(会挂起还是执行别的?)。适合操作时间长的任务加锁。
// 其中,注意内存模型的传入!release-acquire模型保证了先后顺序。
- atomic类型都不允许一个对象赋值另一个对象。因为拷贝构造和赋值构造都会去操作读前者,写入后者,这是两个独立的对象,就涉及道两个独立的操作。
- std::atomic操作
store()
load()
exchange() 读改写操作
compare_exchange_weak()/compare_exchange_strong()
is_lock_free()
fetch_and()
fetch_or()
fetch_sub()
fetch_add()
CAS:compare and swap 比较/交换
load()是从内存带存储器,store()是从存储器道内存,exchange()是读-改-写操作;
读改写也是CAS操作,比较atomic类型的存储值和给的预期值是否相同,同则存预期值,不同则更新原子变量值为期望值;
- 内存顺序 6种
- std::atomic 指针运算
有的有操作:fetch_add()和fetch_sub(),在存储低之上做原子加法,为+=,-=,++提供封装。
看个例子:
class Foo{};
Foo some_array[5];
std::atomic<Foo*>p(some_array);
Foo* x = p.fetch_add(2);
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1);
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);
// 例子中fetch_add和fetch_sub都会返回原指针
// 交换-相加 操作
// p里面存有add2以后的值,load()读取;
- std::atomic<T>模板
T必须具有拷贝赋值运算符。能使用memcpy()做拷贝,还可以使用memcmp做比较。保证CAS的工作。
5.3 同步操作和强制排序
比在在常规的检测v.empty()然后操作v的情况,此时可以使用原子操作去做标志检验符。?但这里感觉普通类型也可以啊。。
写操作发生在读操作之前。
-
同步发生
-
先行发生
-
原子内存顺序
- memory_order_relaxed
一个变量的读写操作是原子的,但是不同线程之间对改变量的访问先后顺序不保证,可能乱序;同一线程的不同变量也可能是乱序的(?); - memory_order_consume
- memory_order_acquire
修饰一个读操作,表示本线程所有后面有关此变量的内存操作都必须在此条原子操作之后执行。 - memory_order_release
修饰一个写操作,表示本线程所有后面有关此变量的内存操作都必须在此条原子操作之后执行。 - memory_order_acq_rel
同时包含acquire和release - memory_order_seq_cst (default)
序列一致模型,对所有线程进行全局同步;
- memory_order_relaxed
-
分为1自由序列
个人理解就是操作可能乱序 -
2345获取释放序列 acquire-release模型
相当于acquire前同步,release前同步,在此操作之前此线程的读写不许乱序。 -
6排序一致序列;
任何操作都是acquire-release的,所有线程上。 -
线程没有必要保证一致性;
-
释放队列与同步
-
栅栏
-
memory barries 属于全家操作,可以影响其他原子操作。
-
自由操作可以使用编译器或者硬件的方式,在独立的变量上自由的进行重新排序。使用栅栏就会强制操作的完成顺序;
9.高级线程管理
9.1 线程池
- 简单线程池
- 任务队列(放函数或数据块)
- 线程队列
- 实现submit,向线程队列提交任务;
- 实现worker_thread,线程队列的函数全是他,它会取任务队列数据执行,没有数据就休眠;
- 要以线程安全队列做载体
小结:这种设计,先放置好线程池,然后任务就可以加入到任务池中了,然后各个线程都执行着工作函数,有任务就执行,没任务就休眠,总之线程池里的线程都会抢着去取任务来执行。
问题:适用简单情况,特别是没有返回值的任务。可能出现死锁。
-
进阶1:等待提交到线程池任务
- submit提交任务后,要返回一个期望,期望值就是所需要的返回值,
- 封装了任务,自定义类对package_task封装
问题:任务相互独立。但当任务有依赖关系时,不行。
-
进阶2:等待依赖关系
- 比如实现快排并行计算时,当前线程需要子线程的任务返回,最后把数据串起来,当前线程才返回;
问题:多个线程修改一组数据,有损性能
- 比如实现快排并行计算时,当前线程需要子线程的任务返回,最后把数据串起来,当前线程才返回;
-
进阶3:避免队列的任务竞争
- 各个线程有自己的任务队列,没任务时就从全局任务表取任务。
问题:各个工作队列分工不均,可能一个线程拿多了,但别的可能却没事干
- 各个线程有自己的任务队列,没任务时就从全局任务表取任务。
-
进阶4:窃取任务
- 要实现一个窃取队列,能给来窃取的线程弹出推送任务。