前言
前面的subprocess库主要讲解的是进程知识与进程间的交互。而进程有可以拥有多个线程,所以threading库提供了管理多个线程执行的API,允许程序在同一个进程空间并发地运行多个操作。
本篇,将详细的介绍Python线程库:threading。
Thread对象
要使用threading库,最简单的方式是使用Thread,它可以直接通过函数实例化一个Thread对象,并调用start让它工作。毕竟,我们用线程也是为了执行耗时任务,把任务封装到一个函数中,直接创建往往最简单。
示例如下:
import threading def print1tonum(num): for i in range(10000): print(i) t = threading.Thread(target=print1tonum,args=(10000,)) t.start()
运行之后,效果如下:
这里,我们创建了一个线程,并向它传递参数告诉它要完成什么工作。
区分线程
一般来说,我们创建线程是避免在主线程中处理耗时的任务,但是有时候,就算是基本的运算任务,因为其叠加起来非常的多,我们会考虑用多个线程进行处理。示例如下:
import threading import time def printThreadName1(): print(threading.current_thread().getName() + " start") time.sleep(0.2) print(threading.current_thread().getName() + ' end') def printThreadName2(): print(threading.current_thread().getName() + " start") time.sleep(0.2) print(threading.current_thread().getName() + ' end') def printThreadName3(): print(threading.current_thread().getName() + " start") time.sleep(0.2) print(threading.current_thread().getName() + ' end') t1 = threading.Thread(name='t1', target=printThreadName1) t2 = threading.Thread(name='t2', target=printThreadName2) t3 = threading.Thread(name='t3', target=printThreadName3) t1.start() t2.start() t3.start()
运行之后,效果如下:
这里,我们会发现print打印非常的混乱,虽然3个线程都是一摸一样的,但结束的时候并不是按顺序结束的,因为它们是同时运行的。(这里需要注意,更多内容接着往下看)
守护线程
运行上面的代码我们会发现,主程序都是在等线程运行完成之后,才结束的。也就是说,创建线程的主程序,无法在线程结束前安全退出。那么,可不可保证运行线程时,主线程可以退出呢?
答案是可以的,这个时候我们需要用到守护线程,这个线程可以一直运行而不阻塞主程序的退出,比如在服务器监控的工具线程,对于这些服务,守护线程往往更有用。
要构造守护线程,需要将上面创建线程的方式增加一个参数daemon,它是一个布尔值,默认值为False,普通线程,改为True就是守护线程。
import threading import time def printThreadName1(): print(threading.current_thread().getName() + " start") time.sleep(0.2) print(threading.current_thread().getName() + ' end') def printThreadName2(): print(threading.current_thread().getName() + " start") time.sleep(0.2) print(threading.current_thread().getName() + ' end') def printThreadName3(): print(threading.current_thread().getName() + " start") time.sleep(0.2) print(threading.current_thread().getName() + ' end') t1 = threading.Thread(name='t1', target=printThreadName1, daemon=True) t2 = threading.Thread(name='t2', target=printThreadName2) t3 = threading.Thread(name='t3', target=printThreadName3) t1.start() t2.start() t3.start()
运行之后,效果如下:
可以看到,博主这里将t1设置为守护线程,但是没有看到t1结束主程序就结束了。如果需要等待一个守护线程完成工作,可以增加join()函数。
t1 = threading.Thread(name='t1', target=printThreadName1, daemon=True) t2 = threading.Thread(name='t2', target=printThreadName2, daemon=True) t3 = threading.Thread(name='t3', target=printThreadName3, daemon=True) t1.start() t2.start() t3.start() t1.join() t2.join() t3.join()
输出结果就不展示了,与前文非守护的线程一样。
需要注意的是,join()函数会无限阻塞,直到线程任务结束。当然,如果想设置一个最大的等待时间,超过时间就不在等待也行。
示例如下:
t1 = threading.Thread(name='t1', target=printThreadName1, daemon=True) t2 = threading.Thread(name='t2', target=printThreadName2, daemon=True) t3 = threading.Thread(name='t3', target=printThreadName3, daemon=True) t1.start() t2.start() t3.start() t1.join(0.4) print(t1.is_alive()) t2.join() t3.join()
这里,设置了t1等待最长时间为0.4秒,读者可以将t1线程的运行时间增加到1秒,看看其效果,is_alive()的意义是等待了0.4秒后,t1是否还在运行,为True代表是的,如下所示:
需要注意的是join()可能会造成死锁。比如现在有3个线程t1,t2,t3,t1需要使用资源12,t2需要使用资源23,t3需要使用资源13,3个同时运行。开始时,t1在使用资源1等待资源2,t2在使用资源2等待资源3,t3在使用资源3等待资源1,它们互相等待对方释放资源,但都不释放,导致循环等待下去,形成死锁。(与主线程等待线程结束差不多的道理)
自定义线程
从上面的所有线程运行,想必读者肯定发现了一个问题,那就是threading.Thread无法提供返回值。而实际的多线程运行中,往往我们都是获取网络数据,然后再处理,所以必须获取其处理的结果。
这个时候,自定义线程就能实现该需求,示例如下:
import threading import requests class GetHTMLThread(threading.Thread): def __init__(self, args, kwargs): super(GetHTMLThread, self).__init__() self.args = args self.kwargs = kwargs def run(self): print(self.args) self.result = requests.get(url=self.kwargs['url']) def get_result(self): return self.result.text t = GetHTMLThread(args=(1,), kwargs={'url': "https://www.baidu.com"}) t.start() t.join() print(t.get_result())
运行之后,效果如下:
定时器线程
在我们使用django搭建服务器时,往往有许多的延时触发任务。比如我们CSDN就是我们搭建的博客网站,那么我们定时更新博客就是一个延时任务,我昨天写完博客,希望明天下午18点发送,那么这个延时任务就是中间的时差。
那么,此时我们使用定时器线程往往效果更好。示例如下:
import threading def getWeather(): print("更新博客") t1 = threading.Timer(0.3, getWeather) t1.setName('t1') t2 = threading.Timer(0.3, getWeather) t2.setName('t2') t1.start() t2.start() t2.cancel()
运行之后,效果如下:
这里会延迟0.3秒执行线程,而之所以t2线程没有执行,是因为我们调用了cancel()函数,取消了该线程的执行。
线程间传送信号
尽管使用多线程的目的是并发地运行单独的任务,但有时候也需要在多个线程间同步操作。而Python中,线程的通信方法是事件对象。
Event管理一个内部标志,调用者可以用set()和clear()方法控制这个标志。其他线程可以使用wait()暂停,直到这个标志被设置,可以有效的阻塞进程直到允许这些线程继续。
import threading import time def wait_for_event(e): print("wait_for_event") event_is_set = e.wait() print("wait_for_event", event_is_set) def wait_for_event_timeout(e, t): while not e.is_set(): print("wait_for_event_timeout") event_is_set = e.wait(t) print("wait_for_event_timeout", event_is_set) if event_is_set: print("运行任务") else: print('运行其他任务') e = threading.Event() t1 = threading.Thread( name='t1', target=wait_for_event, args=(e,) ) t1.start() t2 = threading.Thread( name='t2', target=wait_for_event_timeout, args=(e, 2) ) t2.start() time.sleep(5) e.set()
运行之后,效果如下:
这个例子中,wait_for_event_timeout()函数将检查事件状态而不会无限阻塞。wait_for_event()在wait()调用的位置阻塞,事件状态改变之前它不会返回。
因为后面的线程知识还有很多,但这篇博文已经很长了,所以接下来的锁,释放锁等知识,将在下一个threading库章节讲解。