概述

传统多线程方案会使用“即时创建, 即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。

一个线程的运行时间可以分为3部分:线程的启动时间、线程体的运行时间和线程的销毁时间。在多线程处理的情景中,如果线程不能被重用,就意味着每次创建都需要经过启动、销毁和运行3个过程。这必然会增加系统相应的时间,降低了效率。

使用线程池:由于线程预先被创建并放入线程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性。

线程池模型

这里使用创建Thread()实例来实现,下面会再用继承threading.Thread()的类来实现

  1. # 创建队列实例, 用于存储任务
  2. queue = Queue()
  3. # 定义需要线程池执行的任务
  4. def do_job():
  5. while True:
  6. i = queue.get()
  7. time.sleep(1)
  8. print 'index %s, curent: %s' % (i, threading.current_thread())
  9. queue.task_done()
  10. if __name__ == '__main__':
  11. # 创建包括3个线程的线程池
  12. for i in range(3):
  13. t = Thread(target=do_job)
  14. t.daemon=True # 设置线程daemon 主线程退出,daemon线程也会推出,即时正在运行
  15. t.start()
  16. # 模拟创建线程池3秒后塞进10个任务到队列
  17. time.sleep(3)
  18. for i in range(10):
  19. queue.put(i)
  20. queue.join()复制代码

  • daemon说明:
    如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出;
    如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。
    daemon=True 说明线程是守护线程,守护线程外部没法触发它的退出,所以主线程退出就直接让子线程跟随退出
  • queue.task_done() 说明:
    queue.join()的作用是让主程序阻塞等待队列完成,就结束退出,但是怎么让主程序知道队列已经全部取出并且完成呢?queue.get() 只能让主程序知道队列取完了,但不代表队列里的任务都完成,所以程序需要调用queue.task_done() 告诉主程序,又一个任务完成了,直到全部任务完成,主程序退出

输出结果

  1. index 1, curent: <Thread(Thread-2, started daemon 139652180764416)>
  2. index 0, curent: <Thread(Thread-1, started daemon 139652189157120)>
  3. index 2, curent: <Thread(Thread-3, started daemon 139652172371712)>
  4. index 4, curent: <Thread(Thread-1, started daemon 139652189157120)>
  5. index 3, curent: <Thread(Thread-2, started daemon 139652180764416)>
  6. index 5, curent: <Thread(Thread-3, started daemon 139652172371712)>
  7. index 6, curent: <Thread(Thread-1, started daemon 139652189157120)>
  8. index 7, curent: <Thread(Thread-2, started daemon 139652180764416)>
  9. index 8, curent: <Thread(Thread-3, started daemon 139652172371712)>
  10. index 9, curent: <Thread(Thread-1, started daemon 139652189157120)>
  11. finish复制代码

可以看到所有任务都是在这几个线程中完成Thread-(1-3)

线程池原理

线程池基本原理: 我们把任务放进队列中去,然后开N个线程,每个线程都去队列中取一个任务,执行完了之后告诉系统说我执行完了,然后接着去队列中取下一个任务,直至队列中所有任务取空,退出线程。

上面这个例子生成一个有3个线程的线程池,每个线程都无限循环阻塞读取Queue队列的任务所有任务都只会让这3个预生成的线程来处理。

具体工作描述如下:

  1. 创建Queue.Queue()实例,然后对它填充数据或任务
  2. 生成守护线程池,把线程设置成了daemon守护线程
  3. 每个线程无限循环阻塞读取queue队列的项目item,并处理
  4. 每次完成一次工作后,使用queue.task_done()函数向任务已经完成的队列发送一个信号
  5. 主线程设置queue.join()阻塞,直到任务队列已经清空了,解除阻塞,向下执行

