2013年十月月 发布的文章

关于程序员的构思能力的一些体会

有一段时间,我完全沉迷于在脑海中想象机械绘图和设计新机型所带来的极致享受,这是我一生中有过的最完美的精神愉悦。创造的灵感像泉水般源 源不断 地涌出,我遇到的唯一困难就是必须设法牢牢抓住它们。对我来说,构思中的设备零件都绝对是真实的,所有细节都触手可及,甚至最细微的标识和磨损状态也是如 此。想象发动机在持续不断地运转,仿佛一道迷人的风景呈现在面前,令我欣喜若狂。
                                                                                                       尼古拉. 特斯拉

看完上面这段特斯拉回忆录中的自述后,我们不禁惊叹于特斯拉超乎寻常的大脑构思能力。读完特斯拉的回忆录《被世界遗忘的天才:特斯拉回忆录》后, 我完完全全相信特斯拉是一个不折不扣的“外星人”,就是像克拉克.肯特那样被送到地球的“超人”。不同于超人的是他给地球带来的不是钢铁般的身躯和无比的 正义,而是超级智慧。他的所思所想所做所为完全超越了那个时代,甚至是当今的时代。作为程序员,我们不敢奢望能拥有特斯拉那样的超级构思力,但拥有一个良 好的构思力对于程序员来说还是蛮重要的。

【什么是构思力】

就我自己的认知和经验来说,构思力是一种“在大脑中构造事物的能力”,构造出的事物不是静态的,它在你的构思下不断演化,像是一部电影。日常生活工作中, 绝大部分人都是被动构造,当收到外界的输入时,包括影像、声音、感悟等时,在大脑中应激性的出现一些事物和场景。这种构思的持续时间很短,从长度上来说, 都是微电影,并且很难抓住并转换为现实,价值不大。真正的有价值的构思应该是主动、有意识、有目的地在大脑中构造。因此构思力常用于在创造、创作以及发明 的过程中,各个行业莫不如此。

【构思 vs. 设计】

构思与设计都需要经过脑力完成,甚为相似,难于区分,但个人觉得还是有些许差别。就就像特斯拉回忆录里描述的那种情形,我们称之为构思。构思强调事物从无到有, 都在脑中完成,是一种全脑演算。有时就是一个闪念,瞬间迸发出来,很迅速,并可被快速捕捉到,构思者往往会变得热情高涨,并在短时间内完成主体设计和实 现。构思往往一次成型,多用于整体或全局设计,是真正设计阶段开始的前置条件。构思过程会将事物的全貌在大脑中构造出来;将关键的技术难点在脑中完成突 破,形成思路;会将事物与外围接口在脑中进行对接;会对创造出来的事物在脑中进行初步的验证,证明其正确性。

设计则会将前期构思的事物分解并细化,落于纸面,或画出各种图形,多是渐进和迭代的;有时用作局部优化。

因此可以看出,构思是更高层次的设计

【程序员与构思】

程序员的日常工作与创造关系紧密,而“创新”则离不开构思。哪些工作属于构思范畴呢?目前看来比例不多,在目前这个网络四通八达发达,搜索引擎智能强大的 时代,你要的解决方案基本都能在Internet上找到,只是将现成的方案挪到你的solution中,我觉得算不上构思,顶多是设计,设计如何将现有的 东西组合起来。

构思的结果是崭新的方案或是基于已有方案的优化改进,是有脑力参与的事物演化。但构思不是必须凭空创造,多是站在巨人的肩膀上,是个借鉴再创新的过程。构思有时候可能有“重新发明轮子的味道“,但重造轮子不一定不好。

构思可大可小,Linus Torvalds设计并实现GitMatz发明Ruby等属于大构思,你将某个算法的性能提升20%可算作小构思。

在软件开发领域,构思不是技术领域专有的,业务流程或过程的创新都与构思不无关系。

Non-trivial的开源项目多是构思的结果。我个人在开源lcut, cbehave, buildc时也是深有体会的。当大脑中构思演化出目标图景时,人会变得极为亢奋,软件的主体架子在短短几个小时或一两天内就完成了。很多著名的开源项目也是如此。

