Dec 6

Python: metaclass小记 不指定

felix021 @ 2015-12-6 17:59 [IT » Python] 评论(2) , 引用(0) , 阅读(18425) | Via 本站原创
友情提示:本文不一定适合阅读,如果执意要读,请备好晕车药。

== 题记 ==

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."

-- Tim Peters

== 起因 ==

这句话听起来就很诱人,曾经试图去理解它,但是因为没有实际的需求,就因为烧脑子而放弃了。不妨摘录一段Python document里关于metaclass的概述,简直就是绕口令:
引用
Terminology-wise, a metaclass is simply "the class of a class". Any class whose instances are themselves classes, is a metaclass. When we talk about an instance that's not a class, the instance's metaclass is the class of its class: by definition, x's metaclass is x.__class__.__class__. But when we talk about a class C, we often refer to its metaclass when we mean C.__class__ (not C.__class__.__class__, which would be a meta-metaclass; there's not much use for those although we don't rule them out).


昨天心血来潮想写一个带class initializer的class,发现绕不过metaclass了,于是又翻出来看。

== 概述 ==

其实是要理解metaclass的本质,无非是要时刻牢记两点:1. Python中一切皆对象; 2. class也是一个对象,它的class就是metaclass。

举例来说:
>>> class A(object): pass
...
>>> a = A()
>>> print (a, id(a), type(a))               
(<__main__.A object at 0xb183d0>, 11633616, <class '__main__.A'>)

>>> print (A, id(A), type(A))               
(<class '__main__.A'>, 11991040, <type 'type'>)

>>> print (type, id(type), type(type))
(<type 'type'>, 1891232, <type 'type'>)

其中第一个print很好理解:a是一个A的实例,有自己的id(其实就是内存地址)、a的class是A。

第二个print就有点烧脑子了:A是一个class,也有自己的id(因为A也是一个对象,虽然print出来的时候没有明确说),A的class是type。

而第三个就晕乎了:type是一个type,也有自己的id(因为type也是一个对象),type的class是type,也就是它自己。

再回想上面提到的两点:A是一个对象,它的class是metaclass。也就是说 type 是一个metaclass,而A类是type类的一个对象。

唉,本来想好好解释的,没想到还是说成绕口令了。算了,反正我懂了,继续。

== type ==

没有仔细了解type是什么的同学可能会以为type是一个函数:type(X)用于返回X的类对象。

然而并不完全是这样的:在python里,X(args)可能是调用一个函数,也可能是在实例化一个X的对象——而很不幸地,type(X)实际上是介于二者之间的一个调用:虽然type是一个class,但是它的__call__方法是存在的,于是python把它当成一个函数来调用,实际调用到了源码中的type_call;type_call调用了type.__new__试图初始化一个type类的实例,然而type.__new__(位于源码中的type_new函数)发现卧槽居然只有一个参数,于是就返回了这个参数的type(源码是这么写的:"return (PyObject *) Py_TYPE(x);",并没有生成新的对象)。也就是说,本来是个函数调用,里面却是要初始化一个对象,然而最后返回的却不是初始化的对象!尼玛那个特殊情况为毛不放到函数调用里面啊,开发者脑抽了吗!

感到脑抽的同学可以暂时忽略上面那段话,跟本文没太大关系。继续。

实际上type是在builtin模块中定义,指向源码中PyType_Type对象的一个引用:
//位于Python/bltinmodule.c
PyObject * _PyBuiltin_Init(void)
{
    ...
    SETBUILTIN("type",                  &PyType_Type);
    ...
}

这个PyType_Type又是个什么鬼?好吧,继续贴源码
//位于Objects/typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                    /* tp_name */
    ...
    type_init,                                  /* tp_init */
    0,                                          /* tp_alloc */
    type_new,                                  /* tp_new */
    ...
};

注意2点:

0. PyType_Type,也就是python里的type,是在源码中生成的一个对象;这个对象的类型是PyTypeObject,所以它恰好又是一个类,至于你信不信,反正我信了。后面我把它叫做类对象,注意:不是类的对象,而是类本身是一个对象。

1. PyVarObject_HEAD_INIT递归引用了自己(PyType_Type)作为它的type(在源码中,指定某个对象的type为X,就是指定了它在python环境中的class为X),所以前面第三个print中可以看到,type(type) == type(哈哈哈,写绕口令真好玩)