这个模式下有几个注意的点:

  • 将线程池的线程设置成daemon守护进程,意味着主线程退出时,守护线程也会自动退出,如果使用默认
    daemon=False的话, 非daemon的线程会阻塞主线程的退出,所以即使queue队列的任务已经完成
    线程池依然阻塞无限循环等待任务,使得主线程也不会退出。
  • 当主线程使用了queue.join()的时候,说明主线程会阻塞直到queue已经是清空的,而主线程怎么知道queue已经是清空的呢?就是通过每次线程queue.get()后并处理任务后,发送queue.task_done()信号,queue的数据就会减1,直到queue的数据是空的,queue.join()解除阻塞,向下执行。
  • 这个模式主要是以队列queue的任务来做主导的,做完任务就退出,由于线程池是daemon的,所以主退出线程池所有线程都会退出。 有别于我们平时可能以队列主导thread.join()阻塞,这种线程完成之前阻塞主线程。看需求使用哪个join():
    如果是想做完一定数量任务的队列就结束,使用queue.join(),比如爬取指定数量的网页
    如果是想线程做完任务就结束,使用thread.join()

示例:使用线程池写web服务器


  1. import socket
  2. import threading
  3. from threading import Thread
  4. import threading
  5. import sys
  6. import time
  7. import random
  8. from Queue import Queue
  9. host = ''
  10. port = 8888
  11. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  12. s.bind((host, port))
  13. s.listen(3)
  14. class ThreadPoolManger():
  15. """线程池管理器"""
  16. def __init__(self, thread_num):
  17. # 初始化参数
  18. self.work_queue = Queue()
  19. self.thread_num = thread_num
  20. self.__init_threading_pool(self.thread_num)
  21. def __init_threading_pool(self, thread_num):
  22. # 初始化线程池,创建指定数量的线程池
  23. for i in range(thread_num):
  24. thread = ThreadManger(self.work_queue)
  25. thread.start()
  26. def add_job(self, func, *args):
  27. # 将任务放入队列,等待线程池阻塞读取,参数是被执行的函数和函数的参数
  28. self.work_queue.put((func, args))
  29. class ThreadManger(Thread):
  30. """定义线程类,继承threading.Thread"""
  31. def __init__(self, work_queue):
  32. Thread.__init__(self)
  33. self.work_queue = work_queue
  34. self.daemon = True
  35. def run(self):
  36. # 启动线程
  37. while True:
  38. target, args = self.work_queue.get()
  39. target(*args)
  40. self.work_queue.task_done()
  41. # 创建一个有4个线程的线程池
  42. thread_pool = ThreadPoolManger(4)
  43. # 处理http请求,这里简单返回200 hello world
  44. def handle_request(conn_socket):
  45. recv_data = conn_socket.recv(1024)
  46. reply = 'HTTP/1.1 200 OK \r\n\r\n'
  47. reply += 'hello world'
  48. print 'thread %s is running ' % threading.current_thread().name
  49. conn_socket.send(reply)
  50. conn_socket.close()
  51. # 循环等待接收客户端请求
  52. while True:
  53. # 阻塞等待请求
  54. conn_socket, addr = s.accept()
  55. # 一旦有请求了,把socket扔到我们指定处理函数handle_request处理,等待线程池分配线程处理
  56. thread_pool.add_job(handle_request, *(conn_socket, ))
  57. s.close()复制代码

  1. # 运行进程
  2. [master][/data/web/advance_python/socket]$ python sock_s_threading_pool.py
  3. # 查看线程池状况
  4. [master][/data/web/advance_python/socket]$ ps -eLf|grep sock_s_threading_pool
  5. lisa+ 27488 23705 27488 0 5 23:22 pts/30 00:00:00 python sock_s_threading_pool.py
  6. lisa+ 27488 23705 27489 0 5 23:22 pts/30 00:00:00 python sock_s_threading_pool.py
  7. lisa+ 27488 23705 27490 0 5 23:22 pts/30 00:00:00 python sock_s_threading_pool.py
  8. lisa+ 27488 23705 27491 0 5 23:22 pts/30 00:00:00 python sock_s_threading_pool.py
  9. lisa+ 27488 23705 27492 0 5 23:22 pts/30 00:00:00 python sock_s_threading_pool.py
  10. # 跟我们预期一样一共有5个线程,一个主线程,4个线程池线程复制代码

