python高级之多线程
本节内容
线程与进程定义及区别
python全局解释器锁
线程的定义及使用
互斥锁
线程死锁和递归锁
条件变量同步(Condition)
同步条件(Event)
信号量
队列Queue
Python中的上下文管理器(contextlib模块)
自定义线程池
1.线程与进程定义及区别
线程的定义:
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程的定义:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程和线程的区别
Threads share the address space of the process that created it; processes have their own address space.
线程的地址空间共享,每个进程有自己的地址空间。
Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
一个进程中的线程直接接入他的进程的数据段,但是每个进程都有他们自己的从父进程拷贝过来的数据段
Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
一个进程内部的线程之间能够直接通信,进程之间必须使用进程间通信实现通信
New threads are easily created; new processes require duplication of the parent process.
新的线程很容易被创建,新的进程需要从父进程复制
Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
一个进程中的线程间能够有相当大的控制力度,进程仅仅只能控制他的子进程
Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
改变主线程(删除,优先级改变等)可能影响这个进程中的其他线程;修改父进程不会影响子进程
2.python全局解释器锁
全局解释器锁又叫做GIL
python目前有很多解释器,目前使用最广泛的是CPython,还有PYPY和JPython等解释器,但是使用最广泛的还是CPython解释器,而对于全局解释器锁来说,就是在CPython上面才有的,它的原理是在解释器层面加上一把大锁,保证同一时刻只能有一个python线程在解释器中执行。
对于计算密集型的python多线程来说,无法利用到多线程带来的效果, 在2.7时计算密集型的python多线程执行效率比顺序执行的效率还低的多,在python3.5中对这种情况进行了优化,基本能实现这种多线程执行时间和顺序执行时间差不多的效果。
对于I/O密集型的python多线程来说,GIL的影响不是很大,因为I/O密集型的python多线程进程,每个线程在等待I/O的时候,将会释放GIL资源,供别的线程来抢占。所以对于I/O密集型的python多线程进程来说,还是能比顺序执行的效率要高的。
python的GIL这个东西。。。比较恶心,但是由于CPython解释器中的很多东西都是依赖这个东西开发的,如果改的话,将是一件浩大的工程。。。所以到现在还是存在这个问题,这也是python最为别人所诟病的地方。。。
3.线程的定义及使用
线程的两种调用方式
线程的调用有两种方式,分为直接调用和继承式调用,示例代码如下:
1 #直接调用
2 importthreading3 importtime4
5 def sayhi(num): #定义每个线程要运行的函数
6
7 print("running on number:%s" %num)8
9 time.sleep(3)10
11 if __name__ == '__main__':12
13 t1 = threading.Thread(target=sayhi,args=(1,)) #生成一个线程实例
14 t2 = threading.Thread(target=sayhi,args=(2,)) #生成另一个线程实例
15
16 t1.start() #启动线程
17 t2.start() #启动另一个线程
18
19 print(t1.getName()) #获取线程名
20 print(t2.getName())21
22 #继承式调用
23 importthreading24 importtime25
26
27 classMyThread(threading.Thread):28 def __init__(self,num):29 threading.Thread.__init__(self)30 self.num =num31
32 def run(self):#定义每个线程要运行的函数
33
34 print("running on number:%s" %self.num)35
36 time.sleep(3)37
38 if __name__ == '__main__':39
40 t1 = MyThread(1)41 t2 = MyThread(2)42 t1.start()43 t2.start()
可以看到直接调用是导入threading模块并定义一个函数,之后实例化threading.Thread类的时候,将刚定义的函数名通过target参数传递进去,然后调用实例的start()方法启动一个线程。
而继承式调用是创建一个类继承自threading.Thread类,并在构造方法中调用父类的构造方法,之后重写run方法,run方法中就是每个线程起来之后执行的内容,就类似于前面通过target参数传递进去的函数。之后以这个继承的类来创建对象,并执行对象的start()方法启动一个线程。
从这里可以看出,其实。。。直接调用通过使用target参数把函数带进类里面之后应该是用这个函数替代了run方法。
join和setDaemon
join()方法在该线程对象启动了之后调用线程的join()方法之后,那么主线程将会阻塞在当前位置直到子线程执行完成才继续往下走,如果所有子线程对象都调用了join()方法,那么主线程将会在等待所有子线程都执行完之后再往下执行。
setDaemon(True)方法在子线程对象调用start()方法(启动该线程)之前就调用的话,将会将该子线程设置成守护模式启动,这是什么意思呢?当子线程还在运行的时候,父线程已经执行完了,如果这个子线程设置是以守护模式启动的,那么随着主线程执行完成退出时,子线程立马也退出,如果没有设置守护启动子线程(也就是正常情况下)的话,主线程执行完成之后,进程会等待所有子线程执行完成之后才退出。
示例代码如下:
1 importthreading2 from time importctime,sleep3 importtime4
5 defmusic(func):6 for i in range(2):7 print ("Begin listening to %s. %s" %(func,ctime()))8 sleep(4)9 print("end listening %s"%ctime())10
11 defmove(func):12 for i in range(2):13 print ("Begin watching at the %s! %s" %(func,ctime()))14 sleep(5)15 print('end watching %s'%ctime())16
17 threads =[]18 t1 = threading.Thread(target=music,args=('七里香',))19 threads.append(t1)20 t2 = threading.Thread(target=move,args=('阿甘正传',))21 threads.append(t2)22
23 if __name__ == '__main__':24
25 for t inthreads:26 #t.setDaemon(True)
27 t.start()28 #t.join()
29 #t1.join()
30 t2.join()########考虑这三种join位置下的结果?
31 print ("all over %s" %ctime())
4.互斥锁
互斥锁的产生是因为前面提到过多线程之间是共享同一块内存地址的,也就是说多个不同的线程能够访问同一个变量中的数据,那么,当多个线程要修改这个变量,会产生什么情况呢?当多个线程修改同一个数据的时候,如果操作的时间够短的话,能得到我们想要的结果,但是,如果修改数据不是原子性的(这中间的时间太长)的话。。。很有可能造成数据的错误覆盖,从而得到我们不想要的结果。例子如下:
1 importtime2 importthreading3
4 defaddNum():5 global num #在每个线程中都获取这个全局变量
6 #num-=1 # 如果是这种情况,那么操作时间足够短,类似于原子操作了,所以,能够得到我们想要的结果
7
8 temp=num9 print('--get num:',num ) #因为print会调用终端输出,终端是一个设备,相当于要等待终端I/O就绪之后才能输出打印内容,在等待终端I/O的过程中,该线程已经挂起。。。这时其他线程获取到的是没被改变之前的num值,之后该线程I/O就绪之后切换回来,对num-1了,其他线程在I/O就绪之后也在没被改变之前的num基础上减一,这样。。。就得到了我们不想看到的结果。。。
10 #time.sleep(0.1) # sleep也能达到相同的效果,执行到sleep时,该线程直接进入休眠状态,释放了GIL直到sleep时间过去。
11 num =temp-1 #对此公共变量进行-1操作
12
13
14 num = 100 #设定一个共享变量
15 thread_list =[]16 for i in range(100):17 t = threading.Thread(target=addNum)18 t.start()19 thread_list.append(t)20
21 for t in thread_list: #等待所有线程执行完毕
22 t.join()23
24 print('final num:', num )25 这时候,就需要互斥锁出场了,前面出现的num可以被称作临界资源(会被多个线程同时访问),为了让临界资源能够实现按照我们控制访问,需要使用互斥锁来锁住临界资源,当一个线程需要访问临界资源时先检查这个资源有没有被锁住,如果没有被锁住,那么访问这个资源并同时给这个资源加上锁,这样别的线程就无法访问该临界资源了,直到这个线程访问完了这个临界资源之后,释放这把锁,其他线程才能够抢占该临界资源。这个,就是互斥锁的概念。 示例代码:26
27 importtime28 importthreading29
30 defaddNum():31 global num #在每个线程中都获取这个全局变量
32 #num-=1
33 lock.acquire() #检查互斥锁,如果没锁住,则锁住并往下执行,如果检查到锁住了,则挂起等待锁被释放时再抢占。
34 temp=num35 print('--get num:',num )36 #time.sleep(0.1)
37 num =temp-1 #对此公共变量进行-1操作
38 lock.release() #释放该锁
39
40 num = 100 #设定一个共享变量
41 thread_list =[]42 lock=threading.Lock() #定义互斥锁
43
44 for i in range(100):45 t = threading.Thread(target=addNum)46 t.start()47 thread_list.append(t)48
49 for t in thread_list: #等待所有线程执行完毕
50 t.join()51
52 print('final num:', num )
互斥锁与GIL的关系?
Python的线程在GIL的控制之下,线程之间,对整个python解释器,对python提供的C API的访问都是互斥的,这可以看作是Python内核级的互斥机制。但是这种互斥是我们不能控制的,我们还需要另外一种可控的互斥机制———用户级互斥。内核级通过互斥保护了内核的共享资源,同样,用户级互斥保护了用户程序中的共享资源。
GIL 的作用是:对于一个解释器,只能有一个thread在执行bytecode。所以每时每刻只有一条bytecode在被执行一个thread。GIL保证了bytecode 这层面上是thread safe的。 但是如果你有个操作比如 x += 1,这个操作需要多个bytecodes操作,在执行这个操作的多条bytecodes期间的时候可能中途就换thread了,这样就出现了data races的情况了。
5.线程死锁和递归锁
如果公共的临界资源比较多,并且线程间都使用互斥锁去访问临界资源,那么将有可能出现一个情况:
线程1拿到了资源A,接着需要资源B才能继续执行下去
线程2拿到了资源B,接着需要资源A才能继续执行下去
这样,线程1和线程2互不相让。。。结果就都卡死在这了,这就是线程死锁的由来。。。
示例代码如下:
1 importthreading,time2
3 classmyThread(threading.Thread):4 defdoA(self):5 lockA.acquire() #锁住A资源
6 print(self.name,"gotlockA",time.ctime())7 time.sleep(3)8 lockB.acquire() #锁住B资源
9 print(self.name,"gotlockB",time.ctime())10 lockB.release() #解锁B资源
11 lockA.release() #解锁A资源
12
13 defdoB(self):14 lockB.acquire()15 print(self.name,"gotlockB",time.ctime())16 time.sleep(2)17 lockA.acquire()18 print(self.name,"gotlockA",time.ctime())19 lockA.release()20 lockB.release()21 defrun(self):22 self.doA()23 self.doB()24 if __name__=="__main__":25
26 lockA=threading.Lock()27 lockB=threading.Lock()28 threads=[]29 for i in range(5):30 threads.append(myThread())31 for t inthreads:32 t.start()33 for t inthreads:34 t.join() #等待线程结束
那么,怎么解决这个问题呢?python中提供了一个方法(不止python中,基本上所有的语言中都支持这个方法)那就是递归锁。递归锁的创建是使用threading.RLock(),它里面其实维护了两个东西,一个是Lock,另一个是counter,counter记录了加锁的次数,每加一把锁,counter就会+1,释放一次锁counter就会减一,直到所有加的锁都被释放掉了之后其他线程才能够访问这把锁获取资源。当然这个限制是对于线程之间的,同一个线程中,只要这个线程抢到了这把锁,那么这个线程就可以对这把锁加多个锁,而不会阻塞自己的执行。这就是递归锁的原理。
示例代码:
1 importtime2 importthreading3 classAccount:4 def __init__(self, _id, balance):5 self.id =_id6 self.balance =balance7 self.lock =threading.RLock()8
9 defwithdraw(self, amount):10 with self.lock: #会将包裹的这块代码用锁保护起来,直到这块代码执行完成之后,这把锁就会被释放掉
11 self.balance -=amount12
13 defdeposit(self, amount):14 with self.lock:15 self.balance +=amount16
17
18 def drawcash(self, amount):#lock.acquire中嵌套lock.acquire的场景
19
20 with self.lock:21 interest=0.05
22 count=amount+amount*interest23
24 self.withdraw(count)25
26
27 deftransfer(_from, to, amount):28
29 #锁不可以加在这里 因为其他的线程执行的其它方法在不加锁的情况下数据同样是不安全的
30 _from.withdraw(amount)31 to.deposit(amount)32
33 alex = Account('alex',1000)34 yuan = Account('yuan',1000)35
36 t1=threading.Thread(target = transfer, args = (alex,yuan, 100))37 t1.start()38
39 t2=threading.Thread(target = transfer, args = (yuan,alex, 200))40 t2.start()41
42 t1.join()43 t2.join()44
45 print('>>>',alex.balance)46 print('>>>',yuan.balance)
6.条件变量同步(Condition)
有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。
lock_con=threading.Condition([Lock/Rlock]): 锁是可选选项,不传人锁,对象自动创建一个RLock()。
wait():条件不满足时调用,线程会释放锁并进入等待阻塞;
notify():条件创造后调用,通知等待池激活一个线程;
notifyAll():条件创造后调用,通知等待池激活所有线程。
示例代码:
1 importthreading,time2 from random importrandint3 classProducer(threading.Thread):4 defrun(self):5 globalL6 whileTrue:7 val=randint(0,100)8 print('生产者',self.name,":Append"+str(val),L)9 iflock_con.acquire():10 L.append(val)11 lock_con.notify()12 lock_con.release()13 time.sleep(3)14 classConsumer(threading.Thread):15 defrun(self):16 globalL17 whileTrue:18 lock_con.acquire()19 if len(L)==0:20 lock_con.wait()21 print('消费者',self.name,":Delete"+str(L[0]),L)22 delL[0]23 lock_con.release()24 time.sleep(0.25)25
26 if __name__=="__main__":27
28 L=[]29 lock_con=threading.Condition()30 threads=[]31 for i in range(5):32 threads.append(Producer())33 threads.append(Consumer())34 for t inthreads:35 t.start()36 for t inthreads:37 t.join()
7.同步条件(Event)
条件同步和条件变量同步差不多意思,只是少了锁功能,因为条件同步设计于不访问共享资源的条件环境。event=threading.Event():条件环境对象,初始值 为False;
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
例子1:
1 importthreading,time2 classBoss(threading.Thread):3 defrun(self):4 print("BOSS:今晚大家都要加班到22:00。")5 event.isSet() orevent.set()6 time.sleep(5)7 print("BOSS:<22:00>可以下班了。")8 event.isSet() orevent.set()9 classWorker(threading.Thread):10 defrun(self):11 event.wait()12 print("Worker:哎……命苦啊!")13 time.sleep(0.25)14 event.clear()15 event.wait()16 print("Worker:OhYeah!")17 if __name__=="__main__":18 event=threading.Event()19 threads=[]20 for i in range(5):21 threads.append(Worker())22 threads.append(Boss())23 for t inthreads:24 t.start()25 for t inthreads:26 t.join()
例子2:
1 importthreading,time2 importrandom3 deflight():4 if notevent.isSet():5 event.set() #wait就不阻塞 #绿灯状态
6 count =07 whileTrue:8 if count < 10:9 print('\033[42;1m--green light on---\033[0m')10 elif count <13:11 print('\033[43;1m--yellow light on---\033[0m')12 elif count <20:13 ifevent.isSet():14 event.clear()15 print('\033[41;1m--red light on---\033[0m')16 else:17 count =018 event.set() #打开绿灯
19 time.sleep(1)20 count +=1
21 defcar(n):22 while 1:23 time.sleep(random.randrange(10))24 if event.isSet(): #绿灯
25 print("car [%s] is running.." %n)26 else:27 print("car [%s] is waiting for the red light.." %n)28 if __name__ == '__main__':29 event =threading.Event()30 Light = threading.Thread(target=light)31 Light.start()32 for i in range(3):33 t = threading.Thread(target=car,args=(i,))34 t.start()
8.信号量
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。
计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。(类似于停车位的概念)
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
例子:
1 importthreading,time2 classmyThread(threading.Thread):3 defrun(self):4 ifsemaphore.acquire():5 print(self.name)6 time.sleep(5)7 semaphore.release()8 if __name__=="__main__":9 semaphore=threading.Semaphore(5)10 thrs=[]11 for i in range(100):12 thrs.append(myThread())13 for t inthrs:14 t.start()
9.队列Queue
使用队列方法:
1 创建一个“队列”对象2 importQueue3 q = Queue.Queue(maxsize = 10)4 Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。5
6 将一个值放入队列中7 q.put(10)8 调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为9 1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。10
11 将一个值从队列中取出12 q.get()13 调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。14
15 Python Queue模块有三种队列及构造函数:16 1、Python Queue模块的FIFO队列先进先出。 classqueue.Queue(maxsize)17 2、LIFO类似于堆,即先进后出。 classqueue.LifoQueue(maxsize)18 3、还有一种是优先级队列级别越低越先出来。 classqueue.PriorityQueue(maxsize)19
20 此包中的常用方法(q =Queue.Queue()):21 q.qsize() 返回队列的大小22 q.empty() 如果队列为空,返回True,反之False23 q.full() 如果队列满了,返回True,反之False24 q.full 与 maxsize 大小对应25 q.get([block[, timeout]]) 获取队列,timeout等待时间26 q.get_nowait() 相当q.get(False)27 非阻塞 q.put(item) 写入队列,timeout等待时间28 q.put_nowait(item) 相当q.put(item, False)29 q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号30 q.join() 实际上意味着等到队列为空,再执行别的操作
例子集合:
1 #例子1:
2 importthreading,queue3 from time importsleep4 from random importrandint5 classProduction(threading.Thread):6 defrun(self):7 whileTrue:8 r=randint(0,100)9 q.put(r)10 print("生产出来%s号包子"%r)11 sleep(1)12 classProces(threading.Thread):13 defrun(self):14 whileTrue:15 re=q.get()16 print("吃掉%s号包子"%re)17 if __name__=="__main__":18 q=queue.Queue(10)19 threads=[Production(),Production(),Production(),Proces()]20 for t inthreads:21 t.start()22
23 #例子2
24 importtime,random25 importqueue,threading26 q =queue.Queue()27 defProducer(name):28 count =029 while count <20:30 time.sleep(random.randrange(3))31 q.put(count)32 print('Producer %s has produced %s baozi..' %(name, count))33 count +=1
34 defConsumer(name):35 count =036 while count <20:37 time.sleep(random.randrange(4))38 if notq.empty():39 data =q.get()40 print(data)41 print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data))42 else:43 print("-----no baozi anymore----")44 count +=1
45 p1 = threading.Thread(target=Producer, args=('A',))46 c1 = threading.Thread(target=Consumer, args=('B',))47 p1.start()48 c1.start()49
50 #例子3
51 #实现一个线程不断生成一个随机数到一个队列中(考虑使用Queue这个模块)
52 #实现一个线程从上面的队列里面不断的取出奇数
53 #实现另外一个线程从上面的队列里面不断取出偶数
54
55 importrandom,threading,time56 from queue importQueue57 #Producer thread
58 classProducer(threading.Thread):59 def __init__(self, t_name, queue):60 threading.Thread.__init__(self,name=t_name)61 self.data=queue62 defrun(self):63 for i in range(10): #随机产生10个数字 ,可以修改为任意大小
64 randomnum=random.randint(1,99)65 print ("%s: %s is producing %d to the queue!" %(time.ctime(), self.getName(), randomnum))66 self.data.put(randomnum) #将数据依次存入队列
67 time.sleep(1)68 print ("%s: %s finished!" %(time.ctime(), self.getName()))69
70 #Consumer thread
71 classConsumer_even(threading.Thread):72 def __init__(self,t_name,queue):73 threading.Thread.__init__(self,name=t_name)74 self.data=queue75 defrun(self):76 while 1:77 try:78 val_even = self.data.get(1,5) #get(self, block=True, timeout=None) ,1就是阻塞等待,5是超时5秒
79 if val_even%2==0:80 print ("%s: %s is consuming. %d in the queue is consumed!" %(time.ctime(),self.getName(),val_even))81 time.sleep(2)82 else:83 self.data.put(val_even)84 time.sleep(2)85 except: #等待输入,超过5秒 就报异常
86 print ("%s: %s finished!" %(time.ctime(),self.getName()))87 break
88 classConsumer_odd(threading.Thread):89 def __init__(self,t_name,queue):90 threading.Thread.__init__(self, name=t_name)91 self.data=queue92 defrun(self):93 while 1:94 try:95 val_odd = self.data.get(1,5)96 if val_odd%2!=0:97 print ("%s: %s is consuming. %d in the queue is consumed!" %(time.ctime(), self.getName(), val_odd))98 time.sleep(2)99 else:100 self.data.put(val_odd)101 time.sleep(2)102 except:103 print ("%s: %s finished!" %(time.ctime(), self.getName()))104 break
105 #Main thread
106 defmain():107 queue =Queue()108 producer = Producer('Pro.', queue)109 consumer_even = Consumer_even('Con_even.', queue)110 consumer_odd = Consumer_odd('Con_odd.',queue)111 producer.start()112 consumer_even.start()113 consumer_odd.start()114 producer.join()115 consumer_even.join()116 consumer_odd.join()117 print ('All threads terminate!')118
119 if __name__ == '__main__':120 main()121
122 #注意:列表是线程不安全的
123 #例子4
124 importthreading,time125
126 li=[1,2,3,4,5]127
128 defpri():129 whileli:130 a=li[-1]131 print(a)132 time.sleep(1)133 try:134 li.remove(a)135 except:136 print('----',a)137
138 t1=threading.Thread(target=pri,args=())139 t1.start()
源码
t2=threading.Thread(target=pri,args=())
t2.start()