熬了两个通宵写的!终于把多线程和多进程彻底讲明白了!

本文深入探讨了Python中的多线程和多进程,包括全局解释器锁(GIL)、线程与进程的区别、并发与并行的概念、多线程的适用场景以及Python中实现多线程的方法。通过示例代码展示了线程的创建和守护线程的使用,同时也讨论了多线程中的线程安全问题和避免GIL的方法。文章指出,虽然Python的多线程受到GIL限制,但在IO密集型任务中仍可提高效率,并建议通过多进程解决计算密集型任务。
摘要由CSDN通过智能技术生成

我们知道,在一台计算机中,我们可以同时打开许多软件,比如同时浏览网页、听音乐、打字等等,看似非常正常。但仔细想想,为什么计算机可以做到这么多软件同时运行呢?这就涉及到计算机中的两个重要概念:多进程和多线程了。(PS:万字长文,讲得很详细,建议先收藏再好好看!

同样,在编写爬虫程序的时候,为了提高爬取效率,我们可能想同时运行多个爬虫任务。这里同样需要涉及多进程和多线程的知识。

本文,我们就先来了解一下多线程的基本原理,以及在 Python 中如何实现多线程。

1. 全局解释器锁

全局解释器锁 (英语:Global Interpreter Lock,缩写 GIL)

是 计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行,即便在多核心处理器上,使用GIL的解释器也只允许同一时间执行一个线程。常见的使用GIL的解释器有CPython与 Ruby MRI。

如果,你对上面的不理解,也没有问题。通俗的解释就是:你电脑是一核或者多核 ,还是你的代码写了多个线程,但因为GIL锁的存在你也就只能运行一个线程,无法同时运行多个线程。

接下来,我们来用个图片来解释一下:
在这里插入图片描述
比如图中,假如你开了两个线程(Py thread1 、Py tread2),

当我们线程一(Py thread1)开始执行时,这个线程会去我们的解释器中申请到一个锁。也就是我们的 GIL 锁;

然后,解释器接收到一个请求的时候呢,它就会到我们的 OS 里面,申请我们的系统线程;

系统统一你的线程执行的时候,就会在你的 CPU 上面执行。(假设你现在是四核CPU);

而我们的另一个线程二(py thread2)也在同步运行。

而线程二在向这个解释器申请 GIL 的时候线程二会卡在这里(Python 解释器),因为它的 GIL 锁已经被线程一给拿走了(也就是说:他要进去执行,必须拿到这把锁);

线程二要运行的话,就必须等我们的线程一运行完成之后(也就是把我们的 GIL 释放之后(图片中的第5步)线程二才能拿到这把锁);

当线程二拿到这把锁之后就和线程一的运行过程一样。

① Create > ② GIL > ③ 申请原生线程(OS) > ④ CPU 执行(如果有其他线程,都会卡在 Python 解释器的外边)

这个锁其实是 Python 之父想一劳永逸解决线程的安全问题(也就是禁止多线程同时运行)

2. 多线程的含义

说起多线程,就不得不先说什么是线程。然而想要弄明白什么是线程,又不得不先说什么是进程。

进程我们可以理解为是一个可以独立运行的程序单位。

比如:

打开一个浏览器,这就开启了一个浏览器进程;

打开一个文本编辑器,这就开启了一个文本编辑器进程。

但一个进程中是可以同时处理很多事情的。

比如:在浏览器中,我们可以在多个选项卡中打开多个页面。

有的页面在播放音乐,

有的页面在播放视频,

有的网页在播放动画,它们可以同时运行,互不干扰。

为什么能同时做到同时运行这么多的任务呢?

这里就需要引出线程的概念了,其实这一个个任务,实际上就对应着一个个线程的执行。

而进程呢?

它就是线程的集合,进程就是由一个或多个线程构成的,线程是操作系统进行运算调度的最小单位,是进程中的一个最小运行单元。

比如:

上面所说的浏览器进程,其中的播放音乐就是一个线程,播放视频也是一个线程,当然其中还有很多其他的线程在同时运行,这些线程的并发或并行执行最后使得整个浏览器可以同时运行这么多的任务。

了解了线程的概念,多线程就很容易理解了,多线程就是一个进程中同时执行多个线程,前面所说的浏览器的情景就是典型的多线程执行。

3. 并发和并行

说到多进程和多线程,这里就需要再讲解两个概念,那就是并发和并行。我们知道,一个程序在计算机中运行,其底层是处理器通过运行一条条的指令来实现的。

3.1 并发

英文叫作 concurrency。它是指同一时刻只能有一条指令执行,但是多个线程的对应的指令被快速轮换地执行。比如:

一个处理器,它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。

由于处理器执行指令的速度和切换的速度非常非常快,人完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作,这就使得宏观上看起来多个线程在同时运行。但微观上只是这个处理器在连续不断地在多个线程之间切换和执行,每个线程的执行一定会占用这个处理器一个时间片段,同一时刻,其实只有一个线程在执行。

3.2 并行

英文叫作 parallel。它是指同一时刻,有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器。不论是从宏观上还是微观上,多个线程都是在同一时刻一起执行的。

并行只能在多处理器系统中存在,如果我们的计算机处理器只有一个核,那就不可能实现并行。

而并发在单处理器和多处理器系统中都是可以存在的,因为仅靠一个核,就可以实现并发。

举个例子

比如系统处理器需要同时运行多个线程。如果系统处理器只有一个核,那它只能通过并发的方式来运行这些线程。如果系统处理器有多个核,当一个核在执行一个线程时,另一个核可以执行另一个线程,这样这两个线程就实现了并行执行,当然其他的线程也可能和另外的线程处在同一个核上执行,它们之间就是并发执行。具体的执行方式,就取决于操作系统的调度了。
在这里插入图片描述
4. 多线程适用场景

在一个程序进程中,有一些操作是比较耗时或者需要等待的,比如等待数据库的查询结果的返回,等待网页结果的响应。如果使用单线程,处理器必须要等到这些操作完成之后才能继续往下执行其他操作,而这个线程在等待的过程中,处理器明显是可以来执行其他的操作的。如果使用多线程,处理器就可以在某个线程等待的时候,去执行其他的线程,从而从整体上提高执行效率。

像上述场景,线程在执行过程中很多情况下是需要等待的。

比如

网络爬虫就是一个非常典型的例子,爬虫在向服务器发起请求之后,有一段时间必须要等待服务器的响应返回,这种任务就属于 IO 密集型任务。对于这种任务,如果我们启用多线程,处理器就可以在某个线程等待的过程中去处理其他的任务,从而提高整体的爬取效率。

但并不是所有的任务都是 IO 密集型任务,还有一种任务叫作计算密集型任务,也可以称之为 CPU 密集型任务。顾名思义,就是任务的运行一直需要处理器的参与。此时如果我们开启了多线程,一个处理器从一个计算密集型任务切换到切换到另一个计算密集型任务上去,处理器依然不会停下来,始终会忙于计算,这样并不会节省总体的时间,因为需要处理的任务的计算总量是不变的。如果线程数目过多,反而还会在线程切换的过程中多耗费一些时间,整体效率会变低。

所以,如果任务不全是计算密集型任务,我们可以使用多线程来提高程序整体的执行效率。尤其对于网络爬虫这种 IO 密集型任务来说,使用多线程会大大提高程序整体的爬取效率。

5. Python 实现多线程

在 Python 中,实现多线程的模块叫作 threading,是 Python 自带的模块。下面我们来了解下使用 threading 实现多线程的方法。

在具体实现之前,我们先来测试一下多线程与当线程裸奔的速度对比,为了更加直观,我这里使用把每种线程代码单独写出来并做对比:

单线程裸奔:(这也是一个主线程(main thread))

import timedef start():
    for i in range(1000000):
        i += i    return# 不使用任何线程(裸着来)def main():
    start_time = time.time()    for i in range(10):
        start()
    print(time.time()-start_time)if __name__ == '__main__':
    main()

输出:

6.553307056427002

注意:因为每台电脑的性能不一样,所运行的结果也相对不同(请按实际情况分析)

接下来我们写一个多线程

我们先创建个字典 (thread_name_time) 来存储我们每个线程的名称与对应的时间

import threading,timedef start():
    for i in range(1000000):
        i += i    return# # 不使用任何线程(裸着来)# def main():#     start_time = time.time()#     for i in range(10):#         start()#     print(time.time()-start_time)# if __name__ == '__main__':#     main()def main():
    start_time = time.time()
    thread_name_time = {
   }# 我们先创建个字典 (thread_name_time) 用来来存储我们每个线程的名称与对应的时间

    for i in range(10):        # 也就是说,每个线程顺序执行
        thread = threading.Thread(target=start)# target=写你要多线程运行的函数,不需要加括号
        thread.start()# 上一行开启了线程,这一行是开始运行(也就是开启个 run)
        thread_name_time[i] = thread # 添加数据到我们的字典当中,这里为什么要用i做key?这是因为这样方便我们join

    for i in range(10):
        thread_name_time[i].join()    #     join() 等待线程执行完毕(也就是说卡在这里,这个线程执行完才会执行下一步)
    print(time.time()-start_time)if __name__ == '__main__':
    main()

输出

6.2037984102630615
# 6.553307056427002 裸奔# 6.2037984102630615 单线程顺序执行# 6.429047107696533 线程并发

我们可以看到,速度上的区别不大。

多线程并发不如单线程顺序执行快

这是得不偿失的

造成这种情况的原因就是 GIL

这里是计算密集型,所以不适用

在我们执行加减乘除或者图像处理的时候,都是在从 CPU 上面执行才可以。Python 因为 GIL 存在,同一时期肯定只有一个线程在执行,这样这样就是造成我们开是个线程和一个线程没有太大区别的原因。

而我们的网络爬虫大多时候是属于 IO 密集与计算机密集
在这里插入图片描述
BIOS:B:Base、I:Input、O:Output、S:System

也就是你电脑一开机的时候就会启动。

1. 计算密集型

在上面的时候,我们开启了两个线程,如果这两个线程要同时执行,那同一时期 CPU 上只有一个线程在执行。

那从上图可知,那这两个线程就需要频繁的在上下文切换。

Ps:我们这个绿色表示我们这个线程正在执行,红色代表阻塞。

所以,我们可以明显的观察到,线程的上下文切换也是需要消耗资源的(时间-ms)不断的归还和拿取 GIL 等,切换上下文。明显造成很大的资源浪费。

2. IO 密集型

我们现在假设,有个服务器程序(Socket)也就是我们新开的一个程序(也就是我们网络爬虫的最底层)开始爬取目标网页了,我们那个网页呢,有两个线程同时运行,我们线程二已经请求成功开始运行了,也就是上图的 (Thread 2)绿色一条路过去。

而我们的线程一(Thread 1)- Datagram(这里它开启了一个 UDP),然后等待数据建立(也就是等待哪些 HTML、CSS 等数据返回)也就是说,在 Ready to receive(recvfrom)之间都是准备阶段。这样就是有一段时间一直阻塞,而我们的线程二可以一直无停歇也不用切换上下文就一直在运行。这样的 IO 密集型就有很大的好处。

IO 密集型,这样就把我们等待的时间计算进去了,节省了大部分时间。

这里我们需要注意的是,我们的多线程是运行在 IO 密集型上的,我们得区分清楚。

还有就是,资源等待,比如有时候我们使用浏览器发起了一个 Get 请求,那浏览器图标上面在转圈圈的时候就是我们请求资源等待的时间,(也就是图上面的 Datagram 到 Ready to receive )数据建立到数据接收(就是转圈圈的时间)。我们完全就不需要执行它,就让它等待就好。这个时候让另一个线程去执行就好

换言之就是:第一个线程,我们爬取那个网页转圈圈的时候让另一个线程继续爬取。这样就避免了资源浪费。(把时间都利用起来)

注意: 请求资源是不需要 CPU 进行计算的,CPU 参与是很少的,而我们第一个例子,计算数字的 for 循环中,是需要 CPU 进行计算的。

5.1 Thread 直接创建子线程

5.1.1 非守护线程

复杂的操作之前需要一个简单的示例开始:

import threading, timedef start():
    time.sleep(1)
    print(threading.current_thread().name) # 当前线程名称
    print(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值