930 ℉

15.06.15

2015总决赛第二场:勒布朗-詹姆斯打过的,最伟大的一场总决赛

首先,这是勒布朗-詹姆斯职业生涯最伟大的一场总决赛。胜过2013年第七场(37分的夺冠之战),胜过总决赛第一场(44分)。

然后……克里夫兰骑士,队史上最伟大的一场比赛。

然而,不止因为勒布朗。


流程,比第一场还要戏剧性;场面,仿佛十年前马刺战活塞,二十一年前火箭战尼克斯。整个节奏是很东部化的——你知道,西部从来强弓骏马,东部一向鲜血黄沙。

骑士开局,试图让每个人都融入进攻。香波特与德拉维多瓦轮流持球推反击,勒布朗先行去禁区占地方找篮。第一节中,JR史密斯一上场,就是跟莫兹科夫找二人转。每个人都要试试手。反过来,骑士用局部夹击+返位,锁住了库里。

好在勇士有克雷-汤普森。

比起第一场,骑士减少了无限换防。这意味着,他们得花许多精力和时间,抠每一个防守回合,事实是他们做得很好。虽然德拉维多瓦被克雷的背身打得不善,香波特吃克雷的挡切掩护,但骑士确实让格林和库里没辙了。

比赛第二节,骑士开始用勇士之道还制勇士之身。

当年马刺嫌博古特在场上,禁区里无人可涉足,于是波波维奇让他去罚球。

西部半决赛,勇士嫌托尼-阿伦碍事,于是让博古特看防他,请他跳投。

  • 一个人不擅长什么,就逼他做什么。
  • 第二节,骑士不把博古特当人,强侧施压,对付勇士其他人;勒布朗甚至也不怎么管伊戈达拉,担当一个类似自由卫的角色,到处巡航。

西部半决赛,勇士对付兰多夫,是用德雷蒙-格林找他的挡拆,欺负兰多夫防不出来——格林可是有小前锋的速度!

可是……如果格林遇到个纯小前锋怎么办?

赛前给某报纸写的:

詹姆斯-琼斯:

前全明星周末三分王,从韦德服侍到勒布朗的靠谱三分手;如今骑士队最纯粹单一的三分手(本季季后赛出手56次其中51个三分球);在东部决赛也尝试担当勒夫的角色:摆小个阵容时站4号位,挡拆切出投定点三分。

第二节,詹姆斯-琼斯当三分手,格林反而吃瘪了:总是用三分球和灵活性欺负其他大前锋,今天遇到有人用三分球和灵活性欺负他的了。

再加上博古特一下去,骑士就用莫兹科夫欺压勇士的小球阵容内线,骑士上半场领先了。

布拉特教练太狡猾了。

但到了下半场,骑士的套路又回去了: + 勒布朗一侧持球,找弱侧三分手。 + 勒布朗左翼面筐突破,或中投,或抛射,或找罚球。 + 勒布朗高位持球,递手给德拉维多瓦转圈儿找机会走。

事实:勒布朗前12投7中,上半场独得20分,但此后22投4中,基本靠罚球了。他的体力有耗尽之时。

但他还是在找人:助攻莫兹科夫中投,助攻JR史密斯三分球,助攻香波特三分球……

以及防守。

骑士是怎么赢的?

香波特只有一个三分球得手,是在加时取得领先。但他3个抢断,而且在比赛末尾,断了勇士追分的最后一点希望。

莫兹科夫在第二节后半段简直统治比赛,博古特不在,他巨无霸一般——17分11篮板,唯一能挑剔的,就是罚球差了点。

琼斯在第二节让球队占了主动。汤普森7个前场篮板,两场合计快30个篮板球了。

全都是这些,每个角色球员,微小的细节,拼出来的。

但今天骑士真正的英雄,是德拉维多瓦。

总决赛前给某报纸写的文:

合格的挡拆启动者,能投定点三分,也能在有空间时完成急停跳投,节奏掌握颇佳,但没法单独指挥一支球队的进攻;能找到队友,懂得躲开夹击,轻易不失误,稳健;部分由于臂展的缘故(他的臂展=身高),有突破到篮下的运球能力和速率,但很难完成进攻。合格的三分射手;无私;高篮球智商。穷人版里德诺。

少年时曾挣扎于防守,但有澳大利亚人血液里的坚忍,肯投入精力,有时不免用力过度……在边线对球施压时,偶尔让人想到布鲁斯-鲍文,即:他会依靠勤奋、细节甚至小动作来弥补天分之不足,虽然实际效果还差一些;当然因为臂展的关系,在弧顶防守且没有队友支援时,会显得孤立无援。

东部决赛之后因为某些动作,成为了新闻人物,但早在两年前参加网的试训时,其实已经体现出“为了留在场上不惜一切”的拼命程度。以及:他在骑士的角色与作用,颇有些像五年前的德隆特-韦斯特。

今天,他所做的事:

追库里的每一次走位,严丝合缝,是足以与2001年泰伦-卢防守艾弗森媲美的表现——哦对了,卢现在就在骑士呢。

这不靠天赋,而靠细节。看看JR史密斯那三次二愣子失误你就知道了——对巴恩斯犯规请人家加罚,对克雷-汤普森犯规请人家打四分,最后时刻被库里骗到犯规——德拉维多瓦的细节分寸咬得非常强悍,是辛里奇式的强悍。

更微妙的是,当迫于形势必须换防时,他防过了克雷-汤普森,防过了伊戈达拉,防过了德雷蒙-格林。格林背身单打他两次居然占不到便宜,要靠“投不进欺负你个子矮我刷篮板吧”的手段来得分。

当然,还有最后时刻:不知如何,从人群里捡到了前场篮板,上罚球线,解决了比赛。

这是专注的胜利,或者说,凡人的胜利。

回到勒布朗。

这场比赛,他打得一点都不流丽,全然硬桥硬马,后半程投丢篮砸得叮当乱响。现场看比赛的韦德,一定觉得很陌生——的确,勒布朗在热那几年,一向是游刃有余,最多会犹豫,很少会打得如此艰涩生硬。

但怎么说呢……这是旧克里夫兰骑士式的勒布朗。

比赛后22投只有4中,伊戈达拉防得极好——顺便说句,伊戈达拉是勇士常规时间三分钟扭转11分的真功臣。他锁死了勒布朗,射中了三分球,乱军中助攻了巴恩斯。所以48分钟结束时,库里去和伊戈达拉拥抱时,满脸都是感激之情。

加时最后时刻,勒布朗做了一个久违的动作:过中场便起步加速,开火车般直奔篮筐找擦板点——这是2005-06季,他最爱做的招式。此后,好久不见了。

然后,被格林盖了。

为什么说这是“骑士式的勒布朗”呢?

2007年对活塞那场传奇的48分众所周知,但此前:对活塞前两场,勒布朗合计34投12中8失误;第四场19投8中,晋级的第六场11投3中。

2008年对凯尔特人第七场45分天下皆知,但此前:第一场18投2中,第二场24投6中,第三场16投5中,第四场20投7中——当然,他还是把骑士带到了第七场才败北。

四年前的此时,热vs小牛总决赛时写的:

我知道勒布朗打不好时是什么样的。2006年他面对活塞防守时曾经失误如云。2007年他打活塞头两场合计34投12中。2007年总决赛他被马刺封死了。2008年第一次去波士顿花园时他只有18投2中。但那些场次,你能够明确的认知:勒布朗在拼命,他只是像头困兽找不到出路而已。这样的勒布朗会在2007年第四场一扣劈翻拉希德,在2007年第五场得到传奇的48分,哪怕第六场命中率差,他还是在造罚球和找吉布森。2008年被缠了一整个系列赛后第七场他和皮尔斯大战波士顿花园。更不用提2009年他独自对抗魔术到最后。

他的手感会起伏,但除了2010年对凯尔特人第五场之外,他总是肯拼命打球的。手感不行用传球,传球不行拼防守,哪怕跌跌撞撞失误,他还是会一次又一次拼死突篮下。

旧骑士式的勒布朗是怎样的?2009年东部决赛第四场,骑士114比116输给魔术1比3落后。那场的细节: 就在比赛结束前4秒,刘易斯三分得手,魔术100比98领先。之后勒布朗像一列卡车一样冲向篮下(而且没有为上篮减速!),造皮特鲁斯犯规,罚中,加时。加时赛最后,刘易斯两次罚球得手115比111给骑士留了6秒,然后勒布朗在20秒暂停时一直在抬头看。他当时的表情就是这样:情况不大妙,但是,嗯……

但是之后他投了一记近8米的三分球得到自己第44分,然后立刻扑过去对刘易斯犯规。在比赛结束前,没法中场发球了,他刚过半场就轰出一发三分球,没进。斯坦-范甘迪赛后表示心有余悸,而勒布朗就用那种不死心的眼神,死死看着篮筐。

当他完全没有选择,必须一个人承担一切时,反而可以爆发出类似的火力。对活塞的48分也好,在波士顿花园的45分也好,都是这样“已经到这地步,不算计了,拼了吧”。

比如今天,第四节剩3分钟时,那个将分差拉到11的扬手远射——那时候,他肯定不会考虑自己已经投丢多少了个。

迈阿密的勒布朗,是个在体系里如鱼得水、可以命中率57%、可以自由选择进攻机会、游刃有余的指挥官。 克里夫兰的勒布朗,是个必须一个人扛一切,哪怕18投2中还是得拼命上的家伙。

今天出现的,是后一个。全场50分钟,投篮35发,罚球18次,39分16篮板11助攻,而且那些助攻不是队友跑出来的,是他一个个突破分球找到弱侧,无中生有造出来的。到后来,勇士也知道他要突破,提前收缩,但他还是一个接一个突破:因为他知道,一旦他能吸引对方夹击,哪怕球不中,内切的队友来得及补前场篮板。这仿佛强行攻城,而他自己就是攻城锤与云梯,让队友追随他的脊背,蚁附而上。

这不是他最从容的一场总决赛,但是他迄今为止,打得最伟大、最有承当的一场总决赛。所以比赛结束的瞬间,他将压抑已久的情绪爆发出来,球砸地板,张口怒吼,一切都是顺利成章。他刚在联盟最强主场,透支全力,困兽死斗,做尽了一切,耗下了队史第一场总决赛胜利。

“他防守,他抓篮板,他适时投中球。他给了我们一切。”

呃,这是勒布朗说德拉维多瓦的。不过,用来形容他自己,应该也没问题。在客场,身边没有欧文也没有勒夫,只有德拉维多瓦、汤普森、莫兹科夫、香波特和琼斯们,唯一有点妖异劲的还是JR史密斯这样的二缺,就这样带着一支平民队伍——一如他当年带着大Z、古登、吉布森、帕夫洛维奇们似的——在常规赛67胜的西部冠军手里,硬抢出一场胜利。而且是队史第一场总决赛的胜利。

在他被克里夫兰骑士选中,整整十二年后。


来点调剂

jack.zh 标签:NBA 继续阅读

820 ℉

15.06.15

2015总决赛第一场:勒布朗霸王神勇,然而未能过乌江

上一次NBA总决赛第一场便打加时?2001年总决赛。

上一次NBA总决赛有人得到单场44分以上?2001年总决赛第一场——艾弗森48分,鲨鱼44分20篮板。 2001年,也是常规赛MVP、天下第一精灵后卫(阿伦-艾弗森)与联盟实际上的霸王(鲨鱼)碰面。 今天,常规赛MVP精灵后卫库里和联盟实际上的霸王勒布朗撞上了。

真巧合。


2015年总决赛第一场的前50分钟,尽善尽美的,有你想要的一切:大比分领先(骑士第一节一度29比15领先),逆转(勇士反超,比赛最后时刻还领先3分),反逆转(骑士追平),足以成为话题的“如果”(欧文盖掉库里的绝杀上篮),个人神威(勒布朗-詹姆斯常规时间不可阻挡的42分),戏剧性场面(第一节末和第三节末伊戈达拉各一个扣篮)。

但最后三分钟,比赛结束得有些快了。

开场的一切,没出太大意料:克雷-汤普森对位欧文;双方都竭力推速度;骑士用大量换防对付勇士的挡拆;勒布朗开场便恶狠狠地左腰要位背身单打。半节下来双方8比9,跳投手感都很糙。到此为止,比赛是博古特的内线统治vs勒布朗的为所欲为——你也可以看成是赤木刚宪vs牧绅一。

但这个节奏,是骑士喜欢的——实际上,全场的节奏,都是骑士喜欢的。

骑士5分钟打出17比2,靠的是以下手段: + 无限换防,欧文死追库里。库里前6投2中。 + 勒布朗和欧文大胆追身反击。 + 汤普森和莫兹科夫冲前场篮板。 + 勒布朗左腰要位后,或背身单挑,或找汤普森和莫兹科夫的内切袭篮。

最重要的细节:骑士的前场篮板+退防,实在太好。双内线尤其是汤普森袭击每个前场篮板,同时两翼提早退防限制勇士一传。领防库里运球推进时,还不忘过半场扩一扩汤普森。勒布朗甚至连个罚球不进前场篮板都要去抢。

切割空间,寸土必争的壕沟战。JR史密斯三分得手,骑士29比14领先后,勇士看上去有麻烦了。

但昨天写过了:勇士是可以无限变形的多面体

*斯贝茨……*常规赛时是球队首席替补内线,作用堪比两年前的兰德里,角色类似于穷人版大卫-韦斯特,但季后赛被艾泽利代替……他与大卫-李一样代表了传统好内线:能得分,有技巧,但防守端缺乏速率,不足以抵上他们进攻端的优势。在这个“内线防守好进攻时负责挡人和内切接球就好了”的时代,他和大卫-李的价值主要是奇兵。

第二节,斯贝茨成了奇兵。骑士用换防+锁三分线+空禁区,欺负博古特没射程,格林犹豫不敢中投,于是斯贝茨上来。外加伊戈达拉、利文斯顿和巴博萨领衔的第二部队。

斯贝茨的所作所为:勇士逼出换防后,由他背身单打香波特;然后是两个中投,再高低位连线助攻格林上篮,勇士追到33比36。

然后便是库里:用挡拆逼出换防后,用无球走位摆脱汤普森,底角三分点燃比赛,再追身三分,勇士39比36反超

昨天写过这套路: > 勇士的挡拆,骑士可以用无限换防来对付。但假设骑士换防,没有内线可以对位库里,除非让勒布朗防格林,然后换防库里。反过来,火箭也用换防追过库里, 然并卵,被勇士用换防后的无球走位给破解了。

之后,骑士进入了勒布朗和欧文节奏。

从第二节被反超直到比赛结束,骑士的套路一言以蔽之,便是: + 勒布朗左腰无限背身单打。被夹击就分球。 + 勒布朗与欧文各自清空一侧后找挡拆。 + 比赛末尾,勒布朗和欧文打挡拆,偶尔加入第三者做掩护。

杰夫-范甘迪在第四节很不高兴:“太多单打了,我真不喜欢。”

刚说完,勒布朗绕过掩护跳投得手,旁边只好帮腔:“有效就好了嘛!”

直到48分钟的最后一刻,骑士还有赢球希望的, 但进了加时,就是另一回事了。


昨天写的: > 布拉特教练的应变有一点远胜过斯波厄斯特拉教练:他没有一根筋的“双人夹击赌博到死”,而是会针对性使用各类防守策略。夹击持球者、换防、硬延阻或软延阻,收缩或对球施压,都很溜。但进攻端,骑士的套路主要是:勒布朗和欧文各一侧,找高位挡拆;汤普森和莫兹科夫各一侧找挡切,转移球到弱侧;如果无效,就换小个阵容,勒布朗进左腰背身单打……

勇士的调整是全队的,因为库里、汤普森都有持球和无球走位技能,格林尤其全面,做什么都行;巴恩斯、伊戈达拉、利文斯顿也可以随便拆卸配成超小个阵容。每个人都是零件,随插随用,尤其是格林。怎么都拼得成型。

而骑士的调整,基本是练勒布朗。

今天的骑士,用一句话来形容,那便是大卫-布拉特教练上半场的怒吼:

“绝不能丢掉专注力!不能走神哪怕一分钟!”

是的,全队专注,然后就是看勒布朗。

他们的领先,依靠的是勤奋专注地换防、切割勇士的传球线路、逼迫勇士单打。汤普森15个篮板球包括6个前场篮板,莫兹科夫的内切得到1分。香波特4个抢断,而且总在做各类小事情来为骑士获得球权。 欧文是最感人的。23分7篮板6助攻4抢断只是小事,他不断游动找挡拆也无所谓。但他几乎靠着防守为骑士赢了比赛。

欧文?防守??但他结结实实盖掉了库里两个帽,尤其是48分钟末尾那一下。如果骑士赢了,这会是传说。

但勇士赢球,是另一种方式。

巴恩斯在下半场一度接管了比赛。

格林一直在无微不至的做掩护和挡拆。

博古特开场那两个封盖不提,全场骑士都在试图“单侧清空,吸引博古特注意,转移弱侧”,简直是大白鲨。

汤普森在第四节单挑欧文那段锁住了比分。

利文斯顿做了一切小细节勾当。

伊戈达拉在第一节末尾扣篮挽回了一点士气,第三节末尾抄掉勒布朗的球扣篮追平比分是为功臣。而且,他一改巴恩斯第三节“逼勒布朗走中路”的套路,几乎是勾引勒布朗底线翻身跳投——效果并不坏。

斯贝茨8分钟得了8分,中投如神,全场第一奇兵,真真是大卫-韦斯特般的表现。

艾泽利在加时赛作为首发出场配小球阵容,控制了禁区。

巴博萨和利文斯顿一起推了许多次快攻。

库里在第二节中段接管比赛反超,在加时赛靠四个罚球拉开比分。而且,他一直在利用自己的牵制力,给克雷、斯贝茨们送球。

勇士的火焰是断续而来的,开场是博古特,此后是利文斯顿和伊戈达拉,第二节是斯贝茨与库里,然后是巴恩斯和汤普森。他们变了无数阵容,加时赛用艾泽利配小球阵容出场绝对惊艳。每个人都参加了一点变化。

而骑士,还是勒布朗。


比赛开始前,勒布朗满脸都是*“老子来过这里,老子对这里很熟悉”*的表情,迈克-米勒朝他尖叫,仿佛士兵鼓励元帅:

“统治比赛吧!统治!!”

他也确实做到了。

比赛前半节,得到7分。上半场19分,第三节12分,第四节10分。

背身靠打,强行突破,无休无止。三分球只是偶一为之,其他时候,坚决地、恶狠狠地背身攻击篮筐。巴恩斯、格林、汤普森、伊戈达拉换了一茬又一茬,但勒布朗的背身单挑无法阻挡——不只是命中率,还有他的势头。

上一个这样在总决赛肆无忌惮背身碾压对手的球员?嗯,鲨鱼……

长久以来,可以挑剔勒布朗的,便是他比赛中的选择。2011年总决赛第二场开始,他犹豫了;2013年总决赛前三场,他不太肯投篮;2007年东部决赛第一场把绝杀球让给马绍尔,2010年对凯尔特人第五场拒绝突破篮下——总是如此。

所以,每当他打出2007年对活塞48分包括连续25分、2012年对凯尔特人第六场上半场30分全场45分那种果毅刚烈的比赛时,便让人无法不想:

“如果他就这么一路杀气四溢打下去,多好!”

今天便是这样。勇士不夹击勒布朗,看他是否能一个人解决比赛;而勒布朗也老实不客气,一路碾压背身。这种杀气,是鲨鱼式的。“你们不夹击我,不信我一个人搞得死你们是吧?”

而且他真的,差点就赢了。这一晚,他的打法活脱是鲨鱼,但他的杀气,真的就像十四年前的阿伦-艾弗森。

