设计模式-并发编程
目录
并发编程是现代计算机程序的基本构成部分之一。本文将总结常见的相关内容和常见问题。
并行和并发
- 并发(Concurrency):具有一段时间内同时处理多个任务的能力。
- 并发和顺序(Sequential)相对应。顺序只能做完一件,再做另一件。不能都处于运行阶段。
- 并行(Parallelism):具有在一个时间点上同时处理多个任务的能力。
- 并行和串行(Serial)相对应。串行只能在一个时刻处理一个任务。不能同一时刻同时执行多个任务。
- 并行实际上是并发的子集。本文以并发为主要总结目标。
并发模型
- 从内存共享角度:
- 基于内存共享的:数据直接在内存种共享,临界区需要使用同步控制,有锁或无锁
- 基于消息传递的:进程间通讯或者内存映射
- 基于消息传递的模型:
- Actors模型:强调通信的工作实体,不同的Actor之间通过消息发送和接收进行直接通信,清楚的知道消息的来源和目的地,除此之外,不能再对对方暴露任何和自身属性、行为相关的接口。Actor之间也没有接触点,其消息发送是异步且解耦的,接收Actor可以根据自身状态决定是否接收等。理论上需要一个无限大的区域进行缓冲。代表语言/库有:Erlang/Akka。
- CSP(Communicating Sequential Processes)顺序通信模型:强调通过信道传输消息,发送者和接收者并不知道对方是谁。但CSP的消息交换存在同步的接触点(即读取和写入的位置)。代表语言有:Go。
Dont’t communicate by sharing memory, share memory by communicating. (R. Pike)
常用并发编程模式
常见问题
- 伪共享/假共享(False Sharing):先看一段受害者代码
void f(unsigned __int64 begin, unsigned __int64 end, unsigned __int64 &result) noexcept { result = 0; for(unsigned __int64 i = begin; i < end; ++i) { result += i; } } void paraAdd(unsigned __int64 begin, unsigned __int64 end, unsigned __int64 &result) { unsigned int core = thread::hardware_concurrency(); unsigned __int64 step = (end - begin) / core; vector<thread> threads; unsigned __int64 *results = new unsigned __int64[core]; for(unsigned int i = 0; i < core; ++i) { threads.push_back(thread(f, begin + i * step, begin + (i + 1) * step, ref(result[i]))); } for_each(threads.begin(), threads.end(), mem_fn(&std:🧵:join)); result = 0; for(unsigned int i = 0; i < core; ++i) { result += results[i]; } delete[] results; }
- 这段代码看似将求和分给多个线程进行计算。但实际上,求和总用时甚至高于单一线程求全部和。
- 多核处理器之间虽然共享内存,但是由于CPU缓存(行缓存)的存在,实际上某一核修改数据时,需要花费一定的时间,才能使其他核的缓存同步。当这种同步频繁出现时。共享实际上已经失去了意义。
- 可以通过给数据填充无效字段,来避免行缓存中出现其他核会修改的数据,进而提高并发性能。但这并非最好的方案。还是应当从设计上尽量避免这类粗暴的可同时读写的大段内存共享。
- volatile并不能完美解决问题,因为这相当于放弃了缓存,性能还是会慢。