一、多线程下的资源控制
在分析多核及多CPU编程前,就不得不先回到最初的单核编程上,早期的电脑直到现在的一些简单的单片机,其实都是使用的一个CPU串行的来执行任务。就好比人们去一台ATM上取款,只能一个个的来操作,而不能说你操作一半时换另外一个人再操作ATM。但是这样就会出现一个问题,如果一个人占用时间过长怎么办,后面的人即使只想查询一下,都得等好长的时间。
最初电脑使用纸带也和上面一样,直到后来为了解决这个问题,就出现了多任务系统,直至后来出现PC,出现了现在基于抢占式的CPU时间片轮转的方式来调度线(进)程。但是在这种情况下又出现了新问题。在单核单进程的情况下,资源是独享的,想怎么使用就怎么使用,但是在多进程的情况下,不少的资源是共享的。这样,一个进度在操作一个共享资源时,另外一个进程甚至更多的进程想要操作这个共享资源时,冲突就不可避免。而这个问题,升级到多核甚至多CPU时,问题更加的突出,怎么解决这种问题呢?任务的提出是从最初的单核多进程开始的,那么就看一看从那个时候儿起,这类问题的解决方式?
二、同步机制
1、锁机制
在单核多进程中,最容易理解的,是使用锁机制。另外一种就是使用型业务分解方式,杜绝资源共享,当然这是一种比较少见的情况,更多的是尽量减少资源共享,这样还是回归到锁机制。锁是什么?锁就是一种同步原语操作(如果不清楚什么是原语,回头翻一下操作系统的相关书籍)。锁在不同的OS中的定义和使用的方式都有不同,有的只能用在线程中,有的却可以线程和进程通用。在早期的Linux中没有线程这个概念,所以对理解这个说法应该比较好接受一些。
锁有几个特点:
a、互斥性
互斥性的意思就是一方拥有,另外一方就不能拥有;反之亦然。这对多线程访问资源时,有着至关重要性。这也是锁的基本特性。当然,在OS中还有一种条件锁,如信号灯,它到达某个条件后,锁才会进行操作。
b、递归性
在学习前面的多线程时就提到过,锁有递归锁和非递归锁。递归性是指在同一线程内对锁可以在未释放的情况下再次获得,但需要注意的是,获取和释放锁必须是相同次数。显而易见,其多用于递归函数上。
c、自旋性
也就是在内核中常见的自旋锁,它是一个高速短时间锁,一般在用户态很少用。因为这个锁用不好,就会产生内核崩溃。
另外,从锁的上层应用来看,还可以分为读写锁、乐观锁、悲观锁、偏向锁、轻量锁、重量锁等等。
对锁的各种上层的应用,其实主要就是为了更好的适应各种不同的应用场景,比如读多写少的场景,用读写锁可能会效率更高一些。但反之,则可能出现效率更低的情形。
2、锁的种类
具体到不同的操作系统,锁的种类各有不同,定义也略有不同:
在Windows上一般有事件、临界区和信号量及互斥体;在Linux上一般常用的有互斥体、条件变量和信号灯;另外在不同的语言上,也有一些细节不同,比如有的提供了内存栅栏(Fence)、栅障(Barrier)等。
3、死锁
这里只简单讲一下形成死锁的几个条件:互相持有、循环等待、不可剥夺和互斥。
通过锁机制,在不考虑效率的前提下,可以保障资源在多线程之间的安全控制。当然,在多核和多CPU中也是如此。但实际情况下,效率往往是刚性需求,这就提出了很多的问题,也有了很多的解决方式,适应的场景不尽相同,但得到的结果却是一样,尽量利用计算机中的资源,特别是CPU的资源。
三、无锁机制
无锁机制,在多核多CPU中,如果业务设计得当,采用并行的方式,是可以极大的提高效率的。但任务任务最终分解,都会分解到一个单核上,而为了更好的利用单核,其实还是回归到了单核多线程的操作模型上。也就是上面提到的,共享操作中的互斥是不可避免的。而无锁机制(Lock-free)并不是没有锁,或者说,只是没有上面定义的这种概念的锁。它是使用CAS(compare and swap)这种方式来实现的。CAS通过一个循环来不断的匹配比较,利用原子操作来减少资源占用的时间。其实原子操作从某种意义上说就是一种锁,只是这种原子操作更快速,更安全。在CAS中不会产生死锁,因为总有一个会比较成功。
但是使用无锁机制,会产生另外一个问题,也就是ABA的问题,这个问题,会留在后面进行详细的分析说明。
四、总结
通过上述的分析可以明白,简单粗暴的使用锁可以安全的实现多线程甚至多核多CPU的安全资源控制,但为了追求更高的效率,无锁机制和设计上的任务分解机制,共同协作,把锁的粒度尽量减少直至达到无锁编程,这才是每个程序追求的目标。所以,提升效率不是一个简单的锁与不锁的问题,它涉及到了相关的软件、硬件、库和数据结构以及任务分解调度的方方面面。世界上本来就没有一种放之而四海而皆准的真理,编程也一样。