python并发2:使用asyncio处理并发

asyncio asyncio 是Python3.4 之后引入的标准库的,这个包使用事件循环驱动的协程实现并发。 asyncio 包在引入标准库之前代号 “Tulip”(郁金香),所以在网上搜索资料时,会经常看到这种花的名字。 什么是事件循环? wiki 上说:事件循环是”一种等待程序分配事件或者消息的编程架构“。基本上来说事件循环就是:”当A发生时,执行B"。或者用最简单的例子来解释这一概念就是每个浏览器中都存在的JavaScript事件循环。当你点击了某个东西(“当A发生时”),这一点击动作会发送给JavaScript的事件循环,并检查是否存在注册过的onclick 回调来处理这一点击(执行B)。只要有注册过的回调函数就会伴随点击动作的细节信息被执行。事件循环被认为是一种虚幻是因为它不停的手机事件并通过循环来发如何应对这些事件。 对 Python 来说,用来提供事件循环的 asyncio 被加入标准库中。asyncio 重点解决网络服务中的问题,事件循环在这里将来自套接字(socket)的 I/O 已经准备好读和/或写作为“当A发生时”(通过selectors模块)。除了 GUI 和 I/O,事件循环也经常用于在别的线程或子进程中执行代码,并将事件循环作为调节机制(例如,合作式多任务)。如果你恰好理解 Python 的 GIL,事件循环对于需要释放 GIL 的地方很有用。 线程与协程 我们先看两断代码,分别用 threading 模块和asyncio 包实现的一段代码。 # sinner_thread.py import threading import itertools import time import sys class Signal: # 这个类定义一个可变对象,用于从外部控制线程 go = True def spin(msg, signal): # 这个函数会在单独的线程中运行,signal 参数是前边定义的Signal类的实例 write, flush = sys.stdout.write, sys.stdout.flush for char in itertools.cycle('|/-\\'): # itertools.cycle 函数从指定的序列中反复不断地生成元素 status = char + ' ' + msg write(status) flush() write('\x08' * len(status)) # 使用退格符把光标移回行首 time.sleep(.1) # 每 0.1 秒刷新一次 if not signal.go: # 如果 go属性不是 True,退出循环 break write(' ' * len(status) + '\x08' * len(status)) # 使用空格清除状态消息,把光标移回开头 def slow_function(): # 模拟耗时操作 # 假装等待I/O一段时间 time.sleep(3) # 调用sleep 会阻塞主线程,这么做事为了释放GIL,创建从属线程 return 42 def supervisor(): # 这个函数设置从属线程,显示线程对象,运行耗时计算,最后杀死进程 signal = Signal() spinner = threading.Thread(target=spin, args=('thinking!', signal)) print('spinner object:', spinner) # 显示线程对象 输出 spinner object: <Thread(Thread-1, initial)> spinner.start() # 启动从属进程 result = slow_function() # 运行slow_function 行数,阻塞主线程。同时丛书线程以动画形式旋转指针 signal.go = False spinner.join() # 等待spinner 线程结束 return result def main(): result = supervisor() print('Answer', result) if __name__ == '__main__': main() 执行一下,结果大致是这个样子: ...

2017-06-23 · 8 min · 1522 words

python并发 1:使用 futures 处理并发

