python并发编程

一、程序提速的方法

在这里插入图片描述

二、python对并发编程的支持

  • 多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成;
  • 多进程:multiprocess,利用多核CPU的能力,真正的并行执行任务;
  • 异步IO:asyncio,当线程比较多时,切换线程也会占用CPU资源,可在单线程中利用CPU和IO同时执行的原理,实现函数异步执行;
  • 使用Lock对资源加锁,防止冲突访问;
  • 使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式;
  • 使用线程池Pool/进程池Pool,简化线程/进程的任务提交,等待结果、获取结果;
  • 使用subprocess启动外部程序的进程,并进行输入输出交互;

python子进程的启动方法:fork与spawn,参考子进程启动方式

三、python并发编程的三种方式

3.1 3种方式的区别和选择

多线程Thread,多进程Process,多协程Coroutine。
在这里插入图片描述
在这里插入图片描述

3.2 GIL

在这里插入图片描述

四、实战

4.1 多线程

在这里插入图片描述

4.1.1 多线程网页爬虫示例代码

  • blog_spider.py
import requests
from bs4 import BeautifulSoup

urls = [
    f"https://www.cnblogs.com/sitehome/p/{page}"
    for page in range(1, 50 + 1)
]


def craw(url):
    #print("craw url: ", url)
    r = requests.get(url)
    return r.text


def parse(html):
    # class="post-item-title"
    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[2])):
        print(result)
  • producer_consumer_spider.py
import queue
import blog_spider
import time
import random
import threading

# 生产者
def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
    while True:
        url = url_queue.get()
        html = blog_spider.craw(url)
        html_queue.put(html)
        print(threading.current_thread().name, f"craw {url}",
              "url_queue.size=", url_queue.qsize())
        time.sleep(random.randint(1, 2))

# 消费者
def do_parse(html_queue: queue.Queue, fout):
    while True:
        html = html_queue.get()
        results = blog_spider.parse(html)
        for result in results:
            fout.write(str(result) + "\n")
        print(threading.current_thread().name, f"results.size", len(results),
              "html_queue.size=", html_queue.qsize())
        time.sleep(random.randint(1, 2))


if __name__ == "__main__":
    url_queue = queue.Queue()
    html_queue = queue.Queue()
    for url in blog_spider.urls:
        url_queue.put(url)
	#创建生产者线程
    for idx in range(3):
        t = threading.Thread(target=do_craw, args=(url_queue, html_queue),
                             name=f"craw{idx}")
        t.start()

    fout = open("02.data.txt", "w")
    # 创建消费者线程
    for idx in range(2):
        t = threading.Thread(target=do_parse, args=(html_queue, fout),
                             name=f"parse{idx}")
        t.start()

4.1.2 线程安全问题

线程安全指某个函数在多线程环境中被调用时,能够正确的处理多个线程之间的共享变量,使程序功能正确完成。由于线程的执行随时会发生切换,就造成了不可预料的结果,出现线程不安全。