上一次勒布朗输得如此热血?2008年对凯尔特人第七场,在波士顿花园的45分。

一个细节可以告诉你,勒布朗的统治有多可怕。第四节剩9分钟,勒布朗走到场边预备出场时,甲骨文球馆发出了一阵情感复杂的吁叹声,就像看见一个电锯杀人狂正走向你似的。

比赛剩3分半,库里单挑勒布朗中投,勇士94比93领先。下一回合,勒布朗背身单挑伊戈达拉,翻身底线跳投。不中。

甲骨文球馆发出了到那时为止,全场最大的欢呼。仅仅是对方投丢了一个球。那种声音,就像“他居然没进?我们劫后余生了!我们太幸运了!”

所谓统治力,也无非就是对方会为你的每次失手而庆幸。因为大家预设了:这家伙所当者破,所击者服,简直不可阻挡。

这一晚,霸王神勇,但到底输了十面埋伏;欧文的防守让骑士得以溃围到了乌江边,但库里们,到底没放骑士过去。

哦对了:

马克-杰克逊老师每次在解说里出镜,都是一脸瘪着嘴:

“我前妻选上世界小姐了,现在正在跟休-杰克曼拍恋爱真人秀呢,而我还得给他们在一边做解说”的表情

实在是太棒了……


来点调剂

jack.zh 标签:NBA 继续阅读

959 ℉

15.06.08

Tornado异步与延迟任务

曾经研究过Tornado异步操作,然而一番研究后发现要使一个函数异步化的最好方法就是采用相关异步库,但目前很多功能强大的库都不在此列。经过一番查找文档和搜索示范,终于发现了ThreadPoolExecutor模块和run_on_executor装饰器。用法就是建立线程池,用run_on_executor装饰的函数即运行在其中线程中,从而从主线程中分离出来,达到异步的目的。 另外,TornadoIOLoop实例还有IOLoop.add_callback(callback, *args, **kwargs)方法,文档中的描述如下:

Calls the given callback on the next I/O loop iteration.

It is safe to call this method from any thread at any time, except from a signal handler. Note that this is the only method in IOLoop that makes this thread-safety guarantee; all other interaction with theIOLoop must be done from that IOLoop ‘s thread. add_callback() may be used to transfer control from other threads to the IOLoop ‘s thread.

意思就是在执行add_callback方法后马上就会执行下一行代码,而callback函数将在下一轮事件循环中才调用,从而就能实现延迟任务。在Web APP中应付HTTP请求时,当有一些耗时操作并不需要返回给请求方时,就可以采用延迟任务的形式,比如发送提醒邮件。

示范代码如下:

#!/bin/env python
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient
import tornado.gen
from tornado.concurrent import run_on_executor
# 这个并发库在python3自带;在python2需要安装sudo pip install futures
from concurrent.futures import ThreadPoolExecutor
import time
from tornado.options import define, options
define("port", default=8002, help="run on the given port", type=int)

class SleepHandler(tornado.web.RequestHandler):
    executor = ThreadPoolExecutor(2)

    def get(self):
        # 这样将在下一轮事件循环执行self.sleep
        tornado.ioloop.IOLoop.instance().add_callback(self.sleep)
        self.write("when i sleep")

    @run_on_executor
    def sleep(self):
        time.sleep(5)
        print("yes")
        return 5


if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application(handlers=[
            (r"/sleep", SleepHandler), ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

当然,当需要用到一部网络请求IOLoop的时候,这样做还是最好的:

class GetRemoteIpHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def post(self):
        client = tornado.httpclient.AsyncHTTPClient()

        headers = self.request.headers
        if "X-Real-Ip" in headers:
            remote_ip = headers['X-Real-Ip']
        else:
            remote_ip = self.request.remote_ip

        data_str = urllib.urlencode({"ip": self.get_argument("ipstr", remote_ip)})
        url = "http://ip.taobao.com/service/getIpInfo.php?" + data_str
        client.fetch(url, callback=self.on_response)

    def on_response(self, response):
        jsonObj = json.loads(response.body)
        self.write(response.body)
        self.finish()

jack.zh 标签:python 继续阅读

763 ℉

15.05.28

译:理解并掌握 JavaScript 中 `this` 的用法

按:本文原文来自 Javascript.isSexy 这个网站。这篇文章和文中提到的另一篇文章解决了我一直以来对 this 和 apply, call, bind 这三个方法的困惑。我看过很多国内相关的技术文章,没有一篇能让我彻底理解这些概念的。因此我决定把它译过来,不要让更多的初学者像我一样在这个问题上纠结太长时间。

JavaScript 中,this 这个关键字常常困扰着初学者甚至一些进阶的开发者。这篇文章旨在完完全全阐明 this。当你读完本文之后,你就再也不会为 this 所困惑了。你将会理解 this 的各种使用场景,包括那些最难懂的情形。

我们使用this 的方式和在英语或法语中使用代词的方式十分类似。我们会这样写「李华正在飞快地跑着,因为 正在赶火车。」注意这里代词「他」的用法。我们也可以这样写:「李华正在飞快地跑着,因为李华正在赶火车。」我们通常不会把「李华」这个名字像这样重复使用,因为这样显得很神经。类似地,在 JavaScript 中,我们使用 this 作为一种指代。它指代一个对象(object),也就是那个上下文中的主语,或者说运行时的主体。考虑下面这个例子:

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    fullName: function () {
        // 注意我们使用「this」关键字就像我们在上文中使用「他」一样
        console.log(this.firstName + " " + this.lastName);
        // 我们也可以这样写
        console.log(person.firstName + " " + person.lastName);
    }
}

如果我们使用 person.firstNameperson.lastName 这种写法的话,我们的代码就会变得有歧义。假设有一个全局变量(我们或许有意为之,或许根本没有意识到)的名字也叫 person ,那么 person.firstName 将会尝试读取那个全局变量 person 中的 firstName 属性,这将可能导致极难调试的错误。所以我们使用 this 关键字,不仅仅是因为这看起来十分优雅,还因为这样使用更加准确。使用 this 消除了我们代码中的歧义,就像在上文中使用「他」让我们的话显得更加清晰一样。它让我们明白我们想要指代的李华就是句子刚开头提到的那个李华。

就像代词「他」用来指代之前提到的人一样,this 这个关键字也是用来指代那个被当前函数(就是使用了 this 的函数)绑定的对象。this 这个关键字不仅仅是指代那个对象,并且包含了那个对象的值。这很类似代词,this 可以被视作是指代「上下文」中对象(也称为「祖先对象」)的一种便捷的方式(同时也是一种没有歧义的替换)。我们将在后面学习更多关于「上下文」 的概念。

JavaScript this 用法基础

首先,我们已经知道在 JavaScript 中,函数和对象一样都有属性。而当一个函数执行的时候,它就获得了 this 这个属性。而 this 其实就是一个具有调用当前函数的对象的值的变量。

this 这个变量 永远 指向 一个 对象,并且拥有这个对象的值。虽然 this 可以在全局作用域中出现,但它通常还是会在函数体内或对象的方法内。有一点要注意的是,当我们使用严格模式(strict mode)的时候,this 在全局函数中和匿名函数中的值是未定义的(undefined),不指向任何一个对象。

this 在一个函数体内出现的时候(设为函数 A ),它包含了调用函数 A 的那个对象的值。我们需要使用 this 来读取调用函数 A 的那个对象的方法或是属性。而这在我们不知道那个对象的名字,甚至有时候那个对象没有名字的情况下就变得尤为重要。实际上,this 真的仅仅就是对「祖先对象」,或者说调用这个函数的那个对象,的一个便捷的指代而已。

我们用一个例子来展示 JavaScript 中 this 的一些基本用法,也来回顾一下上文的内容:

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    // 因为 `this` 关键字在 showFullName 方法中被用到,而 showFullName 在 person 这个对象中被定义,
    // 所以 `this` 将会具有 person 这个对象的值,因为 person 对象将会调用 showFullName()
    showFullName: function() {
        console.log (this.firstName + " " + this.lastName);
    }
}
​
person.showFullName(); // Penelope Barrymore

再来看看 jQuery 中 this 用法的例子:

// 一段非常普遍的 jQuery 代码​
    $ ("button").click (function (event) {
    // $(this) 将具有那个 ($("button")) 按钮对象的值
    // 因为那个按钮对象调用了 click() 方法
    console.log ($(this).prop("name"));
});

我来解释一下上面的这个 jQuery 示例:$(this) 是 jQuery 中与 JavaScript 中this 类同的语法,它被用在一个匿名函数中,而这个匿名函数在一个按钮的click() 方法中被执行。$(this) 之所以具有这个按钮对象的值是因为 jQuery 库把 $(this) 和那个调用了 click 方法的对象手动 绑定 (bind)在一起了。 因此,即使$(this) 是在一个匿名函数中被定义,并且自身不能读取外部函数中的this 变量,它仍然能够具有那个 jQuery 按钮对象 ($("button")) 的值。

注意,按钮(button)是一个 HTML 页面上的 DOM 元素,同时也是一个对象;在上面这个例子中的按钮是一个 jQuery 对象,因为我们把它包装在 jQuery 的$() 函数中了。

理解 JavaScript this 的关键

如果你理解了 JavaScript this 的以下这个原则的话,那你对 this 这个关键字就会有一个清晰的认识了:只有一个对象调用了包含 this 的函数的时候,this 才会被赋值。我们不妨把包含 this 的函数称作 this 函数。

在一个对象方法中定义的 this 看起来好像指向了这个对象本身,但仍然只有在某个对象调用了这个 this 函数 的时候它才被赋值。并且被赋的那个值 只依赖于 调用了 this 函数 的那个对象。虽然在大多数情况下, this 都是那个调用了 this 函数 的那个对象,但也有一些情况不是这样的。我将会在后文中讲到这一点。

在全局作用域中使用this

在全局作用域中,当代码在浏览器中执行的时候,所有的全局变量和函数都被定义在 window 对象上。因此,当我们在全局函数中使用 this 的时候,它会指向全局 window 对象并且拥有它的值(除非在严格模式下),此时的 this 就成了整个 JavaScript 应用程序或者说整个网页的主容器。

所以:

var firstName = "Peter",
    lastName = "Ally";
function showFullName () {
    // 在这个函数中,this 将会拥有 window 对象的值
    // 因为 showFullName() 函数,和 firstName, lastName 一样是定义在全局作用域的
    console.log (this.firstName + " " + this.lastName);
}
var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    showFullName:function () {
        // 下面这行中的 `this` 指代 person 对象,因为 showFullName 这个函数将会被 person 对象调用
        console.log (this.firstName + " " + this.lastName);
    }
}
showFullName (); // Peter Ally​

// 所有的全局变量和函数都定义在 window 对象上面,所以:
window.showFullName (); // Peter Ally​

// 在 person 对象中定义的 showFullName() 函数中的 `this` 仍然指向 person 对象,所以:
person.showFullName (); // Penelope Barrymore

this 最容易被误解和难以掌握的情景

this 关键字在以下场景中常常被误解:当我们借用一个使用了 this 的方法的时候;当我们把一个只用了 this 的方法赋给一个变量的时候;当一个使用了 this 的方法被当作回调函数传入的时候;当 this 在闭包中使用的时候。我们能过举例来详细地解释在上面的每一种情形中如何使 this 拥有合适的值。

一点重要的提示

在接下去讲之前,我们先来谈谈「上下文」(Context)这个概念

在 JavaScript 中,上下文的概念和一个英文句子中主语的概念相类似:「John is the winner who returned the money.」这句话中的主语是 John ,我们可以说这句话的语境(上下文)是 John ,因为这句话此时的关注点在 John 身上。代词「who」也是指代先行词 John。正如我们可以使用分号来切换句子的主语一样,我们可以通过让另一个对象去调用本对象的方法的方式来切换上下文。

用代码可以这样描述

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    showFullName: function() {
        // 「上下文」
        console.log(this.firstName + " " + this.lastName);
    }
}

// 当我们在 person 对象上调用 showFullName() 方法的时候,「上下文」是 persion 对象。
// 这时在 showFullName() 方法里面使用的 `this` 就拥有了 person 对象的值
person.showFullName(); // Penelope Barrymore

// 当我们使用另一个对象来调用 showFullName 的时候
var anotherPerson = {
    firstName: "Rohit", 
    lastName: "Khan"
};

// 我们可以使用 apply 方法来显式地设置 `this` 的值。关于 apply() 方法,我们将在后文中详细解释
// `this` 得到的永远是调用它的那个对象的值,因此:
person.showFullName.apply(anotherPerson); // Rohit Khan

// 所以现在上下文就变成了 anotherPerson ,因为是 anotherPerson 使用 apply() 方法调用了 person.showFullName() 方法

在下面这些情景中,this 关键字可能会变得十分难以理解。我们在示例中同时给出了解决有关 this 使用错误的方案。

1. 解决当包含 this 的方法被当做回调函数时遇到的问题

当我们把含有 this 的方法当做回调函数的时候代码往往变得十分难以理解。比如:

// 我们有一个简单的对象,它有一个 clickHandler 方法,我们想要使当页面上的一个按钮被点击时它被调用
var user = {
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],
    clickHandler: function(event) {
        var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 产生 0 到 1 之间的随机数

        // 下面这行会随机打印出一个 data 数组中的人的姓名和年龄 
        console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
    }
}

// 这个 button 被 jQuery 的 $ 包装起来了,所以它变成了一个 jQuery 对象
// 下面这行会输出 undefined 因为 button 对象没有 data 属性
$("button").click(user.clickHandler);

在上面的代码中,按钮($("button")) 是一个对象,我们把 user.clickHandler 传入它的click() 方法作为一个回调函数,这时候我们就明白 user.clickHandler 方法里面的 this 已经不再指向 user 这个对象了。因为 this 是定义在 user.clickHandler 方法里的,所以它现在指向那个调用了user.clickHandler 的对象。而那个对象就是 button 对象。也就是说,user.clickHandler 将会在 button 对象的 click 方法中被执行。

注意在调用 clickHandler() 时,我们虽然写成了 user.clickHander 的形式(事实上我们必须这么写,因为 clickHandler 是在 user 对象中被定义的),但clickHandler 还会在 button 对象的上下文中被执行,this 也因而指向了 button 对象。

讲到这里,我们应该发现当上下文发生变化的时候,换句话说就是当我们在别的对象中调用了本对象内定义的方法的时候,this 关键字就不再指向定义 this 时的那个对象了,而是指向了调用了那个 this 所在方法的对象。

解决 this 方法被当作回调函数传递时指向错误的方法:

因为我们确实想要让 this.data 指向 user 对象的 data 属性,我们可以使用 bind(), apply(), call() 这三个方法来显式地设置 this 的值。

我还写了另一篇文章,Javascript 进阶:Apply, Call 和 Bind 方法详解 来详细解释这三种方法的用法,包括如何使用它们在各种容易出错情景下正确地设置 this 的值。我就不在这里贴出整篇文章了,推荐读者详细地阅读整篇文章,因为我认为要想成为 JavaScript 的高级开发者,和这三种方法打交道是不可避免的。

为了解决上面例子提到的那种问题,我们可以使用 bind 方法:

我们把下面这行:

$("button").click(user.clickHandler);

改正为下面这样,把 clickHandleruser 绑定起来:

$("button").click(user.clickHandler.bind(user));

查看 JSBin 上的在线示例

2. 解决当 this 出现在闭包内遇到的问题

另一个 this 常常被误解的情景是当我们使用闭包的时候。一个非常值得注意的地方是,闭包不能直接通过使用 this 来访问外层函数的 this 变量,因为 this 变量只有当前函数本身可以访问,而其内层函数是访问不到的。举个例子:

var user = {
    tournament: "The Masters",
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],

    clickHandler: function() {
        // 在这里使用 this.data 是可以的,因为 `this` 指向 user 对象,而 data 是 user 对象的一个属性
        this.data.forEach(function(person)) {
            // 但是在内层匿名函数中(就是我们传给 forEach 方法的函数),this 不再指向 user 对象了
            // 这个内层函数不能访问外层函数的 `this` 变量了
            console.log("What is `This` referring to? " + this); //[Object Window]
            console.log(person.name + " is playing at " + this.tournament);
            // T. Woods is playing at undefined
            // P. Mickelson is playing at undefined
        });
    }
}

user.clickHandler(); // 现在 `this` 指向什么?[object Window]

在匿名函数内部的 this 不能获得外层函数 this 的值,所以当没有使用严格模式的时候,它就被绑定在了全局 window 对象上了。

在内层函数中维持 this 的值的方法:

为了解决传入 forEach 的匿名函数中 this 值不正确的问题,我们使用一个常用的解决办法,即当我们进入 forEach 的时候,提前把 this 的值存到另一个变量中去。

var user = {
    tournament: "The Masters",
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],

    clickHandler: function(event) {
        // 为了当 `this` 还指向 user 对象的时候把它的值保存下来,我们把它存到另一个变量中
        // 我们把 `this` 保存到 theUserObj 变量中去,这样我们就可以在之后使用了
        var theUserObj = this;
        this.data.forEach(function(person) {
            // 我们将 this.tournament 替换成 theUserObj.tournament
            console.log(person.name + " is playing at " + theUserObj.tournament);
        });
    }
}

user.clickHandler();
// T. Woods is playing at The Masters
// P. Mickelson is playing at The Masters

值得注意的是,许多 JavaScript 开发者喜欢把 this 存在一个叫做 that 的变量中(就像下面的代码那样)。我觉得用 that 来命名使用的时候十分不方便,所以尽量使用一个合适的名词来描述 this 所指向的对象,所以我在上述代码中使用了 var theUserObj = this

// 一种十分常见的写法
var that = this;

查看 JSBin 上的在线示例

3. 解决把一个 this 方法 赋给一个变量时出现的问题

当我们把一个使用了 this 的方法赋给一个变量的时候,this 的值很可能出乎我们的意料,指向了其他的对象。我们来看一个例子:

// 这个 data 变量是一个全局变量
var data = [
    {name: "Samantha", age: 12},
    {name: "Alexis", age: 14}
];

var user = {
    // 这个 data 变量是 user 对象的一个属性
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],
    showData: function(event) {
        var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 0 和 1 之间的随机数

        // 下面这行随机打印一个 data 数组中的人的信息
        console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
    }
}

// 把 user.showData 赋值给一个变量
var showUserData = user.showData;

// 当我们执行 showUserData 函数的时候,打印在 console 中的值来自于全局的 data 数组,而不是 user 对象的 data 属性
showUserData(); // Samantha 12 (来自全局 data 数组)

当把含有 this 的方法赋值给一个变量时维持 this 的值的方法

我们可以使用 bind 方法来显式地设置 this 的值来解决这个问题:

// 把 showData 方法和 user 对象绑定起来
var showUserData = user.showData.bind(user);

// 现在我们可以从 user 对象中获取值了,因为 `this` 关键字和 user 对象绑定在一起了
showUserData(); // P. Mickelson 43

4. 解决当借用方法的时候 this 的值不正确的问题

在 JavaScript 开发中,借用方法(borrow methods)是一个很常见的用法,作为一个 JavaScript 开发者,我们肯定会在实践中不断地遇到这个问题。而且每次我们也乐于使用这种节约时间的方法。如果你想了解更多关于方法借用的问题,请阅读我的这篇详细解析的文章,Javascript 进阶:Apply, Call 和 Bind 方法详解。

让我们来看看当处于借用方法这样的上下文的时候,this 的相关表现:

// 我们有两个对象。其中一个有一个叫做 avg() 的方法,而另一个没有
// 所以我们想借用一下 (avg()) 这个方法
var gameController = {
    scores: [20, 34, 55, 46, 77],
    avgScore: null,
    players: [
        {name: "Tommy", playerID: 987, age: 23},
        {name: "Pau", playerID: 87, age: 33}
    ]
}

var appController = {
    scores: [900, 845, 809, 950],
    avgScore: null,
    avg: function() {
        var sumOfScores = this.scores.reduce(function(prev, cur, index, array) {
            return prev + cur;
        });

        this.avgScore = sumOfScores / this.scores.length;
    }
}

// 如果我们执行下面的代码,
// gameController.avgScore 属性将会被设置为 appController 对象的 scores 数组的平均数

// 不要执行下面这行代码,这只是用来说明的,而我们现在想让 appController.avgScore 保持 null 值
gameController.avgScore = appController.avg();

在 avg 方法中的 this 不会指向 gameController 对象,而会指向 appController 对象,因为它是被 appController 对象所调用的。

解决当借用方法时 this 指向出错的问题

要解决这个问题,我们只要确保在 appController.avg() 中的 this 指向 gameController 就可以了。我们可以使用 apply() 方法来实现:

// 注意我们使用的是 apply() 方法,所以第二个参数必须是一个数组,这个数组中包含了要传入appController.avg() 的参数
appController.avg.apply(gameController, gameController.scores);

// 即使我们从 appController 对象中借用了 avg() 方法,gameController 的 avgScore 属性仍被成功地设置了
console.log(gameController.avgScore); // 46.4

// appController.avgScore 的值仍然是 null。它没有被更新,只有 gameController.avgScore 被更新了
console.log(appController.avgScore); // null

gameController 对象借用了 appControlleravg() 方法。在 appController.avg() 中的 this 的值会被设置成 gameController 对象,因为我们把 gameController 作为第一个参数传入了 apply() 方法中。传入 apply() 方法的第一个参数会被显式地设置为 this 的值。

查看 JSBin 上的在线示例

结语

我希望你对 JavaScript 中的 this 关键字已经理解了。现在你有了必需的工具(bind, apply, call 方法,和把 this 赋给一个变量)来帮你解决在各种情形下关于 this 的问题了。

正如我们在上文中看到的,this 在有些情况下可能会变得很难以处理,比如原始的上下文(就是 this 定义的地方)发生改变的时候,尤其是在回调函数中,或者被另一个对象调用的时候,再或者是当方法借用的时候。但是只要记住 this 永远具有那个调用 this 函数 的对象的值,就不会出错。

jack.zh 标签:js 继续阅读

955 ℉

15.05.28

说说这篇「说说这篇「我为什么从python转向go」」

说明:

一楼:我为什么从python转向go

二楼:说说这篇「我为什么从python转向go」

恩看了这篇我为什么从python转向go,看来作者也是 KSO 轻办公/企业快盘团队的。作为快盘从无到有时期的工程师之一(总是被潇洒哥说他们改我留下的 bug ),又恰好是 Python/Go 双修(大雾其实我是 Rust 党),其实一开始我是拒绝的,duang duang duang,那就随手写一点把。

恩看了这篇<说说这篇「我为什么从python转向go」>,深有同感,看来作者也是 Python/Go 双修,忍不住还是说说吧,那就随手写一点把。

一段段来吧,首先作者说 Python 是动态语言

我也想这么来


python是一门动态语言,不是强类型系统。对于一个变量,我们有时候压根不知道它是什么类型,然后就可能出现int + string这样的运行时错误。

在python里面,可以允许同名函数的出现,后一个函数会覆盖前一个函数,有一次我们系统一个很严重的错误就是因为这个导致的。

事实上,如果是静态检查,pylint 和 pyflakes 是可以做这件事的,虽然不能和 go 那种静态编译型语言比,但也足够了。如果没记错的话,阿通当年是要求全组都在提交前做静态检查的。我认为这种问题更多的应该是人员素质上来避免,毕竟葱头也说过,代码自己写的就要多回头看看,看能不能重构,能不能做更好。不是说偷懒不行,但是从中得出 Python 动态特性太灵活,Python:怪我咯?

另外,函数作为第一对象,在 Python 中是 feature,Go 要写个 mock,简直虐得不要不要的。

说的让人烦点的话,你不规范编码,不做Review,不知道使用工具,你让我怎么说你呢?OpenStack这样的工程,按你的想法就应该离Python远远的?亲,多了解一下Python社区的工具和编码规范吧,做好团队的code Review吧。


其实这个一直是很多人吐槽python的地方,不过想想,python最开始是为了解决啥问题而被开发出来的?我们硬是要将他用到高性能服务器开发上面,其实也是有点难为它。

如果没记错,无论是轻办公还是快盘,是重 IO 不重 CPU,最大耗时是数据块加密那块,我在的时候是 Java 写的。另外高性能服务器选 Go 也是虐得不要不要的,各种小心翼翼避免 GC。大多数极端情况下,pypy 的性能足矣胜任了,我认为这不算充分条件。

我不是很了解快盘的业务,如果真的是主要的重IO,这个Python还是不错的,很多异步的解决方案。


python的GIL导致导致无法真正的多线程,大家可能会说我用多进程不就完了。但如果一些计算需要涉及到多进程交互,进程之间的通讯开销也是不得不考虑的。

其实,Python 有宏可以绕开这个 GIL,但是呢架构设计得好其实可以避免的,到异步那块我会说。

无状态的分布式处理使用多进程很方便,譬如处理http请求,我们就是在nginx后面挂载了200多个django server来处理http的,但这么多个进程自然导致整体机器负载偏高。

Python 有宏可以绕开这个 GIL,这个真的不了解,查查去。不过GIL是个问题,我在工作中也被他碰一鼻子灰,但主要的原因还是单服务器,重CPU,重实时性,这个时候只能多进程模型了。


但即使我们使用了多个django进程来处理http请求,对于一些超大量请求,python仍然处理不过来。所以我们使用openresty,将高频次的http请求使用lua来实现。可这样又导致使用两种开发语言,而且一些逻辑还得写两份不同的代码。

如果推测没错,你们现在还在用五年前写的 Gateway?那个基于 django route 的流量分发层?四年前我离开的时候已经小范围的使用 Flask+Gevent Demo 测试过了,无论是性能还是负载都比同步模型的 django 有优势。如果还是 django 这套的话,我只能说比较遗憾,毕竟当年金山新员工大赛头牌就是我和几个小伙伴写的实时同步在线文档编辑系统,用的就是这套技术。

业务不做置评,但是我用的Tornado + Gevent PK Go 做过测试,重IO的情况下,相差不大。


因此这是个工程问题,并非语言问题。 Python 提供给了你了这么多工具,硬要选一个传统的,Old fashion 的,Python:怪我咯?

严重同意


django的网络是同步阻塞的,也就是说,如果我们需要访问外部的一个服务,在等待结果返回这段时间,django不能处理任何其他的逻辑(当然,多线程的除外)。如果访问外部服务需要很长时间,那就意味着我们的整个服务几乎在很长一段时间完全不可用。

为了解决这个问题,我们只能不断的多开django进程,同时需要保证所有服务都能快速的处理响应,但想想这其实是一件很不靠谱的事情。

同步模型并非不行,因为 overhead 足够低,很多业务场景下用同步模型反而会取得更好的效果,比如豆瓣。同步模型最大的问题是对于 IO 密集型业务等待时间足够长,这时候需要的不是换语言 ,而是提醒你是不是架构要改一下了。

django的网络是同步阻塞, 不了解啊,但是感觉如果是这样的话,那你们的架构设计就是一团SHI。


虽然tornado是异步的,但是python的mysql库都不支持异步,这也就意味着如果我们在tornado里面访问数据库,我们仍然可能面临因为数据库问题造成的整个服务不可用。

tornado 是有这个问题,但是 gevent 已经解决了。我在 node.js 的某问题下曾经回答过,对于 node 而言,能选择的异步模型只有一个,而 Python 就是太多选择了。另外 pypy+tornado+redis 可以随意虐各种长连接的场景,比如我给我厂写过的一个 push service。

Tornado 可以用异步去访问数据库啊,只是数据库模块会被阻塞,但是不会阻塞下次的访问啊。Gevent的解决方案是更好的选择,并且,Python3.4里面的内置异步,也是不错的选择。当然,说点题外的,自己写一个MySQL的python库,对于快盘,真的那么难吗?


其实异步模型最大的问题在于代码逻辑的割裂,因为是事件触发的,所以我们都是通过callback进行相关处理,于是代码里面就经常出现干一件事情,传一个callback,然后callback里面又传callback的情况,这样的结果就是整个代码逻辑非常混乱。

这个还真不是,如果说没有 ES6 的 JavaScript,可能真有 Callback hell,但这是 Python 啊!Python 早就实现了左值绑定唉,yield 那姿势比某些天天吹的语言不知道高到哪里去了,当然我说的是完整版的 Python3 yield。即便是不完整的 Python 2 yield 用于异步表达式求值也是完全足够的,tornado 的 gen.coroutine 啊。

同步形态写异步,在 Python 实力强的公司里面早普及了,这是个工程问题,并非语言问题。当然把这种事怪在 Python 身上,Python:怪我咯?

这,一楼的,懂不懂Python啊,是不是看见yield 或者coroutine 会头晕的货?


python没有原生的协程支持,虽然可以通过gevent,greenlet这种的上patch方式来支持协程,但毕竟更改了python源码。另外,python的yield也可以进行简单的协程模拟,但毕竟不能跨堆栈,局限性很大,不知道3.x的版本有没有改进。

无论是 Gevent 还是 Greenlet 均没修改 Python 源码,事实上这货已经成为了 Py2 coroutine 的标准,加上豆瓣开源出来的greenify,基本上所有的库都可以平滑的异步化,包括 MySQL 等 C 一级的 lib。自从用上这套技术后,豆瓣的 Python dev 各种爽得不要不要的。

修改 Python 源码?你叫赵四?是个逗比?


当我第一次使用python开发项目,我是没成功安装上项目需要的包的,光安装成功mysql库就弄了很久。后来,是一位同事将他整个python目录打包给我用,我才能正常的将项目跑起来。话说,现在有了docker,是多么让人幸福的一件事情。

而部署python服务的时候,我们需要在服务器上面安装一堆的包,光是这一点就让人很麻烦,虽然可以通过puppet,salt这些自动化工具解决部署问题,但相比而言,静态编译语言只用扔一个二进制文件,可就方便太多了。

恰好我又是在开发基于 docker 的平台, docker 还真不是用来做部署这事的。首先, Python 是有 virtualenv 这个工具的,事实上对比包管理和包隔离,Python 比 Go 高得不知道哪里去了。Python 跟 Git 谈笑风生的时候, Go 的 dev 们还得考虑我怎样才能使得 import 的包稳定在一个版本上(当然现在有很多第三方方案)。Virtualenv + Pip 完全可以实现 Python 部署自动化,所以这个问题我认为是,工具链选取问题。毕竟是个十几年的老妖怪了,Python 啥情况没见过啊,各种打包工具任君选择,强行说 Python 部署不方便,Python:怪我咯?

python非常灵活简单,写c几十行代码才能搞定的功能,python一行代码没准就能解决。但是太简单,反而导致很多同学无法对代码进行深层次的思考,对整个架构进行细致的考量。来了一个需求,啪啪啪,键盘敲完开速实现,结果就是代码越来越混乱,最终导致了整个项目代码失控。

曾经知乎有个帖子问 Python 会不会降低程序员编程能力,我只能说这真的很人有关。你不去思考深层次的东西怪语言不行是没道理的,那好,Go 里面 goroutine 是怎么实现的,一个带 socket 的 goroutine 最小能做到多少内存,思考过?任何语言都有自己的优势和劣势,都需要执行者自己去判断,一味的觉得简单就不会深入思考这是有问题的。另外,代码混乱我认为还是工程上的控制力不够,豆瓣有超过10W行的 Python 实现,虽然不说很完美,大体上做到了不会混乱这么个目标。

还有,C 写几十行搞定的 Python 一行解决这绝对是重大 feature,生产力啊,人员配置啊,招人培养的成本啊,从工程上来说,Python 在这一块完全是加分项,不是每个项目都要求极致的并发,极致的效率,做工程很多时候都是要取舍的。

一楼的:

  1. 你这么说,你第一次使用Go(任何其他语言)做项目,你也装不上环境
  2. 部署Python环境的确是个恼人的问题,特别是需要打包成可运行库(不依赖系统Python)的时候,但是还是有很多的可选方案是比较成熟的
  3. 你确定Go的依赖管理是可行的?以来外部库的版本你确定不会叫你头大?
  4. 你确定你知道Virtualenv + Pip, 在程序里面或者系统环境变量里面设置Python Lib Path?
  5. python非常灵活简单,写c几十行代码才能搞定的功能,这都吐槽啊,大神,写0011呗
  6. Docker跟这个有毛线关系?再说,你说的还不全面甚至是不对

虽然java和php都是最好的编程语言(大家都这么争的),但我更倾向一门更简单的语言。而openresty,虽然性能强悍,但lua仍然是动态语言,也会碰到前面说的动态语言一些问题。最后,前金山许式伟用的go,前快盘架构师葱头也用的go,所以我们很自然地选择了go。

Openresty 用 lua 如果按照动态语言的角度去看,还真算不上,顶多是个简单点的 C。许式伟走的时候大多数都是 CPP,葱头目前我还不知道他创业用的是什么写的,不过他肯定没语言倾向。当年无论是 leo 还是 ufa,一个用 Python 一个用 Java, 他都是从工程实际来选择使用什么样的语言。

  1. 用Go的理由说的不充分,别卖萌啊,好好说话亲。
  2. 我大Lua为何无缘无故的的躺枪,没他什么事情啊。

error,好吧,如果有语言洁癖的同学可能真的受不了go的语法,尤其是约定的最后一个返回值是error。

这其实是 Go style,无论是 go fmt 还是 error style,Go 其实是想抹平不同工程师之间的风格问题。不再为了一个缩进和大括号位置什么的浪费时间。这种方法并不是不好,只是我个人觉得没 rust 那种返回值处理友善。

你是为了给Go攒人品才吐槽他的error语法的吗?我对这件事还能忍,包括{ 格式和import 多余报错,用好工具之后都不是事


GC,java的GC发展20年了,go才这么点时间,gc铁定不完善。所以我们仍然不能随心所欲的写代码,不然在大请求量下面gc可能会卡顿整个服务。所以有时候,该用对象池,内存池的一定要用,虽然代码丑了点,但好歹性能上去了。

1.4 开始 go 就是 100% 精确 GC 了,另外说到卡顿啊,完全和你怎么用对象有关,能内联绝不传引用大部分场景是完全足够的,这样 gc 的影响程度会最低。实在想用池……只能说为啥不选 Java。

你用的是Go1.0吗?现在的Go1.4你说的那些已经不是问题了,拜托,你黑完了Python就算了,选择了Go我也支持你,但是你别这么无知好吗?你确定你测试过最新的Go1.4的GC延迟问题?好吧,你继续你的对象内存池吧。奉劝你跟进一下Go的最新文档,自己多做测试,别看了@特价萝卜的书,就全是Java的那一套。


天生的并行支持,因为goroutine以及channel,用go写分布式应用,写并发程序异常的容易。没有了蛋疼的callback导致的代码逻辑割裂,代码逻辑都是顺序的。

这是有代价的,goroutine 的内存消耗计算(当然1.3还是1.4开始得到了很大的改善,内存最小值限制已经没了),channel 跨线程带来的性能损耗(跨线程锁),还有对 goroutine 的控制力几乎为 0 等。总之这种嘛,算不上是杀手级特性,大家都有,是方便了一点,但也有自己的弊端。比如我们用 go 吧,经常就比较蛋疼 spawn 出去的 goroutine 怎么优美的 shutdown,反而有时候把事情做复杂化了。

我承认吾喜欢goroutine以及channel,但是看了前面你说的东西,我怎么听你说之后没那么自信了?


性能,go的性能可能赶不上c,c++以及openresty,但真的也挺强悍的。在我们的项目中,现在单机就部署了一个go的进程,就完全能够胜任以前200个python进程干的事情,而且CPU和MEM占用更低。

我不严谨的实测大概 gevent+py2 能达到同样逻辑 go 实现的 30%~40%,pypy+tornado 能达到 80%~90%,混合了一些计算和连接处理什么的。主要还是看业务场景吧,纯粹的 CPU bound 当然是 go 好,纯粹的 IO bound 你就是用 C 也没用啊。

我也大致的测试过,在重IO的情况下,同意二楼的数据,重CPU的,纯Python与Go有很大的差距,当然,我们有很多其他的方案搞定这个问题,看《Python编程实战》这本书去。


运维部署,直接编译成二进制,扔到服务器上面就成,比python需要安装一堆的环境那是简单的太多了。当然,如果有cgo,我们也需要将对应的动态库给扔过去。

我们现在根据 glibc 所处的 host 版本不同有2套编译环境,看上去是部署简单了,编译起来坑死你。另外虽然说 disk 便宜,这几行代码就几M了,集群同步部署耗时在某些情况下还真会出篓子。

我基本同意一楼,说句实在话,我爱死Go的静态编译无依赖了。


开发效率,虽然go是静态语言,但我个人感觉开发效率真的挺高,直觉上面跟python不相上下。对于我个人来说,最好的例子就是我用go快速开发了非常多的开源组件,譬如ledisdb,go-mysql等,而这些最开始的版本都是在很短的时间里面完成的。对于我们项目来说,我们也是用go在一个月就重构完成了第一个版本,并发布。

go 的开发效率高是对比 C,对比 python,大概后者只需要3天吧……

我这只小马的经验是,Go确实比Python还是要慢一点,如果考虑Go在某些方面需要重新造轮子,慢的就很多了,但是也没有二楼说的差距那么夸张,主要是,三天,加班加到死我也搞不定啊。


总之,Go 不是不好,Python 也不是不行,做工程嘛,无外乎就是考虑成本,时间成本,人力成本,维护成本等等。 Go 和 Python 互有千秋,就看取舍了。当然一定要说 Python 不行,Python:怪我咯?

总之,我同意大部分的二楼的话,也理解一楼的选择,但是一楼的很多观点例子,不敢苟同啊。其实,我是专业Hello World.党。

jack.zh 标签:golang python 继续阅读

802 ℉

15.05.21

Go并发编程基础(译)

转自: 黑*白

原文: Fundamentals of concurrent programming

译者: youngsterxyf

本文是一篇并发编程方面的入门文章,以Go语言编写示例代码,内容涵盖:

  • 运行期并发线程(goroutines)
  • 基本的同步技术(管道和锁)
  • Go语言中基本的并发模式
  • 死锁和数据竞争
  • 并行计算

在开始阅读本文之前,你应该知道如何编写简单的Go程序。如果你熟悉的是C/C++、Java或Python之类的语言,那么 Go语言之旅 能提供所有必要的背景知识。也许你还有兴趣读一读 为C++程序员准备的Go语言教程为Java程序员准备的Go语言教程

1. 运行期线程

Go允许使用go语句开启一个新的运行期线程,即 goroutine,以一个不同的、新创建的goroutine来执行一个函数。同一个程序中的所有goroutine共享同一个地址空间。

Goroutine非常轻量,除了为之分配的栈空间,其所占用的内存空间微乎其微。并且其栈空间在开始时非常小,之后随着堆存储空间的按需分配或释放而变化。内部实现上,goroutine会在多个操作系统线程上多路复用。如果一个goroutine阻塞了一个操作系统线程,例如:等待输入,这个线程上的其他goroutine就会迁移到其他线程,这样能继续运行。开发者并不需要关心/担心这些细节。

下面所示程序会输出“Hello from main goroutine”。也可能会输出“Hello from another goroutine”,具体依赖于两个goroutine哪个先结束。

goroutine1.go:

func main() {
    go fmt.Println("Hello from another goroutine")
    fmt.Println("Hello from main goroutine")

    // 至此,程序运行结束,
    // 所有活跃的goroutine被杀死
}

接下来的这个程序,多数情况下,会输出“Hello from main goroutine”和“Hello from another goroutine”,输出的顺序不确定。但还有另一个可能性是:第二个goroutine运行得极其慢,在程序结束之前都没来得及输出相应的消息。

goroutine2.go:

func main() {
    go fmt.Println("Hello from another goroutine")
    fmt.Println("Hello from main goroutine")

    time.Sleep(time.Second)     // 等待1秒,等另一个goroutine结束
}

下面则是一个相对更加实际的示例,其中定义了一个函数使用并发来推迟触发一个事件。

publish1.go:

// 函数Publish在给定时间过期后打印text字符串到标准输出
// 该函数并不会阻塞而是立即返回
func Publish(text string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)
    }() // 注意这里的括号。必须调用匿名函数
}

