并发编程基础知识二 线程和进程

一、知乎 线程和进程的区别是什么?

首先来一句概括的总论:进程和线程都是一个时间段的描述,是CPU工作时间段的描述。

下面细说背景:CPU+RAM+各种资源(比如显卡,光驱,键盘,GPS, 等等外设)构成我们的电脑,但是电脑的运行,实际就是CPU和相关寄存器以及RAM之间的事情。

一个最最基础的事实:CPU太快,太快,太快了,寄存器仅仅能够追的上他的脚步,RAM和别的挂在各总线上的设备完全是望其项背。那当多个任务要执行的时候怎么办呢?轮流着来?或者谁优先级高谁来?不管怎么样的策略,一句话就是在CPU看来就是轮流着来。

一个必须知道的事实:执行一段程序代码,实现一个功能的过程介绍 ,当得到CPU的时候,相关的资源必须也已经就位,就是显卡啊,GPS啊什么的必须就位,然后CPU开始执行。这里除了CPU以外所有的就构成了这个程序的执行环境,也就是我们所定义的程序上下文。

当这个程序执行完了,或者分配给他的CPU执行时间用完了,那它就要被切换出去,等待下一次CPU的临幸。在被切换出去的最后一步工作就是保存程序上下文,因为这个是下次他被CPU临幸的运行环境,必须保存。

串联起来的事实:前面讲过在CPU看来所有的任务都是一个一个的轮流执行的,具体的轮流方法就是:先加载程序A的上下文,然后开始执行A,保存程序A的上下文,调入下一个要执行的程序B的程序上下文,然后开始执行B,保存程序B的上下文。。。。

========= 重要的东西出现了========

进程和线程就是这样的背景出来的,两个名词不过是对应的CPU时间段的描述,名词就是这样的功能。

进程就是包换上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文

线程是什么呢?进程的颗粒度太大,每次都要有上下的调入,保存,调出。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。这里a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。

到此全文结束,再一个总结:进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。

二、阮一峰 进程与线程的一个简单解释

进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

关于锁,可以参考面试必备之乐观锁与悲观锁

(1)悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

(2)乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

(3)两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

三、单核多线程

参考对于单核cpu而言,开多线程的目的难倒只能是为了防止阻塞么?

以下是一些单核cpu多线程的疑问,求解答(都指单核)。
1.如果一个进程有n个任务要处理,因为终究是在一个cpu上跑,所以这n个任务在一个线程还是多个线程上跑,执行的总时间是一样的(多线程,线程切换可能更浪费时间)?
2.是否进程开多线程就能抢到更多的cpu时间,python这种带GIL的估计是没戏了,那么java呢?
3.自己抢到更多cpu,机器上的其它程序不就cpu时间少了么?是因为cpu大部分时间都是空闲的,不怕抢?还是因为在做应用层开发的时候,是不用考虑其它程序能不能抢到cpu时间的。
4.一个进程所有线程能抢到的时间片总和是有最大值吗?一个线程一次能拿到多长的cpu时间?
综上,我的最大疑问就是:对于单核cpu而言,开多线程难倒只能防止阻塞么?

(以下回答均针对单核CPU)

问题1概括下来就是很多人喜欢争论的多线程究竟能不能提高性能?首先,回答是“能或者不能”。至于“不能”你已经理解了,那么我来说说为什么多线程“能”提高性能。要知道一个作业可不总是CPU密集型的,必然穿插着大量的IO调用在其中。而IO的一个特性就是阻塞等待。这个阻塞等待的时间消耗往往是远远大于线程切换所消耗的时间的,如果你要访问10个url获取接口内容,假如一次http访问平均阻塞时间大概是1s,那么你是一个一个的线性访问快还是10个线程访问快?相信不用算也知道多线程肯定更快。最后就可以得出结论,多线程在CPU密集型的作业下的确不能提高性能甚至更浪费时间,但是在IO密集型的作业下则可以提升性能(或者更准确点说叫平均响应时间)。

问题2,进程是最小作业单元,跟进程内开多少线程都无关,CPU对进程的调度是统一的。所以多线程无法促进进程被CPU青睐。python的GIL也是只在CPU密集型的作业下显现的,通常的业务充斥着大量的IO,所以如果你不是做科学计算,那么放心大胆的使用多线程吧。

问题3,4,虽说操作系统有自己的调度策略,比如争抢,时间片轮转,但是用户态进程仅仅想通过自身应用级的代码实现如多线程等手段企图加大自身的CPU调度权重是不行的,不过自身的线程是可以实现优先级设置的。也就是说CPU给你整个进程的资源是有限且无法更改的,但是这些资源如何分配你是可以参与的,比如设置线程的优先级,也只是参与不能主导CPU在某个线程的调度时间,这个是无法控制的。跟当时的系统压力有关。

综上,你的问题提到了“阻塞”,这是服务端编程永恒的经典话题。不管是多进程,多线程,还是协程,大多都是致力于解决IO问题,说白了都是怎么样把阻塞变成非阻塞的手段。

四、多核情况

线程进程是怎样使用多核的中,作者做了一个测试:多核时,一个线程是始终由一个cpu核运行还是每个cpu核都会运行该线程呢?测试结果是多核cpu情况下,一个线程不是由某一个核一直执行完成的。

单核多线程与多核多线程中,单核多线程指的是单核CPU轮流执行多个线程,通过给每个线程分配CPU时间片来实现,只是因为这个时间片非常短(几十毫秒),所以在用户角度上感觉是多个线程同时执行。

多核多线程,可以把多线程分配给不同的核心处理,其他的线程依旧等待,相当于多个线程并行的在执行,而单核多线程只能是并发。

这里关于并发和并行的区别,可以参考并发编程基础知识一 并发和并行

五、多线程有什么用?

这么解释问题吧:
1。单进程单线程:一个人在一个桌子上吃菜。
2。单进程多线程:多个人在同一个桌子上一起吃菜。
3。多进程单线程:多个人每个人在自己的桌子上吃菜。

多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。

1。对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。

2。对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。

关于开销,知乎原回答有更多展开。也可以参考多线程还是多进程的选择及区别

六、java多线程 synchronized节选一部分作简介
  • jvm的可见性:当一个共享变量在多个线程工作内存中都存在副本时,如果一个线程修改了这个共享变量,其他线程能够看到这个修改后的值,即可见性。
  • jvm的有序性:假如有个共享变量x=10,线程a执行x=x+1,线程b执行x=x-1。
a的执行顺序:
1 从主存中读取变量x副本到工作内存
2 给x加1
3 将x加1后的值写回主 存
b的执行顺序:
1 从主存中读取变量x副本到工作内存
2 给x减1
3 将x减1后的值写回主存
实际上顺序有可能是这样的:
1:线程a从主存读取x副本到工作内存,工作内存中x值为10
2:线程b从主存读取x副本到工作内存,工作内存中x值为10
3:线程a将工作内存中x加1,工作内存中x值为11
4:线程a将x提交主存中,主存中x为11
5:线程b将工作内存中x值减1,工作内存中x值为9
6:线程b将x提交到中主存中,主存中x为9

synchronized关键字可以保障一个线程计算时,共享变量处于上锁状态:

1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁

volatile关键字,开销比synchronized要小,但是它只能保证可见性,无法保证有序性。volatile会直接操作主存,没有线程对工作内存和主存同步。比较适合直接给共享变量赋值这种操作。

七、优化

1.大多数游戏做多核优化的难点是什么?

推荐阅读更多精彩内容