Python学习之线程池与GIL全局锁详解
作者:渴望力量的哈士奇 发布时间:2021-10-09 21:55:18
线程池
线程池的创建 - concurrent
concurrent
是 Python 的内置包,使用它可以帮助我们完成创建线程池的任务。
方法名 | 介绍 | 示例 |
---|---|---|
futures.ThreadPoolExecutor | 创建线程池 | tpool=ThreadPoolExecutor(max_workers) |
通过调用 concurrent 包的 futures 模块的 ThreadPoolExecutor 类,通过实例化 ThreadPoolExecutor 实现创建线程池的对象,它有一个参数来设置 线程池的数量。这和创建进程池设置的数量是完全相同的。
线程池的常用方法
接下里看一下线程池对象中都有哪些常用的方法 :
函数名 | 介绍 | 用法 |
---|---|---|
submit | 往线程池中添加任务 | submit(target, args) |
done | 确认线程池中的某个线程是否完成了任务 | done() |
rsult | 获取当前线程执行任务的结果 | result() |
submit 函数:通过 submit 函数将参数传入;该函数传入的参数也是传入要执行的函数与该函数的参数,由于它的参数并不用需要通过赋值语句的形式传入,只需要把相应的值传入就可以了(稍后会进行一个练习)。
done 函数:判断当前线程是否执行完成;返回值是 bool 类型。
result 函数:返回当前线程池中线程任务的执行结果,通过这种方法就可以获取线程池的返回值了。
线程池演示案例
1、定义一个函数实现循环的效果
2、定义一个线程池,设置线程的数量
# coding:utf-8
import time
from concurrent.futures import ThreadPoolExecutor
def work(i):
print('第 {} 次循环'.format(i))
time.sleep(1) # 之所以每次都要使用 sleep 函数,是因为函数执行太快;通过 sleep 尝试模拟一下长时间的执行一个任务
if __name__ == '__main__':
thread_poor = ThreadPoolExecutor(4)# 实例化一个线程池,设置线程数量为4
for i in range(20):
thread_poor.submit(work, (i,))# 利用 submit 函数将任务添加至 work 函数
运行效果如下
从运行结果来看,我们的线程任务每次执行4个任务,阻塞一秒后再执行后续的四个线程的任务,是没有问题的。
PS:需要注意的是,运行结果有可能是出现将两个或者多个任务的结果在同一行打印输出,这是因为在同一时间处理了多个线程的任务,这也叫 "并发"。
线程锁
前文的进程池是与进程锁相对应匹配的,同样的线程池也有与之对应的 线程锁 。线程锁的使用方法几乎与进程锁是一样的,只不过线程锁对应的是线程罢了。
1.实例化一个线程锁
2、在 work 函数中调用线程锁
3、并获取 线程 的返回值(线程池也是可以获取返回值的)
代码示例如下:
# coding:utf-8
import os
import time
import threading
from concurrent.futures import ThreadPoolExecutor
lock = threading.Lock() # 全局定义一个 Lock() 实例
def work(i):
lock.acquire() # 区别于 进程锁 只需要在全局实例化一个即可,线程锁需要在线程任务的函数中调用 线程锁 才会生效
print('当前是第 {} 次循环'.format(i))
time.sleep(1) # 之所以每次都要使用 sleep 函数,是因为函数执行太快;通过 sleep 尝试模拟一下长时间的执行一个任务
lock.release()
return '第 {} 次循环的进程id为:{}'.format(i, os.getpid()) # 线程也是基于进程实现的
if __name__ == '__main__':
thread_poor = ThreadPoolExecutor(4) # 实例化一个线程池,设置线程数量为4
result = []
for i in range(20):
result_thread = thread_poor.submit(work, (i,)) # 利用 submit 函数将任务添加至 work 函数;
# 需要注意的是这里不像进程池那样使用赋值的形式传入 work 函数
result.append(result_thread)
for res in result:
print(res.result())
运行结果如下:
从运行结果可以看到,之前一同执行的4个任务现在变成了一次只执行一个任务;每一个个线程都是在主进程 93215下执行的,说明线程与进程还是有所区别的,虽然我们有多个线程任务在执行,但是依然是在主进程下去完成的;同时我们还获取到了 线程的返回值 第 {} 次循环的进程id为:{}'.format(i, os.getpid() 。
利用线程池实现抽奖小案例
案例代码如下:
# coding:utf-8
import threading
import random
from concurrent.futures import ThreadPoolExecutor
lock = threading.Lock()
def luck_draw(arg):
lock.acquire()
# 从手机列表中随机选出一个中奖手机,其他手机均未中奖
phone = random.choice(arg[0])
# 在从奖池中随机选取一个奖品,视为该手机抽中的奖品
price = random.choice(arg[1])
prices.remove(price)
phones.remove(phone)
lock.release()
return '恭喜手机尾号为{}的用户,抽到{}'.format(str(phone)[-5:-1], price)
if __name__ == '__main__':
t = ThreadPoolExecutor(3) # 通过创建三个线程从而实现每个线程完成一项抽奖任务
# 确定抽奖人数
phone_num = int(input('请输入抽奖的用户人数:'))
# 模拟产生出相应数量的手机号
phones = random.sample(range(13300000000, 19999999999), phone_num) # 这里设置的随机号码仅做演示效果
prices = ['一等奖:iPhone12 ProMax', '二等奖:ipad2021pro', '三等奖:air wetter']
result = []
for i in range(3): # 三个任务,每个线程分配一个
t_result = t.submit(luck_draw, (phones, prices))
result.append(t_result)
for res in result:
print(res.result())
运行效果如下:
GIL全局锁
本章节的开头我们就说过,该部分没有代码的相关练习。仅仅是对 GIL全局锁 做一个概念上的简单启蒙。
其他语言的线程与Python线程的区别
多线程与多进程的使用其实是比较复杂的,目前作为初学者来说涉及的还比较浅。最近的几个章节介绍了 进程与线程在CPU的执行方式,这里再进行拓展一下。
下面我们看一张图:
依然是一个CPU 与4个核心(可以认为是4条跑道);
先看左边的两条跑道,是进程1创建的3个线程。这三条线程有一个去了 1core 跑道,另外两条则去了 2core 跑道。线程之间有选择性的进入了不同的跑道,当然进程1的主进程或者说是主线程可能会在 1core 跑道、也可能会在 2core 跑道,这是其他语言进行多线程的样子。
再来看看右边,Python 创建的进程2。当进程创建之后,包含主线程一 * 生了3个线程,而这三个线程都跑到了 4core 跑道 上去。它不会像其他语言那样去寻找不同的有空闲资源的跑道去执行,而是仅仅在主进程所处的跑道去执行线程。造成 Python 中的多线程无法在多条跑道执行任务的主要原因就是因为 GIL全局锁 ,这个 GIL 并不是 Python语法中添加上去的,而是 python解释器 在执行的时候自动加了这把 "锁" 。
GIL 的作用
因为 GIL 锁 的关系,使得 Python 的多线程无法在多个CPU跑道上去执行任务,它只能在单一CPU上进行工作。
这也限制了多线程的性能,毕竟 Python 的多线程只能在一条跑道上运行。跑道满了,运行速度依然会慢。而在多个跑道上运行的任务必然是要比单一跑道效率会高很多。
Python创始人 Guido 之所以保留 GIL 锁,其实也是为了线程之间的安全。虽然这个话题一直都在争论,不过我们也有办法去掉这个 GIL全局锁。
默认的解释器是 Python 自带的解释器,这里我们可以选择一个叫做 pypy 的解释器。通过它来执行 Python 脚本是不含有 GIL全局锁 的,但并不太推荐这种做法。
另外一种解决方法就是使用 多进程 + 多线程 的方式 来弥补这一短板上的问题,通过多进程在每个 CPU 跑道上执行任务,并且每个进程的跑道上再去执行多个线程。 ,让它们在各自的时间片上去运行。这些用法会在后续的章节会介绍到,在此只需要了解即可。
来源:https://blog.csdn.net/weixin_42250835/article/details/124127750


猜你喜欢
- 什么是主键?主键是表中唯一标识该表中每个元组(行)的列。主键对表实施完整性约束。表中只允许使用一个主键。主键不接受任何重复值和空值。表中的主
- 数据加载、存储与文件格式pandas提供了一些用于将表格型数据读取为DataFrame对象的函数。其中read_csv和read_talbe
- 先来看一段创建文件并写入文本的代码,然后作介绍。 #!/usr/bin/env python 'makeFile.py
- 三种方法利用indexOf判断新数组underscore.js中实际上也是使用的类似的indexOf //传入数组 function uni
- 环境:【wind2003[open Tftp server] + virtualbox:ubuntn10 server】tftp
- 本文实例讲述了Python实现清理微信僵尸粉功能。分享给大家供大家参考,具体如下:原理通过Pyhton调用itchat模块登录网页版微信,给
- 在Python中,变量是没有类型的,这和以往看到的大部分编辑语言都不一样。在使用变量的时候,不需要提前声明,只需要给这个变量赋值即可。但是,
- 本文讲述的是通过python+tkinter编写一个简单桌面放大镜的代码示例,具体如下。代码思路:首先全屏截图,然后在鼠标当前位置以小窗口进
- 前言NumPy(Numerical Python)是Python的一种开源的数值计算扩展。这种工具可用来存储和处理大型矩阵,比Python自
- mybatis数据库排序今天用到了对数据库按照倒序进行输出。因为刚接触mybatis,所以对这方面还不是太了解,再网上搜了好长时间终于找到了
- 我们大家都知道MySQL数据库在安装完之后,默认的MySQL数据库,其最大连接数为100,一般流量稍微大一点的论坛或网站这个连接数是远远不够
- 先给大家说下我的项目需求:用户扫一扫二维码会产生一个链接,该链接会向后端发送个请求,返回一个 apk 的下载地址,用户点击下载按钮可以下载此
- <%@LANGUAGE="VBSCRIPT" CODEPAGE="936"%>
- 前言上一篇文章讲解了MySQL的事务的相关概念MySQL的事务特性概念梳理总结文章末尾提出了事务因并发出现的问题有哪些?本篇将着重讲述这个问
- HTML 的拖放 API 依赖 DOM 事件模型,获取拖放和放置元素的相关信息,以此实现拖放功能。我们只需要注册很少几个事件 * ,就能把任
- 企业最有价值的资产通常是其数据库中的客户或产品信息。因此,在这些企业中,数据库管理的一个重要部分就是保护这些数据免受外部攻击,及修复软/硬件
- Go mod开启 Go Modulego env -w GO111MODULE=on或set GO111MODULE=on设置Go Prox
- 什么是作用域程序的执行,离不开作用域,也必须在作用域中才能将代码正确的执行。所以作用域到底是什么,通俗的说,可以这样理解:作用域就是定义变量
- 相比于time模块,datetime模块的接口则更直观、更容易调用。今天就来讲讲datetime模块。 datetime模块定义了两个常量:
- 一、算术运算符主要用于数学运算,其可以连接运算符前后的两个数值或表达式,对数值或表达式进行加 (+)、减(-)、乘(*)、除(/)和取模(%