6、并发并行与同步异步

并发并行

并发:是指系统具有处理多个任务(动作)的能力,通过CPU切换来完成并发效果;

并行:是指系统具有同时处理多个任务(动作)的能力,如果电脑有多个CPU那么在同一时刻发生的任务就称之为并行;

并行是并发的一个子集;

同步异步

同步:当进程到一个IO操作的时候(需要接收外部的数据的时候),如果要决定等那就是同步;

异步:当进程到一个IO操作的时候(需要接收外部的数据的时候),如果要决定不等那就是异步,自己去执行其他的任务,直到数据接受完成之后再回来处理;

异步效率更高,因为同步的等待是没有意义的;

打电话就是类似一个同步的操作,打出电话要一直等待别人接听,而发短信就相当于一个异步操作;

效率对比

传统方式

#!/usr/bin/env python

# -*- coding: utf-8 -*-

# @Time : 2018/4/14 11:07

# @Author : CaiChangEn

# @Email : [email protected]

# @Software: PyCharm

import time,threading

def add():

sum=0

for i in range(1,100000):

sum+=i

print(sum)

def mul():

sum=1

for i in range(1,100000):

sum*=i

print(sum)

start=time.time()

add()

mul()

print('总共用时%s' %(time.time()-start))

# 结果

# 4999950000

# 282422940796034787429...

# 总共用时8.341477155685425

线程方式

#!/usr/bin/env python

# -*- coding: utf-8 -*-

# @Time : 2018/4/14 11:07

# @Author : CaiChangEn

# @Email : [email protected]

# @Software: PyCharm

import time,threading

def add():

sum=0

for i in range(1,100000):

sum+=i

print(sum)

def mul():

sum=1

for i in range(1,100000):

sum*=i

print(sum)

start=time.time()

t1=threading.Thread(target=add)

t2=threading.Thread(target=mul)

l=[]

l.append(t1)

l.append(t2)

for i in l:

i.start()

for i in l:

i.join()

print('总共用时%s' %(time.time()-start))

# 结果

# 4999950000

# 282422940796034787429...

# 总共用时8.40548062324524

结果

可以看出效果不是很明明显,但是对python2.7版本的来讲区别是非常大的,串行的比较快,因为使用多线程的情况下是使用多个CPU进行计算,多核因为GIL的原因是没有用上的,GIL(全局解释锁);

GIL(全局解释锁)

Python本身就是一个解释性的语言,在运行解释器的过程中,为了线程安全加了一把锁,而加了这把锁就造成了一个结果就是在同一时刻只有一个线程在被运行,无论你启多少个线程,有多少个cpu,python在执行的时候会淡定的在同一时刻只允许一个线程运行;

GIL是什么

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

好吧,是不是看上去很糟糕?一个防止多线程并发执行机器码的一个Mutex,乍一看就是个BUG般存在的全局锁嘛!别急,我们下面慢慢的分析。

为什么会有GIL

由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。

Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。

慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?

所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。

GIL的影响

从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。 那么读者就会说了,全局锁只要释放的勤快效率也不会差啊。只要在进行耗时的IO操作的时候,能释放GIL,这样也还是可以提升运行效率的嘛。或者说再差也不会比单线程的效率差吧。理论上是这样,而实际上呢?Python比你想的更糟。

下面我们就对比下Python在多线程和单线程下得效率对比。测试方法很简单,一个循环1亿次的计数器函数。一个通过单线程执行两次,一个多线程执行。最后比较执行总时间。测试环境为双核的Mac pro。注:为了减少线程库本身性能损耗对测试结果带来的影响,这里单线程的代码同样使用了线程。只是顺序的执行两次,模拟单线程。

详解点击此处

任务分类

IO密集型:在多线程的情况是有效果的,可以通过IO切换的时间来进行CPU动态分配,sleep等同于IO操作,因为此时我们的CPU是不工作的;

计算密集型:在多线程的情况是没有效果的,只能通过CPU时间片来决定分配的任务;

结论

对于IO密集型的任务,我们Python的线程是有意义的,因为如何遇到IO操作的时候,CPU是不工作的,当CPU不工作的时候通过多线程的机制就会将CPU转交给其他的线程,如果是计算密集型的任务多线程是没有太大的意义的,因为此时的CPU是没有休息间隔的,CPU会直接运行到CPU最大的时间片之后才会进行切换;

计算密集型的,Python就不推荐了

虽然进程切换消耗大,以及通信的问题,那么以后我们可以通过协程来解决,它主要解决的问题也是IO密集型的;


分享到:


相關文章: