使用asyncio包处理并发
线程和协程对比
线程显示文本旋转
import threading
import itertools
import time
import sys
class Signal:
go = True # 控制线程
def spin(msg, signal):
write, flush = sys.stdout.write, sys.stdout.flush
for char in itertools.cycle('|/-\\'): # 无限循环
status = char + ' ' + msg
write(status)
flush()
write('\x08' * len(status)) # 使用退格符把光标移回去
time.sleep(.1)
if not signal.go: # 退出条件
break
write(' ' * len(status) + '\x08' * len(status)) # 清除打印出来的消息,并把光标移回开头
def slow_function():
time.sleep(3)
return 42
def supervisor():
signal = Signal()
spinner = threading.Thread(target=spin, args=('thinking!', signal)) # 设置从属线程对象
print('spinner object:', spinner)
spinner.start() # 启动从属线程
result = slow_function() # 阻塞主线程,同时从属线程显示旋转指针
signal.go = False
spinner.join()
return result
def main():
result =supervisor()
print('Answer:', result)
if __name__ == '__main__':
main()
协程显示文本旋转
import asyncio
import itertools
async def spin(msg):
for char in itertools.cycle('|/-\\'):
status = char + ' ' + msg
print(status, flush=True, end='\r')
try:
await asyncio.sleep(.1)
except asyncio.CancelledError: # 取消Task对象会抛出异常
break
print(' ' * len(status), end='\r')
async def slow_function():
await asyncio.sleep(3)
return 42
async def supervisor():
spinner = asyncio.create_task(spin('thinking!')) # 获取Task对象
print('spinner object:', spinner)
result = await slow_function()
spinner.cancel() # 取消Task对象
return result
def main():
result = asyncio.run(supervisor())
print('Answer:', result)
if __name__ == '__main__':
main()
- Task驱动协程,Thread对象调用可调用对象
使用asyncio和aiohttp包下载
import time
import os
import sys
import asyncio
import aiohttp
POP20_CC = ('CN IN US ID BR PK NG BD RU JP MX PH VN ET EG DE IR TR CD FR').split()
BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = 'downloads/'
def save_flag(img, filename):
path = os.path.join(DEST_DIR, filename)
with open(path, 'wb') as fp:
fp.write(img)
def show(text):
print(text, end=' ')
sys.stdout.flush()
async def get_flag(session, cc):
url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
async with session.get(url) as resp: # aiohttp阻塞操作,通过协程实现
return await resp.read() # 单独的异步操作
async def download_one(session, cc):
image = await get_flag(session, cc)
show(cc)
save_flag(image, cc.lower() + '.gif')
return cc
async def download_many(cc_list):
async with aiohttp.ClientSession() as session:
res = await asyncio.gather(*[asyncio.create_task(download_one(session, cc)) for cc in sorted(cc_list)])
return len(res)
def main():
t0 = time.time()
count = asyncio.run(download_many(POP20_CC))
elapsed = time.time() - t0
msg = '\n{} flags downloaded in {:.2f}s'
print(msg.format(count, elapsed))
if __name__ == '__main__':
main()
使用as_completed函数
import asyncio
import collections
from enum import Enum
import aiohttp
from aiohttp import web
import tqdm
from flag2_common import main, HTTPStatus, Result, save_flag
DEFAULT_CONCUR_REQ = 5
MAX_CONCUR_REQ = 1000
class FetchError(Exception):
def __init__(self, country_code):
self.country_code = country_code
async def get_flag(session,base_url, cc):
url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower())
async with session.get(url) as resp:
if resp.status == 200:
return await resp.read()
elif resp.status == 404:
raise web.HTTPNotFound()
else:
raise aiohttp.HttpProcessingError(code=resp.status, message=resp.reason, hreaders=resp.hreaders)
async def download_one(session, cc, base_url, semaphore, verbose):
try:
async with semaphore:
image = await get_flag(session, base_url, cc)
except web.HTTPNotFound:
status = HTTPStatus.not_found
msg = 'not found'
except Exception as exc:
raise FetchError(cc) from exc
else:
save_flag(image, cc.lower() + '.gif')
status = HTTPStatus.ok
msg = 'ok'
if verbose and msg:
print(cc, msg)
return Result(status, cc)
async def downloader_coro(cc_list, base_url, verbose, concur_req):
counter = collections.Counter()
semaphore = asyncio.Semaphore(concur_req)
async with aiohttp.ClientSession() as session:
to_do = [download_one(session, cc, base_url, semaphore, verbose) for cc in sorted(cc_list)] # 协程对象列表
to_do_iter = asyncio.as_completed(to_do)
if not verbose:
to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list))
for future in to_do_iter:
try:
res = await future # 获取期物产生的结果
except FetchError as exc:
country_code = exc.country_code
try:
error_msg = exc.__cause__.args[0]
except IndexError:
error_msg = exc.__cause__.__class__.__name__
if verbose and error_msg:
msg = '*** Error for {}: {}'
print(msg.format(country_code, error_msg))
status = HTTPStatus.error
else:
status = res.status
counter[status] += 1
return counter
def download_many(cc_list, base_url, verbose, concur_req):
loop = asyncio.get_event_loop()
coro = downloader_coro(cc_list, base_url, verbose, concur_req)
counts = loop.run_until_complete(coro)
loop.close()
return counts
if __name__ == '__main__':
main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
使用asyncio包编写服务器
- 基础模块
import sys
import re
import unicodedata
import pickle
import warnings
import itertools
import functools
from collections import namedtuple
RE_WORD = re.compile(r'\w+')
RE_UNICODE_NAME = re.compile('^[A-Z0-9 -]+$')
RE_CODEPOINT = re.compile(r'U\+([0-9A-F]{4,6})')
INDEX_NAME = 'charfinder_index.pickle'
MINIMUM_SAVE_LEN = 10000
CJK_UNI_PREFIX = 'CJK UNIFIED IDEOGRAPH'
CJK_CMP_PREFIX = 'CJK COMPATIBILITY IDEOGRAPH'
sample_chars = [
'$', # DOLLAR SIGN
'A', # LATIN CAPITAL LETTER A
'a', # LATIN SMALL LETTER A
'\u20a0', # EURO-CURRENCY SIGN
'\u20ac', # EURO SIGN
]
CharDescription = namedtuple('CharDescription', 'code_str char name')
QueryResult = namedtuple('QueryResult', 'count items')
def tokenize(text):
"""return iterable of uppercased words"""
for match in RE_WORD.finditer(text):
yield match.group().upper()
def query_type(text):
text_upper = text.upper()
if 'U+' in text_upper:
return 'CODEPOINT'
elif RE_UNICODE_NAME.match(text_upper):
return 'NAME'
else:
return 'CHARACTERS'
class UnicodeNameIndex:
def __init__(self, chars=None):
self.load(chars)
def load(self, chars=None):
self.index = None
if chars is None:
try:
with open(INDEX_NAME, 'rb') as fp:
self.index = pickle.load(fp)
except OSError:
pass
if self.index is None:
self.build_index(chars)
if len(self.index) > MINIMUM_SAVE_LEN:
try:
self.save()
except OSError as exc:
warnings.warn('Could not save {!r}: {}'
.format(INDEX_NAME, exc))
def save(self):
with open(INDEX_NAME, 'wb') as fp:
pickle.dump(self.index, fp)
def build_index(self, chars=None):
if chars is None:
chars = (chr(i) for i in range(32, sys.maxunicode))
index = {}
for char in chars:
try:
name = unicodedata.name(char)
except ValueError:
continue
if name.startswith(CJK_UNI_PREFIX):
name = CJK_UNI_PREFIX
elif name.startswith(CJK_CMP_PREFIX):
name = CJK_CMP_PREFIX
for word in tokenize(name):
index.setdefault(word, set()).add(char)
self.index = index
def word_rank(self, top=None):
res = [(len(self.index[key]), key) for key in self.index]
res.sort(key=lambda item: (-item[0], item[1]))
if top is not None:
res = res[:top]
return res
def word_report(self, top=None):
for postings, key in self.word_rank(top):
print('{:5} {}'.format(postings, key))
def find_chars(self, query, start=0, stop=None):
stop = sys.maxsize if stop is None else stop
result_sets = []
for word in tokenize(query):
chars = self.index.get(word)
if chars is None: # shorcut: no such word
result_sets = []
break
result_sets.append(chars)
if not result_sets:
return QueryResult(0, ())
result = functools.reduce(set.intersection, result_sets)
result = sorted(result) # must sort to support start, stop
result_iter = itertools.islice(result, start, stop)
return QueryResult(len(result),
(char for char in result_iter))
def describe(self, char):
code_str = 'U+{:04X}'.format(ord(char))
name = unicodedata.name(char)
return CharDescription(code_str, char, name)
def find_descriptions(self, query, start=0, stop=None):
for char in self.find_chars(query, start, stop).items:
yield self.describe(char)
def get_descriptions(self, chars):
for char in chars:
yield self.describe(char)
def describe_str(self, char):
return '{:7}\t{}\t{}'.format(*self.describe(char))
def find_description_strs(self, query, start=0, stop=None):
for char in self.find_chars(query, start, stop).items:
yield self.describe_str(char)
@staticmethod # not an instance method due to concurrency
def status(query, counter):
if counter == 0:
msg = 'No match'
elif counter == 1:
msg = '1 match'
else:
msg = '{} matches'.format(counter)
return '{} for {!r}'.format(msg, query)
def main(*args):
index = UnicodeNameIndex()
query = ' '.join(args)
n = 0
for n, line in enumerate(index.find_description_strs(query), 1):
print(line)
print('({})'.format(index.status(query, n)))
- TCP Unicode 字符查找服务器
import sys
import asyncio
from charfinder import UnicodeNameIndex
CRLF = b'\r\n'
PROMPT = b'?>'
index = UnicodeNameIndex()
async def handle_queries(reader, writer):
while True:
writer.write(PROMPT)
await writer.drain()
data = await reader.readline()
try:
query = data.decode().strip()
except UnicodeDecodeError:
query = '\x00'
client = writer.get_extra_info('peername')
print('Received from {}: {!r}'.format(client, query))
if query:
if ord(query[:1]) < 32:
break
lines = list(index.find_description_strs(query))
if lines:
writer.writelines(line.encode() + CRLF for line in lines)
writer.write(index.status(query, len(lines)).encode() + CRLF)
await writer.drain()
print('Sent {} results'.format(len(lines)))
print('Close the client socket')
writer.close()
async def main(address='192.168.101.2', port=2323): # 设置服务器PC IP地址,其他PC也可能连接
prot = int(port)
server = await asyncio.start_server(handle_queries, address, port)
host = server.sockets[0].getsockname()
print('Serving on {}. Hit CTRL-C to stop.'.format(host))
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(main(*sys.argv[1:]))
windows在命令行中使用telnet需要手动开启功能
-
打开windows 7 中的telnet功能
控制面板 -> 程序 -> 程序和功能 -> 打开或关闭windows功能 -> Telnet客户端
-
启动测试
# 启动TCP服务器后,TCP客户端命令行
telnet <ip> <port>
总结
- 线程和协程的对比,线程调用可调用的对象
- @asyncio.coroutine装饰器在3.7版本中已经不支持yield from了,改为了async关键字和await
- asyncio.async()方法在3.7中去除了,继续使用将会报语法错误
- 文字旋转中使用‘\x08’回退光标实现
- 简单的TCP服务器
- 太…抽象了,能理解多少算多少吧,还有最后一部分就完了
流畅的Python 2015