关于并行:数组索引的时间复杂度-O1-的本质是并行二分查找

上图是寻址逻辑电路,输出端 A、B 独特组成 2 bit 的地址线,2 bit 的地址线能够示意 00、01、10、11 这 4 个地址,它们别离位于输入端 Z、Y、X、W,通过地址线示意的二进制数就能够找到输入端中的不同地址(当前就能够对其进行读写操作了) 也能够这样了解:输出端 A、B 相当于两个开关,输入端 Z、Y、X、W 相当于 4 个灯泡,两个开关的不同状态的组合就能够管制其中 1 个灯泡中的亮灭。接下来剖析繁多输出端:当输出端A 为 1 时,会选出输入端X、输入端W; 当输出端A 为 0 时(通过非门会变成 1),会选出输入端Z、输入端Y; 论断:不论输出端A 是 0 还是 1,都会选出一半当输出端B 为 1 时,会选出输入端W、输入端Y; 当输出端B 为 0 时(通过非门会变成 1),会选出输入端X、输入端Z; 论断:不论输出端 B 是 0 还是 1,都会选出一半因为只有当与门同时为 1 时,输入端才会输入 1。 当初,当输出端A和输出端B别离为 0、1 时,输入端Y就是输出端A、B 独特选出的地址。 上图的红线为 1(输出端A 的 0 通过非门会将其之后的线路置为 1)那它的工夫复杂度是怎么样的呢? 因为输出端A、输出端B(比方地址 01)是同时输出的,所以电路会进行并行的二分查找,一个输出端的一次二分查找是 O(1),所有的输出端并行,进行一次二分查找同样是 O(1)。 以上是 2 bit 寻址,更多 bit 的寻址同样是并行进行的,所以工夫复杂度同样是 O(1)。 ...

November 27, 2022 · 1 min · jiezi

关于并行:关于并发和并行Go和Erlang之父都弄错了

