一、进程、线程、协程的区别
- 进程(Process):
-
- 进程是操作系统中独立运行的一个程序。
- 每个进程有自己独立的内存空间和系统资源。
- 进程之间通常需要通过进程间通信(IPC)来进行数据交换。
- 进程的创建和切换开销较大。
- 线程(Thread):
-
- 线程是操作系统调度的最小执行单位。
- 所有线程共享同一进程的内存空间和系统资源。
- 线程之间可以直接访问共享的内存,因此需要注意线程安全问题。
- 线程的创建和切换开销较小。
- 协程(Coroutine):
-
- 协程是一种用户级线程,由程序员控制其调度和切换。
- 协程的执行过程中可以主动让出或恢复执行,以实现非抢占式的协作式多任务。
- 协程通常在单线程中运行,不需要进行上下文切换,因此开销较小。
综上所述,进程是操作系统层面的独立执行单元,线程是操作系统调度的最小执行单位,而协程是由程序员控制调度的用户级执行单位。协程相较于进程和线程,具有更小的开销,并且能够实现非阻塞的协作式多任务。需要特别注意的是,python中的线程和Java中的线程是不同的。
- Python中的线程采用的是全局解释器锁(Global Interpreter Lock,简称GIL)模型。这意味着在同一时间只能有一个Python线程执行Python字节码,无法实现真正的并行执行。GIL的存在导致在CPU密集型任务中,Python多线程的效率可能不如预期。
- Java中的线程没有全局锁的限制,可以实现真正的并行执行。每个Java线程都有自己的栈空间,可以独立运行,并发性能较高。
这也是为什么python的并发操作离不开进程的原因,因为只有多进程才能使用多核资源
二、使用方式
结合项目情况来看,多进程使用方式:
uvicorn.run(app="api:app", host="0.0.0.0", port=9000, workers=5) #启动服务时指定workers数量,其数量就是进程数量
线程使用方式:
pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix='route_thread')
pool.submit(start)
协程使用方式:
#这种方式使用协程,当遇到IO等待时,其不会全局阻塞。但是代码也是同步执行的
async def method1():
await method2()
async def method2():
print("method2")
三、实际使用中遇到的问题和解决方案
1、当发现某个方法存在全局阻塞时,率先查找该方法有没有替代的异步方法。如果有就替换成异步方法,这种情况只需要对代码做修改,性能上没有影响。例如:
retriever.vectorstore.aadd_documents(sub_docs)
可以改为
await retriever.vectorstore.aadd_documents(sub_docs)
2、当某个方法不存在异步方法时,可以使用线程池执行耗时操作并等待线程执行结果。例如:
def read_pdf(file_path):
from langchain_community.document_loaders import PDFMinerPDFasHTMLLoader
loader = PDFMinerPDFasHTMLLoader(file_path)
data = loader.load()[0]
其中 data = loader.load()[0] 就是一个耗时操作,并且其代码库中没有提供异步方法。则可以在上层调用时使用线程池:
future = executor.submit(read_pdf, output_pdf_path)
while not future.done():
# 可以做一些其他的事情
await asyncio.sleep(1)
pass
return future.result()
因为这里的代码需要同步获取调用结果。所以在循环中判断没有返回结果时非阻塞休眠。这样就不会造成全局阻塞,不过也有弊端,那就是该接口的流程执行时间变得更长。
3、当循环中存在耗时操作,并且没有异步方式时,可以在循环中加入非阻塞休眠的代码。同样,原流程执行时间也会变得更长:
for page in pdf:
for i in page.get_images(full=True):
item = page.get_image_bbox(i)
img = fitz.Pixmap(pdf, i[0])
hash_md5 = hashlib.md5()
hash_md5.update(img.samples)
md5 = hash_md5.hexdigest()
name = md5 + ".png" img.save("images/" + name)
text = f" ![]({url + name}) " page.insert_text((item.tl + item.br) / 2, text, fontsize=1)
await asyncio.sleep(0.00001)
四、总结
python官方最建议的并发编程方式还是异步的方式。但是,当其受限于第三方库时,还是需要使用线程或进程解决。不过,需要在使用线程时,考虑整个系统的异步通知机制。不然其在等待结果时同样会造成全局阻塞。同样,单进程在遇到CPU瓶颈时也需要使用多进程,进程的无状态也需要被保障。