python并发编程总结

本文先从进程、线程和协程的概念和区别讲起,再讲到python里的并行和并发区别,最后讲了python里面并发编程的几个常用模块的用法。
(老套路了就是理论-->实践,也可以说是八股文-->talk is cheap, show me the code的路线。🐶)

进程、线程和协程

类型
定义
优缺点
进程 是操作系统进行资源分配和调度的独立单位 进程执行开销大,资源好管理和保护
线程 进程中执行运算的最小单位 线程执行开销小,但不利于资源的管理和保护
协程 协程是在一个线程里执行,如果要用到多CPU,可以用多进程+协程。协程的切换只是单纯的操作CPU的上下文,资源很小,效率高(一秒钟切换个上百万次系统都抗的住) 多核资源不能使用,阻塞(Blocking)操作(如IO)会阻塞整个程序

并发和并行

并发并不是同一时间同时做多个任务,实际上是多个任务被不停地切换,每次只能运行一个。
并行才是真正的同一时间执行多个任务。

进程

在开始之前先把下面会用到的计时装饰器代码放上来。

import functools
def logging(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        print('{} took {} seconds'.format(func.__name__, end - start))
        return res
    return wrapper

mulitiprocess

如下分别用单进程和多进程测试calc()

def calc(pid):
    print('current process: {}'.format(pid))
    for i in range(10000000):
        pass

@logging
def single_process():
    for i in range(4):
        calc(i)

from multiprocessing  import Process
@logging
def multil_process():
    processes = []
    for i in range(4):
        p = Process(target=calc,args=(i,))
        processes.append(p)
        p.start()
    for process in processes:
        process.join()

if __name__ == '__main__':
    single_process()
    multil_process()

输出:

current process: 0
current process: 1
current process: 2
current process: 3
single_process took 1.2171530750000001 seconds
current process: 0
current process: 1
current process: 2
current process: 3
multil_process took 0.657159286 seconds

可以看见用多进程快了将近一半。

concurrent.futures

from concurrent import futures
@logging
def multi_by_futures():
    process = [i for i in range(4)]
    with futures.ProcessPoolExecutor() as executor:
        executor.map(calc,process)

输出:

current process: 0
current process: 1
current process: 2
current process: 3
multi_by_futures took 0.6685751779999998 seconds

concurrent.futures和multiprocess效率差不多

准备并发数据

到了线程和协程我们把测试函数换一下,换成个I/O密集型爬虫。
在这之前呢先把URL准备好,毕竟这篇文章的重点不是在爬虫上,所以我先把要访问的URL都准备好。以豆瓣电影为例,我们要让程序并发地区访问和获取首页所有电影链接。

  1. 打开 豆瓣电影

  2. 打开浏览器控制台,输入 console.log(JSON.stringify($$(".item").map(x=>x.href)))

  3. 把控制台输出的文本拷贝下来,放到python文件里命名为movie_list,备用。我这里弄到了140个电影的url。实测140个程序跑起来时间有点长,可以取其中几十个就好了。

    # movie.py
    movie_list=[...]
    #print(len(movie_list))
    #140
    movie_list_30 = movie_list[:30]
    

线程

concurrent.futures

import requests
def download_one(url):
    resp = requests.get(url, headers={
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36"})
    # 记住要加User-Agent的header,否则resp是空    
    # print('Read {} from {}'.format(len(resp.text), url))

def download_all(urls):
    for url in urls:
        download_one(url)

import movie
@logging
def single_thread_download():
    download_all(movie.movie_list_30)

from concurrent import futures
@logging
def multi_thread_download_by_futures():
    with futures.ThreadPoolExecutor(max_workers=4) as executor:
        executor.map(download_one,movie.movie_list_30)

if __name__ == '__main__':
    single_thread_download()
    multi_thread_download_by_futures()

输出:

single_thread_download took 32.909495944999996 seconds
multi_thread_download_by_futures took 7.058155945999999 seconds

可见用多线程快了将近4倍之多。

threading.Thread

thread好难用啊。而且这个似乎有点旧,2017版流畅的python里面都没看到有介绍。只是面试时有时候会被问到。

协程

asyncio

asyncio不支持requests, 要用aiohttp

import asyncio
import aiohttp
import time
async def download_one(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            pass
            #print('Read {} from {}'.format(resp.content_length, url))

async def download_all(sites):
    tasks = [asyncio.create_task(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)

@logging
def multi_download_by_crontine():
    asyncio.run(download_all(movie.movie_list_30))

if __name__ == '__main__':
    multi_download_by_crontine()

输出:

multi_download_by_crontine took 3.3723730859999996 seconds

还记得上面用thread费时7秒多吗(multi_thread_download_by_futures took 7.058155945999999 seconds),协程只用了一半时间。

gevent

这个似乎有点旧,2017版流畅的python里面都没看到有介绍。只是面试时有时候会被问到。

参考:
Chrome Console 控制台打印显示不完整
爬豆瓣影评
aiohttp给请求加header
解决TypeError: 'NoneType' object is not callable不小心把装饰器的返回加了括号,半天没找出原因,看了这篇博客才知道

posted @   阳光下的小水仙  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示