• 单片机串口如何接收不定长数据的?

    单片机串口如何接收不定长数据的?

    我们在使用其他STM32的单片机的时候,会发现有些困难,会发现常用的方法并不能用,在还没有接收完数据的时候,就解决不了。于是,只能用通用的方法来解决了。 这个通用的方法,其实原理和使用IDLE的原理一样:接收完一个字节以后,如果超过了一定的时间,就认为是接收完一帧数据了。首先我们要知道,串口是接收一个字节,就会发生一次中断,如果一帧数据包含10个字节,就会发生10次中断。在接收一个字节以后,会紧跟着接收下一个字节,如果时间超了一定值,就代表一帧数据已经发完了。 下面,我分别用STM32和51单片机的代码来演示一下这个通用代码的实现。 1、STM32(以STM32L0系列为例) 串口中断函数: 2、51单片机(以STC8系列为例) 串口中断函数: void UART1_Isr() interrupt 4 // 串口中断服务函数 { if(RI) // 如果接收到一个字节 { RI = 0; // 中断标志位清0 Res_Buf[Res_Count++]=SBUF; // 把数据保存到接收数组 Res_Sign=1; // 表示已经接收到数据 Res_Times=0; // 延时计数器清0 } } 3、在主函数中处理串口数据 if(Res_Sign==1) // 如果串口接收到数据 { //延时等待接收完一帧数据 do{ Res_Times++; // 延时计数器+1 HAL_Delay(1); // 延时1ms }while(Res_Times<5); // 5ms时间到 //////////// //这里就可以处理接收数据了 //////////// Res_Sign=0; // 接收标志清0 Res_Count=0; // 接收数据字节计数器清0 } 4、程序解释 程序里面有4个全局变量,分别是: unsigned char Res_Buf[256]; //接收数据的数组,用来接收串口数据 unsigned char Res_Count=0; //接收数据的字节计数器,表示本次一帧数据包含几个字节 unsigned char Res_Sign=0; //接收到数据标志,接收到1个字节就会置1 unsigned char Res_Times=0; // 延时计数器,用来判断有没有接收完一帧数据 在串口中断函数里面,每接收到一个字节,就会把接收到的字节保存到Res_Buf数组中,同时,字节计数器+1。然后把Res_Sign置1,表示已经接收到串口数据,但是,有没有接收完,是不一定的。在主函数当中,发现这个变量等于1了,就开始启动延时计数Res_Times,让这个变量++,只要延时到了5ms,就表示接收完一帧数据,退出do while后就可以开始处理数据了,但是,当接收到第二个字节以后,会在中断函数里面把Res_Times清0,也就是说,主函数里面的Res_Times++以后,白加了,只要有数据还没有接收完,这个Res_Times就会一直清0,如果串口接收能接收一万年也接收不完一帧数据,那一万年,Res_Times也到不了5。只有当再也没有串口数据过来了,Res_Times才会加到5,然后退出do while,表示接收完一帧数据了,可以开始处理了。 上面的方法,适合所有的单片机。方法好不好,主要看自身产品的需求,适合就好,不适合就不好。方法有多种,根据你的需求调整到最好,就可以。

    单片机 单片机 串口

  • 如何利用锂电池电压测量电路

    如何利用锂电池电压测量电路

    如今物联网发展越来越好,单片机和锂电池组合已经越来越普遍,生产单片机的商家当让不会放过此商机,不断推出随物联网发展的单片机。 首先带大家了解一下什么是锂电池: 锂电池在充满电的时候,是4.2V;在用完电的时候,不是0V,而是2.7V左右,每个厂家制作的锂电池,略有差异… 鉴于锂电池材料的局限性,电压超过4.2V,会发生危险,比如燃烧;电压低于2.7V左右,会造成无法再次充电,总之… 锂电池电压过高和过低,都会造成永久损坏,所以… 我们的产品在使用锂电池的时候,需要时刻监测锂电池电压。 充电的时候,不要超过4.2V,这个要求,需要产品中加入充电管理芯片,充电管理芯片会自动在4.2V的时候切断充电。 放电的时候,也就是产品在正常使用的时候,不要让锂电池电压低于2.7V,比如,在2.7V的时候,自动强制关机。 那么,锂电池电压监测电路应该怎么设计呢? 如上图所示,应该是初学者最先想到的办法。不过,仔细分析后会发现,有大问题,我们来分析一下··· VBAT连接到锂电池正极,通过两个电阻分压,连接到单片机的ADC引脚。ADC测到的电压,就是锂电池电压的一半··· 因为锂电池的电压范围大概在2.7V到4.2V之间,所以ADC引脚的电压会在1.35~2.1V之间,不会超过普通单片机的3.3V电压,看起来很合理,不过··· 当产品处于关机状态时,我们以为锂电池就不耗电了,其实,通过电路可以发现,锂电池其实还在通过2个10k的电阻耗电··· 随着时间的推移,该产品放着放着电就减少了,而且当电池电压减少到2.7V以下时,就可能无法充起电来了··· 我在国外的一款产品上,看到了这样的一个电路,当然,已经把它使用到我的产品当中 上面电路,很巧妙的解决了这个问题,代价是电路板上多了1个MOS管和2个电阻,CTRL引脚是单片机的一个普通引脚,在单片机断电的时候,要求是高阻态,否则也会耗电··· 这里加MOS管并不是用来控制“是否要测量电池电压”,而是为了在产品关机的时候,不要让锂电池电池的电压通过两个分压电阻。 此时,还有个问题要解决··· 产品在正常使用的过程中,当电池电压小于3.3V时,LDO的输出电压,就不再是3.3V了,随着电池电压的减小,LDO的输出电压也会减小,此时… 如果一直使用3.3V作为基准来测量电池电压,就会出现错误,所以… 需要使用有基准电压引脚的单片机,或者有“内部参考电压”+“内部测量通道”功能的单片机··· 用基准电压引脚计算电池电压,这个大家都清楚,我重点说一下“内部参考电压”+“内部测量通道”这个功能。 简单来说,有了“内部参考电压”+“内部测量通道”之后,我们就可以直接通过内部测量通道得到精确的VDD电压,而不必使用基准电压芯片了,毕竟··· 基准电压芯片也挺贵的,还得在电路板上占个地方,以及多几分钱的焊接费用··· 下面,我们以STC8G系列单片机为例来说一下。 STC8G的ADC第15通道,用来测量内部参考电压源,内部参考电压为1.19V,通过测量它的值,反推出VDD值。 如上图的代码,会获得真实的VDDA值,最终会计算出单位是毫伏真实的电池电压。

    单片机 锂电池 电路

  • 聊聊算法在面试中的地位

    前段时间,有一位好友找到我,向我打听阿里社招笔试是否看重算法题的考察,我给予了肯定的答复。他表现的有些沮丧,表示自己工程底子很扎实,框架源码也研究地很透彻,唯独算法能力不行,leetcode 上的简单题做起来都有点吃力。以至于面试一些公司时,基本都是前几面和面试官聊工程,相聊甚欢,一到笔试就 GG。鉴于我个人在学生时代有过 ACM 经历,对算法还是相当感冒的,个人算法能力不算出众,也不算弱,最好成绩是省赛金牌,区域赛铜牌(主要还是抱得队友的大腿),后来实在是写不动 C++ 了,中途转了 Java,借这个机会跟大家聊一聊,分享下个人对算法的一些认识。 我发现很多人有的一个观念是刷算法并不能很好地帮助他工作,他们中有些人是有了很多学校或者公司的项目经验,有些则是在数据库、RPC、大数据等某个垂直领域有了比较长时间的沉淀,他们会觉得刻意地刷算法题比较偏门,没有太大的价值。一方面有些人会比较自信,不认为需要靠算法来证明自己的价值,另一方面,有些人会认为刷算法题是应届生面试才需要考察的技能,对于社招来说,公司应该更注重考察项目经验和系统设计层面的技能。以我个人经验来看,面试互联网公司时,算法题几乎都是必考的一个环节,从公司的考察点出发,就可以佐证出,算法不重要这个观点的确是有待商榷的。还有一些人轻视算法,是觉得只有大厂才看重算法,一些小公司的面试根本不 care 算法,而且特别是像我文章开头提到那个朋友一样的人,有着比较强的工程能力,我相信在面试中一定可以凭借着这个优势,赢得面试官的好感,那我不妨再反问一句,为什么要让算法成为你的软肋呢? 我已经表露了我对面试中算法重要程度的态度,而且我也认为面试中考察算法能力是非常重要的一环。在公司里做项目,我们往往需要花费数个月去落地,而面试中完成算法题最多只限制在半小时内,虽然时间区间不同,但本质上都是在考察一个人在一个固定的时间内完成某个任务的能力。读题考察了候选人的理解能力,期间我会与候选人沟通,以确保他正确理解的题意,并且在码字之前,我会要求对方先讲解题思路,这考察了沟通能力,有的候选人可能没有经历过刷题训练,缺少一些常见的算法思维,但经过提示后,如果能快速地完成 coding,在笔试中或许也能够通过。所以你看,其实考察算法题其实和也是借此考验了你的工作能力,它要求你在短短的半个小时之内做到 Bug Free,一定程度上这比做工程更难,因为没有人为你测试,而你要想通过这一环节,是需要额外花费精力去训练的。 虽然我认为面试中算法很重要,推荐大家准备面试时多去刷刷题,但我也确实抵制一些偏题、怪题。以我的刷题经验和工作经验结合来看,推荐的难度为 leetcode 简单、中等题,ACM 铜牌、银牌题,仅供参考。记得有一次瞄了一眼阿里的校招在线笔试题,具体是哪个部门不清楚,那个难度估计得是地狱难度了,这类情况仅仅是小概率会发生,至少在我们大部门不会出现特别难的算法题。 很多人说面试造火箭,入职拧螺丝,以此来讽刺面试中算法面是不必要的,我是不赞同的。抛开面试,算法能力也的确是工作中帮助了我。简单举几个例子吧,我通过算法题接触到了欧拉函数、GCD 等数论知识,让我可以非常好地理解 RSA 加密的原理和实现过程,而 RSA 加密是很有可能在工程中被使用到的一种非对称加密方式;通过解决常见的数据结构类算法题,我了解到了跳表的实现,这方便了我去理解 Redis 的 Set 结构;熟练地解决贪心和 DP 等问题,也潜移默化地影响着我在工程项目中的代码逻辑。 字节跳动可以说是业内有名的看重算法面的公司了,但鉴于本人并不了解实际的情况,只能跟大家聊聊阿里的算法面试。分成两部分:实习生面试和社招面试。我这里的经验主要都是基于我所了解的情况,在阿里其他部门(非阿里云)可能情况就不一样了。先说实习生面试吧,算法主要考察的是简单题,主要以贪心、数据结构、模拟为主,可以说非常友好了,主要考验学生对于基础知识的掌握程度,但也要求候选人能够在较短时间内完成,否则很难在整体面试中获得 A 评价。而社招,算法面试的地位肯定是要低于工程能力的考核的,但是对于能不能发 offer 又起着决定性的作用,等于说,即使你的工作履历很 match 岗位,工程经验也很丰富,但算法面一塌糊涂,往往用人部门只能忍痛割爱了。 如果你正在准备面试,我是建议准备下算法,刷一些题目找下手感,leetcode 和各种在线 OJ 都是不错的选择,B 站也有很多视频,具体的刷题列表,我这儿没准备,相信你可以在网上找到很多的。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 算法 leetcode 字节跳动

  • 如何用大数据分析来解决偶发性异常问题?

    如何用大数据分析来解决偶发性异常问题?

    摘要:在研发、生产过程中,如何发现和解决偶发性异常问题,是电子工程师始终想攻克的技术点,利用互联网思维,将大数据分析引入传统测量仪器,是否能碰撞出新的火花?本文将给出答案。 偶发性异常问题几乎存在于各行各业,本文将以新能源汽车中常见的继电器为例来说明大数据分析如何解决偶发性异常问题。 一、偶发性异常的出现 继电器、接触器、连接器等在电路中起着自动调节、安全保护、转换电路、连接电路等作用,广泛的应用于航空、航天、汽车电子等领域,在这些安全要求苛刻的领域对于产品的稳定性要求非常高,如何保证这些产品的稳定性呢? 本文以汽车上常规的继电器产品为例,根据《中华人民共和国基础机电继电器第7部分:试验和测量程序 GB/T 21711.7-2018》测量程序规范,需要测量继电器的回跳时间(对于正在闭合或断开其电路的触点,从触点电路首次闭合或断开的瞬间开始至电路最终闭合或断开的瞬间为止之间的时间)图示如下: 图1 继电器的回跳时间 我们使用一台带有2TB的固态硬盘的示波记录仪记录下此过程。 图2 60s的波形记录 手动展开波形我们就发现了偶发性异常问题---每一个波形的上升沿,继电器的回跳时间竟不一致。研发工程师规定此时间不能超过10ms,但仅仅只录制了1分钟的波形就有40个需要查看的上升沿,如果是1小时的波形呢?靠手动测试工作量大到不敢想象! 图3 逐级展开波形 二、大数据分析 如果引入互联网思维,让机器自己检索问题是不是可以大大提高效率呢?听上去是个很好的主意!但实践起来我们发现此偶发性异常的判定方法不同于任何的如上升沿、幅值等常规规则,这就是一大的难题。 通过集思广益,我们在示波记录仪上开发出了“大数据分析”功能。我们将此独特波形的判定方法写成一个算法文件,然后直接在机器本机进行加载,最终实现了自动判定。 图4 加载算法文件 三、解决偶发性异常问题 加载的算法文件可以当做是一种独特判定方式,记录仪可以实时的针对此判定方法和源数据进行比对,并且将结果显示出来,如上面的这个继电器回跳时间,ZDL6000示波记录仪已经自动的将结果分析出来,并直接给出所有的测试结果,原来需要花费几个小时的工作,现在只需要几分钟!这就是大数据分析解决偶发性异常的意义。 图5 搜索结果 四、产品试用 在工程师的日常开发中,时常遇到偶发性异常问题,借助ZDL6000示波记录仪的大数据分析功能,通过加载算法文件,可以大大节约测试时间,提高工作效率。这样引入互联网思维的测试仪器,欢迎各位用户来试用!

    致远电子 大数据 测量仪器 偶发性异常

  • 数据仓库的前世今生

    数据仓库的起源可以追溯到计算机的发展初期,并且数据仓库是信息技术长期发展的产物,在以后也会一直发展。 一、文件系统 20世纪60年代初期,计算机领域的主要工作是创建运行在主文件上的单个应用。这些应用是以报表处理和程序为特征的,一般是以某种早期的程序设计语言如Fortran或COBOL编写的。主文件存储在廉价的磁带上面,其缺点是只能顺序访问。比如我们想得到磁带上第20分钟处的数据,那时必须顺序访问完前面的19分钟。磁带在提供廉价存储的同时,也带来了数据的大量冗余。20世纪60年代中期,大量的主文件带来了诸多问题,如: 更新数据时需要保持数据的一致性。 程序维护的复杂性。 开发新程序的复杂性。 支持所有主文件需要增加大量的硬件。2 0世纪60年代 - General Mills 和 Dartmouth College 在一个联合研究项目中,制定了术语维度(dimensio ns)和事实(facts)。 二、DASD和DBMS的出现 到了1970年,出现了一种新的存储和访问技术,也就是磁盘存储器,或者称之为直接存取存储设备(Direct Access Storage Device,DASD)。磁盘存储与磁带存储的根本不同在于磁盘上的数据能够直接访问。DASD要访问第n+1条记录,不再需要顺序访问第1、2、3......n条记录,而是一旦知道了第n+1条记录的地址,就可以直接访问它。 随着DASD的发展,出现了一种称为数据库管理系统(Database Management System,DBMS)的新型系统软件。这种新型软件目的是使程序员可以方便的在DASD上面进行存储和访问。伴随着DBMS,出现了“数据库”的概念。 1975年 - Sperry Univac推出MAPPER(MAintain,Prepare,Produce Executive Reports),这是一个数据库管理和报告系统,其中包括世界上第一个第一个专为建设信息中心而设计的平台4GL(当代数据仓库技术的先驱) 三、数据仓库之父的出现 到了20世纪80年代,涌现了一些更为新颖的技术,比如个人计算机(PC)和第四代编程语言(Fourth-Generation Language,4GL)。随着PC和4GL的发展,除了高性能的在线事务处理之外,人们可以利用数据做更多的事情,比如早期的管理信息系统(Management InformationSystem,MIS),如今这种技术成为DSS。 1990年 - 由Ralph Kimball创立的Red Brick Systems推出了Red Brick Warehouse,这是一个专门用于数据仓库的数据库管理系统。 四、多个单独数据库 随着大型在线事务处理系统问世不久,出现了数据抽取技术,可以实现把想要的数据从在线事务处理系统中分离出来,这样就可以解决数据分析性能方面的问题;抽取出来的数据,给人们在使用数据方面带来了极大的灵活性,我们可以使用这些数据做各种分析。 起初,只是对在线事务处理系统中的数据进行抽取。慢慢的人们发现在抽取结果中,加上一些条件限制可以更方便的得到想要的数据。但此时的“数据仓库”是多个单独的数据库,在使用过程中慢慢出现了如下问题: 数据时间不统一 抽取程序的差异 外部数据加载问题 无公共起始数据源 以上问题就会有可能导致,不同部门抽取数据的差异,从而到时分析结果的不同。 五、数据仓库 当人们意识到无休止的抽取带来诸多问题后,开始思考是否可以建立成体系的机构化环境,以减少数据的差异,这也就是数据仓库出现的原因。数据仓库从操作型数据库中抽取数据,通过规范的加工过程,得到粒度化数据,并且这些数据时面向主题、集成、不易失、随时间变化的数据。在数据仓库的基础上,可以建立不同分析角度的BI报表系统。 随着大数据的出现,阿里大数据技术人的宣传,加上出版的一些书籍(大数据之路)对此数据仓库的传播都有着巨大的推动。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 数据仓库 文件系统 DBMS

  • 单核CPU, 1G内存,也能做JVM调优吗?

    最近,笔者的技术群里有人问了一个有趣的技术话题:单核CPU, 1G内存的超低配机器,怎么做JVM调优? 这实际上是两个问题。单核CPU的超低配机器,怎么充分利用CPU?单核CPU, 1G内存的超低配机器,怎么做JVM调优? 对于IO密集型和CPU密集型的应用调优的方法会截然不同。 CPU密集型:  以计算为主,很少有磁盘和网络访问的应用。这种任务CPU一直在运行,CPU的利用率很高。 在给出CPU调优结论之前,先花两分钟熟悉一下I/O基础。 所谓的I/O(Input/Output)操作实际上就是输入输出的数据传输行为。 1,页面请求到服务器会发生网络IO 3,应用程序访问数据库会发生网络IO CPU空闲率是0%(上图中红框id) CPU 在等待磁盘IO操作上花费的时间占比是76.6% (上图中红框 wa) 不少人会这样理解,如果CPU空闲率是0%,真是这样的吗? 后来有人设计了一个IO控制器,专门控制磁盘IO。当发生磁盘和内存间的数据传输前,CPU会给IO控制器发送指令,让IO控制器负责数据传输操作,数据传输完IO控制器再通知CPU。因此,从磁盘读取数据到内存的过程就不再需要CPU参与了,CPU可以空出来处理其他事情,大大提高了CPU利用率。这个IO控制器就是“DMA”,即直接内存访问,Direct Memory Access。现在的计算机基本都采用这种DMA模式进行数据传输。 当应用进程或线程发生IO等待时,CPU会及时释放相应的时间片资源并把时间片分配给其他进程或线程使用,从而使CPU资源得到充分利用。所以, 同时还要考虑线程间上下文切换带来的性能开销,线程数量不能太高。对于单核CPU,要根据IO的密集程度设置线程数。少IO操作频率,缩短IO操作时间。IO操作优化之后,线程数可以设置成更少,线程切的换频率和性能开销也会随之降低。 对于CPU密集型应用。对于单核CPU,为了减少线程切换带来的性能开销怎么做JVM调优? 选择合适的垃圾收集器 以CMS回收过程为例,在耗时较长的并发标记和并发清除阶段,垃圾收集线程和用户线程是同时并行工作的,也就是说并发阶段不会导致用户线程停顿。不过CMS对CPU资源非常敏感。 其实,所有高并发的应用对CPU资源都很敏感。在CMS并发阶段(并发标记和并发清除阶段),虽然不会导致用户线程停顿,但是垃圾收集线程会占用一部分CPU资源,进而导致应用程序变慢,吞吐量降低。CMS默认启动的垃圾收集线程数是(CPU核数+3)/4,当CPU核数在4个以上时,并发回收阶段垃圾收集线程不少于25%的CPU资源(CPU核数)。但是当CPU核数不足4个时,比如CPU核数为2个,CMS对用户程序的影响就可能变得很大,此时需要分配1个核的资源去执行垃圾收集任务,如果本来CPU负载就比较大,还要分出一半的计算能力去执行垃圾收集任务,就可能导致应用程序的执行速度大幅下降,甚至忽然降低50%以上,着实让人无法接受。 说到这有人可能会问:换成其他垃圾收集器,在单核CPU环境下,不一样会有这种因为线程阻塞导致的应用程序执行变慢的问题吗? CMS是响应速度优先的老年代垃圾收集器,是一种以降低GC全局停顿时间(Stop The World)为目标的收集器。其中初始标记和重新标记两个阶段会停止所有用户线程(发生STW),不过耗时很短。CMS的这种设计虽然缩短了STW的时间,但是整个GC过程(四个阶段加在一起的总时间)更长了。基本上可以这样理解,在单核CPU环境下,CMS的四个阶段都会发生Stop The World。所以在单核CPU环境下,绝对不能选择CMS和G1这种对CPU特别敏感的收集器。所以,基本上最古老的Serial Old收集器就成了单核CPU的最佳选择啦。 我们以Java官方的HotSpot JVM为例, 通过上面的图文内容,我们了解了堆内存中对象的分配和流转过程。那么可以基于这些知识来做一些JVM调优的工作。 还可以适当调大MaxTenuringThreshold,来提高年轻代幸存区s0和s1的交换次数,进而减少对象晋升到老年代的几率。 大对象初始化时会跨过年轻代直接分配到老年代,这种情况触发的Major GC和Minor GC就没半点关系了。可以通过-XX:PretenureSizeThreshold参数设置大对象的大小,如果参数被设置成5MB,超过5MB的大对象会直接分配到老年代。 缩短GC时间 缩短GC时间和降低GC频次,两者是鱼和熊掌的关系,不可兼得。如上面所说,在1G内存单核CPU的场景下,响应时间优先的CMS和G1都不适合。在垃圾收集器没有太多选择的情况下,如果想缩短Major GC时间,基本上只能减小老年代的比例了,老年代空间越小,每次Major GC需要处理的对象就越少,GC时间也就越短。老年代空间越小,GC的频次自然也会更高,内存空间就那么多,所以我们需要反复试验,在GC频次和GC时间上找到最佳平衡点来满足业务系统的要求。 结语 JVM调优没有什么可以拿来即用的固定模板或规范,每个应用都有自己的独特场景。不同的应用并发程度不一样,对响应时间和吞吐量要求也不一样,堆内存对象规模、对象生命周期、对象大小等等都不会完全一样,这些因素都会影响到JVM的性能。所以,JVM调优是一个循序渐进的过程,必然需要经历多次迭代,最终才能得到一个较好的折中方案。

    架构师社区 CPU JVM JVM调优

  • 浅谈程序员的“内卷化”

    一、什么是内卷化 最近开始了解到一个很有意思的词——“内卷化”,如果你还不知道这个词,那就非常建议往下看。 什么是内卷化?内卷化,亦称过密化,最初由文化人类学家亚历山大·戈登威泽提出,用于描述社会文化模式的变迁规律。当一种文化模式进入到最终的固定状态时,便逐渐局限于自身内部不断进行复杂化的转变,从而再也无法转化为新的文化形态。在中国语境下,内卷化概念最初闻名自历史学家杜赞奇对于古代中国经济生活的研究成果中。杜赞奇借用内卷化一词描述清代人口爆炸,廉价劳动力过剩,从而无法带动技术革新,使得古代中国的经济形态长期停滞于小农经济阶段的发展状态。 好吧,我相信你没看懂! 举个例子,上图! 这段对话,就形象地为“内卷化”现象做了解释。 二、程序员的“内卷化” 程序员本来是一个需要高学历,高技能的工作。但是随着“科技发展”,慢慢的自称自己为“码农”、“搬砖者”,而且一边喊着“35岁危机”,一边996的干活。身体慢慢的发胖(过劳肥),头发渐渐稀少。 1、码农时代 上世纪 七八十 年代,IT工程师们使用汇编语言操作大型机,码出了各种操作系统,各种数据库,那些年的前辈们,现在后辈们望尘莫及,可能连“尘”都看不见。后来PC的出现,专业院校培养出身的学生们,开始进入职场,运用各种办公软件 、应用软件 和中间件开启了IT工程师的生涯,他们成为了现在各厂的“爸爸”们。前些年兴起了XX培训机构,缴费一万,三个月保证上岗,“全民学Python”。程序员的门槛一下子变低了,每年无数的新人进入这个行业,只要有电脑就能自学,“码农时代”到来了! 2、开源时代 现在的程序员写代码变得比原来的程序员强,因为他们有强大的基础库。springboot 写出来的最简单的项目,以前你想写出来都是不太可能的,你需要实现几十万行码。就算你能做到, 到了今天也没什么稀奇的了,刚刚毕业的程序员随便就弄出来了,你以前写代码的那些能力赶不上了。有人说,我会比新人学得快,学得好。我认为这话有道理,但是不一定。那工作两年后的人,学习新技术和你一样快,没什么差别。 这几年大数据时代的来临,很多开源框架逐渐成熟,以前针对大数据了的计算和存储要费劳力也不一定搞定,现在MR计算框架和Spark轻松帮你搞定PB级别的数据,更可怕的是你只需要会写SQL就行!刚毕业的学生一个月可能就掌握了基本的海量数据查询功能。 很多公司会出现一种情况——“工资倒挂”,大部分原因就是你的工作很容易被代替导致的。但是很多老员工心里不服气啊,凭什么我工作5年了,刚毕业的一个学生跟我拿一样的工资?我来告诉你为什么。这些老员工一般只依赖自己刚进公司那两年为公司写了大量的代码,然后一直维护了三年,没有学习新的技术。而来公司写的程序也许只适合现在的公司,他的这个技能出去后很可能就失业了,所以,他的5年经验价值是很小的。而刚毕业的学生,使用的最新的技术框架,很快做出来了你之前的项目效果,而且新的框架还更简单,另外刚毕业的学生还比你更能加班! 3、加班文化 以前的程序员,很少有加班的,根本没听说过“996福报”。但是有一天,部门出现了一个同事只依靠白天完不成工作,就开始晚上加班,加班了2个小时,把工作赶上正常进度了,但是该同事想,如果再加班两个小时那不就比正常进度快了嘛!该同事996一个月,竟然拿到了高绩效,老板还表扬了这种加班。于是,其他同事开始效仿,就算每天能正常完成工作,还是会加班,有的赶赶进度,有的就是划划水。大家为了保住工作,都开始加班,慢慢的加班对于拿高绩效就不再有竞争力了,而是成了“标配”。此时,那些不愿加班的人,想要高效完成工作,正常过下班生活的程序员成为了“另类”,老板会因为这些员工的“态度”问题,还不给高绩效。慢慢的所有人都开始了996,大家“工作态度”高度一致,老板还是得从其他方面进行评估工作,但是此刻加班已经形成一种“文化”。最后,老板成了最终的获益者,程序员亲手毁了自己的工作环境,而且在长期加班工作中,自己的思考变的迟钝了,不在有那么多创意想法,不会再想那么多提高工作效率的方法,因为只要靠“加班”就行了。 三、如何不被“内卷化” 现在我们已经明确的知道程序员的“内卷化”现象,我们都不希望自己被“内卷化”,那么如何避开“内卷化”呢? 1、Stay Hungry, Stay Foolish 程序员要时刻保持好奇心,持续学习。IT技术这些年发展太快,不想造原子弹那样的高科技。不管是后端的springboot,前端vue,还是现在的各种大数据计算引擎,作为一线开发者的我们都要时刻保持学习的态度,走出自己的舒适区。 2、工程能力 很多同事写代码速度一流的,但是你让他从头开始部署一个项目,他依然不知道自己要怎么做。部署上,页面报错了依然不知道从哪解决,这些程序员一般都有一个口头禅“我的程序在本地跑的没问题,你看看是不是你的程序有bug啊”,这就是缺乏工程能力的表现。所谓工程能力,我把它分为这几部分:架构、规范、管理、排错这几个能力。 架构 架构不仅仅是指技术架构,对业务的深度了解也是重要的一部分。作为一个工作多年的程序员,要学会了解架构知识,一个好的架构能够在以后业务的发展中避免平台的重构。要知道整个平台是怎么运转起来的,数据流转的全流程是怎样的,客户的需求是怎样的。 规范 程序员是最讨厌写文档、定规范的,都喜欢自由。但是,很多次生产环境的意外宕机都是缺乏规范引起的,不管是流程规范,还是操作规范,在平时,我们都要养成“规范”能力,就像你每次上完厕所后会洗手一样。“敬畏生产环境”也不只是喊喊,功夫要用在平时。 管理 程序员都不喜欢被管理,但是不管你是不是管理者,都要学会管理。一是管理自己日常的工作,有序开展,避免无效加班;而是学会管理同事,让同事更高效的配合自己完成工作,也许某一天你就会成为你旁边同事们的管理者。 排错 我认为这是工程能力最重要的表现之一,程序中日志记录要成为每个工程师的习惯。你多年的工作经验也许并不在于你代码写的快,而是在于你比别人更快的定位问题、解决问题。 3、学会思考 互联网刚开始的阶段,很多人都喊“我有idea,就缺程序员了”,而现在越来越多的人喊“大家有什么idea,我来负责开发”,而这也是现在好的产品经理越来越值钱的重要原因。 没有思考,我们就会一直工作,一直加班,循环往复。学会思考,我们才能找到高效工作的方法,避免“恶性加班”,才能提高自己的编程能力,而不是提高编程的“熟练度”。 也许有一天,你正好有一个idea,自己也能实现,可能一不小心就“财富自由”,走向人生巅峰了。 四、结尾 看了这么多,程序员表示也很难。其实回想一下自己进入公司的初衷,如果是为了混饭吃,那你确实很担心这个,很快也许就会有替换你的廉价劳动力了。如果不是,那应该没有什么好担心的,你已经知道如何找到自己的核心竞争力了。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 程序员 内卷化

  • 鱼和熊掌兼得:同时使用 JPA 和 Mybatis

    前言 JPA 和 Mybatis 的争论由来已久,还记得在 2 年前我就在 spring4all 社区就两者孰优孰劣的话题发表了观点,我当时是力挺 JPA 的,这当然跟自己对 JPA 熟悉程度有关,但也有深层次的原因,便是 JPA 的设计理念契合了领域驱动设计的思想,可以很好地指导我们设计数据库交互接口。这两年工作中,逐渐接触了一些使用 Mybatis 的项目,也对其有了一定新的认知。都说认知是一个螺旋上升的过程,随着经验的累积,人们会轻易推翻过去,到了两年后的今天,我也有了新的观点。本文不是为了告诉你 JPA 和 Mybatis 到底谁更好,而是尝试求同存异,甚至是在项目中同时使用 JPA 和 Mybatis。什么?要同时使用两个 ORM 框架,有这个必要吗?别急着吐槽我,希望看完本文后,你也可以考虑在某些场合下同时使用这两个框架。 ps. 本文讨论的 JPA 特指 spring-data-jpa。 建模 @Entity @Table(name = "t_order") public class Order { @Id private String oid; @Embedded private CustomerVo customer; @OneToMany(cascade = {CascadeType.ALL}, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "order") private ListorderItems; } JPA 最大的特点是 sqlless,如上述的实体定义,便将数据库的表和 Java 中的类型关联起来了,JPA 可以做到根据 @Entity 注解,自动创建表结构;基于这个实体实现的 Repository 接口,又使得 JPA 用户可以很方便地实现数据的 CRUD。所以,使用 JPA 的项目,人们很少会提到”数据库设计“,人们更关心的是领域建模,而不是数据建模。 <generatorConfiguration> <context id="my" targetRuntime="MyBatis3"> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="" userId="" password=""/> <javaModelGenerator targetPackage="" targetProject="" /> <sqlMapGenerator targetPackage="" targetProject="" /> <javaClientGenerator targetPackage="moe.cnkirito.demo.mapper" /> <table tableName="t_order" domainObjectName="Order" /> context> generatorConfiguration> Mybatis 用户更多使用的是逆向工程,例如 mybatis-generator 插件根据如上的 xml 配置,便可以直接将表结构转译成 mapper 文件和实体文件。 code first 和 table first 从结果来看是没有区别的,差异的是过程,所以设计良好的系统,并不会仅仅因为这个差异而高下立判,但从指导性来看,无疑设计系统时,更应该考虑的是实体和实体,实体和值对象的关联,领域边界的划分,而不是首先着眼于数据库表结构的设计。 建模角度来看,JPA 的领域建模思想更胜一筹。 数据更新 聊数据库自然离不开 CRUD,先来看增删改这些数据更新操作,来看看两个框架一般的习惯是什么。 JPA 推崇的数据更新只有一种范式,分成三步: 先 findOne 映射成实体 内存内修改实体 实体整体 save 你可能会反驳我说,@Query 也存在 nativeQuery 和 JPQL 的用法,但这并不是主流用法。JPA 特别强调”整体 save“的思想,这与领域驱动设计所强调的有状态密不可分,即其认为,修改不应该是针对于某一个字段:”update table set a=b where colomonA=xx“ ,而应该反映成实体的变化,save 则代表了实体状态最终的持久化。 先 find 后 save 显然也适用于 Mybatis,而 Mybatis 的灵活性,使得其数据更新方式更加地百花齐放。路人甲可以认为 JPA 墨守成规不懂变通,认为 Mybatis 不羁放纵爱自由;路人乙也可以认为 JPA 格式规范易维护,Mybatis 不成方圆。这点不多加评判,留后人说。 从个人习惯来说,我还是偏爱先 find 后整体 save 这种习惯的,不是说这是 JPA 的专利,Mybatis 不具备;而是 JPA 的强制性,让我有了这个习惯。 数据更新角度来看,JPA 强制使用 find+save,mybatis 也可以做到这一点,胜者:无。 数据查询 JPA 提供的查询方式主要分为两种 简单查询:findBy + 属性名 复杂查询:JpaSpecificationExecutor 简单查询在一些简单的业务场景下提供了非常大的便捷性,findBy + 属性名可以自动转译成 sql,试问如果可以少写代码,有谁不愿意呢? 复杂查询则是 JPA 为了解决复杂的查询场景,提供的解决方案,硬是把数据库的一些聚合函数,连接操作,转换成了 Java 的方法,虽然做到了 sqlless,但写出来的代码又臭又长,也不见得有多么的易读易维护。这算是我最不喜欢 JPA 的一个地方了,但要解决复杂查询,又别无他法。 而 Mybatis 可以执行任意的查询 sql,灵活性是 JPA 比不了的。数据库小白搜索的最多的两个问题: 数据库分页怎么做 条件查询怎么做 Mybatis 都可以轻松的解决。 千万不要否认复杂查询:如聚合查询、Join 查询的场景。令一个 JPA 用户抓狂的最简单方式,就是给他一个复杂查询的 case。 select a,b,c,sum(a) where a=xx and d=xx group by a,b,c; 来吧,展示。可能 JPA 的确可以完成上述 sql 的转义,但要知道不是所有开发都是 JPA 专家,没人关心你用 JPA 解决了多么复杂的查询语句,更多的人关心的是,能不能下班前把这个复杂查询搞定,早点回家。 在回到复杂数据查询需求本身的来分析下。我们假设需求是合理的,毕竟项目的复杂性难以估计,可能有 1000 个数据查询需求 JPA 都可以很方便的实现,但就是有那么 10 几个复杂查询 JPA hold 不住。这个时候你只能乖乖地去写 sql 了,如果这个时候又出现一个条件查询的场景,出现了 if else 意味着连 @Query 都用不了,完全退化成了 JdbcTemplate 的时代。 那为什么不使用 Mybatis 呢?Mybatis 使用者从来没有纠结过复杂查询,它简直就是为之而生的。 如今很多 Mybatis 的插件,也可以帮助使用者快速的生成基础方法,虽然仍然需要写 sql,但是这对于开发者来说,并不是一件难事。 不要质疑高并发下,JOIN 操作和聚合函数存在的可能性,数据查询场景下,Mybatis 完胜。 性能 本质上 ORM 框架并没有性能的区分度,因为最终都是转换成 sql 交给数据库引擎去执行,ORM 层面那层性能损耗几乎可以忽略不计。 但从实际出发,Mybatis 提供给了开发者更高的 sql 自由度,所以在一些需要 sql 调优的场景下会更加灵活。 可维护性 前面我们提到 JPA 相比 Mybatis 丧失了 sql 的自由度,凡事必有 trade off,从另一个层面上来看,其提供了高层次的抽象,尝试用统一的模型去解决数据层面的问题。sqlless 同时也屏蔽了数据库的实现,屏蔽了数据库高低版本的兼容性问题,这对可能存在的数据库迁移以及数据库升级提供了很大的便捷性。 同时使用两者 其他细节我就不做分析了,相信还有很多点可以拿过来做对比,但我相信主要的点上文都应该有所提及了。进行以上维度的对比并不是我写这篇文章的初衷,更多地是想从实际开发角度出发,为大家使用这两个框架提供一些参考建议。 在大多数场景下,我习惯使用 JPA,例如设计领域对象时,得益于 JPA 的正向模型,我会优先考虑实体和值对象的关联性以及领域上下文的边界,而不用过多关注如何去设计表结构;在增删改和简单查询场景下,JPA 提供的 API 已经是刻在我 DNA 里面的范式了,使用起来非常的舒服。 在复杂查询场景下,例如 包含不存在领域关联的 join 查询 包含多个聚合函数的复杂查询 其他 JPA 较难实现的查询 我会选择使用 Mybatis,有点将 Mybatis 当做数据库视图生成器的意味。坚定不移的 JPA 拥趸者可能会质疑这些场景的存在的真实性,会质疑是不是设计的漏洞,但按照经验来看,哪怕是短期方案,这些场景也是客观存在的,所以听我一言,尝试拥抱一下 Mybatis 吧。 随着各类存储中间件的流行,例如 mongodb、ES,取代了数据库的一部分地位,重新思考下,本质上都是在用专业的工具解决特定场景的问题,最终目的都是为了解放生产力。数据库作为最古老,最基础的存储组件,的确承载了很多它本不应该承受的东西,那又何必让一个工具或者一个框架成为限制我们想象力的沟壑呢? 两个框架其实都不重,在 springboot 的加持下,引入几行配置就可以实现两者共存了。 我自己在最近的项目中便同时使用了两者,遵循的便是本文前面聊到的这些规范,我也推荐给你,不妨试试。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 JPA Mybatis ORM

  • 10张图让你彻底理解回调函数

    大家好,以前写过一篇关于回调函数的文章C语言函数指针之回调函数,今天又安排了一篇。 不知你是不是也有这样的疑惑,我们为什么需要回调函数这个概念呢?直接调用函数不就可以了?回调函数到底有什么作用?程序员到底该如何理解回调函数? 这篇文章就来为你解答这些问题,读完这篇文章后你的武器库将新增一件功能强大的利器。 一切要从这样的需求说起 假设你们公司要开发下一代国民App“明日油条”,一款主打解决国民早餐问题的App,为了加快开发进度,这款应用由A小组和B小组协同开发。 其中有一个核心模块由A小组开发然后供B小组调用,这个核心模块被封装成了一个函数,这个函数就叫make_youtiao()。 如果make_youtiao()这个函数执行的很快并可以立即返回,那么B小组的同学只需要: 调用make_youtiao() 等待该函数执行完成 该函数执行完后继续后续流程 从程序执行的角度看这个过程是这样的: 保存当前被执行函数的上下文 开始执行make_youtiao()这个函数 make_youtiao()执行完后,控制转回到调用函数中 如果世界上所有的函数都像make_youtiao()这么简单,那么程序员大概率就要失业了,还好程序的世界是复杂的,这样程序员才有了存在的价值。 现实并不容易 现实中make_youtiao()这个函数需要处理的数据非常庞大,假设有10000个,那么make_youtiao(10000)不会立刻返回,而是可能需要10分钟才执行完成并返回。 这时你该怎么办呢?想一想这个问题。 可能有的同学会问,和刚才一样直接调用不可以吗,这样多简单。 是的,这样做没有问题,但就像爱因斯坦说的那样“一切都应该尽可能简单,但是不能过于简单”。 想一想直接调用会有什么问题? 显然直接调用的话,那么调用线程会被阻塞暂停,在等待10分钟后才能继续运行。在这10分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。 这并不是一种高效的做法。 没有一个程序员想死盯着屏幕10分钟后才能得到结果。 那么有没有一种更加高效的做法呢? 想一想我们上一篇中那个一直盯着你写代码的老板(见《从小白到高手,你需要理解同步与异步》),我们已经知道了这种一直等待直到另一个任务完成的模式叫做同步。 如果你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后自然会通知老板,这样老板和程序员都不需要相互等待,这种模式被称为异步。 回到我们的主题,这里一种更好的方式是调用make_youtiao()这个函数后不再等待这个函数执行完成,而是直接返回继续后续流程,这样A小组的程序就可以和make_youtiao()这个函数同时进行了,就像这样: 在这种情况下,回调(callback)就必须出场了。 为什么我们需要回调callback 有的同学可能还没有明白为什么在这种情况下需要回调,别着急,我们慢慢讲。 假设我们“明日油条”App代码第一版是这样写的: make_youtiao(10000);sell(); 可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。 我们已经知道了由于make_youtiao(10000)这个函数10分钟才能返回,你不想一直死盯着屏幕10分钟等待结果,那么一种更好的方法是让make_youtiao()这个函数知道制作完油条后该干什么,即,更好的调用make_youtiao的方式是这样的: “制作10000个油条, 炸好后卖出去 ”,因此调用make_youtiao就变出这样了: make_youtiao(10000, sell); 看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外 还可以指定制作好后该干什么 ,第二个被make_youtiao这个函数调用的函数就叫回调,callback。 现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样: make_youtiao这个函数是怎么实现的呢,很简单: void make_youtiao(int num, func call_back) { // 制作油条 call_back(); //执行回调 } 这样你就不用死盯着屏幕了,因为你把make_youtiao这个函数执行完后该做的任务交代给make_youtiao这个函数了,该函数制作完油条后知道该干些什么,这样就解放了你的程序。 有的同学可能还是有疑问,为什么编写make_youtiao这个小组不直接定义sell函数然后调用呢? 不要忘了明日油条这个App是由A小组和B小组同时开发的,A小组在编写make_youtiao时怎么知道B小组要怎么用这个模块,假设A小组真的自己定义sell函数就会这样写: void make_youtiao(int num) { real_make_youtiao(num); sell(); //执行回调 } 同时A小组设计的模块非常好用,这时C小组也想用这个模块,然而C小组的需求是制作完油条后放到仓库而不是不是直接卖掉,要满足这一需求那么A小组该怎么写呢? void make_youtiao(int num) { real_make_youtiao(num); if (Team_B) { sell(); // 执行回调 } else if (Team_D) { store(); // 放到仓库 }} 故事还没完,假设这时D小组又想使用呢,难道还要接着添加if else吗? 这样的话A小组的同学只需要维护make_youtiao这个函数就能做到工作量饱满了,显然这是一种非常糟糕的设计。 所以你会看到,制作完油条后接下来该做什么不是实现make_youtiao的A小组该关心的事情,很明显只有调用make_youtiao这个函数的使用方才知道。 因此make_youtiao的A小组完全可以通过回调函数将接下来该干什么交给调用方实现,A小组的同学只需要针对回调函数这一抽象概念进行编程就好了,这样调用方在制作完油条后不管是卖掉、放到库存还是自己吃掉等等想做什么都可以,A小组的make_youtiao函数根本不用做任何改动,因为A小组是针对回调函数这一抽象概念来编程的。 以上就是回调函数的作用,当然这也是针对抽象而不是具体实现进行编程这一思想的威力所在。面向对象中的多态本质上就是让你用来针对抽象而不是针对实现来编程的。 异步回调 故事到这里还没有结束。 在上面的示例中,虽然我们使用了回调这一概念,也就是调用方实现回调函数然后再将该函数当做参数传递给其它模块调用。 但是,这里依然有一个问题,那就是make_youtiao函数的调用方式依然是同步的,关于同步异步请参考《从小白到高手,你需要理解同步与异步》,也就是说调用方是这样实现的: make_youtiao(10000, sell);// make_youtiao函数返回前什么都做不了 我们可以看到,调用方必须等待make_youtiao函数返回后才可以继续后续流程,我们再来看下make_youtiao函数的实现: void make_youtiao(int num, func call_back) { real_make_youtiao(num); call_back(); //执行回调 } 看到了吧,由于我们要制作10000个油条,make_youtiao函数执行完需要10分钟,也就是说即便我们使用了回调,调用方完全不需要关心制作完油条后的后续流程,但是调用方依然会被阻塞10分钟,这就是同步调用的问题所在。 如果你真的理解了上一节的话应该能想到一种更好的方法了。 没错,那就是异步调用。 反正制作完油条后的后续流程并不是调用方该关心的,也就是说调用方并不关心make_youtiao这一函数的返回值,那么一种更好的方式是:把制作油条的这一任务放到另一个线程(进程)、甚至另一台机器上。 如果用线程实现的话,那么make_youtiao就是这样实现了: void make_youtiao(int num, func call_back) { // 在新的线程中执行处理逻辑 create_thread(real_make_youtiao, num, call_back);} 看到了吧,这时当我们调用make_youtiao时就会 立刻返回 ,即使油条还没有真正开始制作,而调用方也完全无需等待制作油条的过程,可以立刻执行后流程: make_youtiao(10000, sell);// 立刻返回// 执行后续流程 这时调用方的后续流程可以和制作油条 同时 进行,这就是函数的 异步调用 ,当然这也是异步的高效之处。 新的编程思维模式 让我们再来仔细的看一下这个过程。 程序员最熟悉的思维模式是这样的: 调用某个函数,获取结果 处理获取到的结果 res = request();handle(res); 这就是函数的同步调用,只有request()函数返回拿到结果后,才能调用handle函数进行处理,request函数返回前我们必须 等待 ,这就是同步调用,其控制流是这样的: 但是如果我们想更加高效的话,那么就需要异步调用了,我们不去直接调用handle函数,而是作为参数传递给request: request(handle); 我们根本就不关心request什么时候真正的获取的结果,这是request该关心的事情,我们只需要把获取到结果后该怎么处理告诉request就可以了,因此request函数可以立刻返回,真的获取结果的处理可能是在另一个线程、进程、甚至另一台机器上完成。 这就是异步调用,其控制流是这样的: 从编程思维上看,异步调用和同步有很大的差别,如果我们把处理流程当做一个任务来的话,那么同步下整个任务都是我们来实现的,但是异步情况下任务的处理流程被分为了两部分: 第一部分是我们来处理的,也就是调用request之前的部分 第二部分不是我们处理的,而是在其它线程、进程、甚至另一个机器上处理的。 我们可以看到由于任务被分成了两部分,第二部分的调用不在我们的掌控范围内,同时只有调用方才知道该做什么,因此在这种情况下回调函数就是一种必要的机制了。 也就是说回调函数的本质就是“只有我们才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它模块才知道,因此我们必须把我们知道的封装成回调函数告诉其它模块”。 现在你应该能看出异步回调这种编程思维模式和同步的差异了吧。 接下来我们给回调一个较为学术的定义 正式定义 在计算机科学中,回调函数是指一段以参数的形式传递给其它代码的可执行代码。 这就是回调函数的定义了。 回调函数就是一个函数,和其它函数没有任何区别。 注意,回调函数是一种软件设计上的概念,和某个编程语言没有关系,几乎所有的编程语言都能实现回调函数。 对于一般的函数来说,我们自己编写的函数会在自己的程序内部调用,也就是说函数的编写方是我们自己,调用方也是我们自己。 但回调函数不是这样的,虽然函数编写方是我们自己,但是函数调用方不是我们,而是我们引用的其它模块,也就是第三方库,我们调用第三方库中的函数,并把回调函数传递给第三方库,第三方库中的函数调用我们编写的回调函数,如图所示: 而之所以需要给第三方库指定回调函数,是因为第三方库的编写者并不清楚在某些特定节点,比如我们举的例子油条制作完成、接收到网络数据、文件读取完成等之后该做什么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供一个回调函数,库的使用方来实现该函数,第三方库在特定的节点调用该回调函数就可以了。 另一点值得注意的是,从图中我们可以看出回调函数和我们的主程序位于同一层中,我们只负责编写该回调函数,但并不是我们来调用的。 最后值得注意的一点就是回调函数被调用的时间节点,回调函数只在某些特定的节点被调用,就像上面说的油条制作完成、接收到网络数据、文件读取完成等,这些都是事件,也就是event,本质上我们编写的回调函数就是用来处理event的,因此从这个角度看回调函数不过就是event handler,因此回调函数天然适用于事件驱动编程event-driven,我们将会在后续文章中再次回到这一主题。 回调的类型 我们已经知道有两种类型的回调,这两种类型的回调区别在于回调函数被调用的时机。 注意,接下来会用到同步和异步的概念,对这两个概念不熟悉的同学可以参考上一盘文章《从小白到高手,你需要理解同步和异步》。 同步回调 这种回调就是通常所说的同步回调synchronous callbacks、也有的将其称为阻塞式回调blocking callbacks,或者什么修饰都没有,就是回调,callback,这是我们最为熟悉的回调方式。 当我们调用某个函数A并以参数的形式传入回调函数后,在A返回之前回调函数会被执行,也就是说我们的主程序会等待回调函数执行完成,这就是所谓的同步回调。 有同步回调就有异步回调。 异步回调 不同于同步回调, 当我们调用某个函数A并以参数的形式传入回调函数后,A函数会立刻返回,也就是说函数A并不会阻塞我们的主程序,一段时间后回调函数开始被执行,此时我们的主程序可能在忙其它任务,回调函数的执行和我们主程序的运行同时进行。 既然我们的主程序和回调函数的执行可以同时发生,因此一般情况下,主程序和回调函数的执行位于不同的线程或者进程中。 这就是所谓的异步回调,asynchronous callbacks,也有的资料将其称为deferred callbacks ,名字很形象,延迟回调。 从上面这两张图中我们也可以看到,异步回调要比同步回调更能充分的利用机器资源,原因就在于在同步模式下主程序会“偷懒”,因为调用其它函数被阻塞而暂停运行,但是异步调用不存在这个问题,主程序会一直运行下去。 因此,异步回调更常见于I/O操作,天然适用于Web服务这种高并发场景。 回调对应的编程思维模式 让我们用简单的几句话来总结一下回调下与常规编程思维模式的不同。 假设我们想处理某项任务,这项任务需要依赖某项服务S,我们可以将任务的处理分为两部分,调用服务S前的部分PA,和调用服务S后的部分PB。 在常规模式下,PA和PB都是服务调用方来执行的,也就是我们自己来执行PA部分,等待服务S返回后再执行PB部分。 但在回调这种方式下就不一样了。 在这种情况下,我们自己来执行PA部分,然后告诉服务S:“等你完成服务后执行PB部分”。 因此我们可以看到,现在一项任务是由不同的模块来协作完成的。 即: 常规模式:调用完S服务后后我去执行X任务, 回调模式:调用完S服务后你接着再去执行X任务, 其中X是服务调用方制定的,区别在于谁来执行。 为什么异步回调越来越重要 在同步模式下,服务调用方会因服务执行而被阻塞暂停执行,这会导致整个线程被阻塞,因此这种编程方式天然不适用于高并发动辄几万几十万的并发连接场景, 针对高并发这一场景,异步其实是更加高效的,原因很简单,你不需要在原地等待,因此从而更好的利用机器资源,而回调函数又是异步下不可或缺的一种机制。 回调地狱,callback hell 有的同学可能认为有了异步回调这种机制应付起一切高并发场景就可以高枕无忧了。 实际上在计算机科学中还没有任何一种可以横扫一切包治百病的技术,现在没有,在可预见的将来也不会有,一切都是妥协的结果。 那么异步回调这种机制有什么问题呢? 实际上我们已经看到了,异步回调这种机制和程序员最熟悉的同步模式不一样,在可理解性上比不过同步,而如果业务逻辑相对复杂,比如我们处理某项任务时不止需要调用一项服务,而是几项甚至十几项,如果这些服务调用都采用异步回调的方式来处理的话,那么很有可能我们就陷入回调地狱中。 举个例子,假设处理某项任务我们需要调用四个服务,每一个服务都需要依赖上一个服务的结果,如果用同步方式来实现的话可能是这样的: a = GetServiceA();b = GetServiceB(a);c = GetServiceC(b);d = GetServiceD(c); 代码很清晰,很容易理解有没有。 我们知道异步回调的方式会更加高效,那么使用异步回调的方式来写将会是什么样的呢? GetServiceA(function(a){ GetServiceB(a, function(b){ GetServiceC(b, function(c){ GetServiceD(c, function(d) { .... }); }); });}); 我想不需要再强调什么了吧,你觉得这两种写法哪个更容易理解,代码更容易维护呢? 博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,一个不得不去重读一边代码;另一个在一旁骂自己为什么当初选择维护这个项目。 异步回调代码稍不留意就会跌到回调陷阱中,那么有没有一种更好的办法既能结合异步回调的高效又能结合同步编码的简单易读呢? 幸运的是,答案是肯定的,我们会在后续文章中详细讲解这一技术。 总结 在这篇文章中,我们从一个实际的例子出发详细讲解了回调函数这种机制的来龙去脉,这是应对高并发、高性能场景的一种极其重要的编码机制,异步加回调可以充分利用机器资源,实际上异步回调最本质上就是事件驱动编程,这是我们接下来要重点讲解的内容。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    C语言与CPP编程 C语言 回调函数

  • 数字化转型案例:源自阿里,中台设计流程及方法

    图4-10所示的业务模型。 《数字化转型的道与术:

    架构师社区 数字化 阿里 中台项目

  • 这个Verilog语法你一定不知道

    动态截取固定长度数据语法,即+:和-:的使用,这两个叫什么符号呢?运算符吗? Verilog比较方便的一个特点就是数据的截取和拼接功能了,截取使用方括号[],拼接使用大括号{},例如  reg [7:0] vect; wire a; wire [3:0] b, wire [5:0] c; assign a = vect[1]; //取其中1Bit assign b[3:0] = vect[7:4];//截取4Bit assing c[5:0] = {a, b[3:0], 1'b1}; //拼接 于是举一反三(zi zuo cong ming),为了实现动态截取固定长度数据的功能,使用软件编程的思维写了如下语句,功能很好理解,根据cnt的值,每次截取vect的5Bit数据。:  reg [7:0] vect; reg [1:0] cnt; wire [4:0] out; assign out = vect[cnt+4:cnt]; 一顿操作猛如虎,编译一看傻如狗。使用ModelSim编译之后,提示有如下语法错误:  ** Error: test.v(10): Range must be bounded by constant expressions. 提示vect的范围必须为常量表达式。也就是必须为,vect[6:2]或vect[7:4],不能是vect[a:0],vect[4:b],或vect[a:b]。额,这该怎么办呢? 既然有这个使用场景,那Verilog在设计之初就应该会考虑到这个应用吧!于是就去翻IEEE的Verilog标准文档,在5.2.1章节发现了一个用法可以实现我这个需求,那就是+:和-:符号,这个用法很少,在大部分关于FPGA和Verilog书籍中都没有提到。 (获取IEEE官方Verilog标准文档IEEE_Verilog_1364_2005.pdf下载,公众号(ID:电子电路开发学习)后台回复【Verilog标准】) 大致意思就是,可以实现动态截取固定长度的数据,基本语法为:  vect[base+:width]或[base-:width] 其中base可以为变量,width必须为常量。 下面来举几个例子来理解这个符号。 有如下定义:  reg [7:0] vect_1; reg [0:7] vect_2; wire [2:0] out; 以下写法分别表示什么呢? vect_1[4+:3]; vect_1[4-:3]; vect_2[4+:3]; vect_2[4-:3]; 分为三步: 1.先看定义。 vect_1[7:0]定义是大端模式,则vect_1[4+:3]和vect_1[4-:3]转换后也一定为大端模式;vect_2[0:7]定义是小端模式,则vect_2[4+:3]和vect_2[4-:3]转换后也一定为小端模式。 2.再看升降序。 其中+:表示升序,-:表示降序 3.看宽度转换。  vect_1[4+:3]表示,起始位为4,宽度为3,**升序**,则vect_1[4+:3] = vect_1[6:4] vect_1[4-:3]表示,起始位为4,宽度为3,**降序**,则vect_1[4-:3] = vect_1[4:2] 同理,  vect_2[4+:3]表示,起始位为4,宽度为3,升序,则vect_2[4+:3] = vect_2[4:6] vect_2[4-:3]表示,起始位为4,宽度为3,降序,则vect_2[4-:3] = vect_2[2:4] ModelSim仿真验证,新建test.v文件: module test;     reg [7:0] vect_1;      reg [0:7] vect_2;     initial     begin         vect_1 = 'b0101_1010;         vect_2 = 'b0101_1010;         $display("vect_1[7:0] = %b, vect_2[0:7] = %b", vect_1, vect_2);         $display("vect_1[4+:3] = %b, vect_1[4-:3] = %b", vect_1[4+:3], vect_1[4-:3]);          $display("vect_2[4+:3] = %b, vect_2[4-:3] = %b", vect_2[4+:3], vect_2[4-:3]);          $stop;     end endmodule 在ModelSim命令窗口输入: //进入到源文件所在文件夹 cd c:/users/whik/desktop/verilog //编译 vlog test.v //仿真 vsim work.test //运行 run -all //运行结果 # vect_1[7:0] = 01011010, vect_2[0:7] = 01011010 # vect_1[4+:3] = 101, vect_1[4-:3] = 110 # vect_2[4+:3] = 101, vect_2[4-:3] = 011 # ** Note: $stop    : test.v(15) #    Time: 0 ps  Iteration: 0 Instance: /test # Break in Module test at test.v line 15 这种语法表示需要注意,前者起始位可以是变量,后者的宽度必须是常量,即vect[idx+:cnt]不符合语法标准,vect[idx+:4]或vect[idx-:4]才符合。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    电子电路开发学习 语法 Verilog

  • C语言编程规范 clean code

    规则并不是完美的,通过禁止在特定情况下有用的特性,可能会对代码实现造成影响。但是我们制定规则的目的“为了大多数程序员可以得到更多的好处”, 如果在团队运作中认为某个规则无法遵循,希望可以共同改进该规则。参考该规范之前,希望您具有相应的C语言基础能力,而不是通过该文档来学习C语言。 总体原则 约定 无论是“规则”还是“建议”,都必须理解该条目这么规定的原因,并努力遵守。 在不违背总体原则,经过充分考虑,有充足的理由的前提下,可以适当违背规范中约定。 例外破坏了代码的一致性,请尽量避免。“规则”的例外应该是极少的。 1 命名 命名被认为是软件开发过程中最困难,也是最重要的事情。 标识符的命名要清晰、明了,有明确含义,符合阅读习惯,容易理解。 总体风格 规则1.1 标识符命名使用驼峰风格 类型 命名风格 函数,结构体类型,枚举类型,联合体类型 大驼峰 变量,函数参数,宏参数,结构体中字段,联合体中成员 小驼峰 宏,常量,枚举值,goto 标签 全大写,下划线分割 常量是指,全局作用域下,const 修饰的基本数据类型、枚举、字符串类型的变量,不包括数组、结构体和联合体。 上表中建议1.1 作用域越大,命名应越精确 例: int GetCount(void); // Bad: 描述不精确 int GetActiveConnectCount(void); // Good 文件命名 文件名命名只允许使用小写字母、数字以及下划线(_)。 文件名应尽量简短、准确、无二义性。 不大小写混用的原因是,不同系统对文件名大小写处理会不同(如 MicroSoft 的 DOS, Windows 系统不区分大小写,但是 Unix / Linux, Mac 系统则默认区分)。 dhcp_user_log.c 坏的命名举例: dhcpuserlog.c: 未分割单词,可读性差 函数命名 函数命名统一使用大驼峰风格。 建议1.3 函数的命名遵循阅读习惯 动作类函数名,可以使用动宾结构。如: 判断型函数,可以用形容词,或加 is: DataReady() // OK IsRunning() // OK JobDone() // OK 变量命名 规则1.2 全局变量应增加 'g_' 前缀,函数内静态变量命名不需要加特殊前缀 注意:常量本质也是全局变量,但如果命名风格是全大写,下划线连接的格式,则不适用当前规则。 函数局部变量的命名,在能够表达相关含义的前提下,应该简短。 更好的写法: int Func(...) { enum PowerBoardStatus status; // Good: 结合上下文,status 已经能明确表达意思 status = GetPowerBoardStatus(slot); if (status == POWER_OFF) { ... } ... } 或一些简单的数学计算函数中的变量: int Mul(int a, int b) { return a * b; } 类型命名采用大驼峰命名风格。 类型包括结构体、联合体、枚举类型名。 通过 typedef 对结构体、联合体、枚举起别名时,尽量使用匿名类型。 若需要指针自嵌套,可以增加 'tag' 前缀或下划线后缀。 typedef struct { // Good: 无须自嵌套,使用匿名结构体 int a; int b; } MyType; // 结构体别名用大驼峰风格 typedef struct tagNode { // Good: 使用 tag 前缀。这里也可以使用 'Node_'代替也可以。 struct tagNode *prev; struct tagNode *next; } Node; // 类型主体用大驼峰风格 宏、枚举值采用全大写,下划线连接的格式。 常量推荐采用全大写,下划线连接风格。作为全局变量,也可以保持与普通全局变量命名风格相同。 这里常量如前文定义,是指基本数据类型、枚举、字符串类型的全局 const 变量。 宏举例: #define PI 3.14 #define MAX(a, b) (((a) < (b)) ? (b) : (a)) #ifdef SOME_DEFINE void Bar(int); #define Foo(a) Bar(a) // 特殊场景,用大驼峰风格命名函数式宏 #else void Foo(int); #endif 非常量举例: // 结构体类型,不符合常量定义 const struct MyType g_myData = { ... }; // OK: 用小驼峰 // 数组类型,不符合常量定义 const int g_xxxBaseValue[4] = { 1, 2, 4, 8 }; // OK: 用小驼峰 int Foo(...) { // 局部作用域,不符合常量定义 const int bufSize = 100; // OK: 用小驼峰 ... } 建议1.5 避免函数式宏中的临时变量命名污染外部作用域 当函数式宏需要定义局部变量时,为了防止跟外部函数中的局部变量有命名冲突。 2 排版格式 建议2.1 行宽不超过 120 个字符 如下场景不宜换行,可以例外: 例: #ifndef XXX_YYY_ZZZ #error Header aaaa/bbbb/cccc/abc.h must only be included after xxxx/yyyy/zzzz/xyz.h #endif 规则2.1 使用空格进行缩进,每次缩进4个空格 大括号 K&R风格 换行时,函数左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放行末。 右大括号独占一行,除非后面跟着同一语句的剩余部分,如 do 语句中的 while,或者 if 语句的 else/else if,或者逗号、分号。 函数声明和定义 在声明和定义函数的时候,函数的返回值类型应该和函数名在同一行。 换行举例: ReturnType FunctionName(ArgType paramName1, ArgType paramName2) // Good:全在同一行 { ... } ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1, // 行宽不满足所有参数,进行换行 ArgType paramName2, // Good:和上一行参数对齐 ArgType paramName3) { ... } ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // 行宽限制,进行换行 ArgType paramName3, ArgType paramName4, ArgType paramName5) // Good: 换行后 4 空格缩进 { ... } ReturnType ReallyReallyReallyReallyLongFunctionName( // 行宽不满足第1个参数,直接换行 ArgType paramName1, ArgType paramName2, ArgType paramName3) // Good: 换行后 4 空格缩进 { ... } 规则2.4 函数调用参数列表换行时保持参数进行合理对齐 换行举例: ReturnType result = FunctionName(paramName1, paramName2); // Good:函数参数放在一行 ReturnType result = FunctionName(paramName1, paramName2, // Good:保持与上方参数对齐 paramName3); ReturnType result = FunctionName(paramName1, paramName2, paramName3, paramName4, paramName5); // Good:参数换行,4 空格缩进 ReturnType result = VeryVeryVeryLongFunctionName( // 行宽不满足第1个参数,直接换行 paramName1, paramName2, paramName3); // 换行后,4 空格缩进 条件语句 我们要求条件语句都需要使用大括号,即便只有一条语句。 理由: 规则2.6 禁止 if/else/else if 写在同一行 如下是正确的写法: if (someConditions) { ... } else { // Good: else 与 if 在不同行 ... } 循环 和条件表达式类似,我们要求for/while循环语句必须加上大括号,即便循环体是空的,或循环语句只有一条。 for (int i = 0; i < someRange; i++) { // Good: 使用了大括号 DoSomething(); } while (condition) { } // Good:循环体是空,使用大括号 while (condition) { continue; // Good:continue 表示空逻辑,使用大括号 } switch语句 switch 语句的缩进风格如下: switch (var) { case 0: // Good: 缩进 DoSomething1(); // Good: 缩进 break; case 1: { // Good: 带大括号格式 DoSomething2(); break; } default: break; } switch (var) { case 0: // Bad: case 未缩进 DoSomething(); break; default: // Bad: default 未缩进 break; } 建议2.2 表达式换行要保持换行的一致性,操作符放行末 例: // 假设下面第一行已经不满足行宽要求 if ((currentValue > MIN) && // Good:换行后,布尔操作符放在行末 (currentValue < MAX)) { DoSomething(); ... } int result = reallyReallyLongVariableName1 + // Good: 加号留在行末 reallyReallyLongVariableName2; 变量赋值 每行最好只有一个变量初始化的语句,更容易阅读和理解。 int maxCount = 10; bool isCompleted = false; 例外情况: 对于多个相关性强的变量定义,且无需初始化时,可以定义在一行,减少重复信息,以便代码更加紧凑。 int i, j; // Good:多变量定义,未初始化,可以写在一行 for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { ... } } 初始化包括结构体、联合体及数组的初始化 结构体或数组初始化时,如果换行应保持4空格缩进。 从可读性角度出发,选择换行点和对齐位置。 // Good: 满足行宽要求时不换行 int arr[4] = { 1, 2, 3, 4 }; // Good: 行宽较长时,换行让可读性更好 const int rank[] = { 16, 16, 16, 16, 32, 32, 32, 32, 64, 64, 64, 64, 32, 32, 32, 32 }; 注意: 规则2.11 结构体和联合体在按成员初始化时,每个成员初始化单独一行 指针 声明或定义指针变量或者返回指针类型函数时,"*" 靠左靠右都可以,但是不要两边都有或者都没有空格。 int *p1; // OK. int* p2; // OK. int*p3; // Bad: 两边都没空格 int * p4; // Bad: 两边都有空格 选择"*"跟随类型风格时,避免一行同时声明带指针的多个变量。 int* a, b; // Bad: 很容易将 b 误理解成指针 注意,任何时候 "*" 不要紧跟 const 或 restrict 关键字。 规则2.12 编译预处理的"#"默认放在行首,嵌套编译预处理语句时,"#"可以进行缩进 空格和空行 水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。总体规则如下: 对于大括号内部两侧的空格,建议如下: 常规情况: int i = 0; // Good:变量初始化时,= 前后应该有空格,分号前面不要留空格 int buf[BUF_SIZE] = {0}; // Good:数组初始化时,大括号内空格可选 int arr[] = { 10, 20 }; // Good: 正常大括号内部两侧建议加空格 指针和取地址 x = *p; // Good:*操作符和指针p之间不加空格 p = &x; // Good:&操作符和变量x之间不加空格 x = r.y; // Good:通过.访问成员变量时不加空格 x = r->y; // Good:通过->访问成员变量时不加空格 循环和条件语句: if (condition) { // Good:if关键字和括号之间加空格,括号内条件语句前后不加空格 ... } else { // Good:else关键字和大括号之间加空格 ... } while (condition) {} // Good:while关键字和括号之间加空格,括号内条件语句前后不加空格 for (int i = 0; i < someRange; ++i) { // Good:for关键字和括号之间加空格,分号之后加空格 ... } switch (var) { // Good: switch 关键字后面有1空格 case 0: // Good:case语句条件和冒号之间不加空格 ... break; ... default: ... break; } 建议2.4 合理安排空行,保持代码紧凑 根据上下内容的相关程度,合理安排空行; 函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行 不使用连续 3 个空行,或更多 大括号内的代码块首行之前和末行之后不要加空行。 ret = DoSomething(); if (ret != OK) { // Bad: 返回值判断应该紧跟函数调用 return -1; } int Foo(void) { ... } int Bar(void) // Bad:最多使用连续2个空行。 { ... } int Foo(void) { DoSomething(); // Bad:大括号内部首尾,不需要空行 ... } 一般的,尽量通过清晰的架构逻辑,好的符号命名来提高代码可读性;需要的时候,才辅以注释说明。 注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,按需注释。 注释跟代码一样重要。 写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。 修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。 注释风格 /*//都是可以的。 按注释的目的和位置,注释可分为不同的类型,如文件头注释、函数头注释、代码注释等等; 同一类型的注释应该保持统一的风格。 注意:本文示例代码中,大量使用 '//' 后置注释只是为了更精确的描述问题,并不代表这种注释风格更好。 文件头注释 规则3.1 文件头注释必须包含版权许可 /* Copyright (c) 2020 XXX you may not use this file except in compliance with the License. http://www.apache.org/licenses/LICENSE-2.0 distributed under the License is distributed on an "AS IS" BASIS, See the License for the specific language governing permissions and // 单行函数头 int Func1(void); // 多行函数头 // 第二行 int Func2(void); 函数尽量通过函数名自注释,按需写函数头注释。 不要写无用、信息冗余的函数头;不要写空有格式的函数头。 例: /* * 返回实际写入的字节数,-1表示写入失败 * 注意,内存 buf 由调用者负责释放 */ int WriteString(char *buf, int len); 上面例子中的问题: 代码注释 规则3.4 注释符与注释内容间要有1空格;右置注释与前面代码至少1空格 使用'/*' '*/' /* 这是单行注释 */ DoSomething(); /* * 这是单/多行注释 * 第二行 */ DoSomething(); 选择并统一使用如下风格之一: int foo = 100; // 放右边的注释 int bar = 200; /* 放右边的注释 */ 当右置的注释超过行宽时,请考虑将注释置于代码上方。 被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入易被忽略的缺陷。 正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。 建议3.1 case语句块结束时如果不加break/return,需要有注释说明(fall-through) 例,显式指明 fall-through: switch (var) { case 0: DoSomething(); /* fall-through */ case 1: DoSomeOtherThing(); ... break; default: DoNothing(); break; } 4 头文件 本章从编程规范的角度总结了一些方法,可用于帮助合理规划头文件。 头文件是模块或文件的对外接口。 头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。 头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。 通常情况下,每个.c文件都有一个相应的.h(并不一定同名),用于放置对外提供的函数声明、宏定义、类型定义等。 如果一个.c文件不需要对外公布任何接口,则其就不应当存在。 示例: foo.h 内容 #ifndef FOO_H #define FOO_H int Foo(void); // Good:头文件中声明对外接口 #endif 内部使用的函数声明,宏、枚举、结构体等定义不应放在头文件中。 本规则反过来并不一定成立。比如: 有些特别简单的头文件,如命令 ID 定义头文件,不需要有对应的.c存在。 同一套接口协议下,有多个实例,由于接口相同且稳定,所以允许出现一个.h对应多个.c文件。 有些产品中使用了 .inc 作为头文件扩展名,这不符合C语言的习惯用法。在使用 .inc 作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。但是从产品的实际代码来看,这一条并没有被遵守,一个 .inc 文件被多个 .c 包含。本规范不提倡将私有定义单独放在头文件中,具体见建议4.1。 头文件包含是一种依赖关系,头文件应向稳定的方向包含。 一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。 除了不稳定的模块依赖于稳定的模块外,更好的方式是每个模块都依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。 在这里,假设接口本身是最稳定的。 头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。 而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。 规则4.2 头文件必须编写#define保护,防止重复包含 定义包含保护符时,应该遵守如下规则: 假定 timer 模块的 timer.h,其目录为 #ifndef TIMER_INCLUDE_TIMER_H #define TIMER_INCLUDE_TIMER_H ... #endif 只能通过包含头文件的方式使用其他模块或文件提供的接口。 通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。 同时这种隐式依赖,容易导致架构腐化。 应该改为: a.c 内容 #include "b.h" // Good: 通过包含头文件的方式使用其他.c提供的接口 void Bar(void) { int i = Foo(); ... } b.c内容 int Foo(void) { // Do something } 规则4.4 禁止在 extern "C" 中包含头文件 extern "C" 通常出现在 C,C++ 混合编程的情况下,在 extern "C" 中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。 b.h 内容 ... #ifdef __cplusplus extern "C" { #endif #include "a.h" void B(void); #ifdef __cplusplus } #endif 按照 a.h 作者的本意,函数 Foo 是一个 C++ 自由函数,其链接规范为 "C++"。但在 b.h 中,由于 extern "C"的内部,函数 Foo 的链接规范被不正确地更改了。 extern "C"修饰。非侵入式的做法是,在 卫语句可以有效的减少 if 相关的嵌套层次。例: 原代码嵌套层数是 3: 使用 int Foo(...) { if (!received) { // Good: 使用'卫语句' return -1; } type = GetMsgType(msg); if (type == UNKNOWN) { return -1; } return DealMsg(..); } 建议5.1 对函数的错误返回码要全面处理 示例: char fileHead[128]; ReadFileHead(fileName, fileHead, sizeof(fileHead)); // Bad: 未检查返回值 DealWithFileHead(fileHead, sizeof(fileHead)); // fileHead 可能无效 注意,当函数返回值被大量的显式(void)忽略掉时,应当考虑函数返回值的设计是否合理。 如果所有调用者都不关注函数返回值时,请将函数设计成建议5.2 设计函数时,优先使用返回值而不是输出参数 使用返回值而不是输出参数,可以提高可读性,并且通常提供相同或更好的性能。 函数名为 GetXxx、FindXxx 或直接名词作函数名的函数,直接返回对应对象,可读性更好。 建议5.3 使用强类型参数,避免使用void* 尽管不同的语言对待强类型和弱类型有自己的观点,但是一般认为c/c++是强类型语言,既然我们使用的语言是强类型的,就应该保持这样的风格。 好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。 使用强类型便于编译器帮我们发现错误,如下代码中注意函数 上述问题有可能很隐晦,不易轻易暴露,从而破坏性更大。 如果明确 void *,则在编译阶段就能发现上述问题。 void FooListAddNode(FooNode *foo) { ListAppend(&g_fooList, &foo->link); } void *入参。 建议5.4 模块内部函数参数的合法性检查,由调用者负责 int SomeProc(...) { int data; bool dataOK = GetData(&data); // 获取数据 if (!dataOK) { // 检查上一步结果,其实也就保证了数据合法 return -1; } DealWithData(data); // 调用数据处理函数 ... } void DealWithData(int data) { if (data < MIN || data > MAX) { // Bad: 调用者已经保证了数据合法性 return; } ... } const 指针参数,将限制函数通过该指针修改所指向对象,使代码更牢固、安全。 注意:指针参数要不要加 const 取决于函数设计,而不是看函数实体内有没有发生“修改对象”的动作。 函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。 看能否拆分函数 看能否将相关参数合在一起,定义结构体 内联函数是C99引入的一种函数优化手段。函数内联能消除函数调用的开销;并得益于内联实现跟调用点代码的合并,编译器有更大的视角,从而完成更多的代码优化。内联函数跟函数式宏比较类似,两者的分析详见建议6.1。 将函数定义成内联一般希望提升性能,但是实际并不一定能提升性能。如果函数体短小,则函数内联可以有效的缩减目标代码的大小,并提升函数执行效率。 反之,函数体比较大,内联展开会导致目标代码的膨胀,特别是当调用点很多时,膨胀得更厉害,反而会降低执行效率。 内联函数规模建议控制在 10 行以内。 规则5.3 被多个源文件调用的内联函数要放在头文件中定义 SomeInlineFunc函数的声明而没有定义。other.c包含inline.h,调用inline.h inline int SomeInlineFunc(void); other.c #include "inline.h" int OtherFunc(void) { int ret = SomeInlineFunc(); } 6 宏 函数式宏是指形如函数的宏(示例代码如下所示),其包含若干条语句来实现某一特定功能。 #define ASSERT(x) do { \ if (!(x)) { \ printk(KERN_EMERG "assertion failed %s: %d: %s\n", \ __FILE__, __LINE__, #x); \ BUG(); \ } \ } while (0) 定义函数式宏前,应考虑能否用函数替代。对于可替代场景,建议用函数替代宏。 函数式宏的缺点如下: #的用法和无处不在的括号,影响可读性。 gcc的 宏在预编译阶段展开后,在其后编译、链接和调试时都不可见;而且包含多行的宏会展开为一行。函数式宏难以调试、难以打断点,不利于定位问题。 #define MAX(a, b) (((a) < (b)) ? (b) : (a)) int Max(int a, int b) { return (a < b) ? b : a; } int TestMacro(void) { unsigned int a = 1; int b = -1; (void)printf("MACRO: max of a(%u) and b(%d) is %d\n", a, b, MAX(a, b)); (void)printf("FUNC : max of a(%u) and b(%d) is %d\n", a, b, Max(a, b)); return 0; } MAX中的b的比较提升为无符号数的比较,结果是a < b。输出结果是: 函数没有宏的上述缺点。但是,函数相比宏,最大的劣势是执行效率不高(增加函数调用的开销和编译器优化的难度)。 为此,C99标准引入了内联函数(gcc在标准之前就引入了内联函数)。 内联函数/函数执行严格的类型检查 内联函数/函数的入参求值只会进行一次 内联函数就地展开,没有函数调用的开销 内联函数比函数优化得更好 规则6.1 定义宏时,宏参数要使用完备的括号 如下所示,是一种错误的写法: #define SUM(a, b) a + b // Bad. 100 / SUM(2, 8)将扩展成 100 / (2 + 8)。 这个问题可以通过将整个表示式加上括号来解决,如下所示: 但是这种改法在下面这种场景又有问题: 1 << (2 + 8)(因为+),跟预期结果 #define SUM(a, b) (a) + (b) // Bad. SUM(2, 8) * 10。扩展后的结果为 (2 + 8) * 10不符。 综上所述,正确的写法如下: 但是要避免滥用括号。如下所示,单独的数字或标识符加括号毫无意义。 #define SOME_CONST 100 // Good: 单独的数字无需括号 #define ANOTHER_CONST (-1) // Good: 负数需要使用括号 #define THE_CONST SOME_CONST // Good: 单独的标识符无需括号 宏参数参与 '#', '##' 操作时,不要加括号 宏参数参与字符串拼接时,不要加括号 宏参数作为独立部分,在赋值(包括+=, -=等)操作的某一边时,无需括号 宏参数作为独立部分,在逗号表达式,函数或宏调用列表中,无需括号 规则6.2 包含多条语句的函数式宏的实现语句必须放在 do-while(0) 中 如下所示的宏是错误的用法(为了说明问题,下面示例代码稍不符规范): // Not Good. #define FOO(x) \ (void)printf("arg is %d\n", (x)); \ DoSomething((x)); 用大括号将 #define FOO(x) { \ (void)printf("arg is %d\n", (x)); \ DoSomething((x)); \ } 正确的写法是用 do-while(0) 把执行体括起来,如下所示: // Good. #define FOO(x) do { \ (void)printf("arg is %d\n", (x)); \ DoSomething((x)); \ } while (0) 包含 break, continue 语句的宏可以例外。使用此类宏务必特别小心。 宏中包含不完整语句时,可以例外。比如用宏封装 for 循环的条件部分。 非多条语句,或单个 if/for/while/switch 语句,可以例外。 由于宏只是文本替换,对于内部多次使用同一个宏参数的函数式宏,将带副作用的表达式作为宏参数传入会导致非预期的结果。 如下所示,宏a++传入导致SQUARE执行后跟预期不符: ((a++) * (a++)),变量7,而不是预期的 b = SQUARE(a); a++; // 结果:a = 6,只自增了一次。 建议6.2 函数式宏定义中慎用 return、goto、continue、break 等改变程序流程的语句 首先,宏封装 return 容易导致过度封装和使用。 如下代码,RETURN_IF宏忽略掉了,从而导致对主干流程的理解有偏差。 #define LOG_AND_RETURN_IF_FAIL(ret, fmt, ...) do { \ if ((ret) != OK) { \ (void)ErrLog(fmt, ##__VA_ARGS__); \ return (ret); \ } \ } while (0) #define RETURN_IF(cond, ret) do { \ if (cond) { \ return (ret); \ } \ } while (0) ret = InitModuleA(a, b, &status); LOG_AND_RETURN_IF_FAIL(ret, "Init module A failed!"); // OK. RETURN_IF(status != READY, ERR_NOT_READY); // Bad: 重要逻辑不明显 ret = InitModuleB(c); LOG_AND_RETURN_IF_FAIL(ret, "Init module B failed!"); // OK. 如果 CHECK_PTR会直接返回,而没有释放 CHECK_PTR宏命名也不好,宏名只反映了检查动作,没有指明结果。只有看了宏实现才知道指针为空时返回失败。 综上所述:不推荐宏定义中封装 return、goto、continue、break 等改变程序流程的语句; 对于返回值判断等异常处理场景可以例外。 注意: 包含 return、goto、continue、break 等改变流程语句的宏命名,务必要体现对应关键字。 建议6.3 函数式宏不超过10行(非空非注释) 函数式宏本身的一大问题是比函数更难以调试和定位,特别是宏过长,调试和定位的难度更大。 而且宏扩展会导致目标代码的膨胀。建议函数式宏不要超过10行。 7 变量 在C语言编码中,除了函数,最重要的就是变量。 变量在使用时,应始终遵循“职责单一”原则。 按作用域区分,变量可分为全局变量和局部变量。 全局变量 尽量不用或少用全局变量。 在程序设计中,全局变量是在所有作用域都可访问的变量。通常,使用不必要的全局变量被认为是坏习惯。 使用全局变量的缺点: 破坏函数的独立性和可移植性,使函数对全局变量产生依赖,存在耦合; 在并发编程环境中,使用全局变量会破坏函数的可重入性,需要增加额外的同步保护处理才能确保数据安全。 如不可避免,对全局变量的读写应集中封装。 规则7.1 模块间,禁止使用全局变量作接口 全局变量是模块内部的具体实现,不推荐但允许跨文件使用,但禁止作为模块接口暴露出去。 对全局变量的使用应该尽量集中,如果本模块的数据需要对外部模块开放,应提供对应函数接口。 局部变量 规则7.2 严禁使用未经初始化的变量 这里的变量,指的是局部动态变量,并且还包括内存堆上申请的内存块。 因为他们的初始值都是不可预料的,所以禁止未经有效初始化就直接读取其值。 如果有不同分支,要确保所有分支都得到初始化后才能使用: void Foo(...) { int data; if (...) { data = 100; } Bar(data); // Bad: 部分分支该值未初始化 ... } Warning 530: Symbol 'data' (line ...) not initialized Warning 644: Variable 'data' (line ...) may not have been initialized 如果没有确定的初始值,而仍然进行初始化,不仅不简洁,反而不安全,可能会引入更难发现的问题。 对于后续有条件赋值的变量,可以在定义时初始化成默认值 char *buf = NULL; // Good: 这里用 NULL 代表默认值 if (condition) { buf = malloc(MEM_SIZE); } ... if (buf != NULL) { // 判断是否申请过内存 free(buf); } 无效初始化,隐藏更大问题的反例: void Foo(...) { int data = 0; // Bad: 习惯性的进行初始化 UseData(data); // 使用数据,本应该写在获取数据后面 data = GetData(...); // 获取数据 ... } 因此,应该写简洁的代码,对变量或内存块进行正确、必要的初始化。 例外: 遵从“安全规范”要求,指针变量、表示资源描述符的变量、BOOL变量不作要求。 所谓魔鬼数字即看不懂、难以理解的数字。 魔鬼数字并非一个非黑即白的概念,看不懂也有程度,需要结合代码上下文和业务相关知识来判断 type = 12;就看不懂,但 status = 0;并不能表达是什么状态。 解决途径: 对于单点使用的数字,可以增加注释说明 对于多处使用的数字,必须定义宏或const 变量,并通过符号命名自注释。 禁止出现下列情况: 没有通过符号来解释数字含义,如 #define XX_TIMER_INTERVAL_300MS 300 8 编程实践 表达式 建议8.1 表达式的比较,应当遵循左侧倾向于变化、右侧倾向于不变的原则 当变量与常量比较时,如果常量放左边,如 if (MAX > v)更是难于理解。 应当按人的正常阅读、表达习惯,将常量放右边。写成如下方式: 也有特殊情况,如:if (v = MAX)会有编译告警,其他静态检查工具也会报错。让工具去解决笔误问题,代码要符合可读性第一。 含有变量自增或自减运算的表达式中,如果再引用该变量,其结果在C标准中未明确定义。各个编译器或者同一个编译器不同版本实现可能会不一致。 为了更好的可移植性,不应该对标准未定义的运算次序做任何假设。 示例: x = b[i] + i++; // Bad: b[i]运算跟 i++,先后顺序并不明确。 函数参数: Func(i++, i); // Bad: 传递第2个参数时,不确定自增运算有没有发生 建议8.2 用括号明确表达式的操作顺序,避免过分依赖默认优先级 当表达式包含不常用,优先级易混淆的操作符时,推荐使用括号,比如位操作符: c = (a & 0xFF) + b; /* 涉及位操作符,需要括号 */ 规则8.2 switch语句要有default分支 特例: 如果switch条件变量是枚举类型,并且 case 分支覆盖了所有取值,则加上default分支处理有些多余。 现代编译器都具备检查是否在switch语句中遗漏了某些枚举值的case分支的能力,会有相应的warning提示。 enum Color { RED, BLUE }; // 因为switch条件变量是枚举值,这里可以不用加default处理分支 switch (color) { case RED: DoRedThing(); break; case BLUE: DoBlueThing(); ... break; } goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。使用时,也只允许跳转到本函数goto语句之后的语句。 示例: // Good: 使用 goto 实现单点返回 int SomeInitFunc(void) { void *p1; void *p2 = NULL; void *p3 = NULL; p1 = malloc(MEM_LEN); if (p1 == NULL) { goto EXIT; } p2 = malloc(MEM_LEN); if (p2 == NULL) { goto EXIT; } p3 = malloc(MEM_LEN); if (p3 == NULL) { goto EXIT; } DoSomething(p1, p2, p3); return 0; // OK. EXIT: if (p3 != NULL) { free(p3); } if (p2 != NULL) { free(p2); } if (p1 != NULL) { free(p1); } return -1; // Failed! } 建议8.4 尽量减少没有必要的数据类型默认转换与强制转换 如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。 char ch; unsigned short int exam; ch = -1; exam = ch; // Bad: 编译器不产生告警,此时exam为0xFFFF。

    C语言与CPP编程 编程规范 C语言

  • 三万字,Spark学习笔记

    Spark 基础 Spark特性 Spark使用简练优雅的Scala语言编写,基于Scala提供了交互式编程体验,同时提供多种方便易用的API。Spark遵循“一个软件栈满足不同应用场景”的设计理念,逐渐形成了一套完整的生态系统(包括 Spark提供内存计算框架、SQL即席查询(Spark  SQL)、流式计算(Spark  Streaming)、机器学习(MLlib)、图计算(Graph X)等),Spark可以部署在yarn资源管理器上,提供一站式大数据解决方案,可以同时支持批处理、流处理、交互式查询。 MapReduce计算模型延迟高,无法胜任实时、快速计算的需求,因而只适用于离线场景,Spark借鉴MapReduce计算模式,但与之相比有以下几个优势(快、易用、全面): Spark提供更多种数据集操作类型,编程模型比MapReduce更加灵活; Spark提供内存计算,将计算结果直接放在内存中,减少了迭代计算的IO开销,有更高效的运算效率。 Spark基于DAG的任务调度执行机制,迭代效率更高;在实际开发中MapReduce需要编写很多底层代码,不够高效,Spark提供了多种高层次、简洁的API实现相同功能的应用程序,实现代码量比MapReduce少很多。 Spark作为计算框架只是取代了Hadoop生态系统中的MapReduce计算框架,它任需要HDFS来实现数据的分布式存储,Hadoop中的其他组件依然在企业大数据系统中发挥着重要作用。 Spark的不足:虽然Spark很快,但现在在生产环境中仍然不尽人意,无论扩展性、稳定性、管理性等方面都需要进一步增强;同时Spark在流处理领域能力有限,如果要实现亚秒级或大容量的数据获取或处理需要其他流处理产品。 Cloudera旨在让Spark流数据技术适用于80%的使用场合,就考虑到这一缺陷,在实时分析(而非简单数据过滤或分发)场景中,很多以前使用S4或Storm等流式处理引擎的实现已经逐渐被Kafka+Spark Streaming代替; Hadoop现在分三块HDFS/MR/YARN,Spark的流行将逐渐让MapReduce、Tez走进博物馆;Spark只是作为一个计算引擎比MR的性能要好,但它的存储和调度框架还是依赖于HDFS/YARN,Spark也有自己的调度框架,但不成熟,基本不可商用。 Spark部署(on Yarn) YARN实现了一个集群多个框架”,即在一个集群上部署一个统一的资源调度管理框架,并部署其他各种计算框架,YARN为这些计算框架提供统一的资源调度管理服务,并且能够根据各种计算框架的负载需求调整各自占用的资源,实现集群资源共享和资源弹性收缩; 并且,YARN实现集群上的不同应用负载混搭,有效提高了集群的利用率;不同计算框架可以共享底层存储,避免了数据集跨集群移动 ; 这里使用Spark on Yarn 模式部署,配置on yarn模式只需要修改很少配置,也不用使用启动spark集群命令,需要提交任务时候须指定在yarn上。 Spark运行需要Scala语言,须下载Scala和Spark并解压到家目录,设置当前用户的环境变量(~/.bash_profile),增加SCALA_HOME和SPARK_HOME路径并立即生效;启动scala命令和spark-shell命令验证是否成功;Spark的配置文件修改按照官网教程不好理解,这里完成的配置参照博客及调试。 Spark的需要修改两个配置文件:spark-env.sh和spark-default.conf,前者需要指明Hadoop的hdfs和yarn的配置文件路径及Spark.master.host地址,后者需要指明jar包地址; spark-env.sh配置文件修改如下: scala> val lines = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt") 可以调用SparkContext的parallelize方法,在Driver中一个已经存在的集合(数组)上创建。 scala>  val  lines =sc.textFile(file:///usr/local/spark/mycode/rdd/word.txt) scala>  val  linesWithSpark=lines.filter(line => line.contains("Spark")) map(func)操作:map(func)操作将每个元素传递到函数func中,并将结果返回为一个新的数据集 scala> val  lines = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt") scala> val  words=lines.flatMap(line => line.split(" ")) groupByKey()操作:应用于(K,V)键值对的数据集时,返回一个新的(K, Iterable)形式的数据集; reduceByKey(func)操作:应用于(K,V)键值对的数据集返回新(K, V)形式数据集,其中每个值是将每个key传递到函数func中进行聚合后得到的结果: 行动操作是真正触发计算的地方。Spark程序执行到行动操作时,才会执行真正的计算,这就是惰性机制,“惰性机制”是指,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到行动操作时,才会触发“从头到尾”的真正的计算,常用的行动操作: RDD持久 Spark RDD采用惰性求值的机制,但是每次遇到行动操作都会从头开始执行计算,每次调用行动操作都会触发一次从头开始的计算,这对于迭代计算而言代价是很大的,迭代计算经常需要多次重复使用同一组数据: scala> val  array = Array(1,2,3,4,5) scala> val  rdd = sc.parallelize(array,2)  //设置两个分区 reparititon方法重新设置分区个数:通过转换操作得到新 RDD 时,直接调用 repartition 方法即可,如: val test = (x:Int) => x + 1 Scala高阶函数 Scala使用术语“高阶函数”来表示那些把函数作为参数或函数作为返回结果的方法和函数。比如常见的有map,filter,reduce等函数,它们可以接受一个函数作为参数。 Scala闭包 Scala中的闭包指的是当函数的变量超出它的有效作用域的时候,还能对函数内部的变量进行访问;Scala中的闭包捕获到的是变量的本身而不仅仅是变量的数值,当自由变量发生变化时,Scala中的闭包能够捕获到这个变化;如果自由变量在闭包内部发生变化,也会反映到函数外面定义的自由变量的数值。 Scala部分应用函数 部分应用函数只是在“已有函数”的基础上,提供部分默认参数,未提供默认参数的地方使用下划线替代,从而创建出一个“函数值”,在使用这个函数值(部分应用函数)的时候,只需提供下划线部分对应的参数即可;部分应用函数本质上是一种值类型的表达式,在使用的时候不需要提供所有的参数,只需要提供部分参数。 Scala柯里化函数 scala中的柯里化指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程,新的函数返回一个以原有第二个参数作为参数的函数; {"name":"Michael"} {"name":"Andy", "age":30} {"name":"Justin", "age":19} 读取代码: val conf = new SparkConf().setAppName(appName).setMaster(master); val ssc = new StreamingContext(conf, Seconds(1)); appName是用来在Spark UI上显示的应用名称。master是Spark、Mesos或Yarn集群的URL,或者是local[*]。batch interval可以根据你的应用程序的延迟要求以及可用的集群资源情况来设置。 SparkContext创建: val stream1: DStream[String, String] = ... val stream2: DStream[String, String] = ... val joinedStream = stream1.join(stream2) Dstream的输出 输出操作允许将DStream的数据推送到外部系统,如数据库或files,由于输出操作触发所有DStream转换的实际执行(类似于RDD的操作),并允许外部系统使用转换后的数据,输出操作有以下几种: 在输出DStream中,Dstream.foreachRDD是一个功能强大的原语. DataFrame和SQL操作 可以轻松地对流数据使用DataFrames和SQL操作,但必须使用StreamingContext正在用的SparkContext创建SparkSession。下面例子使用DataFrames和SQL生成单词计数。每个RDD都转换为DataFrame,注册为临时表后使用SQL进行查询: import org.apache.spark._ import org.apache.spark.streaming._ val conf = new SparkConf().setAppName("TestDStream").setMaster("local[2]") val ssc = new StreamingContext(conf, Seconds(1)) 文件流 文件流可以读取本机文件,也可以读取读取HDFS上文件,如果部署的on yarn模式的Spark,则启动spark-shell默认读取HDFS上对应的: hdfs:xxxx/user/xx/ 下的文件; scala> import org.apache.spark._ scala> import org.apache.spark.streaming._ scala> import org.apache.spark.streaming.kafka._ scala> val ssc = new StreamingContext(sc, Seconds(5)) scala> ssc.checkpoint("hdfs://usr/spark/kafka/checkpoint") scala> val zkQuorum = "172.22.241.186:2181" scala> val group = "test-consumer-group" scala> val topics = "yzg_spark" scala> val numThreads = 1 scala> val topicMap =topics.split(",").map((_,numThreads.toInt)).toMap scala> val lineMap = KafkaUtils.createStream(ssc,zkQuorum,group,topicMap) scala> val pair = lineMap.map(_._2).flatMap(_.split(" ")).map((_,1)) scala> val wordCounts = pair.reduceByKeyAndWindow(_ + _,_ -_,Minutes(2),Seconds(10),2) scala> wordCounts.print scala> ssc.start updateStateByKey操作 当Spark Streaming需要跨批次间维护状态时,就必须使用updateStateByKey操作。以词频统计为例,对于有状态转换操作而言,当前批次的词频统计是在之前批次的词频统计结果的基础上进行不断累加,所以最终统计得到的词频是所有批次的单词总的词频统计结果。 def Assign[K, V](       topicPartitions: Iterable[TopicPartition],       kafkaParams: collection.Map[String, Object],       offsets: collection.Map[TopicPartition, Long]) 2、ConsumerStrategies.Subscribe:允许消费订阅固定的主题集合; 3、ConsumerStrategies.SubscribePattern:使用正则表达式指定感兴趣的主题集合。 Spark Streaming开发 IDEA作为常用的开发工具使用maven进行依赖包的统一管理,配置Scala的开发环境,进行Spark Streaming的API开发; 下载并破解IDEA,并加入汉化的包到lib,重启生效; 在IDEA中导入离线的Scala插件:需要确保当前win主机上已经下载安装Scala并设置环境变量,首先下载IDEA的Scala插件,无须解压,然后将其添加到IDEA中,具体为new---setting--plugins--"输入scala"--install plugin from disk; Maven快捷键

    架构师社区 Spark Scala RDD

  • 聊聊中间件开发

    最近频繁地在跟实习生候选人打交道,每次新接触一个候选人,都要花上一定的时间去介绍我们团队,候选人问的最多的一个问题就是「中间件部门一般是干嘛的?」,恰好我之前也接触过一些想从事中间件开发的小伙伴,问过我「现在转行做中间件开发还来得及吗?」诸如此类的问题,索性就写一篇文章,聊聊我个人这些年做中间件开发的感受吧。 什么是中间件开发? 我大四实习时,在一个 20 多人的软件开发团队第一次接触了中间件,当时项目的架构师引入了微博开源的 RPC 框架 Motan,借助于这个框架,我们迅速构建起了一个基于微服务架构的内部电商系统。接着在项目中,由于业务需求,我们又引入了 ActiveMQ...在这个阶段,我已经在使用中间件了,但似乎没有接触到中间件开发,更多的是使用层面上的接触。 我毕业后的第一份工作,公司有几百号研发,当时的 leader 看我对中间件比较感兴趣,有意把我分配在了基础架构团队,我第一次真正意义上成为了一名”中间件研发“,平时主要的工作,是基于开源的 Kong 和 Dubbo,进行一些内部的改造,以提供给业务团队更好地使用。这个阶段,做的事还是比较杂的,业务团队对某些中间件有定制化的需求,都需要去了解这个中间件,熟悉源码。这段时间,也是我成长最快的一个时期,我是在这个期间学会了 Docker、Neo4j、Kong 等以前从来没接触过的技术,并且更加深入地了解 Dubbo 这类 RPC 框架的原理。可能坐在我旁边的就是一个订单部门的同学,抛了一个功能点让我来改造。 现在,我供职于阿里云云原生中间件,相较于上一份中间件研发工作,阿里云这类互联网大公司,任意一个中间件都有少则数人,多则数十人负责,中间件部门和业务部门之间也有着明确的界限。在这里,中间件团队的职责可以细分为三个方向: 中间件团队会被业务团队的需求所驱动,为集团内部提供定制化的解决方案,俗称「自研」。所以你可能并不了解到 HSF、Diamond 这些阿里内部的中间件。 中间件团队会从事开源,花费大量的精力提升中间件的极致性能,提升开源影响力,引领技术先进性。这部分中间件则比较为人所熟知,例如 Dubbo、Spring Cloud Alibaba、RocketMQ、Nacos。 中间件会在阿里云输出商业化产品,相比开源产品,提供更高的 SLA、更强大的功能、更友好的交互。这部分商业化产品诸如:微服务引擎 MSE、消息队列 RocketMQ、分布式应用链路追踪 ARMS。 我的这三段经历,正好反应了不同规模的公司对中间件开发的不同需求。小公司使用中间件,例如 RPC、MQ、缓存等,基本是由业务开发人员自己维护的。但如果后台研发达到数百人,基本就会组建自己的中间件团队,或者选择使用阿里云等云厂商提供的中间件产品。 中间件开发和业务开发的区别 在我看来,中间件开发和业务开发并没有什么高下之分,非要说区别的话,有点像游戏里面的不同转职,有人选择的是魔法师,有人选择的是战士。在职场的练级过程中,每个人的总技能点数是一样的,业务开发有点向全能战士的方向去发展,各个点都涉猎一点,但每个方向能够分配到技能点自然就少了;中间件开发就像《因为太怕痛,就全点防御力了》里面的主角,把技能点都分配到了一个方向。 假设你是在一个小公司工作,现阶段并没有专门的中间件团队,大家都是业务开发,此时我们做一个假设:公司即将成立一个中间件团队或者叫基础架构部,那么会是哪一类人容易被选中呢?一定是那些技术功底扎实,对中间件感兴趣,研究过源码的人。这个假设并非凭空捏造,很多互联网公司的中间件团队都是这么一点点壮大起来的。我想说什么呢?业务开发和中间件开发一开始并没有明确的界限,因此,不用顾忌你现在是不是在从事业务开发,只要你对中间件感兴趣,有过源码级别的研究,就可以成为一个中间件开发。 中间件开发需要具备哪些素质? 越是大的公司,大的中间件团队,责任分工就越垂直。基本在大公司,一个中间件开发可能花几年时间在某一个垂直方向深耕。以下是一些常见的中间件方向,当然,这个分类在各个公司可能由于组织架构的原因,略有不同。 微服务治理。例如 RPC 相关中间件,注册中心,配置中心,限流熔断,链路追踪等等。开源产品例如:Dubbo、SpringCloud、Nacos、Zookeeper、Sentinel、Hystrix、Zipkin。 消息队列。微服务一般强调的同步通信,消息队列单独列出来,主要是因为其异步的机制。开源产品例如:RocketMQ、Kafka 存储中间件。例如缓存,数据库等等,例如 Mysql、Redis。值得一提的是,由于存储相关的系统一般都非常复杂,特别是在分布式存储领域,体系更是繁杂,在阿里内部一般将数据库和缓存这种存储类型的产品当成是和中间件平级的存在,所以如果有人说 Mysql 和 Redis 不是中间件,也没有啥好争吵的。 存储 Proxy。典型的如 ShardingSphere。 网关。例如 Spring Cloud Gateway、Kong、Nginx。 ServiceMesh。Envoy、Istio 在阿里这边也被划分在中间件部门。 其实可以发现,中间件其实并没有一个明确的定义,到底哪些开源产品可以是中间件,哪些又不是。 列举完这些典型的中间件,继续讨论这一节的主题,一个中间件开发者需要具备哪些素质? 语言基础。从 Java 程序员的角度,基础通常就是:集合,并发,JVM,常用工具类。 操作系统基础。中间件开发人员经常和操作系统打交道,所以计算机基础也必不可少,我列举一些关键词,供各位参考 文件 IO。例如 pageCache,mmap,direct IO 等概念。 进程线程。例如 green thread,协程等概念。 内存/CPU。例如 cgroup,cache line,bound core 等概念。 网络基础。可以发现上述的每一个中间件都离不开网络通信,一定需要对 TCP 和 HTTP 的原理烂熟于心,框架层面需要熟悉 NIO、Netty、GRPC、HttpClient 等常用的网络框架/工具。 分布式相关知识。了解 CAP, paxos,raft,zab,2pc/3pc,base 等理论知识,例如我看到有一些应届生简历中的一个项目经历就很有意思:根据 MIT 课程 Lab 实现 Raft 协议的 POC。 源码阅读能力。我认为源码阅读能力是一个中间件开发者必备的素质,网上经常能看到各种源码分析文章,通过阅读开源中间件的源码,可以借鉴别人的设计理念,提升自身的编码水准。 保持技术热情,拥抱变化。中间件技术日新月异,可能一个打败一个中间件的不是同类的产品,而是整体的大环境,例如近几年云原生大火,所有中间件几乎都在拥抱变化,主动向 K8s 对齐,在这个大背景下,就需要中间件开发者拥有 K8s 的基础认知能力,熟悉 pod、service、deployment、statefulSet、operator 这些 K8s 的基本概念。 如何成为中间件开发 看完上述这些要求,可能会有一些同学开始咋舌了,但其实也没那么可怕,这跟最早学习 Java 基础是一样的,很多东西一开始没有接触过,觉得很难,但熟悉之后会发现,也就那么回事。 我的技术交流群中经常会有同学抱怨说,平时只能接触到 CRUD,根本接触不到这些”高大上“的技术。我想说的是,机会都是自己找的。我这里有几个切实可行的建议: 参与开源社区的项目,贡献代码。了解一个中间件最好的方式就是贡献它,带着问题有的放矢地研究源码,是我比较推荐的方式,你所需要做的是寻找一个合适的 issue,解决它。不断重复这个过程,你其实就是一个中间件开发了! 多动手做实验。很多上面提到的中间件开发应用的素质都可以通过动手做实验的方式来学习,例如动手实现一个简易的 RPC 框架,实现一个 Raft 协议的 POC,通过 benchmark 对比 FileChannel 和 MMAP 的性能,相信我,这比看书、看视频、看博客有用的多的多。 参与中间件挑战赛。最早是阿里会举办一年一度的中间件性能挑战赛,后来也有一些其他公司如华为开始效仿这类比赛,参与这些挑战赛也可以积累非常多的经验,同时你还可以借着组队,结交非常多的朋友。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    架构师社区 开发 中间件 业务开发

  • 微服务“大门”如何选择?

    使用微服务网关作为微服务面向客户端的单一入口,是目前普遍采用的微服务架构模式。企业组织通过良好定义的 API 将内部系统向内部和外部用户公开,通常都会采用 API (微服务)网关来处理横向的关注点,包括访问控制、速率限制、负载均衡等等,来实现安全可控的 API 开放。广泛实践的微服务架构中,似乎有很多产品具有这些能力,那如何更好的根据我们的业务场景选择最合适自己的“大门”呢? 性能选择-Nginx Nginx 应该是 Web 应用的标配组件,使用场景包括负载均衡、反向代理、代理缓存等。Nginx 的内核的设计非常微小和简洁,实现的功能也相对简单,仅仅通过查找配置文件与请求进行 URL 匹配,用于启动不同的模块去完成相应的工作。 Nginx 在启动后,会有一个 Master 进程和多个 Worker 进程,Master 进程和 Worker 进程之间是通过进程间通信进行交互的。Worker 工作进程的阻塞点是在像 select()、epoll_wait() 等这样的 I/O 多路复用函数调用处,以等待发生数据可读 / 写事件。Nginx 采用了异步非阻塞的方式来处理请求,是可以同时处理成千上万个请求的。 服务亲和-Zuul & Sping Cloud Gateway Zuul 是 Netflix 开源的微服务网关组件,其可以配合 Eureka、Nacos 等开源产品实现不错的服务发现能力,同时集成Ribbon、Hystrix 或 Sentinel 等组件实现对整个链路的流控。 Zuul 的核心是一系列的过滤器,这些过滤器许多功能,例如: • 鉴权与访问控制: 识别每次请求的合法性,并拒绝那些没有在授权列表中的来源请求。 • 审计与监控:记录每次请求/响应的内容,以及 RT/错误率等,从而分析出 API 的动态质量、安全情况。 • 动态路由负载:动态地将请求路由分流到不同的服务、应用或者集群。 • 统一上下文:在请求转发前根据业务需求设置公共的上下文信息向后传递。 • Mock 响应:针对简单请求可以组合配置中心,直接在网关层直接响应,从而避免其转发到内部。 上面提及的这些特性是 Nginx 所没有的,Netflix 公司研发 Zuul 是为了解决微服务场景的诸多问题,而不仅仅是做一个类似于 nginx 的反向代理。 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0 以上版本中,没有对新版本的 Zuul 2.0 以上最新高性能版本进行集成,仍然还是使用的 Zuul 2.0 之前的非 Reactor 模式的老版本。而为了提升网关的性能,SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。 Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。 两者兼得-Kong Kong 是一款基于 Nginx_Lua 模块写的高可用服务网关,由于 Kong 是基于 Nginx 的,所以可以水平扩展多个 Kong 服务器。通过前置的负载均衡配置把请求均匀地分发到各个 Server,来应对大批量的网络请求。 Kong 主要有三个组件: • Kong Server:基于 nginx 的服务器,用来接收 API 请求。 • Apache Cassandra/PostgreSQL:用来存储操作数据。 • Kong dashboard:官方推荐 UI 管理工具,当然,也可以使用 restfull 方式管理 admin api。 Kong 采用插件机制进行功能定制,插件集(可以是 0 或 N 个)在 API 请求响应循环的生命周期中被执行。插件使用 Lua 编写,基础功能包括:HTTP 基本认证、密钥认证、CORS(Cross-Origin Resource Sharing,跨域资源共享)、TCP、UDP、文件日志、API 请求限流、请求转发以及 Nginx 监控等。 Kong 网关具有以下的特性: • 可扩展性:通过简单地添加更多的服务器,可以轻松地进行横向扩展,这相较于 nginx 能让你省心不少,但可能相对于 Zuul 稍稍弱些; • 模块化:可以通过添加新的插件进行扩展,这些插件可以通过 RESTful Admin API 轻松配置; • 在任何基础架构上运行:Kong 网关可以在任何地方都能运行,可以在云或内部网络环境中部署 Kong。 自建 OR 云产品 但是!有过使用经验的同学应该会发现,真正用起来我们还需要更多的服务发现能力、更全面的监控可观测能力、更高的稳定性保障,那么到底是自己手工打造还是购买成本更合适呢?我们先来看下自建和云产品的比较: 自建 VS 托管云产品 能力 自建 托管 弹性扩缩容 需要自建维护K8s 产品控制台直接操作 多种开发语言 Kong 使用 Lua 来扩展(可能还需要掌握 nginx ),Zuul 用 Java 实现 全都要,不用关注 服务发现 Kong 不支持,Zuul 支持部分需要开发 支持 Eureka、Nacos 等 监控大盘 需要一个团队支持,且要二次开发 控制台一键创建 协议转换 需要服务框架团队开发 支持 Http、Dubbo 等 微服务治理 需要中间件团队支持 集成 MSE 支持无损上下线、金丝雀发布、标签路由、离群实例摘除等 对比可以看到,这些能力使用托管的 MSE 微服务网关就相当于省去了一个运维团队、一个中间件团队、一个多语言开发能力的研发团队。现在,您只要结合自己的业务场景选择合适的引擎即可: 接入层场景选择 Kong,性能高 SSL 安全能力匹配; 业务分支选择 Zuul,自定义扩展方便还有很强的服务发现能力; 或者如果你是 Spring Cloud 技术体系,那么赶紧把 Spring Cloud Gateway 加入你的全家桶吧。 云产品的各引擎对比 更多内容了解: https://www.aliyun.com/product/aliware/mse 总结 微服务网关作为微服务流量的“大门”,它的稳定性、安全性、功能完备性上的要求是要远远高于我们业务自身的,我们往往需要投入非常大的人力和时间在他的运维和开发上,并还未必能保证有非常好的效果;BaaS 化的服务型(全托管)云产品,帮助我们的用户坚持开源技术栈这一大方向不变的基础上,更稳定、更便捷、更专注的为我们业务保驾护航。

    架构师社区 网关 微服务 Nginx

发布文章