2. 在PyType_Type的定义指定了 tp_init = type_init 和 tp_new = type_new 这两个属性值。这是两个函数,也位于源码中的Object/typeobject.c。

关于第2点,在Python document中关于__new__方法的说明里有详细的介绍,这里简单总结一下:在new一个对象的时候,只会调用这个class的__new__方法,它需要生成一个对象、调用这个对象的__init__方法对它进行初始化,然后返回这个对象。

好吧,我发现不得不把简单总结展开,否则确实说不清楚。

== 实例化 ==

这是一个很有意思的设计:把实例化的流程暴露给码农,意味着码农可以在对象的生成前、生成后返回前两个环节对这个对象进行修改(【甚至】在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!不过在document里建议,如果要覆盖__new__方法,那么【应当】返回这个class的父类的__new__方法返回的对象)。这里还有一个非常tricky的地方:虽然没有明确指定,但是__new__方法被硬编码为一个staticmethod(有兴趣的话可以去翻type_new函数),它的第一个参数是需要被实例化的class,其余参数则是需要传给__init__的参数。

说起来非常枯燥,还是举一个例子吧,就用document里给出的Singleton:
class Singleton(object):
    def __new__(cls, *args, **kwargs):
        it = cls.__dict__.get("__it__")
        if it is not None:
            return it
        cls.__it__ = it = object.__new__(cls) #注意
        it.__init__(*args, **kwargs)
        return it

    def __init__(self, *args, **kwargs):
        pass

class DbConnection(Singleton):
    def __init__(self, db_config):
        self._connection = AnyHowToConnectBy(db_config)
       
conn = new DbConnection(db_config)


代码并不复杂,但是可能有点玄乎,需要理解一下那个cls参数,前面说了,它是需要被实例化的class,也就是说,最后一行实际执行的是:
DbConnection.__new__(DbConnection, db_config)

而DbConnection的__new__方法直接继承于Singleton, 所以实际调用的是
Singleton.__new__(DbConnection, db_config)

主要注意的地方,在上面这段代码的第六行,Singleton是继承于object(这里特指python中的那个object对象),因此调用了object.__new__(DbConnection)来生成一个对象,生成过程位于C源码中的object_new函数(Objects/typeobject.c),它会将新生成对象的type指定为DbConnection,然后直接返回。

Singleton.__new__在拿到了生成的DbConnection实例以后,将它保存在了DbConnection类的__it__属性中,然后对该实例进行初始化,最后返回。

可以看到,任何继承于Singleton类的子类,只要不覆盖其__new__方法,每个类永远只会被实例化一次。

好了,第2点暂告一段落,接下来回归正题,尼玛我都快忘了要讲的是metaclass啊。

== metaclass ==

还记的上面可以暂时忽略的那段话吗?type(X)是试图实例化type对象,但是因为只有一个参数,所以源码中只是返回了X的类。而type的标准初始化参数应当有三个:class_name, bases, attributes。最前面那个"class A(object): pass",python解释器实际的流程是:

1. 解析这段代码,得知它需要创建一个【类对象】,这个类的名字叫做'A', 它的父类列表(用tuple表示)是 (object,),它的属性用一个dict来表示就是 {} 。

2. 查找用于生成这个类的metaclass。(终于讲到重点了有木有!)
    查找过程比较蛋疼,位于Python/ceval.c : build_class函数,按顺序优先采用以下几个:
    2.1 定义中使用 __metaclass__ 属性指定的(本例:没有)
    2.2 如果有父类,使用第一个父类的 __class__ 属性,也就是父类的metaclass(本例:object的class,也就是type)
    2.2.1 如果第一个父类没有 __class__ 属性,那就用父类的type(这是针对父类没有父类的情况)
    2.3 使用当前Globals()中的 __metaclass__ 指定的(本例:没有,不过2.2里已经找到了)
    2.4 使用PyClass_Type

    注:2.2.1和2.4中提到了没有父类,或者父类没有父类的情形,这就是python中的old-style class,在python2.2之前所有的对象都是这样的,而2.2之后可以继承于object类,就变成了new-style class。这种设计保持了向后兼容。

3. 使用metaclass来创建这个A类。由于A类的class就是metaclass,所以这个过程其实就是实例化metaclass的过程。本例中找到的metaclass是type,所以最终python执行的相当于这一句:
type('A', (object,), {})