作为Python程序员,平时很少使用并发编程,偶尔使用也只需要派生出一批独立的线程,然后放到队列中,批量执行。所以,不夸张的说,虽然我知道线程、进程、并行、并发的概念,但每次使用的时候可能还需要再打开文档回顾一下。 现在这一篇还是 《流畅的python》读书笔记,译者在这里把future 翻译为“期物”,我觉得不太合适,既然future不能找到一个合适的词汇,暂时还是直接使用 future 吧。 concurrent.futures future 是一种对象,表示异步执行的操作。这个概念是 concurrent.futures模块和asyncio包的基础。 concurrent.futures 模块是Python3.2 引入的,对于Python2x 版本,Python2.5 以上的版本可以安装 futures 包来使用这个模块。 HUGOMORE42 从Python3.4起,标准库中有两个为Future的类:concurrent.futures.Future 和 asyncio.Future。这两个类作用相同:两个Future类的实例都表示可能已经完成或未完成的延迟计算。 Future 封装待完成的操作,可放入队列,完成的状态可以查询,得到结果(或抛出异常)后可以获取结果(或异常)。 我们知道,如果程序中包含I/O操作,程序会有很高的延迟,CPU会处于等待状态,这时如果我们不使用并发会浪费很多时间。 示例 我们先举个例子: 下边是有两段代码,主要功能都是从网上下载人口前20的国际的国旗: 第一段代码(flagss.py)是依序下载:下载完一个图片后保存到硬盘,然后请求下一张图片; 第二段代码(flagss_threadpool.py)使用 concurrent.futures 模块,批量下载10张图片。 运行分别运行两段代码3次,结果如下: images.py 的结果如下 $ python flags.py BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 20 flags downloaded in 6.18s $ python flags.py BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 20 flags downloaded in 5.67s $ python flags.py BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 20 flags downloaded in 6.55s 可以看到,依次下载10张图片,平均需要6秒 ...

2017-06-17 · 5 min · 995 words

python协程3:用仿真实验学习协程