【影响构思力的几个因素】

构思力高低要看大脑的活力。个人理解影响构思力高低的几个因素:

    * 脑部成像构造能力
      
        就像特斯拉那样,每个人脑部都有一定的事物成像能力,比如提到神舟发射,你脑中会呈现某种画面;再比如提到Google数据中心,你脑中又会出现何种场 景。当然这些例子还都是简单的事物还原能力。当提到让你改进神舟飞船或降低Google数据中心能耗时,你的脑中的画面会有怎样的变化呢?能否变化或能否 沿着对应的问题演化能反映出构思能力的高低。当然这是需要有领域知识、眼光和技能的。改进的神舟飞船与降低了能耗的Google数据中心是不存在的,需要 你使用纯粹的空想构造能力对其进行演进的。训练你的脑部构造能力,要求你日常勤于用脑,勤于思考,经常将各种信号输入(语言、声音、感觉)进行转换,在脑 中尝试成像,减少视觉信号的输入。记得小时候印象最深的一件事就是一边听着单田芳老师的评书,一边在大脑中构造对应的场面、人物形象和情节,我想这对我大 脑的构思成像能力是大有裨益的。

    * 知识与眼光的广博
       
        凭空的构思创造毕竟是少数,而多是站在巨人的肩膀上。这要求你对所属领域甚是是相关领域有一定的了解和认知,这样在构思时,才能如特斯拉那样思如泉涌,思想的碰撞火花四溅。这就像拍摄电影,需要在日常积累各种素材和技法,兼容并序。

    * 对问题域的透彻理解

        构思多是行业领域相关的,构思的结果都是隶属于某个领域或行业的。构思出的方案是为了解决一个明确的问题或满足特定需求,因此是否对问题有透彻的理解将直接影响构思过程和结果,以及你构思力的发挥。

以上关于构思力的论述感觉还不够系统成熟,仅是一些主观心得体会罢了,供参考并欢迎交流。

为阻塞型函数调用添加超时机制

我们产品中的一个子模块在进行Oracle实时数据库查询时,常常因数据库性能波动或异常而被阻塞在OCI API的调用上,为此我们付出了“惨痛”的代价。说来说去还是我们的程序设计的不够完善,在此类阻塞型函数调用方面缺少微小粒度的超时机制。

调用阻塞多发生在I/O操作(磁盘、网络、低速设备)、第三方API调用等方面。对于文件/网络I/O操作,我们可利用在非阻塞文件描述符上select /poll的超时机制来替代针对阻塞型文件描述符的系统调用;但在第三方API方面,多数时候是无法用select/poll来进行超时的,我们可以选择 另外一种方法:利用setjmp和longjmp的非局部跳转机制来为特定阻塞调用添加超时机制。其原理大致是:利用定时器(alarm、setitimer)设置超时时间,在SIGALRM的handler中利用longjmp跳到阻塞型调用之前,达到超时跳出阻塞型函数调用的效果。同时这种方法通用性更好些。

这个机制实现起来并不难,但有些细节还是要考虑周全,否则很容易出错。我们的产品是需要运行在LinuxSolaris两个平台下的,因此机制的实现还要考虑移植性的问题。下面简要说说在实现这一机制过程中出现的一些问题与解决方法。

一、第一版

考虑到阻塞型函数的原型各不相同,且我们的产品中对阻塞调用有重试次数的要求,因此打算将这个机制包装成一个,大致是这个模样:

#define add_timeout_to_func(func, n, interval, ret, …) \…

其中func是函数名;n是重试的次数;interval是超时的时间,单位是秒;ret是函数成功调用后的返回值,若失败,也是这个宏的返回值。

我们可以像下面这样使用这个宏:

/* example.c */
int
main()
{
    #define MAXLINE 1024
    char line[MAXLINE];

    int ret = 0;
    int try_times = 3;
    int interval = 1000;
    add_timeout_to_func(read, try_times, interval, ret, STDIN_FILENO, line, MAXLINE);
    if (ret == E_CALL_TIMEOUT) {
        printf("invoke read timeouts for 3 times\n");
        return -1;
    } else if (ret == 0) {
        printf("invoke read ok\n");
        return 0;
    } else {
        printf("add_timeout_to_func error = %d\n", ret);
    }
}

add_timeout_to_func中为阻塞型函数添加的超时机制是利用setjmp/longjmp与信号的处理函数合作完成的。

/* timeout_wrapper.h */
 

#include <setjmp.h>
#include <stdarg.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

extern volatile int invoke_count;
extern jmp_buf invoke_env;

void timeout_signal_handler(int sig);
typedef void (*sighandler_t)(int);
#define E_CALL_TIMEOUT (-9)

#define add_timeout_to_func(func, n, interval, ret, ...) \
    { \
        invoke_count = 0; \
        sighandler_t h = signal(SIGALRM, timeout_signal_handler); \
        if (h == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigjmp(invoke_env) != 0) { \
            if (invoke_count >= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        alarm(interval);\
        ret = func(__VA_ARGS__);\
        alarm(0); \
err:\
        signal(SIGALRM, h);\
end:\
        ;\
    }

/* timeout_wrapper.c */
#include "timeout_wrapper.h"

volatile int invoke_count = 0;
jmp_buf invoke_env;

void
timeout_signal_handler(int sig)
{
    invoke_count++;
    longjmp(invoke_env, 1);
}

编译运行这个程序,分别在Solaris、Linux下运行,遗憾的是两个平台下都以失败告终。

先说一下在Linux下的情况。在Linux下,程序居然不响应第二次SIGALRM信号了。通过strace也可以看出,当alarm被第二次调用后, 系统便阻塞在了read上,没有实现为read增加超时机制的目的。原因何在呢?我在《The Linux Programming Interface》一书中找到了原因。原因大致是这样的,我们按照代码的执行流程来分析:

* add_timeout_to_func宏首先设置了信号的handler,保存了env信息(setjmp),调用alarm设置定时器,然后阻塞在read调用上;
* 1s后,定时器信号SIGALRM产生,中断发生,代码进入信号处理程序,即timeout_signal_handler; Linux上的实现是当进入处理程序时,内核会自动屏蔽对应的信号(SIGALRM)以及此时act.sa_mask字段中的所有信号;在离开 handler后,内核取消这些信号的屏蔽。
* 问题在于我们是通过longjmp调用离开handler的,longjmp对应的invoke_env是否在setjmp时保存了这些被屏蔽的信号呢? 答案是:在Linux上没有。这样longjmp跳到setjmp后也就无法恢复对SIGALRM的屏蔽;当再次产生SIGALRM信号时,程序将无法处 理,也就一直阻塞在read调用上了。

解决方法:将setjmp/longjmp替换为sigsetjmp和siglongjmp,后面这组调用在sigsetjmp时保存了屏蔽信号,这样在 siglongjmp返回时可以恢复到handler之前的信号屏蔽集合,也就是说SIGALRM恢复自由了。在Solaris 下,setjmp/longjmp是可以恢复被屏蔽的信号的。

再说说在Solaris下的情况。在Solaris下,程序在第二次SIGALRM到来之际,居然退出了,终端上显示:“闹钟信号”。这是因为在 Solaris下,通过signal函数设置信号的处理handler仅是一次性的。在应对完一次信号处理后,信号的handler被自动恢复到之前的处 理策略设置,对于SIGALRM来说,也就是程序退出。解决办法:通过多次调用signal设置handler或通过sigaction来长效设置 handler。考虑到移植性和简单性,我们选择了sigaction。在Linux平台下,signal函数底层就是用sigaction实现的,是简洁版的sigaction,因此它的设置不是一次性的,而是长效的。

二、第二版

综上问题的修改,我们有了第二版代码。

/* timeout_wrapper.h */

extern volatile int invoke_count;
extern sigjmp_buf invoke_env;

void timeout_signal_handler(int sig);
typedef void sigfunc(int sig);
sigfunc *my_signal(int signo, sigfunc* func);
#define E_CALL_TIMEOUT (-9)

#define add_timeout_to_func(func, n, interval, ret, …) \
    { \
        invoke_count = 0; \
        sigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \
        if (sf == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigsetjmp(invoke_env, SIGALRM) != 0) { \
            if (invoke_count >= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        alarm(interval); \
        ret = func(__VA_ARGS__);\
        alarm(0); \
err:\
        my_signal(SIGALRM, sf); \
end:\
        ;\
    }

/* timeout_wrapper.c */

volatile int invoke_count = 0;
sigjmp_buf invoke_env;

void
timeout_signal_handler(int sig)
{
    invoke_count++;
    siglongjmp(invoke_env, 1);
}

sigfunc *
my_signal(int signo, sigfunc *func)
{
    struct sigaction act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
#endif
    } else {
#ifdef SA_RESTART
        act.sa_flags |= SA_RESTART;
#endif
    }
    if (sigaction(signo, &act, &oact) < 0)
        return SIG_ERR;
    return oact.sa_handler;
}

这里从《Unix高级环境编程》中借了一段代码,就是那段my_signal的实现。这样修改后,程序在Linux和Solaris下工作都蛮好的。但目前唯一的缺点就是超时时间粒度太大,alarm仅支持秒级定时器,我们至少要支持毫秒级,接下来我们要换掉alarm。

三、第三版

setitimer与alarm是同出一门,共享一个定时器的。不同的是setitimer可以支持到微秒级的粒度,因此我们就用setitimer替换alarm,第三版仅改动了add_timeout_to_func这个宏:

#define add_timeout_to_func(func, n, interval, ret, …) \
    { \
        invoke_count = 0; \
        sigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \
        if (sf == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigsetjmp(invoke_env, SIGALRM) != 0) { \
            if (invoke_count >= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        struct itimerval tick;  \
        struct itimerval oldtick;  \
        tick.it_value.tv_sec = interval/1000; \
        tick.it_value.tv_usec = (interval%1000) * 1000; \
        tick.it_interval.tv_sec = interval/1000; \
        tick.it_interval.tv_usec = (interval%1000) * 1000; \
\
        if (setitimer(ITIMER_REAL, &tick, &oldtick) < 0) { \
            ret = errno; \
            goto err; \
        } \
\
        ret = func(__VA_ARGS__);\
        setitimer(ITIMER_REAL, &oldtick, NULL); \
err:\
        my_signal(SIGALRM, sf); \
end:\
        ;\
    }

至此,一个为阻塞型函数调用添加的超时机制的雏形基本实现完毕了,但要放在产品代码里还需要更细致的打磨。至少目前只是在单进程单线程中跑过,而且要求每个函数中只能调用add_timeout_to_func一次,否则就会有编译错误。

以上完整代码我都放到github上的experiments repository中了,有兴趣的朋友可以下载细看。

关于编程语言学习的一些体会

Learn at least one new language every year.
                                              — Andy Hunt and Dave Thomas

自己一直是“每年学习一门新语言”的忠实拥趸,曾先后认真地学习了HaskellCommon LispPythonGo等语言,对PrologScalaErlangLuaPHP也有一定了解。但几年下来,只有Python一门语言算 是真正被留在我的大脑里,用在了工作中。其他那几门语言留下来的只是一些思想了。这似乎符合了Andy Hunt和Dave Thomas在《程序员修炼之道》中对于这一实践目的的阐述:“学会用多种方式解决问题,扩展我们的视野,避免思路僵化和停滞不前”^_^。

即便是残存的思想,其实也并不深刻。要真正会运用新思维并非那么简单。一门编程语言从入门到精通,至少要经历学语法、做实践、用idioms(写出地道的代码)三个阶段。这让我深刻的感悟到:不以使用为目的的语言学习,都是在浪费生命

有精力多学习些语言自然很好,我迫切期待能拥有一个像“七龙珠”中孙悟空那样的“精神时光屋”呢。但现实中,人的精力是有限的,而我们要面对的计算机科学领域中的知识、技能以及问题却似乎是无限的。因此在“每年至少学习一门新语言”这一实践上,建议不要过于教条。 从编程语言自身来看,范型(Paradigm)是影响语言思维差异的主要因素,而编程语言的范型有限,主流的也就那么几种:命令式(过程式)、函数式、逻 辑式、面向对象等。每种范型的背后都有几种、十几种甚至几十种语言,我们其实没有必要都去学。从拓展视野的角度去说,从每种主流范式中找到一两门典型的语 言去学习就可以了。比如命令式的,我们可以选择C;函数式我们选择Haskell;逻辑式的选择Prolog;面向对象的选择Java等。

即便是从每个范型中挑出一门,你要付出的精力依旧不少,我们还要考虑其实用性:要以使用为目的。如果能将其用在工作中,天天与你相伴,被他人接受,自然最 好;退而求其次,你能找到一两个开源项目,并参与其中也是可以的,至少可以让你保持手热;如果这两点都无法做到,仅仅是凭借个人的热情与坚持,那是不会持 久的,若干时间后,你就会对其生疏,可能连基本的"Hello World"语法都记不得了。不过这个年头,思想也不能不要。在有剩余精力的前提下,挑选些牛人们极力“鼓吹”的语言,吸收一下其思想精华,说不定哪天就 能用得上,让自己和大家都感觉你很NB,抬高一下自己的身价^_^。记住:编程语言也是要拼爹的系出名门的语言(诸如Go、Dart等)自然得到更多的青睐、使用和推广,出位的几率也就高出许多,尤其是在目前新编程语言百花齐放的阶段。因此在选择有思想的新语言时,最好在这些名门之后中做优选。

这个时代喜欢“专家”,因此我们在一两门语言上务必要做到“精专”,这是会给你带来黄油和面包的语言。要专到什么程度呢?我有一个同事,什么问题都用C解决。他甚至为此写了个不小的基础框架,所有业务问题的Code放在框架中被回调即可,即便是这个问题用Python实现只需几行代码。

计算机科学的研究核心是什么?我想肯定不是编程语言,就好比社会科学研究的核心不是人类语言一样。我比较欣赏这样的观点:作为程序员而言,最重要的是去创造,而不是研究。我们应更多的利用已经掌握的语言解决现实中的问题。做 编程语言研究的人可能要了解各种语言的特点与实现方式,但对于大多数的程序员来说,其实我们只需要关注问题域:做底层平台开发的,关注机器模型、通信原理 以及OS原理和实现细节;做算法的,很荣幸,那才是正统的程序设计的核心;前端攻城师则更多关注用户的体验。而在这些解决实际问题的过程中,我们更多采用 的是“制式”的编程语言。即做平台开发的,一般用C,C++等系统编程语言,更多的考虑的是性能;做前端开发的,PHP/JavaScript不可或缺。 我们要考虑的是如何利用这些制式的编程语言去解决问题,而在这些制式语言上,我们要做到精通。

从新兴语言中借鉴新思想,然后在旧语言中实现新语言的特性,其实更多是在旧语言中实现了某 种语法糖,你爱吃,不代表其他人也理解也爱吃,还容易被人误认为是“炫技”。如果你是技术负责人,且经过评估,新语言十分适合这个问题域,那莫不入直接引 入这门语言,让大家都能使用到这门语言的新思想、新特性。

辩证的说,任何一种编程语言都有其利与弊,比如Haskell,纯函数式语言,变量不能改变,无状态,对并行处理具有天然的适应性,但在处理基本IO时却要编写难于理解的monad;而在命令式语言中,这种IO处理简直简单的不得了。

关于函数式语言,个人感觉未来若干年内仍难以大行其道,建议还是跟上命令式语言的演化主线吧。

跨越问题域学习语言,通常收获不大。一个做平台服务端,用惯了C的资深程序员,让他去学PHP写前端代码,估计是无法迸发出任何火花的。

以上是自己这些年关于编程语言学习的一些体会,比较零散,但希望能有帮助。




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:


如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多