再回想一下前面提到的实例化过程,实际上这一句分成两步: 1. 调用type.__new__(type, 'A', (object,), {})生成type的一个实例(也就是A类对象);2. 调用type.__init__(A, 'A', (object,), {}) 对A类对象进行初始化。注意:这里调用的是type.__init__,而不是A.__init__:因为A是type的一个实例。

流程终于解释完啦,不过我觉得还是举个栗子会比较好。就用我看到的那个有点二二的栗子吧:定义一个class,把它的所有属性都改成全大写的。我感觉这个栗子唯一的作用就是用来当栗子了。还好还有这个作用,否则连出生的机会都没有。

== 栗子 ==

直接上代码好了:
def upper_meta(name, bases, attrs):
    new_attrs = {}
    for name, value in attrs.items():
        if not name.startswith('__'):
            new_attrs[name.upper()] = value
        else:
            new_attrs[name] = value
    return type(name, bases, new_attrs)

class Foo(object):
    __metaclass__ = upper_meta
    hello = 'world'

print Foo.__dict__


请不要说“说好的metaclass呢!怎么变成了一个函数!我摔!”,回顾一下最最前面提到的一点:everything is an object in python。upper_meta作为一个函数,它也是一个对象啊。而metaclass也不过就是个对象,并没有本质上的差别——只要它被call的时候能接受name, bases, attrs这三个参数并返回一个类对象就行了。duck-typing的语言用起来就是有这样的一种不可言状的酸爽感。

理解了这一点,这段代码就能理解了,upper_meta返回了一个type类的实例——也就是Foo类,并且可以看到print出来的属性里头只有HELLO而没有hello。

考虑到可能有人不满意,想看使用class来作为metaclass的情形,我就勉为其难换个姿势再举一下这个栗子(真累)。
class upper_meta(type):
    def __new__(cls, name, bases, attrs):
        attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
        return type(name, bases, attrs)

写的太长了,换了一个短一点的oneliner,但是效果不变(其实我就是想炫一下,不服来咬我呀)。

这段代码虽然形式上跟前面的upper_meta函数不一样,但是本质是一样的:调用了upper_meta('Foo', (object,), {'hello': 'world'}),生成了一个新的名为Foo的类对象。

理论上,故事讲到这里应该结束了,然而我想说,压轴戏还没上呢。

== 压轴戏 ==

我要把这栗子举得更高更远,也更符合实际开发的需求:继承。
class Bar(Foo):
    hi = 'there'

print Bar.__dict__

这段代码太简单了,但是埋在下面的逻辑却太复杂了。

它的输出并不是{'HI': 'there'}, 而是{'hi': 'there'}。你print Bar.HELLO, Bar.__metaclass__都能得到预期的输出,但是偏偏没有HI,只有hi。

为什么?这真是个烧脑细胞的事情。我已经把所有的逻辑都展现出来了,甚至还做了特别的标记。然而即便如此,想要把这个逻辑理顺,也是一件非常有挑战性的事情,幸好我已经想明白了:苦海无涯,回头是岸。啊呸,应该是——学海无涯苦作舟,不想明白不回头。

我想说“甚至还做了特别标记”这句话的意思是,我还给【甚至】这两个字做了特别标记:在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!

问题的关键就在这里:前面两个栗子中给出的upper_meta,返回的并不是upper_meta的实例,而是type的实例,而是type的实例,而是type的实例。重说三。

什么意思?再看看代码,最后return的是type(name, bases, attrs),也就是说,Foo类对象并不是upper_meta的实例,而是type的实例(也就是说:虽然指定并被使用的metaclass是upper_meta,但是最终创建出来的Foo类的metaclass是type)。不信你print type(Foo)试试,结果就是type,而不是upper_meta。

为什么这会导致继承于Foo类的Bar类不能由upper_meta来搭建?Bar.__metaclass__不还是upper_meta吗?

这个问题就没有那么困难了,有兴趣的同学可以自己试着分析一下,没兴趣的大概也不会有耐心看到这里吧。

Bar.__metaclass__并不是Bar的原生属性,而是继承于Foo的——所以在print Bar.__dict__的时候看不到__metaclass__。也就是说,在试图创建Bar时,attrs里并没有__metaclass__属性,所以并不会直接采用upper_meta。再回顾一下选择metaclass的顺序就可以发现,实际上在2.2里会选择Foo的metaclass——Foo的metaclass是type,而不是指定的upper_meta。

