Jmeter生活不易 我用pythonPython

Python进程、线程、回调与协程 总结笔记 适合新手明确基本概

2016-09-28  本文已影响2925人  treelake

怎样让python在现代的机器上运行的更快,充分利用多个核心,有效地实现并行、并发一直是人们的追求方向。

GIL

1、CPU Bound

拓展:python3中multiprocessing中使用多参数的技巧,来自stackOverFlow

#!/usr/bin/env python3
from functools import partial
from itertools import repeat
from multiprocessing import Pool, freeze_support
def func(a, b):
    return a + b
def main():
    a_args = [1,2,3]
    second_arg = 1
    with Pool() as pool:
        L = pool.starmap(func, [(1, 1), (2, 1), (3, 1)])
        M = pool.starmap(func, zip(a_args, repeat(second_arg)))
        N = pool.map(partial(func, b=second_arg), a_args)
        assert L == M == N
if __name__=="__main__":
    freeze_support()
    main()

2、I/O Bound

补充:线程是一种轻量进程,实际上在linux内核中,两者几乎没有差别,除了一点——线程并不产生新的地址空间和资源描述符表,而是复用父进程的。线程的调度和进程一样,都必须陷入内核态。

补充:类似进程池,我们也会使用线程池。简单解释就是一个复杂点的程序,会将线程频繁创建的开销通过在线程池中保存空闲线程的方式摊销,然后再从线程池中取出并重用这些线程去处理随后的任务;这样和使用socket连接池效果差不多。

  • 抢占式:现行进程在运行过程中,如果有重要或紧迫的进程到达(其状态必须为就绪),则现运行进程将被迫放弃处理机,系统将处理机立即分配给新到达的进程。

事件循环是一种等待程序分配事件或消息的编程架构。“当A发生时,执行B”。事件循环被认为是一种循环是因为它不停地收集事件并通过循环它们来处理事件。(监听)。

from selectors import DefaultSelector, EVENT_WRITE
import socket
selector = DefaultSelector()
sock = socket.socket()
sock.setblocking(False)
# 设置socket为阻塞或非阻塞
# sock.setblocking(True) is equivalent to sock.settimeout(None)
# sock.setblocking(False) is equivalent to sock.settimeout(0.0)
try:
    sock.connect(('xkcd.com', 80))  # 仅仅发送连接请求
except BlockingIOError:
    pass
# 设置socket为非阻塞必然抛出异常
def connected():
    selector.unregister(sock.fileno())
    print('connected!')
# 注册回调(文件描述符, 事件, 回调函数)
selector.register(sock.fileno(), EVENT_WRITE, connected)
#
def loop():
    while True:
        events = selector.select()
        for event_key, event_mask in events:
            callback = event_key.data
            callback()

我们无视虚假的错误(BlockingIOError)然后调用selector.register,并传入socket文件描述符和一个代表我们等待事件类型的常量。同时我们传入一个回调函数connected用来在事件发生时被调用。connected回调函数被存储为event_key.data。下面的循环中调用在select()处暂停了,直到有下一个IO事件发生,当发生时就获取回调函数并调用。
到此,我们展示了如何开始一个操作,并在该操作的I/O准备好之后执行一个回调。一个异步框架基于两个我们展示的特性:非阻塞的套接字和事件循环,以此来实现单线程的并发。
我们在这里实现的是并发而不是并行。也就是说,我们创建了一个操作重叠IO的小系统。它能在其它I/O作业还在途时(即I/O还未准备好时)开始新的作业。它并没有真正利用多核心来执行并行计算。但是它就是被设计来应对高IO问题,而不是CPU密集型问题的。
异步并不就比多线程快,通常它不是的,实际上就python而言,在服务小数量级的很活跃的链接时,一个类似于上文的事件循环会一定程度上慢于多线程。在这样的工作负载下,如果没有运行时的全局解释器锁,多线程将会表现得更好。异步IO适合的是许多缓慢和可睡眠的链接,适用于相应的情况。

# 阻塞方式的爬虫主干代码
def fetch(url):
    sock = socket.socket()
    sock.connect(('xkcd.com', 80))  # 请求连接,可能会被阻塞
    request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
    sock.send(request.encode('ascii'))  # 发送请求内容,可能会被阻塞
    response = b''
    chunk = sock.recv(4096)  # 读取接收信息,可能会被阻塞
    while chunk:
        response += chunk
        chunk = sock.recv(4096) # 读取接收信息,可能会被阻塞

    # 解析页面信息,返回新的链接,然后将它们加入待爬取队列
    links = parse_links(response)
    q.add(links)

在一次socket操作和下一次之间函数记住了什么状态呢?它拥有套接字,一个URL,还有积累的response。一个运行在线程上的函数使用了编程语言的基本特性来保存临时状态在本地变量中,即它的栈上。这个函数还具有一个延续——那就是,它在IO完成后计划执行的代码。运行时通过储存线程的指令指针来记住函数的后续内容。在IO操作之后,你不需要考虑重新加载这些东西。因为它是语言内在的特性。
但是在异步框架中就不能指望它们自动完成。当要等待一个IO时,一个函数必须显式地保存它的状态,因为函数在IO完成前已经返回了并且丧失了它的栈帧。为了替代本地变量,我们的基于回调的爬虫需要存储了sock和response作为抓取器实例的属性。为了替代指令指针,它需要通过注册回调函数connected和read_response来记录它的后续操作。当应用程序的特性不断增加时,我们通过回调手动储存的状态的复杂性也在增加。这让人头痛。

相比于每个线程的50k的内存消耗和操作系统本身对于线程的硬限制。一个python协程只需要3k的内存(在Jesse的系统上)。Python能够轻易地开始上万的协程。

补充:协程是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序,亦即可以暂停执行的函数。

参考


上一篇 下一篇

猜你喜欢

热点阅读