你可能会这样使用Publish函数(publish1.go):

func main() {
    Publish("A goroutine starts a new thread of execution.", 5*time.Second)
    fmt.Println("Let’s hope the news will published before I leave.")

    // 等待发布新闻
    time.Sleep(10 * time.Second)

    fmt.Println("Ten seconds later: I’m leaving now.")
}

这个程序,绝大多数情况下,会输出以下三行,顺序固定,每行输出之间相隔5秒。

$ go run publish1.go
Let’s hope the news will published before I leave.
BREAKING NEWS: A goroutine starts a new thread of execution.
Ten seconds later: I’m leaving now.

一般来说,通过睡眠的方式来编排线程之间相互等待是不太可能的。下一章节会介绍Go语言中的一种同步机制 - 管道,并演示如何使用管道让一个goroutine等待另一个goroutine。

2. 管道(channel)

管道是Go语言的一个构件,提供一种机制用于两个goroutine之间通过传递一个指定类型的值来同步运行和通讯。操作符<-用于指定管道的方向,发送或接收。如果未指定方向,则为双向管道。

chan Sushi      // 可用来发送和接收Sushi类型的值
chan<- float64  // 仅可用来发送float64类型的值
<-chan int      // 仅可用来接收int类型的值

管道是引用类型,基于make函数来分配。

ic := make(chan int)    // 不带缓冲的int类型管道
wc := make(chan *Work, 10)  // 带缓冲的Work类型指针管道

如果压迫通过管道发送一个值,则将<-作为二元操作符使用。通过管道接收一个值,则将其作为一元操作符使用:

ic <- 3     // 往管道发送3
work := <-wc    // 从管道接收一个指向Work类型值的指针

如果管道不带缓冲,发送方会阻塞直到接收方从管道中接收了值。如果管道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可能接收之前会一直阻塞。

关闭管道(Close)

close 函数标志着不会再往某个管道发送值。在调用close之后,并且在之前发送的值都被接收后,接收操作会返回一个零值,不会阻塞。一个多返回值的接收操作会额外返回一个布尔值用来指示返回的值是否发送操作传递的。

ch := make(chan string)
go func() {
    ch <- "Hello!"
    close(ch)
}()
fmt.Println(<-ch)   // 输出字符串"Hello!"
fmt.Println(<-ch)   // 输出零值 - 空字符串"",不会阻塞
fmt.Println(<-ch)   // 再次打印输出空字符串""
v, ok := <-ch       // 变量v的值为空字符串"",变量ok的值为false

一个带有range子句的for语句会依次读取发往管道的值,直到该管道关闭:

sushi.go:

func main() {
    // 译注:要想运行该示例,需要先定义类型Sushi,如type Sushi string
    var ch <-chan Sushi = Producer()
    for s := range ch {
        fmt.Println("Consumed", s)
    }
}

func Producer() <-chan Sushi {
    ch := make(chan Sushi)
    go func(){
        ch <- Sushi("海老握り") // Ebi nigiri
        ch <- Sushi("鮪とろ握り") // Toro nigiri
        close(ch)
    }()
    return ch
}

3. 同步

下一个示例中,我们让Publish函数返回一个管道 - 用于在发布text变量值时广播一条消息:

publish2.go:

// 在给定时间过期时,Publish函数会打印text变量值到标准输出
// 在text变量值发布后,该函数会关闭管道wait
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)
        close(ch)   // 广播 - 一个关闭的管道都会发送一个零值
    }()
    return ch
}

注意:我们使用了一个空结构体的管道:struct{}。这明确地指明该管道仅用于发信号,而不是传递数据。

我们可能会这样使用这个函数:

publish2.go:

func main() {
    wait := Publish("Channels let goroutines communicate.", 5*time.Second)
    fmt.Println("Waiting for the news...")
    <-wait
    fmt.Println("The news is out, time to leave.")
}

这个程序会按指定的顺序输出以下三行内容。最后一行在新闻(news)一出就会立即输出。

$ go run publish2.go
Waiting for the news...
BREAKING NEWS: Channels let goroutines communicate.
The news is out, time to leave.

4. 死锁

现在我们在Publish函数中引入一个bug:

func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)
        // 译注:注意这里将close函数调用注释掉了
        //close(ch)
    }()
    return ch
}

主程序还是像之前一样开始运行:输出第一行,然后等待5秒,这时Publish函数开启的goroutine会输出突发新闻(breaking news),然后退出,留下主goroutine独自等待。

func main() {
    wait := Publish("Channels let goroutines communicate.", 5*time.Second)
    fmt.Println("Waiting for the news...")
    // 译注:注意下面这一句
    <-wait
    fmt.Println("The news is out, time to leave.")
}

此刻之后,程序无法再继续往下执行。众所周知,这种情形即为死锁。

死锁是线程之间相互等待,其中任何一个都无法向前运行的情形。

Go语言对于运行时的死锁检测具备良好的支持。当没有任何goroutine能够往前执行的情形发生时,Go程序通常会提供详细的错误信息。以下就是我们的问题程序的输出:

Waiting for the news...
BREAKING NEWS: Channels let goroutines communicate.
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    .../goroutineStop.go:11 +0xf6

goroutine 2 [syscall]:
created by runtime.main
    .../go/src/pkg/runtime/proc.c:225

goroutine 4 [timer goroutine (idle)]:
created by addtimer
    .../go/src/pkg/runtime/ztime_linux_amd64.c:73

大多数情况下找出Go程序中造成死锁的原因都比较容易,那么剩下的就是如何解决这个bug了。

5. 数据竞争(data race)

死锁也许听起来令人挺忧伤的,但伴随并发编程真正灾难性的错误其实是数据竞争,相当常见,也可能非常难于调试。

当两个线程并发地访问同一个变量,并且其中至少一个访问是写操作时,数据竞争就发生了。

下面的这个函数就有数据竞争问题,其行为是未定义的。例如,可能输出数值1。代码之后是一个可能性解释,试图搞清楚这一切是如何发生得。

datarace.go:

func race() {
    wait := make(chan struct{})
    n := 0
    go func() {
        // 译注:注意下面这一行
        n++ // 一次访问: 读, 递增, 写
        close(wait)
    }()
    // 译注:注意下面这一行
    n++ // 另一次冲突的访问
    <-wait
    fmt.Println(n) // 输出:未指定
}

代码中的两个goroutine(假设命名为g1g2)参与了一次竞争,我们无法获知操作会以何种顺序发生。以下是诸多可能中的一种:

  • g1n 中获取值0
  • g2n 中获取值0
  • g1 将值从0增大到1
  • g1 将1写到 n
  • g2 将值从0增大到1
  • g2 将1写到 n
  • 程序输出 n 的值,当前为1

“数据竞争(data race)”这名字有点误导的嫌疑。不仅操作的顺序是未定义的,其实根本没有任何保证(no guarantees whatsoever)。编译器和硬件为了得到更好的性能,经常都会对代码进行上下内外的顺序变换。如果你看到一个线程处于中间行为状态时,那么当时的场景可能就像下图所示的一样:

避免数据竞争的唯一方式是线程间同步访问所有的共享可变数据。有几种方式能够实现这一目标。Go语言中,通常是使用管道或者锁。(syncsync/atomic包中还有更低层次的机制可供使用,但本文中不做讨论)。

Go语言中,处理并发数据访问的推荐方式是使用管道从一个goroutine中往下一个goroutine传递实际的数据。有格言说得好:“不要通过共享内存来通讯,而是通过通讯来共享内存”。

datarace.go:

func sharingIsCaring() {
    ch := make(chan int)
    go func() {
        n := 0 // 仅为一个goroutine可见的局部变量.
        n++
        ch <- n // 数据从一个goroutine离开...
    }()
    n := <-ch   // ...然后安全到达另一个goroutine.
    n++
    fmt.Println(n) // 输出: 2
}

以上代码中的管道肩负双重责任 - 从一个goroutine将数据传递到另一个goroutine,并且起到同步的作用:发送方goroutine会等待另一个goroutine接收数据,接收方goroutine也会等待另一个goroutine发送数据。

Go语言内存模型 - 要保证一个goroutine中对一个变量的读操作得到的值正好是另一个goroutine中对同一个变量写操作产生的值,条件相当复杂,但goroutine之间只要通过管道来共享所有可变数据,那么就能远离数据竞争了。

6. 互斥锁

有时,通过显式加锁,而不是使用管道,来同步数据访问,可能更加便捷。Go语言标准库为这一目的提供了一个互斥锁 - sync.Mutex

要想这类加锁起效的话,关键之处在于:所有对共享数据的访问,不管读写,仅当goroutine持有锁才能操作。一个goroutine出错就足以破坏掉一个程序,引入数据竞争。

因此,应该设计一个自定义数据结构,具备明确的API,确保所有的同步都在数据结构内部完成。下例中,我们构建了一个安全、易于使用的并发数据结构,AtomicInt,用于存储一个整型值。任意数量的goroutine都能通过AddValue方法安全地访问这个数值。

datarace.go:

/ AtomicInt是一个并发数据结构,持有一个整数值
// 该数据结构的零值为0
type AtomicInt struct {
    mu sync.Mutex // 锁,一次仅能被一个goroutine持有。
    n  int
}

// Add方法作为一个原子操作将n加到AtomicInt
func (a *AtomicInt) Add(n int) {
    a.mu.Lock() // 等待锁释放,然后持有它
    a.n += n
    a.mu.Unlock() // 释放锁
}

// Value方法返回a的值
func (a *AtomicInt) Value() int {
    a.mu.Lock()
    n := a.n
    a.mu.Unlock()
    return n
}

func lockItUp() {
    wait := make(chan struct{})
    var n AtomicInt
    go func() {
        n.Add(1) // 一个访问
        close(wait)
    }()
    n.Add(1) // 另一个并发访问
    <-wait
    fmt.Println(n.Value()) // 输出: 2
}

7. 检测数据竞争

竞争有时非常难于检测。下例中的这个函数有一个数据竞争问题,执行这个程序时会输出55555。尝试一下,也许你会得到一个不同的结果。(sync.WaitGroup是Go语言标准库的一部分;用于等待一组goroutine结束运行。)

raceClosure.go:

func race() {
    var wg sync.WaitGroup
    wg.Add(5)
    // 译注:注意下面这行代码中的i++
    for i := 0; i < 5; i++ {
        go func() {
            // 注意下一行代码会输出什么?为什么?
            fmt.Print(i) // 6个goroutine共享变量i
            wg.Done()
        }()
    }
    wg.Wait() // 等待所有(5个)goroutine运行结束
    fmt.Println()
}

对于输出55555,一个貌似合理的解释是:执行i++的goroutine在其他goroutine执行打印语句之前就完成了5次i++操作。实际上变量i更新后的值为其他goroutine所见纯属巧合。

一个简单的解决方案是:使用一个局部变量,然后当开启新的goroutine时,将数值作为参数传递:

raceClosure.go:

func correct() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(n int) { // 使用局部变量
            fmt.Print(n)
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println()
}

这次代码就对了,程序会输出期望的结果,如:24031。注意:goroutine之间的运行顺序是不确定的。

仍旧使用闭包,但能够避免数据竞争也是可能的,必须小心翼翼地让每个goroutine使用一个独有的变量。

raceClosure.go:

func alsoCorrect() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        n := i // 为每个闭包创建一个独有的变量
        go func() {
            fmt.Print(n)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println()
}

数据竞争自动检测

一般来说,不太可能能够自动检测发现所有可能的数据竞争情况,但Go(从版本1.1开始)有一个强大的数据竞争检测器

这个工具用起来也很简单:只要在使用go命令时加上-race标记即可。开启检测器运行上面的程序会给出清晰且信息量大的输出:

$ go run -race raceClosure.go
Race:
==================
WARNING: DATA RACE
Read by goroutine 2:
  main.func·001()
      ../raceClosure.go:22 +0x65

Previous write by goroutine 0:
  main.race()
      ../raceClosure.go:20 +0x19b
  main.main()
      ../raceClosure.go:10 +0x29
  runtime.main()
      ../go/src/pkg/runtime/proc.c:248 +0x91

Goroutine 2 (running) created at:
  main.race()
      ../raceClosure.go:24 +0x18b
  main.main()
      ../raceClosure.go:10 +0x29
  runtime.main()
      ../go/src/pkg/runtime/proc.c:248 +0x91

==================
55555
Correct:
01234
Also correct:
01324
Found 1 data race(s)
exit status 66

该工具发现一处数据竞争,包含:一个goroutine在第20行对一个变量进行写操作,跟着另一个goroutine在第22行对同一个变量进行了未同步的读操作。

注意:竞争检测器只能发现在运行期确实发生的数据竞争(译注:我也不太理解这话,请指导)

8. Select语句

select语句是Go语言并发工具集中的最后一个工具。select用于从一组可能的通讯中选择一个进一步处理。如果任意一个通讯都可以进一步处理,则从中随机选择一个,执行对应的语句。否则,如果又没有默认分支(default case),select语句则会阻塞,直到其中一个通讯完成。

以下是一个玩具示例,演示select语句如何用于实现一个随机数生成器:

randBits.go:

// RandomBits函数 返回一个管道,用于产生一个比特随机序列
func RandomBits() <-chan int {
    ch := make(chan int)
    go func() {
        for {
            select {
            case ch <- 0: // 注意:分支没有对应的处理语句
            case ch <- 1:
            }
        }
    }()
    return ch
}

下面是相对更加实际一点的例子:如何使用select语句为一个操作设置一个时间限制。代码会输出变量news的值或者超时消息,具体依赖于两个接收语句哪个先执行:

select {
case news := <-NewsAgency:
    fmt.Println(news)
case <-time.After(time.Minute):
    fmt.Println("Time out: no news in one minute.")
}

函数 time.After 是Go语言标准库的一部分;它会在等待指定时间后将当前的时间发送到返回的管道中。

9. 综合所有示例

花点时间认真研究一下这个示例。如果你完全理解,也就对Go语言中并发的应用方式有了全面的掌握。

这个程序演示了如何将管道用于被任意数量的goroutine发送和接收数据,也演示了如何将select语句用于从多个通讯中选择一个。

matching.go:

func main() {
    people := []string{"Anna", "Bob", "Cody", "Dave", "Eva"}
    match := make(chan string, 1) // 为一个未匹配的发送操作提供空间
    wg := new(sync.WaitGroup)
    wg.Add(len(people))
    for _, name := range people {
        go Seek(name, match, wg)
    }
    wg.Wait()
    select {
    case name := <-match:
        fmt.Printf("No one received %s’s message.\n", name)
    default:
        // 没有待处理的发送操作
    }
}

// 函数Seek 发送一个name到match管道或从match管道接收一个peer,结束时通知wait group
func Seek(name string, match chan string, wg *sync.WaitGroup) {
    select {
    case peer := <-match:
        fmt.Printf("%s sent a message to %s.\n", peer, name)
    case match <- name:
        // 等待某个goroutine接收我的消息
    }
    wg.Done()
}

示例输出:

$ go run matching.go
Cody sent a message to Bob.
Anna sent a message to Eva.
No one received Dave’s message.

10. 并行计算

并发的一个应用是将一个大的计算切分成一些工作单元,调度到不同的CPU上同时地计算。

将计算分布到多个CPU上更多是一门艺术,而不是一门科学。以下是一些经验法则:

  • 每个工作单元应该花费大约100微秒到1毫秒的时间用于计算。如果单元粒度太小,切分问题以及调度子问题的管理开销可能就会太大。如果单元粒度太大,整个计算也许不得不等待一个慢的工作项结束。这种缓慢可能因为多种原因而产生,比如:调度、其他进程的中断或者糟糕的内存布局。(注意:工作单元的数目是不依赖于CPU的数目的)
  • 尽可能减小共享的数据量。并发写操作的代价非常大,特别是如果goroutine运行在不同的CPU上。读操作之间的数据共享则通常不会是个问题。
  • 数据访问尽量利用良好的局部性。如果数据能保持在缓存中,数据加载和存储将会快得多得多,这对于写操作也格外地重要。

下面的这个示例展示如何切分一个开销很大的计算并将其分布在所有可用的CPU上进行计算。先看一下有待优化的代码:

convolution.go:

type Vector []float64

// 函数Convolve 计算 w = u * v,其中 w[k] = Σ u[i]*v[j], i + j = k
// 先决条件:len(u) > 0, len(v) > 0
func Convolve(u, v Vector) (w Vector) {
    n := len(u) + len(v) - 1
    w = make(Vector, n)

    for k := 0; k < n; k++ {
        w[k] = mul(u, v, k)
    }
    return
}

// 函数mul 返回 Σ u[i]*v[j], i + j = k.
func mul(u, v Vector, k int) (res float64) {
    n := min(k+1, len(u))
    j := min(k, len(v)-1)
    for i := k - j; i < n; i, j = i+1, j-1 {
        res += u[i] * v[j]
    }
    return
}

思路很简单:确定合适大小的工作单元,然后在不同的goroutine中执行每个工作单元。以下是并发版本的 Convolve:

func Convolve(u, v Vector) (w Vector) {
    n := len(u) + len(v) - 1
    w = make(Vector, n)

    // 将 w 切分成花费 ~100μs-1ms 用于计算的工作单元
    size := max(1, 1<<20/n)

    wg := new(sync.WaitGroup)
    wg.Add(1 + (n-1)/size)
    for i := 0; i < n && i >= 0; i += size { // 整型溢出后 i < 0
        j := i + size
        if j > n || j < 0 { // 整型溢出后 j < 0
            j = n
        }

        // 这些goroutine共享内存,但是只读
        go func(i, j int) {
            for k := i; k < j; k++ {
                w[k] = mul(u, v, k)
            }
            wg.Done()
        }(i, j)
    }
    wg.Wait()
    return
}

工作单元定义之后,通常情况下最好将调度工作交给运行时和操作系统。然而,对于Go 1.* 你也许需要告诉运行时希望多少个goroutine来同时地运行代码。

func init() {
    numcpu := runtime.NumCPU()
    runtime.GOMAXPROCS(numcpu) // 尝试使用所有可用的CPU
}

jack.zh 标签:golang 继续阅读

972 ℉

15.05.05

Golang编程经验总结

如何选择web框架:

首先Golang语言开发web项目不一定非要框架,本身已经提供了Web开发需要的一切必要技术。当然如果想要ruby里面Rail那种高层次全栈式的MVC框架, Golang里面暂时没有,但是不是所有人都喜欢这种复杂的框架。Golang里面一些应用层面的技术需要自己去组装,比如session,cache, log等等. 可选择的web框架有martini, goji等,都是轻量级的。

Golang的web项目中的keepalive

