threading:Python线程的基础知识
前言
前面的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库章节讲解。