Python辣鸡,Python多线程不能并行?

1. Python多线程的缺陷

比如我现在要执行2个倒计时函数,
我可以选择用一个线程顺序执行这2个函数,按顺序倒计时2次;
也可以用两个线程并行这2个函数,也就是同时倒计时。

我的电脑有4个CPU核心,是可以并行4个线程的,因此我并行上述2个线程完全没问题。

也就是说,我用2个线程执行2个倒计时用的时间应该是一个线程执行2次倒计时时间的一半。

1.1 Java单线程和多线程执行倒计时函数

在Java中确实是如此,如下。

Java单进程执行:
如下代码,有一个倒计时函数countDown(),我在主函数里顺序执行两次倒计时500000000下:

public class Main {
    private static void countDown(long count) {
        while (count > 0) {
            count--;
        }
        System.out.println("倒计时完成");
    }
    
    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        countDown(500000000);
        countDown(500000000);
        long endTime = System.currentTimeMillis();
        System.out.println("耗时间为:" + (endTime - startTime));
    }
}

输出:

倒计时完成
倒计时完成
耗时间为:456

Java多线程执行:

public class Main {
    private static void countDown(long count) {
        while (count > 0) {
            count--;
        }
        System.out.println("倒计时完成");
    }

    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        Thread t1 = new Thread(() -> countDown(500000000));
        Thread t2 = new Thread(() -> countDown(500000000));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long endTime = System.currentTimeMillis();
        System.out.println("耗时间为:" + (endTime - startTime));
    }
}

输出:

倒计时完成
倒计时完成
耗时间为:274

使用了多线程确实速度变快了很多,花费时间几乎是单线程的一半。(为什么不是严格的一半?因为用了多线程之后,线程之间切换也需要开销)

但是Python的表现却不是这样

1.2 Python单线程和多线程执行倒计时函数

同样的,使用Python来对比单进程和多线程执行倒计时函数,如下:

Python单线程执行:

import time
from threading import Thread
# 倒计时函数
def count_down(count):
    while count > 0:
        count -= 1
    print("倒计时完成")


start = time.time()

count_down(50000000)
count_down(50000000)

end = time.time()
print(f"耗时为:{end -  start}")

输出:

倒计时完成
倒计时完成
耗时为:3.884136199951172

Python多线程执行:

import time
from threading import Thread
# 倒计时函数
def count_down(count):
    while count > 0:
        count -= 1
    
    print("倒计时完成")


start = time.time()

t1 = Thread(target=count_down, args=(50000000, ))
t2 = Thread(target=count_down, args=(50000000,))
t1.start()
t2.start()
t1.join()
t2.join()

end = time.time()
print(f"耗时为:{end -  start}")

输出:

倒计时完成
倒计时完成
耗时为:4.136513948440552

此时却发现,Python多线程执行用的时间比单线程用的时间还要多??

这是因为Python的多线程并不能并行!

为什么Python的多线程不能并行?

2. GIL

Python在同一时间,只有一个线程可以运行。但是这是为什么,就是Pyhton中的GIL进行了限制!

当线程要想执行,就要获取GIL,但是一个Python进程只有一个GIL,因此同一时刻只有一个线程可以执行。

那么GIL是什么?

2.1 什么是GIL

GIL全称叫做Global Interpreter Lock,翻译过来就是全局解释器锁,那么GIL有什么作用?

GIL的作用是用于Python的内存管理。

Python使用的自动内存管理机制,也就是创建的对象不用了之后就可以自动回收。

引用计数法:

Python使用引用记数法来给每个对象进行标记,每个对象有一个引用计数值count。
如果有变量引用该对象,就会给count +1;
如果一个变量不引用该对象了,就将count -1;
当一个对象的引用计数值count为0,该对象就可以回收了。

如下代码:

import sys
a = [] # a引用了该列表对象, 引用计数为1
b = a # b也引用了该列表对象,引用计数为2
print(sys.getrefcount(a)) # 这里该对象又被该方法的形参引用,引用计数为3

输出:

3

加锁:

也就是当一个变量引用一个对象,需要修改该对象的引用计数值,那么如果是多个进程同时引用某个对象,会同时修改该对象的引用计数值。,有可能就会造成某个对象的引用计数值count不正确,比如:

刚开始某个变量的计数值count是1,现在有两个线程同时引用该对象,也就是同时执行count += 1操作。

count += 1其实是分为三步的:

1. 读取count的值
2. 修改count的值

