浅显理解 Python 闭包

闭包这个概念在 JavaScript 中讨论和使用得比较多,不过在 Python 中却不是那么显而易见,之所以说“不是那么”,是因为即使用到了,也没用注意到而已,比如定义一个 Decorator 时,就已经用到闭包了。网上对闭包的各种解释,感觉非常晦涩,在这里谈谈我的浅显认识:要形成闭包,首先得有一个嵌套的函数,即函数中定义了另一个函数,闭包则是一个集合,它包括了外部函数的局部变量,这些局部变量在外部函数返回后也继续存在,并能被内部函数引用。

举个例子

这是个经常使用到的例子,定义一个函数 generate_power_func,它返回另一个函数,现在闭包形成的条件已经达到。

1
2
3
4
5
6
def generate_power_func(n):
print "id(n): %X" % id(n)
def nth_power(x):
return x**n
print "id(nth_power): %X" % id(nth_power)
return nth_power

对于内部函数 nth_power,它能引用到外部函数的局部变量 n,而且即使 generate_power_func 已经返回。把这种现象就称为闭包。具体使用一下。

1
2
3
4
5
>>> raised_to_4 = generate_power_func(4)
id(n): 246F770
id(nth_power): 2C090C8
>>> repr(raised_to_4)
'<function nth_power at 0x2c090c8>'

从结果可以看出,当 generate_power_func(4) 执行后, 创建和返回了 nth_power 这个函数对象,内存地址是 0x2C090C8,并且发现 raised_to_4 和它的内存地址相同,即 raised_to_4 只是这个函数对象的一个引用。先在全局命名空间中删除 generate_power_func,再试试会出现什么结果。

1
2
3
>>> del generate_power_func
>>> raised_to_4(2)
16

啊哈,居然没出现错误, nth_power 是怎么知道 n 的值是 4,而且现在 generate_power_func 甚至都不在这个命名空间了。对,这就是闭包的作用,外部函数的局部变量可以被内部函数引用,即使外部函数已经返回了。

closure 属性和 cell 对象

现在知道闭包是怎么一回事了,那就到看看闭包到底是怎么回事的时候了。Python 中函数也是对象,所以函数也有很多属性,和闭包相关的就是 __closure__ 属性。__closure__ 属性定义的是一个包含 cell 对象的元组,其中元组中的每一个 cell 对象用来保存作用域中变量的值。

1
2
3
4
5
6
>>> raised_to_4.__closure__
(<cell at 0x2bf4ec0: int object at 0x246f770>,)
>>> type(raised_to_4.__closure__[0])
<type 'cell'>
>>> raised_to_4.__closure__[0].cell_contents
4

就如刚才所说,在 raised_to_4__closure__ 属性中有外部函数变量 n 的引用,通过内存地址可以发现,引用的都是同一个 n。如果没用形成闭包,则 __closure__ 属性为 None。对于 Python 具体是如何实现闭包的,可以查看 Python闭包详解,它通过分析 Python 字节码来讲述闭包的实现。

最后总结

闭包特性有着非常多的作用,不过都是需要时才会不经意的用上,不要像使用设计模式一样去硬套这些法则。这篇文章按照自己的理解翻译至 Python Closures Explained,可能和原文有些不同之处,如有疑惑,请查看原文。附上一些参考资料。

  1. 闭包的概念、形式与应用: 可以从其中了解闭包的应用
  2. Python闭包详解:从字节码出发了解 Python 闭包的实现机制
  3. 理解Javascript的闭包: 从 Javascript 的闭包中了解一些闭包特性,可以和 Python 作下对比

原文地址:https://serholiu.com/python-closures

利用图片指纹检测高相似度图片

大概五年前吧,我那时还在为一家约会网站做开发工作。他们是早期创业公司,但他们也开始拥有了一些稳定用户量。不像其他约会网站,这家公司向来以洁身自好为主要市场形象。它不是一个供你鬼混的网站——是让你能找到忠实伴侣的地方。

由于投入了数以百万计的风险资本(在US大萧条之前),他们关于真爱并找寻灵魂伴侣的在线广告势如破竹。Forbes(福布斯,美国著名财经杂志)采访了他们。全国性电视节目也对他们进行了专访。早期的成功促成了事业起步时让人垂涎的指数级增长现象——他们的用户数量以每月加倍的速度增长。对他们而言,一切都似乎顺风顺水。

但他们有一个严重的问题——色情问题

该约会网站的用户中会有一些人上传色情图片,然后设置为其个人头像。这种行为破坏了很多其他用户的体验——导致很多用户取消了会员。

可能对于现在的一些约会网站随处可见几张色情图片也许并不能称之为是问题。或者可以说是习以为常甚至有些期待,只是一个被接受然后被无视的在线约会的副产品。

然而,这样的行为既不应该被接受也应该被忽视。

别忘了,这次创业可是将自己定位在优秀的约会天堂,免于用户受到困扰其他约会网站的污秽和垃圾的烦扰。简而言之,他们拥有很实在的以风险资本作为背后支撑的名声,而这也正是他们需要保持的风格。

该约会网站为了能迅速阻止色情图片的爆发可以说是不顾一切了。他们雇佣了图片论坛版主团队,真是不做其他事只是每天盯着监管页面8个小时以上,然后移除任何被上传到社交网络的色情图片。

毫不夸张的说,他们投入了数万美元(更不用说数不清的人工小时)来解决这个问题,然而也仅仅只是缓解,控制情况不变严重而不是在源头上阻止。

色情图片的爆发在2009年的七月达到了临界水平。8个月来第一次用户量没能翻倍(甚至已经开始减少了)。更糟糕的是,投资者声称若该公司不能解决这个问题将会撤资。事实上,污秽的潮汐早已开始冲击这座象牙塔了,将它推翻流入大海也不过是时间问题。

正在这个约会网站巨头快要撑不住时,我提出了一个更鲁棒的长期解决方案:如果我们使用图片指纹来与色情图片的爆发斗争呢?

你看,每张图片都有一个指纹。正如人的指纹可以识别人,图片的指纹能识别图片。

这促使了一个三阶段算法的实现:

  1. 为不雅图片建立指纹,然后将图片指纹存储在一个数据库中。

  2. 当一个用户上传一份新的头像时,我们会将它与数据库中的图片指纹对比。如果上传图片的指纹与数据库任意一个不雅图片指纹相符,我们就阻止用户将该图片设置为个人头像。

  3. 当图片监管人标记新的色情图片时,这些图片也被赋予指纹并存入我们的数据库,建立一个能用于阻止非法上传且不断进化的数据库。

我们的方法,尽管不十分完美,但是也卓有成效。慢慢地,色情图片爆发的情况有所减慢。它永远不会消失——但这个算法让我们成功将非法上传的数量减少了**80%**以上。

这也挽回了投资者的心。他们继续为我们提供资金支持——直到萧条到来,我们都失业了。

回顾过去时,我不禁笑了。我的工作并没持续太久。这个公司也没有坚持太久。甚至还有几个投资者卷铺盖走人了。

但有一样确实存活了下来。提取图片指纹的算法。几年之后,我把这个算法的基本内容分享出来,期望你们可以将它应用到你们自己的项目中。

但最大的问题是,我们怎么才能建立图片指纹呢?

继续读下去一探究竟吧。

即将要做的事情

我们打算用图片指纹进行相似图片的检测。这种技术通常被称为“感知图像hash”或是简单的“图片hash”。

什么是图片指纹/图片哈希

图片hash是检测一张图片的内容然后根据检测的内容为图片建立一个唯一值的过程。

比如,看看本文最上面的那张图片。给定一张图片作为输入,应用一个hash函数,然后基于图片的视觉计算出一个图片hash。相似的图片也应当有相似的hash值。图片hash算法的应用使得相似图片的检测变得相当简单了。

