0%

引言

在阅读 tornado.iostream 模块时涉及到一些网络异常的处理,在深入了解异常的定义和处理后,觉得有必要对 TCP/IP 协议做一个简单的复习整理,尤其针对直接涉及 socket 编程的 TCP 协议部分。

以下总结的内容依次从 TCP/IP 的分层结构、数据封装与分用、以太网帧结构、IP 数据包结构、TCP 数据段(或报文段)结构来进行,并专门对 TCP 的三次握手和四次挥手做一个较为详细的介绍。

TCP/IP 协议

TCP/IP 的分层结构

OSI (Open System Interconnect, 开放系统互连参考模型)为开放式互连信息系统提供了一种理论上的网络模型,而 TCP/IP 则是实际实现运行的网络模型。TCP/IP 采用四层结构,它与 OSI 七层结构的对应关系如下图所示:

注:上述协议分层不是完美的,ICMP/IGMP 作为 IP 的附属协议,其数据是封装在 IP 数据包中的,所以在逻辑上它们是在 IP 的上层,这个在后面数据分用的示意图中会体现;ARP/RARP 也遇到同样的问题,它们与 IP 数据报一样都有各自的以太网帧结构,但又是为 IP 地址转换服务的,所以在逻辑上它们是在 IP 的下层。在《TCP-IP详解》中就将 ARP/RARP 划分到网络接口层。在这个图中,将这几个协议都放在与 IP 同一层只是为了选择一种分类方式,不是绝对的。

Read more »

引言

注:正文中引用的 Tornado 代码除特别说明外,都默认引用自 Tornado 4.0.1。

通过前面 IOLoop、 tornado.gen 模块的分析,我们基本了解了 Tornado 这个异步框架的核心实现,IOLoop 模块负责驱动异步执行, tornado.gen 模块提供 coroutine 的实现,负责支持使用同步方式编写异步代码。到此为止,一切看起来都还不错。接下来我们来看一看 Tornado 中如何处理异步调用上下文状态的。

一起来思考一个在(所有)异步框架中都会遇到的问题,一个 “异步调用” 可以简单理解为:传递一个 “回调函数” 后便立即返回的调用,框架会在异步动作完成后执行 “回调函数”。很显然,这样就导致了一个问题,由于 “回调函数” 实际执行的环境已经脱离了 “异步调用” 时的环境,这便要求 “回调函数” 不能依赖 ”调用时“ 环境。如果真有这个限制的话,那么这个框架使用起来就不是那么顺手了,试想下面一些场景:

  1. (在一个线程中)处理多个异步操作时,可能需要一些共享的资源,通常我们可以把这些资源保存到 “线程局部变量” 或者 “全局变量” 中以达到共享的目的。但当不是所有的异步操作都需要这些资源时,将资源暴露到不需要的操作中,很可能引发不可预知的问题。

  2. “回调函数” 执行时的环境已经不是 “调用时” 环境,如果 “回调函数” 抛出一些异常,那么很显然不能被 “调用时” 的上下文捕捉到。这与同步代码比较起来显得不够直观,我们希望减少这种差异。

针对这个问题,Tornado 提供了 tornado.stack_context 模块来解决。按照我个人的理解,简单来说就是通过该模块, Tornado 提供了一个叫 StackContext 的机制, StackContext 是一个栈式上下文结构,它能够像 threadlocal 一样为当前操作保存一个栈式上下文快照,当异步执行结束回调时便可以借助这个机制恢复调用时的环境。

通过源代码中注释,我们来看看 Facebook 的工程师们给出的介绍(注:我知道我的翻译就是一坨,看不懂这坨的可以直接看源代码的英文原文注释。):

StackContext 允许应用程序在切换到其他上下文执行时也能保持一个像 threadlocal 一样的状态。一些令人振奋的的例子是使用 StackContext 可以避免显式地使用异步调用的封装器,以及为当前调用增加一些额外的上下文用于输出日志。

