python 线程,进程与协程
引言
在学习过socket和socketserver后,我们了解到socketserver可以支持IO多路复用。在定义socketserver服务端的时候一般会使用:
server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer)
ThreadingTCPServer这个类便是可以支持多线程和TCP协议的socketserver模块。读源码的时候可以发现其继承关系:
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
右边的TCPServer实际上是它主要的功能父类,而左边的ThreadingMixIn则是实现了多线程的类,它自己本身则没有任何代码。MixIn在python的类命名中,很常见,一般被称为“混入”,戏称“乱入”,通常为了某种重要功能被子类继承。
class ThreadingMixIn: daemon_threads = False def process_request_thread(self, request, client_address): try: self.finish_request(request, client_address) self.shutdown_request(request) except: self.handle_error(request, client_address) self.shutdown_request(request) def process_request(self, request, client_address): t = threading.Thread(target = self.process_request_thread, args = (request, client_address)) t.daemon = self.daemon_threads t.start()
一、线程:
import threading import time def show(arg): time.sleep(1) print('thread'+str(arg)) for i in range(10): t = threading.Thread(target=show, args=(i,)) t.start() print('main thread stop')
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()
#!/usr/bin/env python # _*_ coding:utf_8 _*_ import threading import time NUM = 0 def f1(): global NUM NUM+=1 name = t.getName() time.sleep(1) print(name,'执行结果',NUM) for i in range(10): t = threading.Thread(target=f1) t.start() #f1作为子线程执行,由于存在time.sleep(1),当主进程执行完毕之后,子线程才开始执行。导致结果出错。 print('执行结束')
执行结果:
执行结束 Thread-1 执行结果 10 Thread-4 执行结果 10 Thread-3 执行结果 10 Thread-5 执行结果 10 Thread-2 执行结果 10 Thread-6 执行结果 10 Thread-8 执行结果 10 Thread-7 执行结果 10 Thread-10 执行结果 10 Thread-9 执行结果 10
- Lock 普通锁(不可嵌套)
- RLock 普通锁(可嵌套)常用
- Semaphore 信号量
- event 事件
- condition 条件
import threading import time NUM = 0 def f1(i,lock): global NUM name = t.getName()#t是定义的线程,将这句代码放在线程锁之后会导致无法获取正确的线程名 lock.acquire() #定义锁作用空间的起始位置 NUM+=1 #name = threading.current_thread().name #等效于getName,不过输出的是每次执行的线程名 time.sleep(1) print(name,i,'执行结果',NUM) lock.release() #释放锁 lock = threading.RLock() #lock = threading.Lock() for i in range(30): t = threading.Thread(target=f1,args=(i,lock,)) t.start() #f1作为子线程执行,由于存在time.sleep(1),当主进程执行完毕之后,子线程才开始执行。如果不使用线程锁的话结果出错。 print('执行结束')
import threading import time NUM = 0 def f1(i,lock): global NUM name = t.getName() lock.acquire() #定义锁作用空间的起始位置 NUM+=1 name = t.getName() time.sleep(1) print(name,i,'执行结果',NUM) lock.release() #释放锁 lock = threading.BoundedSemaphore(5) for i in range(30): t = threading.Thread(target=f1,args=(i,lock,)) t.start() print('执行结束')
import threading def func(e,i): print(i) e.wait() #检测当前event是什么状态,如果是红灯,则阻塞,绿灯则放行。默认为红灯。 print(i+100) event = threading.Event() for i in range(10): t = threading.Thread(target=func,args=(event,i)) t.start() event.clear() #将状态设置为红灯。 inp = input('>>> ') if inp.strip() =='b': event.set() #将状态设置为绿灯
import threading def condiction(): ret = False inp = input('>>> ') if inp == 'y': ret = True return ret def func(cond,i): print(i) cond.acquire() cond.wait_for(condiction) #接受函数condition的返回值,Wait until a condition evaluates to True print(i+100) cond.release() c=threading.Condition() for i in range(10): t = threading.Thread(target=func,args=(c,i)) t.start()
上面的例子每次只会释放一个线程。
import threading def run(n): con.acquire() con.wait() #Wait until notified or until a timeout occurs print('run this threading %s'%n) con.release() if __name__ == '__main__': con= threading.Condition() for i in range(10): t = threading.Thread(target=run,args=(i,)) t.start() while True: inp = input('>>> ') if inp =='q': break con.acquire() con.notify(int(inp)) #根据条件唤醒一个到多个线程,如果线程之前没有处于acquire的Lock状态,则报错 #RuntimeError: cannot notify on un-acquired lock con.release()
from threading import Timer def hello(): print('hello') t=Timer(1,hello) #延迟1s后执行 t.start()
既然介绍了多线程和线程锁,那就不得不提及python的GIL,也就是全局解释器锁。在编程语言的世界,python因为GIL的问题广受诟病,因为它在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程。所以我们经常能发现,python中的多线程编程有时候效率还不如单线程,就是因为这个原因。那么,对于这个GIL,一些普遍的问题如下:
-
每种编程语言都有GIL吗?
以python官方Cpython解释器为代表....其他语言好像未见。
-
为什么要有GIL?
作为解释型语言,Python的解释器必须做到既安全又高效。我们都知道多线程编程会遇到的问题。解释器要留意的是避免在不同的线程操作内部共享的数据。同时它还要保证在管理用户线程时总是有最大化的计算资源。那么,不同线程同时访问时,数据的保护机制是怎样的呢?答案是解释器全局锁GIL。GIL对诸如当前线程状态和为垃圾回收而用的堆分配对象这样的东西的访问提供着保护。
-
为什么不能去掉GIL?
首先,在早期的python解释器依赖较多的全局状态,传承下来,使得想要移除当今的GIL变得更加困难。其次,对于程序员而言,仅仅是想要理解它的实现就需要对操作系统设计、多线程编程、C语言、解释器设计和CPython解释器的实现有着非常彻底的理解。
在1999年,针对Python1.5,一个“freethreading”补丁已经尝试移除GIL,用细粒度的锁来代替。然而,GIL的移除给单线程程序的执行速度带来了一定的负面影响。当用单线程执行时,速度大约降低了40%。虽然使用两个线程时在速度上得到了提高,但这个提高并没有随着核数的增加而线性增长。因此这个补丁没有被采纳。
另外,在python的不同解释器实现中,如PyPy就移除了GIL,其执行速度更快(不单单是去除GIL的原因)。然而,我们通常使用的CPython占有着统治地位的使用量,所以,你懂的。
在Python 3.2中实现了一个新的GIL,并且带着一些积极的结果。这是自1992年以来,GIL的一次最主要改变。旧的GIL通过对Python指令进行计数来确定何时放弃GIL。在新的GIL实现中,用一个固定的超时时间来指示当前的线程以放弃这个锁。在当前线程保持这个锁,且当第二个线程请求这个锁的时候,当前线程就会在5ms后被强制释放掉这个锁(这就是说,当前线程每5ms就要检查其是否需要释放这个锁)。当任务是可行的时候,这会使得线程间的切换更加可预测。 -
GIL对我们有什么影响?
最大的影响是我们不能随意使用多线程。要区分任务场景。
在单核cpu情况下对性能的影响可以忽略不计,多线程多进程都差不多。在多核CPU时,多线程效率较低。GIL对单进程和多进程没有影响。 -
在实际使用中有什么好的建议?
建议在IO密集型任务中使用多线程,在计算密集型任务中使用多进程。深入研究python的协程机制,你会有惊喜的。
- queue.Queue :先进先出队列
- queue.LifoQueue :后进先出队列
- queue.PriorityQueue :优先级队列
- queue.deque :双向队列
import queue q = queue.Queue(5)#定义最大元素个数 q.put(11) q.put(22) q.put(33) print(q.get()) print(q.get()) print(q.get())
-
maxsize 队列的最大元素个数,也就是
queue.Queue(5)
中的5。当队列内的元素达到这个值时,后来的元素默认会阻塞,等待队列腾出位置。def __init__(self, maxsize=0):self.maxsize = maxsize self._init(maxsize)
- qsize() 获取当前队列中元素的个数,也就是队列的大小
- empty() 判断当前队列是否为空,返回True或者False
- full() 判断当前队列是否已满,返回True或者False
-
put(self, block=True, timeout=None)
往队列里放一个元素,默认是阻塞和无时间限制的。如果,block设置为False,则不阻塞,这时,如果队列是满的,放不进去,就会弹出异常。如果timeout设置为n秒,则会等待这个秒数后才put,如果put不进去则弹出异常。
- get(self, block=True, timeout=None)
从队列里获取一个元素。参数和put是一样的意思。 -
join() 阻塞进程,直到所有任务完成,需要配合另一个方法task_done。
def join(self):with self.all_tasks_done: while self.unfinished_tasks: self.all_tasks_done.wait()
-
task_done() 表示某个任务完成。每一条get语句后需要一条task_done。
import queue q = queue.Queue(5) q.put(11) q.put(22) print(q.get()) q.task_done() print(q.get()) q.task_done() q.join()
import queue q = queue.LifoQueue() q.put(123) q.put(456) print(q.get())
q = queue.PriorityQueue() q.put((1,"zhang1")) q.put((1,"zhang2")) q.put((1,"zhang3")) q.put((3,"zhang3")) print(q.get())
q = queue.deque() q.append(11) q.append(22) q.appendleft(33) q.appendleft(44) print(q.popleft()) print(q.popleft()) print(q.pop()) print(q.pop()) q.append(55)
生产者消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
#!/use/bin/env python #_*_ coding:utf_8 _*_ import time import queue import threading q = queue.Queue(10) def productor(i): while True: q.put('开工,生产%s个'%i) time.sleep(1) def consumer(k): while True: print('%s,消费%s'%(q.get(),k)) time.sleep(2) for i in range(3): t= threading.Thread(target=productor,args=(i,)) t.start() for k in range(10): v = threading.Thread(target=consumer,args=(k,)) v.start()
#!/usr/bin/env python # -*- coding:utf-8 -*- import queue import threading import time class ThreadPool: def __init__(self, maxsize=5): self.maxsize = maxsize self._q = queue.Queue(maxsize) for i in range(maxsize): self._q.put(threading.Thread) # 【threading.Thread,threading.Thread,threading.Thread,threading.Thread,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) #print(threading.current_thread()) print(threading.currentThread()) #源码中写道currentThread = current_thread,这两个命令效果是一样的。 time.sleep(1) p.add_thread() #在方法执行完之后将新线程加入线程池 for i in range(100): t = pool.get_thread()# threading.Thread类 obj = t(target=task,args=(i,pool,)) obj.start()
这是个比较low的线程池,预先开辟5个线程空间,不考虑实际环境,而且使用过的线程并没有被回收,看一下执行结果便清楚了。
0 <Thread(Thread-1, started 5600)> 1 <Thread(Thread-2, started 5752)> 2 <Thread(Thread-3, started 4460)> 3 <Thread(Thread-4, started 2692)> 4 <Thread(Thread-5, started 2424)> 5 <Thread(Thread-6, started 4628)> 6 <Thread(Thread-7, started 5532)> 7 <Thread(Thread-8, started 4084)> 8 <Thread(Thread-9, started 4472)> 9 <Thread(Thread-10, started 6024)> 10 <Thread(Thread-11, started 1580)> 11 <Thread(Thread-12, started 1960)> 12 <Thread(Thread-13, started 1148)> 13 <Thread(Thread-14, started 4908)> 14 <Thread(Thread-15, started 4468)> 15 <Thread(Thread-16, started 1660)> 16 <Thread(Thread-17, started 5200)> 17 <Thread(Thread-18, started 6108)> 18 <Thread(Thread-19, started 3648)> 19 <Thread(Thread-20, started 3740)> 20 <Thread(Thread-21, started 2628)> 21 <Thread(Thread-22, started 3752)> #提前中断,原循环为100个
下面是武sir提供的一个较好的线程池
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author:Alex Li import queue import threading import contextlib import time StopEvent = object() #设置终止符,可以为None class ThreadPool(object): def __init__(self, max_num, max_task_num = None): if max_task_num: self.q = queue.Queue(max_task_num) #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): #执行完action后判断当前任务状态。 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) 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(300): 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))
进程
python的multiprocess模块提供了Process类,实现进程相关的功能。
由于它是基于fork机制的,因此不被windows平台支持。想要在windows中运行,必须使用if __name__ == '__main__:
的方式,显然这只能用于调试和学习,不能用于实际环境。
下面是一个简单的多进程例子,可以发现Process的用法和Thread的用法几乎一模一样。
from multiprocessing import Process def foo(i): print("This is Process ", i) if __name__ == '__main__': for i in range(5): p = Process(target=foo, args=(i,)) p.start()
进程的数据共享
每个进程都有自己独立的数据空间,不同进程之间通常是不能共享数据,创建一个进程需要非常大的开销。
from multiprocessing import Process list_1 = [] def foo(i): list_1.append(i) print("This is Process ", i," and list_1 is ", list_1) if __name__ == '__main__': for i in range(5): p = Process(target=foo, args=(i,)) p.start() print("The end of list_1:", list_1)
运行上面的代码,可以发现列表list_1在各个进程中只有自己的数据,完全无法共享。
想要进程之间进行资源共享可以使用queues/Array/Manager这三个multiprocess模块提供的类。
使用Array共享数据
from multiprocessing import Process from multiprocessing import Array def Foo(i,temp): temp[0] += 100 for item in temp: print(i,'----->',item) if __name__ == '__main__': temp = Array('i', [11, 22, 33, 44]) for i in range(2): p = Process(target=Foo, args=(i,temp)) p.start()
对于Array数组类,括号内的“i”表示它内部的元素全部是int类型,而不是指字符i,列表内的元素可以预先指定,也可以指定列表长度。概括的来说就是Array类在实例化的时候就必须指定数组的数据类型和数组的大小,类似temp = Array('i', 5)
。对于数据类型有下面的表格对应:
'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
使用Manager共享数据
from multiprocessing import Process,Manager def Foo(i,dic): dic[i] = 100+i print(dic.values()) if __name__ == '__main__': manage = Manager() dic = manage.dict() for i in range(10): p = Process(target=Foo, args=(i,dic)) p.start() p.join()
Manager比Array要好用一点,因为它可以同时保存多种类型的数据格式。
使用queues的Queue类共享数据
import multiprocessing from multiprocessing import Process from multiprocessing import queues def foo(i,arg): arg.put(i) print('The Process is ', i, "and the queue's size is ", arg.qsize()) if __name__ == "__main__": li = queues.Queue(20, ctx=multiprocessing) for i in range(10): p = Process(target=foo, args=(i,li,)) p.start()
这里就有点类似上面的队列了。但是会产生脏数据
既然涉及到进程间资源共享,进程之间肯定会有脏数据的产生,在进程中也设置了进程锁,和线程锁的使用方法一样:
from multiprocessing import Process from multiprocessing import queues from multiprocessing import Array from multiprocessing import RLock, Lock, Event, Condition, Semaphore import multiprocessing import time def foo(i,lis,lc): lc.acquire() lis[0] = lis[0] - 1 time.sleep(1) print('say hi',lis[0]) lc.release() if __name__ == "__main__": # li = [] li = Array('i', 1) li[0] = 10 lock = RLock() for i in range(10): p = Process(target=foo,args=(i,li,lock)) p.start()
同样的,进程也有进程池的概念,不过python中提供了进程池,只需from multiprocessing import Pool
即可
#!/usr/bin/env python # -*- coding:utf-8 -*- from multiprocessing import Pool import time def f1(args): time.sleep(1) print(args) if __name__ == '__main__': p = Pool(5) for i in range(30): p.apply_async(func=f1, args= (i,)) p.close() # 等子进程执行完毕后关闭进程池 # time.sleep(2) # p.terminate() # 立刻关闭进程池 p.join()
进程池内部维护一个进程序列,当使用时,去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有以下几个主要方法:
apply:从进程池里取一个进程并执行
apply_async:apply的异步版本
terminate:立刻关闭进程池
join:主进程等待所有子进程执行完毕。必须在close或terminate之后。
close:等待所有进程结束后,才关闭进程池。
ps:IO密集型操作用线程,CPU密集型操作用进程,协程也适合处理IO密集型操作。
协程
线程和进程的操作是由程序触发系统接口,最后的执行者是系统。而协程的操作则是程序员指定的,人为的实现并发处理。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时。协程,则只使用一个线程,分解一个线程成为多个“微线程”,在一个线程中规定某个代码块的执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO)。
有第三方模块为我们提供了协程,在使用它们之前,需要先安装。这里介绍一下greenlet和gevent。本质上,gevent是对greenlet的高级封装,因此一般用它就行,这是一个相当高效的模块。
3.1 greenlet
from greenlet import greenlet def test1(): print(12) gr2.switch() print(34) gr2.switch() def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch()
实际上,greenlet就是通过switch方法在不同的任务之间进行切换。
3.2 gevent
from gevent import monkey; monkey.patch_all() import gevent import requests def f(url): print('GET: %s' % url) resp = requests.get(url) data = resp.text 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/'), ])
通过joinall将任务f和它的参数进行统一调度,实现单线程中的协程。代码封装层次很高,实际使用只需要了解它的几个主要方法即可。