解决方法很简单:关键就是前面被特别标记了的【应当】返回这个class的父类的__new__方法返回的对象。具体到代码应当是这样:
class upper_meta(type):
    def __new__(cls, name, bases, attrs):
        attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
        return super(upper_meta, cls).__new__(cls, name, bases, attrs)

    def __init__(cls, name, bases, attrs):
        print >>sys.stderr, 'in upper_meta.__init__' #FOR TEST ONLY


新增的__init__方法并不是必须的,有兴趣的同学可以跟上面的栗子对比一下,由于前面返回的是type类的实例,调用到的是type.__init__;而这样正确的写法就会调用到upper_meta.__init__。(p.s. super也是烧脑细胞的东西,但用于解决钻石继承的问很有意思,有兴趣的同学可以看看Cooperative methods and "super")

果然很烧脑细胞吧。

关于metaclass的选择,还有另外一个坑:在metaclass 2.3提到了,找不到metaclass的情况下,会使用Globals()中定义的__metaclass__属性指定的元类来创建类,那么为什么下面的代码却没有生效呢?

def __metaclass__(name, bases, attrs):
    attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
    return type(name, bases, attrs)

class Foo(object):
    hello = 'world'

print Foo.__dict__


== class initializer ==

回到我最初的需求:我需要创建带class initializer的类。为什么会有这样的需求?最常见的metaclass的应用场景是对数据库的封装。举例来说,我希望创建一个Table类,所有表都是继承于这个类,同时我还想给每一个表都设置一个缓存dict(使用主键作为key缓存查询结果)。一个很自然的想法是这样的:

class Table(object):
    _pk_cache = {}
   
    @classmethod
    def cache(cls, obj):
        cls._pk_cache[obj.pkey()] = obj;
       
    @classmethod
    def findByPk(cls, pkey):
        return cls._pk_cache[pkey]

    def __init__(self, pkey, args):
        self._pkey = pkey
        self._args = args
        type(self).cache(self)
       
    def pkey(self):
        return self._pkey

    def __repr__(self):
        return type(self).__name__ + ':' + repr(self._args)
       
class Student(Table):
    pass
   
class Grade(Table):
    pass
   
s1 = Student(1, 's1')
g1 = Grade(1, 'g1')

print Student.findByPk(1)

可惜这是错的。从输出结果就能看出来,返回的是一个Grade对象,而不是预期的Student对象。原因很简单:子类们并不直接拥有_pk_cache ,它们访问的是Table的_pk_cache ,而该dict只被初始化了一次。

当然,我可以在每一个继承于Table的class里新增一句 _pk_cache = {},但是这样的实现太丑了,而且一不注意就会漏掉导致出错。

所以我需要一个class initializer,在class被创建的时候,给它新增一个_pk_cache 。

在搞清楚了metaclass之后,解决方法特别简单:
class TableInitializer(type):
    def __new__(cls, name, bases, attrs):
        attrs['_pk_cache'] = {}
        return super(TableInitializer, cls).__new__(cls, name, bases, attrs)
       
class Table(object):
    __metaclass__ = TableInitializer

    ... #以下不变


完。(终于完结了,我写了一整个下午啊...)
Oct 29

php code snippit 不指定

felix021 @ 2015-10-29 10:51 [IT » 程序设计] 评论(0) , 引用(0) , 阅读(9943) | Via 本站原创
转置二维数组:
function transpose($array) {
    array_unshift($array, null);
    return call_user_func_array('array_map', $array);
}


utf-8字符串转为utf-8字符数组:
function utf8_str2arr($str)
{
    preg_match_all("/./u", $str, $arr);
    return $arr[0];
}


按显示宽度截取utf-8字符串
function substr_width($str, $start, $width)
{
    $arr = utf8_str2arr($str);
    $arr_ret = [];
    $i = 0;
    while ($width > 0 and $i < count($arr))
    {
        $arr_ret[] = $arr[$start + $i];
        if (strlen($arr_ret[$i]) == 1) //ascii,width=1
            $width -= 1;
        else
            $width -= 2;
        $i++;
    }
    if ($width < 0)
        array_pop($arr_ret);
    return join('', $arr_ret);
}


让进程在后台运行(detached process),出乎意料地简单
pclose(popen("nohup $cmd &", 'r'));
Oct 16

ssh clone session 不指定

felix021 @ 2015-10-16 11:48 [IT » 软件] 评论(2) , 引用(0) , 阅读(6484) | Via 本站原创
在Windows下用惯了Secure CRT的Clone Session功能,切换到mac下面,还真有点怀念。于是搜了一下,发现达到类似的效果倒是也不难。