关于keepalive, 是比较复杂的, 注意以下几点:

  1. http1.1 默认支持keepalive, 但是不同浏览器对keepalive都有个超时时间, 比如firefox,默认超时时间115秒, 不同浏览器不一样;
  2. Nginx默认超时时间75秒;
  3. golang默认超时时间是无限的

要控制golang中的keepalive可以设置读写超时, 举例如下:

server := &http.Server{
    Addr:           ":9999",
    Handler:        framework,
    ReadTimeout:    32 * time.Second,
    WriteTimeout:   32 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
server.ListenAndServe()

github.com/go-sql-driver/mysql使用主意事项:

这是使用率极高的一个库

在用它进行事务处理的情况下, 要注意一个问题, 由于它内部使用了连接池, 使用事务的时候如果没有Rollback或者Commit, 这个取出的连接就不会放回到池子里面, 导致的后果就是连接数过多, 所以使用事务的时候要注意正确地使用。

github.com/garyburd/redigo/redis使用注意事项:

这也是一个使用率极高的库

同样需要注意,它是支持连接池的, 所以最好使用连接池, 正确的用法是这样的:

func initRedis(host string) *redis.Pool {
    return &redis.Pool{
        MaxIdle: 64,    
        IdleTimeout: 60 * time.Second,
        TestOnBorrow: func(c redis.Conn, t time.Time) error {
            _, err := c.Do("PING")

            return err
        },
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", host)
            if err != nil {
                return nil, err
            }

            _, err = c.Do("SELECT", config.RedisDb)

            return c, err
        },
    }
}

另外使用的时候也要把连接放回到池子里面, 否则也会导致连接数居高不下。用完之后调用rd.Close(), 这个Close并不是真的关闭连接,而是放回到池子里面。

如何全局捕获panic级别错误:

server := &http.Server{
    Addr:           ":9999",
    Handler:        framework,
    ReadTimeout:    32 * time.Second,
    WriteTimeout:   32 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
server.ListenAndServe()
  • 需要注意的是捕获到pannic之后, 程序的执行点不会回到触发pannic的地方,需要程序再次执行, 一些框架支持这一点,比如martini里面有c.Next()。
  • 如果程序main里启动了多个goroutine, 每个goroutine里面都应该捕获pannic级别错误, 否则某个goroutine触发panic级别错误之后,整个程序退出, 这是非常不合理的。

最容易出错的地方:

使用指针,但是没有判断指针是否为nil, Golang中array, struct是值语义, slice,map, chanel是引用传递。

如何获取程序执行栈:

defer func() {
    if err := recover(); err != nil {
        var st = func(all bool) string {
            // Reserve 1K buffer at first
            buf := make([]byte, 512)

            for {
                size := runtime.Stack(buf, all)
                // The size of the buffer may be not enough to hold the stacktrace,
                // so double the buffer size
                if size == len(buf) {
                    buf = make([]byte, len(buf)<<1)
                    continue
                }
                break
            }

            return string(buf)
        }
        lib.Log4e("panic:" + toString(err) + "\nstack:" + st(false))
    }
}()

具体方法就是调用

 runtime.Stack

如何执行异步任务:

比如用户提交email:

给用户发邮件, 发邮件的步骤是比较耗时的, 这个场景适合可以使用异步任务:

result := global.ResponseResult{ErrorCode: 0, ErrorMsg: "GetInviteCode success!"}
render.JSON(200, &result)
go func() {
    type data struct {
        Url string
    }
    name := "beta_test"
    subject := "We would like to invite you to the private beta of Screenshot."
    url := config.HttpProto + r.Host + "/user/register/" + *uniqid
    html := ParseMailTpl(&name, &beta_test_mail_content, data{url})
    e := this.SendMail(mail, subject, html.String())
    if e != nil {
        lib.Log4w("GetInviteCode, SendMail faild", mail, uniqid, e)
    } else {
        lib.Log4w("GetInviteCode, SendMail success", mail, uniqid)
    }
}()

思路是启动一个goroutine执行异步的操作

当前goroutine继续向下执行。特别需要注意的是新启动的个goroutine如果对全局变量有读写操作的话,需要注意避免发生竞态条件, 可能需要加锁。

如何使用定时器:

通常情况下, 写一些定时任务需要用到crontab, 在Golang里面是不需要的, 提供了非常好用的定时器。举例如下:

func Init() {
    ticker := time.NewTicker(30 * time.Minute)
    for {
        select {
        case c := <-global.TaskCmdChannel:
            switch *c {
            case "a":
                //todo
            }
        case c := <-global.TaskImageMessageChannel:
            m := new(model.TaskModel)
            m.Init()
            m.CreateImageMessage(c)
            m = nil
        case <-ticker.C:
            m := new(model.TaskModel)
            m.Init()
            m.CleanUserExpiredSessionKey()
            m = nil
        }
    }
}

多goroutine执行如果避免发生竞态条件:

Data races are among the most common and hardest to debug types of bugs in concurrent systems. A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write. See the The Go Memory Model for details.

官方相关说明:

多goroutine执行,访问全局的变量,比如map,可能会发生竞态条件,如何检查呢?首先在编译的时候指定 -race参数,指定这个参数之后,编译出来的程序体积大一倍以上, 另外cpu,内存消耗比较高,适合测试环境, 但是发生竞态条件的时候会panic,有详细的错误信息。go内置的数据结构array,slice,map都不是线程安全的。

没有设置runtime.GOMAXPROCS会有竞态条件的问题吗?

答案是没有

因为没有设置runtime.GOMAXPROCS的情况下, 所有的goroutine都是在一个原生的系统thread里面执行, 自然不会有竞态条件。

如何充分利用CPU多核:

runtime.GOMAXPROCS(runtime.NumCPU() * 2)

以上是根据经验得出的比较合理的设置。

解决并发情况下的竞态条件的方法:

  1. channel, 但是channel并不能解决所有的情况,channel的底层实现里面也有用到锁, 某些情况下channel还不一定有锁高效, 另外channel是Golang里面最强大也最难掌握的一个东西, 如果发生阻塞不好调试。
  2. 加锁, 需要注意高并发情况下,锁竞争也是影响性能的一个重要因素, 使用读写锁,在很多情况下更高效, 举例如下:

code:

    var mu sync.RWMutex

    ...

    mu.RLock()
    defer mu.RUnlock()
    conns := h.all_connections[img_id]

    for _, c := range conns {
        if c == nil /*|| c.uid == uid */ {
            continue
        }

        select {
        case c.send <- []byte(message):
        default:
            h.conn_unregister(c)
        }
    }

使用锁有个主意的地方是避免死锁,比如循环加锁。

  1. 原子操作(CAS), Golang的atomic包对原子操作提供支持,Golang里面锁的实现也是用的原子操作。

获取程序绝对路径:

Golang编译出来之后是独立的可执行程序,不过很多时候需要读取配置,由于执行目录有时候不在程序所在目录,路径的问题经常让人头疼,正确获取绝对路径非常重要, 方法如下:

func GetCurrPath() string {
    file, _ := exec.LookPath(os.Args[0])
    path, _ := filepath.Abs(file)
    index := strings.LastIndex(path, string(os.PathSeparator))
    ret := path[:index]
    return ret
}

Golang函数默认参数:

大家都知道Golang是一门简洁的语言,不支持函数默认参数. 这个特性有些情况下确实是有用的,如果不支持,往往需要重写函数,或者多写一个函数。其实这个问题非常好解决, 举例如下:

func (this *ImageModel) GetImageListCount(project_id int64,  paramter_optional ...int) int {
    var t int

    expire_time := 600
    if len(paramter_optional) > 0 {
        expire_time = paramter_optional[0]
    }
    ...
}

性能监控:

go func() {
    profServeMux := http.NewServeMux()
    profServeMux.HandleFunc("/debug/pprof/", pprof.Index)
    profServeMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    profServeMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    profServeMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    err := http.ListenAndServe(":7789", profServeMux)
    if err != nil {
        panic(err)
    }
}()

接下来就可以使用go tool pprof分析。

如何进行程序调试:

对于调试,每个人理解不一样,

如果要调试程序功能, 重新编译即可, Golang的编译速度极快。如果在开发的时候调试程序逻辑, 一般用log即可, Golang里面最好用的log库是log4go, 支持log级别。如果要进行断点调试, GoEclipse之类的是支持的, 依赖Mingw和GDB, 我个人不习惯这种调试方法。

守护进程(daemon)

下面给出完整的真正可用的例子:

package main

import (
    "fmt"
    "log"
    "os"
    "runtime"
    "syscall"
    "time"
)

func daemon(nochdir, noclose int) int {
    var ret, ret2 uintptr
    var err syscall.Errno

    darwin := runtime.GOOS == "darwin"

    // already a daemon
    if syscall.Getppid() == 1 {
        return 0
    }

    // fork off the parent process
    ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
    if err != 0 {
        return -1
    }

    // failure
    if ret2 < 0 {
        os.Exit(-1)
    }

    // handle exception for darwin
    if darwin && ret2 == 1 {
        ret = 0
    }

    // if we got a good PID, then we call exit the parent process.
    if ret > 0 {
        os.Exit(0)
    }

    /* Change the file mode mask */
    _ = syscall.Umask(0)

    // create a new SID for the child process
    s_ret, s_errno := syscall.Setsid()
    if s_errno != nil {
        log.Printf("Error: syscall.Setsid errno: %d", s_errno)
    }
    if s_ret < 0 {
        return -1
    }

    if nochdir == 0 {
        os.Chdir("/")
    }

    if noclose == 0 {
        f, e := os.OpenFile("/dev/null", os.O_RDWR, 0)
        if e == nil {
            fd := f.Fd()
            syscall.Dup2(int(fd), int(os.Stdin.Fd()))
            syscall.Dup2(int(fd), int(os.Stdout.Fd()))
            syscall.Dup2(int(fd), int(os.Stderr.Fd()))
        }
    }

    return 0
}

func main() {
    daemon(0, 1)
    for {
        fmt.Println("hello")
        time.Sleep(1 * time.Second)
    }

}

进程管理:

个人比较喜欢用supervisord来进行进程管理,支持进程自动重启,supervisord是一个python开发的工具,用pip安装即可。

代码热更新:

代码热更新一直是解释型语言比较擅长的,Golang里面不是做不到,只是稍微麻烦一些,就看必要性有多大。如果是线上在线人数很多, 业务非常重要的场景, 还是有必要, 一般情况下没有必要。

  1. 更新配置. 因为配置文件一般是个json或者ini格式的文件,是不需要编译的, 在线更新配置还是相对比较容易的, 思路就是使用信号, 比如SIGUSER2, 程序在信号处理函数中重新加载配置即可。

  2. 热更新代码. 目前网上有多种第三方库, 实现方法大同小异。先编译代码(这一步可以使用fsnotify做到监控代码变化,自动编译),关键是下一步graceful restart进程,实现方法可参考:http://grisha.org/blog/2014/06/03/graceful-restart-in-golang/ 也是创建子进程,杀死父进程的方法。

条件编译:

条件编译时一个非常有用的特性,一般一个项目编译出一个可执行文件,但是有些情况需要编译成多个可执行文件,执行不同的逻辑,这比通过命令行参数执行不同的逻辑更清晰.比如这样一个场景,一个web项目,是常驻进程的, 但是有时候需要执行一些程序步骤初始化数据库,导入数据,执行一个特定的一次性的任务等。假如项目中有一个main.go, 里面定义了一个main函数,同目录下有一个task.go函数,里面也定义了一个main函数,正常情况下这是无法编译通过的, 会提示“main redeclared”。解决办法是使用go build 的-tags参数。步骤如下(以windows为例说明):

  1. 在main.go头部加上 // +build main
  2. 在task.go头部加上// +build task
  3. 编译住程序:go build -tags 'main'
  4. 编译task:go build -tags 'task' -o task.exe

将项目有关资源文件打包进主程序:

使用go generate命令,参考godoc的实现。

与C/C++ 交互

  1. Cgo,Cgo支持Golang和C/C++混编, 在Golang里面使用pthread,libuv之类的都不难,github上也有相关开源代码;
  2. Swig,很多库都用Swig实现了Golang的绑定,Swig也可以反向回调Golang代码。
  3. syscall包, 该包让你以Golang的方式进行系统编程,不需要再使用C/C++,syscall提供了很多系统接口,比如epoll,原始socket套接字编程接口等。

其他:

近几年最热门的技术之一Docker是用Golang开发的,已经有相关的书出版, 对系统运维,云计算感兴趣的可以了解。

转自yxw的专栏

jack.zh 标签:golang 继续阅读

1511 ℉

15.04.28

gevent程序员指南

由Gevent社区编写

gevent是一个基于libev的并发库。它为各种并发和网络相关的任务提供了整洁的API。

介绍

本指南假定读者有中级P ython水平,但不要求有其它更多的知识,不期待读者有 并发方面的知识。本指南的目标在于给予你需要的工具来开始使用gevent,帮助你 驯服现有的并发问题,并从今开始编写异步应用程序。

贡献者

按提供贡献的时间先后顺序列出如下: Stephen Diehl Jérémy Bethmont sww Bruno Bigras David Ripton Travis Cline Boris Feld youngsterxyf Eddie Hebert Alexis Metaireau Daniel Velkov

同时感谢Denis Bilenko写了gevent和相应的指导以形成本指南。

这是一个以MIT许可证发布的协作文档。你想添加一些内容?或看见一个排版错误? Fork一个分支发布一个request到 Github. 我们欢迎任何贡献。

核心部分

Greenlets

在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

在任何时刻,只有一个协程在运行。

这与multiprocessing或threading等提供真正并行构造的库是不同的。 这些库轮转使用操作系统调度的进程和线程,是真正的并行。 同步和异步执行

并发的核心思想在于,大的任务可以分解成一系列的子任务,后者可以被调度成 同时执行或异步执行,而不是一次一个地或者同步地执行。两个子任务之间的 切换也就是上下文切换。

在gevent里面,上下文切换是通过yielding来完成的. 在下面的例子里, 我们有两个上下文,通过调用gevent.sleep(0),它们各自yield向对方。

import gevent

def foo():
    print('Running in foo')
    gevent.sleep(0)
    print('Explicit context switch to foo again')

def bar():
    print('Explicit context to bar')
    gevent.sleep(0)
    print('Implicit context switch back to bar')

gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
])

Running in foo
Explicit context to bar
Explicit context switch to foo again
Implicit context switch back to bar

下图将控制流形象化,就像在调试器中单步执行整个程序,以说明上下文切换如何发生。

当我们在受限于网络或IO的函数中使用gevent,这些函数会被协作式的调度, gevent的真正能力会得到发挥。Gevent处理了所有的细节, 来保证你的网络库会在可能的时候,隐式交出greenlet上下文的执行权。 这样的一种用法是如何强大,怎么强调都不为过。或者我们举些例子来详述。

下面例子中的select()函数通常是一个在各种文件描述符上轮询的阻塞调用。

import time
import gevent
from gevent import select

start = time.time()
tic = lambda: 'at %1.1f seconds' % (time.time() - start)

def gr1():
    # Busy waits for a second, but we don't want to stick around...
    print('Started Polling: %s' % tic())
    select.select([], [], [], 2)
    print('Ended Polling: %s' % tic())

def gr2():
    # Busy waits for a second, but we don't want to stick around...
    print('Started Polling: %s' % tic())
    select.select([], [], [], 2)
    print('Ended Polling: %s' % tic())

def gr3():
    print("Hey lets do some stuff while the greenlets poll, %s" % tic())
    gevent.sleep(1)

gevent.joinall([
    gevent.spawn(gr1),
    gevent.spawn(gr2),
    gevent.spawn(gr3),
])

Started Polling: at 0.0 seconds
Started Polling: at 0.0 seconds
Hey lets do some stuff while the greenlets poll, at 0.0 seconds
Ended Polling: at 2.0 seconds
Ended Polling: at 2.0 seconds

下面是另外一个多少有点人造色彩的例子,定义一个非确定性的(non-deterministic) 的task函数(给定相同输入的情况下,它的输出不保证相同)。 此例中执行这个函数的副作用就是,每次task在它的执行过程中都会随机地停某些秒。

import gevent
import random

def task(pid):
    """
    Some non-deterministic task
    """
    gevent.sleep(random.randint(0,2)*0.001)
    print('Task %s done' % pid)

def synchronous():
    for i in range(1,10):
        task(i)

def asynchronous():
    threads = [gevent.spawn(task, i) for i in xrange(10)]
    gevent.joinall(threads)

print('Synchronous:')
synchronous()

print('Asynchronous:')
asynchronous()

Synchronous:
Task 1 done
Task 2 done
Task 3 done
Task 4 done
Task 5 done
Task 6 done
Task 7 done
Task 8 done
Task 9 done
Asynchronous:
Task 3 done
Task 7 done
Task 9 done
Task 2 done
Task 4 done
Task 1 done
Task 8 done
Task 6 done
Task 0 done
Task 5 done

上例中,在同步的部分,所有的task都同步的执行, 结果当每个task在执行时主流程被阻塞(主流程的执行暂时停住)。

程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行所有给定的greenlet。执行流程只会在 所有greenlet执行完后才会继续向下走。

要重点留意的是,异步的部分本质上是随机的,而且异步部分的整体运行时间比同步 要大大减少。事实上,同步部分的最大运行时间,即是每个task停0.002秒,结果整个 队列要停0.02秒。而异步部分的最大运行时间大致为0.002秒,因为没有任何一个task会 阻塞其它task的执行。

一个更常见的应用场景,如异步地向服务器取数据,取数据操作的执行时间 依赖于发起取数据请求时远端服务器的负载,各个请求的执行时间会有差别。

import gevent.monkey
gevent.monkey.patch_socket()

import gevent
import urllib2
import simplejson as json

def fetch(pid):
    response = urllib2.urlopen('http://json-time.appspot.com/time.json')
    result = response.read()
    json_result = json.loads(result)
    datetime = json_result['datetime']

    print('Process %s: %s' % (pid, datetime))
    return json_result['datetime']

def synchronous():
    for i in range(1,10):
        fetch(i)

def asynchronous():
    threads = []
    for i in range(1,10):
        threads.append(gevent.spawn(fetch, i))
    gevent.joinall(threads)

print('Synchronous:')
synchronous()

print('Asynchronous:')
asynchronous()

确定性

就像之前所提到的,greenlet具有确定性。在相同配置相同输入的情况下,它们总是 会产生相同的输出。下面就有例子,我们在multiprocessing的pool之间执行一系列的 任务,与在gevent的pool之间执行作比较。

import time

def echo(i):
    time.sleep(0.001)
    return i

# Non Deterministic Process Pool

from multiprocessing.pool import Pool

p = Pool(10)
run1 = [a for a in p.imap_unordered(echo, xrange(10))]
run2 = [a for a in p.imap_unordered(echo, xrange(10))]
run3 = [a for a in p.imap_unordered(echo, xrange(10))]
run4 = [a for a in p.imap_unordered(echo, xrange(10))]

print(run1 == run2 == run3 == run4)

# Deterministic Gevent Pool

from gevent.pool import Pool

p = Pool(10)
run1 = [a for a in p.imap_unordered(echo, xrange(10))]
run2 = [a for a in p.imap_unordered(echo, xrange(10))]
run3 = [a for a in p.imap_unordered(echo, xrange(10))]
run4 = [a for a in p.imap_unordered(echo, xrange(10))]

print(run1 == run2 == run3 == run4)

False
True

即使gevent通常带有确定性,当开始与如socket或文件等外部服务交互时, 不确定性也可能溜进你的程序中。因此尽管gevent线程是一种“确定的并发”形式, 使用它仍然可能会遇到像使用POSIX线程或进程时遇到的那些问题。

