Python踩坑王(第二期)并行处理/多线程/多进程的使用——又名:在Python里使用并行到底是鸡肋还是尽善尽美?

原创,未经授权请勿转载!
开发基于python3.7;
IDE是pycharm2021社区版;
侵删!侵删!侵删!重要的事情要说三遍。

记一次Python使用多线程时遇到的奇怪bug

简单的开始

需求:
需要对大量的原始数据逐个执行一次验证操作,原始数据org_data量特别大,每次识别需要经历提取和验证两次操作。
于是乎,up主自然而言就想到了通过写两个函数,通过多线程并行处理提取和验证。
(因保密问题,本文代码为演示版,仅用来展示用法和原理,非原版代码。)

方案v1.0:

  1. 多线程或多进程设定,通过 concurrent.futures包 来实现,简单方便快捷;
  2. 一个先进先出的队列,使用 queue包 来实现,作为线程或进程间安全通信的工具;
  3. 函数a put,负责从原始数据org_data提取出数据,并放入队列;
  4. 函数b rec,负责从队列中取数,处理完之后,放入到result_list结果列表中;

v1.0代码:

from concurrent.futures import ThreadPoolExecutor
import queue
import time
org_data = list(range(1000))
midqueue = queue.Queue()
result_list = []
t = ThreadPoolExecutor(max_workers=2)
# 读取函数
def a():
    while org_data:
        temp_put = org_data.pop() # 模拟原版数据读取操作
        print(f'put:{temp_put}')
        midqueue.put(temp_put)
    print(put.done())
put = t.submit(a)

def b():
    while not midqueue.empty():
        temp_get = midqueue.get()
        print(f'get:{temp_get}')
        result_list.append(temp_get)
    print(result_list)
get = t.submit(b)
t.shutdown(wait=True)

v1.0设想的一切美好,但……代码跑起来,却并不是!
问题:
v1.0bug信息
可以看到,线程get(函数b)只执行了一部分,就退出了。

原因:
如果函数a执行的速度慢,导致queue里没对象了,线程get以为任务已经完成了!
好嘛,并行还得考虑这个。OK,V1.1解决它。

方案v1.1:

  1. 多线程或多进程设定,通过 concurrent.futures包 来实现,简单方便快捷;
  2. 一个先进先出的队列,使用 queue包 来实现,作为线程或进程间安全通信的工具;
  3. 函数a put,负责从原始数据org_data提取出数据,并放入队列;
  4. 函数b rec,负责从队列中取数,处理完之后,放入到result_list结果列表中;
  5. 让函数b rec,同时监控线程put,如果线程put没结束,即使queue为空,也继续循环。

v1.1代码:

org_data = list(range(1000))
midqueue = queue.Queue()
result_list = []
t = ThreadPoolExecutor(max_workers=2)
# 读取函数
def a():
    while org_data:
        temp_put = org_data.pop() # 模拟原版数据读取操作
        print(f'put:{temp_put}')
        midqueue.put(temp_put)
    print(put.done())
put = t.submit(a)

def b():
    while (not midqueue.empty() or not put.done()):
        temp_get = midqueue.get()
        print(f'get:{temp_get}')
        result_list.append(temp_get)
    print(f'最终结果是:{result_list}')
get = t.submit(b)
t.shutdown(wait=True)

运行结果,似乎OK!
[假装有图]


进阶搞事

虽然v1.1可以跑起来了,但实际需求要更加复杂一些。
最大的区别是,函数b是可以进行批处理的,可以一次性传一个batch,处理起来速度并不比单条慢多少;还可能函数b是一个有使用次数限制或者按次收费的API。
我想,函数a能不能攒一部分结果,达到某个数量之后,再往b里传。
于是乎,v2.0来了。

方案v2.0:

  1. 多线程或多进程设定,通过 concurrent.futures包 来实现,简单方便快捷;
  2. 一个先进先出的队列,使用 queue包 来实现,作为线程或进程间安全通信的工具;
  3. 函数a put,负责从原始数据org_data提取出数据,攒够了150个之后,再打包统一放入队列;
  4. 函数b rec,负责从队列中取数,处理完之后,放入到result_list结果列表中;
  5. 让函数b rec,同时监控线程put,如果线程put没结束,即使queue为空,也继续循环。

v2.0代码:

org_data = list(range(1000))
midqueue = queue.Queue()
result_list = []

t = ThreadPoolExecutor(max_workers=2)
# 读取函数
def a():
    mid_temp_list = []
    while org_data:
        temp_put = org_data.pop() # 模拟原版数据读取操作
        print(f'put:{temp_put}')
        mid_temp_list.append(temp_put)
        if len(mid_temp_list) == 150:
            midqueue.put(mid_temp_list)
            mid_temp_list = []
        else:
            pass
    print(put.done())
put = t.submit(a)

def b():
    while (not midqueue.empty() or not put.done()):
        temp_get = midqueue.get()
        print(f'get:{temp_get}') # 假设这里的b函数可以自动做批处理
        result_list.append(temp_get)
    print(f'最终结果是:{result_list}')
get = t.submit(b)
t.shutdown(wait=True)

然后果然不出所料,出问题了。
问题:
在这里插入图片描述
死锁了!
在这里插入图片描述
最后输出的rensult是这样……