前两篇我们已经介绍了python 协程的使用和yield from 的原理,这一篇,我们用一个例子来揭示如何使用协程在单线程中管理并发活动。 什么是离散事件仿真 Wiki上的定义是: 离散事件仿真将系统随时间的变化抽象成一系列的离散时间点上的事件,通过按照事件时间顺序处理事件来演进,是一种事件驱动的仿真世界观。离散事件仿真将系统的变化看做一个事件,因此系统任何的变化都只能是通过处理相应的事件来实现,在两个相邻的事件之间,系统状态维持前一个事件发生后的状态不变。 人话说就是一种把系统建模成一系列事件的仿真系统。在离散事件仿真中,仿真“钟”向前推进的量不是固定的,而是直接推进到下一个事件模型的模拟时间。 假设我们抽象模拟出租车的运营过程,其中一个事件是乘客上车,下一个事件则是乘客下车。不管乘客做了5分钟还是50分钟,一旦下车,仿真钟就会更新,指向此次运营的结束时间。 事件?是不是想到了协程! 协程恰好为实现离散事件仿真提供了合理的抽象。 HUGOMORE42 第一门面向对象的语音 Simula 引入协程这个概念就是为了支持仿真。 Simpy 是一个实现离散事件仿真的Python包,通过一个协程表示离散事件仿真系统的各个进程。 出租车对运营仿真 仿真程序会创建几辆出租车,每辆出租车会拉几个乘客,然后回家。出租车会首先驶离车库,四处徘徊,寻找乘客;拉到乘客后,行程开始;乘客下车后,继续四处徘徊。 徘徊和行程所用的时间使用指数分布生成,我们将时间设为分钟数,以便显示清楚。 完整代码如下:(taxi_sim.py) #! -*- coding: utf-8 -*- import random import collections import queue import argparse DEFAULT_NUMBER_OF_TAXIS = 3 DEFAULT_END_TIME = 180 SEARCH_DURATION = 5 TRIP_DURATION = 20 DEPARTURE_INTERAVAL = 5 # time 是事件发生的仿真时间,proc 是出租车进程实例的编号,action是描述活动的字符串 Event = collections.namedtuple('Event', 'time proc action') # 开始 出租车进程 # 每辆出租车调用一次taxi_process 函数,创建一个生成器对象,表示各辆出租车的运营过程。 def taxi_process(ident, trips, start_time=0): ''' 每次状态变化时向创建事件,把控制权交给仿真器 :param ident: 出租车编号 :param trips: 出租车回家前的行程数量 :param start_time: 离开车库的时间 :return: ''' time = yield Event(start_time, ident, 'leave garage') # 产出的第一个Event for i in range(trips): # 每次行程都会执行一遍这个代码块 # 产出一个Event实例,表示拉到了乘客 协程在这里暂停 等待下一次send() 激活 time = yield Event(time, ident, 'pick up passenger') # 产出一个Event实例,表示乘客下车 协程在这里暂停 等待下一次send() 激活 time = yield Event(time, ident, 'drop off passenger') # 指定的行程数量完成后,for 循环结束,最后产出 'going home' 事件。协程最后一次暂停 yield Event(time, ident, 'going home') # 协程执行到最后 抛出StopIteration 异常 def compute_duration(previous_action): '''使用指数分布计算操作的耗时''' if previous_action in ['leave garage', 'drop off passenger']: # 新状态是四处徘徊 interval = SEARCH_DURATION elif previous_action == 'pick up passenger': # 新状态是开始行程 interval = TRIP_DURATION elif previous_action == 'going home': interval = 1 else: raise ValueError('Unkonw previous_action: %s' % previous_action) return int(random.expovariate(1/interval)) + 1 # 开始仿真 class Simulator: def __init__(self, procs_map): self.events = queue.PriorityQueue() # 带优先级的队列 会按时间正向排序 self.procs = dict(procs_map) # 从获取的procs_map 参数中创建本地副本,为了不修改用户传入的值 def run(self, end_time): ''' 调度并显示事件,直到时间结束 :param end_time: 结束时间 只需要指定一个参数 :return: ''' # 调度各辆出租车的第一个事件 for iden, proc in sorted(self.procs.items()): first_event = next(proc) # 预激协程 并产出一个 Event 对象 self.events.put(first_event) # 把各个事件加到self.events 属性表示的 PriorityQueue对象中 # 此次仿真的主循环 sim_time = 0 # 把 sim_time 归0 while sim_time < end_time: if self.events.empty(): # 事件全部完成后退出循环 print('*** end of event ***') break current_event = self.events.get() # 获取优先级最高(time 属性最小)的事件 sim_time, proc_id, previous_action = current_event # 更新 sim_time print('taxi:', proc_id, proc_id * ' ', current_event) active_proc = self.procs[proc_id] # 从self.procs 字典中获取表示当前活动的出租车协程 next_time = sim_time + compute_duration(previous_action) try: next_event = active_proc.send(next_time) # 把计算得到的时间发送给出租车协程。协程会产出下一个事件,或者抛出 StopIteration except StopIteration: del self.procs[proc_id] # 如果有异常 表示已经退出, 删除这个协程 else: self.events.put(next_event) # 如果没有异常,把next_event 加入到队列 else: # 如果超时 则走到这里 msg = '*** end of simulation time: {} event pendding ***' print(msg.format(self.events.qsize())) def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, seed=None): '''初始化随机生成器,构建过程,运行仿真程序''' if seed is not None: random.seed(seed) # 获取可复现的结果 # 构建taxis 字典。值是三个参数不同的生成器对象。 taxis = {i: taxi_process(i, (i + 1) * 2, i*DEPARTURE_INTERAVAL) for i in range(num_taxis)} sim = Simulator(taxis) sim.run(end_time) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Taxi fleet simulator.') parser.add_argument('-e', '--end-time', type=int, default=DEFAULT_END_TIME, help='simulation end time; default=%s' % DEFAULT_END_TIME) parser.add_argument('-t', '--taxis', type=int, default=DEFAULT_NUMBER_OF_TAXIS, help='number of taxis running; default = %s' % DEFAULT_NUMBER_OF_TAXIS) parser.add_argument('-s', '--seed', type=int, default=None, help='random generator seed (for testing)') args = parser.parse_args() main(args.end_time, args.taxis, args.seed) 运行程序, ...

2017-06-15 · 4 min · 669 words

python协程2:yield from 从入门到精通