涉及并发长期存在的问题就是竞争条件(race condition)。简单来说, 当两个并发线程/进程都依赖于某个共享资源同时都尝试去修改它的时候, 就会出现竞争条件。这会导致资源修改的结果状态依赖于时间和执行顺序。 这是个问题,我们一般会做很多努力尝试避免竞争条件, 因为它会导致整个程序行为变得不确定。

最好的办法是始终避免所有全局的状态。全局状态和导入时(import-time)副作用总是会 反咬你一口!

创建Greenlets

gevent对Greenlet初始化提供了一些封装,最常用的使用模板之一有

import gevent
from gevent import Greenlet

def foo(message, n):
    """
    Each thread will be passed the message, and n arguments
    in its initialization.
    """
    gevent.sleep(n)
    print(message)

# Initialize a new Greenlet instance running the named function
# foo
thread1 = Greenlet.spawn(foo, "Hello", 1)

# Wrapper for creating and running a new Greenlet from the named
# function foo, with the passed arguments
thread2 = gevent.spawn(foo, "I live!", 2)

# Lambda expressions
thread3 = gevent.spawn(lambda x: (x+1), 2)

threads = [thread1, thread2, thread3]

# Block until all threads complete.
gevent.joinall(threads)

Hello
I live!

除使用基本的Greenlet类之外,你也可以子类化Greenlet类,重载它的_run方法。

import gevent
from gevent import Greenlet

class MyGreenlet(Greenlet):

    def __init__(self, message, n):
        Greenlet.__init__(self)
        self.message = message
        self.n = n

    def _run(self):
        print(self.message)
        gevent.sleep(self.n)

g = MyGreenlet("Hi there!", 3)
g.start()
g.join()

Hi there!

Greenlet状态

就像任何其他成段代码,Greenlet也可能以不同的方式运行失败。 Greenlet可能未能成功抛出异常,不能停止运行,或消耗了太多的系统资源。

一个greenlet的状态通常是一个依赖于时间的参数。在greenlet中有一些标志, 让你可以监视它的线程内部状态:

  • started – Boolean, 指示此Greenlet是否已经启动
  • ready() – Boolean, 指示此Greenlet是否已经停止
  • successful() – Boolean, 指示此Greenlet是否已经停止而且没抛异常
  • value – 任意值, 此Greenlet代码返回的值
  • exception – 异常, 此Greenlet内抛出的未捕获异常

代码:

import gevent

def win():
    return 'You win!'

def fail():
    raise Exception('You fail at failing.')

winner = gevent.spawn(win)
loser = gevent.spawn(fail)

print(winner.started) # True
print(loser.started)  # True

# Exceptions raised in the Greenlet, stay inside the Greenlet.
try:
    gevent.joinall([winner, loser])
except Exception as e:
    print('This will never be reached')

print(winner.value) # 'You win!'
print(loser.value)  # None

print(winner.ready()) # True
print(loser.ready())  # True

print(winner.successful()) # True
print(loser.successful())  # False

# The exception raised in fail, will not propogate outside the
# greenlet. A stack trace will be printed to stdout but it
# will not unwind the stack of the parent.

print(loser.exception)

# It is possible though to raise the exception again outside
# raise loser.exception
# or with
# loser.get()

True
True
You win!
None
True
True
True
False
You fail at failing.

程序停止

当主程序(main program)收到一个SIGQUIT信号时,不能成功做yield操作的 Greenlet可能会令意外地挂起程序的执行。这导致了所谓的僵尸进程, 它需要在Python解释器之外被kill掉。

对此,一个通用的处理模式就是在主程序中监听SIGQUIT信号,在程序退出 调用gevent.shutdown。

import gevent
import signal

def run_forever():
    gevent.sleep(1000)

if __name__ == '__main__':
    gevent.signal(signal.SIGQUIT, gevent.shutdown)
    thread = gevent.spawn(run_forever)
    thread.join()

超时

超时是一种对一块代码或一个Greenlet的运行时间的约束。

import gevent
from gevent import Timeout

seconds = 10

timeout = Timeout(seconds)
timeout.start()

def wait():
    gevent.sleep(10)

try:
    gevent.spawn(wait).join()
except Timeout:
    print('Could not complete')

超时类也可以用在上下文管理器(context manager)中, 也就是with语句内。

import gevent
from gevent import Timeout

time_to_wait = 5 # seconds

class TooLong(Exception):
    pass

with Timeout(time_to_wait, TooLong):
    gevent.sleep(10)

另外,对各种Greenlet和数据结构相关的调用,gevent也提供了超时参数。 例如:

import gevent
from gevent import Timeout

def wait():
    gevent.sleep(2)

timer = Timeout(1).start()
thread1 = gevent.spawn(wait)

try:
    thread1.join(timeout=timer)
except Timeout:
    print('Thread 1 timed out')

# --

timer = Timeout.start_new(1)
thread2 = gevent.spawn(wait)

try:
    thread2.get(timeout=timer)
except Timeout:
    print('Thread 2 timed out')

# --

try:
    gevent.with_timeout(1, wait)
except Timeout:
    print('Thread 3 timed out')

Thread 1 timed out
Thread 2 timed out
Thread 3 timed out

猴子补丁(Monkey patching)

我们现在来到gevent的死角了. 在此之前,我已经避免提到猴子补丁(monkey patching) 以尝试使gevent这个强大的协程模型变得生动有趣,但现在到了讨论猴子补丁的黑色艺术 的时候了。你之前可能注意到我们提到了monkey.patch_socket()这个命令,这个 纯粹副作用命令是用来改变标准socket库的。

import socket
print(socket.socket)

print("After monkey patch")
from gevent import monkey
monkey.patch_socket()
print(socket.socket)

import select
print(select.select)
monkey.patch_select()
print("After monkey patch")
print(select.select)

class 'socket.socket'
After monkey patch
class 'gevent.socket.socket'

built-in function select
After monkey patch
function select at 0x1924de8

Python的运行环境允许我们在运行时修改大部分的对象,包括模块,类甚至函数。 这是个一般说来令人惊奇的坏主意,因为它创造了“隐式的副作用”,如果出现问题 它很多时候是极难调试的。虽然如此,在极端情况下当一个库需要修改Python本身 的基础行为的时候,猴子补丁就派上用场了。在这种情况下,gevent能够 修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。

例如,Redis的python绑定一般使用常规的tcp socket来与redis-server实例通信。 通过简单地调用gevent.monkey.patch_all(),可以使得redis的绑定协作式的调度 请求,与gevent栈的其它部分一起工作。

这让我们可以将一般不能与gevent共同工作的库结合起来,而不用写哪怕一行代码。 虽然猴子补丁仍然是邪恶的(evil),但在这种情况下它是“有用的邪恶(useful evil)”。

数据结构

事件

事件(event)是一个在Greenlet之间异步通信的形式。

import gevent
from gevent.event import Event

'''
Illustrates the use of events
'''

evt = Event()

def setter():
    '''After 3 seconds, wake all threads waiting on the value of evt'''
    print('A: Hey wait for me, I have to do something')
    gevent.sleep(3)
    print("Ok, I'm done")
    evt.set()

def waiter():
    '''After 3 seconds the get call will unblock'''
    print("I'll wait for you")
    evt.wait()  # blocking
    print("It's about time")

def main():
    gevent.joinall([
        gevent.spawn(setter),
        gevent.spawn(waiter),
        gevent.spawn(waiter),
        gevent.spawn(waiter),
        gevent.spawn(waiter),
        gevent.spawn(waiter)
    ])

if __name__ == '__main__': main()

事件对象的一个扩展是AsyncResult,它允许你在唤醒调用上附加一个值。 它有时也被称作是future或defered,因为它持有一个指向将来任意时间可设置 为任何值的引用。

import gevent
from gevent.event import AsyncResult
a = AsyncResult()

def setter():
    """
    After 3 seconds set the result of a.
    """
    gevent.sleep(3)
    a.set('Hello!')

def waiter():
    """
    After 3 seconds the get call will unblock after the setter
    puts a value into the AsyncResult.
    """
    print(a.get())

gevent.joinall([
    gevent.spawn(setter),
    gevent.spawn(waiter),
])

队列

队列是一个排序的数据集合,它有常见的put / get操作, 但是它是以在Greenlet之间可以安全操作的方式来实现的。

举例来说,如果一个Greenlet从队列中取出一项,此项就不会被 同时执行的其它Greenlet再取到了。

import gevent
from gevent.queue import Queue

tasks = Queue()

def worker(n):
    while not tasks.empty():
        task = tasks.get()
        print('Worker %s got task %s' % (n, task))
        gevent.sleep(0)

    print('Quitting time!')

def boss():
    for i in xrange(1,25):
        tasks.put_nowait(i)

gevent.spawn(boss).join()

gevent.joinall([
    gevent.spawn(worker, 'steve'),
    gevent.spawn(worker, 'john'),
    gevent.spawn(worker, 'nancy'),
])

Worker steve got task 1
Worker john got task 2
Worker nancy got task 3
Worker steve got task 4
Worker nancy got task 5
Worker john got task 6
Worker steve got task 7
Worker john got task 8
Worker nancy got task 9
Worker steve got task 10
Worker nancy got task 11
Worker john got task 12
Worker steve got task 13
Worker john got task 14
Worker nancy got task 15
Worker steve got task 16
Worker nancy got task 17
Worker john got task 18
Worker steve got task 19
Worker john got task 20
Worker nancy got task 21
Worker steve got task 22
Worker nancy got task 23
Worker john got task 24
Quitting time!
Quitting time!
Quitting time!

如果需要,队列也可以阻塞在put或get操作上。

put和get操作都有非阻塞的版本,put_nowait和get_nowait不会阻塞, 然而在操作不能完成时抛出gevent.queue.Empty或gevent.queue.Full异常。

在下面例子中,我们让boss与多个worker同时运行,并限制了queue不能放入多于3个元素。 这个限制意味着,直到queue有空余空间之间,put操作会被阻塞。相反地,如果队列中 没有元素,get操作会被阻塞。它同时带一个timeout参数,允许在超时时间内如果 队列没有元素无法完成操作就抛出gevent.queue.Empty异常。

import gevent
from gevent.queue import Queue, Empty

tasks = Queue(maxsize=3)

def worker(n):
    try:
        while True:
            task = tasks.get(timeout=1) # decrements queue size by 1
            print('Worker %s got task %s' % (n, task))
            gevent.sleep(0)
    except Empty:
        print('Quitting time!')

def boss():
    """
    Boss will wait to hand out work until a individual worker is
    free since the maxsize of the task queue is 3.
    """

    for i in xrange(1,10):
        tasks.put(i)
    print('Assigned all work in iteration 1')

    for i in xrange(10,20):
        tasks.put(i)
    print('Assigned all work in iteration 2')

gevent.joinall([
    gevent.spawn(boss),
    gevent.spawn(worker, 'steve'),
    gevent.spawn(worker, 'john'),
    gevent.spawn(worker, 'bob'),
])

Worker steve got task 1
Worker john got task 2
Worker bob got task 3
Worker steve got task 4
Worker bob got task 5
Worker john got task 6
Assigned all work in iteration 1
Worker steve got task 7
Worker john got task 8
Worker bob got task 9
Worker steve got task 10
Worker bob got task 11
Worker john got task 12
Worker steve got task 13
Worker john got task 14
Worker bob got task 15
Worker steve got task 16
Worker bob got task 17
Worker john got task 18
Assigned all work in iteration 2
Worker steve got task 19
Quitting time!
Quitting time!
Quitting time!

组和池

组(group)是一个运行中greenlet的集合,集合中的greenlet像一个组一样 会被共同管理和调度。 它也兼饰了像Python的multiprocessing库那样的 平行调度器的角色。

import gevent
from gevent.pool import Group

def talk(msg):
    for i in xrange(3):
        print(msg)

g1 = gevent.spawn(talk, 'bar')
g2 = gevent.spawn(talk, 'foo')
g3 = gevent.spawn(talk, 'fizz')

group = Group()
group.add(g1)
group.add(g2)
group.join()

group.add(g3)
group.join()

bar
bar
bar
foo
foo
foo
fizz
fizz
fizz

在管理异步任务的分组上它是非常有用的。

就像上面所说,Group也以不同的方式为分组greenlet/分发工作和收集它们的结果也提供了API。

import gevent
from gevent import getcurrent
from gevent.pool import Group

group = Group()

def hello_from(n):
    print('Size of group %s' % len(group))
    print('Hello from Greenlet %s' % id(getcurrent()))

group.map(hello_from, xrange(3))

def intensive(n):
    gevent.sleep(3 - n)
    return 'task', n

print('Ordered')

ogroup = Group()
for i in ogroup.imap(intensive, xrange(3)):
    print(i)

print('Unordered')

igroup = Group()
for i in igroup.imap_unordered(intensive, xrange(3)):
    print(i)

Size of group 3
Hello from Greenlet 31048720
Size of group 3
Hello from Greenlet 31049200
Size of group 3
Hello from Greenlet 31049040
Ordered
('task', 0)
('task', 1)
('task', 2)
Unordered
('task', 2)
('task', 1)
('task', 0)

池(pool)是一个为处理数量变化并且需要限制并发的greenlet而设计的结构。 在需要并行地做很多受限于网络和IO的任务时常常需要用到它。

import gevent
from gevent.pool import Pool

pool = Pool(2)

def hello_from(n):
    print('Size of pool %s' % len(pool))

pool.map(hello_from, xrange(3))

Size of pool 2
Size of pool 2
Size of pool 1

当构造gevent驱动的服务时,经常会将围绕一个池结构的整个服务作为中心。 一个例子就是在各个socket上轮询的类。

from gevent.pool import Pool

class SocketPool(object):

    def __init__(self):
        self.pool = Pool(1000)
        self.pool.start()

    def listen(self, socket):
        while True:
            socket.recv()

    def add_handler(self, socket):
        if self.pool.full():
            raise Exception("At maximum pool size")
        else:
            self.pool.spawn(self.listen, socket)

    def shutdown(self):
        self.pool.kill()

锁和信号量

信号量是一个允许greenlet相互合作,限制并发访问或运行的低层次的同步原语。 信号量有两个方法,acquire和release。在信号量是否已经被 acquire或release,和拥有资源的数量之间不同,被称为此信号量的范围 (the bound of the semaphore)。如果一个信号量的范围已经降低到0,它会 阻塞acquire操作直到另一个已经获得信号量的greenlet作出释放。

from gevent import sleep
from gevent.pool import Pool
from gevent.coros import BoundedSemaphore

sem = BoundedSemaphore(2)

def worker1(n):
    sem.acquire()
    print('Worker %i acquired semaphore' % n)
    sleep(0)
    sem.release()
    print('Worker %i released semaphore' % n)

def worker2(n):
    with sem:
        print('Worker %i acquired semaphore' % n)
        sleep(0)
    print('Worker %i released semaphore' % n)

pool = Pool()
pool.map(worker1, xrange(0,2))
pool.map(worker2, xrange(3,6))

Worker 0 acquired semaphore
Worker 1 acquired semaphore
Worker 0 released semaphore
Worker 1 released semaphore
Worker 3 acquired semaphore
Worker 4 acquired semaphore
Worker 3 released semaphore
Worker 4 released semaphore
Worker 5 acquired semaphore
Worker 5 released semaphore

范围为1的信号量也称为锁(lock)。它向单个greenlet提供了互斥访问。 信号量和锁常常用来保证资源只在程序上下文被单次使用。 线程局部变量

Gevent也允许你指定局部于greenlet上下文的数据。 在内部,它被实现为以greenlet的getcurrent()为键, 在一个私有命名空间寻址的全局查找。

import gevent
from gevent.local import local

stash = local()

def f1():
    stash.x = 1
    print(stash.x)

def f2():
    stash.y = 2
    print(stash.y)

    try:
        stash.x
    except AttributeError:
        print("x is not local to f2")

g1 = gevent.spawn(f1)
g2 = gevent.spawn(f2)

gevent.joinall([g1, g2])

1
2
x is not local to f2

很多集成了gevent的web框架将HTTP会话对象以线程局部变量的方式存储在gevent内。 例如使用Werkzeug实用库和它的proxy对象,我们可以创建Flask风格的请求对象。

from gevent.local import local
from werkzeug.local import LocalProxy
from werkzeug.wrappers import Request
from contextlib import contextmanager

from gevent.wsgi import WSGIServer

_requests = local()
request = LocalProxy(lambda: _requests.request)

@contextmanager
def sessionmanager(environ):
    _requests.request = Request(environ)
    yield
    _requests.request = None

def logic():
    return "Hello " + request.remote_addr

def application(environ, start_response):
    status = '200 OK'

    with sessionmanager(environ):
        body = logic()

    headers = [
        ('Content-Type', 'text/html')
    ]

    start_response(status, headers)
    return [body]

WSGIServer(('', 8000), application).serve_forever()

Flask系统比这个例子复杂一点,然而使用线程局部变量作为局部的会话存储, 这个思想是相同的。 子进程

自gevent 1.0起,gevent.subprocess,一个Python subprocess模块 的修补版本已经添加。它支持协作式的等待子进程。

import gevent
from gevent.subprocess import Popen, PIPE

def cron():
    while True:
        print("cron")
        gevent.sleep(0.2)

g = gevent.spawn(cron)
sub = Popen(['sleep 1; uname'], stdout=PIPE, shell=True)
out, err = sub.communicate()
g.kill()
print(out.rstrip())

cron
cron
cron
cron
cron
Linux

很多人也想将gevent和multiprocessing一起使用。最明显的挑战之一 就是multiprocessing提供的进程间通信默认不是协作式的。由于基于 multiprocessing.Connection的对象(例如Pipe)暴露了它们下面的 文件描述符(file descriptor),gevent.socket.wait_read和wait_write 可以用来在直接读写之前协作式的等待ready-to-read/ready-to-write事件。

import gevent
from multiprocessing import Process, Pipe
from gevent.socket import wait_read, wait_write

# To Process
a, b = Pipe()

# From Process
c, d = Pipe()

def relay():
    for i in xrange(10):
        msg = b.recv()
        c.send(msg + " in " + str(i))

def put_msg():
    for i in xrange(10):
        wait_write(a.fileno())
        a.send('hi')

def get_msg():
    for i in xrange(10):
        wait_read(d.fileno())
        print(d.recv())

if __name__ == '__main__':
    proc = Process(target=relay)
    proc.start()

    g1 = gevent.spawn(get_msg)
    g2 = gevent.spawn(put_msg)
    gevent.joinall([g1, g2], timeout=1)

然而要注意,组合multiprocessing和gevent必定带来 依赖于操作系统(os-dependent)的缺陷,其中有:

  • 在兼容POSIX的系统创建子进程(forking)之后, 在子进程的gevent的状态是不适定的(ill-posed)。一个副作用就是, multiprocessing.Process创建之前的greenlet创建动作,会在父进程和子进程两 方都运行。
  • 上例的put_msg()中的a.send()可能依然非协作式地阻塞调用的线程:一个 ready-to-write事件只保证写了一个byte。在尝试写完成之前底下的buffer可能是满的。
  • 上面表示的基于wait_write()/wait_read()的方法在Windows上不工作 (IOError: 3 is not a socket (files are not supported)),因为Windows不能监视 pipe事件。

Python包gipc以大体上透明的方式在 兼容POSIX系统和Windows上克服了这些挑战。它提供了gevent感知的基于 multiprocessing.Process的子进程和gevent基于pipe的协作式进程间通信。 Actors

actor模型是一个由于Erlang变得普及的更高层的并发模型。 简单的说它的主要思想就是许多个独立的Actor,每个Actor有一个可以从 其它Actor接收消息的收件箱。Actor内部的主循环遍历它收到的消息,并 根据它期望的行为来采取行动。

Gevent没有原生的Actor类型,但在一个子类化的Greenlet内使用队列, 我们可以定义一个非常简单的。

import gevent
from gevent.queue import Queue

class Actor(gevent.Greenlet):

    def __init__(self):
        self.inbox = Queue()
        Greenlet.__init__(self)

    def receive(self, message):
        """
        Define in your subclass.
        """
        raise NotImplemented()

    def _run(self):
        self.running = True

        while self.running:
            message = self.inbox.get()
            self.receive(message)

下面是一个使用的例子:

import gevent
from gevent.queue import Queue
from gevent import Greenlet

class Pinger(Actor):
    def receive(self, message):
        print(message)
        pong.inbox.put('ping')
        gevent.sleep(0)

class Ponger(Actor):
    def receive(self, message):
        print(message)
        ping.inbox.put('pong')
        gevent.sleep(0)

ping = Pinger()
pong = Ponger()

ping.start()
pong.start()

ping.inbox.put('start')
gevent.joinall([ping, pong])

真实世界的应用

Gevent ZeroMQ

ZeroMQ 被它的作者描述为 “一个表现得像一个并发框架的socket库”。 它是一个非常强大的,为构建并发和分布式应用的消息传递层。

ZeroMQ提供了各种各样的socket原语。最简单的是请求-应答socket对 (Request-Response socket pair)。一个socket有两个方法send和recv, 两者一般都是阻塞操作。但是Travis Cline 的一个杰出的库弥补了这一点,这个库使用gevent.socket来以非阻塞的方式 轮询ZereMQ socket。通过命令:

pip install gevent-zeromq

你可以从PyPi安装gevent-zeremq。

# Note: Remember to ``pip install pyzmq gevent_zeromq``
import gevent
from gevent_zeromq import zmq

# Global Context
context = zmq.Context()

def server():
    server_socket = context.socket(zmq.REQ)
    server_socket.bind("tcp://127.0.0.1:5000")

    for request in range(1,10):
        server_socket.send("Hello")
        print('Switched to Server for %s' % request)
        # Implicit context switch occurs here
        server_socket.recv()

def client():
    client_socket = context.socket(zmq.REP)
    client_socket.connect("tcp://127.0.0.1:5000")

    for request in range(1,10):

        client_socket.recv()
        print('Switched to Client for %s' % request)
        # Implicit context switch occurs here
        client_socket.send("World")

publisher = gevent.spawn(server)
client    = gevent.spawn(client)

gevent.joinall([publisher, client])


Switched to Server for 1
Switched to Client for 1
Switched to Server for 2
Switched to Client for 2
Switched to Server for 3
Switched to Client for 3
Switched to Server for 4
Switched to Client for 4
Switched to Server for 5
Switched to Client for 5
Switched to Server for 6
Switched to Client for 6
Switched to Server for 7
Switched to Client for 7
Switched to Server for 8
Switched to Client for 8
Switched to Server for 9
Switched to Client for 9

简单server

# On Unix: Access with ``$ nc 127.0.0.1 5000``
# On Window: Access with ``$ telnet 127.0.0.1 5000``

from gevent.server import StreamServer

def handle(socket, address):
    socket.send("Hello from a telnet!\n")
    for i in range(5):
        socket.send(str(i) + '\n')
    socket.close()

server = StreamServer(('127.0.0.1', 5000), handle)
server.serve_forever()

WSGI Servers

Gevent为HTTP内容服务提供了两种WSGI server。从今以后就称为 wsgi和pywsgi:

    gevent.wsgi.WSGIServer
    gevent.pywsgi.WSGIServer

在1.0.x之前更早期的版本里,gevent使用libevent而不是libev。 Libevent包含了一个快速HTTP server,它被用在gevent的wsgi server。

在gevent 1.0.x版本,没有包括http server了。作为替代,gevent.wsgi 现在是纯Python server gevent.pywsgi的一个别名。

流式server

这个章节不适用于gevent 1.0.x版本

熟悉流式HTTP服务(streaming HTTP service)的人知道,它的核心思想 就是在头部(header)不指定内容的长度。反而,我们让连接保持打开, 在每块数据前加一个16进制字节来指示数据块的长度,并将数据刷入pipe中。 当发出一个0长度数据块时,流会被关闭。

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

8
<p>Hello

9
World</p>

0

上述的HTTP连接不能在wsgi中创建,因为它不支持流式。 请求只有被缓冲(buffered)下来。

from gevent.wsgi import WSGIServer

def application(environ, start_response):
    status = '200 OK'
    body = '<p>Hello World</p>'

    headers = [
        ('Content-Type', 'text/html')
    ]

    start_response(status, headers)
    return [body]

WSGIServer(('', 8000), application).serve_forever()

然而使用pywsgi我们可以将handler写成generator,并以块的形式yield出结果。

from gevent.pywsgi import WSGIServer

def application(environ, start_response):
    status = '200 OK'

    headers = [
        ('Content-Type', 'text/html')
    ]

    start_response(status, headers)
    yield "<p>Hello"
    yield "World</p>"

WSGIServer(('', 8000), application).serve_forever()

但无论如何,与其它Python server相比gevent server性能是显胜的。 Libev是得到非常好审查的技术,由它写出的server在大规模上表现优异为人熟知。

为了测试基准,试用Apache Benchmark ab或浏览Benchmark of Python WSGI Servers来与其它server作对比。

$ ab -n 10000 -c 100 http://127.0.0.1:8000/

Long Polling

import gevent
from gevent.queue import Queue, Empty
from gevent.pywsgi import WSGIServer
import simplejson as json

data_source = Queue()

def producer():
    while True:
        data_source.put_nowait('Hello World')
        gevent.sleep(1)

def ajax_endpoint(environ, start_response):
    status = '200 OK'
    headers = [
        ('Content-Type', 'application/json')
    ]

    start_response(status, headers)

    while True:
        try:
            datum = data_source.get(timeout=5)
            yield json.dumps(datum) + '\n'
        except Empty:
            pass

gevent.spawn(producer)

WSGIServer(('', 8000), ajax_endpoint).serve_forever()

Websockets

运行Websocket的例子需要gevent-websocket包。

# Simple gevent-websocket server
import json
import random

from gevent import pywsgi, sleep
from geventwebsocket.handler import WebSocketHandler

class WebSocketApp(object):
    '''Send random data to the websocket'''

    def __call__(self, environ, start_response):
        ws = environ['wsgi.websocket']
        x = 0
        while True:
            data = json.dumps({'x': x, 'y': random.randint(1, 5)})
            ws.send(data)
            x += 1
            sleep(0.5)

server = pywsgi.WSGIServer(("", 10000), WebSocketApp(),
    handler_class=WebSocketHandler)
server.serve_forever()

HTML Page:

<html>
    <head>
        <title>Minimal websocket application</title>
        <script type="text/javascript" src="jquery.min.js"></script>
        <script type="text/javascript">
        $(function() {
            // Open up a connection to our server
            var ws = new WebSocket("ws://localhost:10000/");

            // What do we do when we get a message?
            ws.onmessage = function(evt) {
                $("#placeholder").append('<p>' + evt.data + '</p>')
            }
            // Just update our conn_status field with the connection status
            ws.onopen = function(evt) {
                $('#conn_status').html('<b>Connected</b>');
            }
            ws.onerror = function(evt) {
                $('#conn_status').html('<b>Error</b>');
            }
            ws.onclose = function(evt) {
                $('#conn_status').html('<b>Closed</b>');
            }
        });
    </script>
    </head>
    <body>
        <h1>WebSocket Example</h1>
        <div id="conn_status">Not Connected</div>
        <div id="placeholder" style="width:600px;height:300px;"></div>
    </body>
</html>

聊天server

最后一个生动的例子,实现一个实时聊天室。运行这个例子需要 Flask (你可以使用Django, Pyramid等,但不是必须的)。 对应的Javascript和HTML文件可以在这里找到。

# Micro gevent chatroom.
# ----------------------

from flask import Flask, render_template, request

from gevent import queue
from gevent.pywsgi import WSGIServer

import simplejson as json

app = Flask(__name__)
app.debug = True

rooms = {
    'topic1': Room(),
    'topic2': Room(),
}

users = {}

class Room(object):

    def __init__(self):
        self.users = set()
        self.messages = []

    def backlog(self, size=25):
        return self.messages[-size:]

    def subscribe(self, user):
        self.users.add(user)

    def add(self, message):
        for user in self.users:
            print(user)
            user.queue.put_nowait(message)
        self.messages.append(message)

class User(object):

    def __init__(self):
        self.queue = queue.Queue()

@app.route('/')
def choose_name():
    return render_template('choose.html')

@app.route('/<uid>')
def main(uid):
    return render_template('main.html',
        uid=uid,
        rooms=rooms.keys()
    )

@app.route('/<room>/<uid>')
def join(room, uid):
    user = users.get(uid, None)

    if not user:
        users[uid] = user = User()

    active_room = rooms[room]
    active_room.subscribe(user)
    print('subscribe %s %s' % (active_room, user))

    messages = active_room.backlog()

    return render_template('room.html',
        room=room, uid=uid, messages=messages)

@app.route("/put/<room>/<uid>", methods=["POST"])
def put(room, uid):
    user = users[uid]
    room = rooms[room]

    message = request.form['message']
    room.add(':'.join([uid, message]))

    return ''

@app.route("/poll/<uid>", methods=["POST"])
def poll(uid):
    try:
        msg = users[uid].queue.get(timeout=10)
    except queue.Empty:
        msg = []
    return json.dumps(msg)

if __name__ == "__main__":
    http = WSGIServer(('', 5000), app)
    http.serve_forever()

jack.zh 标签:python 继续阅读

803 ℉

15.04.24

C基础-理解c语言的sizeof

一起看看sizeof。c语言通过类型长度来达到指针的灵活性,我觉得,某种意义上讲,是sizeof功能成就了c指针。

基础知识

首先,要知道sizeof 是关键字不是函数。也就是说,用到sizeof的地方其实在编译阶段就已经计算出结果了,不是(也不能)在程序运行时动态地计算。换句话说,代码中同一个sizeof的调用只能输出一个值,而不可能有其它别的值(后文会看到,其实变长数组是颠覆了这个规律的)。再换句话说,就是反汇编就能看到sizeof调用的结果!

其次,sizeof的计算结果跟编译器的字节对齐方式有关。在默认情况下,c编译器为每一个变量按其类型大小分配空间,这种默认方式是可以修改的,通过#pragma pack (n)或者__attribute((aligned (n)))

最后,要知道对齐是个性能要求,不是必须的。我们这里仅考虑gcc编译器,不考虑vc编译器。比如ia32下,gcc对doublelong long这样的8字节变量,仍然是按4字节对齐,即使设置#pragma pack(8)的情况下。到了x64_64下,开始统一了,全部真的都是按照类型大小对齐的了。至于为何跟性能相关,我们以后讲到cpu cache时再重新考虑这个问题,现在我们只要知道,因为地址总线放地址时肯定都是对齐的,所以不对齐的话会增加读取周期就行了。

下文全部以gcc+x86_64+结构体,来求解sizeof

计算原理

对于每个数据类型都有一个align_size,其实就是类型大小:char是1,short是2,int是4,long是8,double是8,long double是16。

一个基本原则就是结构体里的每个成员都能按照自己类型的align_size去对齐。计算过程如下:

  • 计算出该结构体里面成员中最大的align_size,此也即该结构体的align_size
  • 该结构体中得每个成员都按照自己的align_size去对齐,空余部分会做填充处理,成员是数组的话,其align_size就是数组元素的align_size。成员又是另一个结构体的话,用此计算方法先求出另一个结构体的align_size
  • 整个结构体是首尾都按照该结构体的align_size对齐的,即手部和尾部的空余部分会做填充处理

很显然,编译器在计算过程中会自动填充空余部分。否则,想想一个结构体,内部空间都不是连续填充的,会让编译器分配空间时疯掉的。

代码示例

我们举例看看,long double的类型大小是16字节。

struct S{
    char a;
    long double b[1];
};
struct S sample;
printf("%ld,%ld\n", sizeof(struct S), sizeof(sample.b));

打印结果是32,16。因为结构体成员中最大的align_size(数组的话,看数组里面的单个元素的align_size)是16字节,所以整个结构体就是16字节对齐的,这样char也自动填充到16字节了。故,总的结构体大小就是32字节了。

那么,将结构体的b[1]改为b[0]呢?也就是说,结构体内部引入零数组成员

此时结果是16,0。也就是说该结构体还是16字节对齐,说明零数组元素还是参与了计算结构体的align_size,但是没有占用空间。

那么,还是维持为b[0],我们利用零数组的特性来放点数据呢?改为:

struct S *sample =(struct S*)malloc(sizeof(long double) * 1234 +sizeof(struct S));

结果还是16,0。也就是说跟零数组的数据大小没有关系,sizeof是感知不到的。

那么,如果不用malloc,直接初始化赋值呢?因为我们知道字符串数组是可以这么做的,但是作为结构体成员呢?改为struct s sample={'1',{1,2,3}}这种方式:

#include <stdio.h>
struct S{
    char a;
    long double b[];
};
struct S sample={'1',{1,2,3}};
int main ()
{
    printf("%Lf, %Lf, %Lf\n", sample.b[0], sample.b[1], sample.b[2]);
    //printf("%ld,%ld\n", sizeof(sample),sizeof(sample.b));//加上此句会编译不过
    return 0;
}

初始化赋值是正常的,但是sizeof那句编译不过:

error: invalid application of ‘sizeof’ to incomplete type ‘long double[]’

另外,对于b[]这种非零数组成员的结构体必须为全局静态定义,否则编译不过:

error: non-static initialization of a flexible array member

必须全局静态存储区间存放,才允许初始化赋值,是可以理解的,因为字符串数组(即使是定义在函数内部也能直接赋值。区别是char *str="123"是存放在全局静态空间,char str[]="123"是存放在栈空间,并且后者的sizeof是能得出数组大小的)就是这么实现的。但是在加上sizeof(sample.b)就编译不过,难道是gcc没有做好?因为编译期间是可以通过计算全局静态区间的b[]大小算出来的。(为方便下文描述,这种形式我们称为非零长数组)

那么,再看看改为b[0]会怎么样。此时是零长数组了,所以不必放在全局静态存储空间了。

#include <stdio.h>
int main ()
{
    struct S{
        char a;
        long double b[0];
    };
    struct S sample={'1',{1,2,3}};
    printf("%Lf, %Lf, %Lf\n", sample.b[0], sample.b[1], sample.b[2]);
    printf("%ld,%ld\n", sizeof(sample),sizeof(sample.b));
    return 0;
}

b[0]表面上可以正常初始化赋值,但是实际没有做处理的:编译期间可以看到gcc的数组越界初始化警告:(此时会体会到-Werror的好处了)

warning: excess elements in array initializer [enabled by default]
warning: (near initialization for ‘sample.b’) [enabled by default]

带着警告的运行实现结果:

0.000000, -0.000000, 0.000000
16,0

显然,b[]中3个数值没有正确处理。而sizeof的运算结果和上面的一致:还是无法感知数组大小。

我们最后抛开结构体内成员限制(下文有原因),观察下变长数组:元素个数是一个变量(跟上面的b[]形式的非零长数组是有区别的)。它实际就是在栈空间(既然变长,肯定不能是全局静态空间)分配空间的。

#include<stdio.h>

int main(void)
{
    int i;
    scanf("%d", &i);
    char str[i];
    printf("sizeof(str[%d])=%d\n",i,sizeof(str));
    return 0;
}

如果输入i为3,结果为3,3。对sizeof而言,变长数组是维持了数组的sizeof特性,毕竟以前普通数组就是这样的效果!需要注意的是,变长数组只能定义在栈空间,不能全局静态存储空间或堆空间定义,而且由编译器会尽量放到栈帧局部变量部分的最后。放在最后,是为了方便扩展,那如果块范围内有多个变长数组呢?多个就一个一个放到最后,逐个顺序扩展(后面会有示例)。

那如果变长数组作为入参呢?也是维持了数组的sizeof特性,还是和以前一样:对入参做sizeof,结果就是指针长度8。(此处仅考虑一维的情况,后文会考虑多维)

涉及多维数组以及指针的内容,我们以后再讲。。。

关于变长数组的补充

变长数组(variable length array,即VLA)是c99引入的,我们上面看过一个例子了。现在再看一个:

#include<limits.h>
#include<stdio.h>

int main(int argc, char *argv[])
{   
    int i, n;
    n = atoi(argv[1]);
    char str[n+1];
    for (i = 0; i < n; i++) {
        str[i] = (char)('0' + i);
    }
    str[n]='\0';
    printf("str is %s\n", str);
    printf("str:%ld, %ld\n",sizeof(str[ULONG_MAX]),sizeof(str));
    return 0;
}

当入参argv[1]为“3”时,看结果:

str is 012
str:1, 4

可见,对于变长数组,感知到了数组大小!另外,我们看到sizeof(str[ULONG_MAX])也是可以的,看来计算时是只看元素类型大小,不考虑数组下标范围的!

总结变长数组的特点,如下:

  • 1)必须在块范围内定义,不能在文件范围内定义(static修饰)或全局引用(extern修饰),即保证只能是栈空间分配;
  • 2)变长数组不能作结构体或者联合的成员,只能以独立数组方式存在;
  • 3)作用域为块范围,即其生存周期为所在块入栈和出栈之间的时间内;

第二个特点也是我们不去设置变长数组成员的结构体的原因

其实有个函数功能和变长数组类似,就是void alloca(size_t size)。它也是在栈中分配size字节大小的空间,当本栈帧退出时释放空间。要注意的是,alloca()执行失败时,不会返回一个NULL指针,因为它本质就是一条调整栈顶指针的汇编指令,不能有丰富的返回值。汇编实现就导致了很差的移植性,所以,相对而言,这种场景更应该用变长数组。

最后看下,变长数组在多维时的处理。看代码:

#include <stdio.h>
int main( void )
{
    int i;
    for(i=0;i<2;i++) {
        int m,n;
        scanf("%d %d", &m,&n);
        char a[m][n];
        char (*p)[n]=a;
        printf("%ld %ld", sizeof(a), sizeof(*p));
    }
    return 0;
}

输入3,4时,结果是12,4;输入5,6时,结果是30,6

可见,变长数组在各个维度上都很好地维持了普通数组的sizeof特性。另外,通过for循环也看到了块范围内变长数组可使用的重复性。

对于二维变长数组作为函数参数也维持了普通二维数组的效果:

#include <stdio.h>

void func(int,int,long double a[*][*]);
void func2(long double a[2][6]);

int main(void)
{
     int m=2, n=3;
     long double a[m][m*n];
     func(m,n,a);
     long double b[2][6];
     func2(b);
     return 0;
}

void func(int m,int n,long double x[m][m*n])
{
     printf("%ld %ld %ld\n",sizeof(x), sizeof(x[0]), sizeof(x[0][0]));
}

void func2(long double x[2][6]){
     printf("%ld %ld %ld\n",sizeof(x), sizeof(x[0]), sizeof(x[0][0]));
}

输出结果表明,两种处理是一样的:

8 96 16
8 96 16

可见,变长多维数组维持普通多维数组一样的效果:直接对入参取sizeof的结果是指针长度8,其它都能正常感知数组大小。为何数组做入参时,sizeof就识别不出来,只能做指针大小处理呢?看懂下面这个例子,就明白了:

#include <stdio.h>

void func(long double x[][3]){
      printf("%ld %ld %ld\n",sizeof(x), sizeof(x[0]), sizeof(x[0][0]));
}