特别地,我们将会使用“差别Hash”或简单的DHash算法计算图片指纹。简单来说,DHash算法着眼于两个相邻像素之间的差值。然后,基于这样的差值,就建立起一个hash值了。

为什么不使用md5,sha-1等算法?

不幸的是,我们不能在实现中使用加密hash算法。由于加密hash算法的本质使然,输入文件中非常微小的差别也能造成差异极大的hash值。而在图片指纹的案例中,我们实际上希望相似的输入可以有相似的hash输出值。

图片指纹可以用在哪里?

正如我上面举的例子,你可以使用图片指纹来维护一个保存不雅图片的数据库——当用户尝试上传类似图片时可以发出警告。

你可以建立一个图片的逆向搜索引擎,比如TinEye,它可以记录图片以及它们出现的相关网页。

你还可以使用图片指纹帮助管理你个人的照片收集。假设你有一个硬盘,上面有你照片库的一些局部备份,但需要一个方法删除局部备份,一张图片仅保留一份唯一的备份——图片指纹可以帮你做到。

简单来说,你几乎可以将图片指纹/哈希用于任何需要你检测图片的相似副本的场景中。

需要的库有哪些?

为了建立图片指纹方案,我们打算使用三个主要的Python包:

你可以使用下列命令一键安装所需要的必备库:

1
$ pip install pillow imagehash

第一步:为一个图片集建立指纹

第一步就是为我们的图片集建立指纹。

也许你会问,但我们不会,我们不会使用那些我为那家约会网站工作时的色情图片。相反,我创建了一个可供使用的人工数据集。

对计算机视觉的研究人员而言,数据集 CALTECH-101是一个传奇般的存在。它包含来自101个不同分类中的至少7500张图片,内容分别有人物,摩托车和飞机。

从这7500多张图片中,我随机的挑选了17张。

然后,从这17张随机挑选的图片中,以几个百分点的比例随机放大/缩小并创建N张新图片。这里我们的目标是找到这些近似副本的图片——有点大海捞针的感觉。

你也想创建一个类似的数据集用于工作吗?那就下载 CALTECH-101数据集,抽取大概17张图片即可,然后运行repo下的脚本文件gather.py。

回归正题,这些图片除了宽度和高度,其他各方面都是一样的。而且因为他们没有相同的形状,我们不能依赖简单的md5校验和。最重要的是,有相似内容的图片可能有完全不相同的md5哈希。然而,采取图片哈希,相似内容的图片也有相似的哈希指纹。

所以赶紧开始写代码为数据集建立指纹吧。创建一个新文件,命名为index.py,然后开始工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# import the necessary packages
from PIL import Image
import imagehash
import argparse
import shelve
import glob

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required = True,
help = "path to input dataset of images")
ap.add_argument("-s", "--shelve", required = True,
help = "output shelve database")
args = vars(ap.parse_args())

要做的第一件事就是引入我们需要的包。我们将使用PIL或Pillow中的Image类载入硬盘上的图片。这个imagehash库可以被用于构建哈希算法。

Argparse库用于解析命令行参数,shelve库用作一个存储在硬盘上的简单键值对数据库(Python字典)。glob库能很容易的获取图片路径。

然后传递命令行参数。第一个,—dataset是输入图片库的路径。第二个,—shelve是shelve数据库的输出路径。

下一步,打开shelve数据库以写数据。这个db数据库存储图片哈希。更多的如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# loop over the image dataset
for imagePath in glob.glob(args["dataset"] + "/*.jpg"):
# load the image and compute the difference hash
image = Image.open(imagePath)
h = str(imagehash.dhash(image))

# extract the filename from the path and update the database
# using the hash as the key and the filename append to the
# list of values
filename = imagePath[imagePath.rfind("/") + 1:]
db[h] = db.get(h, []) + [filename]

# close the shelf database
db.close()

以上就是大部分工作的内容了。开始循环从硬盘读取图片,创建图片指纹并存入数据库。

现在,来看看整个范例中最重要的两行代码:

1
2
filename = imagePath[imagePath.rfind("/") + 1:]
db[h] = db.get(h, []) + [filename]

正如本文早些时候提到的,有相同指纹的图片被认为是一样的。

因此,如果我们的目标是找到近似图片,那就需要维护一个有相同指纹值的图片列表。

而这也正是这几行代码做的事情。

前一个代码段提取了图片的文件名。而后一个代码片段维护了一个有相同指纹值的图片列表。

为了从我们的数据库中提取图片指纹并建立哈希数据库,运行下列命令:

1
$ python index.py —dataset images —shelve db.shelve

这个脚本会运行几秒钟,完成后,就会出现一个名为db.shelve的文件,包含了图片指纹和文件名的键值对。

这个基本算法正是几年前我为这家约会创业公司工作时使用的算法。我们获得了一个不雅图片集,为其中的每张图片构建一个图片指纹并将其存入数据库。当来一张新图片时,我只需简单地计算它的哈希值,检测数据库查看是否上传图片已被标识为非法内容。

下一步中,我将展示实际如何执行查询,判定数据库中是否存在与所给图片具有相同哈希值的图片。

第二步:查询数据集

既然已经建立了一个图片指纹的数据库,那么现在就该搜索我们的数据集了。

打开一个新文件,命名为search.py,然后开始写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# import the necessary packages
from PIL import Image
import imagehash
import argparse
import shelve

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required = True,
help = "path to dataset of images")
ap.add_argument("-s", "--shelve", required = True,
help = "output shelve database")
ap.add_argument("-q", "--query", required = True,
help = "path to the query image")
args = vars(ap.parse_args())

我们需要再一次导入相关的包。然后转换命令行参数。需要三个选项,—dataset初始图片集的路径,—shelve,保存键值对的数据库的路径,—query,查询/上传图片的路径。我们的目标是对于每个查询图片,判定数据库中是否已经存在。

现在,写代码执行实际的查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# open the shelve database
db = shelve.open(args["shelve"])

# load the query image, compute the difference image hash, and
# and grab the images from the database that have the same hash
# value
query = Image.open(args["query"])
h = str(imagehash.dhash(query))
filenames = db[h]
print "Found %d images" % (len(filenames))

# loop over the images
for filename in filenames:
image = Image.open(args["dataset"] + "/" + filename)
image.show()

# close the shelve database
db.close()

首先打开数据库,然后载入硬盘上的图片,计算图片的指纹,找到具有相同指纹的所有图片。

如果有图片具有相同的哈希值,会遍历这些图片并展示在屏幕上。

这段代码使我们仅仅使用指纹值就能判定图片是否已在数据库中存在。

结果

正如本文早些时候提到的,我从CALTECH-101数据集的7500多张图片中随机选取17张,然后通过任意缩放一部分点产生N张新的图片。

这些图片在尺寸上仅仅是少数像素不同—但也是因为这一点我们不能依赖于文件的md5哈希(这一点已在“优化算法”部分进行了详尽的描述)。然而,我们可以使用图片哈希找到近似图片。

打开你的终端并执行下述命令:

1
$ python search.py —dataset images —shelve db.shelve —query images/84eba74d-38ae-4bf6-b8bd-79ffa1dad23a.jpg

如果一切顺利你就可以看到下述结果:

左边是输入图片。载入这张图片,计算它的图片指纹,在数据库中搜索指纹查看是否存在有相同指纹的图片。

当然——正如右边所示,我们的数据集中有其他两张指纹相同的图片。尽管从截图中还不能十分明显的看出,这些图片,虽然有完全相同的视觉内容,也不是完全相同!这三张图片的高度宽度各不相同。

尝试一下另外一个输入图片:

1
$ python search.py —dataset images —shelve db.shelve —query images/9d355a22-3d59-465e-ad14-138a4e3880bc.jpg

下面是结果:

左边仍然是我们的输入图片。正如右边展示的,我们的图片指纹算法能够找出具有相同指纹的三张完全相同的图片。

最后一个例子:

1
$ python search.py —dataset images —shelve db.shelve —query images/5134e0c2-34d3-40b6-9473-98de8be16c67.jpg

这一次左边的输入图片是一个摩托车。拿到这张摩托车图片,计算它的图片指纹,然后在指纹数据库中查找该指纹。正如我们在右边看到的,我们也能判断出数据库中有三张图片具有相同指纹。

优化算法

有很多可以优化本算法的方法——但最关键性的是要考虑到相似但不相同的哈希。

比如,本文中的图片仅仅是一小部分点重组了(依比例增大或减小)。如果一张图片以一个较大的因素调整大小,或者纵横比被改变了,对应的哈希就会不同了。

然而,这些图片应该仍然是相似的。

为了找到相似但不相同的图片,我们需要计算汉明距离(Hamming distance).汉明距离被用于计算一个哈希中的不同位数。因此,哈希中只有一位不同的两张图片自然比有10位不同的图片更相似。

然而,我们遇到了第二个问题——算法的可扩展性。

考虑一下:我们有一张输入图片,又被要求在数据库中找到所有相似图片。然后我们必须计算输入图片和数据库中的每一张图片之间的汉明距离。

随着数据库规模的增长,和数据库比对的时间也随着延长。最终,我们的哈希数据库会达到一个线性比对已经不实际的规模。

解决办法,虽然已超出本文范围,就是利用 K-d treesVP trees 将搜索问题的复杂度从线性减小到次线性。

总结

本文中我们学会了如何构建和使用图片哈希来完成相似图片的检测。这些图片哈希是使用图片的视觉内容构建的。

正如一个指纹可以识别一个人,图片哈希也能唯一的识别一张图片。

使用图片指纹的知识,我们建立了一个仅使用图片哈希就能找到和识别具有相似内容的图片的系统。

然后我们又演示了图片哈希是如何应用于快速找到有相似内容的图片。

repo 目录下下载代码。

周末学计算机视觉

如果你很喜欢本文而且还想了解更多与计算机视觉,图片处理以及建立图片搜索引擎相关的东西,那就去我的博客吧,地址是 PyImageSearch.com

祝福!


译文地址:http://www.pyimagesearch.com/
原文地址:https://realpython.com/blog/python/fingerprinting-images-for-near-duplicate-detection/

非阻塞服务器需要注意的主要问题

原文:http://amix.dk/blog/post/19581#The-main-issue-with-non-blocking-servers
译文:http://blog.csdn.net/chong232/article/details/6153161

