【多线程相关其一】Python并发编程

1.为什么要引入并发编程

场景1:一个网络爬虫,按顺序爬取花了1个小时,采用并发下载减少到20分钟。
场景2:一个APP应用,优化前每次打开页面需要3秒,采用异步并发提升到每次200毫秒

引入并发,就是为了提升程序运行速度

1.1 程序提速的方法

在这里插入图片描述
单线程串行
一般程序运行时单线程串行,我们有一个线程,CPU先执行,然后进行IO(数据的读取和写出)
在IO期间,CPU是不做什么事情的。等IO完成后,CPU开始进行运算,然后进行下次IO.

整体时间是会有些浪费,那就是在IO期间,CPU是等待的状态。

引入多线程并发
当我们引入多线程并发时,它的运行就发生了变化。
CPU开始执行时,如果遇到了IO操作,它就会切换到另外一个task进行执行。
当IO完成了以后,会通知CPU进行下一步的处理。
这里有个知识点,咱们电脑中的CPU和IO,这两个它们是可以同时并行进行的。

IO的执行,比如说读取内存、磁盘、网络,它的过程是不需要CPU参与的
这样的话,CPU可以释放出来,来执行其他的TASK,实现并发的加速.
但这种原理上,其实还是一个CPU来进行运行的。

2、python对并发编程的支持

多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成
多进程:multiprocessing,利用多核CPU的能力,真正的并发执行任务
异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行

python提供的额外函数来提供辅助能力

1.使用lock对共享资源加锁,防止冲突访问。
比如说,多线程和进程同时访问同一个文件,同时写入就会有冲突。可以把文件锁起来,就可以有序访问
2.使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式
比如说爬虫的时候,可以用生产者-消费者模式来改造,生产者就是边爬取,消费者就是边解析,这两个是分开的两个步骤
3.使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果
4.使用subprocess启动外部程序的进程,并进行输入输出交互
比如说当你写好了一个exe程序,使用它,就可以调起这个exe程序,并跟它进行输入输出的交互,来实现交互式的进程通信

3、怎么选择多进程多线程协程

python并发编程的方式
多线程Thread、多进程Process、协程Coroutine
在这里插入图片描述
3.1、CPU密集计算与IO密集型计算
CPU密集型(CPU-bound):
CPU密集型也叫计算密集型,是指I/O在很短的时间就可以完成,CPU需要大量的计算和处理,特点是CPU占用率相当高。
(也就是说系统的硬盘、内存性能相对CPU要好很多。系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),
I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading(负载)很高。)
例如:压缩解压缩、加密解密、正则表达式搜索

CPU使用率较高(例如:计算圆周率、对视频进行高清解码、矩阵运算等情况)的情况下,通常,线程数只需要设置为CPU核心数的线程个数就可以了。
(此时是最大效率。但是如果线程数/进程数远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程或进程也是要消耗时间的。
因此对于 CPU 密集型的任务来说,线程数/进程数等于 CPU 数是最好的了。)
这一情况多出现在一些业务复杂的计算和逻辑处理过程中。比如说,现在的一些机器学习和深度学习的模型训练和推理任务,包含了大量的矩阵运算。

IO密集型(I/O bound)
IO密集型指的是系统运作大部分的状况是CPU在等I/O(硬盘/内存)的读/写操作,CPU占用率较低。
(硬盘适合长期保存大量的数据,而内存则适合暂存正在运行的程序和数据。)
例如:文件处理程序、网络爬虫程序、读写数据库程序

CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,通常就需要开CPU核心数数倍的线程。
其计算公式为:IO密集型核心线程数 = CPU核数 / (1-阻塞系数)。
当线程进行 I/O 操作 CPU 空闲时,启用其他线程继续使用 CPU,以提高 CPU 的使用率。例如:数据库交互,文件上传下载,网络传输等。

简而言之,如果你的程序依赖大量的外部数据源,比如说内存、磁盘和网络,就是IO密集型
否则,如果只在CPU中进行计算,那就是CPU密集型。
详见:一分钟明白IO密集型与CPU密集型的区别

3.2、多进程、多线程、协程的对比

多进程Process(multiprocessing)多线程Thread(threading)多协程Coroutine(asyncio)
优点可以利用多核CPU并行运算相比进程,更轻量级、占用资源少内存开销最少、启动协程数量最多
缺点占用资源最多、可启动数目比线程少多线程只能并发执行,不能利用多CPU(GIL,GIL全局解释器锁,cpython特有的)支持的库有限制(aiohttp vs requests)、代码实现复杂
适用场景CPU密集型计算IO密集型计算、同时运行的任务数目要求不多IO密集型计算、需要超多任务运行、但有现成库支持的场景

3.3 .单线程与多线程爬虫

import threading
import time
 
from wj_demo.threading_demo.demo_base02.blog_spider import urls, craw
 
"""
I/O密集型操作
"""
 
 
def single_thread():
    """
    单线程爬取
    :return:
    """
    print("single_thread begin")
    for url in urls:
        craw(url)
    print("single_thread end")
 
 
def multi_thread():
    """
    多线程爬取
    :return:
    """
    print("multi_thread begin")
    threads = []
    for url in urls:
        threads.append(
            threading.Thread(target=craw, args=(url,))
        )
 
    for thread in threads:
        thread.start()
 
    for thread in threads:
        thread.join()
 
    print("multi_thread end")
 
 
if __name__ == "__main__":
    start = time.time()
    single_thread()
    end = time.time()
    single_thread_cost = end - start
    print(f"{single_thread.__name__} cost:", single_thread_cost, "seconds")
 
    start = time.time()
    multi_thread()
    end = time.time()
    multi_thread_cost = end - start
    print(f"{multi_thread.__name__} cost:", multi_thread_cost, "seconds")
 
    rate = single_thread_cost / multi_thread_cost
    print(f'{single_thread.__name__} is  cost {rate} {multi_thread.__name__}')
 
# single_thread cost: 17.854352235794067 seconds
# multi_thread cost: 6.92170524597168 seconds
# single_thread is  cost 2.579473063546734 multi_thread

3.4 异步协程asyncio爬虫

import asyncio
import time
 
from wj_demo.threading_demo.thread02_base02.blog_spider import urls  
 
start = time.time()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = [loop.create_task(async_craw(url)) for url in urls]
loop.run_until_complete(asyncio.gather(*tasks))
end = time.time()
print('use time seconds:', end - start)  
# use time seconds: 2.738969087600708

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

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

python解释器有多个版本 Cpython 、Jpython、Pypypython,但普遍使用的是Cpython解释器

GIL不是python的特点而是Cpython解释器的特点
GIL是保证解释器级别的数据的安全
GIL会导致同一个进程下的多个线程无法同时执行

在这里插入图片描述
为什么用GIL?

设计之初,是为了规避并发问题引入GIL,现在却很难去除了。
为了解决多线程之间数据完整性和状态同步问题

原因详解:

Python中对象的管理,是使用引用计数进行的,引用数为0则释放对象

开始:线程A和线程B都引用了对象obj,obj.ref_num=2,线程A和线程B都想撤销对obj的引用
在这里插入图片描述

简化了python对共享资源的管理
不加锁,会导致引用计数器混乱 释放掉别人的内存

怎么规避GIL带来的限制?

1、多线程threading机制依然是有用的,用于IO密集型计算
因为I/O(read,write,send,recv,etc)期间,线程会释放GIL,实现CPU和IO的并行
因为多线程用于IO密集型计算依然可以大幅提升速度
但是多线程用于CPU密集型计算时,只会更加拖慢速度

2、使用multiprocessing的多进程机制实现并行计算、利用多核CPU优势
为了应对GIL的问题,Python提供了multiprocessing

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值