应用场景:不断消费一个容器里面的数据,使用同一个线程池,实现高可用性并减少系统性能开销;(这里拿redis作为容器来做示范),线程池的使用请查看https://www.cnblogs.com/hoojjack/p/10846010.html。
需求:程序开始前创建一个线程池,然后一直用这个线程池来运行程序,不销毁这个线程池,尽量高效的使用这个线程池;实现高可用性和节约内存的作用;
线程池消费原理:将任务都放在一个任务池A里面,然后等线程池里面的线程空闲了就分发给空闲的线程,然后线程的任务执行完毕后继续接任务;这就要控制一下接受任务的速度,如果无限循环的将任务写入任务池A里面,那么就会导致系统内存爆炸,例如迭代版本二实例;
主体程序:
import redis, time from concurrent.futures import ThreadPoolExecutor class Redis_demo(object): def __init__(self): self.redis_pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0, decode_responses=True) def consumer(self): redis_client = redis.Redis(connection_pool=self.redis_pool) data = redis_client.rpop("test_qly") return data def List_all(self): lists = [] while True: data = self.consumer() lists.append(data) if len(lists) > 20: return lists def parse(self, data): time.sleep(5) return data + "," def Future(self, future): response = future.result() print(response) def main(self): while True: datas = self.List_all() obj_list = [] threadPool = ThreadPoolExecutor(max_workers=4) for data in datas: future = threadPool.submit(self.parse, data) obj_list.append(future) future.add_done_callback(self.Future) threadPool.shutdown(wait=True)
迭代版本一:
if __name__ == '__main__': cla = Redis_demo() while True: datas = cla.List_all() obj_list = [] threadPool = ThreadPoolExecutor(max_workers=4) for data in datas: future = threadPool.submit(cla.parse, data) obj_list.append(future) future.add_done_callback(cla.Future) threadPool.shutdown(wait=True)
缺点:while循环每次执行的时候都要新创建一个线程池,把任务执行完毕之后再销毁;下一次的循环再创建一个线程池,没有实现线程的高可用性;
迭代版本二:
if __name__ == '__main__': cla = Redis_demo() threadPool = ThreadPoolExecutor(max_workers=4) while True: datas = cla.List_all() obj_list = [] for data in datas: future = threadPool.submit(cla.parse, data) obj_list.append(future) future.add_done_callback(cla.Future) # threadPool.shutdown(wait=True) # 在这个例子里面必须要关闭这个,因为执行这个后,线程池就会销毁掉,那么下一次循环调用线程池的话就会报错,因为线程池已经销毁掉了
缺点:这个其实也叫错误,在开头已经说了,再详细说一遍吧;while循环会一直把任务存进任务池A里面供线程池来消费,但是存任务的速度远远比线程池里面消费的速度快,这就导致任务池A里面存放的任务一直在变多,然后系统内存就会爆炸;程序执行一会后系统内存就会占用100%,然后该任务会被系统kill掉。
迭代版本三:
def __init__(self):
self.thread_time = 0
self.times = 0
self.redis_pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0, decode_responses=True)
def Future(self, future):
response = future.result()
print(response)
status = future.done()
if status:
self.times += 1
# 重写上面两个方法
if __name__ == '__main__': cla = Redis_demo() threadPool = ThreadPoolExecutor(max_workers=4) while True: datas = cla.List_all() obj_list = [] for data in datas: future = threadPool.submit(cla.parse, data) obj_list.append(future) future.add_done_callback(cla.Future) # 防止线程池里面堆积太多的数据引起内存爆炸 while True: if cla.times > cla.thread_time-1: cla.times = 0 break else: time.sleep(2)
实现原理:用future.done()来判断所有线程执行的任务数,用全局变量times来累计,如果全局变量times与任务数相同了,代表该任务池A里面所有的任务都执行完了,线程池里面的线程也空闲了,然后再把任务写入任务池A中,如果全局变量times小于任务数,则进入等待阶段,知道所有的任务全部执行完毕;
好处:实现了线程池部分高可用性(一个线程池从头用到尾)、节约了内存开销;
缺点:该线程池中如果有一个线程反应慢了,那么其他空闲线程就会等待这个响应慢的线程结束后才会重新拉取任务;严重一点,如果一个线程堵塞了,那么就会卡住,因为全局变量times总是小于任务数;还有就是必须要用到(future.add_done_callback(cla.Future)),如果没有回调函数就没法统计执行完毕的任务数。
迭代版本四(终极版):
def parse(self, data):
print(threading.current_thread().name) # 打印线程名字,监控是否有线程一直在堵塞状态
time.sleep(5) return data + ","
# 重写上面的方法,打印线程号
if __name__ == '__main__': cla = Redis_demo() threadPool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="threadName") # 线程序号前面添加默认名称,例如:线程名字是_1,添加后就成为threadName_1
while True: datas = cla.List_all() obj_list = [] for data in datas: future = threadPool.submit(cla.parse, data) obj_list.append(future) future.add_done_callback(cla.Future) # 检测线程池是否空闲了,如果有空闲的就执行新任务,如果所有线程都在执行任务,那么就进入等待阶段 while True: status = False for future in obj_list[-4:]: # 这里的4与线程池里面的线程数保持一致,每次的任务数尽量是线程池数量的整数倍 status = future.done() # 查看任务执行状态,True表示该任务已经执行完毕 print(status, "打印执行状态") if status: break if status: break else: time.sleep(1) # threadPool.shutdown(wait=True) # 这行代码其实没什么实质性作用,因为线程池从程序运行开始就不会停,直到程序停止
实现原理:将所有任务放到一个列表obj_list中,然后不断的循环验证左右的任务是否有执行完毕的,去最后4个任务查看一下状态,如果最后4个里面也有执行完毕的,那么就写入新的任务到任务池A;
好处:需求里面要求的点基本上全部都实现了;
缺点:严格来说,如果有堵塞的线程,那么就会减少工作的线程,所以就要求对程序要有严格的测试,保证程序不会出现严重堵塞的问题;其次,最好还是打印一下线程名称,这样就能更好的监控是否有堵塞的线程;
代码解决办法:1、建一个元祖,将线程号存入元祖,然后每隔一段时间检测一下元祖中元素的数量,如果小于线程数就发出报警信息;2、建一个字典,将每个线程出现的次数计入字典中,如果线程间数量相差太多的话发出报警信息。