一步一步写线程之三多线程设计开发

一、多线程开发

多线程开发在实际应用中是非常多的。正如前面分析所说,只要写一个线程,就可以认为是多线程开发。多线程开发没有最优,只有在指定场景下的最合适。没有任何一个模型可以包打所有的场景,所以在网上经常可以看到一些多线程的库需要指定应用场景或者根据应用场景来配置相关的参数。
多线程开发,其实就是综合运用多线程来处理任务,这些任务可能是计算也可能是数据存储等等。因而,在这个过程中为了保证任务执行的安全、可靠和最终结果的正确性,就必须使用一些同步机制;同时为了更好的发挥多线程的作用,就需要及时的调度相关的任务,这就又需要一些消息机制和队列机制。在一些复杂的IO操作中,为了及时的回传结果达到异步的效果,可能还需要回调机制。不管是使用传统的回调函数异或c++11后的future机制等,其实目的都是一样的。
而这些方法统一到一个工程综合运用,就考验了一个设计者和开发者的综合实力,这也是为什么在面试时,这一块是经常问到的主要原因。毕竟能把多线程搞得门儿清的,特别是复杂的状况下的设计开发都清晰明白的程序员,水平一定不低,这可不是靠背背面试答案,刷刷力扣就能得来的经验和抽象思维能力。

二、多线程面对的基本场景

在实际的开发场景中,多线程常用到的应用场景分为以下几种:
1、CPU密集型
即在整个应用程序的运行过程中,基本或者很少操作内存,IO操作更少,或者说大部分的任务都由CPU来实现。比如一些科学计算、图像处理等等。这种情况下,多线程的开发设计相对来说没有太高的难度。它更象是一个个完全独立的线程,多线程间的互相操作很少。即使故意设计成多线程之间的数据交换同步,只会降低效率而不会增加效率。当然如果实际情况是这种情况就另当别论了。
举一个例子,计算机有200个核心,恰好有300个科学任务需要处理,每个任务需要计算时间长达十个小时。那前面提到的什么消息处理、任务调度(指应用层面的,非OS)的意义基本不存在了。换句话说,从宏观看,这仍然是某种意义上的串行。因为不可能在200个核心上轮换调度300长达十小时的科学任务,这个没有意义,只会增加处理时间。毕竟上下文的切换代价也是很大的。不如直接一个个的执行完成后再进行下一轮的任务工作。

2、IO密集型
IO密集型其实在实际应用中是比较常见的。比如互联网应用中的大数据处理、网络通信、服务端开发等等。IO密集型指的是在实际的工作中,对硬件IO的任务是主流,象网卡、磁盘、PCI等等。这种任务一般来说有一个显著的特点,即CPU的处理能力远大于一个量级以上的IO操作。或者换句话说,CPU可以支持成千上万甚至更多的IO任务。
但这里有一点仍然需要说明,如果IO仍然是长时间被满负荷占用,对设计其实要求也相对低了下来。不过这种情况极为特殊,很难遇到。一般来说,IO密集型的任务都是断续进行的。比如网络通信,除了传一些较大的文件时,可能需要单独处理,其它情况下都是IO动作很短的时间就结束了。最常见的就是IM,大家聊天基本也就是几十个字,即使经常使用语音和视频,它的数量也很有限(都经过算法处理),距离IO瓶颈还很远,更谈不上CPU的瓶颈了。
这种情况下,设计就非常有意义了。各种任务调度、并发控制,数据同步转发,消息传递和异步回调等等,都是在这种情况下大展伸手的。

3、混合密集性
混合密集性其实就比较少见,而且即使见到也属于分时间段的CPU密集型和IO密集型,同时是二者的,可能只有在一些大型的数据中心才可能遇到。这个可能对设计的要求更严格,但可能对并行计算要求更高,并发反而成了其次的了。

4、非密集型
这种是最常见的实际场景,不管是CPU计算还是IO通信都是阵发的,而且也不会有多大。比如在公司部署一个私有的IM,大公司也就几千人,而且还不一定同时在线。一个公司的网站,每日的点击量更是有限。即使是一些小型电商网站也不会遇到长时间的高并发的情况(正常情况下)。
此时,对设计的要求是一个渐进的推进的,可以从一个简单的多线程设计到一个线程池再到并发框架再到消息、队列等的调度等等,这也是大多数程序员开发者经历的一个技术成长的过程。

三、任务和数据处理

1、内存数据并发
多线程任务的第一个难点其实并不是上面提到的什么消息、任务队列和调度以及异步什么的。其实很多程序员遇到的第一个难点甚至是其开发生涯的全部中,只有这一个,那就是内存数据的同步。
多线程对内存数据的操作分为两大类即多线程读内存和多线程写内存。这两个可以同时存在,也可以只有其中一个。当然,某一种为主也是很广泛的。一般来说,多线程对内存的读相对是安全的。但是多线程写和读写并发操作时,特别是对某些共享数据操作时,安全性和可靠性的设计就非常重要了,这也是经常提到的多线程同步。
这里就存在了一个问题,如何让多线程的运行效率更高的前提下保证数据的安全和整个程序的稳定性。

2、IO并发
IO并发的难点在于IO的速度和CPU以及内存的读写速度差着好几个数量级。如何又更好的平衡CPU、内存和IO操作,这才是重点。在不同的应用场景下,可以采用一些缓存、队列,消息分发等等方式来处理这种不匹配的情况。尽量做到整体的平衡和安全。