使用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(0.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()

4.2 多进程

在这里插入图片描述

python 线程池实线原理
线程池样例

4.3 异步IO(协程)

在这里插入图片描述
使用协程的时候,要确保使用的库要支持协程,比如requests库不支持协程,可使用aiohttp.

import asyncio
import aiohttp
import blog_spider

# 信号量控制协程的并发度
semaphore = asyncio.Semaphore(10)


async def async_craw(url):
    async with semaphore:
        print("craw url: ", url)
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                result = await resp.text()
                await asyncio.sleep(5)
                print(f"craw url: {url}, {len(result)}")

# 创建时间循环
loop = asyncio.get_event_loop()

tasks = [
    loop.create_task(async_craw(url))
    for url in blog_spider.urls]

import time

start = time.time()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print("use time seconds: ", end - start)

4.4 多线程之间通信

多线程之间要完成的任务是相互独立互不干扰的话,那么线程之间是不需要进行通信的,自己干自己的事就行。但是如果多线程之间要进行合作,那么就必须要进行线程通信。

4.4.1 使用共享变量通信

特点:使用共享变量通信的方式是线程不安全的,一般要结合线程锁使用。

from threading import Thread, Lock
from time import sleep, time
# 两个共享变量
urls = []  # 任务列表,用于存放要爬取的文章详情url
is_finished = False
lock = Lock()

def get_detail_content(urls, name):
    global is_finished

    print("%s开始爬取文章内容" % name)
    while not is_finished or len(urls):  # 如果生产者没有生产完或者生产者生产完了但消费者没有消费完就从urls取出url进行爬取
        try:
            with lock:
                url = urls.pop()
            sleep(0.001)  # 爬取1个内容页花0.001秒
            print("%s 文章:%s 爬取结束" % (name, url))
        except:  # 这里是为了防止消费者消费太快,当生产者还在生产但urls元素为0时,pop会报错。此时应该重新判断urls中是否有元素
            continue
    print("所有文章内容爬取结束")


def get_detail_url(urls):
    global is_finished
    print("开始爬取文章列表页")
    art_id = 10000  # 假设共有10000个文章url
    page_url_num = 100
    start_id = 1
    while start_id < 10000:
        end_id = start_id + page_url_num
        for id in range(start_id, end_id):  # 假设有100页列表页,每一页有100个url,共10000个url
            url = "http://www.zbpblog.com/blog/%d.html" % id
            with lock:
                urls.append(url)
        sleep(0.01)  # 爬一个列表页花0.01秒
        start_id = end_id
    with lock:
        is_finished = True
    print("文章列表页爬取结束")

if __name__ == "__main__":
    st = time()
    print("主线程负责计时")
    # 创建1个生产者线程
    producer = Thread(target=get_detail_url, args=(urls,))
    consumers = []
    for i in range(3):  # 创建3个消费者线程
        name = "Thread %d" % (i + 1)
        consumer = Thread(target=get_detail_content, args=(urls, name))
        consumers.append(consumer)
    producer.start()
    sleep(0.1)  # 睡0.1秒是为了让生产者先生产些链接到urls中
    for consumer in consumers:
        consumer.start()
    producer.join()
    for consumer in consumers:
        consumer.join()
    et = time()
    print("任务结束,耗时:%.2f" % (et - st))

4.4.2 使用队列通信

Queue相比于普通的list结构而言,Queue是线程安全的,而list不是线程安全的。原因是Queue内部使用了锁和条件变量来进行线程同步,但是list没有用到线程同步技术。
备注:

  1. 设置进程不等待消费者线程和生产者线程执行结束而结束,所以对消费者和生产者线程使用 setDaemon(True) 设置为守护线程;
  2. 进程需要等待任务队列中的任务被执行完才结束,使用队列的join()方法, 必须配合task_done()方法使用
from threading import Thread
from time import sleep, time
from queue import Queue

urls = Queue(500)
is_finished = False


def get_detail_content(urls, name):
    print("%s开始爬取文章内容" % name)
    while True:
        url = urls.get()
        sleep(0.001)
        urls.task_done()  # 标记这一次取出来的url任务已经执行完
        print("%s 文章:%s 爬取结束" % (name, url))
    print("所有文章内容爬取结束")


def get_detail_url(urls):
    global is_finished

    print("开始爬取文章列表页")
    art_id = 10000  # 假设共有10000个文章url
    page_url_num = 10
    start_id = 1
    while start_id < 10000:
        end_id = start_id + page_url_num
        for id in range(start_id, end_id):
            url = "http://www.zbpblog.com/blog/%d.html" % id
            urls.put(url)
        sleep(0.01)
        start_id = end_id

    # 生产者全部生产完成标志位
    is_finished = True

    print("文章列表页爬取结束")


if __name__ == "__main__":
    st = time()
    print("主线程负责计时")
    producer = Thread(target=get_detail_url, args=(urls,))
    producer.setDaemon(True)  # 设置为守护线程
    consumers = []
    for i in range(10):
        name = "Thread %d" % (i + 1)
        consumer = Thread(target=get_detail_content, args=(urls, name))
        consumer.setDaemon(True)  # 设置为守护线程
        consumers.append(consumer)
    producer.start()
    for consumer in consumers:
        consumer.start()
    while not is_finished:
        urls.join()  # 等待urls队列的任务被执行完才往下执行
    et = time()
    print("任务结束,耗时:%.2f" % (et - st))

4.4.3 线程同步

线程同步是为了解决多线程编程中,由于竞争使用资源或修改变量而造成数据不一致的问题。
(1)使用锁进行同步,比如互斥锁Lock、重入锁RLock

  • 缺点:
  1. 加锁和释放锁会消耗时间,所以锁会影响程序性能
  2. 锁可能会引起死锁

(2)使用条件变量进行同步
条件变量必须要配合互斥锁使用,因为条件变量是一种多线程竞争的共享资源。通过条件变量可以实现等待和通知的机制。

cond = Condition()  # 创建一个条件变量
cond.acquire()      # 给条件变量上锁
cond.wait()         # 等待,会阻塞下面的代码执行,当其他线程调用notify的时候才会被唤醒
do_something()      
cond.notify()       # 通知和唤醒其他使用了条件变量cond的线程
cond.release()
from threading import Condition,Thread

cond = Condition()
a_say = [1,3,5]
b_say = [2,4,6]

class A(Thread):
    def __init__(self,cond,say):
        super(A,self).__init__(name="A")
        self.cond = cond
        self.say = say

    def run(self):
        self.cond.acquire()
        for i in range(len(self.say)):
            print("%s say %d" % (self.name,self.say.pop(0)))
            self.cond.notify()  # A说完就要通知B,让B开始说

            if len(self.say):
                self.cond.wait()    # A说完就不能在说,而是等待B说完,等B通知到A,A才能继续说

        self.cond.release()

class B(Thread):
    def __init__(self,cond,say):
        super(B,self).__init__(name="B")
        self.cond = cond
        self.say = say

    def run(self):
        self.cond.acquire()
        for i in range(len(self.say)):
            self.cond.wait()    # 一开始是A先说而不是B先说,所以一开始B是处于等待状态
            print("%s say %d" % (self.name,self.say.pop(0)))
            self.cond.notify()  # B说完就要通知A,让A继续说

        self.cond.release()

if __name__=="__main__":
    a = A(cond,a_say)
    b = B(cond,b_say)
    b.start()       # 必须让b线程先启动,a后启动,如果a先启动,那么a会在b没有执行wait()的情况下执行notify(),所以这个notify()通知相当于无效。之后a执行wait().b也执行wait()双方都处于等待,于是这个进程就卡住
    a.start()

4.5 多进程之间通信

参考

### Python 并发编程教程与最佳实践 #### 理解并发编程的重要性 在现代计算环境中,高并发应用程序的需求日益增长。Python作为一种流行的语言,在处理并发任务方面提供了丰富的工具支持[^1]。 #### 主要的并发模型及其特点 Python 支持三种主要的并发模型:线程、进程异步编程。每种模型都有其独特的优势局限性: - **线程**:适合用于I/O密集型任务,如网络请求或文件读写。然而,由于全局解释器锁(GIL),CPU密集型任务可能不会从中受益太多。 - **进程**:绕过了GIL的问题,适用于CPU密集型工作负载。但是创建新进程的成本较高,并且涉及更多的资源消耗。 - **异步编程**:利用协程实现了轻量级的任务切换,非常适合于大量短时间等待的操作,比如Web服务器响应客户端请求[^5]。 #### 编码实例展示不同类型的并发方法 ##### 使用 `threading` 模块实现多线程 ```python import threading def task(name): print(f"Thread {name} is running") threads = [] for i in range(3): t = threading.Thread(target=task, args=(i,)) threads.append(t) t.start() for thread in threads: thread.join() ``` ##### 利用 `multiprocessing` 进行多进程操作 ```python from multiprocessing import Process def process_task(name): print(f'Process {name}: starting') if __name__ == '__main__': processes = [Process(target=process_task, args=(f'{i}',)) for i in range(3)] for p in processes: p.start() for p in processes: p.join() ``` ##### 异步函数定义及执行 ```python import asyncio async def async_function(): await asyncio.sleep(1) print('Async function completed.') loop = asyncio.get_event_loop() loop.run_until_complete(async_function()) ``` #### 关键概念技术要点 当涉及到具体实施时,有几个重要的考虑因素需要牢记: - 对于线程来说,确保任何共享的数据结构都是线程安全的; - 当采用多进程方案时,则需关注跨进程间的有效通讯机制; - 而对于异步编程而言,理解事件循环的工作原理至关重要[^3]。 #### 应对挑战的方法论建议 为了克服并发编程中存在的潜在困难,推荐遵循以下指导原则: - 明确区分任务性质——判断是属于I/O还是CPU绑定类型; - 合理选择合适的并发模式以匹配特定需求; - 探索最新的库更新发展趋势,以便充分利用改进后的特性[^2];
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值