原因:
其实很简单,但当时排查了很久,问题出在 150 的那个容量上。
按照v2.0的代码,是每满150,就往queue里写一次,但1000整除150是除不尽的,最后还剩下100个数,没往队列里放。原本我以为这样只会丢数据而已,但实际上却影响了线程get。
另一个关键在于,Queue的get函数,是阻塞的。
在这里插入图片描述

我们设想一下最后100个数时,put和get之间通信是这样的:
get:queue空了,但是,put,你还在运行吗?
put:我还在运行,请继续执行if选项。
get:好咧,我执行temp_get = midqueue.get()操作先!
(问题来了,queue.get()是阻塞的,阻塞的意思是,get()函数会一直等待queue里被填进东西,不进来,就一直等着。)
put:好了,我运行完了,那100数算了,扔了,不放队列了。
get:(苦苦等待中……)

死锁的问题似乎解决了。v2.0.1修复bug!
v2.0.1代码:

org_data = list(range(1000))
midqueue = queue.Queue()
result_list = []

t = ThreadPoolExecutor(max_workers=2)
# 读取函数
def a():
    mid_temp_list = []
    while org_data:
        temp_put = org_data.pop() # 模拟原版数据读取操作
        print(f'put:{temp_put}')
        mid_temp_list.append(temp_put)
        if len(mid_temp_list) == 150:
            midqueue.put(mid_temp_list)
            mid_temp_list = []
        else:
            pass
    if mid_temp_list:
    	midqueue.put(mid_temp_list)
    else:
    	pass
    print(put.done())
put = t.submit(a)

def b():
    while (not midqueue.empty() or not put.done()):
        temp_get = midqueue.get()
        print(f'get:{temp_get}') # 假设这里的b函数可以自动做批处理
        result_list.append(temp_get)
    print(f'最终结果是:{result_list}')
get = t.submit(b)
t.shutdown(wait=True)

运行结果,OK,没问题了!
[假装有图]

一点小担心,再进阶一下

v2.0.1的结果看起来没什么问题了。
但是在b函数的判断逻辑上,我还是有一点杞人忧天的担心。
b函数的while逻辑判断是:

while (not midqueue.empty() or not put.done())

如果我没记错的话,计算机会先判断

(not midqueue.empty())

再判断

(not put.done())

那么,会不会有一种很凑巧的情况:

  • b函数先判断 midqueue里,是空;在它判断完之后,a函数正好运行完毕,并将数据传入了midqueue里,同时,put线程关闭。
  • b函数再判断 put线程也是关闭的,正好,循环结束。

这样,就会有一部分数据漏在了queue里。
这个担忧已经超越了我的知识范畴,先Mark,等有机会再向开发大佬们请教一下吧!
不过有备无犯,代码被暂时我改成了v2.0.2版本:

org_data = list(range(1000))
midqueue = queue.Queue()
result_list = []

t = ThreadPoolExecutor(max_workers=2)
# 读取函数
def a():
    mid_temp_list = []
    while org_data:
        temp_put = org_data.pop() # 模拟原版数据读取操作
        print(f'put:{temp_put}')
        mid_temp_list.append(temp_put)
        if len(mid_temp_list) == 150:
            midqueue.put(mid_temp_list)
            mid_temp_list = []
        else:
            pass
    if mid_temp_list:
    	midqueue.put(mid_temp_list)
    else:
    	pass
put = t.submit(a)

def b():
    while True:
        if midqueue.empty():
            if put.done():
                if midqueue.empty():
                    break
                else:
                    continue
        else:
            temp_get = midqueue.get()
            print(f'get:{temp_get}') # 假设这里的b函数可以自动做批处理
            result_list.append(temp_get)
    print(f'最终结果是:{result_list}')
get = t.submit(b)
t.shutdown(wait=True)

笨人也有办法嘛!ヾ(◍°∇°◍)ノ゙
(双手搓丸子的那鲁托点头称是!)

连夜赶了个稿,本来还想记录下Python并行处理的两种最常用设计模式,下次再说吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Python中,我们可以同时使用多线程多进程来实现并行处理任务。 多线程多进程都是实现并行计算的方式,但它们的实现方式不同。多线程是在同一进程内创建多个线程,每个线程独立运行,但它们共享进程的资源。多进程是创建多个独立的进程,每个进程拥有自己的资源,它们之间通过进程间通信(IPC)进行数据交换。 在Python中,我们可以使用`multiprocessing`模块来创建多进程使用`threading`模块来创建多线程。同时使用多线程多进程可以充分利用CPU资源,提高程序的运行效率。 以下是一个示例代码,同时使用多线程多进程来处理数据: ```python import multiprocessing import threading def process_data(data): # 进程处理数据 pass def thread_data(data): # 线程处理数据 pass if __name__ == '__main__': # 创建进程 process1 = multiprocessing.Process(target=process_data, args=(data,)) process2 = multiprocessing.Process(target=process_data, args=(data,)) # 启动进程 process1.start() process2.start() # 创建线程 thread1 = threading.Thread(target=thread_data, args=(data,)) thread2 = threading.Thread(target=thread_data, args=(data,)) # 启动线程 thread1.start() thread2.start() ``` 在上面的示例代码中,我们创建了两个进程和两个线程来处理数据。在实际应用中,我们需要根据具体情况选择使用多线程多进程或者两者结合使用,以达到最优的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值