这个线程池web服务器编写框架包括下面几个组成部分及步骤:

  • 定义线程池管理器ThreadPoolManger,用于创建并管理线程池,提供add_job()接口,给线程池加任务
  • 定义工作线程ThreadManger, 定义run()方法,负责无限循环工作队列,并完成队列任务
  • 定义socket监听请求s.accept() 和处理请求 handle_requests() 任务。
  • 初始化一个4个线程的线程池,都阻塞等待这读取队列queue的任务
  • 当socket.accept()有请求,则把connsocket做为参数,handlerequest方法,丢给线程池,等待线程池分配线程处理

GIL 对多线程的影响

因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

但是对于IO密集型的任务,多线程还是起到很大效率提升,这是协同式多任务当一项任务比如网络 I/O启动,而在长的或不确定的时间,没有运行任何 Python 代码的需要,一个线程便会让出GIL,从而其他线程可以获取 GIL 而运行 Python。这种礼貌行为称为协同式多任务处理,它允许并发;多个线程同时等待不同事件。

两个线程在同一时刻只能有一个执行 Python ,但一旦线程开始连接,它就会放弃 GIL ,这样其他线程就可以运行。这意味着两个线程可以并发等待套接字连接,这是一件好事。在同样的时间内它们可以做更多的工作。

线程池要设置为多少?

服务器CPU核数有限,能够同时并发的线程数有限,并不是开得越多越好,以及线程切换是有开销的,如果线程切换过于频繁,反而会使性能降低

线程执行过程中,计算时间分为两部分:

  • CPU计算,占用CPU
  • 不需要CPU计算,不占用CPU,等待IO返回,比如recv(), accept(), sleep()等操作,具体操作就是比如
    访问cache、RPC调用下游service、访问DB,等需要网络调用的操作

那么如果计算时间占50%, 等待时间50%,那么为了利用率达到最高,可以开2个线程:假如工作时间是2秒, CPU计算完1秒后,线程等待IO的时候需要1秒,此时CPU空闲了,这时就可以切换到另外一个线程,让CPU工作1秒后,线程等待IO需要1秒,此时CPU又可以切回去,第一个线程这时刚好完成了1秒的IO等待,可以让CPU继续工作,就这样循环的在两个线程之前切换操作。

那么如果计算时间占20%, 等待时间80%,那么为了利用率达到最高,可以开5个线程:可以想象成完成任务需要5秒,CPU占用1秒,等待时间4秒,CPU在线程等待时,可以同时再激活4个线程,这样就把CPU和IO等待时间,最大化的重叠起来

抽象一下,计算线程数设置的公式就是:N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。由于有GIL的影响,python只能使用到1个核,所以这里设置N=1

关于我

如果文章对你有收获,可以收藏转发,这会给我一个大大鼓励哟!

想要获取更多Python学习资料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起来学习讨论吧!
本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理

