Python中的defer库

说到defer库,就必须先提到python中的一个比较特别的网络库twisted。
他与其他网络库不同的地方在于,对于很多用连接的并发,他不会使用多线程去实现。比如网络库同时发出10个请求,对于一般的多线程网络库,就会初始化10个线程,每个线程负责一个连接。当一个线程遇到了耗时操作(比如发出request之后等待response,或者向本地磁盘写数据),他就会阻塞。多线程网络库的问题在于,大量的线程大部分时间会处于阻塞状态,真正使用cpu的时间很短,这样为了使cpu高效率运行,就必须同时并发很多很多线程,但是大量的线程难以管理,需要很复杂的同步机制,也难以debug,再加上线程之间上下文切换的损失,也会使整体效率下降。
而twisted库就没有这个问题,他从头到尾只使用一个线程(并不是真的一个线程能实现,但是几乎所有的工作都是一个主线程实现的)每当这个线程遇到耗时操作的时候,代码并不会阻塞,而是立即返回一个deferred对象,这个对象代表了耗时操作的结果,当然了,立即返回的时候,这个deferred对象并没有结果,当耗时操作完成之后,deferred获得结果值,并且出发各种callback。使用了defer的twisted库,只需要一个主线程就能完成大量的并发连接,因为一旦连接进入等候状态,线程并不会阻塞,而是立即得到了一个deferred对象,然后代码继续往下跑,当请求有了回应时,刚刚返回的deferred对象得到结果值,并且触发callback,而callback中对request的response进行处理。
但是defer的结果值是如何被赋值的?事实上返回deferred的时候,必须给deferred分出一个小线程,该线程运行耗时操作,保证主线程继续运行,然后小线程中的耗时操作完成的时候,deferred被赋值,同时强制中断主线程,开始跑deferred中挂的各种callback函数,完成之前等待耗时操作结果的任务。
下面简单介绍一下deferred的简单特性和用法:
1.deferred可以通过addCallback函数挂上回调,其中每个callback都必须有一个参数以接收上一个callback的返回值,每个callback也必须返回一个对结果的处理值。第一个callback的到的实参是耗时操作返回的结果,于是耗时操作完成时,结果被整个调用链读取和处理,所有callback跑完之后,结果被放在deferred的result字段里。
2.其中,如果有一个callback没有返回一个结果值,而是返回了另一个deferred(返回子deferred的情况很多,比如对一个耗时操作的结果处理时,又需要触发另一个耗时操作)那么子deferred将继承到返回他的callback之后的所有callback,而父亲callback链则清空。当子deferred的callback链(其实之前是父亲的)跑完之后,他会把结果添加到父亲的result字段中。所以,如果耗时操作的处理函数又要触发耗时操作,也没有问题,因为子deferred会继承父亲的所有callback。
3.defer库中有一个task文件,里边有很多方便的函数,比如我们可以通过task.deferLater,模拟一个返回deferred的耗时操作(因为直接返回deferred对象的耗时操作很少,基本是和twisted配套的库中才有的),所以如果想模拟一个返回deferred的耗时操作,可以使用deferLater函数。他返回一个deferred对象,并在你给定的时间之后触发结果。
4.如果想要自己或者第三方的一个耗时函数返回deferred对象,要使用defer.thread.deferToThread,这个函数返回一个deferred对象,在你传给他的函数(耗时操作)执行完毕之后,给deferred赋值并且触发callback
5.DeferredList通过一个包括很多deferred的list初始化,他的callback在所有他包括的deferred运行完毕时被触发,其参数是所有deferred的运行结果。

下面我们看一个简单的defer示例程序:

#-*- coding: utf-8 -*-
from twisted.internet import defer
from twisted.internet import task
from twisted.internet import reactor