1. mkdir ~/.ssh/cm_socket

2. 创建 ~/.ssh/config

  Host *
  ControlMaster auto
  ControlPath ~/.ssh/cm_socket/%r@%h:%p

3. 之后只要是相同的 user@host:port 都会共用一个tcp connection,不需要再输入密码了。

p.s. 补充说明一下,通常来说,能登录服务器也就意味着可以建立信任关系,那么clone session的意义就减少了很多,但是仍然有两个优势:

1. 类似B公司的relay机器,必须使用token认证才能登录的,信任关系不能满足需求(Google Authenticator这样的两步认证方案也类似)

2. 对于延迟比较高的机器,直接复用tcp connection,可以减少相当多的时间(tcp三次握手、ssh认证的各种交互等)。
Aug 18

T02安装Ubuntu 不指定

felix021 @ 2015-8-18 12:42 [IT » 操作系统] 评论(1) , 引用(0) , 阅读(14056) | Via 本站原创
前一段时间在京东众筹购入了一款名为"光棍一号T02"的电视棒,Intel Atom Z3735F(1.33G*4) + 2GB + 32GB,之所以下单,最主要是因为他们家宣称可以支持ubuntu(因为看起来好像就是intel Compute Stick的贴牌产品,而后者是可以支持ubuntu的)。可是到手了以后,问了众筹方才知道,原来安装镜像是不公开的,需要在发货前说明,他们装好再寄过来(跟Compute Stick一个尿性,据说是因为第三方的驱动问题)。没办法,只能自己动手了。

在网上有找到一篇文章:Install Ubuntu 14.04 LTS on the 2GB Intel Compute Stick,里面提供了一个64bit的ubuntu 14.04镜像以及相应的步骤,可以直接安装到2GB内存的Compute Stick上;以及对安装32位UEFI支持的一些说明和提示。

情况是这样的,Intel Compute Stick 2GB+32GB版本出厂预装的是Win8,Ubuntu版本只有1GB内存+16GB存储,所以大神们琢磨着弄一个ubuntu到windows版本上去,可是intel官方因为第三方驱动的原因不开放镜像,于是他们自己做了一个。从文章里可以知道,stick可以在UEFI里面选择启动的系统是32bit的win8还是64bit的ubuntu,也就是说可以按需向操作系统提供32/64位的uefi接口。可是手头的这个T02只有一个非常简单的32bit的uefi,所以整个过程就比较蛋疼了,下面记录一下,备忘。

1. 准备相关材料:http://pan.baidu.com/s/1qWN0jIO

包括 (a) 安装镜像 ubuntu14.04_z3735f.iso (b) 启动盘制作工具rufus-2.2p.exe (c) 支持32位efi的grub启动器 bootia32.efi (d) 安装后用的grub.tar.gz

2. 制作启动盘

使用rufus把iso写入到一个U盘里。把wubi.exe删掉,然后把 bootia32.efi 放到 EFI/BOOT/ 下面

3. 安装使用启动盘启动T02,可以先通过"try ubuntu"体验一下效果。

(a) 如果希望直接安装的话,建议选择"install ubuntu",比较快且不容易出错。
(b) 安装中进行分区的时候选择“使用整个磁盘”,这样会自动使用GPT分区并且创建一个EFI分区。由于本身内存空间有限,建议不要考虑双系统共存了。
(c) 安装期间建议不要插入TF卡,速度超慢。不要安装到TF卡上。

==》 安装完以后不要急着重启

4. 复制文件

(a) 把启动盘上的 bootia32.efi 拷贝到新系统的 [挂载点]/boot/efi/EFI/ubuntu 下面去。
(b) 把grub.tar.gz的内容解压到新系统的随便一个目录中(比如 /home 下面)

5. 重启,进入EFI Shell(因为新系统中没有包含配置好的32位的启动程序,会自动进入到EFI Shell中)

6. 进入grub shell:根据前面列出文章中的Troubleshooting,按顺序执行:

(a) fs0:  (注意冒号)
(b) cd EFI\ubuntu
(c) bootia32.efi  (于是就引入了一个裸的grub shell)

7. 进入initrd中的准系统:在grub shell下面执行

(a) linux (hd0,gpt2)/vmlinuz
(b) initrd (hd0,gpt2)/initrd.img
(c) boot

于是就进入了initrd中的

8. 安装支持32位efi的grub(这一步可能有点坑)

(a) chroot到rootfs
$ mkdir -p /rootfs
$ mount -o rw -t ext4 /dev/mmcblk0p2 /rootfs
$ mount -t vfat /dev/mmcblk0p1 /rootfs/boot/efi/
$ mount --bind /dev /rootfs/dev
$ cd /rootfs
$ chroot .

(b) 安装grub-ia32
这一步记得不是很清楚了,因为各种deb包有奇怪的依赖关系,总之大概是这样的:
$ cd /home
$ dpkg -i *.deb
根据报错信息按需卸载系统中存在的package(dpkg --remove xxx),然后再重新执行上一句直到安装成功。

【注意】安装成功以后、在/rootfs/efi/EFI/ubuntu下面应该有 grubia32.efi 这个启动器。

(c) 重启进入efi shell
是的,因为efi配置里面还是grubx64.efi

9. 从EFI Shell启动进入系统

> fs0:
> cd EFI\ubuntu
> grubia32.efi

一切顺利的话,这里就可以启动进入ubuntu了(真心不容易啊)。

10. 更新EFI配置(依然是参考文章的Troubleshooting)

$ sudo apt-get install efibootmgr #安装EFI配置编辑器
$ sudo efibootmgr -v #查看当前的efi配置(可以看到启动器是grubx64.efi,这个不对!)
$ sudo efibootmgr -b 0003 -B  #删除所有的EFI启动配置
$ sudo efibootmgr -b 0002 -B
$ sudo efibootmgr -b 0001 -B
$ sudo efibootmgr -b 0000 -B
$ sudo efibootmgr -c -d /dev/mmcblk0 -p 1 -l \\EFI\\ubuntu\\grubia32.efi -L ubuntu #加入新的启动配置
$ sync

大功告成!


==== 后记 ====

启动以后就可以按需安装各种package拉,运行速度还不错,wifi没问题,显卡驱动正常(可以调整分辨率),但是声音和蓝牙驱动貌似还是有问题,不过对于一个server来说都是小问题。稍微麻烦一点的是wifi的延迟不太正常,局域网每两次ping的响应,差不多都会有一次是1000ms, 一次是2ms,但是加上一个有线USB网卡以后就都一切正常(这是什么病啊。。),接下来再试用几天看看拉。

@2015-08-19
T02是Intel Atom Z3735f (BayTrail)方案(x86_64 1.33GHz*4 + 2GB ram + 32GB rom),手头有一个2012年8月份入手的mk802(居然已经3年了,依然在勤恳地工作!感谢它!),全志A10的方案(Cortex A8 1GHz*1 + 1GB ram + 4GB rom),二者都安装上了Ubuntu Desktop版,正好作个对比。

实际使用上二者性能上的差距还是蛮直观的,ssh登录的时候,T02马上就进入shell,MK要卡几秒;在桌面程序上,比如Chrome,启动速度、页面加载速度什么的,感受很明显。下面是一些简单的测试数据:

1. 最简单的,编译一个 busy.c 源码(main里面只有一行 while(1)):
T02:0.124s
MK:0.251s

2. 编译bash
以前自己打过patch的一个bash的tar.gz

2.1 解压
T02:0.598s
MK:1.552s

2.2 configure
T02:1min
MK:1min56s

2.3 make
T02:2min16s(make),45s(make -j4)
MK:5m26s

3. CPU温度/频率@T02
这个只测T02:一个720p视频播放、5个busy.c的a.out,测试是否有风扇对着吹的情况
无风扇:81°C/81°C/87°C/87°C,500MHz*4(自动降频)
有风扇:72°C/72°C/79°C/79°C,1333MHz*4(继续吹可能还会再降点温)
Jul 14

vim tips 不指定

felix021 @ 2015-7-14 11:00 [IT » 软件] 评论(0) , 引用(0) , 阅读(3293) | Via 本站原创
~ vimdiff 比对文件的时候,可以使用 "]c" 或者 "[c" 跳转到下一个/上一个不同的地方。

~ retab 将文件中的tab字符按vimrc中指定的格式转换成空格

~ 未完待续
Jul 7

记一个诡异的问题 不指定

felix021 @ 2015-7-7 23:36 [IT » 操作系统] 评论(0) , 引用(0) , 阅读(9684) | Via 本站原创
今天Sandy同学在开发一个网络相关应用的时候遇到了一个奇怪的问题。