作者|Yossi.Kreinin起源|OSChina网站翻译|Andy、袁不语、YuanyuanL、姜鹏飞校对|胡燕君(OneFlow) 依据字面词义,并发(concurrent)是指竞争或反抗,而并行(parallelism)指两条直线永不相交的状态。在计算机中的并行和并发问题上,我与Joe Armstrong(译注:Erlang语言发明者)和Rob Pike(译注:Go语言发明者)这俩人的认识并不统一。 上面我以自动售货机和分礼物为例来阐明我的观点。 首先,并行和并发都是十分风行的概念,很多编程语言和工具通常都会重点宣传本人“兼具高度并行性和高度并发性”。但我却认为,并行和并发须要的是不同的工具,而对单个工具来说,并行和并发不可兼得。例如: Erlang、Rust、Go和STM Haskell善于并发解决Flow、Cilk、checkedthreads和Haskell偏重并行处理也有像Haskell这样的语言或零碎,既能解决并行,也能解决并发,但在Haskell外部,并行和并发分属两套不同的工具集。而且Haskell的官网wiki(http://www.haskell.org/haskel...)也解释了两者的应用准则,如果谋求并行,那么不应该应用并发工具: 经验总结:优先应用纯并行,而后再思考并发。Haskell早就意识到一个工具难以同时解决这两个问题。专门针对并行环境设计的新编程语言ParaSail,也面临同样的难处,尽管它也兼备针对并行和并发的工具集,但它也倡议在并行、非并发程序中尽量不要应用并发性能。 这就与很多人的观点天壤之别了,尤其是一些并发性能的铁粉,他们认为本人的并发工具解决起并行任务时也能熟能生巧。Rob Pike就说,因为Go是一种并发性语言,所以它也很适宜并行(http://talks.golang.org/2012/...)。 并发使得并行变得更容易(进步可扩展性等)。同样,Joe Armstrong也是这么说Erlang语言的。他甚至还说:当初还想用非并发的思维去解决并行的问题是缘木求鱼。 到底是什么起因导致这种一致,为什么Haskell和ParaSail的开发者认为并发和并行要用不同的工具,而Go和Erlang的开发者却认为并行语言也能解决并发呢? 我感觉这是因为大家要解决的问题不同,对并行和并发的区别有不同了解,才导致最终论断不同。Joe Armstrong已经画过一张图来解释两者的区别,经自己的灵魂画手还原如下: 尽管并发在很多时候只波及繁多队列,但我还是依照Joe Armstrong的原图画了2个队列。从图中咱们能够看出: “并行”意味着可乐散发速度更快;从部分来看,“并行”实质上还是并发问题 ;先排队者先失去可乐;谁先拿到可乐都没关系,反正最终每个人都会拿到;当然,如果自动售货机里的可乐中途卖完了,前面的人就会拿不到——那也是没有方法,人生就是这样。以上是用自动售货机举例,如果换一个比喻,假如是给一群孩子散发礼物呢,这两种类比之间有实质性区别吗? 有区别。自动售货机是一个事件处理零碎:人们不定时地到来,并且从机器中取不定数量的可乐。而分礼物是一个计算零碎:你晓得每个孩子想要什么,你晓得你买了什么礼物,并且你要决定哪个礼物分给哪个孩子。 在分礼物的场景中,“并发”与“并行”有不同的意义: 从下面的例子中,能够看出“并行”和“并发”有如下区别: 和自动售货机一样,“并发”也遵循“先到先得”的规定;“并行”则不一样:礼物已有标签,每个孩子拿到什么都是确定的;“并发”必须靠队列来维持秩序,防止两个孩子同时抢一个礼物——就像零碎要避免多个工作争先访问共享的数据,防止零碎瘫痪一样;“并行”模式下,就不须要队列了,不论谁先谁后,每个人都会拿到属于本人的礼物。在俄语中,“concurrent(并发,读作kon-koo-rent)”和“competitor(竞争对手)”是同义词。这种含意正好符合了分礼物中的“并发”情景的竞争属性:每个人都心愿本人最早到,这样就能挑到最好的礼物。 但“并行”场景则不同,每个礼物盒都写上了对应小朋友的名字。在逻辑上,孩子和礼物之间一一对应,互不穿插,互不烦扰。(但为什么我在图中画的连线却相交呢?一会儿我会解释这个问题。咱们能够先这么想:每个孩子奔向本人的礼物时路线会有相交,就像不同处理器寻找本人想要的数据时门路会相撞,但这并不影响到谁最终拿到什么礼物。) 1 计算和事件处理咱们能够联想电话客服、Web服务器、银行柜台等利用场景,它们和自动售货机是相似的事件处理零碎,都要解决并发工作,都必须解决不可预知的申请带来的不可避免的抵触。并行设计能够在肯定水平上进步处理速度,但其问题的本源还是在于并发中的抵触。 而在计算零碎中,例如分礼物、图形处理、计算机视觉、科学计算等中,则不会呈现并发抵触。输出是确定的,只需依据输出进行计算,而后输入,不会有意外烦扰。这时,并行的办法能够进步计算速度,但也容易呈现bug。 接下来咱们将持续探讨计算零碎和事件处理零碎的差异。 2 确定性:现实VS妄想在计算零碎中,咱们须要更多的确定性,因为这能够使编程人员轻松很多----如果程序的输入后果是可确定的,那咱们就能够释怀地去测试优化或者重构代码,只有输入后果没变,就阐明咱们没做错。 而且这种确定性要求的只是一种大略的确定,并不需要100%雷同----例如,在计算机视觉训练中,你并不要求机器每次找到的100张图片都是同一组,只有下面都有小猫就行;你也不要求机器每次都求出截然不同的圆周率近似值,只有它是在3和4之间就能够了。 在计算零碎中,领有确定性是现实状况,而且是能够达到的。 然而在事件处理零碎中,一切都是不确定的。如果事件产生程序不同,产生的后果也不同。譬如,如果我比你来得早,那罐仅剩的可乐就是我的;如果咱俩的银行联名账户只剩一块钱,而我的取款操作比你晚了0.001秒,钱就会打给你。 但如果再给我一次机会,那么钱属谁手就不肯定了。这就是事件处理零碎的不确定性。同样的程序运行屡次,后果能够背道而驰。而所有可能的后果数将随抵触的数量指数式增长。还有比这更让人头疼的事吗? 3 如何判断并行平安:确定性VS正确性当程序以不同的程序处理事件时,你怎么确定其中没有bug? 在计算零碎中,只有你每次运行程序都能失去雷同的后果,即便是谬误的后果,你也能够根本判定程序是并行平安的。就像运行一个搜寻小猫的程序,你每次都搜出了同一种类的小狗,那阐明你的程序有问题,然而不存在并行方面的问题。 然而在事件处理零碎中,状况就简单了一些,只有当零碎每次都能失去正确的后果,你能力确定零碎是并行平安的。 比方程序要模仿两个人同时取钱,不可能要求每次都是同一个人取到最初的一块钱,那如何判断这个程序没有bug?咱们只能从侧面来看,例如账户有没有呈现的负余额,会不会间断两次取空一个账户,会不会凭空减少账户金额等等。如果上述这些异常情况都没有产生,你就能够认为你的模拟程序没有问题。 对电话服务来说也是这样,你须要先定义出一系列“正确的后果”,而后用这些“正确的后果”验证你的电话服务是否失常。 但很遗憾,如果一个零碎的后果不能体现时序相干的问题,那你就无奈晓得零碎的时序有没有bug。这也是个头疼的问题。 4 并行谬误:易定位VS难定义对于标好名字的礼物来说,如果呈现并行谬误,是很容易发现的--即便礼物上的名字是用外语写的,孩子看不懂,这都没关系,为什么?请看下图: 所以对并行零碎来说,零碎不意识这些“标记”没关系,只有有标记就行。如果发现两个孩子抢夺同一份礼物,就能够找意识标记的人来解决抵触。 这就如同一个自动化的测试工具,不须要晓得哪个工作别离拜访什么数据,只有晓得一个工作不能拜访被别的工作批改过的数据就行了。如果产生了这种抵触拜访,那就当做bug报告给程序员来解决。 这样的工具当初曾经有很多了,Cilk语言有,Checkedthreads也带有一个基于Valgrind的相似工具。(注:Checkedthreads,一个C++并行框架https://github.com/yosefk/che...;Valgrind,一套反对动态分析的工具http://valgrind.org) 但Haskell不必这么干,因为首先它不存在副作用(side effect),这就保障了并行处理不会呈现抵触。但即使没有这种动态保障,Haskell的动静冲突检测机制也能防止抵触。 造成的意外后果就是,在计算零碎中,你不须要精确标记出bug所在,也不须要指出bug导致的问题是什么。 而在事件处理零碎中,必须有一个常识丰盛、责任感强的“小孩儿”来掌控全局,维持秩序。 此外,事件处理零碎还隐含了一个通用的规定:他人正在解决的过程,你就不能插手了,必须乖乖地在队列里等待。在“分礼物”场景中,“小孩儿”的存在能够保障这条规定失去恪守,让分礼物的工作更顺利,但还是难以避免其余可能的抵触。 在上图中,小孩A中途来到,回来后没有通过排队就接触礼物,违反规定,于是被零碎检测为bug,小孩儿呈现,重整秩序。 但纵观全程,小孩A先拆开了礼物,但又中途来到队列,回来后发现礼物被前面的小孩B领了——这种后果合乎零碎的要求吗?一方面,小孩B没有插队,也没有阻挡小孩A领礼物,并未违反零碎规定;但另一方面,小孩B领到本属于小孩A的礼物,这并不合乎零碎的预设。 上述情况体现在具体代码中是这样的:以下是一个谬误的汇款操作,能够轻易被主动调试工具检测出“有bug”: src.balance -= amountdst.balance += amount此处没有任何同步操作。src.balance可能被两个过程同时批改,导致其中的一次批改是有效的。好在,Helgrind等数据争用检测工具能够通过监控内存拜访发现这样的同步问题,Cilk和checkedthreads外部也有类似的工具。 但下列代码中的bug,上述工具可能就检测不进去了: atomic { src.balance -= amount } atomic { dst.balance += amount }下面代码中的“atomic”示意原子操作,意思是对账户余额的批改必须顺次进行。这样一来,检测工具就能够少操些心了——但这样就意味着没有bug了? ...

June 8, 2022 · 1 min · jiezi

关于并行:并行排序

归并排序归并排序能够简略得扩大为并行模式,每个合并操作互相独立,因而咱们以归并排序为例实现一下多线程排序。 自下而上归并排序根本示意图如下,假如 N % 2 == 0,归并排序共 log2(N) 层,共 n-1 次合并操作 代码如下,自底向上逐层合并, grpSize 为以后层的块大小,相邻块进行合并。repo1 和 repo2 别离指向输出数据和长期空间,每次合并之后进行替换secondGrpSize 确保了遇到数组开端时,第二个块大小的正确性secondGrpSize == 0 第二个块大小为零时,无需排序,间接拷贝输出数据到新空间即可mergeList 参数为: 块1起始地位,块2起始地位,块1长度,块2长度,指标数组template <class T> void mergesort(T *data, int N) { T *temp = new T[N]; T *repo1, *repo2, *aux; repo1 = data; repo2 = temp; for (int grpSize = 1; grpSize < N; grpSize <<= 1) {#pragma omp parallel for for (int stIdx = 0; stIdx < N; stIdx += 2 * grpSize) { int nextIdx = stIdx + grpSize; int secondGrpSize = min(max(0, N - nextIdx), grpSize); if (secondGrpSize == 0) { for (int i = 0; i < N - stIdx; i++) repo2[stIdx + i] = repo1[stIdx + i]; } else mergeList(repo1 + stIdx, repo1 + nextIdx, grpSize, secondGrpSize, repo2 + stIdx); } aux = repo1; repo1 = repo2; repo2 = aux; } if (repo1 != data) memcpy(data, temp, sizeof(T) * N); delete[] temp;}template <class T>void mergeList(T *src1, T *src2, int len1, int len2, T *dest) { int idx1 = 0, idx2 = 0; int loc = 0; while (idx1 < len1 && idx2 < len2) { if (src1[idx1] <= src2[idx2]) { dest[loc] = src1[idx1]; idx1++; } else { dest[loc] = src2[idx2]; idx2++; } loc++; } for (int i = idx1; i < len1; i++) dest[loc++] = src1[i]; for (int i = idx2; i < len2; i++) dest[loc++] = src2[i];}测试:16 线程下 ...

August 26, 2021 · 2 min · jiezi

填坑之PHP的yield和协程在一起的日子里(二)

首先是,这是我第一次把公众号文章复制粘贴到sf.gg来。其次是,很久很久之前,我挖了一个yield的一个坑,自己挖的坑自己填,不然迟早会把自己埋掉。最后是,如果想看之前那个坑,请发送“yield”给文章末尾的公众号,我开通了高大上的自动回复功能,稀罕地不得了!PS:那篇文章中在最后我犯了一个错误,误下了一个结论:foreach中不能使用send并猜测这是PHP的bug,实际上并不是,真实的原因粗暴简单的理解就是send会让生成器继续执行一次导致。这件事情告诉我们:除了装逼之外,甩锅也是有打脸风险的那篇坑里,内容和你能在百毒上搜索到的大多数文章都是差不多的,不过我那篇坑标题起得好:《yield是个什么玩意(上)》,也就是暗示大家还有下篇,所以起标题也是需要一定技术含量的。我坚信,在座的各位辣鸡在看完上篇坑文后最想说的注定是泰迪熊这句话(这是文化属性,不以各位的意志而转移):回到今天主旨上来,强调几点:虽然文章标题中有“yield和协程”这样的关键字,但实际上yield并不是协程,看起来有不少人直接将yield和协程划了等号。yield的本质是生成器,英文名字叫做Generator。yield只能用在function中,但用了yield就已经不是传统意义上的function了,同时如果你企图在function之外的其他地方用yield,你会被打脸。yield的最重要作用就是:自己中断一坨代码的执行,然后主动让出CPU控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。这个就比较屌了,假如你请求了一个费时10s的服务器API,此时是可以让出CPU给路人甲。粗暴地说上面的过程就算是协程的基本概念。多线程和多进程都是操作系统参与的调度,而协程是用户自主实现的调度,协程的关键点实际上是“用户层实现自主调度”,大概有“翻身农奴把歌唱”的意思。下面我通过一坨代码来体会一把“翻身农奴”,你们感受一下:<?phpfunction gen1() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN1 : {$i}".PHP_EOL; // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么 // 就是那种“你看!你看!尼玛,我调度了!卧槽” sleep( 1 ); // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱 yield; }}function gen2() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN2 : {$i}".PHP_EOL; // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么 // 就是那种“你看!你看!尼玛,我调度了!卧槽” sleep( 1 ); // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱 yield; }}$task1 = gen1();$task2 = gen2();while( true ) { // 首先我运行task1,然后task1主动下了地狱 echo $task1->current(); // 这会儿我可以让task2介入进来了 echo $task2->current(); // task1恢复中断 $task1->next(); // task2恢复中断 $task2->next();}上面代码执行结果如下图:虽然我话都说到这里了,但是肯定还是有人get不到“所以,到底发生了什么?”。你要知道,如果function gen1和function gen2中没有yield,而是普通函数,你是无法中断其中的for循环的,诸如下面这样的代码:<?phpfunction gen1() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN1 : {$i}".PHP_EOL; sleep( 1 ); }}function gen2() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN2 : {$i}".PHP_EOL; }}gen1();gen2();// 看这里,看这里,看这里!// 上面的代码一旦运行,一定是先运行完gen1函数中的for循环// 其次才能运行完gen2函数中的for循环,绝对不会出现// gen1和gen2交叉运行这种情况我似乎已然精通了yield写到这里后我也开始蹩了,和以往的憋了三天蹦不出来个屁有所不同,我这次蹩出了一个比较典型的应用场景:curl。下面我们基于上面那坨辣鸡代码将gen1修改为一个耗时curl网络请求,gen2将向一个文本文件中写内容,我们的目的就是在耗时的curl开始后主动让出CPU,让gen2去写文件,以实现CPU的最大化利用。<?php$ch1 = curl_init();// 这个地址中的php,我故意sleep了5秒钟,然后输出一坨jsoncurl_setopt( $ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1" );curl_setopt( $ch1, CURLOPT_HEADER, 0 );$mh = curl_multi_init();curl_multi_add_handle( $mh, $ch1 );function gen1( $mh, $ch1 ) { do { $mrc = curl_multi_exec( $mh, $running ); // 请求发出后,让出cpu yield; } while( $running > 0 ); $ret = curl_multi_getcontent( $ch1 ); echo $ret.PHP_EOL; return false;}function gen2() { for ( $i = 1; $i <= 10; $i++ ) { echo “gen2 : {$i}".PHP_EOL; file_put_contents( “./yield.log”, “gen2”.$i, FILE_APPEND ); yield; }}$gen1 = gen1( $mh, $ch1 );$gen2 = gen2();while( true ) { echo $gen1->current(); echo $gen2->current(); $gen1->next(); $gen2->next();}上面的代码,运行以后,我们再等待curl发起请求的5秒钟内,同时可以完成文件写入功能,如果换做平时的PHP程序,就只能是先阻塞等待curl拿到结果后才能完成文件写入。文章太长,就像“老太太的裹脚布一样,又臭又长”,所以,最后再对代码做个极小幅度的改动就收尾不写了!<?php$ch1 = curl_init();// 这个地址中的php,我故意sleep了5秒钟,然后输出一坨jsoncurl_setopt( $ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1" );curl_setopt( $ch1, CURLOPT_HEADER, 0 );$mh = curl_multi_init();curl_multi_add_handle( $mh, $ch1 );function gen1( $mh, $ch1 ) { do { $mrc = curl_multi_exec( $mh, $running ); // 请求发出后,让出cpu $rs = yield; echo “外部发送数据{$rs}".PHP_EOL; } while( $running > 0 ); $ret = curl_multi_getcontent( $ch1 ); echo $ret.PHP_EOL; return false;}function gen2() { for ( $i = 1; $i <= 10; $i++ ) { echo “gen2 : {$i}".PHP_EOL; file_put_contents( “./yield.log”, “gen2”.$i, FILE_APPEND ); $rs = yield; echo “外部发送数据{$rs}".PHP_EOL; }}$gen1 = gen1( $mh, $ch1 );$gen2 = gen2();while( true ) { echo $gen1->current(); echo $gen2->current(); $gen1->send(“gen1”); $gen2->send(“gen2”);}我们修改了内容:将$gen1->next()修改成了$gen1->send(“gen1”)在function gen1中yield有了返回值,并且将返回值打印出来这件事情告诉我们:yield和send,是可以双向通信的,同时告诉我们send可以用来恢复原来中断的代码,而且在恢复中断的同时可以携带信息回去。写到这里,你是不是觉得这玩意的可利用价值是不是比原来高点儿了?我知道,有人肯定叨叨了:“老李,你代码特么写的真是辣鸡啊!你之前保证过了的 — 只在公司生产环境写辣鸡代码的。可你看看你这辣鸡光环到笼罩都到demo里了,你连demo都不放过了!你怎么说?!”。兄dei,“又不是不能用”。而且我告诉你,上面这点儿curl demo来讲明白yield还是不够的,后面还有两三篇yield呢,照样是烂代码恶心死你,爱看不看。我劝你心放宽,你想想你这么烂的代码都经历了,还有什么不能经历的?文章最后补个小故事:其实yield是PHP 5.5就已经添加进来了,这个模块的作者叫做Nikita Popov,网络上的名称是Nikic。我们知道PHP7这一代主力是惠新宸,下一代PHP主力就是Nikic了。早在2012年,Nikic就发表了一篇关于PHP yield多任务的文章,链接我贴出来大家共赏一下 — http://nikic.github.io/2012/1…最近开了一个微信公众号,所有文章都在这里(手贱弄成服务号了) ...

March 11, 2019 · 2 min · jiezi

为何选用Vue做MVC架构模式

关键词 并行开发 代码复用 关注点分离经典的MVC架构模式MVC架构模式是经典设计模式中的经典,是一种编程的方法论。具有高度抽象的特征,经典MVC用简单的定义体现出解决复杂通用问题的办法,只有不断思考和体会才能用来解决不同情况下程序设计所遇到的问题。MVC不能脱离具体的框架而独自存在,把握抽象必须用具体;把握具体的内在结构和外在关系只能用抽象。在前端领域中有代表MVC的Anguler、代表MVVM的Vue、和代表单向数据流的React都是很棒的前端框架,分别代表了一种具体的设计模式应用,在不同的应用场景中使用对了框架才能把这些框架的特性充分的发挥出来,如果只是从字面上找对应,Anguler才是MVC的代表,Vue显然不是MVC的代表,为什么我要选用Vue来做MVC架构模式呢? 下面就来说一下为何这么做。背景我所在的部门业务场景上是分客群分产品的, 产品需求上迭代非常快,全场景一年要发布120多个版本,技术架构主要考量以下几点:产品稳定性多产品并行开发,支持4-10个产品线独立开发部署多产品公共功能可复用,保证开发效率提升用户体验技术架构本身的生命周期取决于业务的生命周期,好的架构模式应该是模块化的,可以针对其中的一个模块来进行优化升级,延长技术架构的生命周期就会节约很多成本。选择Vue1.足够的小巧,轻量,关注点聚焦,不会干扰整体项目设计架构;同时也有全家桶来做关注点分离,需要更多能力时全家桶中有可选择的项,减少重新造轮子。2.上手成本低、开发效率也很高3.Vue生态成熟和有庞大的开发者群体不选Angular的原因: 个人角度还是很喜欢Angular,但Angular也有一些问题, 如大版本发布太快和变化太大;大而全,这是优点也是缺点,缺点就是设计过重不利于每个团队根据实际情况基于Angular来重新设计架构,换掉其中的一部分设计,也很难让团队成员理解和适应。不选React的原因: 因Facebook的开源项目React嵌入了竞争防止协议,留下比较大的法务隐患,17年9月内部就抛弃了React框架,已经使用Ract的项目也要限定期限改为其他框架。即使后来FaceBook的开源协议有所改善,但这种事情还是让大家心留余悸,不能再提了。经典MVC回顾MVC全名是Model View Controller,是软件工程中的一种软件架构模式,把软件系统分为三个 基本部分:模型(Model)、视图(View)和控制器(Controller)。是一种软件设计典范,用一种业务逻辑和数据显式分离的方法组织代码,将业务逻辑聚集到一个部件里面,在界面和用户围绕数据的交互能被改进和个性化定制的同时而不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。Model(模型) 是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。模型表示企业数据和业务规则。在MVC的三个部件中,模型拥有最多的处理任务。模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。View(视图) 是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的。视图是用户看到并与之交互的界面。MVC好处是它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,不管这些数据是联机存储的还是一个雇员列表,作为视图来讲,它只是作为一种输出数据并允许用户操纵的方式。Controller(控制器) 是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。控制器接受用户的输入并调用模型和视图去完成用户的需求,所以当单击Web页面中的超链接和发送HTML表单时,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。MVVM架构模式MVVM模式是工程师在解决WPF应用程序开发复杂度时提出的解决方案,它实现了View和Model的自动同步,开发者不需要再手动的绑定输入监听和手动的将数据结果展示在view上,这就是双向数据绑定的优势,后来Backbone、Vue等都是MVVM模式的前端框架。ViewModel解决了View和Model之间转换的开发效率问题。 但是ViewModel内部的复杂度又变成了新的问题,其中一个问题就是双向数据绑定劣势。在双向数据绑定中,Model(可以理解为状态的集合) 中可以修改自己或其他Model的状态, 用户的操作(如在输入框中输入内容)也可以修改状态。这使的改变一个状态有可能会触发一连串的状态的变化,最后很难预测最终的状态是什么样的。使得代码变得很难调试。为了解决这个问题便有了后来的Vue 单向数据流的解决方案-Vuex。 在复杂度较高的业务上使用单向数据流来解耦View和Model的关系。可以看出经典MVC架构模式的重点是要解决业务逻辑复用和数据显式分离,前端MVC本质要解决的还是这两点,不同的是前端用组件化和MVVM分别来解决业务逻辑复用和数据显式分离,MVC、MVVM都要解决MV这两层的的问题。前后端分离后、前端使用单页应用就可以完成用户界面部分的管理,此时在前端架构中需要有一层来管理页面切换这就是前端MVC的Controller控制层。MVC和MVVM本质上这是两种架构模式,是为了解决不同场景遇到的开发问题。 两者解决的问题有相似之处,MVVM中的ViewModel和MVC中的Controller作用有些相似。用Vue框架的项目引入Vuex后ViewModel的作用被弱化,MVVM和单向数据流都无法反映整个框架的架构模式。同时整体架构还要解决单页应用等问题,更需要有一个准确的理论模型,从而选择MVC架构模式来做项目架构更适合实际情况。front MVC架构定义代码库结构图在业务层(不包括基础组件)和经典MVC想比Front MVC的核心由Model变成了View。 View层的设计是重点,基础组件尽量使用组件特性和MVVM、避免过度设计造成层级复杂度变大;业务组件可以用EventBus的方式来处理跨层级交互的问题。 Model层由Vuex来管理,Vuex适合数据量大并且函数集中处理,管理接口数据很合适;以便明确各种方式的最佳实践范围。Model(模型) 是应用程序中用于处理应用程序状态管理和数据交互的部分。主要由状态管理Vuex和数据交互axios实现。View (视图) 是应用程序中业务逻辑处理和内容展示的部分。不同于经典MVC中的View,Front MVC的View层业务逻辑会比较重,这是由于前端的复用主要体现在组件上,前端框架Vue的Global API一系列特性也非常好的支撑这种组件化业务需求。这里的View层主要有基础组件库、公共组件库、产品组件库组成,其中前两个库是可以复用的。Controller(控制器) 是应用程序中处理用户交互的部分。 不同于MVC中的Controller, Front MVC的Controller层是通过路由机制加载View主要是有viewRouter来实现,再有View根据交互逻辑来控制Model层的调用。在这里View是应用程序的核心部件。这样设计的特点:首先实现了单页应用,用户体验交回前端管理MVC各层是模块化的,可灵活调整和复用层级划分清晰,利于用最佳实践实现Model层数据可复用,跨页面公共数据根据情况可只调用一次View层可复用并行开发模式并行开发效率高,但产品多、开发人员多、发版频繁一定会造成冲突和相互影响,必须重点关注保证产品稳定性。控制好公共部分的改动频率和修改权限。解决方法:各产品库的node_module依赖项统一管理,兼容公共部分依赖项。业务公共库各产品负责人有修改权限,既保证公共部分协同开发,也保证公共部分稳定。基础组件库精心打磨,控制发版频次。只在产品库中编译项目,公共库使用git sub module的方式引入,各产品单独发布部署包。各产品单独部署。结论只要能解决问题,架构应该是越简单越好,就像经典MVC架构模式一样简洁,一样有力量。不断总结分析,寻求不同的解决方法,设计时整体考量,经常在抽象和具体之前切换角度思考,才能兼顾开发效率和技术支撑。本文是从项目整体设计上对MVC架构模式进行了一个实践总结,欢迎大家交流指正!

January 20, 2019 · 1 min · jiezi

Java 几种线程状态之间的相互关系

Java Thread 可能处在以下几种状态Java Doc 里通过一个枚举类型 Enum<Thread.State> 来定义。线程可以处于以下状态之一:NEW 尚未启动的线程处于此状态。RUNNABLE 在Java虚拟机中执行的线程处于此状态。BLOCKED 被阻塞等待监视器锁定的线程处于此状态。WAITING 无限期等待另一个线程执行特定操作的线程处于此状态。TIMED_WAITING 正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。TERMINATED 已退出的线程处于此状态。线程在给定时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。通过示例而不是Java doc中给出的正式定义,可以很容易地理解任何令人困惑的概念。如果它们是现实生活中的例子,它可能更具有相关性。我想分享一些可能有助于理解这些线程状态的现实例子。由http://fastthread.io生成的传递图,显示哪些线程阻塞了哪些线程BLOCKED 阻塞Java doc正式将BLOCKED状态定义为:“阻塞等待监视器锁的线程处于此状态。”现实生活中的例子:今天你要去面试。这是您梦寐以求的工作,这是您过去几年一直瞄准的目标。你早上醒来,准备好了,穿上你最好的衣服,在镜子前面看起来很敏锐。现在你走出你的车库,意识到你的妻子已经开车了。在这种情况下,你只有一辆车,那么会发生什么?在现实生活中,可能会发生斗争:-)。在这里你被阻止,因为你的妻子已经开车了。你将无法参加面试。这是BLOCKED状态。用技术术语解释它,你是线程T1,你的妻子是线程T2,锁是汽车。T1在锁(即汽车)上被阻挡,因为T2已经获得了这个锁。提示:线程在等待监视器锁进入同步块/方法或在调用Object#wait()方法后重新输入同步块/方法时,将进入BLOCKED状态。WAITING 等候Java doc正式将WAITING状态定义为:“无限期等待另一个线程执行特定操作的线程处于此状态。”现实生活中的例子:让我们说几分钟后你的妻子带着车回家了。现在你意识到面试的时间已经到了,而且到达那里还有很长的路要走。因此,您将所有动力都放在汽车的油门踏板上。当允许的速度限制仅为60英里/小时时,您以100英里/小时的速度行驶。你不走运,交通警察看到你超过限速,他把你拉到路边。现在你进入WAITING状态,我的朋友。你停止开车,坐在车里闲逛,直到警察调查你,然后让你走。基本上,在他让你离开之前,你会陷入等待状态。用技术术语解释它,你是线程T1,警察是线程T2。你释放锁(即你停止开车),然后进入等待状态。直到警察(即T2)让你离开,你将陷入这种等待状态。提示:线程在调用以下方法之一时将进入WAITING状态:Object#wait() 未指定超时时间Thread#join() 未指定超时时间LockSupport#park()在对象上调用Object.wait()的线程处于WAITING状态,直到另一个线程调用该对象上的Object.notify()或Object.notifyAll()。调用Thread.join()的线程处于WAITING状态,以使指定的线程终止。TIMED_WAITING 指定时间的等待Java doc正式将TIMED_WAITING状态定义为:“等待另一个线程在指定的等待时间内执行操作的线程处于此状态。”现实生活中的例子: 尽管所有的戏剧性,你在采访中表现得非常好,给每个人留下了深刻的印象并得到了这份高薪工作。(恭喜!)你回到家里,告诉你的邻居关于这份新工作,以及你对此感到非常兴奋。你的朋友说他也在同一栋办公楼工作。他建议你们两个应该一起开车。你认为这是一个好主意。所以在工作的第一天,你去他家。你把车停在他家门口。你等了10分钟,但你的邻居仍然没有出来。你继续开始上班,因为你不想在第一天被推迟。现在这是TIMED_WAITING。用技术术语解释它,你是线程T1,你的邻居是线程T2。你释放锁定(即停止驾驶汽车)并等待长达 10分钟。如果你的邻居T2在10分钟内没出来,你就开始再次开车了。提示:线程在调用以下方法之一时将进入TIMED_WAITING状态:Thread#sleep()Object#wait() 指定超时时间Thread#join() 指定超时时间LockSupport#parkNanos()LockSupport#parkUntil()结论当有人在分析线程转储时,理解这些不同的线程状态是至关重要的。处于RUNNABLE,BLOCKED,WAITING和TIMED_WATING状态的线程数是多少?哪些线程被阻止?谁阻止了他们?用于锁定的对象是什么?这些是在线程转储中要分析的一些重要指标。这些详细的线程转储分析可以通过在线工具轻松完成,例如:http://fastthread.io/

December 20, 2018 · 1 min · jiezi