5分钟看懂系列:Python 线程池原理及实现的更多相关文章

  1. java多线程系列(六)---线程池原理及其使用

    线程池 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 java多线程系列(三)之等待通知 ...

  2. 5分钟看懂系列:HTTP缓存机制详解

    原创文章首发于公众号:「码农富哥」,欢迎收藏和关注,如转载请注明出处! 什么是HTTP缓存 HTTP 缓存可以说是HTTP性能优化中简单高效的一种优化方式了,缓存是一种保存资源副本并在下次请求时直接使 ...

  3. 10分钟看懂, Java NIO 底层原理

    目录 写在前面 1.1. Java IO读写原理 1.1.1. 内核缓冲与进程缓冲区 1.1.2. java IO读写的底层流程 1.2. 四种主要的IO模型 1.3. 同步阻塞IO(Blocking ...

  4. Java多线程系列--“JUC线程池”03之 线程池原理(二)

    概要 在前面一章"Java多线程系列--“JUC线程池”02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包括:线程池示例参考代 ...

  5. 线程池原理及python实现

    为什么需要线程池 目前的大多数网络服务器,包括Web服务器.Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短. 传统多线程方案中我们采 ...

  6. 对Python线程池

    本文对Python线程池进行详细说明介绍,IDE选择及编码的解决方案进行了一番详细的描述,实为Python初学者必读的Python学习经验心得. AD: 干货来了,不要等!WOT2015 北京站演讲P ...

  7. Java并发包源码学习系列:线程池ScheduledThreadPoolExecutor源码解析

    目录 ScheduledThreadPoolExecutor概述 类图结构 ScheduledExecutorService ScheduledFutureTask FutureTask schedu ...

  8. Java多线程系列--“JUC线程池”06之 Callable和Future

    概要 本章介绍线程池中的Callable和Future.Callable 和 Future 简介示例和源码分析(基于JDK1.7.0_40) 转载请注明出处:http://www.cnblogs.co ...

  9. Java多线程系列--“JUC线程池”02之 线程池原理(一)

    概要 在上一章"Java多线程系列--“JUC线程池”01之 线程池架构"中,我们了解了线程池的架构.线程池的实现类是ThreadPoolExecutor类.本章,我们通过分析Th ...

随机推荐

  1. web自动化测试难点 滚动条操作、日期框处理、上传文件

    如何把页面滑到最低部? 一般来说,做web自动化测试时,不需要单独写代码,把页面滑到可见,因为click操作,只要是元素存在并且加载出来了,就可以点击到,无需另外写滑动元素的代码. 如果特殊情况需要滑 ...

  2. maven打包时报No compiler is provided in this environment处理

    系统:macOS 开发工具:Idea 问题描述:在idea中执行mvn clean install时报No compiler is provided in this environment. Perh ...

  3. C语言讲义——dll调用

    DLL:Dynamic Link Library,动态链接库.一个应用程序可使用多个DLL文件,一个DLL文件也可以被不同的应用程序使用. 先新建一个dll项目 再创建C项目进行调用 #include ...

  4. 你知道第一个C语言C++编译器是如何诞生的吗?

    当今几乎所有的实用的编译器/解释器(以下统称编译器)都是用 C 语言编写的,有一些语言比如 Clojure,Jython 等是基于 JVM 或者说是用 Java 实现的,IronPython 等是基于 ...

  5. finalize和clean

    弊端 1.执行的时间不确定,资源释放不能靠这2个方法.Cleaner规范指出:"清除方法在System.exit期间的行为是与实现相关的.不确保清除动作是否会被调用." 2#.如果 ...

  6. mysql事务原理以及锁

    一.Innodb事务原理 1.什么是事务 a.事务(Transaction)是数据库区别于文件系统的重要特性之一,事务会把数据库从一种一致性状态转换为另一种一致性状态. b.在数据库提交时,可以确保要 ...

  7. python之Bug之字符串拼接bug

    \r\n拼接Bug 环境: python3.4.pycharm2017 偶然的学习中遇到了一个问题,百思不得姐,什么问题呢,大家输入太快了,难免有失误就如下面的代码 #构造响应数据 response_ ...

  8. 第3.11节 Python强大的字符串格式化新功能:format字符串格式化的格式控制

                                                第3.11节 format字符串格式化的格式控制 一.    引言 上节介绍了四种format进行字符串格式化的 ...

  9. 第4.2节 神秘而强大的Python生成器精讲

    一. 生成器(generator)概念 生成器是一个特殊的迭代器,它保存的是算法,每次调用next()或send()就计算出下一个元素的值,直到计算出最后一个元素,没有更多的元素时,抛出StopIte ...

  10. 【学习笔记】浅析后缀自动机(SAM)及基础应用

    解决子串相关问题的强大工具 我们知道一个长度为 \(n\) 的字符串中所有的子串数目为 \(O(n^2)\) 个,这很大程度上限制了我们对某些子串相关问题的研究.所以有没有解决方案,使得我们可以在可承 ...