int main(void)
{
      long double a[1][3];
      long double b[2][3];
      func(a);
      func(b);
      return 0;
}

没错,是gcc编译器支持的这种数组第一维可以省略的参数形式的后遗症。

总结

我们最后做个总结。对结构体的成员而言,零数组用在堆空间,非零数组用在全局静态空间,变长数组不能用(独立的变长数组用在栈空间)。对sizeof处理而言,只有变长数组(以及非零数组的独立形式)以栈为存储空间的处理维持了普通数组的特性,能够感知实际的数组大小,从这个角度看,sizeof也沾上了函数色彩

jack.zh 标签:C 继续阅读

851 ℉

15.04.17

Docker基础技术:Linux CGroup

转自 coolshell.cn 原作者 陈皓

前面,我们介绍了Linux Namespace,但是Namespace解决的问题主要是环境隔离的问题,这只是虚拟化中最最基础的一步,我们还需要解决对计算机资源使用上的隔离。也就是说,虽然你通过Namespace把我Jail到一个特定的环境中去了,但是我在其中的进程使用用CPU、内存、磁盘等这些计算资源其实还是可以随心所欲的。所以,我们希望对进程进行资源利用上的限制或控制。这就是Linux CGroup出来了的原因。

Linux CGroup全称Linux Control Group, 是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)。这个项目最早是由Google的工程师在2006年发起(主要是Paul Menage和Rohit Seth),最早的名称为进程容器(process containers)。在2007年时,因为在Linux内核中,容器(container)这个名词太过广泛,为避免混乱,被重命名为cgroup,并且被合并到2.6.24版的内核中去。然后,其它开始了他的发展。

Linux CGroupCgroup 可​​​让​​​您​​​为​​​系​​​统​​​中​​​所​​​运​​​行​​​任​​​务​​​(进​​​程​​​)的​​​用​​​户​​​定​​​义​​​组​​​群​​​分​​​配​​​资​​​源​​​ — 比​​​如​​​ CPU 时​​​间​​​、​​​系​​​统​​​内​​​存​​​、​​​网​​​络​​​带​​​宽​​​或​​​者​​​这​​​些​​​资​​​源​​​的​​​组​​​合​​​。​​​您​​​可​​​以​​​监​​​控​​​您​​​配​​​置​​​的​​​ cgroup,拒​​​绝​​​ cgroup 访​​​问​​​某​​​些​​​资​​​源​​​,甚​​​至​​​在​​​运​​​行​​​的​​​系​​​统​​​中​​​动​​​态​​​配​​​置​​​您​​​的​​​ cgroup。

主要提供了如下功能:

  • Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
  • Prioritization: 优先级控制,比如:CPU利用和磁盘IO吞吐。
  • Accounting: 一些审计或一些统计,主要目的是为了计费。
  • Control: 挂起进程,恢复执行进程。

使​​​用​​​ cgroup,系​​​统​​​管​​​理​​​员​​​可​​​更​​​具​​​体​​​地​​​控​​​制​​​对​​​系​​​统​​​资​​​源​​​的​​​分​​​配​​​、​​​优​​​先​​​顺​​​序​​​、​​​拒​​​绝​​​、​​​管​​​理​​​和​​​监​​​控​​​。​​​可​​​更​​​好​​​地​​​根​​​据​​​任​​​务​​​和​​​用​​​户​​​分​​​配​​​硬​​​件​​​资​​​源​​​,提​​​高​​​总​​​体​​​效​​​率​​​。

在实践中,系统管理员一般会利用CGroup做下面这些事(有点像为某个虚拟机分配资源似的):

  • 隔离一个进程集合(比如:nginx的所有进程),并限制他们所消费的资源,比如绑定CPU的核。
  • 为这组进程 分配其足够使用的内存
  • 为这组进程分配相应的网络带宽和磁盘存储限制
  • 限制访问某些设备(通过设置设备的白名单)

那么CGroup是怎么干的呢?我们先来点感性认识吧。

首先,Linux把CGroup这个事实现成了一个file system,你可以mount。在我的Ubuntu 14.04下,你输入以下命令你就可以看到cgroup已为你mount好了。

hchen@ubuntu:~$ mount -t cgroup
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,relatime,blkio)
cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,net_prio)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,relatime,hugetlb)

或者使用lssubsys命令:

$ lssubsys  -m
cpuset /sys/fs/cgroup/cpuset
cpu /sys/fs/cgroup/cpu
cpuacct /sys/fs/cgroup/cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
blkio /sys/fs/cgroup/blkio
net_cls /sys/fs/cgroup/net_cls
net_prio /sys/fs/cgroup/net_prio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb

我们可以看到,在/sys/fs下有一个cgroup的上上录,这个目录下还有很多子目录,比如: cpu,cpuset,memory,blkio……这些,这些都是cgroup的子系统。分别用于干不同的事的。

如果你没有看到上述的目录,你可以自己mount,下面给了一个示例:

mkdir cgroup
mount -t tmpfs cgroup_root ./cgroup
mkdir cgroup/cpuset
mount -t cgroup -ocpuset cpuset ./cgroup/cpuset/
mkdir cgroup/cpu
mount -t cgroup -ocpu cpu ./cgroup/cpu/
mkdir cgroup/memory
mount -t cgroup -omemory memory ./cgroup/memory/

一旦mount成功,你就会看到这些目录下就有好文件了,比如,如下所示的cpu和cpuset的子系统:

hchen@ubuntu:~$ ls /sys/fs/cgroup/cpu /sys/fs/cgroup/cpuset/
/sys/fs/cgroup/cpu:
cgroup.clone_children  cgroup.sane_behavior  cpu.shares         release_agent
cgroup.event_control   cpu.cfs_period_us     cpu.stat           tasks
cgroup.procs           cpu.cfs_quota_us      notify_on_release  user

/sys/fs/cgroup/cpuset/:
cgroup.clone_children  cpuset.mem_hardwall             cpuset.sched_load_balance
cgroup.event_control   cpuset.memory_migrate           cpuset.sched_relax_domain_level
cgroup.procs           cpuset.memory_pressure          notify_on_release
cgroup.sane_behavior   cpuset.memory_pressure_enabled  release_agent
cpuset.cpu_exclusive   cpuset.memory_spread_page       tasks
cpuset.cpus            cpuset.memory_spread_slab       user
cpuset.mem_exclusive   cpuset.mems

你可以到/sys/fs/cgroup的各个子目录下去make个dir,你会发现,一旦你创建了一个子目录,这个子目录里又有很多文件了。

hchen@ubuntu:/sys/fs/cgroup/cpu$ sudo mkdir haoel
[sudo] password for hchen: 
hchen@ubuntu:/sys/fs/cgroup/cpu$ ls ./haoel
cgroup.clone_children  cgroup.procs       cpu.cfs_quota_us  cpu.stat           tasks
cgroup.event_control   cpu.cfs_period_us  cpu.shares        notify_on_release

好了,我们来看几个示例。

CPU 限制

假设,我们有一个非常吃CPU的程序,叫deadloop,其源码如下: deadloop.c

int main(void)
{
    int i = 0;
    for(;;) i++;
    return 0;
}

用sudo执行起来后,毫无疑问,CPU被干到了100%(下面是top命令的输出)

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND     
3529 root      20   0    4196    736    656 R 99.6  0.1   0:23.13 deadloop

然后,我们这前不是在/sys/fs/cgroup/cpu下创建了一个haoel的group。我们先设置一下这个group的cpu利用的限制:

hchen@ubuntu:~# cat /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us 
-1
root@ubuntu:~# echo 20000 > /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us

我们看到,这个进程的PID是3529,我们把这个进程加到这个cgroup中:

# echo 3529 >> /sys/fs/cgroup/cpu/haoel/tasks

然后,就会在top中看到CPU的利用立马下降成20%了。(前面我们设置的20000就是20%的意思)

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND     
3529 root      20   0    4196    736    656 R 19.9  0.1   8:06.11 deadloop

下面的代码是一个线程的示例:

#define _GNU_SOURCE         /* See feature_test_macros(7) */

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>


const int NUM_THREADS = 5;

void *thread_main(void *threadid)
{
    /* 把自己加入cgroup中(syscall(SYS_gettid)为得到线程的系统tid) */
    char cmd[128];
    sprintf(cmd, "echo %ld >> /sys/fs/cgroup/cpu/foo/tasks", syscall(SYS_gettid));
    system(cmd); 
    sprintf(cmd, "echo %ld >> /sys/fs/cgroup/cpuset/foo/tasks", syscall(SYS_gettid));
    system(cmd);

    long tid;
    tid = (long)threadid;
    printf("Hello World! It's me, thread #%ld, pid #%ld!\n", tid, syscall(SYS_gettid));

    int a=0; 
    while(1) {
        a++;
    }
    pthread_exit(NULL);
}
int main (int argc, char *argv[])
{
    int num_threads;
    if (argc > 1){
        num_threads = atoi(argv[1]);
    }
    if (num_threads<=0 || num_threads>=100){
        num_threads = NUM_THREADS;
    }

    /* 设置CPU利用率为50% */
    mkdir("/sys/fs/cgroup/cpu/haoel", 755);
    system("echo 50000 > /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us");

    mkdir("/sys/fs/cgroup/cpuset/haoel", 755);
    /* 限制CPU只能使用#2核和#3核 */
    system("echo \"2,3\" > /sys/fs/cgroup/cpuset/haoel/cpuset.cpus");

    pthread_t* threads = (pthread_t*) malloc (sizeof(pthread_t)*num_threads);
    int rc;
    long t;
    for(t=0; t<num_threads; t++){
        printf("In main: creating thread %ld\n", t);
        rc = pthread_create(&threads[t], NULL, thread_main, (void *)t);
        if (rc){
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }

    /* Last thing that main() should do */
    pthread_exit(NULL);
    free(threads);
}

内存使用限制

我们再来看一个限制内存的例子(下面的代码是个死循环,其它不断的分配内存,每次512个字节,每次休息一秒):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    int size = 0;
    int chunk_size = 512;
    void *p = NULL;

    while(1) {

        if ((p = malloc(p, chunk_size)) == NULL) {
            printf("out of memory!!\n");
            break;
        }
        memset(p, 1, chunk_size);
        size += chunk_size;
        printf("[%d] - memory is allocated [%8d] bytes \n", getpid(), size);
        sleep(1);
    }
    return 0;
}

然后,在我们另外一边:

# 创建memory cgroup
$ mkdir /sys/fs/cgroup/memory/haoel
$ echo 64k > /sys/fs/cgroup/memory/haoel/memory.limit_in_bytes

# 把上面的进程的pid加入这个cgroup
$ echo [pid] > /sys/fs/cgroup/memory/haoel/tasks

你会看到,一会上面的进程就会因为内存问题被kill掉了。

磁盘I/O限制

我们先看一下我们的硬盘IO,我们的模拟命令如下:(从/dev/sda1上读入数据,输出到/dev/null上)

sudo dd if=/dev/sda1 of=/dev/null

我们通过iotop命令我们可以看到相关的IO速度是55MB/s(虚拟机内):

TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND          
8128 be/4 root       55.74 M/s    0.00 B/s  0.00 % 85.65 % dd if=/de~=/dev/null...

然后,我们先创建一个blkio(块设备IO)的cgroup

mkdir /sys/fs/cgroup/blkio/haoel

并把读IO限制到1MB/s,并把前面那个dd命令的pid放进去(注:8:0 是设备号,你可以通过ls -l /dev/sda1获得):

root@ubuntu:~# echo '8:0 1048576'  > /sys/fs/cgroup/blkio/haoel/blkio.throttle.read_bps_device 
root@ubuntu:~# echo 8128 > /sys/fs/cgroup/blkio/haoel/tasks

再用iotop命令,你马上就能看到读速度被限制到了1MB/s左右。

TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND          
8128 be/4 root      973.20 K/s    0.00 B/s  0.00 % 94.41 % dd if=/de~=/dev/null...

CGroup的子系统

好了,有了以上的感性认识我们来,我们来看看control group有哪些子系统:

  • blkio — 这​​​个​​​子​​​系​​​统​​​为​​​块​​​设​​​备​​​设​​​定​​​输​​​入​​​/输​​​出​​​限​​​制​​​,比​​​如​​​物​​​理​​​设​​​备​​​(磁​​​盘​​​,固​​​态​​​硬​​​盘​​​,USB 等​​​等​​​)。
  • cpu — 这​​​个​​​子​​​系​​​统​​​使​​​用​​​调​​​度​​​程​​​序​​​提​​​供​​​对​​​ CPU 的​​​ cgroup 任​​​务​​​访​​​问​​​。​​​
  • cpuacct — 这​​​个​​​子​​​系​​​统​​​自​​​动​​​生​​​成​​​ cgroup 中​​​任​​​务​​​所​​​使​​​用​​​的​​​ CPU 报​​​告​​​。​​​
  • cpuset — 这​​​个​​​子​​​系​​​统​​​为​​​ cgroup 中​​​的​​​任​​​务​​​分​​​配​​​独​​​立​​​ CPU(在​​​多​​​核​​​系​​​统​​​)和​​​内​​​存​​​节​​​点​​​。​​​
  • devices — 这​​​个​​​子​​​系​​​统​​​可​​​允​​​许​​​或​​​者​​​拒​​​绝​​​ cgroup 中​​​的​​​任​​​务​​​访​​​问​​​设​​​备​​​。​​​
  • freezer — 这​​​个​​​子​​​系​​​统​​​挂​​​起​​​或​​​者​​​恢​​​复​​​ cgroup 中​​​的​​​任​​​务​​​。​​​
  • memory — 这​​​个​​​子​​​系​​​统​​​设​​​定​​​ cgroup 中​​​任​​​务​​​使​​​用​​​的​​​内​​​存​​​限​​​制​​​,并​​​自​​​动​​​生​​​成​​​​​内​​​存​​​资​​​源使用​​​报​​​告​​​。​​​
  • net_cls — 这​​​个​​​子​​​系​​​统​​​使​​​用​​​等​​​级​​​识​​​别​​​符​​​(classid)标​​​记​​​网​​​络​​​数​​​据​​​包​​​,可​​​允​​​许​​​ Linux 流​​​量​​​控​​​制​​​程​​​序​​​(tc)识​​​别​​​从​​​具​​​体​​​ cgroup 中​​​生​​​成​​​的​​​数​​​据​​​包​​​。​​​
  • net_prio — 这个子系统用来设计网络流量的优先级
  • hugetlb — 这个子系统主要针对于HugeTLB系统进行限制,这是一个大页文件系统。

    ​​​

注意,你可能在Ubuntu 14.04下看不到net_cls和net_prio这两个cgroup,你需要手动mount一下:

$ sudo modprobe cls_cgroup
$ sudo mkdir /sys/fs/cgroup/net_cls
$ sudo mount -t cgroup -o net_cls none /sys/fs/cgroup/net_cls

$ sudo modprobe netprio_cgroup
$ sudo mkdir /sys/fs/cgroup/net_prio
$ sudo mount -t cgroup -o net_prio none /sys/fs/cgroup/net_prio

关于各个子系统的参数细节,以及更多的Linux CGroup的文档,你可以看看下面的文档:

CGroup的术语

CGroup有下述术语:

  • 任务(Tasks):就是系统的一个进程。
  • 控制组(Control Group):一组按照某种标准划分的进程,比如官方文档中的Professor和Student,或是WWW和System之类的,其表示了某进程组。Cgroups中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组。而资源的限制是定义在这个组上,就像上面示例中我用的haoel一样。简单点说,cgroup的呈现就是一个目录带一系列的可配置文件。
  • 层级(Hierarchy):控制组可以组织成hierarchical的形式,既一颗控制组的树(目录结构)。控制组树上的子节点继承父结点的属性。简单点说,hierarchy就是在一个或多个子系统上的cgroups目录树。
  • 子系统(Subsystem):一个子系统就是一个资源控制器,比如CPU子系统就是控制CPU时间分配的一个控制器。子系统必须附加到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。Cgroup的子系统可以有很多,也在不断增加中。

下一代的CGroup

上面,我们可以看到,CGroup的一些常用方法和相关的术语。一般来说,这样的设计在一般情况下还是没什么问题的,除了操作上的用户体验不是很好,但基本满足我们的一般需求了。

不过,对此,有个叫Tejun Heo的同学非常不爽,他在Linux社区里对cgroup吐了一把槽,还引发了内核组的各种讨论。

对于Tejun Heo同学来说,cgroup设计的相当糟糕。他给出了些例子,大意就是说,如果有多种层级关系,也就是说有多种对进程的分类方式,比如,我们可以按用户来分,分成Professor和Student,同时,也有按应用类似来分的,比如WWW和NFS等。那么,当一个进程即是Professor的,也是WWW的,那么就会出现多层级正交的情况,从而出现对进程上管理的混乱。另外,一个case是,如果有一个层级A绑定cpu,而层级B绑定memory,还有一个层级C绑定cputset,而有一些进程有的需要AB,有的需要AC,有的需要ABC,管理起来就相当不易。

层级操作起来比较麻烦,而且如果层级变多,更不易于操作和管理,虽然那种方式很好实现,但是在使用上有很多的复杂度。你可以想像一个图书馆的图书分类问题,你可以有各种不同的分类,分类和图书就是一种多对多的关系。

所以,在Kernel 3.16后,引入了unified hierarchy的新的设计,这个东西引入了一个叫__DEVEL__sane_behavior的特性(这个名字很明显意味目前还在开发试验阶段),它可以把所有子系统都挂载到根层级下,只有叶子节点可以存在tasks,非叶子节点只进行资源控制。

我们mount一下看看:

$ sudo mount -t cgroup -o __DEVEL__sane_behavior cgroup ./cgroup

$ ls ./cgroup
cgroup.controllers  cgroup.procs  cgroup.sane_behavior  cgroup.subtree_control 

$ cat ./cgroup/cgroup.controllers
cpuset cpu cpuacct memory devices freezer net_cls blkio perf_event net_prio hugetlb

我们可以看到有四个文件,然后,你在这里mkdir一个子目录,里面也会有这四个文件。上级的cgroup.subtree_control控制下级的cgroup.controllers。

举个例子:假设我们有以下的目录结构,b代表blkio,m代码memory,其中,A是root,包括所有的子系统()。

# A(b,m) - B(b,m) - C (b)
#               \ - D (b) - E

# 下面的命令中, +表示enable, -表示disable

# 在B上的enable blkio
# echo +blkio > A/cgroup.subtree_control

# 在C和D上enable blkio 
# echo +blkio > A/B/cgroup.subtree_control

# 在B上enable memory  
# echo +memory > A/cgroup.subtree_control

在上述的结构中,

  • cgroup只有上线控制下级,无法传递到下下级。所以,C和D中没有memory的限制,E中没有blkio和memory的限制。而本层的cgroup.controllers文件是个只读的,其中的内容就看上级的subtree_control里有什么了。
  • 任何被配置过subtree_control的目录都不能绑定进程,根结点除外。所以,A,C,D,E可以绑上进程,但是B不行。

我们可以看到,这种方式干净的区分开了两个事,一个是进程的分组,一个是对分组的资源控制(以前这两个事完全混在一起),在目录继承上增加了些限制,这样可以避免一些模棱两可的情况。

当然,这个事还在演化中,cgroup的这些问题这个事目前由cgroup的吐槽人Tejun Heo和华为的Li Zefan同学负责解决中。总之,这是一个系统管理上的问题,而且改变会影响很多东西,但一旦方案确定,老的cgroup方式将一去不复返。

参考

jack.zh 标签:docker 继续阅读

1 2 3 4 5 6 7 8 9 10
Fork me on GitHub