3、IO高并发写
这里重点说一下高并发写入。在一般情况下,实际应用的场景大多还是以高并发的读为主,并发写入一般都是临时的。比如常见的多线程多任务下载,一般来说也不会持续到硬件撑不住的情况。毕竟下载软件都会监控当前硬件的实际情况并根据情况来自动调节实际的写入机制,搞死PC,对大家都没有好处,对吧。所以下载软件的设计一定是在整体硬件许可的情况下有较大冗余和风险控制。
但在某些数据中心或者大模型的计算中,需要大量的数据存储,那么这也是有一些策略的,从多线程的角度上来看主要有:
使用异步操作,动态控制写入的速度和数量(动态控制线程数量和写入数量);增加写入前的缓存(比如Redis);批量写入,同时根据情况扩大硬件写缓冲区。如果可以动态扩展,比如云上,还可以增加自动扩展机制和并行分片技术,这就超出了多线程编程的范围了。

4、其它
其它的基本就比较简单了,一般就是简单的数据同步或者有个异步回调或者消息、条件触发之类的开发,这也是广大程序员最多的多线程应用场景。

四、多线程的设计

多线程的设计其实仍然是遵循由简单开始,逐步进行升级优化的方式。有两个重要的原则:
1、设计的开始不要复杂
这个非常重要,很多架构师或者程序员,都喜欢一开始把所有的情况都考虑到,然后不顾一切的都设计进去。各种高大上的设计方法、设计思想和理念以及设计手段都一股脑的堆了上去。这样做的缺点非常明显,
首先是设计复杂性提高意味着设计和实现都显著的提高了难度;其次是开发成本的大幅攀升;第三是对设计人员的素质能力要求以及核心开发人员的能力要求相对苛刻;最后,复杂的设计和开发往往意味着风险的增大(Bug的增多)。
所以要循序渐进,根据实际的应用不断的再迭代开发,这才是一种相对较好的设计思想和理念。

2、不能过早展开对设计的优化
这个重点针对的是开发人员和一些担任开发任务的架构师,他们往往在开发过程中,急于实现一些设计架构,很早的对一些实现进行抽象或者应用更优秀的多线程调度算法。这本来是无可厚非,甚至应该是鼓励的。
但在实际中,过早的优化往往会出现很尴尬的情况。举个经历过很多的实际工作案例,抽象的东西根本用不到;更好的调度算法导致了复杂性的增加,引入了不少的BUG。最好的方式是在实际工作中遇到瓶颈或者可预见的短期内会有一些瓶颈再做优化。当然,如果程序稳定交付后,为了升级,可以逐一的对一些模块进行抽象、分层。对一些接口进行优化,将同步改为异步等等。而不要同时进行大规模的优化,这种风险是非常大的,特别是在多线程的状态下。

3、多线程设计涉及到的一些技术和问题
在多线程设计中,首先遇到的就是数据同步问题,这里面就涉及到锁的粒度的设计、偏向锁、内存对齐和伪共享等情况;其次是内存模型,不同的内存模型对原子操作的处理和对内存数据的处理也是有着很大的不同的,这个在涉及到跨平台或者跨设备(指的是不同的硬件架构设备)设计中非常重要;再次,需要熟悉最好是深入源码级别的了解同步的实质、OS对线程的和进程的调度方式(这样可以更优雅的设计出异步机制),特别是在不同的操作系统的差异性上要清楚明白;还有,多线程的内存分配和回收中,如何提高效率,保证安全稳定也非常重要,比如前面分析过的不同的内存分析库ptmalloc、tcmalloc和jemalloc,其实就是对内存的一种倾向性的管理。这也意味着多线程的设计不能单纯的求大求全,而要针对具体的应用进行量身打造。
也就是说,没有一种设计是可以包打天下的。
同样,线程池、内存池和线程模型(HA/HS,LF),CSP和Actor并发模型等等技术都需要如何稳妥的配合应用设计。

4、多线程的安全数据结构
在多线程开发中,往往要进行一些线程间的数据共享,除了使用同步机制直接处理外,也可以抽象出线程安全的数据结构,在一些库或者开源框架中,都有实现。但是在STL中,大多数的数据结构都是非线程安全的。
前面也提到过使用CAS实现一些无锁的多线程安全的数据结构,都可以在实际的场景中根据情况进行应用。

5、多线程间的通信
多线程间除了数据共享同步,另外一个重要的就是线程间协调通信,按要求完成任务。举一个简单的例子,有十个线程,其中三个线程用来读入数据,三个线程用来计算,两个线程用来存储,一个线程用来管理上述的线程。那么管理线程需要决定什么时候启动或结束计算线程并发结果分发到写入线程等等。
这时,线程间的通信设计就非常重要。可以根据应用场景采用条件变量、事件、消息或者管道以及Socket等等来进行事件(触发机制)、消息(实时性低)、控制命令(实时性高)等等的传输。

6、其它
在书籍和资料中,还有一些多线程的相关的设计模式,这些大家都可以参考一下,还是那句话,没有最优秀只有最合适的设计。同时,新技术也在不断的涌现,闭门造车在当下肯定是不合适的。所以,紧紧跟进新技术的推进肯定是一个优秀设计者必备的技能。

这里只讲到多线程的设计开发,更高层次的抽象,如DDD、六大原则等都不划到这个设计之中。当然在实际的设计开发中,不可能完全的把这些都切割开来,但是在此处为了突出重点,还是要人为的划出一个界线,否则就没办法聚焦了。

五、总结

多线程的很多情况,其实就是生产者和消费者的问题,这在后面的文章中会在线程模型中进行详细的分析说明。其实正如《倚天屠龙记》中太极拳一样,多线程的设计开发本身就是一种动态开放的方式。没有必须如何如何才可以的绝对性条件,如果有,说明设计一定有问题。
学习并掌握多线程的各种知识和设计方法,在实际情况中,综合运用这些技能,有针对性的解决主要应用痛点,这样设计出来的多线程程序,就很可能是当下最合适的。

你可能感兴趣的:(C++,C++11,c++)