helios741 / myblog

觉得好请点小星星,欢迎有问题交流(issue/email)
109 stars 21 forks source link

浅谈python、javascript以及go语言面对并发的处理 #59

Open helios741 opened 5 years ago

helios741 commented 5 years ago

本文针对不同的语言分析它们对并发的支持,读者需要有操作系统的一些概念,如用户级线程和内核级别线程、进程和线程的区别,这些我在预备中只是会做一些相关的说明,保证能顺利理解此文。

0、预备知识【可选】

一、CPU的抢占

先来看看操作系统的进程转换图: image

先不用管图中线条的颜色。

首先明确几个问题

什么时候会发生CPU的抢占

当CPU的中的一个时间片到期之后就会让出CPU,这个时候CPU就会根据调度规则从就绪队列中取出一个任务运行。

举个例子,就好比下面的过程 image 皇帝可能自身有个翻牌的规则,比如根据高矮胖瘦或者技术,翻一个最优妃子的牌,当然不同的皇帝可能翻牌的规则可能不一样,可能新皇帝比较懒,认为谁来的早谁伺候就行啦。

也就是转换图中的蓝色线就表示发生抢占。

什么时候会发生CPU的调度

当CPU空闲的时候,CPU还是满载就hang住了(貌似是废话哟)。

对应转换图中绿色红色就会发生CPU的调度。

所有的调度会是抢占式调度么

CPU的调度也有两种,抢占式(对应转换图中的绿色线)和非抢占式调度(对应转换图中的红色线)。

抢占式调度和非抢占式调度发生在什么时候

转换为就绪状态就会发生CPU的抢占,也就是对应图中绿色的线(创建一个新进程的时候也会发生抢占,可能那个时候CPU可能正在忙,并不会发生抢占)。 下面分析一下抢占式的状态转换:

非抢占式调度就比较好理解了:

二、什么是并发

上面说完了什么是CPU的抢占。其实CPU的抢占过程就是并发的过程,并发就是指的能支持多任务,但是不一定是同一个时刻执行的,图行如下: image

这就像如果是单核CPU的电脑的话,我们可以一边写代码一边听歌甚至一般打开着浏览器,这些对于使用者看来是同时执行的,但是归根到底还是一个CPU,因为CPU的切换的够快,所以你看不出来。

还有个并行比较容易和并发混淆,并行和并发的区别主要在同时上, 并行指的是能同时运行多个任务,并发指的是能运行多个任务,不一定同时。

知乎上有个很好的回答:并发与并行的区别?

三、进程

进程可以理解为是程序的动态表示,程序代码放在磁盘上就是静态文件,程序运行起来就是操作系统中的一个进程。

进程是操作系统分配资源的最小单位,比如我们任意语言的一个helloword程序,总会有变量,所以这就要有内存资源,以及对程序分配的堆栈信息。

四、内核线程和用户线程

线程也会分为内核线程和用户线程。 内核线程是直接由内核进程启动的进程(没错,线程是进程,是不是要迷糊了咯)。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。

对于用户线程来说,对于内核是不感知的,用户级别的线程是由使用者控制的。用户通过线程控制库来控制用户线程的创建,撤销以及同步等功能,可以看下图,理解一下:

image

系统线程也叫轻量级进程,对于内核来说,进程是调度的最小单元。 对于多核计算机来说,进程可以被调度到不同的核上,如果是单核就会有抢占的情况。

1、单线程的javascript

2、”假“多线程的python

先不要在意我为什么说python的多线程是“假”的。我们先来看看一个同步的程序:

import time

def count(n):
    for i in range(n):
        n += 0
    print("Done")

start = time.clock()
count(50000000)
end = time.clock()
print(end - start)

输出为:

Done
4.205902

然后我们再来看看高大上的多线程的版本是什么样子的。

import time
from threading import Thread

def count(n):
    for i in range(n):
        n += 0
    print("Done")

start = time.clock()
n = 50000000
t1 = Thread(target=count, args=[n // 2])
t2 = Thread(target=count, args=[n // 2])
t1.start()
t2.start()
t1.join()
t2.join()

end = time.clock()

print(end - start)

再来看看运行的结果

Done
Done
6.093098

现在知道我为什么要说他是假的了吧,多线程竟然比单线程还要慢,可能有经验的程序员就会说了,你这个肯定是在单核的计算机上跑的,由于CPU切换任务导致的耗时。 其实这句话可以说的也对也不对,因为我是在四核的MAC上跑的,就是30核的机器也是类似的结果,但是python的多线程是真的确确实实跑在一个核上的(针对cpython解释器)。

这个时候就必须扯一扯python的全局解释器锁(GIL)了,它就是一个全局锁,为了保证一段时间内只有一个线程去执行(加锁)。python解释器为什么要这么做呢,主要原因有两个:

  1. 一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition)
  2. 二是因为CPython大量使用C语言库,但大部分C语言库都不是原生线程安全的(线程安全会降低性能和增 加复杂度)。

当然python也是能够通过多进程指定并行的去执行:

import time
import multiprocessing

gend=1

def count(n, t):
    start = time.clock()
    for i in range(n * t):
        n += 0
    end = time.clock()
    print(end - start)
    global gend
    gend = time.clock()
    print("Done")

n = 5000000
pool = multiprocessing.Pool(processes = 3)
gstart=time.clock()
for i in xrange(1, 4):
    pool.apply_async(count, (n, i))
pool.close()
pool.join()
print("all run time: {0}".format(gend - gstart))

输出结果如下:

0.406385
Done
0.727774
Done
1.021318
Done
all run time: 0.928852

是不是以为就完了呢,还记得javascript中的事件循环么,因为对于强io的情况过于高效了,所以在python中也支持了。

3、天生就支持并发的Go语言

4、依赖jvm的java

参考