# 耗时操作的外壳函数,返回一个deferred对象,一般调用这个函数之后,接住deferred对象,然后其addCallbacks加一些在耗时操作
# 完成之后的处理函数。这里使用了task模块来模拟了一个耗时操作,其中实参3模拟了消耗的时间,on_done模拟了耗时操作的返回值
def time_wasted_wrapper(job_id):
    def on_done():
        print('time-wasted job '+str(job_id)+' done!')
        # 耗时操作完成了,返回之歌结果
        return job_id
    print('begin time-wasted job '+str(job_id))
    # 返回一个deferred对象,真实情况下,这里可能是一个直接返回deferred对象的函数,也可能是一个正常阻塞函数,但是你可以用
    # deferToThread来获得一个deferred对象
    return task.deferLater(reactor, 3, on_done)

def on_one_job_done(result):
    print('result plus 1!')
    return result+1

# 所有deferred完成之后,触发回调提醒我们
def all_jobs_done(result):
    print(str(result))
    print('all jobs are done!')
    reactor.stop()

# 一次搞10个模拟的耗时操作,耗时操作都用时3秒,而后续处理都是对耗时操作的结果加1
def install_jobs():
    job_list = list()
    for i in range(10):
        job = time_wasted_wrapper(i)
        job.addCallback(on_one_job_done)
        job_list.append(job)
    deferred_list = defer.DeferredList(job_list)
    deferred_list.addCallback(all_jobs_done)

# 主函数,在调用完所有耗时操作之后,直接把主线程交给reactor处理,这里我们就能看到,开始调用10个耗时操作很快就能完成,而过一会
# 耗时操作的结果才陆续返回
if __name__ == '__main__':
    install_jobs()
    print('all ``
obs have started!')
    reactor.run()

我们可以使用一个修饰器使代码更好懂:

#-*- coding: utf-8 -*-
from twisted.internet import defer
from twisted.internet import task
from twisted.internet import reactor
# defer提供简单的写法,本身我们需要在拿到deferred之后把后续处理都挂在其callback中,但是这样写,代码就很分散,在debug的时候
# 跳来跳去很难读懂。如果使用defer.inlineCallbacks修饰器,就可以写出更好读懂的defer相关函数。被修饰的函数会返回一个deferred
# 对象,这个deferred对象会在函数中defer.returnValue函数跑成之后获得结果值,函数中间的各种yield都不会产生结果值,他们只是
# 中断点。整个函数中,各种耗时操作都可以被yield,yield一个deferred对象之后,这个函数就中断在yield处,主线程开始执行其他任务
# 当中途yield出来的deferred有了结果之后,这个函数从yield的地方继续往下运行,直到运行到returnValue处,使得这整个函数返回
# 的deferred对象获得结果。
# 其实这个修饰器就是是的代码看起来紧凑一点,吧需要家callback的代码都集中并且串行地写在同一个函数里,并且对于deferred的callback
# 中又要产生deferred的情况,不用return一个deferred对象,而是直接yield出去,看起来更好懂。
@defer.inlineCallbacks
def time_wasted_wrapper(job_id):
    print('begin time-wasted job '+str(job_id))
    yield task.deferLater(reactor, 3, lambda:None)
    # 其实从现在开始,就想当一个callback函数了
    print('time-wasted job '+str(job_id)+' done!')
    print('result plus 1!')
    defer.returnValue(job_id+1)

# 所有deferred完成之后,触发回调提醒我们
def all_jobs_done(result):
    print(str(result))
    print('all jobs are done!')
    reactor.stop()

# 一次搞10个模拟的耗时操作,耗时操作都用时3秒,而后续处理都是对耗时操作的结果加1
def install_jobs():
    job_list = list()
    for i in range(10):
        job = time_wasted_wrapper(i)
        job_list.append(job)
    deferred_list = defer.DeferredList(job_list)
    deferred_list.addCallback(all_jobs_done)

# 主函数,在调用完所有耗时操作之后,直接把主线程交给reactor处理,这里我们就能看到,开始调用10个耗时操作很快就能完成,而过一会
# 耗时操作的结果才陆续返回
if __name__ == '__main__':
    install_jobs()
    print('all job have started!')
    reactor.run()

yield比较多,可能难以理解,总之,在defer修饰器的环境下,yield并不是一般的yield含义,不是一个可以迭代的generator,yield在这里仅仅代表一个中断点。

你可能感兴趣的:(python,scrapy)