这个有些不好理解,异常处理器可以视为这么一种想法(idea)的延伸,它就像一种本地栈的状态,栈在暂停和在新的上下文中恢复时需要被保持(注:把异常处理器也抽象处理成一种特化的上下文,能够被转移。)。 StackContext ++把恢复调用栈的工作转到一种控制一个上下文转移的机制上++。

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@contextlib.contextmanager
def die_on_error():
try:
yield
except Exception:
logging.error("exception in asynchronous operation",exc_info=True)
sys.exit(1)

with StackContext(die_on_error):
# Any exception thrown here *or in callback and its desendents*
# will cause the process to exit instead of spinning endlessly
# in the ioloop.
http_client.fetch(url, callback)
ioloop.start()

大多数应用程序都不需要和 StackContext 直接打交道。什么时候需要用到,这里有些经验法则可供参考:

  • 如果你在写一个不依赖于 tornado.ioloop 或者 tornado.iostream 这类函数库(这类库提供 stack_context 的默认支持)的异步库(比如一个线程池),那么在任何异步操作之前你需要使用 stack_context.wrap() 函数来取得操作开始时的栈式上下文快照。

  • 如果正在写一个需要使用到一些共享资源(比如连接池)的异步库,那么你需要在 with stack_context.NullContext(): 块中创建那些共享资源。这样可以防止 StackContexts 从一个请求泄漏到另一个请求。

  • 如果你想写一些在可以保持到异步回调时异常处理器,那么创建一个 StackContext 或者 ExceptionStackContext ,并把异步调用放在它们的 with 块中。

Read more »

引言

注:正文中引用的 Tornado 代码除特别说明外,都默认引用自 Tornado 4.0.1。

tornado.gen 模块是一个基于 python generator 实现的异步编程接口。通过该模块提供的 coroutine (注:这里 coroutine 指的是 ”协程” 概念而不是后面具体实现的 decorator:@gen.decorator),大大简化了在 Tornado 中编写异步代码的工作 —— 支持 “同步方式编写异步代码” ,避免编写烦人的回调函数。参考官方文档的例子,通常我们编写的异步代码如下:

1
2
3
4
5
6
7
8
9
10
class AsyncHandler(RequestHandler):
@asynchronous
def get(self):
http_client = AsyncHTTPClient()
http_client.fetch("http://example.com",
callback=self.on_fetch)

def on_fetch(self, response):
do_something_with_response(response)
self.render("template.html")

而使用 tornado.gen 模块提供的 decorator ,在 Tornado 3.1 以前我们可以这样写异步代码:

1
2
3
4
5
6
7
8
class GenAsyncHandler(RequestHandler):
@asynchronous
@gen.engine
def get(self):
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://example.com")
do_something_with_response(response)
self.render("template.html")

Tornado 3.1 及以上版本,可以直接使用 @gen.coroutine 来代替 @asynchronous:

1
2
3
4
5
6
7
class GenAsyncHandler(RequestHandler):
@gen.coroutine
def get(self):
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://example.com")
do_something_with_response(response)
self.render("template.html")

注:@asynchronous 在 tornado.web 中定义,对于使用了 @gen.coroutine 装饰的方法不需要再使用 @asynchronous 进行装饰,但同时使用前述 2 个 decorator 进行方法装饰也是合法的,在同时使用的情况下需要注意的是 @asynchronous 必须是第 1 个 decorator 。

很显然,采用同步方式编写的异步代码相比起分散在各处的异步回调函数代码,更利于代码的阅读和逻辑的组织。

该模块的实现非常巧妙也不容易理解,作为阅读 Tonardo 源码的笔记,我将在后面内容中结合源码和自己的理解对其实现进行分析。

Read more »

原文:signal — Set handlers for asynchronous events

