1. 多线程编程实际上是一种"异步"编程。实现异步编程也不一定非得多线程,使用协程也可以进行异步化,而且协程拥有高的执行效率。
2. 在界面编程中,任何一个方法处理时间超过50ms,都应该考虑异步实现,否则将带来糟糕的用户体验。
3. 在单线程程序中,同一时刻线程只能干一件事,所有的任务都有序地执行,业务逻辑是连续的。
4. 多线程可以利用多核CPU,在物理层面上进行并行计算。协程实现的异步化,本质上是在同一个线程内部执行的长跳转(类似于longjump),在单CPU上切换,无法利用多核实现真异步。
5. 多线程程序,业务逻辑上是割裂的,因为想要将整个业务切割成多个部分放到不同的CPU上处理。这种割裂具体体现就是任务的回调函数,回调函数执行的部分就是被切分出来的剩余业务逻辑。
6. 线程之间的数据传送应该是单向的,使用任务队列进行解耦。每个线程应该只关心自己的那部分,如果业务逻辑还需要切分,则将剩余业务逻辑继续切割出去,不要试图将执行结果回送,这样会导致线程之间业务逻辑出现耦合。
锁的问题
多线程编程,遇到资源竞争的问题,解决方法是加锁保护。但是如果锁使用不当,会造成死锁。如何避免死锁:
1. 增大锁粒度(并发性能下降)
2. 死锁检测(c++死锁模拟与检测-CSDN博客)
3. 使用原子变量(无锁化编程,典型的无锁队列无锁队列Disruptor使用笔记_discruptor-CSDN博客)
变量可见性问题
如果在写数据之后,即便其他读线程读到旧值也不影响业务正确性,则可以不用满足实时可见性(但需保证读操作和写操作满足原子性,不能出现脏读和脏写)
static int globalIdx = 0;//
int func()
{
int tmpId = globalIdx;//读
if((tmpId % 10) != 0)
{
processA();
}
else//严格每处理10次A流程之后,处理一次B流程
{
processB();
}
globalIdx++;//写
}
/*
如果某个线程调用func时,发现满足A流程处理条件,则进入A处理流程,并对globalIdx加1.
如果下一个线程调用func时,没有感知到globalIdx已经变化,还满足A流程处理条件,则继续处理
A流程,这样就违背了每处理10次A之后,处理一次B的业务逻辑需求。为了解决该问题,需要让globalIdx
满足多线程可见性(加volatile)。
*/
变量原子性问题
如何保证数据读写的原子性。对于简单数据类型(int, float,bool等),c++ 11提供了相应的原子变量,可以保证其读写的原子性(注:如果是32位cpu, 执行4字节以内数据的读写,一条指令就够,通常也可以保证读写原子性, 不过前提是该数据地址没有跨越缓存行(内存对齐),但是过于依赖硬件特性,写出来的程序兼容性会很差,所以该用原子变量的地方,老老实实使用原子变量, 况且原子变量已经保证了可见性,也不需要加volatile)。
此外,c++ 11的std::atomic<>类模板不仅是一套特化的类型,其作为一个原发模板也可以对自定义的类型创建对应的原子类型变量。但是自定义的复杂类型作为原子模板参数,有诸多限制,例如不能有虚函数表,必须使用编译器自动合成的拷贝赋值运算符(成员变量也同理),还需要具有按位可比性等,所以通常实现复杂类型的读写原子性需要借助互斥锁mutex完成。