【第九章】:线程、进程和协程
一、线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
注:cpu内一个核数只能同时运行一个线程,所以多核cpu同时可以运行多个线程;但是在Python中,即使是多核cpu,同时运行的线程也只有一个,Python语言设计之初就不支持多核,所以在Python程序中,启用越多的线程,程序不一定运行的就很快,因为cpu要进行大量的上下文切换,反而消耗时间;GIL全局解释锁保障线程的上下文关系,保障当前只有一个线程在运行,与lock数据加锁无关
1、threading模块
线程创建有2种方式:如下
直接调用
import threading,time def run(n): print("test...",n) time.sleep(2) if __name__ == '__main__': t1 = threading.Thread(target=run,args=("t1",)) t2 = threading.Thread(target=run,args=("t2",)) # 两个同时执行,然后等待两秒程序结束 t1.start() t2.start() # 程序输出 # test... t1 # test... t2
继承式调用
import threading,time class MyThread(threading.Thread): def __init__(self,num): # threading.Thread.__init__(self) super(MyThread,self).__init__() self.num =num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(2) if __name__ == '__main__': # 两个同时执行,然后等待两秒程序结束 t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start() # 程序输出 # running on number:1 # running on number:2
2、join
join等待线程执行完后,其他线程再继续执行(串行)
import threading,time def run(n,sleep_time): print("test...",n) time.sleep(sleep_time) print("test...done", n) if __name__ == '__main__': t1 = threading.Thread(target=run,args=("t1",2)) t2 = threading.Thread(target=run,args=("t2",3)) # 两个同时执行,然后等待t1执行完成后,主线程和子线程再开始执行 t1.start() t2.start() t1.join() # 等待t1 print("main thread") # 程序输出 # test... t1 # test... t2 # test...done t1 # main thread # test...done t2
3、Daemon
Daemon守护进程,主程序执行完毕时,守护线程会同时退出,不管是否执行完任务
import threading,time def run(n): print('[%s]------running----\n' % n) time.sleep(2) print('--done--') def main(): for i in range(5): t = threading.Thread(target=run, args=[i, ]) t.start() t.join(1) print('starting thread', t.getName()) m = threading.Thread(target=main, args=[]) m.setDaemon(True) # 将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时, # m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务 m.start() m.join(timeout=2) print("---main thread done----") # 程序输出 # [0]------running---- # starting thread Thread-2 # [1]------running---- # --done-- # ---main thread done----
4、Mutex 线程锁(互斥锁)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
import time import threading def addNum(): global num # 在每个线程中都获取这个全局变量 print('--get num:', num) time.sleep(1) num -= 1 # 对此公共变量进行-1操作 num = 100 # 设定一个共享变量 thread_list = [] for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: # 等待所有线程执行完毕 t.join() print('final num:', num)
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
对程序进行加锁
import time import threading def addNum(): global num # 在每个线程中都获取这个全局变量 print('--get num:', num) time.sleep(1) lock.acquire() # 修改数据前加锁 num -= 1 # 对此公共变量进行-1操作 lock.release() # 修改后释放 num = 100 # 设定一个共享变量 thread_list = [] lock = threading.Lock() # 生成全局锁 for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: # 等待所有线程执行完毕 t.join() print('final num:', num)
机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下
5、RLock 递归锁
当一个大锁中还要再包含子锁的时候,如果再用threading.Lock的话,程序锁和钥匙会出现对不上的情况,这时候就需要用到递归锁
import threading, time def run1(): print("grab the first part data") lock.acquire() global num num += 1 lock.release() return num def run2(): print("grab the second part data") lock.acquire() global num2 num2 += 1 lock.release() return num2 def run3(): lock.acquire() res = run1() print('--------between run1 and run2-----') res2 = run2() lock.release() print(res, res2) if __name__ == '__main__': num, num2 = 0, 0 lock = threading.RLock() for i in range(10): t = threading.Thread(target=run3) t.start() while threading.active_count() != 1: print(threading.active_count()) else: print('----all threads done---') print(num, num2)
递归锁实现原理其实很简单,只是把每次锁和钥匙对应记录起来,就不是出现锁死的情况
6、Semaphore 信号量
Mutex 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
import threading, time def run(n): semaphore.acquire() time.sleep(1) print("run the thread: %s\n" % n) semaphore.release() if __name__ == '__main__': num = 0 semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行 for i in range(20): t = threading.Thread(target=run, args=(i,)) t.start() while threading.active_count() != 1: pass # print threading.active_count() else: print('----all threads done---') print(num)
实现效果,每次同时执行5个线程
7、Event
通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
import threading,time def light(): count = 0 while True: if count < 10: #红灯 print("\033[41;1m红灯\033[0m",10-count) elif count >= 10 and count < 30: #绿灯 event.set() # 设置标志位 print("\033[42;1m绿灯\033[0m",30-count) else: event.clear() #把标志位清空 count = 0 time.sleep(1) count +=1 def car(n): while True: if event.is_set(): print("\033[32;0m[%s]在路上飞奔.....\033[0m"%n) else: print("\033[31;0m[%s]等红灯等的花都谢了.....\033[0m" % n) time.sleep(1) if __name__ == "__main__": event = threading.Event() light = threading.Thread(target=light) light.start() car = threading.Thread(target=car,args=("tesla",)) car.start()
8、queue
实现解耦、队列;先进先出,后进后出(当get不到数据时,会一直卡着等待数据)
import queue q = queue.Queue() for i in range(10): q.put(i) for t in range(10): print(q.get()) # 0 # 1 # 2 # 3 # 4 # 5 # 6 # 7 # 8 # 9
queue is especially useful in threaded programming when information must be exchanged safely between multiple threads. class queue.Queue(maxsize=0) #先入先出 class queue.LifoQueue(maxsize=0) #last in fisrt out class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列 Constructor for a priority queue. maxsize is an integer that sets the upperbound limit on the number of items that can be placed in the queue. Insertion will block once this size has been reached, until queue items are consumed. If maxsize is less than or equal to zero, the queue size is infinite. The lowest valued entries are retrieved first (the lowest valued entry is the one returned by sorted(list(entries))[0]). A typical pattern for entries is a tuple in the form: (priority_number, data). exception queue.Empty Exception raised when non-blocking get() (or get_nowait()) is called on a Queue object which is empty. exception queue.Full Exception raised when non-blocking put() (or put_nowait()) is called on a Queue object which is full. Queue.qsize() Queue.empty() #return True if empty Queue.full() # return True if full Queue.put(item, block=True, timeout=None) Put item into the queue. If optional args block is true and timeout is None (the default), block if necessary until a free slot is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Full exception if no free slot was available within that time. Otherwise (block is false), put an item on the queue if a free slot is immediately available, else raise the Full exception (timeout is ignored in that case). Queue.put_nowait(item) Equivalent to put(item, False). Queue.get(block=True, timeout=None) Remove and return an item from the queue. If optional args block is true and timeout is None (the default), block if necessary until an item is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Empty exception if no item was available within that time. Otherwise (block is false), return an item if one is immediately available, else raise the Empty exception (timeout is ignored in that case). Queue.get_nowait() Equivalent to get(False). Two methods are offered to support tracking whether enqueued tasks have been fully processed by daemon consumer threads. Queue.task_done() Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing on the task is complete. If a join() is currently blocking, it will resume when all items have been processed (meaning that a task_done() call was received for every item that had been put() into the queue). Raises a ValueError if called more times than there were items placed in the queue. Queue.join() block直到queue被消费完毕
python多线程,不适合cpu密集操作型的任务,适合io操作密集型的任何;
补充:
获取线程ID的方式(python的threading因为封装的太好, 很多本源的东西在threading对象里是拿不到的. 首先需要说明的是python threading的name跟ident,这些看起来是线程名字,线程id其实只是个标识,注意是标识而已. 简单过了下threading创建对象及启动线程的代码,发现ident跟pstree查到的线程id是两码事. )
import time import threading import ctypes print(threading.currentThread()) print(threading.currentThread().ident) print(ctypes.CDLL('libc.so.6').syscall(186)) # <_MainThread(MainThread, started 139645800499008)> # 139645800499008 # 74856
二、进程
要以一个整体的形式暴露给操作系统管理,里面包含了对各种资源的调用,内存的管理,网络接口的调用等;对各种资源的管理集合,就可以称为进程
1、multiprocessing模块(多进程)
multiprocessing与threading使用方法类似,下面创建个程序看看
#多进程 import multiprocessing,time import threading def thread_run(): print("thread id ",threading.get_ident()) def run(name): time.sleep(1) print("process----",name) t = threading.Thread(target=thread_run,) t.start() if __name__ == "__main__": for i in range(10): p = multiprocessing.Process(target=run,args=("lzl",)) p.start()
做一个程序,对比下主进程和子进程的id号以及关系
#多进程id from multiprocessing import Process import os def info(title): print(title) print('module name:', __name__) print('parent process:', os.getppid()) # 父进程id print('process id:', os.getpid()) # 子进程id def f(name): info('\033[31;1mfunction f\033[0m') print('hello', name) if __name__ == '__main__': info('\033[32;1mmain process line\033[0m') p = Process(target=f, args=('bob',)) p.start() p.join() # 输出 # main process line # module name: __main__ # parent process: 7668 # process id: 7496 # function f # module name: __mp_main__ # parent process: 7496 # process id: 7188 # hello bob
2、进程间通信
不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以用以下方法:
① Queue
Queue使用方法跟threading里的queue差不多
#Queue 进程间通信 import multiprocessing def f(q): q.put([42,None,"hello"]) if __name__ == "__main__": q = multiprocessing.Queue() p = multiprocessing.Process(target=f,args=(q,)) p.start() print(q.get()) p.join() #输出 [42, None, 'hello']
② Pipe
#Pipe 进程间通信 import multiprocessing def f(conn): conn.send("hello from child") conn.close() pass if __name__ == "__main__": parent_conn,child_conn = multiprocessing.Pipe() p = multiprocessing.Process(target=f,args=(child_conn,)) p.start() print(parent_conn.recv()) p.join() #输出 hello from child
Queue和Pipe实际上实现的是进程间的数据传递,并没有在进程间共享数据,如果要共享数据的话,得用到下面的Manager
③ Manager
#Manager 进程间共享数据 import multiprocessing import os def f(d,l): d["1"] = 1 d["2"] = 2 l.append(os.getpid()) if __name__ == "__main__": manager = multiprocessing.Manager() d = manager.dict() #创建一个字典,进程间可以共享数据 l = manager.list() p_list = [] for i in range(10): p = multiprocessing.Process(target=f,args=(d,l,)) p.start() p_list.append(p) for t in p_list: t.join() print(d) print(l) #输出 # {'2': 2, '1': 1} # [516, 3628, 6076, 5020, 5396, 4752, 6072, 3608, 3704, 5124]
④ 进程同步
Without using the lock output from the different processes is liable to get all mixed up
from multiprocessing import Process, Lock def f(l, i): l.acquire() print('hello world', i) l.release() if __name__ == '__main__': lock = Lock() for num in range(10): Process(target=f, args=(lock, num)).start()
3、进程池
进程创建子进程的过程,子进程克隆了一遍父进程里的数据,如果父进程占用空间特别大,子进程启动过多就会导致系统空间不够用,所以引出了进程池的概念;进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
- apply 同步执行(串行)
- apply_async 异步执行(并行)
# 进程池 from multiprocessing import Process, Pool import time,os def Foo(i): time.sleep(2) print("in process",os.getpid()) return i + 100 def Bar(arg): print('-->exec done:',arg,os.getpid()) if __name__ == "__main__": pool = Pool(5) #允许进程池同时放入5个进程 print("主进程:",os.getpid()) for i in range(10): #pool.apply_async(func=Foo, args=(i,), callback=Bar) #callback回调 执行完func后再执行callback 用主程序执行 pool.apply(func=Foo, args=(i,)) pool.close() pool.join() # 进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。 # 主进程: 5896 # in process 1520 # in process 5596 # -->exec done: 102 5896 # in process 3384 # -->exec done: 100 5896 # -->exec done: 101 5896 # in process 6112 # -->exec done: 103 5896 # in process 1472 # -->exec done: 104 5896 # in process 1520 # in process 5596 # -->exec done: 106 5896 # -->exec done: 105 5896 # in process 3384 # -->exec done: 107 5896 # in process 6112 # -->exec done: 108 5896 # in process 1472 # -->exec done: 109 5896
最近有更新-》》http://www.cnblogs.com/lianzhilei/p/7009826.html
三、协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
好处:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
1、实例
使用yield实现协程操作例子 ,单线程下实现多并发的效果
# yield实现协程 def consumer(name): print("------>starting eating baozi..") while True: new_baozi = yield print("[%s] is eating baozi %s"%(name,new_baozi)) def producer(): n = 0 while n < 5 : n +=1 con.send(n) #唤醒yield并且传值 con2.send(n) print("\033[32;1m[producer]\033[0m is making baozi %s" % n) if __name__ == "__main__": con = consumer("c1") #生成生成器 con2 = consumer("c2") con.__next__() #唤醒yield con2.__next__() producer() # 输出 # ------>starting eating baozi.. # ------>starting eating baozi.. # [c1] is eating baozi 1 # [c2] is eating baozi 1 # [producer] is making baozi 1 # [c1] is eating baozi 2 # [c2] is eating baozi 2 # [producer] is making baozi 2 # [c1] is eating baozi 3 # [c2] is eating baozi 3 # [producer] is making baozi 3 # [c1] is eating baozi 4 # [c2] is eating baozi 4 # [producer] is making baozi 4 # [c1] is eating baozi 5 # [c2] is eating baozi 5 # [producer] is making baozi 5
协程之所以可以出来高并发,原理遇到I/O操作就切换,只剩下CPU操作(CPU操作非常快)
2、greenlet
greenlet封装好的协程,利用.swith对协程操作进行手动切换
#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian from greenlet import greenlet def test1(): print("in test1 12") gr2.switch() print("in test1 34") gr2.switch() def test2(): print("in test2 56") gr1.switch() print("in test2 78") gr1 = greenlet(test1) #启动一个协程 gr2 = greenlet(test2) gr1.switch() #切换操作 类似于yeild里的next() # 输出 # in test1 12 # in test2 56 # in test1 34 # in test2 78
3、gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
#!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian import gevent def foo(): print("runing in foo") gevent.sleep(2) print("context swith to foo again") def bar(): print("context to bar") gevent.sleep(1) print("context to swith bar to bar") gevent.joinall([ #启动协程 gevent.spawn(foo), gevent.spawn(bar), ]) #输出 # runing in foo # context to bar # context to swith bar to bar # context swith to foo again
还原生成环境下,利用gevent做同步与异步的性能对比
# 同步异步性能对比 import urllib.request import gevent,time from gevent import monkey monkey.patch_all() #monkey.patch_all()执行后可以识别urllib里面的I/0操作 def f(url): print("GET: %s"%url) resp = urllib.request.urlopen(url) data = resp.read() print("%d bytes received from %s"%(len(data),url)) # 同步开销 urls = [ 'https://www.python.org/', 'https://www.yahoo.com/', 'https://github.com/', ] time_start = time.time() for url in urls: f(url) print("同步cost time",time.time()-time_start) # 异步开销 async_time_start = time.time() gevent.joinall([ gevent.spawn(f,'https://www.python.org/'), gevent.spawn(f,'https://www.yahoo.com/'), gevent.spawn(f,'https://github.com/') ]) print("异步cost time",time.time()-async_time_start) # 输出 # GET: https://www.python.org/ # 47446 bytes received from https://www.python.org/ # GET: https://www.yahoo.com/ # 431619 bytes received from https://www.yahoo.com/ # GET: https://github.com/ # 25478 bytes received from https://github.com/ # 同步cost time 4.225241661071777 # GET: https://www.python.org/ # GET: https://www.yahoo.com/ # GET: https://github.com/ # 25478 bytes received from https://github.com/ # 461925 bytes received from https://www.yahoo.com/ # 47446 bytes received from https://www.python.org/ # 异步cost time 2.5521459579467773
由上面程序可知,同步开销时间为4秒,异步开销为2.5秒,大大节省了开销,这就是协程的魅力;monkey.patch_all()使gevent能识别到urllib中的I/O操作
通过gevent实现单线程下的多socket并发
import sys import socket import time import gevent from gevent import socket,monkey monkey.patch_all() def server(port): s = socket.socket() s.bind(('0.0.0.0', port)) s.listen(5) while True: conn, addr = s.accept() gevent.spawn(handle_request, conn) def handle_request(conn): try: while True: data = conn.recv(1024) print("recv:", data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) except Exception as ex: print(ex) finally: conn.close() if __name__ == '__main__': server(8001)
import socket HOST = 'localhost' # The remote host PORT = 8001 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) while True: msg = bytes(input(">>:"),encoding="utf8") s.sendall(msg) data = s.recv(1024) #print(data) print('Received', repr(data)) s.close()
我们在这里留一个问题,现在已经明白了异步的优点,当遇到I/0操作时会进行切换操作,那么程序是如何知道之前的I/O执行完毕再切换回来的呢?!
四、论事件驱动与异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
上面的几种方式,各有千秋,
第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式
看图说话讲事件驱动模型
在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的
方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
1. 有一个事件(消息)队列;
2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了
在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。
在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。
在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。
当我们面对如下的环境时,事件驱动模型通常是一个好的选择:
- 程序中有许多任务,而且…
- 任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
- 在等待事件到来时,某些任务会阻塞。
当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。
网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。
总结:异步IO涉及到了事件驱动模型,进程中维护一个消息队列,当客户端又请求时,就会把请求添加到消息队列中,线程从消息队列中轮询取要处理的请求,遇到I/O阻塞时(操作系统处理调用I/O接口处理,与程序无关),则进行上下文切换,处理其他请求,当I/O操作完成时,调用回调函数,告诉线程处理完成,然后再切换回来,处理完成后返回给客户端 Nginx能处理高并发就是用的这个原理
Select\Poll\Epoll异步IO
Select\Poll\Epoll详解: http://www.cnblogs.com/lianzhilei/p/5843277.html
IO多路复用、异步IO详解: http://www.cnblogs.com/lianzhilei/p/5955526.html
进程与线程详细对比-》 点击