非阻塞服务器有一个严重的问题,一些人甚至在没解决这个问题的背景下就开发自己的应用框架(比如 Python 的 Tornado

当你使用非阻塞服务器的时候,你会获得出色的性能并且不需要担心可扩展性,然而同时你需要意识到一个问题:你的IO调用、网络系统调用也都是非阻塞的吗?很多人忽略了,他们使用的非阻塞服务器,其实是构建在阻塞库之上的。

在这篇文章里,我将深入对比多线程的服务器与非阻塞的服务器分别是如何工作的,以及你之所以需要在”使用的服务器”与”使用的库”在阻塞模式上保持一致的原因。

Read more

对比Ruby和Python的垃圾回收(2):代式垃圾回收机制

上周,我根据之前在RuPy上做的一个名为“Visualizing Garbage Collection in Ruby and Python.”的报告写了这篇文章的上半部分。在上篇中,我解释了标准Ruby(也被称为Matz的Ruby解释器或是MRI)是如何使用名为标记回收(Mark and Sweep)的垃圾回收算法,这个算法是为1960年原版本的Lisp所开发。同样,我也介绍了Python使用一种有53年历史的GC算法,这种算法的思路非常不同,称之为引用计数。

事实证明,Python在引用计数之外,还用了另一个名为Generational Garbage Collection的算法。这意味着Python的垃圾回收器用不同的方式对待新创建的以及旧有的对象。并且在即将到来的2.1版本的MRI Ruby中也首次引入了Generational Garbage Collection 的垃圾回收机制(在另两个Ruby的实现:JRuby和Rubinius中,已经使用这种GC机制很多年了,我将在下周的RubyConf大会上将它是如何在这两种Ruby实现中工作的)。

当然,这句话“用不同的方式对待新创建的以及旧有的对象”是有点模糊不清,比如如何定义新、旧对象?又比如对于Ruby和Python来说具体是如何采取不同的对待方式?今天,我们就来谈谈这两种语言GC机制的运行原理,回答上边那些疑问。但是在我们开始谈论Generational GC之前,我们先要花点时间谈论下Python的引用计数算法的一个严重的理论问题。

Python中的循环数据结构以及引用计数

通过上篇,我们知道在Python中,每个对象都保存了一个称为引用计数的整数值,来追踪到底有多少引用指向了这个对象。无论何时,如果我们程序中的一个变量或其他对象引用了目标对象,Python将会增加这个计数值,而当程序停止使用这个对象,则Python会减少这个计数值。一旦计数值被减到零,Python将会释放这个对象以及回收相关内存空间。

从六十年代开始,计算机科学界就面临了一个严重的理论问题,那就是针对引用计数这种算法来说,如果一个数据结构引用了它自身,即如果这个数据结构是一个循环数据结构,那么某些引用计数值是肯定无法变成零的。为了更好地理解这个问题,让我们举个例子。下面的代码展示了一些上周我们所用到的节点类:

我们有一个构造器(在Python中叫做 init ),在一个实例变量中存储一个单独的属性。在类定义之后我们创建两个节点,ABC以及DEF,在图中为左边的矩形框。两个节点的引用计数都被初始化为1,因为各有两个引用指向各个节点(n1和n2)。

现在,让我们在节点中定义两个附加的属性,next以及prev:

跟Ruby不同的是,Python中你可以在代码运行的时候动态定义实例变量或对象属性。这看起来似乎有点像Ruby缺失了某些有趣的魔法。(声明下我不是一个Python程序员,所以可能会存在一些命名方面的错误)。我们设置 n1.next 指向 n2,同时设置 n2.prev 指回 n1。现在,我们的两个节点使用循环引用的方式构成了一个双端链表。同时请注意到 ABC 以及 DEF 的引用计数值已经增加到了2。这里有两个指针指向了每个节点:首先是 n1 以及 n2,其次就是 next 以及 prev。

现在,假定我们的程序不再使用这两个节点了,我们将 n1 和 n2 都设置为null(Python中是None)。

好了,Python会像往常一样将每个节点的引用计数减少到1。

在Python中的零代(Generation Zero)

请注意在以上刚刚说到的例子中,我们以一个不是很常见的情况结尾:我们有一个“孤岛”或是一组未使用的、互相指向的对象,但是谁都没有外部引用。换句话说,我们的程序不再使用这些节点对象了,所以我们希望Python的垃圾回收机制能够足够智能去释放这些对象并回收它们占用的内存空间。但是这不可能,因为所有的引用计数都是1而不是0。Python的引用计数算法不能够处理互相指向自己的对象。

当然,上边举的是一个故意设计的例子,但是你的代码也许会在不经意间包含循环引用并且你并未意识到。事实上,当你的Python程序运行的时候它将会建立一定数量的“浮点数垃圾”,Python的GC不能够处理未使用的对象因为应用计数值不会到零。

这就是为什么Python要引入Generational GC算法的原因!正如Ruby使用一个链表(free list)来持续追踪未使用的、自由的对象一样,Python使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python的内部C代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表:

从上边可以看到当我们创建ABC节点的时候,Python将其加入零代链表。请注意到这并不是一个真正的列表,并不能直接在你的代码中访问,事实上这个链表是一个完全内部的Python运行时。

相似的,当我们创建DEF节点的时候,Python将其加入同样的链表:

现在零代包含了两个节点对象。(他还将包含Python创建的每个其他值,与一些Python自己使用的内部值。)

检测循环引用

随后,Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。

为了便于理解,来看一个例子:

从上面可以看到 ABC 和 DEF 节点包含的引用数为1.有三个其他的对象同时存在于零代链表中,蓝色的箭头指示了有一些对象正在被零代链表之外的其他对象所引用。(接下来我们会看到,Python中同时存在另外两个分别被称为一代和二代的链表)。这些对象有着更高的引用计数因为它们正在被其他指针所指向着。

接下来你会看到Python的GC是如何处理零代链表的。

通过识别内部引用,Python能够减少许多零代链表对象的引用计数。在上图的第一行中你能够看见ABC和DEF的引用计数已经变为零了,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。

从某种意义上说,Python的GC算法类似于Ruby所用的标记回收算法。周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的,这正类似于Ruby的标记过程。

Python中的GC阈值

Python什么时候会进行这个标记过程?随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。

当然,事实并非如此。因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。

随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。

通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。

弱代假说

来看看代垃圾回收算法的核心行为:垃圾回收器会更频繁的处理新对象。一个新的对象即是你的程序刚刚创建的,而一个来的对象则是经过了几个时间周期之后仍然存在的对象。Python会在当一个对象从零代移动到一代,或是从一代移动到二代的过程中提升(promote)这个对象。

为什么要这么做?这种算法的根源来自于弱代假说(weak generational hypothesis)。这个假说由两个观点构成:首先是年亲的对象通常死得也快,而老对象则很有可能存活更长的时间。

假定现在我用Python或是Ruby创建一个新对象:

根据假说,我的代码很可能仅仅会使用ABC很短的时间。这个对象也许仅仅只是一个方法中的中间结果,并且随着方法的返回这个对象就将变成垃圾了。大部分的新对象都是如此般地很快变成垃圾。然而,偶尔程序会创建一些很重要的,存活时间比较长的对象-例如web应用中的session变量或是配置项。

通过频繁的处理零代链表中的新对象,Python的垃圾收集器将把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足阈值的条件,收集器才回去处理那些老变量。

回到Ruby的自由链

即将到来的Ruby 2.1版本将会首次使用基于代的垃圾回收算法!(请注意的是,其他的Ruby实现,例如JRuby和Rubinius已经使用这个算法许多年了)。让我们回到上篇博文中提到的自由链的图来看看它到底是怎么工作的。

请回忆当自由链使用完之后,Ruby会标记你的程序仍然在使用的对象。

从这张图上我们可以看到有三个活跃的对象,因为指针n1、n2、n3仍然指向着它们。剩下的用白色矩形表示的对象即是垃圾。(当然,实际情况会复杂得多,自由链可能会包含上千个对象,并且有复杂的引用指向关系,这里的简图只是帮助我们了解Ruby的GC机制背后的简单原理,而不会将我们陷入细节之中)

同样,我们说过Ruby会将垃圾对象移动回自由链中,这样的话它们就能在程序申请新对象的时候被循环使用了。

Ruby2.1基于代的GC机制

从2.1版本开始,Ruby的GC代码增加了一些附加步骤:它将留下来的活跃对象晋升(promote)到成熟代(mature generation)中。(在MRI的C源码中使用了old这个词而不是mature),接下来的图展示了两个Ruby2.1对象代的概念图:

在左边是一个跟自由链不相同的场景,我们可以看到垃圾对象是用白色表示的,剩下的是灰色的活跃对象。灰色的对象刚刚被标记。

一旦“标记清除”过程结束,Ruby2.1将剩下的标记对象移动到成熟区:

跟Python中使用三代来划分不同,Ruby2.1只用了两代,左边是年轻的新一代对象,而右边是成熟代的老对象。一旦Ruby2.1标记了对象一次,它就会被认为是成熟的。Ruby会打赌剩下的活跃对象在相当长的一段时间内不会很快变成垃圾对象。

重要提醒:Ruby2.1并不会真的在内存中拷贝对象,这些代表不同代的区域并不是由不同的物理内存区域构成。(有一些别的编程语言的GC实现或是Ruby的其他实现,可能会在对象晋升的时候采取拷贝的操作)。Ruby2.1的内部实现不会将在标记&清除过程中预先标记的对象包含在内。一旦一个对象已经被标记过一次了,那么那将不会被包含在接下来的标记清除过程中。

现在,假定你的Ruby程序接着运行着,创造了更多新的,更年轻的对象。则GC的过程将会在新的一代中出现,如图:

如同Python那样,Ruby的垃圾收集器将大部分精力都放在新一代的对象之上。它仅仅会将自上一次GC过程发生后创建的新的、年轻的对象包含在接下来的标记清除过程中。这是因为很多新对象很可能马上就会变成垃圾(白色标记)。Ruby不会重复标记右边的成熟对象。因为他们已经在一次GC过程中存活下来了,在相当长的一段时间内不会很快变成垃圾。因为只需要标记新对象,所以Ruby 的GC能够运行得更快。它完全跳过了成熟对象,减少了代码等待GC完成的时间。

偶然的Ruby会运行一次“全局回收”,重标记(re-marking)并重清除(re-sweeping),这次包括所有的成熟对象。Ruby通过监控成熟对象的数目来确定何时运行全局回收。当成熟对象的数目双倍于上次全局回收的数目时,Ruby会清理所有的标记并且将所有的对象都视为新对象。

白障

这个算法的一个重要挑战是值得深入解释的:假定你的代码创建了一个新的年轻的对象,并且将其作为一个已存在的成熟对象的子嗣加入。举个例子,这种情况将会发生在,当你往一个已经存在了很长时间的数组中增加了一个新值的时候:

让我们来看看图,左边的是新对象,而成熟的对象在右边。在左边标记过程已经识别出了5个新的对象目前仍然是活跃的(灰色)。但有两个对象已经变成垃圾了(白色)。但是如何处理正中间这个新建对象?这是刚刚那个问题提到的对象,它是垃圾还是活跃对象呢?

当然它是活跃对象了,因为有一个从右边成熟对象的引用指向它。但是我们前面说过已经被标记的成熟对象是不会被包含在标记清除过程中的(一直到全局回收)。这意味着类似这种的新建对象会被错误的认为是垃圾而被释放,从而造成数据丢失。

Ruby2.1 通过监视成熟对象,观察你的代码是否会添加一个从它们到新建对象的引用来克服这个问题。Ruby2.1 使用了一个名为白障(white barriers)的老式GC技术来监视成熟对象的变化 – 无论任意时刻当你添加了从一个对象指向另一个对象的引用时(无论是新建或是修改一个对象),白障就会被触发。白障将会检测是否源对象是一个成熟对象,如果是的话则将这个成熟对象添加到一个特殊的列表中。随后,Ruby2.1会将这些满足条件的成熟对象包括到下一次标记清除的范围内,以防止新建对象被错误的标记为垃圾而清除。

Ruby2.1 的白障实现相当复杂,主要是因为已有的C扩展并未包含这部分功能。Koichi Sasada以及Ruby的核心团队使用了一个比较巧妙的方案来解决这个问题。如果想了解更多的内容,请阅读这些相关材料:Koichi在 EuRuKo 2013 上的演讲 Koichi’s fascinating presentation

站在巨人的肩膀上

乍眼一看,Ruby和Python的GC实现是截然不同的,Ruby使用John MaCarthy的原生“标记并清除”算法,而Python使用引用计数。但是仔细看来,可以发现Python使用了些许标记清除的思想来处理循环引用,而两者同时以相似的方式使用基于代的垃圾回收算法。Python划分了三代,而Ruby只有两代。

这种相似性应该不会让人感到意外。两种编程语言都使用了几十年前的计算机科学研究成果来进行设计,这些成果早在语言成型之前就已经被做出来了。我比较惊异的是当你掀开不同编程语言的表面而深入底层,你总能够发现一些相似的基础理念和算法。现代编程语言应该感激那些六七十年代由麦卡锡等计算机先贤所作出的计算机科学开创性研究。


原文:Pat Shaughnessy Generational GC in Python and Ruby
译文:http://blog.jobbole.com/73300/

对比Ruby和Python的垃圾回收(1)

注:这篇文章基于我在布达佩斯的RuPy大会上所作的演讲。我觉得与其直接将幻灯片发布出来,不如在我还有印象的时候将它写成博客来的更有意义。同样,我会在将来发布RuPy大会的视频链接。我计划将在RubyConf大会上发表类似的演讲,除了有关于Python的部分,并且将对比MRI、JRuby和Rubinius的垃圾回收器是怎样工作的。

如果想要对Ruby垃圾回收器以及内部原理有更加深入的了解,你可以在我即将出版的新书《Ruby Under a Microscope》中找到答案。

如果算法和业务逻辑是一个人的大脑,那么垃圾回收机制是人体的哪个器官呢?

在”Ruby Python”大会上,我想对比Ruby和Python内部的垃圾回收机制是一件很有意思的事情。在开始之前,我们为什么要讨论垃圾回收机制呢?毕竟这是一个最迷人的,最令人激动的主题,不是吗?你们有多少人对垃圾回收机制感到兴奋?(许多大会参与者竟然举起了双手!)

最近,在Ruby社区中有一篇帖子,关于怎样通过修改Ruby GC的设置来提高单元测试的速度。这棒极了!通过减少GC垃圾回收的处理来提高测试的速度,这是一件好事,但是不怎的,GC不会真正的让我感到兴奋。就如咋一看就感觉令人厌烦,枯燥的技术帖子。

事实上,垃圾回收是一个令人着迷的主题:垃圾回收算法不仅是计算机科学历史一个重要的部分,更是前沿研究的一个主题。例如,MRI Ruby解释器使用的”Mark Sweep”算法已经超过了50年的历史,与此同时,在Rubinius解释器中使用的一种垃圾回收算法,是在Ruby中的另一种实现方式,这种算法仅仅是在2008才被研究出来。

然而,”垃圾回收”的这个名称非常不恰当。

应用程序的心脏

垃圾回收系统要做的不仅仅是”回收垃圾”。事实上,它主要完成三个重要任务:

  • 为新的对象分配内存
  • 标记垃圾对象
  • 回收垃圾对象占用的内存

想象你的应用程序是一个人的身体:所有你写的优雅代码,业务逻辑,算法,将会成为你的应用程序的大脑或智能。与此类似的,你认为垃圾回收器会成为身体的哪一个部分呢?(我从大会的听众中得到了很多有趣的答案:肾,白细胞)

我认为垃圾回收器是一个应用的心脏。正如心脏为身体的其他部分提供血液和养料一样,垃圾回收器提供内存和对象供程序使用。如果你的心脏停跳,你将活不了几秒。如果垃圾回收器停止运行或者变慢,就像动脉阻塞一样,你的程序将变的慢下来,最后死掉!

一个简单的例子

通过例子来验证理论是一种很好的方式。这里有一个简单的类,用Python和Ruby写成,我们可以将它们作为一个简单的例子:

于此同时,两种代码如此相似,让我感到非常吃惊:Python和Ruby在表达相同的语义时几乎没有差别。但是,两种语言的内部实现方式是否相同呢?

空闲对象链表

在上面的代码中,当我们调用了Node.new(1)之后,ruby将会做什么?也就是说,Ruby怎样创建一个新的对象?

令人惊讶的是,Ruby做的事情非常少!事实上,在代码运行之前,Ruby解释器会提前创建成千上万的对象放置到一个链表中,这个链表被称为”空闲对象链表”(free list)。空闲对象链表(free list)在概念上看起来像下面的样子:

每一个白色方块可以想象成一个预创建的,没有使用的Ruby对象。当我们调用Node.new,Ruby简单的使用一个对象,并且将它的引用返回给我们:

在上图中,左边的灰色方块代表一个活跃的Ruby对象,已被使用,而其余的白色方块代码没有使用的对象。(注意:当然,图中是一种简化的实现版本。事实上,Ruby将会使用另外一个对象保存字符串”ABC”,使用第三个对象保存Node的定义,以及其他的对象保存代码处理过的抽象语法数”AST”。)

如果我们再次调用Node.new,Ruby仅仅返回另外一个对象的引用。

约翰麦卡锡在1960年在Lisp中首次实现了垃圾回收机制

这种使用预创建对象链表的简单算法发明于50多年前,它的作者是传说中的计算机科学家,约翰·麦卡锡,正是他实现了最初的Lisp解释器。Lisp不仅是第一个函数式编程语言,并且包含了计算机科学中许多突破性的进展。其中之一便是通过垃圾回收机制自动管理内存。

标准版Ruby,也就是”Matz’s Ruby Interpreter”(MRI),使用了一种类似于约翰麦卡锡在1960年实现的Lisp的垃圾回收算法。就像Lisp一样,Ruby会预先创建对象并且在你创建对象或值的时候返回对象的引用。

在Python中分配对象内存

从上面我们可以看出,Ruby会预先创建对象,并且保存在空闲对象链表(free list)中。那么Python呢?

当然Python内部也会由于各种原因使用空闲对象链表(它使用链表循环确定对象),Python为对象和值分配内存的方式常常不同于Ruby。

假设我们创建一个Node对象使用Python:

Python不同于Ruby,当你创建对象的时候,Python会立即向操作系统申请分配内存。(Python 事实上实现了自己的内存分配系统,它在操作系统内存堆上提供了另外一层抽象,但是今天没有时间深入探讨。 )

当我们创建第二个对象时,Python将再次向操作系统申请更多的内存:

看起来相当简单,当我们创建Python对象的时候,将花费时间申请内存。

Ruby将没有用的对象扔的到处都是,直到下一个垃圾回收过程

Ruby开发者生活在一个脏乱的房间

回到Ruby,由于我们分配越来越多的对象,Ruby将继续为我们从空闲对象链表(free list)获取预分配对象。因此,空闲对象链表将变得越来越短:

或者更短:

请注意,我将一个新的值赋给了n1,Ruby会遗留下旧的值。”ABC”, “JKL”和”MNO”等结点对象会依然保留在内存中。Ruby不会立即清理旧的对象,尽管程序不再使用!作为一名Ruby开发者就像生活在一个脏乱的房间,衣服随意扔在地板上,厨房的水槽中堆满了脏盘子。作为一个Ruby开发者,你必须在一大堆垃圾对象中工作。

当你的程序不在使用任何对象的时候,Python会立刻进行清理。

Python开发者生活在一所整洁的房子

垃圾回收机制在Python和Ruby中迥然不同,让我们回到前面三个Python中Node对象的例子:

从内部来看,每当我们新建一个对象,Python将在对象对应的C语言结构中保存一个数字,叫做引用计数(reference count)。最初,Python将它的值设为1。

值为1表明每个对象有一个指针或引用指向它。假设我们创建一个新的对象,JKL:

正如前面所说,Python将”JKL”的引用设置为1。同样注意到我们改变n1指向了”JKL”,不再引用”ABC”,同时将”ABC”的引用计数减少为0。

通过这一点,Python垃圾回收器将会立即执行!无论何时,只要一个对象的引用计数变为0,python将立即释放这个对象,并且将它的内存返回给操作系统。

上图中,Python将回收”ABC”对象的内存。记住,Ruby只是将旧的对象遗留在那里,并且不去释放它们占用的内存。

这种垃圾回收算法被称为”引用计数”,由乔治柯林斯发明于1960年。非常巧合的是在同一年约翰麦卡锡大叔发明了”空闲对象链表算法”。正如Mike Bernstein在Ruby Conference大会上所说”1960年是属于垃圾回收器的…”。

作为一个Python开发者,就像生活在一个整洁的房间中。你知道,你的室友有些洁癖,他会把你使用过的任何东西都清洗一遍。你把脏盘子,脏杯子一放到水槽中他就会清洗。

现在看另外一个例子,假设我们让n2和n1指向同样的结点:

上图左边可以看到,Python减少了”DEF”的引用计数并且立即回收了”DEF”对象。同时可以看到,由于n1和n2同时指了”JKL”对象,所以它的引用计数变为了2。

标记回收算法

最终脏乱的房间将堆满垃圾,生活不能总是如此。Ruby程序在运行一段时间之后,空闲对象链表最终将被用尽。

上图中所有的预分配对象都被用尽(方块全部变成了灰色),链表上没有对象可用(没有剩余的白色方块)。

此时,Ruby使用了一种由约翰麦卡锡发明的被称为”标记回收”的算法。首先,Ruby将停止程序的执行,Ruby使用了”停止这个世界,然后回收垃圾”的方式。然后,Ruby会扫描所有的指向对象和值的指针或引用。同样,Ruby也会迭代虚拟机内部使用的指针。它会标记每一个指针所能到达的对象。在下图中,我使用了”M”指出了这些标记:

上面三个”M”标记的对象为活跃对象,依然被我们的程序使用。在Ruby解释器内部,通常使用”free bitmap”的数据结构来保存一个对象是否被标记:

Ruby将”free bitmap”保存在一个独立的内存区域,以便可以更好的利用Unix的”copy-on-write”特性。更详细的信息,请参考我的另一篇文章《为什么Ruby2.0的垃圾回收器让我们如此兴奋》。

如果活跃对象被标记了,那么其余的便是垃圾对象,意味着它们不再会被代码使用。在下图中,我使用白色的方块表示垃圾对象:

接下来,Ruby将清理没有使用的,垃圾对象,将它们链入空闲对象链表(free list):

在解释器内部,这个过程非常迅速,Ruby并不会真正的将对象从一个地方拷贝到另一个地方。相反的,Ruby会将垃圾对象组成一个新的链表,并且链入空闲对象链表(free list)。

现在,当我们要创建一个新的Ruby对象的时候,Ruby将为我们返回收集的垃圾对象。在Ruby中,对象是可以重生的,享受着多次的生命!

标记回收算法 vs. 引用计数算法

乍一看,Python的垃圾回收算法对于Ruby来说是相当让人感到惊讶的:既然可以生活在一个整洁干净的房间,为什么要生活在一个脏乱的房间呢?为什么Ruby周期性的强制停止运行程序,去清理垃圾,而不使用Python的算法呢?

然而,引用计数实现起来不会像它看起来那样简单。这里有一些许多语言不愿像Python一样使用引用计数算法的原因:

首先,实现起来很困难。Python必须为每一个对象留有一定的空间来保存引用计数。这会导致一些细微的内存开销。但更遭的是,一个简单的操作例如改变一个变量或引用将导致复杂的操作,由于Python需要增加一个对象的计数,减少另一个对象的计数,有可能释放一个对象。
其次,它会减慢速度。尽管Python在程序运行过程中垃圾回收的过程非常顺畅(当你把脏盘子放到水槽后,它立马清洗干净),但是运行的并不十分迅速。Python总是在更新引用计数。并且当你停止使用一个巨大的数据结构时,例如一个包含了大量元素的序列,Python必须一次释放许多对象。减少引用计数可能是一个复杂的,递归的过程。
最后,它并不总是工作的很好。在我演讲的下一部分,也就是下一篇帖子中能看到,引用计数不能处理循环引用数据结构,它包含循环引用。

下一次…

下周我将发布演讲的其他部分。我将讨论Python怎样处理循环引用数据结构,以及在即将到来的Ruby2.1中,垃圾回收器是怎样工作的。
传送门


原文:Pat Shaughnessy Visualizing Garbage Collection in Ruby and Python
译文:http://python.jobbole.com/60900/

使用 Python 进行并发编程

让计算机程序并发的运行是一个经常被讨论的话题,今天我想讨论一下Python下的各种并发方式。

并发方式

线程(Thread


多线程几乎是每一个程序猿在使用每一种语言时都会首先想到用于解决并发的工具(JS程序员请回避),使用多线程可以有效的利用CPU资源(Python例外)。然而多线程所带来的程序的复杂度也不可避免,尤其是对竞争资源的同步问题。

然而在python中由于使用了全局解释锁(GIL)的原因,代码并不能同时在多核上并发的运行,也就是说,Python的多线程不能并发,很多人会发现使用多线程来改进自己的Python代码后,程序的运行效率却下降了,这是多么蛋疼的一件事呀!如果想了解更多细节,推荐阅读这篇文章。实际上使用多线程的编程模型是很困难的,程序员很容易犯错,这并不是程序员的错误,因为并行思维是反人类的,我们大多数人的思维是串行(精神分裂不讨论),而且冯诺依曼设计的计算机架构也是以顺序执行为基础的。所以如果你总是不能把你的多线程程序搞定,恭喜你,你是个思维正常的程序猿:)

Python提供两组线程的接口,一组是thread模块,提供基础的,低等级(Low Level)接口,使用Function作为线程的运行体。还有一组是threading模块,提供更容易使用的基于对象的接口(类似于Java),可以继承Thread对象来实现线程,还提供了其它一些线程相关的对象,例如Timer,Lock

使用thread模块的例子

1
2
3
4
5
import thread
def worker():
"""thread worker function"""
print 'Worker'
thread.start_new_thread(worker)

使用threading模块的例子

1
2
3
4
5
6
import threading
def worker():
"""thread worker function"""
print 'Worker'
t = threading.Thread(target=worker)
t.start()

或者Java Style

1
2
3
4
5
6
7
8
9
10
import threading
class worker(threading.Thread):
def __init__(self):
pass
def run():
"""thread worker function"""
print 'Worker'

t = worker()
t.start()

进程(Process)

由于前文提到的全局解释锁的问题,Python下比较好的并行方式是使用多进程,这样可以非常有效的使用CPU资源,并实现真正意义上的并发。当然,进程的开销比线程要大,也就是说如果你要创建数量惊人的并发进程的话,需要考虑一下你的机器是不是有一颗强大的心。

Python的mutliprocess模块和threading具有类似的接口。

1
2
3
4
5
6
7
8
from multiprocessing import Process

def worker():
"""thread worker function"""
print 'Worker'
p = Process(target=worker)
p.start()
p.join()

由于线程共享相同的地址空间和内存,所以线程之间的通信是非常容易的,然而进程之间的通信就要复杂一些了。常见的进程间通信有,管道,消息队列,Socket接口(TCP/IP)等等。

Python的mutliprocess模块提供了封装好的管道和队列,可以方便的在进程间传递消息。

Python进程间的同步使用锁,这一点喝线程是一样的。

另外,Python还提供了进程池Pool对象,可以方便的管理和控制线程。

远程分布式主机(Distributed Node)

随着大数据时代的到临,摩尔定理在单机上似乎已经失去了效果,数据的计算和处理需要分布式的计算机网络来运行,程序并行的运行在多个主机节点上,已经是现在的软件架构所必需考虑的问题。

远程主机间的进程间通信有几种常见的方式

  • TCP/IP
    TCP/IP是所有远程通信的基础,然而API比较低级别,使用起来比较繁琐,所以一般不会考虑

  • 远程方法调用 Remote Function Call
    RPC 是早期的远程进程间通信的手段。Python下有一个开源的实现 RPyC

  • 远程对象 Remote Object
    远程对象是更高级别的封装,程序可以想操作本地对象一样去操作一个远程对象在本地的代理。远程对象最广为使用的规范CORBA,CORBA最大的好处是可以在不同语言和平台中进行通信。当让不用的语言和平台还有一些各自的远程对象实现,例如Java的RMI ,MS的DCOM

    Python的开源实现,有许多对远程对象的支持

  • 消息队列 Message Queue
    比起RPC或者远程对象,消息是一种更为灵活的通信手段,常见的支持Python接口的消息机制有

在远程主机上执行并发和本地的多进程并没有非常大的差异,都需要解决进程间通信的问题。当然对远程进程的管理和协调比起本地要复杂。

Python下有许多开源的框架来支持分布式的并发,提供有效的管理手段包括:

  • Celery
    Celery是一个非常成熟的Python分布式框架,可以在分布式的系统中,异步的执行任务,并提供有效的管理和调度功能。参考这里

  • SCOOP
    SCOOP(Scalable COncurrent Operations in Python)提供简单易用的分布式调用接口,使用Future接口来进行并发。

  • Dispy
    相比起Celery和SCOOP,Dispy提供更为轻量级的分布式并行服务

  • PP
    PP(Parallel Python)是另外一个轻量级的Python并行服务, 参考这里

  • Asyncoro
    Asyncoro是另一个利用Generator实现分布式并发的Python框架,

当然还有许多其它的系统,我没有一一列出

另外,许多的分布式系统多提供了对Python接口的支持,例如 Spark

伪线程(Pseudo-Thread)

还有一种并发手段并不常见,我们可以称之为伪线程,就是看上去像是线程,使用的接口类似线程接口,但是实际使用非线程的方式,对应的线程开销也不存的。

  • greenlet
    greenlet提供轻量级的coroutines来支持进程内的并发。

    greenlet是Stackless的一个副产品,使用tasklet来支持一中被称之为微线程(mirco-thread)的技术,这里是一个使用greenlet的伪线程的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from greenlet import greenlet

    def test1():
    print 12
    gr2.switch()
    print 34

    def test2():
    print 56
    gr1.switch()
    print 78

    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()

    运行以上程序得到如下结果:

    1
    2
    3
    12
    56
    34

    伪线程gr1 switch会打印12,然后调用gr2 switch得到56,然后switch回到gr1,打印34,然后伪线程gr1结束,程序退出,所以78永远不会被打印。通过这个例子我们可以看出,使用伪线程,我们可以有效的控制程序的执行流程,但是伪线程并不存在真正意义上的并发。

    eventlet,gevent和concurence都是基于greenlet提供并发的。

  • eventlet
    eventlet是一个提供网络调用并发的Python库,使用者可以以非阻塞的方式来调用阻塞的IO操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import eventlet
    from eventlet.green import urllib2

    urls = ['http://www.google.com', 'http://www.example.com', 'http://www.python.org']

    def fetch(url):
    return urllib2.urlopen(url).read()

    pool = eventlet.GreenPool()

    for body in pool.imap(fetch, urls):
    print("got body", len(body))

    执行结果如下

    1
    2
    3
    ('got body', 17629)
    ('got body', 1270)
    ('got body', 46949)

    eventlet为了支持generator的操作对urllib2做了修改,接口和urllib2是一致的。这里的GreenPool和Python的Pool接口一致。

  • gevent
    gevent和eventlet类似,关于它们的差异大家可以参考这篇文章

    1
    2
    3
    4
    5
    6
    7
    import gevent
    from gevent import socket
    urls = ['www.google.com', 'www.example.com', 'www.python.org']
    jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
    gevent.joinall(jobs, timeout=2)

    print [job.value for job in jobs]

    执行结果如下:

    1
    ['206.169.145.226', '93.184.216.34', '23.235.39.223']
  • concurence
    concurence是另外一个利用greenlet提供网络并发的开源库,我没有用过,大家可以自己尝试一下。

实战运用

通常需要用到并发的场合有两种,一种是计算密集型,也就是说你的程序需要大量的CPU资源;另一种是IO密集型,程序可能有大量的读写操作,包括读写文件,收发网络请求等等。

计算密集型

对应计算密集型的应用,我们选用著名的蒙特卡洛算法来计算PI值。基本原理如下

蒙特卡洛算法利用统计学原理来模拟计算圆周率,在一个正方形中,一个随机的点落在1/4圆的区域(红色点)的概率与其面积成正比。也就该概率 p = Pi * R*R /4 : R* R , 其中R是正方形的边长,圆的半径。也就是说该概率是圆周率的1/4, 利用这个结论,只要我们模拟出点落在四分之一圆上的概率就可以知道圆周率了,为了得到这个概率,我们可以通过大量的实验,也就是生成大量的点,看看这个点在哪个区域,然后统计出结果。

基本算法如下:

1
2
3
4
5
from math import hypot
from random import random

def test(tries):
return sum(hypot(random(), random()) < 1 for _ in range(tries))

这里test方法做了n(tries)次试验,返回落在四分之一圆中的点的个数。判断方法是检查该点到圆心的距离,如果小于R则是在圆上。

通过大量的并发,我们可以快速的运行多次试验,试验的次数越多,结果越接近真实的圆周率。

这里给出不同并发方法的程序代码

  • 非并发
    我们先在单线程,但进程运行,看看性能如何

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from math import hypot
    from random import random
    import eventlet
    import time

    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    def calcPi(nbFutures, tries):
    ts = time.time()
    result = map(test, [tries] * nbFutures)

    ret = 4. * sum(result) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    print calcPi(3000,4000)
  • 多线程 thread
    为了使用线程池,我们用multiprocessing的dummy包,它是对多线程的一个封装。注意这里代码虽然一个字的没有提到线程,但它千真万确是多线程。

    通过测试我们开(jing)心(ya)的发现,果然不出所料,当线程池为1是,它的运行结果和没有并发时一样,当我们把线程池数字设置为5时,耗时几乎是没有并发的2倍,我的测试数据从5秒到9秒。所以对于计算密集型的任务,还是放弃多线程吧。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from multiprocessing.dummy import Pool

    from math import hypot
    from random import random
    import time

    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    def calcPi(nbFutures, tries):
    ts = time.time()
    p = Pool(1)
    result = p.map(test, [tries] * nbFutures)
    ret = 4. * sum(result) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    if __name__ == '__main__':
    p = Pool()
    print("pi = {}".format(calcPi(3000, 4000)))
  • 多进程 multiprocess
    理论上对于计算密集型的任务,使用多进程并发比较合适,在以下的例子中,进程池的规模设置为5,修改进程池的大小可以看到对结果的影响,当进程池设置为1时,和多线程的结果所需的时间类似,因为这时候并不存在并发;当设置为2时,响应时间有了明显的改进,是之前没有并发的一半;然而继续扩大进程池对性能影响并不大,甚至有所下降,也许我的Apple Air的CPU只有两个核?

    当心,如果你设置一个非常大的进程池,你会遇到 Resource temporarily unavailable的错误,系统并不能支持创建太多的进程,毕竟资源是有限的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from multiprocessing import Pool

    from math import hypot
    from random import random
    import time

    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    def calcPi(nbFutures, tries):
    ts = time.time()
    p = Pool(5)
    result = p.map(test, [tries] * nbFutures)
    ret = 4. * sum(result) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    if __name__ == '__main__':
    print("pi = {}".format(calcPi(3000, 4000)))
  • gevent (伪线程)
    不论是gevent还是eventlet,因为不存在实际的并发,响应时间和没有并发区别不大,这个和测试结果一致。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import gevent
    from math import hypot
    from random import random
    import time

    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    def calcPi(nbFutures, tries):
    ts = time.time()
    jobs = [gevent.spawn(test, t) for t in [tries] * nbFutures]
    gevent.joinall(jobs, timeout=2)
    ret = 4. * sum([job.value for job in jobs]) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    print calcPi(3000,4000)
  • eventlet (伪线程)
    代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from math import hypot
    from random import random
    import eventlet
    import time

    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    def calcPi(nbFutures, tries):
    ts = time.time()
    pool = eventlet.GreenPool()
    result = pool.imap(test, [tries] * nbFutures)

    ret = 4. * sum(result) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    print calcPi(3000,4000)
  • SCOOP
    SCOOP中的Future接口符合 PEP-3148 的定义,也就是在Python3中提供的 Future 接口。

    在缺省的SCOOP配置环境下(单机,4个Worker),并发的性能有提高,但是不如两个进程池配置的多进程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from math import hypot
    from random import random
    from scoop import futures

    import time

    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    def calcPi(nbFutures, tries):
    ts = time.time()
    expr = futures.map(test, [tries] * nbFutures)
    ret = 4. * sum(expr) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    if __name__ == "__main__":
    print("pi = {}".format(calcPi(3000, 4000)))

  • Celery
    任务代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from celery import Celery

    from math import hypot
    from random import random

    app = Celery('tasks', backend='amqp', broker='amqp://guest@localhost//')
    app.conf.CELERY_RESULT_BACKEND = 'db+sqlite:///results.sqlite'

    @app.task
    def test(tries):
    return sum(hypot(random(), random()) < 1 for _ in range(tries))

    客户端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from celery import group
    from tasks import test

    import time

    def calcPi(nbFutures, tries):
    ts = time.time()
    result = group(test.s(tries) for i in xrange(nbFutures))().get()

    ret = 4. * sum(result) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    print calcPi(3000, 4000)

    使用Celery做并发的测试结果出乎意料(环境是单机,4frefork的并发,消息broker是rabbitMQ),是所有测试用例里最糟糕的,响应时间是没有并发的5~6倍。这也许是因为控制协调的开销太大。对于这样的计算任务,Celery也许不是一个好的选择。

  • asyncoro
    Asyncoro的测试结果和非并发保持一致。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import asyncoro

    from math import hypot
    from random import random
    import time

    def test(tries):
    yield sum(hypot(random(), random()) < 1 for _ in range(tries))


    def calcPi(nbFutures, tries):
    ts = time.time()
    coros = [ asyncoro.Coro(test,t) for t in [tries] * nbFutures]
    ret = 4. * sum([job.value() for job in coros]) / float(nbFutures * tries)
    span = time.time() - ts
    print "time spend ", span
    return ret

    print calcPi(3000,4000)

IO密集型

IO密集型的任务是另一种常见的用例,例如网络WEB服务器就是一个例子,每秒钟能处理多少个请求时WEB服务器的重要指标。

我们就以网页读取作为最简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from math import hypot
import time
import urllib2

urls = ['http://www.google.com', 'http://www.example.com', 'http://www.python.org']

def test(url):
return urllib2.urlopen(url).read()

def testIO(nbFutures):
ts = time.time()
map(test, urls * nbFutures)

span = time.time() - ts
print "time spend ", span

testIO(10)

在不同并发库下的代码,由于比较类似,我就不一一列出。大家可以参考计算密集型中代码做参考。

通过测试我们可以发现,对于IO密集型的任务,使用多线程,或者是多进程都可以有效的提高程序的效率,而使用伪线程性能提升非常显著,eventlet比没有并发的情况下,响应时间从9秒提高到0.03秒。同时eventlet/gevent提供了非阻塞的异步调用模式,非常方便。这里推荐使用线程或者伪线程,因为在响应时间类似的情况下,线程和伪线程消耗的资源更少。

总结

Python提供了不同的并发方式,对应于不同的场景,我们需要选择不同的方式进行并发。选择合适的方式,不但要对该方法的原理有所了解,还应该做一些测试和试验,数据才是你做选择的最好参考。


原文地址:http://my.oschina.net/taogang/blog/389293

Python 实现 socket 通讯 (TCP/UDP)

1. TCP

1.1 TCP-Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8 -*-
# TCP-Server

import socket

# 1. 创建 socket 对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 将 socket 绑定到指定地址
address = ('127.0.0.1', 10140)
s.bind(address)

# 3. 接收连接请求
s.listen(5)

# 4. 等待客户请求一个连接
# 调用 accept 方法时,socket 会进入 "waiting" 状态。
# accept方法返回一个含有两个元素的元组 (connection, address)。
# 第一个元素 connection 是新的 socket 对象,服务器必须通过它与客户通信;
# 第二个元素 address 是客户的 Internet 地址。
ss, addr = s.accept()
print 'got connect from', addr

# 5. 处理:服务器和客户端通过 send 和 recv 方法通信
# send 方法返回已发送的字节个数。
# 调用 recv 时,服务器必须指定一个整数,它对应于可通过本次方法调用来接收的最大数据量。
# recv方法在接收数据时会进入 "blocked" 状态,最后返回一个字符 串,用它表示收到的数据。
# 如果发送的数据量超过了recv 所允许的,数据会被截短。
# 多余的数据将缓冲于接收端。以后调用recv时,多余的数据会从缓冲区删除。
while True:
ra = ss.recv(512)
print 'client:', ra
ss.send('received')

# 6. 传输结束,关闭连接
ss.close()
s.close()
Read more

Python 多线程响应 Ctrl + C,用 Event 实现

在用 python 编写多线程程序时,经常需要用 Ctrl + C 中止进程,可是大家都知道,在 python 中,除了主线程可以响应控制台的 Ctrl + C ,其他线程是无法捕获到的,也就是说,当主线程被中止后,其他线程也会被强制中止,这样线程们就没有机会处理自己还没有完成的工作。

而在实际应用中,我们可能会有这样的要求:

  1. 当按下 Ctrl + C 时,我们希望所有线程先处理完自己的任务,再主动停止

  2. 当所有线程停止后,主线程才终止

这篇文章】提供了一种方法,我对其做了进一步改进,写了如下的代码,希望能起到抛砖引玉的作用:

Read more

Python 引用,拷贝,对象回收,弱引用

引用

python中,在对对象赋值,参数传递,函数返回等等, 都是引用传递的. 直接copy个例子来【1】:

1
2
3
4
a = [1, 2, 3]
b = a
b.append(5)
print a, b

输出结果为:

1
[1, 2, 3, 5] [1, 2, 3, 5]

面的结果有助于理解引用的实际情况。 具体查看一个对象的引用数,可以使用sys.getrefcount(ojb)获取,但这个函数有点邪恶,有时似乎并不给出正确的结果,正常来说获取的值都比你想要的大,一般是大1,因为给这个函数传参数也算一个引用。但有时会大得离谱,来例子:

1
2
3
import sys
a = "a"
sys.getrefcount(a)

在我的机器上,输出结果尽然为14,网络遛了一圈,有人说是python内部对“a”这个对象进行了引用。好吧!就这样理解把,有高见的可以留言告我一下!

Read more

Python 中用 Ctrl+C 终止多线程程序的问题解决

花了一天时间用python为服务写了个压力测试。很简单,多线程向服务器发请求。但写完之后发现如果中途想停下来,按Ctrl+C达不到效果,自然想到要用信号处理函数捕捉信号,使线程都停下来,问题解决的方法请往下看:

Read more
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×