Python的 signal 模块为使用信号处理器提供了一些途径。关于信号和处理器的工作过程有一些规则需要注意:

  • 除非明确地重置处理器,否则一旦为一个特定的信号设置了处理器,该处理器就将一直有效(Python模仿了BSD的风格接口而不管底层是如何实现的)。唯一例外的是SIGCHLD信号处理器,其由底层实现决定。

  • 无法从critical sections临时阻塞信号(因为在类UNIX系统中都不支持)。

  • 尽管Python信号处理器能在接收到相应信号时立即被异步调用,但实际上是在Python解释器的原子指令之间被调用(注: 意思就是Python信号处理器的调用必须在一条“原子指令”执行完之后才能被调用)。这就意味着若信号到达时Python解释器正在执行一个长时间的纯C计算(比如正则表达式处理一大段文本),那么信号处理器的调用将会被推迟不确定时间。

  • 当一个信号在I/O操作期间到来,信号处理器调用完成后可能导致I/O操作抛出异常。这个依赖于顶层Unix系统的系统调用中断语义。

  • 因为 C 信号处理器总是会返回,所以就没有处理类似 SIGFPE 和 SIGSEGV 导致的同步错误(注: 在POSIX兼容的平台上,SIGFPE(floating-point exception,浮点异常)是当一个进程执行了一个错误的算术操作时发送给它的信号,SIGSEGV(egmentation violation, 段违例)是当一个进程执行了一个无效的内存引用,或发生段错误时发送给它的信号。)。

  • Python默认设置了一些信号处理器:SIGPIPE信号会被忽略(所以在管道或者套接字上 “写” 错误时将抛出一个普通的Python异常);SIGINT信号会被包装成一个 KeyboardInterrupt 异常。这些信号处理器都可以被overriden。

  • 多线程信号编程时需要注意一些问题。在同时使用多线程和信号时需要记住的基本规则:signal() 只能在主线程中执行;任何线程都可以执行 alarm(), getsignal(), pause(), setitimer()或者 getitimer();只有主线程可以设置新的信号处理器,也只有主线程可以接受到信号(尽管底层线程实现支持将信号发送给任何一个独立线程,但是Python的信号模块强制实现只能主线程能接受信号)。这就意味着在Python中信号不能用于线程间通信,使用锁来代替。

变量

signal.SIG_DFL

  • 这是两个标准信号处理选项之一,该处理选项将简单地执行默认的信号处理函数。例如,在大多数系统中收到 SIGQUIT 信号后转存文件(dump core )并退出,而忽略掉 SIGCHLD 信号。

signal.SIG_IGN

  • 这是另一个默认信号处理器,该处理选项将会忽略指定的信号。例如,signal(SIGCHLD, SIG_IGN)

SIG

  • All the signal numbers are defined symbolically. For example, the hangup signal is defined as signal.SIGHUP; the variable names are identical to the names used in C programs, as found in . The Unix man page for ‘signal()‘ lists the existing signals (on some systems this is signal(2), on others the list is in signal(7)). Note that not all systems define the same set of signal names; only those names defined by the system are defined by this module. (注:每一个信号名称是有一个代表正整数的宏来表示,但是不应该试图去推测宏代表的具体数字,而是直接使用信号名称。这是因为这个数字会随着系统的不同或同一系统的不同版本而不同,但是名称还算是标准化和统一的)
    Read more »

PEP: 475
Title: Retry system calls failing with EINTR
Version: $Revision$
Last-Modified: $Date$
Author: Charles-François Natali cf.natali@gmail.com, Victor Stinner victor.stinner@gmail.com
BDFL-Delegate: Antoine Pitrou solipsis@pitrou.net
Status: Final
Type: Standards Track
Content-Type: text/x-rst
Created: 29-July-2014
Python-Version: 3.5
Resolution: https://mail.python.org/pipermail/python-dev/2015-February/138018.html

摘要

标准库中提供的系统调用函数在捕获到 EINTR 错误时能够自动重试,以减轻程序编码的负担。

所谓系统调用,我们指的是标准C函数库提供的操作 I/O 或者其他系统资源的函数。

基本原理

中断系统调用

