• 并发编程实战:用多线程、多进程、多协程加速程序运行


    一、为什么要引入并发编程

    场景一:一个网络爬虫,按顺序爬花了一个小时,采用并发下载减少到20分钟

    场景二:一个APP应用,优化前每次打开页面需要3秒,采用异步编发提升到每次200毫秒

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

    二、有哪些程序提速的方法

    三、Python对并发编程的支持

    ①多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成

    ②多进程:multiprocessing,利用多核CPU的能力,真正的并行执行任务

    ③异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行

    ④使用Lock对资源加锁,防止冲突访问

    ⑤使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式

    ⑥使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果使用subprocess启动外部程序的进程,并进行输入输出交互

     

    四、Python并发编程的三种方式

    ①多线程(Thread)

    ②多进程(Process)

    ③多协程(Coroutine)

    五、CPU密集型计算、IO密集型计算

    1、CPU密集型(CPU-bound)

    CPU密集型也叫计算密集型,是指I/O在很短的时间就可以完成,CPU需要大量的极端和处理,特点是CPU占用率相当高。

    例如:压缩解压缩、加密解密、正则表达式搜索。

    2、IO密集型(I/O-bound)

    IO密集型指的是系统运作大部分的状况是CPU在等I/O(硬盘/内存)的读/写操作,CPU占用率较低。

    例如:文件处理程序、网络爬虫程序、读写数据程序。

    六、多进程、多线程、多协程

    一个进程中可以启动多个线程,一个线程中可以启动多个协程

    1、多进程 Process(multiprocessing)

    优点:可以利用多核CPU并行运算

    缺点:点用资源最多、可启动数目比线程少

    适用于:CPU密集型计算

    2、多线程Thread(threading)

    优点:相比进程,更轻量级、占用资源少缺点:

    相比进程:多线程只能并发执行,不能利用多CPU(GIL)

    相比协程:启动数目有限制,占用内存资源,有线程切换开销

    适用于:IO密集型计算、同时运行的任务数目要求不多

    3、多协程 Coroutine(asyncio)

    优点:内存开销最少、启动协程数量最多

    缺点:支持的库有限制(aiohttp vs requests)、代码实现复杂

    适用于:IO密集型计算、需要超多任务运行、但有现成库支持的场景

    七、如何根据任务选择对应技术

    八、GIL

    1、Python速度慢的两大原因

    ①动态类型语言,解释型语言,边解释边执行

    ②GIL,无法利用多核CPU并发执行

    2、GIL是什么

    GIL(Global Interpreter Lock):全局解释器锁

    是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。

    即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。

    由于GIL的存在,即使电脑有多核CPU,但是时刻也只能使用1个,相比并发加速的C++/JAVA速度慢。

    上面这张图,就是 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。

    读者可能会问,为什么 Python 线程会去主动释放 GIL 呢?毕竟,如果仅仅要求 Python 线程在开始执行时锁住 GIL,且永远不去释放 GIL,那别的线程就都没有运行的机会。其实,CPython 中还有另一个机制,叫做间隔式检查(check_interval),意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况,每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。

    GIL 是python的全局解释器锁,同一进程中假如有多个线程运行,一个线程在运行python程序的时候会霸占python解释器(加了一把锁即GIL),使该进程内的其他线程无法运行,等该线程运行完后其他线程才能运行。如果线程运行过程中遇到耗时操作,则解释器锁解开,使其他线程运行。所以在多线程中,线程的运行仍是有先后顺序的,并不是同时进行。

    多进程中因为每个进程都能被系统分配资源,相当于每个进程有了一个python解释器,所以多进程可以实现多个进程的同时运行,缺点是进程系统资源开销大

    3、为什么会存在GIL

    ①简而言之:Python设计初期,为了规避并发问题引入了GIL,现在想去除却去不掉了。

    ②为了解决多线程之间数据完整性和状态同步问题

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

    例子:线程A和线程B都引用了对象obj,obj.ref_num=2, 线程A和B都想撤销对obj的引用,如下图片,如果没有GIL锁,则线程A以及线程B对同一个资源进行释放,有可能造成内存破坏

    GIL确实有好处:简化了Python对共享资源的管理

    4、怎样规避GIL带来的限制

    ①多线程 threading 机制依然是有用的,用于IO密集型计算

    因为在 I/O(read、write、send、recv)期间,线程会释放GIL,实现CPU和IO的并行因此多线程用于IO密集型计算依然可以大幅提升速度

    但是多线程用于CPU密集型计算时,只会更加拖慢速度。

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

    九、使用多线程,python爬虫被加速10倍

    import requests
    import threading
    import time
    
    urls = []
    for i in range(2, 52):
        urls.append('https://www.cnblogs.com/#p{0}'.format(i))
    
    
    def craw(url):
        r = requests.get(url)
        print(url, len(r.text))
    
    
    # 单线程:耗时:single_thread cost: 3.8452553749084473 seconds
    def single_thread():
        print('single_thread start')
        for url in urls:
            craw(url)
        print('single_thread end')
    
    
    # 多线程:耗时:multi_thread cost: 0.36998510360717773 seconds
    def multi_thread():
        print('multi_thread start')
        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()
        print('single_thread cost:', end - start, 'seconds')
    
        start = time.time()
        multi_thread()
        end = time.time()
        print('multi_thread cost:', end - start, 'seconds')

     

    十、python实现生产者消费者爬虫

    1、多组件的Pipeline技术结构

    复杂的事情一般不会一下子做完,而是会分成很多中间步骤一步步完成。

    2、生产者消费者爬虫的架构

     3、多线程数据通信的queue.Queue

    4、单线程:获取第一页的所有文章的链接的标题

    import requests
    from bs4 import BeautifulSoup
    
    urls = []
    for i in range(2, 52):
        urls.append('https://www.cnblogs.com/#p{0}'.format(i))
    
    
    def craw(url):
        r = requests.get(url)
        return r.text
    
    
    def parse(html):
        soup = BeautifulSoup(html, 'html.parser')
        links = soup.find_all('a', class_='post-item-title')
        return [(link['href'], link.get_text()) for link in links]
    
    
    if __name__ == '__main__':
        for result in parse(craw(urls[0])):
            print(result)

    5、多线程实现生产者消费者模型

    import requests
    import queue
    from bs4 import BeautifulSoup
    import time
    import threading
    
    urls = []
    for i in range(2, 52):
        urls.append('https://www.cnblogs.com/#p{0}'.format(i))
    
    
    def craw(url):
        r = requests.get(url)
        return r.text
    
    
    def parse(html):
        soup = BeautifulSoup(html, 'html.parser')
        links = soup.find_all('a', class_='post-item-title')
        # 返回页面中的每篇文章的链接和标题
        return [(link['href'], link.get_text()) for link in links]
    
    
    # 生产者生产任务
    def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
        while True:
            url = url_queue.get()
            html = craw(url)
            html_queue.put(html)
            print('生产者:', threading.current_thread().name, url, 'url_queue.size={0}'.format(url_queue.qsize()))
            time.sleep(2)
    
    
    # 消费者消费任务
    def do_parse(html_queue: queue.Queue, fout):
        while True:
            html = html_queue.get()
            results = parse(html)
            for res in results:
                fout.write(str(res) + '\n')
            print('消费者', threading.current_thread().name, 'results.size', len(results), 'html_queue.size={0}'.format(html_queue.qsize()))
            time.sleep(2)
    
    
    if __name__ == '__main__':
        url_queue = queue.Queue()
        html_queue = queue.Queue()
        for url in urls:
            url_queue.put(url)
        # 生产者开启3个线程
        for id in range(3):
            t = threading.Thread(target=do_craw, args=(url_queue, html_queue), name='craw{0}'.format(id))
            t.start()
        
        # 消费者开启2个线程
        # 把消费的任务写到文件中
        fout = open('02.data.txt', 'w')
        for id in range(2):
            t = threading.Thread(target=do_parse, args=(html_queue, fout), name='parse{0}'.format(id))
            t.start()

    十一、python线程安全问题以及解决方法

    ①概念:线程安全指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

    由于线程的执行随时会发生切换,就造成了不可预料的结果,出现线程不安全

    问题的代码:

    import threading
    import time
    
    
    class Account:
        def __init__(self, balance):
            self.balance = balance
    
    
    def draw(account, amount):
        if account.balance >= amount:
            time.sleep(1)
            print(threading.current_thread().name, '取钱成功')
            account.balance -= amount
            print(threading.current_thread().name, '余额', account.balance)
        else:
            print(threading.current_thread().name, '取钱失败,余额不足')
    
    
    if __name__ == '__main__':
        account = Account(1000)
        ta = threading.Thread(name='ta', target=draw, args=(account, 800))
        tb = threading.Thread(name='tb', target=draw, args=(account, 800))
    
        ta.start()
        tb.start()

     打印结果:

    发现执行的是错误的结果。开启了两个线程,去取钱,第一个线程去取钱(1000-800)还剩200,第二个线程取钱应该执行的是else后面的代码,打印余额不足才对。但是因为多个线程去执行时会发生线程切换,当第一个线程在减去余额之前,切换了第二个线程去取钱,这个时候,账户还是1000元,这就是线程安全问题。

    ②Lock用于解决线程安全问题

    import threading
    import time
    
    lock = threading.Lock()
    
    
    class Account:
        def __init__(self, balance):
            self.balance = balance
    
    
    def draw(account, amount):
        with lock:
            if account.balance >= amount:
                time.sleep(1)
                print(threading.current_thread().name, '取钱成功')
                account.balance -= amount
                print(threading.current_thread().name, '余额', account.balance)
            else:
                print(threading.current_thread().name, '取钱失败,余额不足')
    
    
    if __name__ == '__main__':
        account = Account(1000)
        ta = threading.Thread(name='ta', target=draw, args=(account, 800))
        tb = threading.Thread(name='tb', target=draw, args=(account, 800))
    
        ta.start()
        tb.start()

    十二、线程池

    1、线程池的原理

    2、使用线程池的好处

    ①提升性能:因为减去了大量新建、终止线程的开销,重用了线程资源。

    ②适用场景:适合处理突发性大量请求或需要大量线程完成任务、但实际任务处理时间较短。

    ③防御功能:能有效避免系统因为创建线程过多,而导致系统负荷过大相应变慢等问题。

    ④代码优势:使用线程池的语法比自己新建线程执行线程更加简洁。

    3、 ThreadPoolExecutor的使用语法

    4、使用线程池改造爬虫程序

    import concurrent.futures
    
    
    import requests
    from bs4 import BeautifulSoup
    
    urls = []
    for i in range(2, 52):
        urls.append('https://www.cnblogs.com/#p{0}'.format(i))
    
    
    def craw(url):
        r = requests.get(url)
        return r.text
    
    
    def parse(html):
        soup = BeautifulSoup(html, 'html.parser')
        links = soup.find_all('a', class_='post-item-title')
        return [(link['href'], link.get_text()) for link in links]
    
    
    with concurrent.futures.ThreadPoolExecutor() as pool:
        htmls = pool.map(craw, urls)
        htmls = list(zip(urls, htmls))
        for url, html in htmls:
            pass
            print(url, len(html))
    print('生产者:end')
    
    
    with concurrent.futures.ThreadPoolExecutor() as pool:
        futures = {}
        for url, html in htmls:
            future = pool.submit(parse, html)
            futures[future] = url
        for future, url in futures.items():
            print(url, future.result())
    print('消费者:end')

    十三、Python使用线程池在web服务中实现加速

    1、web服务的架构以及特点

     web后台服务的特点:

    ①web服务对响应时间要求非常高,比如要求200ms返回

    ②web服务有大量的依赖IO操作的调用,比如磁盘文件、数据库、远程API

    ③web服务经常需要处理几万人、几百万人的同时请求

    2、使用线程池ThreadPoolExecutor加速

    使用线程池ThreadPoolExecutor的好处:

    ①方便的将磁盘文件、数据库、远程API的IO调用并发执行

    ②线程池的线程数目不会无限创建(导致系统挂掉),具有防御功能

    import flask
    import json
    import time
    app = flask.Flask(__name__)
    
    
    def read_file():
        time.sleep(0.1)
        return 'file_result'
    
    
    def read_db():
        time.sleep(0.2)
        return 'db_result'
    
    
    def read_api():
        time.sleep(0.3)
        return 'api_result'
    
    
    @app.route('/')
    def index():
        result_file = read_file()
        result_db = read_db()
        result_api = read_api()
        return json.dumps({
            'result_file': result_file,
            'result_db': result_db,
            'result_api': result_api,
        })
    
    
    if __name__ == '__main__':
        app.run()

    运行程序,获取花费多长时间:

    600多毫秒。windows可以下载postman软件来查看耗时。

    然后下面我们采用此案城池来进行加速:

    import flask
    import json
    import time
    from concurrent.futures import ThreadPoolExecutor
    
    app = flask.Flask(__name__)
    pool = ThreadPoolExecutor()
    
    
    def read_file():
        time.sleep(0.1)
        return 'file_result'
    
    
    def read_db():
        time.sleep(0.2)
        return 'db_result'
    
    
    def read_api():
        time.sleep(0.3)
        return 'api_result'
    
    
    @app.route('/')
    def index():
        result_file = pool.submit(read_file)
        result_db = pool.submit(read_db)
        result_api = pool.submit(read_api)
        return json.dumps({
            'result_file': result_file.result(),
            'result_db': result_db.result(),
            'result_api': result_api.result(),
        })
    
    
    if __name__ == '__main__':
        app.run()

    运行程序,获取花费多长时间:

     300多毫秒,时间相比较加速了一倍。

    十四、使用多进程multiprocessing加速

    1、有了多线程threading,为什么还要用多进程multiprocessing

     如果遇到了CPU密集型计算,多线程反而会降低执行速度。

     mutilprocessing模块就是python为了解决GIL缺陷引入的一个模块,原理是用多进程在多CPU上并行执行。

    2、多进程multiprocessing知识梳理(对比多线程threading)

    3、代码实战:单线程、多线程、多进程对比CPU密集计算速度

     代码演示:计算一个cpu密集型任务,单线程、多线程、多进程三种方法执行的效率

    import math
    import time
    from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
    num_list = [112272535095293] * 100
    
    
    # 判断一个数字是否为素数:cpu计算密集型任务
    def is_prime(n):
        if n < 2:
            return False
        if n == 2:
            return True
        if n % 2 == 0:
            return False
        # 对此数字开根号
        sqrt_n = int(math.floor(math.sqrt(n)))
        for i in range(3, sqrt_n + 1, 2):
            if n % i == 0:
                return False
        return True
    
    
    # 单线程
    def single_thread():
        for num in num_list:
            is_prime(num)
    
    
    # 多线程
    def multi_thread():
        with ThreadPoolExecutor() as pool:
            pool.map(is_prime, num_list)
    
    
    # 多进程
    def multi_process():
        with ProcessPoolExecutor() as pool:
            pool.map(is_prime, num_list)
    
    
    if __name__ == '__main__':
        start = time.time()
        single_thread()
        end = time.time()
        print('单线程:', end - start, '')
        start = time.time()
        multi_thread()
        end = time.time()
        print('多线程:', end - start, '')
        start = time.time()
        multi_process()
        end = time.time()
        print('多进程:', end - start, '')

     运行程序,输出结果查看,多线程花费的时间比单进程花费的时间还多,多进程花费的时间相比就很快。

     4、python在Flask项目中使用多进程池进行加速

    import math
    import json
    import flask
    from concurrent.futures import ProcessPoolExecutor
    app = flask.Flask(__name__)
    
    
    # 判断一个数字是否为素数
    def is_prime(n):
        if n < 2:
            return False
        if n == 2:
            return True
        if n % 2 == 0:
            return False
        # 对此数字开根号
        sqrt_n = int(math.floor(math.sqrt(n)))
        for i in range(3, sqrt_n + 1, 2):
            if n % i == 0:
                return False
        return True
    
    
    @app.route('/is_prime/<num_list>')
    def api_is_prime(num_list):
        num_list = [int(x) for x in num_list.split(',')]
        result = pool.map(is_prime, num_list)
        return json.dumps(dict(zip(num_list, result)))
    
    
    if __name__ == '__main__':
        pool = ProcessPoolExecutor()
        app.run()

    运行程序,访问:

    十五、python异步IO实现并发爬虫

    1、原理

     

     2、asyncio使用

     

    代码演示:

    import asyncio
    import aiohttp
    import time
    
    
    urls = []
    for i in range(2, 52):
        urls.append('https://www.cnblogs.com/#p{0}'.format(i))
    
    
    # 定义协程
    async def async_craw(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                result = await  resp.text()
                print('craw url:', url, len(result))
    
    loop = asyncio.get_event_loop()
    
    
    # 定义超级循环
    tasks = [loop.create_task(async_craw(url)) for url in urls]
    
    start = time.time()
    loop.run_until_complete(asyncio.wait(tasks))
    end = time.time()
    print('use time', end - start, '')

     单线程异步爬虫花费的时间是0.4秒。之前我们演示的单线程爬虫耗时:8秒,和多线程爬虫耗时1秒(见章节九)

    3、在异步IO中使用信号量控制爬虫并发度:信号量(Semaphore)

    信号量(Semaphore):又称为信号量、旗语。是一个同步对象,用于保持在0至指定最大值之间的一个计数值。

    当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一。

    当线程完成一次对semaphore对象的释放(release)时,计数值加一。

    当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。

    semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态。

     代码演示:

    import asyncio
    import aiohttp
    import time
    
    
    # 加入信号量,控制并发度
    semaphore = asyncio.Semaphore(10)
    
    
    urls = []
    for i in range(2, 52):
        urls.append('https://www.cnblogs.com/#p{0}'.format(i))
    
    
    # 定义协程
    async def async_craw(url):
        async with semaphore:
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    result = await  resp.text()
                    # 做演示,方便观察,睡3秒
                    await asyncio.sleep(3)
                    print('craw url:', url, len(result))
    
    loop = asyncio.get_event_loop()
    
    
    # 定义超级循环
    tasks = [loop.create_task(async_craw(url)) for url in urls]
    
    start = time.time()
    loop.run_until_complete(asyncio.wait(tasks))
    end = time.time()
    print('use time', end - start, '')

    十六、python使用subprocess播放歌曲

    1、介绍

    2、使用

    3、代码演示

    播放歌曲:

    import subprocess
    
    proc = subprocess.Popen(
        ['start', 'E:\My_Study\data\燕无歇-蒋雪儿.mp3'],
        shell=True
    )
    
    proc.communicate()

    运行程序,实现播放歌曲。

  • 相关阅读:
    java 截取pdf
    webService 发送soap请求,并解析返回的soap报文
    常用网址
    扫描文件夹下代码行数
    CodeMIrror 简单使用
    常用 linux 命令(部分)
    windows下RabbitMQ 监控
    一定要写的日志
    创业思路
    10月9日后计划
  • 原文地址:https://www.cnblogs.com/zhangguosheng1121/p/15460216.html
Copyright © 2020-2023  润新知