有可能发生这种情况:

线程1读取count的值,count = 1
线程2读取count的值,count = 1
线程1修改count = 2
线程2修改count = 2

也就是count += 1执行2次,结果应该是3,但是由于多线程同时执行,count修改后最终结果却是2。

而当count == 0时,该对象就会被回收,而上述的多线程同时修改count值的问题,会造成该对象不该回收却被回收了。

为了防止线程安全问题,就需要给每个对象的引用计数值加锁,那么每个对象的引用计数值都需要加锁,不但很麻烦,而且容易造成死锁。

因此Python在设计出来的时候,只设计了一把锁 : GIL,当一个线程需要执行,需要线获取GIL,然后执行,这样就不会有多个线程同时对一个对象的引用计数count进行操作,这样就很安全。

GIL只有1个,线程要获取GIL再执行,因此Python同一时刻只有一个线程可以执行。

既然由于GIL的限制,让Python多线程无法并行,为什么不去掉GIL,或者更改内存管理机制?

注: Python有低层有多种实现,官方的是使用C实现的叫CPython,还是使用Java实现的叫Jython,C#实现的等等,但是只有CPython受到GIL的限制。

2.2 Python为什么不舍弃GIL

Python成也GIL,败也GIL。

GIL导致多线程无法并行,因此是被最多人诟病的一点,但为什么至今CPython还在使用呢?

Python的第三方库非常丰富,使用C写的,由于GIL让同一个时刻只有一个线程执行,保证了线程安全,因此这些库在设计的时候不用考虑线程安全问题,实现起来非常容易。

如果现在修改的话,那么那么库都不能使用了。

并且很多人认为,程序执行效率影响最大的不是CPU而是I/O。

GIL结构简单,效率非常高,如果使用其他的机制,效率会降低,因此GIL至今仍然在使用。

3. Python的多线程这么辣鸡,那还用不用?

在前面那个倒计时的例子中,其实是一个极端的例子。一般程序可以分为三种:

  1. CPU密集型
  2. I/O密集型
  3. CPU和I/O均衡型

上面那个倒计时的例子,是属于CPU密集型,因为全部都是CPU指令。

但是由于GIL,同一时间只有一个线程可以执行,也只能利用到一个CPU的核心。因此对于CPU密集型,使用多线程不是最好的选择。

3.1 多线程

对于多进程来说,创建出多个进程,只不过增加了该程序被执行的概率。

因为操作系统的基本调度单位是线程,一个进程创建出多个线程,多个线程去争抢时间片,因此该进程执行的概率更大(就好比使用多个号参与抽奖,自己中奖的概率更高)。

但是Python的多线程由于GIL又不能利用到多个CPU核心。

3.2 多进程

GIL只是锁住了一个进程,如果使用多进程,就可以解除GIL的限制。

使用多进程的话,不同进程切换开销更大,但是起码多个进程可以并行,可以充分利用CPU的多个核心

因此,对于CPU密集型,可以使用多进程。

还是倒计时的例子,使用多进程代码如下:

import time
from multiprocessing import Pool

# 倒计时函数
def count_down(count):
    while count > 0:
        count -= 1
    
    print("倒计时完成")


start = time.time()

pool = Pool(processes=2)
p1 = pool.apply_async(count_down, [50000000])
p2 = pool.apply_async(count_down, [50000000])
pool.close()
pool.join()

end = time.time()
print(f"耗时为:{end -  start}")

输出:

倒计时完成
倒计时完成
耗时为:2.0172834396362305

可以看到,使用多进程可以实行两个进程并行执行倒计时函数,速度提高了不少。

3.3 协程

对于I/O密集型,多进程和多线程都不是也不一定是最好的选择。

多进程开销大可以充分利用CPU,但是I/O密集型程序执行CPU指令的时间比较少,用不到。

而多线程,需要操作系统参与调度,开销虽然比多进程小,但是还是有点大。

而Python原生实现了协程。

协程比线程更加轻量级,一个线程里面可以创建多个协程,并且协程调度有Python虚拟机来实现,不需要操作系统来参与

对于I/O密集型程序,大部分时间都在等待I/O,CPU执行指令的时间比较少,比如爬虫,因此使用协程来实现比较合适。

3.4 多进程 VS 多线程 VS 协程
多进程多线程协程
是否并行并行非并行非并行
调度开销很大
适合程序类型CPU密集型其他I/O密集型
  • 8
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值