0%

摘要

大多数时候我们编写的(Web)服务端程序,都是基于已有的网络服务框架或者服务器,这些服务端程序的关注点主要是处理领域业务,很少直接涉及到如何高效稳定处理与客户端的通信问题。所以,当你需要从服务端打得火热的各种框架中选择一个来开发应用程序,当你需要修改现有框架或优化现有应用,当你需要构建一个非Web网络服务程序,甚至当你需要从0到1编写一个网络服务器时,这些知识和经验还远远不够。

对于网络服务器程序来说,如何处理客户端连接是其前提和基础,能够稳定地处理大量的并发连接是服务器程序最重要的性能指标。由于操作系统任务处理和I/O模型的限制,我们可用的网络服务器模型是有限的。不同的服务模型可以应对不同的环境,具有不同的优点和缺点。

从操作系统任务处理的角度来看,有三种服务器模型可选:

  • 多进程模型
  • 多线程模型
  • (单线程)异步模型

以上模型的特点及其应用,可以参考一些Web服务器的设计,这里不再赘述。后续会有相关的笔记进行稍微深入的讨论。

使用多进程或者多线程,其差异主要在于进程和线程的特性,所从I/O模型角度描述时我们使用线程来描述。从I/O模型的角度来看,最常用的网络服务器模型有:

  • 单线程阻塞I/O模型
  • 多线程阻塞I/O模型
  • 单线程非阻塞I/O模型(包括非阻塞I/O,I/O复用)
  • 单线程异步I/O模型
Read more »

引言

阻塞式I/O模型下,一个线程只能处理一个流的I/O事件,为了同时高效处理多个流,只能采用多进程(fork)或者多线程的方式。

使用非阻塞I/O模型,可以通过循环遍历的方式同时处理多个流(忙轮询),但是如果所有流都没有数据,就会造成CPU的浪费(在一些服务端应用程序上便不高效)。

为了避免CPU的浪费,引入一个代理(select/poll,I/O复用),这个代理能够同时监视多个流的事件,当没有流事件发生时阻塞调用线程,释放CPU资源。

虽然select/poll能够同时监视多个流(描述符)事件,但是并不知道具体是哪一个流的事件发生,其实现是遍历所有的描述符,故称为无差别轮询。随着监视的描述符增多,性能会下降,并且有些系统实现里面单个进程监视的描述符数量是有最大限制的。

所以有了epoll(linux)/kqueue(freebsd)/iocp(windows)的出现,不同于非阻塞式I/O的忙轮询和I/O复用的无差别轮询,epoll只在某个流事件发生时通知调用者(内核实现)。

select

调用select告诉内核对那些描述符(就读、写或异常条件)感兴趣以及等待多长时间。感兴趣的描述符不局限于套接字,任何描述符都可以使用select来测试。以下为select的函数签名:

select 函数监视的文件描述符分3类,分别是readfds、writefds和exceptfds(都是值-结果参数)。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有异常)或者超时(timeout指定等待时间, 如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

timeout告知内核等待所指定的描述符中的任何一个就绪可花多长时间。其timeval结构用于指定这段时间的秒数和微妙数(虽然允许指定,实际上由于操作系统内核实现的原因这个微秒级的分辨率实际上并不很精确)。

Read more »

1 同步与异步

同步和异步关注点是消息通信机制 (synchronous communication/ asynchronous communication),是一种编程模型。

  • 在同步模型下,由消息的处理者主动去等待消息的提供者。也就是说在发出一个“调用”后,在没有得到结果之前,该“调用”就不返回,但是一旦调用返回,就得到了返回值。

  • 在异步模型下,由消息提供者来通知处理者可以提供需要的结果(通知机制由异步实现模型来保证)。也就是说在“调用”在发出后就立即返回了,不需要等待结果,换句话说,调用者不会立刻得到结果。“被调用者”有结果提供时会主动通知调用者(一般通过状态更新(如.NET APM)或者回调函数(如Node.js,Tornado)的形式)。

举个通俗的例子:

你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,“你稍等,我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

2 阻塞与非阻塞

阻塞和非阻塞关注的是线程在等待调用结果(消息、结果)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程做不了其他事情(被挂起或者自旋),调用线程只有在得到结果之后才会返回。

  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程(例如返回一个结果代理对象或者错误码)。

还是上面的例子

你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。

在这里阻塞与非阻塞与是否同步异步无关,跟老板通过什么方式回答你结果无关。

3 UNIX I/O模型

《UNIX网络编程卷I》第6章中定义如下几种I/O模型:

  • 阻塞式I/O模型(blocking I/O)
  • 非阻塞式I/O模型(noblocking I/O)
  • I/O复用(mutiplexing, select/poll/epoll)
  • 信号驱动I/O(SIGIO)
  • 异步I/O(AIO)

通常一个输入包括两个过程:

  1. 等待数据准备好;
  2. 从内核向进程空间复制数据。

就socket的输入操作来说,第一步就是等待所有的数据从网络中达到。当所有分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从缓冲区复制到应用的进程缓冲区。

Read more »

1 摘要

介绍python属性查找的相关基础知识,通过底层代码的分析,详细描述了python中对象属性的查找/赋值过程以及在object和type中对描述器的不同处理。并结合这些知识,简单探寻了一下描述器在python实现中的作用。

2 准备

在详解python属性查找之前,我们先来了解一些相关的概念,以便于理解整个查找过程。

2.1 描述器

描述器(descriptor )是一个有 “绑定行为” 的对象属性(object attribute),它的访问控制被描述器协议方法重写。描述器协议的方法为:

descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None

一个对象具有其中任一个方法就会成为描述器,从而在被当作对象属性时重写默认的查找行为。

如果一个对象同时定义了 __get__() __set__() ,它叫做资料描述器(data descriptor)。仅定义了 __get__() 的描述器叫非资料描述器(non-data descriptor, 常用于方法,当然其他用途也是可以的)。

资料描述器和非资料描述器的区别在于:相对于实例的字典的优先级。 如果实例字典中有与描述器同名的属性,如果描述器是资料描述器,优先使用资料描述器,如果是非资料描述器,优先使用字典中的属性。后面内容会具体介绍。

描述器是强大的,应用广泛的。描述器正是属性, 实例方法, 静态方法, 类方法和 super 背后的实现机制(详细的解释请点击这里)。描述器在 Python 自身中广泛使用,用以实现 Python2.2 中引入的新式类。描述器简化了底层的C代码,并为 Python 的日常编程提供了一套灵活的新工具。

2.2 Python 方法解析顺序

在支持多重继承的编程语言中,查找方法具体来自那个类时的基类搜索顺序通常被称为方法解析顺序(Method Resolution Order),简称 MRO。Python中查找属性也遵循 MRO。对于仅支持单重继承的语言,MRO非常简单,只需要按着继承链搜索就行。但是在支持多继承的语言中,情况就比较复杂。“菱形继承” 的问题在 python 引入 “新式类” 后更加突出。

Python的历史版本中先后出现三种不同的MRO:经典方式、Python2.2 新式算法、Python2.3 新式算法(也称作C3)。Python 3中只保留了最后一种,即C3算法。C3算法最早被提出是用于Lisp的,在 python 中采用主要是为了解决之前之前基于深度优先搜索算法不满足本地优先级和单调性的问题:

  • 本地优先级: 指声明时父类的顺序,比如C(A,B),在访问C类对象属性时,根据声明顺序应该先查找A类再查找B类。
  • 单调性: 指如果在解析C类继承顺序时,A类排在B类的前面,那么在C类所有的子类里也必须满足这个顺序。

从 Python2.2 开始为解决引入新类所带来的方法解析顺序,采用的方案是在类定义时就计算出它的MRO ,并存储为该类对象的一个属性 __mro__ 。然后在查找属性时按照 __mro__ ** 依次搜索基类。

2.3 metaclass

在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。Python中的类还远不止如此。在python中类同样也是一种对象,只要你使用关键字class,解释器在执行的时候就会创建一个对象。例如:

1
class ObjectCreator(object):pass

上面的代码会在python解释器执行的时候生成一个名叫 ObjectCreator 的对象。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。 创建 “类” 的 “类” 就是元类(metaclass),python默认的内建元类是type。

1
2
3
4
>>> object.__class__
<type 'type'>
>>> type(object)
<type 'type'>

在 python 中通过 type(obj) ,或者 obj.__class__ 来获取对象的类型(type)。这里有个特别的 type 的 type 是 type。

1
2
3
4
5
6
7
8
>>> type(object)
<type 'type'>
>>> type(type)
<type 'type'>
>>> type.__class__
<type 'type'>
>>> object.__class__
<type 'type'>

换句话说,元类(metaclass)的实例化是类(class),类(class)的实例化是类实例对象(object)。

注:type 这个类型(内建函数)根据传入的参数不同具有两个不同的功能,这可能是出于向后兼容的原因。

  • type(object) -> the object’s type,返回的是 object 实例的类型。
  • type(name, bases, dict) -> a new type,返回的是一个新类型。
Read more »