python-线程、进程、协程
进程 && 线程
进程:是内存中的一个独立的句柄,我们可以理解为一个应用程序在内存中就是一个进程。 各个进程之间是内存相互独立,不可共享的
线程:每个应用运行之后就会对应启动一个主线程,通过主线程可以创建多个字线程,各个线程共享主进程的内存空间。
关于线程、进程的解释有一篇有趣而生动的解释(http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html)
默认应用程序:是单进程、单线程的。
进程是资源分配的最小单位。与程序相比,程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体。进程是程序在某个数据集上 的执行,是一个动态实体。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消,反映了一个程序在一定的数据集上运行的 全部动态过程。每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。
线程是轻量级的进程或子进程,是CPU调度的最小单位,所有的线程都存在于相同的进程。所以线程基本上是轻量级的进程,它负责在单个程序里执行 多任务。通常由操作系统负责多个线程的调度和执行。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。
GIL(全局解释器锁)
我们知道多进程(mutilprocess) 和 多线程(threading)的目的是用来被多颗CPU进行访问, 提高程序的执行效率。 但是在python内部存在一种机制(GIL),在多线程 时同一时刻只允许一个线程来访问CPU。
GIL 并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。
Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有
GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把
GIL 归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
虽然python支持多线程,但是由于GIL的限制,在实际运行时,程序运行后开启多个线程,但在通过GIL后同时也只能有一个线程被CPU执行。
x进程和线程对比
对比维度 |
多进程 |
多线程 |
总结 |
数据共享、同步 |
数据共享复杂,需要用IPC;数据是分开的,同步简单 |
因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 |
各有优势 |
内存、CPU |
占用内存多,切换复杂,CPU利用率低 |
占用内存少,切换简单,CPU利用率高 |
线程占优 |
创建销毁、切换 |
创建销毁、切换复杂,速度慢 |
创建销毁、切换简单,速度很快 |
线程占优 |
编程、调试 |
编程简单,调试简单 |
编程复杂,调试复杂 |
进程占优 |
可靠性 |
进程间不会互相影响 |
一个线程挂掉将导致整个进程挂掉 |
进程占优 |
分布式 |
适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 |
适应于多核分布式 |
进程占优
|
如何选用?
单进程,多线程的程序(io操作不占用CPU):如果是CPU密集型,那么则不能提高效率。如果是IO密集型,那么则能提高效率。
多进程,单线程的程序:CPU密集型的,一般用多进程提高并发效率。
小结:
CPU密集型:多进程
IO密集型:多线程
线程的创建方法:
有两种方式来创建线程:
一种是创建一个threading.Thread对象,在它的初始化函数(__init__)中将可调用对象作为参数传入。
一种是通过继承Thread类,重写它的run方法;
第一种:
import threading def f1(arg): print(arg) t = threading.Thread(target=f1,args=(123,)) t.start() #什么时候去执行f1方法? 是在执行t.run()方法时执行f1()方法。t.run()不用我们去执行,代码自动执行
从上边可以看出,创建一个多线程程序,只需要3步:
1. 创建执行函数
2. 创建threading.Thread对象
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): """This constructor should always be called with keyword arguments. Arguments are: *group* should be None; reserved for future extension when a ThreadGroup # 为了将来拓展保留的 class is implemented. *target* is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called. # 一个可调用程序,在线程启动后执行 *name* is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number. # 线程的名字,默认值为“Thread-N“,N是一个数字 *args* is the argument tuple for the target invocation. Defaults to (). # 参数args表示调用target时的参数列表 *kwargs* is a dictionary of keyword arguments for the target invocation. Defaults to {}. # 参数kwargs表示调用target时的关键字参数。 If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread. """
3. 启动线程
第二种:
import threading class MyThread(threading.Thread): def __init__(self,func,args): self.func = func self.args = args super(MyThread,self).__init__() #执行父类的构造方法 def run(self): self.func(self.args) def f2(arg): print(arg) obj = MyThread(f2,123) obj.start()
多线程的执行方法:
import time from threading import Thread def do_thread(num): print('this is thread %s' %str(num)) time.sleep(3) for i in range(5): t = Thread(target=do_thread,args=(i,)) t.start()
以上方法就开启了一个5个线程,target用来定义开启线程后要执行的方法,args为参数
线程的其它方法:
1 setName(), getName()
setName(): 给线程设置一个名字
getName(): 获取线程的名称
import time from threading import Thread def do_thread(num): print('this is thread %s' %str(num)) time.sleep(2) for i in range(2): t = Thread(target=do_thread,args=(i,)) t.start() t.setName('Mythread_{0}'.format(str(i))) print(t.getName())
out:
this is thread 0
Mythread_0
this is thread 1
Mythread_1
2 setDaemon()
setDaemon(True/False): 设置创建的子线程为前台线程或后台线程.设置为True则子线程为后台线程。线程默认为前台线程(不设置此方法)
前台线程: 当子线程创建完成后,主线程和子线程(前台线程)同时运行,如果主线程执行完成,而子线程还未完成则等待子线程执行完成以后整个程序才结束。
后台线程: 当子线程创建完成后,如果子线程还未结束,而主线程运行结束则不管子线程了,程序就结束。
此方法设置必须在 start() 方法前进行设置, 看代码:
import time from threading import Thread def do_thread(num): print('this is thread %s' %str(num)) time.sleep(3) print('OK',str(num)) for i in range(2): t = Thread(target=do_thread,args=(i,)) #不设置此方法默认为前台线程 #t.setDaemon(True) t.setName('Mythread_{0}'.format(str(i))) t.start() print(t.getName())
out:
this is thread 0
Mythread_0
this is thread 1
Mythread_1
OK 1
OK 0
后台线程:
import time from threading import Thread def do_thread(num): print('this is thread %s' %str(num)) time.sleep(3) #执行到此时主线程执行完成了,程序结束,下面的代码不会执行 print('OK',str(num)) for i in range(2): t = Thread(target=do_thread,args=(i,)) #设置线程为后台线程 t.setDaemon(True) t.setName('Mythread_{0}'.format(str(i))) t.start() print(t.getName())
out:
this is thread 0
Mythread_0
this is thread 1
Mythread_1
3 join()
join(timeout) : 多线程的 wait(),当主线程执行 子线程.join() 方法后,主线程将等待子线程执行完再接着执行。当加上timeout参数后,如果超过timeout时间不管子线程有没有执行完都将结束等待
看下面两个例子
import time from threading import Thread def do_thread(num): time.sleep(3) print("this is thread %s" % str(num)) for i in range(2): t = Thread(target=do_thread, args=(i,)) t.setName("Mythread_{0}".format(str(i))) t.start() print("print in main thread: thread name:", t.getName())
out:
print in main thread: thread name: Mythread_0
print in main thread: thread name: Mythread_1
this is thread 0
this is thread 1
上面无join方法时,主线程执行完print,等待子线程函数中的print执行完成,这个程序退出。 下面我们看看加上join方法后的效果
import time from threading import Thread def do_thread(num): time.sleep(3) print("this is thread %s" % str(num)) for i in range(2): t = Thread(target=do_thread, args=(i,)) t.setName("Mythread_{0}".format(str(i))) t.start() t.join() print("print in main thread: thread name:", t.getName())
out:
this is thread 0
print in main thread: thread name: Mythread_0
this is thread 1
print in main thread: thread name: Mythread_1
当程序运行到join后,将等待子程序执行完成,然后才向下执行。这样真个程序就变成一个单线程的顺序执行了。多线程就没什么鸟用了。
join()与setDaemon()都是等待子线程结束,有什么区别呢:
当执行join()后主线程就停了,直到子线程完成后才开始接着主线程执行,整个程序是线性的
setDaemon() 为前台线程时,所有的线程都在同时运行,主线程也在运行。只不过是主线程运行完以后等待所有子线程结束。这个还是一个并行的执行,执行效率肯定要高于join()方法的。
线程锁
线程锁分类:互斥锁,信号量,条件变量,事件
线程是内存共享的,当多个线程对内存中的同一个公共变量进行操作时,会导致线程争抢的问题,为了解决此问题,可以使用线程锁。
假设这样一种情况,有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1
#!/usr/bin/env python # -*- coding:utf-8 -*- # Auther: pangguoping import threading import time NUM = 0 class MyThread(threading.Thread): def run(self): global NUM NUM += 1 time.sleep(0.5) msg = self.name + ' set num to ' + str(NUM) print(msg) if __name__ == '__main__': for i in range(5): t = MyThread() t.start()
out:
Thread-4 set num to 5
Thread-1 set num to 5
Thread-5 set num to 5
Thread-2 set num to 5
Thread-3 set num to 5
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。因此有了线程锁的概念。
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。 互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将 资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
我们现在引入锁,上面的代码修改为:
互斥锁:
import threading import time NUM = 0 class MyThread(threading.Thread): def run(self,l): #加锁 l.acquire() global NUM NUM += 1 time.sleep(0.5) msg = self.name + ' set num to ' + str(NUM) #解锁 l.release() print(msg) if __name__ == '__main__': lock = threading.Lock() for i in range(5): t = MyThread() t.run(lock)
out:
Thread-1 set num to 1
Thread-2 set num to 2
Thread-3 set num to 3
Thread-4 set num to 4
Thread-5 set num to 5
可以看出,加了线程锁之后,尝试任何次数的执行,最终结果都是一样的,消除了线程不安全的隐患。过程是这样的:我们先建立了一个 threading.Lock类对象lock,在run方法里,我们使用lock.acquire()获得了这个锁。此时,其他的线程就无法再获得该锁 了,他们就会阻塞在“l.acquire()”这里,直到锁被另一个线程释放l.release()。
上边的锁机制,叫做“互斥锁”。互斥锁的特点:同一时间仅允许一个线程进行更改数据。如果同一时间有多个线程更改数据的话,我们可以使用 信号量 来解决这个问题
信号量:
信号量(Semaphore)是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
Semaphore信号量管理一个内置的计数器:
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
import threading import time def run(n): semaphore.acquire() time.sleep(1) print('run the thread: %s' %n) semaphore.release() if __name__ == '__main__': semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 for i in range(20): t = threading.Thread(target=run,args=(i,)) t.start()
执行之后会发现,每个1秒,会有5个线程执行,有print的操作。
import threading import time def do(): semaphro.acquire() print('this is {0} set the semaphore'.format(threading.current_thread().getName())) time.sleep(2) semaphro.release() print("\033[1;30mthi is {0} release the semaphore\033[0m".format(threading.current_thread().getName())) if __name__ == '__main__': semaphro = threading.Semaphore(2) for i in range(10): t = threading.Thread(target=do) t.setName("Thread_{0}".format(str(i))) t.start() print('finished')
上例中,虽然创建了10个线程,但同时只有2个线程在运行,就是因为在线程中通过Semaphore设置了2个信号量。只有其中一个释放后另其它的线程再能开始执行
out:
this is Thread_0 set the semaphore
this is Thread_1 set the semaphore
finished
thi is Thread_0 release the semaphore
thi is Thread_1 release the semaphore
this is Thread_3 set the semaphore
this is Thread_2 set the semaphore
thi is Thread_3 release the semaphore
this is Thread_4 set the semaphore
thi is Thread_2 release the semaphore
this is Thread_5 set the semaphore
thi is Thread_4 release the semaphore
thi is Thread_5 release the semaphore
this is Thread_7 set the semaphore
this is Thread_6 set the semaphore
this is Thread_8 set the semaphore
thi is Thread_6 release the semaphore
thi is Thread_7 release the semaphore
this is Thread_9 set the semaphore
thi is Thread_8 release the semaphore
thi is Thread_9 release the semaphore
条件变量:
互斥锁是最简单的线程同步机制,Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量, 除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一 些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会 重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。
可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。线程通过acquire获得Condition 对象,当调用wait方法时,线程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个线程。当调用 notify方法时,Condition对象会从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁。
Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock。
除了notify方法外,Condition对象还提供了notifyAll方法,可以通知waiting池中的所有线程尝试acquire内部 锁。由于上述机制,处于waiting状态的线程只能通过notify方法唤醒,所以notifyAll的作用在于防止有线程永远处于沉默状态。
假设有这样一种场景:有一群生产者(Producer)和一群消费者(Consumer)通过一个市场来交互产品。生产者的”策略“是如果市场 上剩余的产品少于1000个,那么就生产100个产品放到市场上;而消费者的”策略“是如果市场上剩余产品的数量多余100个,那么就消费3个产品。用 Condition解决生产者与消费者问题的代码如下:
参数说明: con.acquire和con.wait 通常在加锁的时候配合使用,表示在这里等待
con.acquire() # 这三个必须放一起,是固定用法,通常用于解锁的时候 con.notify(num) # 表示一次放多少个线程过去 con.release()
第一种方式:
import threading import time class Producer(threading.Thread): def run(self): global count while True: if con.acquire(): if count > 1000: con.wait() else: count = count+100 msg = self.name+' produce 100, count=' + str(count) print(msg) con.notify() con.release() time.sleep(1) class Consumer(threading.Thread): def run(self): global count while True: if con.acquire(): if count < 100: con.wait() else: count = count-3 msg = self.name+' consume 3, count='+str(count) print(msg) con.notify() con.release() time.sleep(1) count = 500 con = threading.Condition() def test(): for i in range(2): p = Producer() p.start() for i in range(5): c = Consumer() c.start() if __name__ == '__main__': test() out: Thread-1 produce 100, count=600 Thread-2 produce 100, count=700 Thread-3 consume 3, count=697 Thread-4 consume 3, count=694 Thread-5 consume 3, count=691 Thread-6 consume 3, count=688 Thread-7 consume 3, count=685 Thread-1 produce 100, count=785 Thread-3 consume 3, count=782 Thread-2 produce 100, count=882 Thread-4 consume 3, count=879 Thread-5 consume 3, count=876 Thread-6 consume 3, count=873 Thread-7 consume 3, count=870 Thread-4 consume 3, count=867 Thread-1 produce 100, count=967 Thread-5 consume 3, count=964 Thread-3 consume 3, count=961 Thread-2 produce 100, count=1061 Thread-6 consume 3, count=1058 Thread-7 consume 3, count=1055 Thread-3 consume 3, count=1052 Thread-4 consume 3, count=1049 Thread-5 consume 3, count=1046 Thread-7 consume 3, count=1043 Thread-6 consume 3, count=1040 Thread-3 consume 3, count=1037 Thread-4 consume 3, count=1034 Thread-5 consume 3, count=1031 Thread-7 consume 3, count=1028 Thread-6 consume 3, count=1025 Thread-3 consume 3, count=1022 Thread-4 consume 3, count=1019 Thread-5 consume 3, count=1016 Thread-7 consume 3, count=1013
第二种方式:con.wait_for() 表示等待某个条件成立,如果成立的话继续,不成立的话等待,接收True或者False
con.acquire() # 这三个同样一起使用 con.wait_for(条件) # 等待条件返回True con.release()
import threading def condition(): ret = False r = input(">>> ") if r == 'true': ret = True else: pass return ret def func(i, con): print(i) con.acquire() # con.acquire和con.wait 必须配合使用,表示在这里等待 con.wait_for(condition) print(i+100) con.release() c = threading.Condition() for i in range(10): t = threading.Thread(target=func, args=(i, c,)) t.start()
小结:目前,可以解决线程间“同步问题”的方法,已经有2种:互斥锁和条件变量。其实队列也是一种!!!
队列解决同步问题代码案例
import threading import time from queue import Queue class Producer(threading.Thread): def run(self): global queue count = 0 while True: for i in range(100): if queue.qsize() > 1000: pass else: count = count +1 msg = '生成产品'+str(count) queue.put(msg) print(msg) time.sleep(1) class Consumer(threading.Thread): def run(self): global queue while True: for i in range(3): if queue.qsize() < 100: pass else: msg = self.name + '消费了 '+queue.get() print(msg) time.sleep(1) queue = Queue() def test(): for i in range(500): queue.put('初始产品'+str(i)) for i in range(2): p = Producer() p.start() for i in range(5): c = Consumer() c.start() if __name__ == '__main__': test()
那么除了条件,还有其他方法来解决生产者与消费者模型中的“同步问题”么?事件(event)
事件(event)
很多时候,线程之间会有互相通信的需要。常见的情形是次要线程为主要线程执行特定的任务,在执行过程中需要不断报告执行的进度情况。前面的条件变量同步已经涉及到了线程间的通信(threading.Condition的notify方法)。更通用的方式是使用threading.Event对象。注意:事件其实也是一种锁,只不过这把锁是把线程全部锁住,直到信号被set为True的时候,线程才恢复运行。
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。
threading.Event可以使一个线程等待其他线程的通知。其内置了一个标志,初始值为False。线程通过wait()方法进入等待 状态,直到另一个线程调用set()方法将内置标志设置为True时,Event通知所有等待状态的线程恢复运行。还可以通过isSet()方法查询 Envent对象内置状态的当前值。
线程的事件, 用于主线程控制子线程的执行。它的本质就是定义了一个全局的flag标识,并通过一些方法来获取、设置此标识。包括:
wait()方法:当flag标识为False时,wait()方法将阻塞,为True时,wait()不阻塞
set()方法:设置flag标识为True
clear()方法: 设置flag标识为False
初始化时flag标识为False(阻塞状态)
is_set()/isSet() : 判断当前flag标识是否为True
import threading def do(event): print('start') #默认初始化状态为False,到这里就阻塞了 event.wait() print('execute\n') if __name__ == '__main__': event_obj = threading.Event() for i in range(10): t = threading.Thread(target=do,args=(event_obj,)) t.start() inp = input('input>>>>:') if inp == 'true': #如果为True,则flag=True ,不阻塞,子进程继续运行 event_obj.set() else: event_obj.clear()
模拟红绿灯
import threading def func(i, e): print(i) e.wait() # 表示在这里检测信号。如果检测到为红灯,则停止。如果为绿灯,则放行 print(i+100) event = threading.Event() for i in range(10): t = threading.Thread(target=func, args=(i,event)) t.start() event.clear() # 全部都停止,设置为红灯 inp = input(">>> ") if inp == "1": event.set() # 表示放行,设置为绿灯
补充:Timer
定时器,指定n秒后执行某操作
from threading import Timer def hello(): print("hello, world") t = Timer(1, hello) t.start() # after 1 seconds, "hello, world" will be printed
锁的介绍:
1、死锁:
如果多个线程要调用多个现象,而A线程调用A锁占用了A对象,B线程调用了B锁占用了B对象,A线程不能调用B对象,B线程不能调用A对象,于是一直等待。这就造成了线程“死锁”。
Threading模块中,也有一个类,RLock,称之为可重入锁。该锁对象内部维护着一个Lock和一个counter对象。counter对象记
录了acquire的次数,使得资源可以被多次require。最后,当所有RLock被release后,其他线程才能获取资源。在同一个线程
中,RLock.acquire可以被多次调用,利用该特性,可以解决部分死锁问题。
class _RLock: """This class implements reentrant lock objects. A reentrant lock must be released by the thread that acquired it. Once a thread has acquired a reentrant lock, the same thread may acquire it again without blocking; the thread must release it once for each time it has acquired it. """ def __init__(self): self._block = _allocate_lock() self._owner = None # 初始化一个计数器 self._count = 0 def __repr__(self): owner = self._owner try: owner = _active[owner].name except KeyError: pass return "<%s %s.%s object owner=%r count=%d at %s>" % ( "locked" if self._block.locked() else "unlocked", self.__class__.__module__, self.__class__.__qualname__, owner, self._count, hex(id(self)) ) def acquire(self, blocking=True, timeout=-1): """Acquire a lock, blocking or non-blocking. When invoked without arguments: if this thread already owns the lock, increment the recursion level by one, and return immediately. Otherwise, if another thread owns the lock, block until the lock is unlocked. Once the lock is unlocked (not owned by any thread), then grab ownership, set the recursion level to one, and return. If more than one thread is blocked waiting until the lock is unlocked, only one at a time will be able to grab ownership of the lock. There is no return value in this case. When invoked with the blocking argument set to true, do the same thing as when called without arguments, and return true. When invoked with the blocking argument set to false, do not block. If a call without an argument would block, return false immediately; otherwise, do the same thing as when called without arguments, and return true. When invoked with the floating-point timeout argument set to a positive value, block for at most the number of seconds specified by timeout and as long as the lock cannot be acquired. Return true if the lock has been acquired, false if the timeout has elapsed. """ me = get_ident() if self._owner == me: # 每次调用acquire,计数器加1 self._count += 1 return 1 rc = self._block.acquire(blocking, timeout) if rc: self._owner = me self._count = 1 return rc __enter__ = acquire def release(self): """Release a lock, decrementing the recursion level. If after the decrement it is zero, reset the lock to unlocked (not owned by any thread), and if any other threads are blocked waiting for the lock to become unlocked, allow exactly one of them to proceed. If after the decrement the recursion level is still nonzero, the lock remains locked and owned by the calling thread. Only call this method when the calling thread owns the lock. A RuntimeError is raised if this method is called when the lock is unlocked. There is no return value. """ if self._owner != get_ident(): raise RuntimeError("cannot release un-acquired lock") # 每次调用release,计数器减1 self._count = count = self._count - 1 if not count: self._owner = None self._block.release() def __exit__(self, t, v, tb): self.release() # Internal methods used by condition variables def _acquire_restore(self, state): self._block.acquire() self._count, self._owner = state def _release_save(self): if self._count == 0: raise RuntimeError("cannot release un-acquired lock") count = self._count self._count = 0 owner = self._owner self._owner = None self._block.release() return (count, owner) def _is_owned(self): return self._owner == get_ident()
Ps:在使用RLock的时候,要注意RLock.acquire和Rlock.release必须是成对出现的。否则会造成资源死锁,其他线程一直等待的情况,即“死锁”。
死锁发生的条件:
- 互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
- 请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。
- 环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,...pn},进程p0(或线程)等待p1占用的资 源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等 待)
上面的文字太生硬,看一个比喻就懂了:
迎面开来的汽车A和汽车B过马路,汽车A得到了半条路的资源(满足死锁发生条件1:资源访问是排他性的,我占了路你就不能上来,除非你爬我头上 去),汽车B占了汽车A的另外半条路的资源,A想过去必须请求另一半被B占用的道路(死锁发生条件2:必须整条车身的空间才能开过去,我已经占了一半,尼 玛另一半的路被B占用了),B若想过去也必须等待A让路,A是辆兰博基尼,B是开奇瑞QQ的屌丝,A素质比较低开窗对B狂骂:快给老子让开,B很生气,你 妈逼的,老子就不让(死锁发生条件3:在未使用完资源前,不能被其他线程剥夺),于是两者相互僵持一个都走不了(死锁发生条件4:环路等待条件),而且导 致整条道上的后续车辆也走不了。
2、活锁
活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
一个比喻:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。
3、饥饿
是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。
一个比喻:在“首堵”北京的某一天,天气阴沉,空气中充斥着雾霾和地沟油的味道,某个苦逼的临时工交警正在处理塞车,有两条道A和B上都堵满了车 辆,其中A道堵的时间最长,B相对相对堵的时间较短,这时,前面道路已疏通,交警按照最佳分配原则,示意B道上车辆先过,B道路上过了一辆又一辆,A道上 排队时间最长的确没法通过,只能等B道上没有车辆通过的时候再等交警发指令让A道依次通过,这也就是ReentrantLock显示锁里提供的不公平锁机 制(当然了,ReentrantLock也提供了公平锁的机制,由用户根据具体的使用场景而决定到底使用哪种锁策略),不公平锁能够提高吞吐量但不可避免 的会造成某些线程的饥饿。
4、同步阻塞
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“同步阻塞”
线程池
python的线程池有两种实现方式,我们先来看一个比较简单的实现方式.
实现思路:
- 通过队列(先进先出队列,队列都是在内存中操作,进程退出,队列清空)来实现,线程池中没有线程时为阻塞状态.
- 自定义一个线程池类,构造方法时,创建一个指定元素数量的队列,队列中的元素为
线程类
- 使用线程时,使用队列的get方法得到一个线程类,使用__call__方法创建一个线程,使用线程执行指定的程序
- 程序执行完成后,在队列中添加一个新的
线程类
import threading,time,queue class ThreadPool: def __init__(self,maxsize): self.maxsize=maxsize self._q=queue.Queue(maxsize) for i in range(maxsize): self._q.put(threading.Thread) def get_thread(self): return self._q.get() def add_thread(self): self._q.put(threading.Thread) pool=ThreadPool(5) def task(arg,p): print(arg) time.sleep(1) p.add_thread() for i in range(100): t = pool.get_thread() #线程池中没有线程为阻塞状态 obj=t(target=task,args=(i,pool)) obj.start()
此方式的缺点:
1. 线程池中线程不能被重用,一直被创建和取出
2. 线程池长度及所占内存资源有浪费,可能过多(如果任务数小于线程池大小)。如果线程执行比较快,可能只需要少量的线程就能完成
所以有了第二种方式.
第二种方式是也是使用队列,但队列中的元素为为一个个(函数名,函数参数,)
的元组,创建一个线程组成的列表,线程轮流去队列中取到元组,分解后执行函数,然后取下一个函数.
import queue import threading import contextlib import time StopEvent = object() class ThreadPool(object): def __init__(self, max_num, max_task_num = None): if max_task_num: self.q = queue.Queue(max_task_num) else: self.q = queue.Queue() self.max_num = max_num self.cancel = False self.terminal = False self.generate_list = [] self.free_list = [] def run(self, func, args, callback=None): """ 线程池执行一个任务 :param func: 任务函数 :param args: 任务函数所需参数 :param callback: 任务执行失败或成功后执行的回调函数,回调函数有两个参数1、任务函数执行状态;2、任务函数返回值(默认为None,即:不执行回调函数) :return: 如果线程池已经终止,则返回True否则None """ if self.cancel: return if len(self.free_list) == 0 and len(self.generate_list) < self.max_num: self.generate_thread() w = (func, args, callback,) self.q.put(w) def generate_thread(self): """ 创建一个线程 """ t = threading.Thread(target=self.call) t.start() def call(self): """ 循环去获取任务函数并执行任务函数 """ current_thread = threading.currentThread self.generate_list.append(current_thread) event = self.q.get() while event != StopEvent: func, arguments, callback = event try: result = func(*arguments) success = True except Exception as e: success = False result = None if callback is not None: try: callback(success, result) except Exception as e: pass with self.worker_state(self.free_list, current_thread): if self.terminal: event = StopEvent else: event = self.q.get() else: self.generate_list.remove(current_thread) def close(self): """ 执行完所有的任务后,所有线程停止 """ self.cancel = True full_size = len(self.generate_list) while full_size: self.q.put(StopEvent) full_size -= 1 def terminate(self): """ 无论是否还有任务,终止线程 """ self.terminal = True while self.generate_list: self.q.put(StopEvent) self.q.empty() @contextlib.contextmanager def worker_state(self, state_list, worker_thread): """ 用于记录线程中正在等待的线程数 """ state_list.append(worker_thread) try: yield finally: state_list.remove(worker_thread) # How to use pool = ThreadPool(5) def callback(status, result): # status, execute action status # result, execute action return value pass def action(i): print(i) for i in range(30): ret = pool.run(action, (i,), callback) time.sleep(5) print(len(pool.generate_list), len(pool.free_list)) print(len(pool.generate_list), len(pool.free_list)) pool.close() pool.terminate()
进程
python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非 常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的 转换。multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
基本使用
多进程的使用方法与多线程基本一模一样。只需要将对应的模块和方法,替换为multiprocessing.Process即可。这里不再做过多的说明和举例,线程里已经很具体了。
from multiprocessing import Process def foo(arg): print('say hi',arg) if __name__ == "__main__": for i in range(10): p = Process(target=foo,args=(i,)) #p.daemon = True # 等同于线程的threading.Thread.setDaemon p.start() #p.join() out: say hi 0 say hi 2 say hi 4 say hi 1 say hi 3 say hi 6 say hi 8 say hi 7 say hi 5 say hi 9
进程锁
当多个进程需要访问共享资源的时候,Lock可以用来避免访问的冲突。
同样的,进程锁也包含了其他类型,包括RLock,Semaphore(用来控制对共享资源的访问数量,例如池的最大连接数)。Event。同 线程也是一样的。所属模块不同而 已:multiprocessing.Lock(),multiprocessing.RLock(),multiprocessing.Semaphore(n) ,multiprocessing.Event()。 具体使用案例可以查看http://www.cnblogs.com/kaituorensheng/p/4445418.html 或者将上边讲到的线程案例进行修改。
进程间数据共享
默认情况下,进程间的数据是不可以进行共享的。但是可以通过以下三个方法进行数据共享:queues, Array, Manager.dict()
1)queues
from multiprocessing import Process from multiprocessing import queues import multiprocessing def foo(i,arg): arg.put(i) print('say hi',i,arg.qsize()) if __name__ == "__main__": li = queues.Queue(20,ctx=multiprocessing) # 源码中执行了ctx.lock for i in range(10): p = Process(target=foo,args=(i,li,)) p.start() out: say hi 0 1 say hi 2 2 say hi 1 4 say hi 3 4 say hi 6 7 say hi 5 7 say hi 4 7 say hi 7 8 say hi 9 9 say hi 8 10
2)Array(不常用)
数组:Array的特点:1.它的内存地址是连续的,而列表不是 2. 数组中元素的数据类型,在创建的时候就已经定义好了。3. 个数一定。在创建的时候就需要指定
from multiprocessing import Process from multiprocessing import Array def foo(i,arg): arg[i] = i + 100 for item in arg: print(item) print('================') if __name__ == "__main__": li = Array('i', 5) # 创建一个最大包含5个元素的数组 for i in range(5): p = Process(target=foo,args=(i,li,)) p.start() out: 100 0 0 0 0 ================ 100 101 0 0 0 ================ 100 101 102 0 0 ================ 100 101 102 103 0 ================ 100 101 102 103 104 ================
代码中的"i"类型是包含在C中类型对应表中的:
'c': ctypes.c_char, 'u': ctypes.c_wchar, 'b': ctypes.c_byte, 'B': ctypes.c_ubyte, 'h': ctypes.c_short, 'H': ctypes.c_ushort, 'i': ctypes.c_int, 'I': ctypes.c_uint, 'l': ctypes.c_long, 'L': ctypes.c_ulong, 'f': ctypes.c_float, 'd': ctypes.c_double
3)Manager.dict(常用)
from multiprocessing import Process from multiprocessing import Manager def foo(i,arg): arg[i] = i + 100 print(arg.values()) if __name__ == "__main__": obj = Manager() li = obj.dict() for i in range(5): p = Process(target=foo,args=(i,li,)) #p.daemon = True p.start() p.join() out: [100] [100, 101] [100, 101, 102] [100, 101, 102, 103] [100, 101, 102, 103, 104]
进程池
进程池在multiprocessing模块中已经定义好,只需要直接使用即可。主要包含2个方法apply和apply_async
进程池的使用
from multiprocessing import Pool import time def f1(arg): print(arg,'b') time.sleep(5) print(arg,'a') if __name__ == "__main__": pool = Pool(5) # 定义30个任务 for i in range(30): # pool.apply(func=f1,args=(i,)) pool.apply_async(func=f1,args=(i,)) # pool.close() # 等待所有的任务执行完毕后,修改进程状态为close。否则会阻塞到这里 time.sleep(2) pool.terminate() # 立即终止全部子进程 pool.join() # 主进程在这里等待子进程全部执行完毕 out: 0 b 1 b 2 b 3 b 4 b # 执行到一半已经被强制终止 Process finished with exit code 0
apply(self, func, args=(), kwds={}) # 使用arg和kwds参数调用func函数,结果返回前会一直阻塞,这样就导致子进程会顺序执行,而不是并发执行
apply_async(self, func, args=(), kwds={}, callback=None,error_callback=None)# apply()方法的一个变体,会返回一个结果对象。如果callback被指定,那么callback可以接收一个参数然后被调用,当结果准备好回调 时会调用callback,调用失败时,则用error_callback替换callback。 Callbacks应被立即完成,否则处理结果的线程会被阻塞。对比apply方法,该方法不会阻塞,类似线程的setDaemon
pool.close() # 阻止更多的任务提交到pool,待任务完成后,工作进程会退出
pool.terminate() # 不管任务是否完成,立即停止工作进程。在对pool对象进程垃圾回收的时候,会立即调用terminate()。
pool.join() # wait工作线程的退出,在调用join()前,必须调用close() or terminate()。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。
参考:https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.pool
协程
概念
线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;
协程的实现
协程的实现主要借助2个模块,greenlet和gevent(内部调用greenlet)
import gevent def fun1(): print("www.baidu.com") # 第一步 gevent.sleep(0) print("end the baidu.com") # 第三步 def fun2(): print("www.zhihu.com") # 第二步 gevent.sleep(0) print("end th zhihu.com") # 第四步 gevent.joinall([ gevent.spawn(fun1), gevent.spawn(fun2), ])
import greenlet def fun1(): print("12") # 第一步 gr2.switch() print("56") # 第三步 gr2.switch() def fun2(): print("34") # 第二步 gr1.switch() print("78") # 第四步 gr1 = greenlet.greenlet(fun1) gr2 = greenlet.greenlet(fun2) gr1.switch()
from gevent import monkey; monkey.patch_all() import gevent import urllib2 def f(url): print('GET: %s' % url) resp = urllib2.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://github.com/'), ])
event loop是协程执行的控制点, 如果你希望执行协程, 就需要用到它们。
event loop提供了如下的特性:
•注册、执行、取消延时调用(异步函数)
•创建用于通信的client和server协议(工具)
•创建和别的程序通信的子进程和协议(工具)
•把函数调用送入线程池中
import asyncio async def cor1(): print("COR1 start") await cor2() print("COR1 end") async def cor2(): print("COR2") loop = asyncio.get_event_loop() loop.run_until_complete(cor1()) loop.close() out: COR1 start COR2 COR1 end
最后三行是重点。
•asyncio.get_event_loop() : asyncio启动默认的event loop
•run_until_complete() : 这个函数是阻塞执行的,知道所有的异步函数执行完成,
•close() : 关闭event loop。
参考:
http://www.cnblogs.com/jishuweiwang/p/5660933.html
http://blog.csdn.net/songfreeman/article/details/50822795
感谢两位博主