《深入理解计算机系统(第三版)》
三种基本的构造并发程序的方法:
- 进程。进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信(IPC)机制
- I/O 多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式的调度它们自己的逻辑流。逻辑流被模型化为状态机,数据达到文件描述符后,主程序显式的从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。
- 线程。线程是一个运行在一个单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像I/O多路复用流一样共享同一个虚拟地址空间
I/O多路复用技术的优劣
I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,某些时间会导致流向前推进。一般的思路是将逻辑流模型化为状态机。不严格地说,一个状态机就是一组装填,输入事件和转移,其中转移是将状态和输入事件映射到状态。事件驱动设计的一个优点是,他比基于进程的设计给了程序员更多的对程序行为的控制。
另一个优点是,一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。这使得在流之间共享数据变得很容易。一个作为单个进程运行相关的优点是,你可以利用熟悉的调试工具,例如GDB,来调试你的并发服务器,就相对顺序程序那样。
最后,事件驱动设计常常比基于进程的设计要高效得多,因为他们不需要进程上下文切换来调度新的流。
事件驱动设计一个明显的缺点就是编码复杂。
基于事件的设计另一个重要的缺点是他们不能充分利用多核处理器。
基于线程的并发编程
线程就是运行在进程上下文中的逻辑流。基于线程的逻辑流结合了基于进程和基于I/O多路复用的流的特性。同进程一样,线程有内核自动调度,并且内核通过了一个整数ID来识别线程。统计与I/O多路复用的流一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟空间的所有内容,包括他的代码、数据、堆、共享库和打开的文件。
-
线程间通信方式:共享变量, Queue
-
线程间同步方式:
- Lock,Rlock,
- 条件变量condition:用于复杂的线程间同步
- 信号量:控制线程并发数量
条件变量示例:
import threading
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name='小爱')
self.cond = cond
def run(self):
with self.cond:
self.cond.wait()
print("{} : 在 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 好啊 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 君住长江尾 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 共饮长江水 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 此恨何时已 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 定不负相思 ".format(self.name))
self.cond.notify()
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name='天猫精灵')
self.cond = cond
def run(self):
with self.cond:
print("{} : 小爱同学 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我们来对古诗吧 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我住产长江头 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 日日思君不见君 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 此水几时休 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 只愿君心似我心 ".format(self.name))
self.cond.notify()
self.cond.wait()
if __name__ == '__main__':
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
# 启动顺序很重要
xiaoai.start()
tianmao.start()
天猫精灵 : 小爱同学
小爱 : 在
天猫精灵 : 我们来对古诗吧
小爱 : 好啊
天猫精灵 : 我住产长江头
小爱 : 君住长江尾
天猫精灵 : 日日思君不见君
小爱 : 共饮长江水
天猫精灵 : 此水几时休
小爱 : 此恨何时已
天猫精灵 : 只愿君心似我心
小爱 : 定不负相思
信号量(semaphore)
# Semaphore 是用于控制进入数量的锁
import threading
import time
class HtmlSpider(threading.Thread):
def __init__(self, url, sem):
super().__init__()
self.url = url
self.sem = sem
def run(self):
time.sleep(2)
print('got html text success')
self.sem.release()
class UrlProducer(threading.Thread):
def __init__(self, sem):
super().__init__()
self.sem = sem
def run(self):
for i in range(20):
self.sem.acquire()
html_thread = HtmlSpider('https://baidu.com/{}'.format(i), sem)
html_thread.start()
if __name__ == '__main__':
# 一次允许3个并发
sem = threading.Semaphore(3)
url_producer = UrlProducer(sem)
url_producer.start()
线程池 concurrent
为什么要用线程池:
- 主线程中年可以获取某一个线程的状态或者某一个任务的状态,以及返回值。
- 当一个线程完成的时候,主线程能立即知道
- futures 能让多线程和多进程编码接口一致
import time
from concurrent.futures import as_completed
from concurrent.futures.thread import ThreadPoolExecutor
def get_html(times):
time.sleep(times)
print('get page {} success'.format(times))
return times
excutor = ThreadPoolExecutor(max_workers=2)
# 通过submit函数提交执行的函数到线程池中,submit是非阻塞的,会立即返回
task1=excutor.submit(get_html,(3))
task2=excutor.submit(get_html,(2))
# done 用于判定某个任务是否完成
print(task1.done())
time.sleep(3)
print(task1.done())
# result 是阻塞的方法,得到执行的返回结果
print(task1.result())
结果
False
get page 2 success
get page 3 success
True
3