在 POSIX 系统中,信号是很常见的,系统调用编码的时候必须准备捕获它们。一些常见的例子:

  • 最常见的是 SIGINT 信号,当按下 CTRL+C 时发送该信号。 Python 在默认情况下捕获该信号后会抛出一个 KeyboardInterrupt 异常。

  • 当使用到子进程时,子进程退出时会发送 SIGCHLD 信号。

  • 改变终端窗口大小时会向在该终端中运行的应用程序发送 SIGWINCH 信号。

  • 通过 CTRL+z 或者 SIGWINCH 命令将应用程序放到后台执行时发送 SIGCONT 信号。

编写一个安全的 C 信号处理器是困难的:因为并不是所有的函数都是 “异步信号安全的” (例如,printf()malloc() 函数就不是异步信号安全的),同时要处理好信号中断后的重入也是很麻烦的。然后幸运的是,当进程在执行系统调用的过程中被信号中断而失败的时会返回 EINTR 错误以便于程序处理,这样就不必强制要求函数是信号安全的。

这是一种依赖于系统的行为:在某些系统中,设置了 SA_RESTART 标识后,一些系统调用在捕获到 EINTR 错误后会自动重试。尽管如此,当在 Python 中调用 signal.signal() 函数设置信号处理器后会清除 SA_RESTART 标识:这样一来,在 Python 中所有的系统调用都可能会被信号中断而导致失败。

因为接收到一个信号并不是发生异常,所以健壮的 POSIX 编程要求必须能够处理 EINTR 错误(在大多情况下,也就意味着将一个希望操作成功的系统调用函数包装在一个循环中)。如果没有 python 提供的原生支持,应用程序编码就会更繁琐(由于python提供了支持,所以我们不需要专门去处理 EINTR 错误,这样可以使 python 代码更简洁。)。

Python 3.4 中的情况

在 Python 3.4 中,捕获 InterruptedError 异常(专门用于包装 EINTR 错误的异常类型)的代码被复制到各处系统调用处。但实际上也只有一小部分模块捕获了该异常,要修复这个问题让所有的 Python 模块都处理该异常需要花费好几年的时间。下面是一段捕获 InterruptedError 异常并自动重试 file.read() 的代码示例:

1
2
3
4
5
6
while True:
try:
data = file.read(size)
break
except InterruptedError:
continue

Python 标准库中已经实现内部捕获 InterruptedError 异常的模块列表:

  • asyncio
  • asyncore
  • io, _pyio
  • multiprocessing
  • selectors
  • socket
  • socketserver
  • subprocess

其他编程语言比如 Perl, Java 和 Go 中系统调用时捕获 EINTR 错误并自动重试已经在语言底层实现了,所以不会影响到库和应用程序。

Read more »

背景

在一般的分布式应用中,要安全有效地同步多服务器多进程之间的共享资源访问,就要涉及到分布式锁。目前项目是基于 Tornado 实现的分布式部署,同时也使用了 Redis 作为缓存。参考了一些资料并结合项目自身的要求后,决定直接使用Redis实现全局的分布式锁。

使用 Redis 实现分布式锁

使用 Redis 实现分布式锁最简单方式是创建一对 key-value 值,key 被创建为有一定的生存期,因此它最终会被释放。而当客户端想要释放时,则直接删除 key 。基于不同的 Redis 命令,有两种实现方式:

  1. Redis 官方早期给的一个实现,使用 SETNX,将 value 设置为超时时间,由代码实现锁超时的检测[有缺陷,有限制,并发不高时可用];
  2. 有同学自己的实现:使用 INCR + EXPIRE,利用 Redis 的超时机制控制锁的生存期[不建议使用];
  3. Redis 官方给的一个改进实现:使用 SET resource-name anystring NX EX max-lock-time(Redis 2.6.12 后支持) 实现, 利用 Redis 的超时机制控制锁的生存期[Redis 2.6.12 以后建议使用]。
使用 SETNX 实现

Redis 官方最早在 SETNX 命令页给了一个基于该命令的分布式锁实现