大约是这样的一个单例类Foo(以下是类python的伪代码,实际是VB.NET),当调用方法 bar('remove', key, value) 的时候,经常(而不是总是)在for循环过程中报错,错误信息是 "循环过程中_pool已经被修改" 。

class Foo(singleton):
    _pool  = {}
    _mutex = Threading.Mutex()

    def bar(self, action, key, value=None):
        self._mutex.waitOne()

        if action == 'add':
            self._pool[key] = value

        else: #'remove'
            remove_keys = []
            for key, value in self._pool.items():
                if do(key, value):
                    remove_keys.append(key)
            for key in remove_keys:
                del self._pool[key]

        self._mutex.release()

_pool 作为Foo的一个私有成员并且被 _mutex 保护着,理论上是不会出现这个问题的,然而各种排查的表现都指向了线程间的竞争问题,因为只有在调用到 bar('add', key, value) 的时候,_pool才有可能被修改到。

仔细查了一下MSDN上面Threading.Mutex的说明,在备注一栏中藏着一句话:"拥有互斥体的线程可以在对 WaitOne 的重复调用中请求相同的互斥体而不会阻止其执行。",也就是说,如果是同一个线程两次调用 bar 方法的话,这个 _mutex 就相当于失效了。

换用其他的互斥锁机制(例如Syncing)并不解决这个问题(事实上Mutex已经是第三个尝试选项了)。我们甚至试着采用Threading.Senaphore,然而却导致整个进程卡住。

于是debug.print把 Threading.Thread.CurrentThread.ManagedThreadId 输出到控制台,发现在出现错误之前是有多个不同的thread id,但是出错的时候,确实是同一个线程两次调用 bar 方法,也就是说 _mutex 确实不能解决这个问题。

经过各种排查,确认是正好在 do(key, value) 方法调用中出现的,然而 Sandy 同学信誓旦旦地保证,do(key, value) 方法绝对不可能递归地调用回到 bar 函数。由于 do(key, value) 内部调用了某个阻塞的网络请求,据此我推测,.NET的网络模型底层使用了线程+纤程的模型,那只能想办法了。通过查看 bar('add', key, value) 的调用栈,发现确实这是同一个请求,但是中间夹杂了一个未知的"外部过程",也就是说空闲的线程被调度来做其他的事情了。

深坑一个,但是既然找到了原因,就可以考虑如何针对性地去解决它,初步的想法是,Semaphore理论上应该是可以解决这个问题的,可能之前没有细看MSDN、调用方式有点问题。

Sandy同学的想法是,既然 Mutex 已经过滤掉了线程间的冲突,那我们就自己模拟 Semaphore 来解决线程内的冲突,只要简单增加一个初始值为 0 的 _counter 变量 ,在 self._mutex.waitOne() 后面加上
while self._counter != 0:
    Threading.sleep(10) #10ms
self._counter = 1

并在 self._mutex.release() 之前执行 self._counter = 0 就可以了。

想法是美好的,但是一执行就卡死在 foo('add') 调用的 while 循环里。简单分析一下就能发现,这个线程既然一直在while循环里面,就不可能被调度回到 foo('remove') 的纤程去修改 _counter=0,于是就卡住了。

没办法,再回头去仔细看MSDN,Threading.Semaphore 确实没有类似Mutex这样的同线程调用,于是把这个代码按照Example重新写了一遍,但是还是卡死了……

然后我瞬间醒悟过来——这似乎根本就是一个因为.net底层实现导致的死锁!除非上层应用能控制线程调度的细节,否则无论是信号量还是修改过的Mutex(同一个线程不能多次获得的)都不能解决这个问题。于是暂时的结论是可能要采用自己实现的线程池来进行调度,但是改动似乎很大。

完。

UPDATE @ 20150720 后来仔细考虑了下,根本问题是在do(key, value) 内部调用的那个“阻塞”请求,在临界区内本就不该调用阻塞请求。按照.net的文档,那个请求应当是非阻塞的,但是不知道为什么在这里阻塞了。由于我对.net并不了解,我没有再继续追究了。
Dec 31

记一次蛋疼的性能调优 不指定

