在传统的系统编程中,并发操作采用事件驱动的多路复用模型来实现,而并行操作采用多线程或多进程模型,但在现在(Python/JS/C#)的编程中,在传统的并行操作层面上再实施一层封装,即采用任务(task)方式来管理并发和并行任务的处理,任务的方式则通过几个API实施复用,屏蔽底层实现的复杂性。
多路复用采用的是事件循环机制实现,程序中用于处理各种事件,通常程序都处于阻塞状态直到事件到达,事件到达后唤醒事件处理函数用于处理该事件,当事件处理完成后程序又处于阻塞状态,事件循环机制常用于事件、消息、信号的处理程序中,由于多个事件复用于同一线程中,这样的编程模型我们又称之为多路复用模型。
一个场景的多任务处理流程如下图,右侧各种任务处理函数则通过一个任务执行器实施统一管理。
异步任务
并发任务则通过异步包asyncio库来实现任务的并行处理,该库通过yield from来转移程序的控制权即让自身函数进入阻塞状态,示例代码如下,每个包含yield from的函数均需配上装饰器asyncio.coroutine,该函数内不能有其他阻塞操作,可实现任意任务操作。
import asyncioimport itertoolsimport sys@asyncio.coroutinedef spin(msg): write, flush = sys.stdout.write, sys.stdout.flush for char in itertools.cycle('|/-\\'): status = char + ' ' +msg write(status) flush() write('\x08' * len(status)) try: yield from asyncio.sleep(10) except asyncio.CancelledError: break write(' ' * len(status) + '\x08' * len(status))@asyncio.coroutinedef slow_function(): # pretend waiting a long time for IO yield from asyncio.sleep(3) return 42 pass@asyncio.coroutinedef supervisor(): spinner = asyncio.async(spin("thinking!")) print("spinner object:", spinner) result = yield from slow_function() spinner.cancel() return resultdef main(): loop = asyncio.get_event_loop() result = loop.run_until_complete(supervisor()) loop.close() print('Answer:', result)if __name__ == "__main__":main()
如何将更多的并发任务提供到同一线程中来呢,多路复用则采用gather方法来实现:
tasks = [item.run() for item in self.items] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(*tasks)) loop.close()
多路复用的程序框架就是采用主线程事件循环方式,其余各类事件均可加入到循环中来,如果有事件到达就会唤醒对应的处理函数,这里就是yield from 阻塞后继续唤醒又从该函数点继续往下执行。
并发与并行任务
并发的方式有2种:线程级并发与进程级并发,在python中线程中因涉及到GIL全局锁的存在,线程级并行本质上也是并发任务,但python不同的是无论线程或者进程级并行两者在代码逻辑上处理一致,API函数也保持一致,具有很高的友好度。线程级并行示例代码如下:
from concurrent import futuresimport timeimport randomfrom datetime import datetimeimport threadingMAX_WORKERS = 10def download_one(n:int): tid = threading.current_thread().ident t = datetime.now() print("time:%s, tid:%s, sleep n:%s" % (t, tid, n)) time.sleep(n) return tid passdef download_many(): workers = min(MAX_WORKERS, 100) args = [random.randint(1,10) for i in range(workers)] print("args:" % (args)) with futures.ThreadPoolExecutor(workers) as executor: res = executor.map(download_one, args) return len(res)def download_many2(): workers = min(MAX_WORKERS, 100) args = [random.randint(1,10) for i in range(workers)] print("args:" % (args)) with futures.ThreadPoolExecutor(workers) as executor: to_do = [] for cc in args: future = executor.submit(download_one, cc) to_do.append(future) rets = [] for future in futures.as_completed(to_do): res = future.result() msg = '{} result: {!r}'.format(future, res) rets.append(res) print(msg) return len(rets)if __name__ == "__main__":download_many2()
进程级并行则只需要将ThreadPoolExecutor修改为ProcessPoolExectuor即可,具体修改如下:
futures.ProcessPoolExecutor(workers)
其余代码无需修改,这样就屏蔽了任务并行的差异性。Python中还有线程库(threading)和进程库(multiprocessing),而ThreadPoolExecutor和ProcessPoolExectuor都是在它们上面实施了一层封装,使之用起来更加简单方便。