1
Acquire lock: SETNX lock.foo <current Unix time + lock timeout + 1>
1
Release lock: DEL lock.foo
  1. 如果 SETNX 返回 1,则表明客户端获取锁成功, lock.foo 被设置为有效 Unix time。客户端操作完成后调用 DEL 命令释放锁。

  2. 如果 SETNX 返回 0,则表明锁已经被其他客户端持有。这时我们可以先返回或进行重试等对方完成或等待锁超时。

处理死锁问题:
上述算法中,如果持有锁的客户端发生故障、意外崩溃、或者其他因素因素导致没有释放锁,该怎么解决?。我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件:

  1. C1 和 C2 读取 lock.foo 检查时间戳,先后发现超时了。
  2. C1 发送DEL lock.foo。
  3. C1 发送SETNX lock.foo 并且成功了。
  4. C2 发送DEL lock.foo
  5. C2 发送SETNX lock.foo 并且成功了。
  6. ERROR: 由于竞态的问题,C1 和 C2 都获取了锁,这下子问题大了。
Read more »

以下内容完全来源于 RednaxelaFX 两篇文章的总结:

  1. 方法分派(method dispatch)的几个例子
  2. C# 4的方法动态分派逻辑变了……

面向对象语言中经常会对函数调用的第一个参数做特殊处理,包括语法和语义都很特别。

  1. 语法的特别之处在于实际上的第一个参数不用写在参数列表里,而是写在某种特殊符号之前(b.foo(0)的“.”),也就是所谓的隐含参数。
  2. 语义的特别之处在于这第一个参数的称为方法调用的接收者(reciever),它的实际类型会参与到方法分派的判断中,而其余的参数要么只参与静态类型判断(单一分派+方法重载),要么也以实际类型参与到方法分派的判断(多分派)。

在单一分派静态类型的面向对象语言中,重载仍然是编译时概念:编译器只会根据静态变量的类型来判断选择哪个版本的重载,而不像运行时多态那样根据值的实际类型来判断。

在C#4.0之前方法都是单一分派的,C#4.0增加了“动态类型”,因为dynamic类型运行时才能确定类型,所以会参与方法分派。也就是说,如果一个虚方法调用的参数的类型是都是dynamic(如果有静态类型参数,那么静态类型参数将不参与方法分派,而是在编译期参与方法推断:This means that for all arguments not statically typed dynamic, the compile time types will be used, regardless of their runtime types.),那么整个方法调用都无法在编译时判定到底应该选用哪个具体版本。CLR会根据方法接收者(this)和每个参数的实际类型来进行方法分派,这个语义与多分派的语义是相同的。如下代码所示:

Read more »

引言

在之前与同事的讨论中发现对C#泛型反射时的一些术语理解有些错误或者不够深刻,借此文对相应的知识进行一下整理。本文分为两个部分,第一部分主要简单的介绍一下 “运行时中泛型”,第二部分对泛型反射的一些术语进行解释并参照代码和输出结果进行分析和巩固。

CLR中的泛型

在.NET中将泛型类型或者泛型方法编译为MSIL时是包含有类型参数的元数据。也就是说在MSIL层面,只负责泛型的声明和使用,而泛型实例化是由CLR在运行时负责完成的。

泛型声明:

1
2
3
4
5
6
7
8
9
10
11
public class Foo<T>
{
public void M1<TU>()
{
// 泛型的使用
var listOne = new List<Int32>();

Console.WriteLine(typeof(TU));
Console.WriteLine(typeof(T));
}
}

上述泛型类将被编译成名叫 Foo`1 < T > 的泛型类型 ,其 M1< TU > 泛型方法对应的IL代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.method public hidebysig instance void  M1<TU>() cil managed
{
// 代码大小 40 (0x28)
.maxstack 1
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> listOne)
IL_0000: nop
IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
IL_0006: stloc.0
IL_0007: ldtoken !!TU
IL_000c: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0011: call void [mscorlib]System.Console::WriteLine(object)
IL_0016: nop
IL_0017: ldtoken !T
IL_001c: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0021: call void [mscorlib]System.Console::WriteLine(object)
IL_0026: nop
IL_0027: ret
} // end of method Foo`1::M1

CLR会根据运行时提供的类型参数是值类型还是引用类型而不同,对于值类型CLR将会为每个类型创建(第一次遇到时)专用的泛型类型,对于引用类型则略有不同。所有的引用类型都会重用同一个泛型版本,大体上可以想象为所有的引用类型使用一个 Object 的泛型实例化版本。之所以这样设计,是因为引用的大小是相同的(32位系统上是32位,64位系统上是64位),而值类型则不然。这样设计的好处是对于引用类型可以共享同一份native code, 避免“类型爆炸”。(实例化的泛型类型虽然享有独立的元数据,但是共享同一个EEClass,即其TypeHandle指向的MethodTable中的EEClass地址相同。)

泛型与反射

#####一些术语

  1. 开放泛型类型/方法(open generic type/method):所有泛型参数都未绑定值的泛型类型/方法定义,例如,Dictionary< TKey, TVal >。

  2. 开放构造类型/方法(open constructed type/method):部分泛型参数绑定了具体类型的值的泛型类型/方法, 例如,Dictionary< String, TVal>。

  3. 封闭构造类型/方法(close constructed type/method):按照 ContainsGenericParameters=true 来定义,分为两种:1)封闭泛型类型/方法(close generic type/method),所有泛型参数都绑定了具体类型的值的泛型类型/方法,例如,Dictionary< String, Int32>;2)普通的类型/方法。

注意:在泛型类型上可以调用MakeGenericType生成构造类型,在泛型方法定义上调用MakeGenericMethod生成构造方法。ContainsGenericParameters 属性提供一种标准方法来区分封闭构造类型/方法(可以实例化/调用)和开放构造类型/方法(不能实例化/调用)。

ContainsGenericParameters 属性递归搜索类型参数
1、对于开放类型 A< T> 上的所有方法调用 ContainsGenericParameters 都将返回true;
2、对于下面代码中的 listOfSomeUnknownTypeOfList 类型,由于 g1是 开放泛型类型所以listOfSomeUnknownTypeOfList.ContainsGenericParameters 返回 true。

1
2
var gl = typeof(List<>);
var listOfSomeUnknownTypeOfList = gl.MakeGenericType(gl);
  1. 泛型类型:包括开放泛型类型、开放构造类型、封闭泛型类型

  2. 泛型方法:包括开放泛型方法、开放构造方法(注:不包括开放泛型类型、开放构造类型中的非泛型开放构造方法,见后面 “开放泛型类型(及开放构造类型)上的方法反射” 部分内容)、封闭泛型方法

Read more »

摘要

一直以来,闭包这种编程结构都是一些语言的重要组成部分。在某些场景中使用闭包能够优雅地解决一些棘手的问题。同时闭包的使用有益于模块化编程,它能以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。

闭包是一门十分有用的技术,但是由于在C#中函数不是一等公民(First-class citizen)的原因,以前在使用C#的时候我没有深入地去关注其中对闭包的支持。而最近一段时间由于工作需要,主要使用 javascript/python 进行开发,在这两种编程语言中函数都是被视为一等对象,实践中大量使用闭包简化编程。例如在javascript中使用闭包模拟对象实例,在python中利用闭包的特性定义功能强大的装饰器等等。

对于这样一门技术,仅仅会使用不是目的,知其然而知其所以然才是。这篇笔记即是出于这样一种目的而整理的。相比较而言我只对C#、javascript、python比较熟悉,而三者中javascript对闭包的支持相对纯粹和完备,所以笔记中我主要用javascript语言表述。但是涉及具体语言的部分,也使用C#和python语言表述,比如对 “闭包陷阱” 的解决,python受限制的闭包实现。

注:笔记相关的代码,主要的目的是描述问题,所以我并没有一一运行过,不能确保都是正确的。但这不影响相关内容。

闭包,匿名函数,函数对象,自由变量,好乱的样子

在(一般的)编程语言中,局部变量的作用域仅限于包含它们的函数,脱离了创建它的函数环境后便无法被访问到。但在一些支持嵌套定义函数的语言中,如果内部的函数引用了外部的函数的变量,则可能延长变量的生命周期。例如下面的javascript代码:

1
2
3
4
5
6
7
8
9
10
11
12
function foo(x){
var i = x * 2;
function(y){
return i+y;
}
}

var f1 = foo(1);
var f2 = foo(2);

print(f1(2)); // 4
print(f2(2)); // 6

外部 foo 函数执行后返回了foo内部定义的匿名函数,以及不在该内部函数中定义的外部变量i。即使离开了创建i的函数环境,我们依然能够通过f1,f2访问到i。这就是一个典型的闭包(Closure)。

其实闭包并不是一个新概念,而是早在上个世纪60年代高级语言发展初期就已经产生。那么究竟闭包是什么呢?按照维基百科闭包的解释,一般我们有两种定义:

  1. 在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,指的是引用了自由变量的函数(自由变量指定的除去函数局部变量之外的其他变量,比如前面例子中的变量i)。

  2. 另一种说法认为闭包是由函数和相关的引用环境组合而成的实体。

从定义上可以看出,这两种对闭包的定义具有完全不同的关注点。对于第一种定义,强调的是闭包是函数,是一类特殊的函数。第二种定义认为闭包是函数和引用环境组成的实体,本质上不再是函数,而是函数对象,能够作为对象使用的函数。术语 first class function 是对这个概念的精确描述。函数本质上只是一些可执行的代码,一旦被定义好以后就不会发生变化,没有状态,具有引用的透明性。闭包作为函数对象,可以由同一个函数与不同的引用环境组成不同的实例,有状态,没有引用的透明性,所以闭包不再是单纯的函数。

从理解的角度的来说,第二种定义更为精确和利于理解。

注:匿名函数与闭包是不同概念,在一般支持闭包的语言中都支持匿名函数,匿名函数可以让我们更容易实现闭包。

Read more »

摘要

ioloop.py 是整个tornado的核心模块,负责实现服务器的异步非阻塞机制。其中 IOLoop 类是一个基于level-triggered的I/O事件循环,它使用I/O多路复用模型(select模型)监视每个文件描述符的I/O事件是否就绪,当文件描述符I/O事件就绪后调用对应的处理器(handler)进行处理。本篇笔记是对tornado v4.0.1的 IOLoop模块的源码解析。

IOLoop

IOLoop在Linux下使用epoll, 在BSD/Mac OS X下使用kqueue,否则使用selelct

1
2
3
4
5
6
7
8
9
10
11
@classmethod
def configurable_default(cls):
if hasattr(select, "epoll"):
from tornado.platform.epoll import EPollIOLoop
return EPollIOLoop
if hasattr(select, "kqueue"):
# Python 2.6+ on BSD or Mac
from tornado.platform.kqueue import KQueueIOLoop
return KQueueIOLoop
from tornado.platform.select import SelectIOLoop
return SelectIOLoop

通过调用add_handler方法将一个文件描述符(v4.0中增加了对file-like object的支持)加入到I/O事件循环中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def add_handler(self, fd, handler, events):
"""Registers the given handler to receive the given events for ``fd``.

The ``fd`` argument may either be an integer file descriptor or
a file-like object with a ``fileno()`` method (and optionally a
``close()`` method, which may be called when the `IOLoop` is shut
down).

The ``events`` argument is a bitwise or of the constants
``IOLoop.READ``, ``IOLoop.WRITE``, and ``IOLoop.ERROR``.

When an event occurs, ``handler(fd, events)`` will be run.

.. versionchanged:: 4.0
Added the ability to pass file-like objects in addition to
raw file descriptors.
"""

fd, obj = self.split_fd(fd)
self._handlers[fd] = (obj, stack_context.wrap(handler))
self._impl.register(fd, events | self.ERROR)
Read more »