真正的python 多线程!一个修饰符让你的多线程和C语言一样快

让人想犯罪 __ 提交于 2020-05-02 08:30:12

> Python 多线程因为GIL的存在,导致其速度比单线程还要慢。但是近期我发现了一个相当好用的库,这个库只需要增加一个修饰符就可以使原生的python多线程实现真正意义上的并发。本文将和大家一起回顾下GIL对于多线程的影响,以及了解通过一个修饰符就可以实现和C++一样的多线程。


## 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.)

从官方的解释来看,这个全局锁是用来防止多线程同时执行底层计算代码的。之所以这么做,是因为底层库Cpython,在内存管理这块是线程不安全的。

## GIL有好处吗
对GIL的第一印象是这东西限制了多线程并发,对python而言是个弊大于利的存在。但是从stackoverflow上的讨论来看,这个存在还是相当有必要的。
- 增加了单线程的运行速度
- 可以更方便地整合一些线程不安全的C语言库到python里面去
首先单线程的运行速度更快了,因为有这个全局锁的存在,在执行单线程计算的时候不需要再额外增加锁,减少了不必要的开支。第二个则是可以更好地整合用C语言所写的python库。现在其实挺多用C语言写好底层计算然后封装提供python接口的,比如数据处理领域的pandas库,人工智能领域的计算框架Tensorflow或者pytorch,他们的底层计算都是用C语言写的。由于这个全局锁的存在,我们可以更方便(安全)地把这些C语言的计算库整合成一个python包,对外提供python接口。

## GIL对性能的影响大吗
对于需要做大量计算的任务而言,影响是相当大的。我们先来看一段单线程代码:

```python
class A(object):
def run(self):
ans = 0
for i in range(100000000):
ans += i
a = A()
for _ in range(5):
a.run()
```
以上这段代码是跑5次计算,每次计算是从1累加到1千万,跑这段代码需要17.46s。
紧接着,我们用python的多线程库来实现一个多线程计算:

```python
import threading

class A(object):
def run(self):
ans = 0
for i in range(100000000):
ans += i
threads = []
for _ in range(5):
a = A()
th = threading.Thread(target=a.run)
th.start()
threads.append(th)
for th in threads:
th.join()
```
这里我们启动了5个线程同时计算,然后我们又测试下时间: **41.35**秒!!!这个时候GIL的问题就体现出来了,我们通过多线程来实现并发,结果比单线程慢了2倍多。
### 一个神奇的修饰符
话不多说,我们先来看下代码。以下这段代码和上面的多线程代码几乎一样。但是我们要注意到,在类A的定义上面,我们增加了一个修饰符*@parl.remote_class*。
```python
import threading
import parl

@parl.remote_class
class A(object):
def run(self):
ans = 0
for i in range(100000000):
ans += i
threads = []
parl.connect("localhost:6006")
for _ in range(5):
a = A()
th = threading.Thread(target=a.run)
th.start()
threads.append(th)
for th in threads:
th.join()
```
现在我们来看下计算时间:**3.74秒**!!!相比于单线程的17.46s,这里只用了接近1/5的时间(因为我们开了5个线程)。这里是我觉得比较神奇的地方,并没有做太多的改动,只是在我的单线程类上面增加了一个修饰符,然后用原生的python多线程继续跑代码就变得相当快了。

### 完整的使用说明:

1. 安装这个库:

```shell
pip install --upgrade git+https://github.com/PaddlePaddle/PARL.git
```
2. 在本地通过命令启动一个并发服务(只需要启动一次)
```shell
xparl start --port 6006
```
3. 写代码的时候通过修饰符修饰你要并发的类@parl.remote。
这里需要注意的是只有经过这个修饰符修饰的类才可以实现并发。
4. 在代码最开始的时候通过parl.connect('localhost:6006')来初始化这个包。

最后贴下这个库的使用文档:
https://parl.readthedocs.io/en/latest/parallel_training/setup.html
源码在这里:
https://github.com/PaddlePaddle/PARL/tree/develop/parl/remote

后续会继续研究源码,看下是怎么做到一个修饰符就能加速的。大家如果读过了源码可以一起讨论下:)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!