学会使用Python的threading模块、掌握并发编程基础
threading模块
Python中提供了threading模块来实现线程并发编程,官方文档如下:
添加子线程
实例化Thread类
使用该方式新增子线程任务是比较常见的,也是推荐使用的。
简单的代码示例如下,创建3个子线程并向其添加任务,然后运行并打印它们的线程ID和线程名字:
import threading
import time
def task(params):
print("sub thread run")
currentThread = threading.current_thread()
time.sleep(3)
print("current subthread id : %s\ncurrent subthread name : %s\ncurrent subthread params : %s" % (
currentThread.ident, currentThread.name, params))
if __name__ == "__main__":
print("main thread run")
for item in range(3):
subThreadIns = threading.Thread(target=task, args=(item, ))
subThreadIns.start()
print("main thread run end")
# main thread run
# sub thread run
# sub thread run
# sub thread run
# main thread run end
# current subthread id : 123145534398464
# current subthread name : Thread-1
# current subthread params : 0
# current subthread id : 123145544908800
# current subthread name : Thread-3
# current subthread params : 2
# current subthread id : 123145539653632
# current subthread name : Thread-2
# current subthread params : 1
❶:返回一个线程对象,注意args的参数必须是一个tuple,否则抛出异常,也就是说单实参必须添加逗号
❷:start()方法是指该线程对象能够被系统调度了,但不是立即运行该线程,而是等待系统调度后才运行。所以你会看见上面子线程的运行顺序是0、2、1,另外一个线程对象只能运行一次该方法,若多次运行则抛出RunTimeError的异常。
❸:获取当前的线程对象
❹:获取当前线程对象的编号和名字,以及传入的参数。当线程启动时,系统都会分配给它一个随机的编号和名字
首先上述代码会先运行主线程,然后会创建3个子线程并运行。
当子线程运行的时候碰到了sleep(3)这种I/O操作时会释放掉GIL锁,并将线程执行权交还给了主线程。
然后主线程就运行完毕了,此时主线程并不会被kill掉,而是等待子线程运行结束后才会被kill掉,而子线程则是运行完毕后会被立刻kill掉。
我们可以看见,上面3个任务如果按照串行执行共会花费9.+秒时间,而通过多线程来运行,则仅需花费3.+秒的时间,极大的提升了任务处理效率。
自定义类覆写run()方法
上面的子线程任务对象是一个全局函数,我们也可以将它作为方法来进行调用。
书写一个类并继承Threading类,覆写run()方法即可:
import threading
import time
class TaskClass(threading.Thread): # ❶
def __init__(self, params):
self.params = params # ❷
super(__class__, self).__init__()
def run(self):
print("sub thread run")
currentThread = threading.currentThread()
time.sleep(3)
print("current subthread id : %s\ncurrent subthread name : %s\ncurrent subthread params : %s" % (
currentThread.ident, currentThread.name, self.params))
if __name__ == "__main__":
print("main thread run")
for item in range(3):
subThreadIns = TaskClass(item)
subThreadIns.start()
print("main thread run end")
# main thread run
# sub thread run
# sub thread run
# sub thread run
# main thread run end
# current subthread id : 123145495068672
# current subthread name : Thread-1
# current subthread params : 0
# current subthread id : 123145500323840
# current subthread name : Thread-2
# current subthread params : 1
# current subthread id : 123145505579008
# current subthread name : Thread-3
# current subthread params : 2
❶:必须继承Threading类并调用父类的__init__()方法
❷:传入的参数
源码浅析
为什么添加子线程有2种截然不同的方式呢?它们之间有什么区别?这些都可以从源码中找到答案。
我们从Thread类的实例看起,首先是__init__()方法(threading.py line 738 - 800),它主要做了一些初始化的准备工作:
class Thread:
_initialized = False
_exc_info = _sys.exc_info
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
# 如果group不是None,就会抛出断言异常
assert group is None, "group argument must be None for now"
# 如果kwargs是None,则构造一个空字典
if kwargs is None:
kwargs = {}
# 传入的执行任务的函数或者None
self._target = target
# 线程名字
self._name = str(name or _newname())
# 任务函数传入的元组参数
self._args = args
# 任务函数传入的关键字参数
self._kwargs = kwargs
# 是否是守护线程启动,如果不是None则以守护线程模式启动
if daemon is not None:
self._daemonic = daemon
# 如果是None,则继承当前线程的守护模式
else:
self._daemonic = current_thread().daemon
# 线程编号
self._ident = None
# 锁定状态,None
self._tstate_lock = None
# 一把Event事件锁
self._started = Event()
# 是否停止运行的标志位
self._is_stopped = False
# 初始化状态改为True
self._initialized = True
self._stderr = _sys.stderr
_dangling.add(self)
参数释义:
- group:应该为None,为了日后扩展ThreadGroup类而保留的
- target:传入一个可调用对象,即线程任务task,默认为None,即可以不进行传入
- name:线程启动时将不再由系统分配线程名称,而是自定义,默认情况下,系统分配的线程名称会由 "Thread-N" 的格式构成一个唯一的名称,其中 N 是小的十进制数
- args:用于调用目标函数的参数元组,默认是()空元组,你必须传入一个元组
- kwargs:用于调用目标函数的关键字参数字典,默认是None,你必须传入一个字典
- daemon:命名关键字参数,应当传入一个布尔值,默认为None,它会指定该线程是否是以守护线程模式启动,如果为None,该线程将继承当前线程的守护模式属性
接下来看start()方法,它是告知系统当前线程完成调度,可随时启用的方法(threading.py line 828 - 851):
def start(self):
# 如果初始状态不为True,则抛出异常
if not self._initialized:
raise RuntimeError("thread.__init__() not called")
# 判断当前线程是否被锁住,如果被锁住则抛出异常
if self._started.is_set():
raise RuntimeError("threads can only be started once")
with _active_limbo_lock:
_limbo[self] = self
try:
# 执行引导
_start_new_thread(self._bootstrap, ())
except Exception:
with _active_limbo_lock:
del _limbo[self]
raise
self._started.wait()
这里关键是看self._bootstrap()方法,该该方法位于(threading.py line 870 - 888),看看它会做什么事情:
def _bootstrap(self):
try:
self._bootstrap_inner()
except:
if self._daemonic and _sys is None:
return
raise
继续找self._bootstrap_inner()方法,该该方法位于(threading.py line 901 - 964)。
在该方法的916行时,它会执行run()方法:
def _bootstrap_inner(self):
...
try:
# 执行run
self.run()
except SystemExit:
pass
except:
...
如果此时你按照第二种添加子线程的方式,则直接会运行被子类TaskClass覆写的run()方法。
如果是第一种添加子线程的方式,则还需要往里面看(threading.py line 835 - 868):
def run(self):
try:
# self._target = 我们自己传递的可调用对象task
if self._target:
self._target(*self._args, **self._kwargs)
finally:
del self._target, self._args, self._kwargs
至此可以发现,不管是使用哪一种方式添加子线程,都会运行5个方法。
所以说它们内部实现其实都是一样的,没什么特别的,也不要觉得它特别神奇。
threading模块方法大全
以下是threading模块提供的类或方法:
类方法 | 描述 | 返回值 |
---|---|---|
threading.Thread(target, args, kwargs) | 创建并返回一个线程对象 | threadObject |
threading.Timer(interval, function, args, kwargs) | 创建并返回一个延迟启动的线程对象 | threadObject |
threading.active_count() | 获取当前进程下存活的线程数量 | int |
threading.enumerate() | 查看当前进程存活了的所有线程对象,以列表形式返回 | [threadObject, ...] |
threading.main_thread() | 获取主线程对象 | threadObject |
threading.current_thread() | 获取当前正在执行的线程对象 | threadObject |
threading.currentThread() | 获取当前正在执行的线程对象 | threadObject |
threading.get_ident() | 获取当前正在执行的线程对象的编号 | int |
下面我将使用该代码对上述功能进行演示:
import threading
import time
class TaskClass(threading.Thread):
def run(self):
time.sleep(3)
pass
if __name__ == "__main__":
for i in range(3):
subThreadIns = TaskClass()
subThreadIns.start()
1)获取当前进程下存活的线程数量:
print(threading.active_count())
# 4
2)查看当前进程存活了的所有线程对象,以列表形式返回:
print(threading.enumerate())
# [<_MainThread(MainThread, started 4425459136)>, <TaskClass(Thread-1, started 123145449238528)>, <TaskClass(Thread-2, started 123145454493696)>, <TaskClass(Thread-3, started 123145459748864)>]
3)获取主线程对象:
print(threading.main_thread())
# <_MainThread(MainThread, started 4565407168)>
4)获取当前正在执行的线程对象:
print(threading.currentThread())
# <_MainThread(MainThread, started 4383299008)>
5)获取当前正在执行的线程对象的编号:
print(threading.get_ident())
# 4380034496
threadObject方法大全
以下是针对线程对象提供的属性或者方法:
方法/属性 | 描述 | 返回值 |
---|---|---|
threadObject.start() | 通知系统该线程调度完毕,可以随时进行启动,一个线程对象只能运行一次该方法,若多次运行则抛出RunTimeError异常 | ... |
threadObject.join(timeout=None) | 主线程默认会等待子线程运行结束后再继续执行,timeou为等待的秒数,如不设置该参数则一直等待。 | ... |
threadObject.getName() | 获取线程对象的名字 | str |
threadObject.setName(name) | 设置线程对象的名字 | None |
threadObject.is_alive() | 查看线程对象是否存活 | bool |
threadObject.isAlive() | 查看线程对象是否存活,不推荐使用 | bool |
threadObject.isDaemon() | 查看线程对象是守护线程 | bool |
threadObject.setDaemon() | 设置线程对象为守护线程,主线程运行完毕之后设置为守护线程的子线程便立即结束执行 | None |
threadObject.ident | 获取线程对象的编号 | int |
threadObject.name | 获取或者设置线程对象的名字 | str or None |
daemon | 查看线程对象是守护线程 | bool |
主线程阻塞
默认情况下,当子线程启动后,主线程会依旧往下运行而不是等待所有的子线程运行完毕后再继续往下运行。
如图所示,主线程在运行结束后并不会被理解kill掉,而是所有的子线程运行完毕后主线程才会被kill掉:
我们可以利用threadObject.join(timeout=None)来让主线程等待子线程运行完毕后再继续向下运行,timeout为等待的秒数,如不设置该参数则一直等待。
如图所示,这是没有设置timeout的示意图,主线程必须等待所有子线程运行完毕后再接着运行:
代码示例:
import threading
import time
class TaskClass(threading.Thread):
def run(self):
thName = threading.current_thread().name
print("%s start run" % thName)
time.sleep(3)
print("%s run end" % thName)
if __name__ == "__main__":
print("main thread start run")
threadLst = []
for i in range(3):
threadLst.append(TaskClass())
for ins in threadLst:
ins.start() # 开始运行所有子线程
for ins in threadLst:
ins.join() # 让主线程等待所有子线程运行完毕后再接着运行,注意,设置主线程等待的子线程必须处于活跃状态
print("main thread carry on run")
print("main thread run end")
# main thread start run
# Thread-1 start run
# Thread-2 start run
# Thread-3 start run
# Thread-1 run end
# Thread-2 run end
# Thread-3 run end
# main thread carry on run
# main thread run end
守护线程
守护线程是指当主线程运行完毕后,子线程是否还要继续运行。
默认threadObject.setDaemon()为None,也就是False,即当前主线程运行完毕后,子线程依旧可以接着运行。
如果threadObject.setDaemon()为True,则当前主线程运行完毕后,子线程即使没有运行完毕也会结束运行。
代码示例:
import threading
import time
class TaskClass(threading.Thread):
def run(self):
thName = threading.current_thread().name
print("%s start run" % thName)
time.sleep(3)
print("%s run end" % thName)
if __name__ == "__main__":
print("main thread start run")
threadLst = []
for i in range(3):
threadLst.append(TaskClass())
for ins in threadLst:
# 注意,守护线程的设置必须在线程未启动时设置
ins.setDaemon(True)
ins.start()
print("main thread carry on run")
print("main thread run end")
# main thread start run
# Thread-1 start run
# Thread-2 start run
# Thread-3 start run
# main thread carry on run
# main thread run end
join()与setDaemon(True)共存
如果同时设置setDaemon(True)与join()方法会怎么样呢?有两种情况:
- join()方法没有设置timeout(没有设置即表示死等)或者timeout的时间比子线程作业时间要长,这代表子线程会死在主线程之前,setDaemon(True)也就没有了意义,即失效了
- join()设置了timeout并且timeout的时间比子线程作业时间要短,这代表主线程会死在子线程之前,setDaemon(True)生效,子线程会跟着主线程一起死亡。
情况一:
import threading
import time
class TaskClass(threading.Thread):
def run(self):
thName = threading.current_thread().name
print("%s start run" % thName)
time.sleep(3)
print("%s run end" % thName)
if __name__ == "__main__":
subThread = TaskClass()
subThread.setDaemon(True) # 主线程运行完后会立即终止子线程的运行。但是由于有join(),故不生效。
subThread.start()
subThread.join() # 主线程必须等待子线程运行结束后再接着运行
print("main thread run end")
# Thread-1 start run
# Thread-1 run end
# main thread run end
情况2:
import threading
import time
class TaskClass(threading.Thread):
def run(self):
thName = threading.current_thread().name
print("%s start run" % thName)
time.sleep(3)
print("%s run end" % thName)
if __name__ == "__main__":
subThread = TaskClass()
subThread.setDaemon(True) # 主线程运行完后会立即终止子线程的运行。但是由于有join(),故不生效。
subThread.start()
subThread.join(1) # 主线程必须等待子线程运行结束后再接着运行,只等待1s
print("main thread run end")
# Thread-1 start run
# main thread run end
线程延迟启动
使用threading模块中提供的Timer类,可让子线程延迟启动,如下所示:
import threading
import time
def task():
print("sub thread start run")
time.sleep(3)
print("sub thread run end")
if __name__ == "__main__":
print("main thread run")
t1 = threading.Timer(interval=3, function=task)
t1.start() # 3秒后才启动子线程
t1.join()
print("main thread run end")
# main thread run
# sub thread start run
# sub thread run end
# main thread run end
如果要用类的形式,则可以继承threading.Timer()类,并修改self.function属性,个人极度不推荐这样做。
如下所示,在不知道某一个方法怎么使用时扒扒源码看一看,翻翻官方文档就大概能了解:
import threading
import time
class TaskClass(threading.Timer):
def __init__(self, *args, **kwargs):
# 必须要修改function为你想执行的方法
super(__class__, self).__init__(*args, **kwargs)
self.function = self.task
def task(self, x, y):
print("sub thread start run")
time.sleep(3)
print("parmas %s %s" % (x, y))
print("sub thread run end")
if __name__ == "__main__":
# 必须传入一个None
t1 = TaskClass(interval=3, function=None, args=(1, 2))
t1.start()
t1.join()
print("main thread run end")
# sub thread start run
# parmas 1 2
# sub thread run end
# main thread run end
多线程编程应用场景
由于GIL锁的存在,Python中对于I/O操作来说可以使用多线程编程,如果是计算密集型的操作则不应该使用多线程进行处理,因为没有I/O操作就不能通过I/O切换来执行其他线程,故对于计算密集型的操作来说多线程没有什么优势,甚至还可能比普通串行还慢(因为涉及到线程切换,虽然是毫秒级别,但是计算的数值越大这个切换也就越密集,GIL锁是100个CPU指令切换一次的)
注意:我们是在Python2版本下进行此次测试,Python3版本确实相差不大,但是,从本质上来说依然是这样的。
计算密集型程序的普通串行运行时间:
import threading
import time
num = 0
def add():
global num
for i in range(10000000): # 一千万次
num += 1
def sub():
global num
for i in range(10000000): # 一千万次
num -= 1
if __name__ == '__main__':
start_time = time.time()
add()
sub()
end_time = time.time()
print("执行时间:",end_time - start_time)
# ==== 执行结果 ==== 三次采集
"""
大约在 1.3 - 1.4 秒
"""
计算密集型程序的多线程并发运行时间:
# coding:utf-8
import threading
import time
num = 0
def add():
global num
for i in range(10000000): # 一千万次
num += 1
def sub():
global num
for i in range(10000000): # 一千万次
num -= 1
if __name__ == '__main__':
start_time = time.time()
t1 = threading.Thread(target=add,)
t2 = threading.Thread(target=sub,)
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print(u"执行时间:",end_time - start_time)
# ==== 执行结果 ==== 三次采集
"""
大约 4 - 5 秒
"""
学会使用Python的threading模块、掌握并发编程基础的更多相关文章
- python中threading模块详解(一)
python中threading模块详解(一) 来源 http://blog.chinaunix.net/uid-27571599-id-3484048.html threading提供了一个比thr ...
- Python:使用threading模块实现多线程编程
转:http://blog.csdn.net/bravezhe/article/details/8585437 Python:使用threading模块实现多线程编程一[综述] Python这门解释性 ...
- python中并发编程基础1
并发编程基础概念 1.进程. 什么是进程? 正在运行的程序就是进程.程序只是代码. 什么是多道? 多道技术: 1.空间上的复用(内存).将内存分为几个部分,每个部分放入一个程序,这样同一时间在内存中就 ...
- Python中使用模块和库编程
""" python中使用模块和库编程 导入模块 import modulename [as alias] from modulename import fun1,fun ...
- TCP与UDP比较 以及并发编程基础知识
一.tcp比udp真正可靠地原因 1.为什么tcp比udp传输可靠地原因: 我们知道在传输数据的时候,数据是先存在操作系统的缓存中,然后发送给客户端,在客户端也是要经过客户端的操作系统的,因为这个过程 ...
- Java并发编程基础
Java并发编程基础 1. 并发 1.1. 什么是并发? 并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力.如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互 ...
- 并发-Java并发编程基础
Java并发编程基础 并发 在计算机科学中,并发是指将一个程序,算法划分为若干个逻辑组成部分,这些部分可以以任何顺序进行执行,但与最终顺序执行的结果一致.并发可以在多核操作系统上显著的提高程序运行速度 ...
- Java并发编程系列-(1) 并发编程基础
1.并发编程基础 1.1 基本概念 CPU核心与线程数关系 Java中通过多线程的手段来实现并发,对于单处理器机器上来讲,宏观上的多线程并行执行是通过CPU的调度来实现的,微观上CPU在某个时刻只会运 ...
- Python学习-第三天-面向对象编程基础
Python学习-第三天-面向对象编程基础 类和对象 简单的说,类是对象的蓝图和模板,而对象是类的实例.这个解释虽然有点像用概念在解释概念,但是从这句话我们至少可以看出,类是抽象的概念,而对象是具体的 ...
随机推荐
- 初探DBSCAN聚类算法
DBSCAN介绍 一种基于密度的聚类算法 他最大的优势是可以发现任意形状的聚类簇,而传统的聚类算法只能使用凸的样本聚集类 两个参数: 邻域半径R和最少点数目minpoints. 当邻域半径R内的点的个 ...
- [Django框架 - 静态文件配置、request对象方法初识、 pycharm链接数据库、ORM实操增删改查、django请求生命周期]
[Django框架 - 静态文件配置.request对象方法初识. pycharm链接数据库.ORM实操增删改查.django请求生命周期] 我们将html文件默认都放在templates文件夹下 将 ...
- [Linux] 完全卸载mysql
参考 https://www.jianshu.com/p/ef58fb333cd6
- brk 和 sbrk 区别
转自:https://www.cnblogs.com/chengxuyuancc/p/3566710.html brk和sbrk的定义,在man手册中定义了这两个函数: 1 #include < ...
- 企业定制CRM系统的5步流程
由于所处的行业不同,不同的企业对CRM系统的需求也不同.除了行业通用的功能之外,每个企业都有自己独特的功能需求.为了让CRM系统跟上这种需求的变化,CRM厂商通常也会提供CRM系统定制开发功能来满足不 ...
- 创建一个本地Yum 仓库,提升速度,减少带宽
1 mkdir /YUM 2 mount -t iso9660 -o loop /home/dan/Centos-7-x86_x64-DVD.iso /mnt/iso/ 3 rpm -ivh delt ...
- 记go中一次http超时引发的事故
记一次http超时引发的事故 前言 分析下具体的代码实现 服务设置超时 客户端设置超时 http.client context http.Transport 问题 总结 参考 记一次http超时引发的 ...
- Python+Selenium自动化 模拟鼠标操作
Python+Selenium自动化 模拟鼠标操作 在webdriver中,鼠标的一些操作如:双击.右击.悬停.拖动等都被封装在ActionChains类中,我们只用在需要使用的时候,导入这个类就 ...
- nginx 重定向到系统维护页面
上周末兄弟项目准备扩展服务器以便提供更好的服务,兄弟项目有一些功能是实时提供到我这边的,需要我这边暂时把对应系统功能屏蔽,因为使用nginx,所以可以直接配置nginx重定向到固定系统维护页面. ng ...
- C# 尝试还原程序包是出错:找不到“XXXXX”版本的程序包“XXXXXX”
在C#管理程序包的时候有时会出现找不到某某版本的程序包 如果出现这样的情况,解决办法是在你当前项目获取当前类库下的packages.config里去删除一段配置就可以解决! 我的缺少的是版本为9.0. ...