有时候代码在运行的时绝大多数时间都在等待。比如说拉取一个网页,需要等待服务器返回;比如说访问一个数据库,需要等待数据块被释放;比如说延时操作,需要等待一段时间再运行。
而一般的python代码运行起来只有一个进程,所有的语句都是顺序执行的。这样就会很浪费时间。如果有三个操作分别耗时t1,t2,t3,那么顺序执行的时间将会是sum(t1,t2,t3)。如果它们能够并行执行的话,花费的时间将会是max(t1,t2,t3)。
python提供了线程类,在一个python进程内可以创建多个线程,可并行运行多个线程。
16.1 线程创建和使用
python的线程创建有两种方式,一种通过参数实例化threading.Thread类;另外一种是自己继承threading.Thread类,重写__init__()和run()方法。
先来介绍使用参数实例化一个threading.Thread类。这种方法很简洁,是推荐的方法。
threading.Thread类的实例化需要几个参数,其中target参数需要传入一个可执行的对象,可以是一个函数,也可以是一个可执行文件;而name参数是线程的名字;args参数是target对象的参数元组,如果target是一个函数的话,那么args需要传入这个函数的参数;kwargs是target对象的字典参数。
下面我们先定一个函数,用于作为线程的可执行对象。这段代码的作用是,随机生成一个0到5秒的数字,并且延迟这个随机时间。如果生成3,那么就延迟3秒。然后打印出传入的线程名和延迟描述。
def randnum(threadname): a=random.randint(0,5) time.sleep(a) print(threadname+" sleep "+str(a))
接着就可以实例化这个线程对象了
from threading import Threadt=Thread(target=randnum,name=threadname,args=(threadname,))
可以看到线程实例化的参数target为randnum,传入的args参数为(threadname,)。线程的名字也是threadname。这里需要注意的是,randnum只有一个参数,而args参数需要输入一个拥有一个数据的元组。tuple在表示只有一个数据的元组时,使用(a,)这种方式,如果使用(a),则表示的是a。所以这里参数使用的是(threadname,)。
Thread实例化后并不会启动运行一个线程,需要使用start()方法线程才会启动运行。使用join()方法可以等待一个线程的结束。可能是正常结束或者出现异常而结束。
一段完整的代码如下
from threading import Threadimport randomimport timedef randnum(threadname): a=random.randint(0,5) time.sleep(a) print(threadname+" sleep "+str(a))starttime=time.time()threadlist=[]for i in range(5): threadname="thread"+str(i) t=Thread(target=randnum,name=threadname,args=(threadname,)) threadlist.append(t) t.start()for t in threadlist: t.join()endtime=time.time()print('total time :'+ str(endtime-starttime))
这段代码定义了一个函数,可以随机延迟打印一句话。然后使用实例化Thread类的方式初始化一个线程。接着调用线程的start()方法启动这个线程。接着使用join()方法循环等待所有的线程运行结束。最后计算出总的执行时间
一个可能的运行结果是
thread0 sleep 2thread1 sleep 2thread4 sleep 3thread3 sleep 5 thread2 sleep 5total time :5.00261211395
可以看到每个线程最长延迟了5秒,而总的执行时间也差不多是5秒。
下面来介绍另外一种使用线程的方法。需要自己继承线程类,并且重写__init__()方法以及run()方法。因为使用线程对象的start()来启动一个线程的时候,start()方法实际上会调用run()方法。
我们先来看看代码
class MyThread(Thread): def __init__(self,threadname): Thread.__init__(self) self.threadname=threadname def run(self): self.randnum(self.threadname) def randnum(self,threadname): a=random.randint(0,5) time.sleep(a) print(threadname+" sleep "+str(a))
上述的代码重写了__init__()方法,只接收一个变量,就是threadname。使用继承的方式来编写自己的线程类,需要在__init__()里面显式调用Thread的__init__()来做一些初始化的工作。
在这段代码里面randnum函数作为类的一个方法出现,实际上它也可以是一个外部函数,或者一个可执行文件的语句。
run()方法的作用是调用了randnum函数。
此时初始化这个线程类的语句就很简单了,因为我们在定义类的时候已经封装得很好了。
t=MyThread(threadname)
此时完整的代码为
from threading import Threadimport randomimport timeclass MyThread(Thread): def __init__(self,threadname): Thread.__init__(self) self.threadname=threadname def run(self): self.randnum(self.threadname) def randnum(self,threadname): a=random.randint(0,5) time.sleep(a) print(threadname+" sleep "+str(a))starttime=time.time()threadlist=[]for i in range(5): threadname="thread"+str(i) t=MyThread(threadname) threadlist.append(t) t.start()for t in threadlist: t.join()endtime=time.time()print('total time :'+ str(endtime-starttime))
16.2 线程的其它问题
如果想要启动一个线程,让它在python退出之后也存在,那么可以通过thread的setDaemon()来进行设置。
在某些机器上面不支持thread类,那么可以使用dummy_thread类。这个类不是python内部实现的,是一个c语言的外部类,因此无法检测它的状态。无法对它进行join()。
python的thread类只能够在一个cpu核运行,因为python进程锁的关系。如果要尽多核能力,可以使用multiprocessing类,它可以平衡多核cpu的使用。
线程的使用还有很多内容。这个涉及到了操作系统部分的内容。
比如多个线程之间可能访问同一个资源,比如同一个文件,数据库的同一块数据等等。此时在同一个时间可能有多个线程进行数据的读写,但是数据的存储空间只有一个,在这里就需要多个线程排队访问了。这需要一个锁来控制线程的访问。这就是线程锁的概念。
比如多个线程之间可能需要通信,一个线程执行完了,可能需要唤起另外一个线程,这个又涉及到了线程之间通信的问题。有时候线程A需要等线程B的唤起,而线程B需要等待线程C的唤起,线程C又需要等待线程A的唤起。这个时候就形成了死锁。每个线程都在等待,无法进行下一步。
这些内容在threading类当中都有体现,但是这里不进行探讨。
领取专属 10元无门槛券
私享最新 技术干货