Python并发编程
多线程 和多进程 是操作系统层面的并发,而多协程 是用户层面的并发。多线程 是一个进程内的多个执行流,多进程 是多个独立的进程,而多协程是在单个线程内实现的轻量级任务切换。
怎么选择?
- 什么是I/O密集型任务和CPU密集型任务?
- I/O密集型任务:主要消耗时间在等待I/O操作(读取磁盘、内存、网络)完成上,如文件读写、网络请求等。
- CPU密集型任务:主要消耗时间在计算上,如复杂的数学运算、图像处理、压缩解压缩等。
- 多进程、多线程和多协程的对比
一个进程中可以包含多个线程,而每个线程可以包含多个协程。
- 多线程:相比于多进程 开销小;但是由于GIL的存在,只能并发执行,不能利用多CPU;相对于协程,启数目有限制,占用内存较多,开销较大。适用于I/O密集型任务。
- 多进程:可以充分利用多CPU;占用资源最多 ,可启动数目比线程小。适用于CPU密集型任务。
- 多协程:相比于线程和进程,开销最小 ,可启动数目最多;但是支持的库有限制,代码编写复杂。适用于I/O密集型任务,适用于超多任务运行的场景。
全局解释器锁(GIL)
python速度慢的两大原因
- 解释型语言:Python是一种解释型语言,代码在运行时需要经过解释器逐行解释执行,相比于编译型语言,执行速度较慢。
- 全局解释器锁(GIL):Python的多线程实现中存在全局解释器锁(GIL),它限制了同一时刻只有一个线程可以执行Python字节码,导致多线程无法充分利用多核CPU的优势。
GIL是什么
全局解释器锁。它使得任何时刻仅有一个线程在执行。即便是在多核CPU上运行的Python程序,也只能有一个线程在某一时刻执行。
为什么会有GIL
GIL的设计初衷是为了简化内存管理和垃圾回收机制,避免多线程同时访问共享资源时出现数据不一致的问题。通过引入GIL,Python解释器可以确保在任何时刻只有一个线程在执行,从而避免了复杂的锁机制,提高了单线程程序的性能。
怎么规避GIL的影响
- 使用多线程:对于I/O密集型任务,多线程可以有效地利用等待I/O操作的时间,提升程序的并发性能。但是多线程用于CPU密集型任务时,由于需要频繁切换线程,反而会降低性能。
- 使用多进程:对于CPU密集型任务,可以使用多进程来充分利用多核CPU的优势。每个进程都有自己的Python解释器和内存空间,避免了GIL的限制。
多线程(threading模块)
基础语法示例代码
python
import threading
import requests
import time
url = [
f"https://www.cnblogs.com/#p{page}" for page in range(1, 50)
]
def crawl(url: str):
response = requests.get(url)#发送HTTP GET请求
print(url, len(response.text))
def single_thread_crawl():
print("单线程爬取开始")
begin_time = time.time()
for u in url:
crawl(u)
end_time = time.time()
print("单线程爬取结束")
print(f"单线程爬取耗时: {end_time - begin_time} 秒")
def multi_thread_crawl():
print("多线程爬取开始")
begin_time = time.time()
threads = []
for u in url:
threads.append(
threading.Thread(target=crawl, args=(u,))
)
for t in threads:
t.start()
for t in threads:
t.join()
end_time = time.time()
print("多线程爬取结束")
print(f"多线程爬取耗时: {end_time - begin_time} 秒")
single_thread_crawl()
multi_thread_crawl()
多组件pipeline技术架构
多组件pipeline技术架构是一种将复杂任务拆分为多个独立组件,并通过管道(pipeline)连接这些组件以实现数据流动和处理的架构模式。这种架构通常用于数据处理、图像处理、音频处理等领域,能够提高系统的可维护性、可扩展性和性能。
- 每个组件负责特定的任务,如数据采集、预处理、分析、存储等。
- 组件之间通过管道连接,数据从一个组件流向下一个组件。
多线程数据通信(queue.Queue)
在多线程编程中,线程之间需要进行数据通信和共享。Python的
queue模块提供了一个线程安全的队列类,可以方便地实现多线程之间的数据通信。接下来是一个使用
queue.Queue实现多线程数据通信的示例代码:我是要展示一个生产者-消费者模型的例子,其中一个线程负责生产数据,另一个线程负责消费数据。
blog_spider.py
python
import threading
import requests
import time
from bs4 import BeautifulSoup
url = [
f"https://www.cnblogs.com/#p{page}" for page in range(1, 10)
]
def crawl(url: str):#用于爬取指定URL的网页内容
response = requests.get(url)#发送HTTP GET请求
return response.text#返回网页的HTML内容
def parse(html):
soup = BeautifulSoup(html, "html.parser")#两个参数,第一个是html内容,第二个是解析器,html.parser意思是使用Python内置的HTML解析器
titles = soup.find_all("a", class_="post-item-title")#获取所有class属性为post-item-title的a标签
return [(t["href"], t.get_text()) for t in titles]
producer_consumer.py
python
import queue #queue模块提供了同步的队列类,用于多线程编程中的线程间通信
import blog_spider
import time
import random
import threading
def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
while True:
url = url_queue.get()
if url is None:
url_queue.task_done()
break
html = blog_spider.crawl(url)
html_queue.put(html)
print(threading.current_thread().name,
f"爬取完成: {url}",
"url_queue大小=", url_queue.qsize(),
)
url_queue.task_done()
time.sleep(random.randint(1,2))
def do_parse(html_queue:queue.Queue,fout):
while True:
html = html_queue.get()
if html is None:
html_queue.task_done()
break
results = blog_spider.parse(html)
for result in results:
fout.write(str(result) + "\n")#写入文件,fout是一个文件对象
print(threading.current_thread().name,
f"result大小: {len(results)}",
"html_queue大小=", html_queue.qsize(),
)
html_queue.task_done()
time.sleep(random.randint(1,2))
if __name__ == '__main__':
url_queue = queue.Queue()
html_queue = queue.Queue()
for url in blog_spider.url:
url_queue.put(url)
crawl_threads = []
parse_threads = []
for idx in range(3):
t = threading.Thread(
target=do_crawl,#target参数指定线程要执行的函数
args=(url_queue, html_queue),#args参数传递给目标函数的参数
name=f"爬虫线程-{idx}"
)
t.start()
crawl_threads.append(t)
fout = open("producer_consumer_result.txt", "w", encoding="utf-8")
for idx in range(2):
t = threading.Thread(
target=do_parse,
args=(html_queue, fout),
name=f"解析线程-{idx}"
)
t.start()
parse_threads.append(t)
url_queue.join() # 等待所有任务完成
print("所有URL爬取完成")
for _ in range(3):
url_queue.put(None) # 发送结束信号给爬取线程
for t in crawl_threads:
t.join()
html_queue.join() # 等待所有任务完成
print("所有HTML解析完成")
for _ in range(2):
html_queue.put(None) # 发送结束信号给解析线程
for t in parse_threads:
t.join()
fout.close()
print("所有线程已结束")
线程锁
线程安全是指某个函数、函数库在多线程环境下被调用,能够正确的处理多个线程的共享变量,使程序正确完成。
因此,可以使用Lock对象来实现线程安全,确保同一时刻只有一个线程能够访问共享资源,从而避免数据竞争和不一致的问题。
python
import time
import threading
lock = threading.Lock()
class Account:
def __init__(self,banlance):
self.banlance=banlance
def draw(account,amount):
with lock:#是lock.acquire()和lock.release()的简写形式
if(account.banlance>=amount):
time.sleep(0.1)
print(threading.current_thread().name+"取钱成功!取出金额:"+str(amount))
account.banlance-=amount
print(threading.current_thread().name+"余额为:"+str(account.banlance))
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()
ta.join()
tb.join()
线程池
线程池是一种管理和复用线程的技术,可以避免频繁创建和销毁线程带来的开销,提高程序的性能和响应速度。Python的
concurrent.futures模块提供了一个方便的线程池实现,可以使用ThreadPoolExecutor类来创建和管理线程池。
- map(): 将一个可迭代对象中的每个元素传递给指定的函数,并返回一个包含结果的迭代器。
- submit() : 提交一个可调用对象(函数)到线程池,并返回一个
Future对象,表示该任务的执行结果。
多进程(multiprocessing模块)
多进程是指在操作系统层面上同时运行多个进程,每个进程都有自己的内存空间和资源。Python的
multiprocessing模块提供了一个简单的接口来创建和管理进程。
多进程基础语法
Process类参数:
- target:指定进程要执行的函数。
- args:传递给目标函数的参数,以元组的形式传递。
- kwargs:传递给目标函数的关键字参数,以字典的形式传递。
方法: - start():启动进程,调用目标函数。
- join():等待进程结束,阻塞当前线程直到进程完成。
- is_alive(): 检查进程是否仍在运行,返回布尔值。
属性: - name:进程的名称,可以在创建进程时指定,默认值为"Process-N",其中N是一个递增的整数。
- pid:进程的ID,由操作系统分配的唯一标识符。
python
import time
from multiprocessing import Process
import os
def coding(name,num):
print(os.getpid())#获取当前进程ID
print(os.getppid())#获取父进程ID
for i in range(1,num + 1):
print(f'{name}正在敲第{i}行代码')
time.sleep(0.05)
def music(name,num):
print(os.getpid())
print(os.getppid())
for i in range(1,num + 1):
print(f'{name}正在听第{i}首音乐')
if __name__ == '__main__':
p1 = Process(target=coding, args=('小明', 10),name="小明的编码进程") # 方式一:args方式传参
p2 = Process(target=music, kwargs={'name':'小红', 'num':10},name="小红的音乐进程") # 方式二:kwargs方式传参
p1.start()
p2.start()
print(p1.name," ",p1.pid)
print(p2.name," ",p2.pid)
p1.join()
p2.join()
print(f"主进程的父进程:{os.getppid()}")#pycharm运行时,主进程的父进程是pycharm的进程ID
多进程的特点
数据隔离
进程之间的数据是相互隔离的,每个进程都有自己的内存空间,无法直接访问其他进程的内存数据。
如果需要在进程之间共享数据,可以使用multiprocessing模块提供的Queue、Pipe等通信机制,或者使用共享内存(Value和Array)来实现数据共享。
python
list = []
def producer():
for i in range(10):
list.append(i)
print(f"生产者生产了数据: {i}")
def consumer():
while True:
if list:
item = list.pop(0)
print(f"消费者消费了数据: {item}")
if __name__ == '__main__':
p1 = Process(target=producer)
p2 = Process(target=consumer)
p1.start()
p2.start()
p1.join()
print("生产者进程结束")
p2.join()
print("消费者进程结束")
上面的代码输出只会输出生产者生产的数据,而消费者进程不会消费任何数据,因为两个进程的list变量是独立的,互不影响。
python
from multiprocessing import Process, Queue
queue = Queue()
def producer(q: Queue):
for i in range(5):
q.put(i)
print(f"生产者生产了数据: {i}")
def consumer(q: Queue):
while True:
item = q.get()
if item is None:
break
print(f"消费者消费了数据: {item}")
if __name__ == '__main__':
p1 = Process(target=producer, args=(queue,))
p2 = Process(target=consumer, args=(queue,))
p1.start()
p2.start()
p1.join()
p2.join()
上面的代码中,使用Queue实现了进程间的数据共享,生产者进程将数据放入队列中,消费者进程从队列中获取数据进行消费。
主进程等待子进程结束再结束
在多进程编程中,主进程通常会等待所有子进程结束后再退出。可以使用join()方法来实现这一点。
python
if __name__ == '__main__':
p1 = Process(target=coding, args=('小明', 10))
p2 = Process(target=music, args=('小红', 10))
p1.start()
p2.start()
p1.join() # 主进程等待p1结束
p2.join() # 主进程等待p2结束
print("所有子进程结束,主进程退出")
如果想让主进程结束之后子进程继续运行,可以将子进程设置为守护进程(daemon)。
python
if __name__ == '__main__':
p1 = Process(target=coding, args=('小明', 10))
p2 = Process(target=music, args=('小红', 10))
p1.daemon = True # 设置为守护进程
p2.daemon = True # 设置为守护进程
p1.start()
p2.start()
print("主进程结束,子进程作为守护进程也会结束")
这样,主进程结束后,守护进程也会随之结束,不会继续运行。
多协程(asyncio模块)
多协程是指在单个线程内实现的轻量级任务切换,通过协程可以在等待I/O操作时切换到其他任务,从而提高程序的并发性能。
Python的
asyncio模块提供了一个强大的异步编程框架,可以方便地实现多协程编程。
Greenlet库
Greenlet是一个轻量级的协程库,可以在单个线程内实现多协程的切换。Greenlet通过手动切换协程来实现并发执行,适合I/O密集型任务。
python
from greenlet import greenlet
def foo():
print("foo")
g2.switch()
print("foo again")
g2.switch()
def bar():
print("bar")
g1.switch()
print("bar again")
if __name__ == '__main__':
g1 = greenlet(foo)
g2 = greenlet(bar)
g1.switch()
Asyncio模块
asyncio是Python内置的异步编程模块,提供了事件循环、协程、任务等功能,可以方便地实现多协程编程。适合I/O密集型任务。
基本使用
python
import asyncio
import time
async def task(i):
print(f"Task {i} is starting")
await asyncio.sleep(i) #模拟IO操作
print(f"Task {i} is completed")
return pow(i, 2)
def task_callback(obj):
print(f"Task {obj} is completed, the result is {obj.result()}")
start = time.time()
# 1) 构建一个事件循环
loop = asyncio.get_event_loop()#获取当前线程的事件循环,如果没有则创建一个新的事件循环
# 2) 创建协程对象
tasks = [
asyncio.ensure_future(task(i)) for i in range(5)
]
for task in tasks:
task.add_done_callback(task_callback)
# 3) 收集任务并等待
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(f"All tasks completed in {end - start} seconds")
新版本语法支持
- **asyncio.run()**可以替换掉事件循环的创建和关闭,更加简洁。
- **asyncio.create_task()**可以替换掉asyncio.ensure_future(),更加直观。
- **asyncio.gather()**可以替换掉asyncio.wait(),更加方便地收集任务结果,并且还会返回任务的结果。
python
import asyncio
import time
async def task(i):
print(f"Task {i} is starting")
await asyncio.sleep(i) #模拟IO操作
print(f"Task {i} is completed")
return pow(i, 2)
def task_callback(obj):
print(f"Task {obj} is completed, the result is {obj.result()}")
async def main():
start = time.time()
# 创建协程对象
tasks = [
asyncio.create_task(task(i)) for i in range(5)
]
for t in tasks:
t.add_done_callback(task_callback)
# 收集任务并等待
result = await asyncio.gather(*tasks) # gather方法等待所有任务完成,tasks前面有*号表示将列表解包成单独的参数传递给gather函数
print("Results:", result)
end = time.time()
print(f"All tasks completed in {end - start} seconds")
asyncio.run(main())