上一篇python协程1:yield的使用介绍了: 生成器作为协程使用时的行为和状态 使用装饰器预激协程 调用方如何使用生成器对象的 .throw(…) 和 .close() 方法控制协程 这一篇将介绍: 协程终止时如何返回值 yield新句法的用途和语义 HUGOMORE42 让协程返回值 先看一个例子: 这段代码会返回最终均值的结果,每次激活协程时不会产出移动平均值,而是最后一次返回。 #! -*- coding: utf-8 -*- from collections import namedtuple Result = namedtuple('Result', 'count average') def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break # 为了返回值,协程必须正常终止;这里是退出条件 total += term count += 1 average = total/count # 返回一个namedtuple,包含count和average两个字段。在python3.3前,如果生成器返回值,会报错 return Result(count, average) 我们调用这段代码,结果如下 >>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(20) # 并没有返回值 >>> coro_avg.send(30) >>> coro_avg.send(40) >>> coro_avg.send(None) # 发送None终止循环,导致协程结束。生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回值。 Traceback (most recent call last): ... StopIteration: Result(count=3, average=30) return 表达式的值会传给调用方,赋值给StopIteration 异常的一个属性。这样做虽然看着别扭,但为了保留生成器对象耗尽时抛出StopIteration异常的行为,也可以理解。 ...

2017-06-14 · 5 min · 858 words

python协程1:协程 10分钟入门

最近找到一本python好书《流畅的python》,是到现在为止看到的对python高级特性讲述最详细的一本。 看了协程一章,做个读书笔记,加深印象。 协程定义 协程的底层架构是在pep342 中定义,并在python2.5 实现的。 HUGOMORE42 python2.5 中,yield关键字可以在表达式中使用,而且生成器API中增加了 .send(value)方法。生成器可以使用.send(…)方法发送数据,发送的数据会成为生成器函数中yield表达式的值。 协程是指一个过程,这个过程与调用方协作,产出有调用方提供的值。因此,生成器可以作为协程使用。 除了 .send(…)方法,pep342 和添加了 .throw(…)(让调用方抛出异常,在生成器中处理)和.close()(终止生成器)方法。 python3.3后,pep380对生成器函数做了两处改动: 生成器可以返回一个值;以前,如果生成器中给return语句提供值,会抛出SyntaxError异常。 引入yield from 语法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去之前把生成器的工作委托给子生成器所需的大量模板代码。 协程生成器的基本行为 首先说明一下,协程有四个状态,可以使用inspect.getgeneratorstate(…)函数确定: GEN_CREATED # 等待开始执行 GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态) GEN_SUSPENDED # 在yield表达式处暂停 GEN_CLOSED # 执行结束 #! -*- coding: utf-8 -*- import inspect # 协程使用生成器函数定义:定义体中有yield关键字。 def simple_coroutine(): print('-> coroutine started') # yield 在表达式中使用;如果协程只需要从客户那里接收数据,yield关键字右边不需要加表达式(yield默认返回None) x = yield print('-> coroutine received:', x) my_coro = simple_coroutine() my_coro # 和创建生成器的方式一样,调用函数得到生成器对象。 # 协程处于 GEN_CREATED (等待开始状态) print(inspect.getgeneratorstate(my_coro)) my_coro.send(None) # 首先要调用next()函数,因为生成器还没有启动,没有在yield语句处暂停,所以开始无法发送数据 # 发送 None 可以达到相同的效果 my_coro.send(None) next(my_coro) # 此时协程处于 GEN_SUSPENDED (在yield表达式处暂停) print(inspect.getgeneratorstate(my_coro)) # 调用这个方法后,协程定义体中的yield表达式会计算出42;现在协程会恢复,一直运行到下一个yield表达式,或者终止。 my_coro.send(42) print(inspect.getgeneratorstate(my_coro)) 运行上述代码,输出结果如下 ...

2017-06-12 · 4 min · 644 words

CSS入门指南-1:工作原理

