threading模块
线程简述
线程(轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文。可以将它们认为是在一个主进程或"主线程"中并行运行的一些"迷你进程"。
线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录运行的上下文。它其他线程运行时,它可以被抢占(中断)和临时挂起(睡眠/加锁)---这种做法叫做让步(yielding)。
多线程的创建
使用Thread类,可以有很多方法来创建线程,其中常用的有:
- 创建Thread的示例,传给它一个函数;
- 派生Thread的子类,重新run方法,并创建子类的实例。
示例1:创建Thread的实例,传给它一个函数
from threading import Threadimport timedef test(): print("---hello-world---") time.sleep(1)for i in range(5): #创建线程,线程执行的任务是target指定的函数,如果函数需要传入参数,则可以指定args=(),或者kwargs={} t = Thread(target=test) t.start()
运行结果:
---hello-world------hello-world------hello-world------hello-world------hello-world---
示例2:使用Thread子类创建线程
import threadingimport time# 自定义类继承threading类class myThread(threading.Thread): # 重新run方法 def run(self): for i in range(3): time.sleep(1) msg = "I'm " + self.name+' @ '+str(i) print(msg)if __name__ == "__main__": # 创建线程 t = myThread() t.start()
运行结果:
I'm Thread-1 @ 0I'm Thread-1 @ 1I'm Thread-1 @ 2
python的threading.Thread
类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
多线程共享全局变量
在一个进程中,多个线程之间是共享全局变量的,即一个线程修改了全局变量,另外一个线程在此之后获取的这个全局变量是被修改后的。比如下面例子:
from threading import Threadimport timenum = 100def thread1(): global num for i in range(3): num += 1 print("I'am Thread1 ." + " my num is "+str(num))def thread2(): print("I'am Thread2. " +" my num is "+ str(num))t1 = Thread(target=thread1)t1.start()# 让程序睡眠1秒钟,确保线程1执行完毕。time.sleep(1)t2 = Thread(target=thread2)t2.start()
运行结果:
I'am Thread1. my num is 103I'am Thread2. my num is 103
线程关于全局变量注意点
在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)一个进程中的各个线程与主线程共享同一片数据空间。因此与进程相比,线程之间的信息共享和通信更加容易。在一个程序中,线程的执行是:每个线程运行一小会,然后让步给其他线程(再次排队等待更多的CPU时间)。在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其他线程进行通信。
当然,这种共享是有风险的。如果两个或多个线程访问同一片数据,由于数据的访问顺序不同可能导致结果不一致。这种情况叫竞态条件。辛运的是,大多数线程库都有一些同步源语,以允许线程管理器控制执行和访问。
同步和互斥锁
同步
一般在多线程代码中,总有一些函数或者代码块不希望被多个线程同时执行,如果有两个线程运行的顺序发生变化,就有可能造成代码的执行轨迹或行为不同,产生不一样的数据。这时候就需要使用同步了。
同步可以理解为协同步调,按预定的先后次序进行运行
,比如你先说,我再讲。
示例1:多个线程对全局变量修改的bug
from threading import Threadimport timenum = 0 def work1(): global num for i in range(1000000): num += 1 print("-work1-num:%d"%num) def work2(): global num for i in range(1000000): num += 1 print("-work2-num:%d"%num)t1 = Thread(target=work1)t1.start() #time.sleep(3)t2 = Thread(target=work2)t2.start()
运行结果:
-work1-num:1105962-work2-num:1150358
这个程序是两个线程同时对全局变量num进行相加操作,但是因为多线程中线程的执行顺序是不同的,因此出现最后相加结果不是2000000的结果。
示例2:避免全局变量被修改的方法
避免上面的情况可以有很多种方法,第一种是将上面time.sleep(3)的注释去掉,就是在3秒内让线程1执行,3s内执行完毕再执行线程2对num变量进行自增。(不过这种方法跟单线程没区别,也就没有意义去创建多线程了...)第二种就是使用轮询,代码示例如下:
from threading import Threadimport timenum = 0item = 1def work1(): global num global item if item == 1: for i in range(1000000): num += 1 item = 0 print("-work1-num:%d"%num)def work2(): global num while True:# 轮询,一直查看条件是否满足,线程2一直在执行... if item != 1: for i in range(1000000): num += 1 break print("-work2-num:%d"%num)t1 = Thread(target=work1)t1.start()#time.sleep(3)t2 = Thread(target=work2)t2.start()
运行结果:
-work1-num:1000000-work2-num:200000
这次结果就正确的了,不过这种方法效率也不高。第三种方法就是锁了。
互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定
。
#创建锁mutex = threading.Lock()#锁定mutex.acquire([blocking])#释放mutex.release()
其中,锁定方法acquire可以有一个blocking参数。
- 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
- 如果设定blocking为False,则当前线程不会堵
示例:
from threading import Thread,Lockimport timenum = 0def work1(): global num # 上锁 mutex.acquire() for i in range(1000000): num += 1 # 解锁 mutex.release() print("-work1-num:%d"%num)def work2(): global num mutex.acquire() for i in range(1000000): num += 1 mutex.release() print("-work2-num:%d"%num)# 创建一把锁,这个锁默认是没有上锁的mutex = Lock()t1 = Thread(target=work1)t1.start()#time.sleep(3)t2 = Thread(target=work2)t2.start()
运行结果:
-work1-num:1000000-work2-num:2000000
代码中定义了一把锁mutex,线程t1和线程t2都互相竞争着这把锁,谁先上锁,另一方就上不了锁而堵塞。当上锁的线程执行完毕进行解锁,堵塞的线程就争夺到上锁权而进行代码块的运行。