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 import threading 3 import time 4 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 import threading 24 import time 25 26 27 class MyThread(threading.Thread): 28 def __init__(self,num): 29 threading.Thread.__init__(self) 30 self.num = num 31 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 import threading 2 from time import ctime,sleep 3 import time 4 5 def music(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 def move(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 in threads: 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 import time 2 import threading 3 4 def addNum(): 5 global num #在每个线程中都获取这个全局变量 6 # num-=1 # 如果是这种情况,那么操作时间足够短,类似于原子操作了,所以,能够得到我们想要的结果 7 8 temp=num 9 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 import time 28 import threading 29 30 def addNum(): 31 global num #在每个线程中都获取这个全局变量 32 # num-=1 33 lock.acquire() # 检查互斥锁,如果没锁住,则锁住并往下执行,如果检查到锁住了,则挂起等待锁被释放时再抢占。 34 temp=num 35 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 import threading,time 2 3 class myThread(threading.Thread): 4 def doA(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 def doB(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 def run(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 in threads: 32 t.start() 33 for t in threads: 34 t.join() # 等待线程结束
那么,怎么解决这个问题呢?python中提供了一个方法(不止python中,基本上所有的语言中都支持这个方法)那就是递归锁。递归锁的创建是使用threading.RLock(),它里面其实维护了两个东西,一个是Lock,另一个是counter,counter记录了加锁的次数,每加一把锁,counter就会+1,释放一次锁counter就会减一,直到所有加的锁都被释放掉了之后其他线程才能够访问这把锁获取资源。当然这个限制是对于线程之间的,同一个线程中,只要这个线程抢到了这把锁,那么这个线程就可以对这把锁加多个锁,而不会阻塞自己的执行。这就是递归锁的原理。
示例代码:
1 import time 2 import threading 3 class Account: 4 def __init__(self, _id, balance): 5 self.id = _id 6 self.balance = balance 7 self.lock = threading.RLock() 8 9 def withdraw(self, amount): 10 with self.lock: # 会将包裹的这块代码用锁保护起来,直到这块代码执行完成之后,这把锁就会被释放掉 11 self.balance -= amount 12 13 def deposit(self, amount): 14 with self.lock: 15 self.balance += amount 16 17 18 def drawcash(self, amount):#lock.acquire中嵌套lock.acquire的场景 19 20 with self.lock: 21 interest=0.05 22 count=amount+amount*interest 23 24 self.withdraw(count) 25 26 27 def transfer(_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 import threading,time 2 from random import randint 3 class Producer(threading.Thread): 4 def run(self): 5 global L 6 while True: 7 val=randint(0,100) 8 print('生产者',self.name,":Append"+str(val),L) 9 if lock_con.acquire(): 10 L.append(val) 11 lock_con.notify() 12 lock_con.release() 13 time.sleep(3) 14 class Consumer(threading.Thread): 15 def run(self): 16 global L 17 while True: 18 lock_con.acquire() 19 if len(L)==0: 20 lock_con.wait() 21 print('消费者',self.name,":Delete"+str(L[0]),L) 22 del L[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 in threads: 35 t.start() 36 for t in threads: 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 import threading,time 2 class Boss(threading.Thread): 3 def run(self): 4 print("BOSS:今晚大家都要加班到22:00。") 5 event.isSet() or event.set() 6 time.sleep(5) 7 print("BOSS:<22:00>可以下班了。") 8 event.isSet() or event.set() 9 class Worker(threading.Thread): 10 def run(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 in threads: 24 t.start() 25 for t in threads: 26 t.join()
例子2:
1 import threading,time 2 import random 3 def light(): 4 if not event.isSet(): 5 event.set() #wait就不阻塞 #绿灯状态 6 count = 0 7 while True: 8 if count < 10: 9 print('