这是CSS设计指南的读书笔记,用于加深学习效果。 最近想做一个小程序,前端是必修课,那就从css开始吧。 css 工作原理 每个html元素都有一组样式属性,可以通过css来设定。当html元素的同一个样式属性有多种样式值的时候,css就要靠层叠机智来决定最终应用哪种样式。 HUGOMORE42 css规则 规则实际上是一条完整的css指令,规则声明了要修改的元素和要应用给改元素的样式。 为文档添加样式的三种方法: 写在元素标签里(也叫行内样式,只能影响它所在的标签,会覆盖嵌入样式和链接样式) 写在<style> 标签里(也就嵌入样式,应用范围仅限于当前页面,页面样式会覆盖外部样式表中的样式,但会被行内样式覆盖) 写在单独css样式表中(也叫链接样式,样式表是一个扩展名为.css 的文件,可以在任意多个HTML页面链接同一个样式表文件。链接样式的作用范围是整个网站) 除了这三种为页面添加样式的方法,还有一种在样式表中链接其他样式表的方法,使用@import 指令:例如 @import url(css/styles.css) @import 指令必须出现在样式表中其他样式之前,否则@吹灭;@import引用的样式表不会被加载。 对这个基本的结构有三种方法可以进行扩展 **第一种方法:**多个声明包含在一条规则里。 p {color: red; font-size: 12px; font-weight: bold;} **第二种方法:**多个选择器组合在一起。例如:如果想让<h1>、<h2>和<h3>的文本都变成蓝色粗体可以这么写: h1 {color: blue; font-weight: bold;} h2 {color: blue; font-weight: bold;} h3 {color: blue; font-weight: bold;} 也可以这么写: h1, h2, h3 {color: blue; font-weight: bold;} 分组选择符以逗号作为分隔符 第三种方法: 多条规则应用给一个选择符。 例如,写完上边的规则,还想把h3变成斜体,那么可以再为h3单独写一条规则: h1, h2, h3 {color: blue; font-weight: bold;} h3 {font-style: italic;} 选择特定元素的选择符 用于选择特定元素的操作符有三种 上下文选择符。基于祖先或者同胞元素选择一个元素。 ID和类选择符。基于id和class属性的值选择元素。 属性选择符。基于属性的有无和特征选择元素。 上下文选择符 比如我们想给article中的段落设置不同的字号,可以使用上下文选择符来解决。 上下文选择符的格式如下: 标签1 标签2 {声明} 其中标签2 是我们要选择的目标,而且只有在 标签1是其祖先元素的情况下才会被选中。 上下文选择符,叫后代组合式选择符,就是一组以空格分隔的标签名。用于选择作为特定祖先元素后代的标签。 article p {font-weight: bold;} 上边例子中,只有article后代的p元素才会应用后边的样式。 上下文选择符以空格作为分隔符 特殊的上下文选择符 子选择符 > 格式如下: 标签1 > 标签2 标签1 必须是 标签2 的父元素,不能是其它的祖先元素。 ...

2017-05-27 · 3 min · 451 words

Elasticsearch 安装和使用

安装使用 Elasticsearch 两种方法: 方法1 手动安装 Elasticsearch 安装到ubuntu Elasticsearch与Logstash需要Java作为运行环境 安装Java 8 将甲骨文Java PPA添加至apt: sudo add-apt-repository -y ppa:webupd8team/java 更新apt软件包数据库: sudo apt-get update HUGOMORE42 安装甲骨文Java 8的最新稳定版本,命令如下(在弹出的许可协议中点击接受): sudo apt-get -y install oracle-java8-installer 安装Elasticsearch 方法1 通过添加Elastic的软件包源列表利用软件包管理器安装Elasticsearch。 运行以下命令以将Elasticsearch公共GPG密钥导入apt: wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - 接下来,创建Elasticsearch源列表: echo "deb http://packages.elastic.co/elasticsearch/${ELASTICSEARCH_VERSION}/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elk.list 更新apt软件包数据库: sudo apt-get update 安装Elasticsearch sudo apt-get -y install elasticsearch Elasticsearch已经安装完成。下面编辑其配置文件: sudo vi /etc/elasticsearch/elasticsearch.yml 限制来自外部的Elasticsearch实例访问活动(端口9200),找到指定network.host的一行,取消其注释并将其值替换为“localhost”: elasticsearch.yml excerpt (updated) network.host: localhost 启动elasticsearch ...

2017-05-18 · 2 min · 421 words