felix021 @ 2014-12-31 16:49 [IT » Python] 评论(2) , 引用(0) , 阅读(15838) | Via 本站原创
手头项目中有一个模块,一般情况下需要用python将数十万条数据加载到一个dict中处理,其中每条数据是一个小的dict,整体速度稍微有点慢(毕竟python不适合处理大量琐碎的小对象),由于在性能要求范围内,所以也没怎么在意。

但是在最近的性能测试中用160w+数据来压的时候,发现性能恶化得厉害。虽然算法是线性的,但是实际运行时间却明显不对劲。增加一些log后,发现在处理过程中,每隔几万条数据就会出现一个很明显的lag,而且lag的时间越拉越长。

由于不像是算法本身的问题,初步猜测可能是python中dict的rehash带来的时间开销。但是根据一般哈希表的实现方法,lag出现得太平均,又不是很符合逻辑。

大胆假设,小心求证,翻了一下python源码,Objects/dictobject.c 中 "static int dictresize(PyDictObject *mp, Py_ssize_t minused)" 函数被多处调用,其中PyDict_SetItem的末尾的调用是:"dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used)",也就是说,在需要rehash的情况下,按4倍(少于50000个item)或2倍的规模扩大。

用下面这段代码测试1600w数据,将输出数据拷贝到Excel并生成图表,可以很明显地看出lag的出现规律与上述扩张规则非常相符。
begin = time.time()
i = 0
d = {}
while True:
    i += 1
    if i % 50000 == 0:
        print '%d\t%.4f' % (i, time.time() - begin)
    d[i] = i


{图一}
点击在新窗口中浏览此图片

将上述代码稍作修改,每次插入的value是个dict,测试100w数据,生成图表,每隔10w左右产生一个lag,且lag时间越拉越长,与遇到的问题现象一致。
from copy import deepcopy
data = {'abcdefg': 1234, 'hijklnm': 4.0, 'opqrst': 'uvwxyz'}
begin = time.time()
i = 0
d = {}
while True:
    i += 1
    if i % 50000 == 0:
        print '%d\t%.4f' % (i, time.time() - begin)
    d[i] = deepcopy(data)


{图二}
点击在新窗口中浏览此图片

因此大体可以判断问题出在大量零碎小对象上,很自然地,就联想到会不会是gc在捣蛋。查了一下,虽然Python对象内部是引用计数的管理方式,但是为了避免循环引用导致的内存泄漏,解释器还是内置了一个gc,当现有对象数量超过某个阈值以后扫描一下,看看是否可以回收一些空间。由于我们的代码中并不存在循环引用的对象,这种gc其实是没有意义的,于是把gc关掉再测:
from copy import deepcopy
import gc
gc.disable()
data = {'abcdefg': 1234, 'hijklnm': 4.0, 'opqrst': 'uvwxyz'}
begin = time.time()
i = 0
d = {}
while True:
    i += 1
    if i % 50000 == 0:
        print '%d\t%.4f' % (i, time.time() - begin)
    d[i] = deepcopy(data)


{图三}
点击在新窗口中浏览此图片

一条直线。
Nov 30
最近项目上有需要,大概就是有一个list里的东西需要处理,例如一堆文件什么的,于是有一个file_processor——按顺序处理一个文件列表。简单封装了一下multiprocessing这个库,发现用起来很方便,很轻松地就实现了多进程并行处理(进程间无交互):

import multiprocess

slices = multiprocess.split_list(filelist, 8) #分成8份
processes = map(lambda slice: multiprocess.spawn(file_processor, slice), slices)
sys.exit(multiprocess.start_and_join(processes))


multiprocess.py 则是这样的:
#!/usr/bin/python
#coding: utf-8

import sys
from multiprocessing import Process

def split_list(data, n_slice, hash_func=lambda i, d: i): #default: sequential
    slices = []
    for i in range(n_slice):
        slices.append([])

    for i, d in enumerate(data):
        slices[hash_func(i, d) % n_slice].append(d)

    return slices

def spawn(target, *args, **kwargs):
    return Process(target=target, args=args, kwargs=kwargs)

def start_and_join(processes, killall_if_fail=True):
    for p in processes:
        p.start()

    exitcode = 0
    for p in processes:
        p.join()
        if p.exitcode != 0:
            exitcode = p.exitcode
            break

    if exitcode != 0:
        for p in processes:
            if killall_if_fail and p.is_alive():
                p.terminate()

    return exitcode
分页: 11/100 第一页 上页 6 7 8 9 10 11 12 13 14 15 下页 最后页 [ 显示模式: 摘要 | 列表 ]