这些数据科学家必备的技能,你拥有哪些?

摘要: 想要成为数据科学家,没有这些技能怎么能行?1.教育背景88%的数据科学家受过高等教育且拥有硕士学位,其中46%拥有博士学位。虽然有些人是例外,但通常需要非常强大的教育背景才能拥有成为数据科学家所必需的知识深度。要想成为数据科学家,你可以先获得计算机科学、社会科学、物理科学和统计学的学士学位,最常见的是数学和统计学(32%),其次是计算机科学(19%)和工程学(16%),任何这些课程的学位都可以为你提供处理和分析大数据所需的技能。完成学位课程后,大多数数据科学家还进行在线培训,例如学习如何使用Hadoop或大数据查询等特殊技能。除课堂学习外,你还可以通过构建应用程序或探索数据分析来练习你在课堂上学到的知识,以便你了解更多信息。2.R编程对于数据科学家来说,R的至少是所有分析工具最应该深入了解工具,因为R专为满足数据科学需求而设计。你可以使用R来解决数据科学中遇到的任何问题,事实上,43%的数据科学家正在使用R来解决统计问题。然而,R有一个陡峭的学习曲线。如果你已经掌握了某种编程语言,那么就会更难学习。不过不要担心,互联网上还有很多资源可以帮助你开始使用R,例如Simplilearn的R编程语言数据科学培训,它是有抱负的数据科学家的重要资源。技术技能:计算机科学3.Python编码Python是适合数据科学家学习的一种优秀编程语言,也是我在数据科学角色中看到的最常见的编码语言,其中还有Java,Perl或C/C ++。O’Reilly曾经调查过数据科学家,调查的受访者中有40%使用Python作为他们的主要编程语言。由于python的多功能性,你可以将其用于数据科学过程中涉及的几乎所有步骤。它可以采用各种格式的数据,同时可以轻松地将SQL表导入代码中,并且还允许你创建数据集,这样你就可以在Google上找到所需的任何类型的数据集。4.Hadoop平台虽然这个不是硬性要求,但在许多情况下它是非常有用的,拥有Hive或Pig的经验也是一个很好的加分项。CrowdFlower对3490个LinkedIn上数据科学工作者进行的一项研究发现Apache Hadoop被评为数据科学家第二重要技能。作为数据科学家,你可能会遇到这样的情况,即你拥有的数据量超过系统内存或需要将数据发送到不同的服务器,这时Hadoop就能发挥其作用了。你可以使用Hadoop快速将数据传输到各种系统上的点。同时你还可以使用Hadoop进行数据探索、数据过滤、数据采样和汇总。5.SQL数据库/编码尽管NoSQL和Hadoop已经成为数据科学的一个重要组成部分,但大多数人还是希望能够在SQL中编写和执行复杂查询。SQL(结构化查询语言)是一种编程语言,可以帮助你执行添加,删除和从数据库中提取数据等操作。它还可以帮助你执行分析功能和转换数据库结构。作为数据科学家,你需要精通SQL,因为SQL可以专门用于帮助你访问和处理数据。当你使用它来查询数据库时你会发现,它简洁的命令可以帮助你节省时间并减少执行困难查询所需的编程量。学习SQL将帮助你更好地理解关系数据库并提升你作为数据科学家的形象。6.Apache SparkApache Spark正在成为全球最受欢迎的大数据技术。它就像Hadoop一样是一个大数据计算框架,唯一的区别是Spark比Hadoop更快。这是因为Hadoop需要读取和写入磁盘,这使得速度变慢,但Spark将其计算缓存在内存中。Apache Spark专为数据科学而设计,它可以帮助更快地运行复杂的算法。它还有助于数据科学家处理复杂的非结构化数据集,你可以在一台机器或一组机器上使用它。Apache spark使数据科学家能够防止数据科学中的数据丢失。Apache Spark的优势在于其速度和平台,这使得开展数据科学项目变得容易。借助Apache spark,你可以执行从数据采集到分布式计算的分析。7.机器学习和AI大量数据科学家并不精通机器学习领域和技术,这包括神经网络,强化学习,对抗性学习等。如果你想从其他数据科学家中脱颖而出,你需要了解机器学习技术,如监督机器学习、决策树、逻辑回归等。这些技能将帮助你解决基于主要组织结果预测的不同数据科学问题。数据科学需要应用于机器学习的不同领域。Kaggle在其中一项调查中发现,一小部分数据专业人员具备先进的机器学习技能,如监督机器学习、无监督机器学习、时间序列、自然语言处理、异常值检测、计算机视觉、推荐引擎、强化学习和对抗性学习。8.数据可视化商业世界经常产生大量数据,这些数据需要被翻译成易于理解的格式。与原始数据相比,人们可以更自然地以图表和图形的形式理解数据,常言道:“一张图片胜过千言万语”。作为数据科学家,你必须能够借助数据可视化工具(如ggplot,d3.js和Matplottlib以及Tableau)可视化数据。这些工具将帮助你将项目中的复杂结果转换为易于理解的格式。问题是,很多人不了解序列相关性或p值,你需要直观地向他们展示这些术语在结果中的表示。数据可视化使组织有机会直接处理数据,他们可以快速掌握并且帮助他们在竞争中抓住新商机。9.非结构化数据数据科学家能够处理非结构化数据至关重要。非结构化数据是未定义的内容,不适合数据库表,其中包括视频、博客文章、客户评论、社交媒体帖子、音频等。对这些类型的数据进行排序很困难,因为它们没有逻辑可言。由于其复杂性,大多数人将非结构化数据称为“黑暗分析”。使用非结构化数据可以帮助你揭示对决策有用的洞察力。作为数据科学家,你必须能够理解和操纵来自不同的平台的非结构化数据。非技术技能10.好奇心“我没有特殊才能。我只是充满好奇心。”-爱因斯坦。毫无疑问,你最近可能多次看到过这句话,因为它与数据科学家有关。一位资深数据科学家描述过它的含义,并在几个月前的博客中将其视为必要的“软技能。好奇心可以被定义为获得更多知识的愿望。作为数据科学家,你需要能够提出有关数据的问题,因为数据科学家花费大约80%的时间来发现和准备数据。这是因为数据科学领域是一个发展非常快的领域,你必须学习更多以跟上节奏。你需要通过在线阅读内容和阅读有关数据科学趋势的相关书籍来定期更新你的知识。不要被在互联网上飞来飞去的大量数据所淹没,你必须能够知道如何理解这一切。好奇心是成为数据科学家所需要的技能之一。例如,最初你可能没有太多了解你收集的数据。好奇心将使你能够筛选数据以查找答案和更多见解。11.商业头脑要成为一名数据科学家,你需要对你正在从事的行业有充分的了解,并了解贵公司正在努力解决的业务问题。在数据科学方面,除了确定企业应利用其数据的新方法之外,能够识别哪些问题对于业务而言至关重要是重要的。为了能够做到这一点,你必须了解你解决的问题如何影响业务。这就是你需要了解企业运营方式的原因,以便你可以将你的工作指向正确的方向。12.沟通技巧寻找强大数据科学家的公司正在寻找能够清晰流利地将技术发现转化为非技术团队的人员,例如市场营销部门或销售部门。数据科学家必须使企业能够通过量化的洞察力来制定决策,此外还要了解非技术同事的需求,以便恰当地纠正数据。除了说出公司理解的相同语言外,你还需要使用数据叙述进行沟通。作为数据科学家,你必须知道如何围绕数据创建故事情节,以便任何人都能轻松理解。例如,呈现数据表不如以叙事格式从这些数据中分享见解那样有效。使用讲故事将帮助你将你的发现正确地传达给你的雇主。沟通时,请注意嵌入在你分析的数据中的结果和值。大多数企业主不想知道你分析的内容,他们对如何积极地影响他们的业务感兴趣。学会专注于通过沟通提供价值和建立持久的关系。13.团队合作数据科学家不能单独工作,你不得不与公司高管合作制定战略,与产品经理和设计师一起创造更好的产品,还要与营销人员合作以推出更好的营销活动,最重要的还要与客户和服务器软件开发人员合作创建数据管道并改进工作流程,你必须与组织中的每个人(包括你的客户)合作。从本质上讲,你将与你的团队成员合作开发应用,以了解解决问题所需的业务目标和数据。你需要了解正确的方法来解决问题以及如何将结果转换并呈现给所有相关人员都能轻松理解的内容。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 26, 2019 · 1 min · jiezi

Python中的可迭代对象、迭代器、For循环工作机制、生成器

1.iterable iterator区别要了解两者区别,先要了解一下迭代器协议:迭代器协议是指:对象需要提供__next__()方法,它返回迭代中的元素,在没有更多元素后,抛出StopIteration异常,终止迭代。可迭代对象就是:实现了迭代器协议的对象。协议是一种约定,可迭代对象实现迭代器协议,Python的内置工具(如for循环,sum,min,max函数等)通过迭代器协议访问对象,因此,for循环并不需要知道对象具体是什么,只需要知道对象能够实现迭代器协议即可。迭代器(iterator)与可迭代对象(iterable)并不是同一个概念。直观上:1.可迭代对象(iterable):凡是具有__iter__的方法的类,都是可迭代的类。可迭代类创建的对象实现了__iter__方法,因此就是可迭代对象。用list、tuple等容器创建的对象,都是可迭代对象。可迭代对象通过__iter__方法返回一个迭代器,然后在内部调用__next__方法进行迭代,最后没有元素时,抛出异常(这个异常python自己会处理,不会让开发者看见)。2.迭代器(iterator):迭代器对象必须同时实现__iter__和__next__方法才是迭代器。对于迭代器来说,iter 返回的是它自身 self,next 则是返回迭代器中的下一个值,最后没有元素时,抛出异常(异常可以被开发者看到)。从上面2点可以看出:1.迭代器一定是可迭代对象,因为它实现了__iter__()方法;2.通过iter()方法(在类的内部就是__iter__)能够使一个可迭代对象返回一个迭代器。Python学习qun:5563,70268,这里是python学习者聚集地,欢迎喜欢Python的小伙伴!3.迭代器的 iter 方法返回的是自身,并不产生新的迭代器对象。而可迭代对象的 iter 方法通常会返回一个新的迭代器对象。第3点性质正是可迭代对象可以重复遍历的原因(每次返回一个独立的迭代器,就可以保证不同的迭代过程不会互相影响);而迭代器由于返回自身,因此只能遍历一次。上面3点可以通过下面的例子看出来:from collections import Iterablefrom collections import Iteratorprint isinstance(iter([1,2]),Iterator)print isinstance(iter([1,2]),Iterable)print isinstance([1,2],Iterator)print isinstance([1,2],Iterable)##resultTrueTrueFalseTrue##id可以查看一个对象在内存中的地址test=[1,2,3]testIter=iter(test)print id(testIter)print id(testIter)print id(iter(test))print id(iter(test))print id(test.iter())print id(test.iter())##result:可迭代对象每次调用iter方法都会返回一个新的迭代器对象,而迭代器对象调用iter方法返回自身67162576 67162576 67162688 67162632 67162856 671630242.iterable的工作机制拿一个例子看看,首先定义一个有__iter__方法,但是没有next()方法的类 (PS:在python2中是next(),python3是__next__()):from collections import Iterable, Iteratorclass Student(object): def init(self,score): self.score=score def iter(self): return iter(self.score) test= Student([80,90,95])print isinstance(test, Iterable)print isinstance(test, Iterator)for i in test: print i##resultTrueFalse809095##可重复遍历for i in test: print i##result809095上面代码的结果印证了定义中提到的:缺少了next()方法,可迭代对象就不是迭代器。此外,注意到:可迭代对象通过__iter__方法每次都返回了一个独立的迭代器,这样就可以保证不同的迭代过程不会互相影响。也就是说,通过iterable可以实现重复遍历,而迭代器是无法重复遍历的!因此,如果想要把可迭代对象转变为迭代器,可以先调用iter()方法返回一个迭代器。然后就可以用next()不断迭代了!print isinstance(iter(test),Iterator)testIter=iter(test)print testIter.next()print testIter.next()print testIter.next()##resultTrue809095一旦取完了可迭代对象中所有的元素,再次调用next就会发生异常print testIter.next()##resultStopIteration: 3.迭代器Iterator的工作机制看下面这个例子:class Student(object): def init(self,score): self.score=score def iter(self): return self def next(self): if self.score<100: self.score+=1 return self.score else: raise StopIteration() test= Student(90)print isinstance(test, Iterable)print isinstance(test, Iterator)print test.next()print test.next()print test.next()for i in test: print i##resultTrueTrue919293949596979899100##如果此时再对test这个迭代器调用next方法,就会抛出异常test.next()##resultStopIteration: 这个例子印证了定义中的:迭代器对象必须同时实现__iter__和__next__方法才是迭代器。那么,使用迭代器好处在哪呢?Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。一个很常见的应用就是:Python在处理列表的时候,是直接把整个列表读进内存的,当遇到大量样本时的时候会变得很慢。而迭代器的优势在于只把需要的元素读进内存,因此占用内存更少。换句话说,迭代器是一种惰性求值模式,它是有状态的,只有在调用时才返回值,没有调用的时候就等待下一次调用。这样就节省了大量内存空间。4.for循环的工作机制有了上面2个例子,就可以总结一下在可迭代对象与迭代器中的For循环工作机制了。当对象本身就是迭代器时,For循环工作机制:1.调用 iter__方法,返回自身self,也就是返回迭代器。2.不断地调用迭代器的next()方法,每次按序返回迭代器中的一个值。3.迭代到最后没有元素时,就抛出异常 StopIteration。在可迭代对象中,for循环工作机制:1.先判断对象是否为可迭代对象(等价于判断有没有__iter__或__getitem__方法),没有的话直接报错,抛出TypeError异常。有的话,调用 iter__方法,返回一个迭代器。2.在python内部不断地调用迭代器的__next__方法,每次按序返回迭代器中的一个值。3.迭代到最后没有元素时,就抛出异常 StopIteration,这个异常 python 自己会处理,不会暴露给开发者。借用网络上的一张图直观理解一下:Python中的可迭代对象、迭代器、For循环工作机制、生成器此外,还要注意,python中的for循环其实兼容了两种机制:1.如果对象有__iter__会返回一个迭代器。2.如果对象没有__iter,但是实现了__getitem,会改用下标迭代的方式。__getitem__可以帮助一个对象进行取数和切片操作。当for发现没有__iter__但是有__getitem__的时候,会从0开始依次读取相应的下标,直到发生IndexError为止,这是一种旧的迭代协议。iter方法也会处理这种情况,在不存在__iter__的时候,返回一个下标迭代的iterator对象来代替。一个重要的例子是str,字符串就是没有__iter__方法的,但是却依然可以迭代,原因就是其在for循环时调用了__getitem__方法。看一个例子: from collections import Iterable, Iterator class Student(object): def init(self,score): self.score=score def getitem(self,n): return self.score[n] test= Student([80,90,95]) print isinstance(test, Iterable) print isinstance(test, Iterator) print isinstance(iter(test), Iterable) print isinstance(iter(test), Iterator) for i in test: print i ##result False False True True 80 90 95 for i in range(0,3): print test[i] ##result 80 90 95 for i in iter(test): print i ##result 80 90 95可以看到,实现了__getitem__方法的对象本身,尽管不是iterable与iterator,仍旧是可以调用for循环的。通过iter方法,返回一个下标迭代的iterator对象。5.generator的原理最后说一下生成器,生成器是一种特殊的迭代器,当然也是可迭代对象。对于生成器,Python会自动实现迭代器协议,以便应用到迭代中(如for循环,sum函数)。由于生成器自动实现了迭代器协议,所以,我们可以调用它的next方法,并且,在没有值可以返回的时候,生成器自动产生StopIteration异常。创建生成器的方法:将return 改为yield。具体的实现网络上教程很多,不细说了。Python学习qun:5563,70268,这里是python学习者聚集地,欢迎喜欢Python的小伙伴!6.总结到一幅图片很好的描述了本文的所有内容,就拿它作为文末的总结吧! ...

March 19, 2019 · 1 min · jiezi

什么值得读-计算机速成课

简介 什么值得读 ???? 好文章???? 好视频???? 好项目✍️ 好书籍????一门来自 YouTube 的《计算机基础知识》视频课程已经由 Crash Course 字幕组 翻译完成。推荐理由这次推荐的视频是一门计算机速成课 小二 已经看完了觉得还是挺有趣的,每个视频大概在 10分钟左右而且视频制作精良且视频相互关联性不怎么强可独立观看,在讲一些原理时都有相应的动画来帮助我们快速理解计算机 概念 和 大纲 也方便我们更深入学习时有一个大致方向。视频翻译精良而且还带上了片头总结方便我们回顾和查阅知识点。一些非科班出身的同学有些计算机的基础课程没有上过虽然平时开发时不会有什么大问题,但是补上相关知识对于整个计算机有更深入的了解还是非常有用的。另外一些同学比如 小二 自己一上课就偷偷开小差就偶尔有听听课也是需要补上这一课的。再者了解一些计算机的历史还是挺有趣的,例如 BUG 一词的由来1947年9月,哈佛马克2型的操作员从故障继电器中拔出一只死虫,Grace Hopper 曾说 “从那时起,每当电脑出了问题,我们就说它出了 bug(虫子)。”当然 Crash Course 字幕组 翻译的质量相当高,想学习计算机相关英语和扩充词汇的小伙伴也可以试着读读字幕翻译 全40集中英字幕文本%E5%85%A840%E9%9B%86%E4%B8%AD%E8%8B%B1%E5%AD%97%E5%B9%95%E6%96%87%E6%9C%AC)。播放列表名称bilibiliYouTube早期的计算 - Early Computing视频地址视频地址电子计算 - Electronic Computing视频地址视频地址布尔逻辑与逻辑电路 - Boolean Logic & Logic Gates视频地址视频地址二进制 - Representing Numbers and Letters with Binary视频地址视频地址算术逻辑单元 - How Computers Calculate - the ALU视频地址视频地址寄存器 & 内存 - Registers and RAM视频地址视频地址中央处理器 - The Central Processing Unit(CPU视频地址视频地址指令和程序 - Instructions & Programs视频地址视频地址高级 CPU 设计 - Advanced CPU Designs视频地址视频地址编程史话 - Early Programming视频地址视频地址编程语言 - The First Programming Languages视频地址视频地址编程原理:语句和函数 - Programming Basics: Statements & Functions视频地址视频地址算法初步 - Intro to Algorithms视频地址视频地址数据结构 - Data Structures视频地址视频地址阿兰·图灵 - Alan Turing视频地址视频地址软件工程 - Software Engineering视频地址视频地址集成电路、摩尔定律 - Integrated Circuits & Moore’s Law视频地址视频地址操作系统 - Operating Systems视频地址视频地址内存 & 储存介质 - Memory & Storage视频地址视频地址文件系统 - Files & File Systems视频地址视频地址压缩 - Compression视频地址视频地址命令行界面 - Keyboards & Command Line Interfaces视频地址视频地址屏幕 & 2D 图形显示 - Screens & 2D Graphics视频地址视频地址冷战和消费主义 - The Cold War and Consumerism视频地址视频地址个人计算机革命 - The Personal Computer Revolution视频地址视频地址图形用户界面 - Graphical User Interfaces视频地址视频地址3D 图形 - 3D Graphics视频地址视频地址计算机网络 - Computer Networks视频地址视频地址互联网 - The Internet视频地址视频地址万维网 - The World Wide Web视频地址视频地址网络安全 - Cybersecurity视频地址视频地址黑客与攻击 - Hackers & Cyber Attacks视频地址视频地址加密 - Cryptography视频地址视频地址机器学习与人工智能 - Machine Learning & Artificial Intelligence视频地址视频地址计算机视觉 - Computer Vision视频地址视频地址自然语言处理 - Natural Language Processing视频地址视频地址机器人 - Robots视频地址视频地址计算机中的心理学 - Psychology of Computing视频地址视频地址教育型科技 - Educational Technology视频地址视频地址奇点,天网,计算机的未来 - The Singularity, Skynet, and the Future of Computing视频地址视频地址打赏&联系如果您感觉有收获,欢迎给我打赏,以激励我输出更多的优质内容。本文原稿来自 PushMeTop ...

March 19, 2019 · 2 min · jiezi

编程语言中的错误处理

在日常的编程过程中,不可避免地需要处理错误的情况,而每一种编程语言都自有其错误处理逻辑,其背后的考量是什么?下面来探讨一下各编程语言中的错误处理,尝试总结出一些通用的方法与原则。一、什么是异常讨论一个问题之前,第一步就是要明晰下它所涉及的概念。首先,标题所说的错误是广义的错误,它包括异常(Exception)与错误(Error)。下文中提到的『错误』均为狭义的区别与异常的错误。程序中的异常(Exception)是指发生在程序执行过程中非频繁非正常的事件,它位于程序正常流程之外。异常大致可分为两类:硬件异常:由CPU发起,它们可能是某些指令序列的执行导致的。比如除零或访问非法内存地址等。软件异常:由应用程序或操作系统显式发起。例如系统可以检测到指定的参数非法值。编程语言中的异常则属于软件异常。而程序中的错误(Error),通常是指发生在程序执行过程中正常的事件,它就在程序正常流程范围之内。二、正确区分异常与错误与异常概念最容易混淆的就是错误。二者通常可以通过下面三个维度来区分:是否正常/可预期/终止程序概念是否正常是否可预期是否终止程序异常否否是错误是是否前两个维度主要是对概念的描述,最后一个维度(是否终止程序,即是否可恢复)建议作为定义错误与异常的标准。如果一个事件它不可恢复应该定义为异常,及时终止程序退出,避免程序进入不可预知的状态(如造成数据不一致);如果一个事件可以预测出错误,那么就应该check,并做一些相应的恢复处理。如Golang中的Error与Panic就是遵循该原则而设计。三、错误处理区分了异常与错误,下一步则是考虑针对错误的处理机制。正确地区分了异常与错误的概念,我们就可以根据具体场景,正确地定义出异常与错误,以及安排相应的错误处理。通常,编程语言中的错误处理可以分为两类:check式,检查返回值,以C语言为代表,Go亦如此;try/catch式,目前大部分主流编程语言中的异常处理均采用类似方式,如C++/Java/PHP/JavaScript等。虽然目前主流编程语言中的异常处理均采用『try/catch』式的原则,但是大都数在写代码过程中都是双管齐下的,依据具体的场景,选择合适的处理方式(抛异常 or 检查返回值)。3.1 check式最早的C语言是通过检查函数返回值(通常零值/非空成功;非零值/空失败)来进行错误处理的。如定义一个函数:int foo() { // <try something here> if (failed) { return 1; } return 0;}调用者则在进行下一步操作之前,需要判断foo函数返回值:int err = foo();if (err) { // Error! Deal with it.} 基于C或者底层级别的系统均是通过这种检查返回值的方式来处理错误的。如Window和Linux操作系统级别的调用(API)。这种方式很简单,代码可读性也较好,但是写起来非常繁琐,这意味着你需要对每一个函数在调用之前的都需要手动check一下。而且,一旦忘记检查,很容易出现bug。Golang则在C语言的基础上增加了更符合现代编程语言的语法和库。它允许函数有两个返回值,通常最后一个返回值为Error类型,调用者可以通过检查该类型返回值来检查函数返回情况,没有错误则使用第二个返回值,继续接下来的业务逻辑操作。如:func foo() (int, error){ // <try something here> if (failed) { return -1, errors.New(“something error”) } return 0, nil;}调用:if sum, err := foo(); err != nil { // Deal with the error.}// do something with sum…Golang的实现方式看起来比C语言更加优雅一些,但是频繁地检查返回值仍然不可避免。C语言在不使用goto语句的情况下,异常代码复用几乎不可能,Golang也难以解决这个问题。于是在后来发展起来的面向对象编程语言中,大部分都引入了类似try/catch式的异常处理机制。3.2 try/catch式下面主要以Java语言举例说明Java中所有的错误处理均基于Throwable顶层父类,其下有两个子类:Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。这是Java对异常与错误的划分,并且在此基础上为进一步提高程序健壮性,引入了checked异常与unchecked异常概念:针对那些除了RuntimeException与其子类,以及错误(Error),其他的都是需要编译时强制检查的异常,否则编译器会报错。try{ method();}catch (IOException ioe){ System.out.println(“I/O failure”);}// …void method() throws IOException{ throw new IOException(“some text”);}也正是Java的这种处理方式让人诟病:checked异常容易让相关代码里充斥着大量的try/catch,使代码同样变得晦涩难懂。同时,checked异常所起到的作用也只是将捕获的异常,包装成运行时异常,然后再重新抛出。正如,前文所言,每一门编程语言在设计之初都有自身的考量,且在进行实际的错误处理时均会同时考虑『check』与『try/catch』两种方式。在C#中,并没有引入checked异常概念,而是把检查的义务又『还给』了开发者。除此之外,try/catch式异常处理通常会有很大的性能开销,故应当慎用。四、『check』 OR『try/catch』即使发展至今,关于异常处理(『try/catch』)与检查返回值(『check』)这两种错误处理方式仍然争议不断。check式与try/catch式两种错误处理的方式,没有哪一种是绝对优势的,都有各自的优缺点,这有赖于语言设计者当时的权衡与抉择。但是不管哪种编程语言,基本衍生于这两类处理方式。REFERENCEShttp://www.cs.tut.fi/~popl/ny…http://joeduffyblog.com/2016/...https://www.zhihu.com/questio...https://nedbatchelder.com/tex...https://www.javaworld.com/art… ...

March 18, 2019 · 1 min · jiezi

java中CAS

前言:在JDK1.5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁锁机制存在以下问题: (1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。 (2)一个线程持有锁会导致其它所有需要此锁的线程挂起。 (3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。 volatile是不错的机制,但是volatile不能保证原子性,因此对于同步最终还是要回到锁机制上来。 独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。 而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap(比较与交换)。一、什么是CAS CAS,Compare and Swap(比较与交换)。 我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。 在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。 CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” 通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。 类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。二、CAS的目的 利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。三、CAS存在的问题 CAS虽然很高效的解决原子操作,但是CAS仍然存在以下三大问题: 1、ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A - 2B-3A。 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 举个例子:update table set x=x+1, version=version+1 where id=#{id} and version=#{version}; 1 2、循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 3、只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。 从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。四、concurrent包的实现 由于java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式: (1)A线程写volatile变量,随后B线程读这个volatile变量。 (2)A线程写volatile变量,随后B线程用CAS更新这个volatile变量。 (3)A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。 (4)A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。 Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式: 首先,声明共享变量为volatile; 然后,使用CAS的原子条件更新来实现线程之间的同步; 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。 AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的,从整体来看,concurrent包的实现示意图如下:  ...

March 17, 2019 · 1 min · jiezi

2019五个最棒的机器学习课程

摘要: 爱学习的朋友们,你们的福利来了!2019五个最棒的机器学习课程!!凭借强大的统计学基础,机器学习正在成为最有趣,节奏最快的计算机科学领域之一,目前已经有无穷无尽的行业和应用正在使用机器学习使它们更高效和智能。聊天机器人、垃圾邮件过滤、广告投放、搜索引擎和欺诈检测是机器学习模型正在实际应用于日常生活的几个例子。机器学习到底是什么呢?我认为机器学习是让我们找到模式并为人类无法做的事情创建数学模型。机器学习课程与包含探索性数据分析,统计,通信和可视化技术等主题的数据科学课程不同,它更侧重于教授机器学习算法,如何以数学方式工作,以及如何在编程语言中使用它们。以下是今年五大机器学习课程的简要回顾。最好的五个机器学习课程:1. 机器学习-Coursera2. 深度学习专项课程-Coursera3. 使用Python进行机器学习-Coursera4. 高级机器学习专项课程-Coursera5. 机器学习-EdX什么是一个非常好的机器学习课程?标准:上面的每门课程都遵循以下标准:严格关注机器学习;使用免费的开源编程语言,即Python,R或Octave;这些开源的语言都有很多免费的开源库;包含练习和实践经验的编程任务;解释算法如何以数学方式工作;有吸引力的讲师和有趣的讲座;有了这个标准,很多课程都会被淘汰,为了让自己沉浸其中并尽可能快速全面地学习ML,我相信除了在线学习之外,你还应该寻找各种书籍。以下是两本对我的学习经历产生重大影响的书籍。两本优秀的书籍伴侣除了参加下面的任何视频课程,如果你对机器学习还不熟悉,你应该考虑阅读以下书籍:统计学习简介,可在线免费获取。本书提供了令人难以置信的清晰直接的解释和示例,以提高你对许多基本机器学习技术的整体数学直觉。而去更多地是关于事物的理论方面,但它确实包含许多使用R编程语言的练习和例子。使用Scikit-Learn和TensorFlow进行动手机器学习,可通过Safari订阅获得。这是对前一本书的一个很好的补充,因为本文主要关注使用Python进行机器学习的应用。本书将强化你的编程技巧,并向你展示如何立即将机器学习应用于项目。现在,让我们来看看课程描述和评论。1:机器学习 - Coursera此课程的创作者是Andrew Ng,一个斯坦福大学教授,谷歌大脑、Coursera的创始人之一。本课程使用开源编程语言Octave而不是Python或R进行教学。对于某些人来说,这可能是不太友好,但如果你是一个完整的初学者,Octave实际上是一种最简单学习ML基础知识的方法。总的来说,课程材料非常全面,并且由Ng直观地表达,该课程完整地解释了理解每个算法所需的所有数学,包括一些微积分解释和线性代数的复习,课程相当独立,但事先对线性代数的一些了解肯定会有所帮助。提供者:Andrew Ng,斯坦福大学费用:免费审核,证书79美元课程结构:单变量的线性回归回顾线性代数多变量的线性回归Octave/Matlab教程Logistic回归正则化神经网络:表示神经网络:学习应用机器学习的建议机器学习系统设计支持向量机降维异常检测推荐系统大规模机器学习应用示例:Photo OCR如果你可以承诺完成整个课程,你将在大约四个月内掌握机器学习的基础知识。之后,你可以轻松地进入更高级或专业的主题,如深度学习,ML工程或任何其他引起你兴趣的话题。毫无疑问,这是新手开始的最佳课程。2:深度学习专项课程-Coursera同样由Andrew Ng教授,这是一个更高级的课程系列,适合任何有兴趣学习神经网络和深度学习的人。每门课程的作业和讲座都使用Python编程语言,并将TensorFlow库用于神经网络。这第一个机器学习课程的很好的后续,因为你现在将接触使用Python进行机器学习。提供者:Andrew Ng,deeplearning.ai费用:免费审核,证书每月49美元课程结构:1. 神经网络与深度学习深度学习简介神经网络基础知识浅层神经网络深度神经网络2. 改进神经网络:超参数调整,正则化和优化深度学习的实践方面优化算法超参数调整,批量标准化和编程框架3. 构建机器学习项目ML策略(1)ML策略(2)4. 卷积神经网络卷积神经网络的基础深度卷积模型:案例研究物体检测特殊应用:人脸识别和神经风格转移5. 序列模型递归神经网络自然语言处理和Word嵌入序列模型和注意机制为了理解本课程中介绍的算法,你应该熟悉线性代数和机器学习。如果你需要一些建议来获取所需的数学,请参阅本文末尾的学习指南。3:使用Python进行机器学习-Coursera这是另一个初学者课程,这个课程仅关注最基本的机器学习算法。本课程使用Python教学,并且对数学的要求不是很高。通过每个模块,你将有机会使用交互式Jupyter笔记本来完成你刚学过的新概念。每个笔记本都增强了你的知识,并为你提供了在实际数据上使用算法的具体说明。提供者:IBM价格:免费审核,证书每月39美元课程结构:机器学习简介回归分类聚类推荐系统项目本课程最好的一点是为每种算法提供实用的建议。当引入新算法时,老师会向你提供它的工作原理,它的优点和缺点,以及你应该使用它的哪种情况。这些点经常被排除在其他课程之外,这些信息对于新学员来说非常重要。4:高级机器学习专项课程-Coursera这是另一个高级系列课程,涉及了非常多的网络类型。如果你有兴趣尽可能多地使用机器学习技术,这个课程很关键。本课程的教学非常棒,由于其先进性,你需要学习更多的数学。本课程涵盖的大部分内容对许多机器学习项目至关重要。提供者:国立研究大学高等经济学院成本:免费审核,每月49美元的证书课程:1. 深度学习简介优化简介神经网络简介深度学习图像无监督表示学习Dee学习序列项目2. 如何赢得数据科学竞赛:向顶级Kagglers学习介绍和回顾关于模型的特征处理和生成最终项目描述探索性数据分析验证数据泄漏度量标准优化高级特征工程-1超参数优化高级特征工程-2Ensembling项目3. 机器学习的贝叶斯方法贝叶斯方法和共轭先验的介绍期望最大化算法变分推断和潜在Dirichlet分配(LDA)马尔可夫链蒙特卡洛变分自动编码器高斯过程和贝叶斯优化项目4. 实践强化学习简介:我为什么要关心?RL的核心:动态编程无模型方法基于近似值的方法基于政策的方法探索5. 计算机视觉中的深度学习图像处理和计算机视觉入门视觉识别的卷积特征物体检测对象跟踪和动作识别图像分割与合成6. 自然语言处理简介和文本分类语言建模和序列标记语义的向量空间模型序列到序列任务对话系统7. 通过机器学习解决大型强子对撞机挑战数据科学家的粒子物理入门粒子识别寻找稀有衰变中的新物理学在新的CERN实验中用机器学习搜索暗物质提示探测器优化完成这一系列课程大约需要8到10个月,所以如果你从今天开始,在不到一年的时间里,你将学到大量的机器学习算法,并能够开始处理更多尖端的应用程序。在这几个月中,你还将创建几个真正的项目,使计算机学习如何阅读,查看和播放。这些项目将成为你投资组合的理想选择,并将使你的GitHub对任何感兴趣的雇主都非常活跃。5:机器学习-EdX这是一个高级课程,具有文中任何其他课程的最高数学先决条件。你需要非常牢固地掌握线性代数、微积分、概率和编程。该课程在Python或Octave中都有编程作业,但该课程不教授任何一种语言。与其他课程的不同之处是对机器学习的概率方法的讲解。如果你有兴趣阅读教科书,例如机器学习:概率视角,这是硕士课程中最流行的数据科学书籍之一。提供者:哥伦比亚大学费用:免费审核,证书300美元课程结构:最大似然估计,线性回归,最小二乘法岭回归,偏差方差,贝叶斯规则,最大后验推断最近邻分类,贝叶斯分类器,线性分类器,感知器Logistic回归,Laplace逼近,核方法,高斯过程最大边距,支持向量机(SVM),树木,随机森林,提升聚类,K均值,EM算法,缺失数据高斯混合,矩阵分解非负矩阵分解,潜在因子模型,PCA和变化马尔可夫模型,隐马尔可夫模型连续状态空间模型,关联分析模型选择,后续步骤课程中的许多主题都包含在针对初学者的其他课程中,但数学并未在这里淡化。如果你已经学习了这些技术,有兴趣深入研究数学,并希望从事实际推导出某些算法的编程作业,那么请学习本课程。学习指南这里是你学习机器学习之旅的快速指南,首先,我们将介绍大多数机器学习课程的先决条件。课程先决条件高级的课程在开始之前需要以下知识:线性代数概率微积分程序设计这些是能够理解机器学习如何在幕后工作的简单组件。许多初级课程通常要求至少一些编程和熟悉线性代数基础知识,例如向量,矩阵。本文的第一个课程,Andrew Ng的机器学习,包含了你需要的大部分数学的复习,但是如果你以前没有学过线性代数,那么同时学习机器学习和线性代数可能会很困难。另外,我建议学习Python,因为大多数优秀的ML课程都使用Python。如果你学习使用Octave的Andrew Ng的机器学习课程,你应该在课程期间或之后学习Python,因为你最终需要它。另外,另一个很棒的Python资源是dataquest.io,它在他们的交互式浏览器环境中有一堆免费的Python课程。在学习了必备必需品之后,你就可以开始真正理解算法的工作原理了。基本算法在机器学习中有一套基本的算法,每个人都应该熟悉并具有使用经验。这些是:线性回归Logistic回归k-Means聚类k-最近邻居支持向量机(SVM)决策树随机森林朴素贝叶斯这些是必需品,上面列出的课程基本上包含所有这些。在开展新项目时,了解这些技术如何工作以及何时使用它们将非常重要。在基础知识之后,一些更先进的学习技巧将是:集成学习Boosting降维强化学习神经网络与深度学习这只是一个开始,但这些算法通常是你在最有趣的机器学习解决方案中看到的,它们是你工具箱的有效补充。就像基本技术一样,你学习的每一个新工具都应该养成一个习惯,立即将它应用到项目中,以巩固你的理解,并在需要复习时有所回头。解决一个项目在线学习机器学习具有挑战性并且非常有益。重要的是要记住,只是观看视频和参加测验并不意味着你真的在学习这些材料。如果你正在进行的项目使用不同的数据并且目标与课程本身不同,你将学到更多。一旦你开始学习基础知识,你应该寻找可以应用这些新技能的有趣数据。上面的课程将为你提供何时应用某些算法的直觉,因此立即将它们应用于你自己的项目中是一种很好的做法。通过反复试验,探索和反馈,你将发现如何尝试不同的技术,如何衡量结果,以及如何分类或预测。有关要采用何种ML项目的一些灵感,请参阅此示例列表。解决项目可以让你更好地理解机器学习环境,当你深入了解深度学习等更高级的概念时,实际上可以使用无限数量的技术和方法来理解和使用。阅读新研究机器学习是一个快速发展的领域,每天都有新的技术和应用出现。一旦你通过基础知识,你应该有能力通过一些关于你感兴趣的主题的研究论文。有几个网站可以获得符合你标准的新论文的通知。Google学术搜索始终是一个好的开始,输入“机器学习”和“深度学习”等关键词,或者你感兴趣的任何其他内容,点击左侧的“创建提醒”链接即可收到电子邮件。让它成为每周习惯,阅读这些警报,扫描文件,看看它们是否值得阅读,然后承诺了解正在发生的事情。如果它与你正在处理的项目有关,请查看你是否可以将这些技术应用于你自己的问题。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 15, 2019 · 1 min · jiezi

Collection集合框架

概述Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。1.Set(无序、不可重复)Set集合类似于一个罐子,“丢进"Set集合里的多个对象之间没有明显的顺序。Set继承自Collection接口,不能包含有重复元素。Set判断两个对象相同不是使用”==“运算符,而是根据equals方法。也就是说,我们在加入一个新元素的时候,如果这个新元素对象和Set中已有对象进行equals比较都返回false,则Set就会接受这个新元素对象,否则拒绝。如果希望遍历Set集合中的元素只能调用其iterator方法,通过返回的Iterator对象来完成。1.1 HashSetHashSet是底层通过HashMap实现,为快速查找设计的Set。存入HashSet的对象必须定hashCode(),HashSet使用HASH算法来存储集合中的元素,因此具有良好的存取和查找性能。当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在HashSet中的存储位置。值得注意的是,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等1.2 TreeSetTreeSet是依靠TreeMap来实现的。 TreeSet集合底层数据结构是红黑树(自平衡二叉查找树)。TreeSet的本质是一个”有序的,并且没有重复元素”的集合,而且支持自定义排序。2.List(有序、可重复) List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许加入重复元素,因为它可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引2.1 LinkedList概括的说,LinkedList 是线程不安全的,允许元素为null的双向链表。 因其底层数据结构是链表,所以可想而知,它的增删只需要移动指针即可,故时间效率较高。不需要批量扩容,也不需要预留空间,所以空间效率比ArrayList高。缺点就是需要随机访问元素时,时间效率很低,虽然底层在根据下标查询Node的时候,会根据index判断目标Node在前半段还是后半段,然后决定是顺序还是逆序查询,以提升时间效率。不过随着n的增大,总体时间效率依然很低。构造方法//集合元素数量transient int size = 0;//链表头节点transient Node<E> first;//链表尾节点transient Node<E> last;//啥都不干public LinkedList() {}public LinkedList(Collection<? extends E> c) { this(); addAll(c);}对比JDK1.6构造方法private transient Entry<E> header = new Entry<E>(null, null, null);private transient int size = 0;public LinkedList() { header.next = header.previous = header;}public LinkedList(Collection<? extends E> c) { this(); addAll(c);}可以看出在1.7之前LinkedList是双向循环链表,在这之后,因为不再使用header节点,所以默认构造方法什么也不做,first和last会被默认初始化为null。节点Node结构private static class Node<E> { E item;//元素值 Node<E> next;//后置节点 Node<E> prev;//前置节点 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }}增加1 addAll//addAll ,在尾部批量增加public boolean addAll(Collection<? extends E> c) { return addAll(size, c);//以size为插入下标,插入集合c中所有元素}//以index为插入下标,插入集合c中所有元素public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index);//检查越界 [0,size] 闭区间 Object[] a = c.toArray();//拿到目标集合数组 int numNew = a.length;//新增元素的数量 if (numNew == 0)//如果新增元素数量为0,则不增加,并返回false return false; Node<E> pred, succ; //index节点的前置节点,后置节点 if (index == size) { //在链表尾部追加数据 succ = null; //size节点(队尾)的后置节点一定是null pred = last;//前置节点是队尾 } else { succ = node(index);//取出index节点,作为后置节点 pred = succ.prev; //前置节点是,index节点的前一个节点 } for (Object o : a) {//遍历要添加的节点。 @SuppressWarnings(“unchecked”) E e = (E) o; Node<E> newNode = new Node<>(pred, e, null);//以前置节点 和 元素值e,构建new一个新节点, if (pred == null) //如果前置节点是空,说明是头结点 first = newNode; else//否则 前置节点的后置节点设置为新节点 pred.next = newNode; pred = newNode;//步进,当前的节点为前置节点了,为下次添加节点做准备 } if (succ == null) {//循环结束后,判断,如果后置节点是null。 说明此时是在队尾append的。 last = pred; //则设置尾节点 } else { pred.next = succ; // 否则是在队中插入的节点 ,更新前置节点 后置节点 succ.prev = pred; //更新后置节点的前置节点 } size += numNew; // 修改数量size modCount++; //修改modCount return true;}//根据index 查询出Node,Node<E> node(int index) { // assert isElementIndex(index);//通过下标获取某个node 的时候,(增、查 ),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i–) x = x.prev; return x; }}private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}private boolean isPositionIndex(int index) { return index >= 0 && index <= size; //插入时的检查,下标可以是size [0,size]}小结:为什么添加新节点只是让前一个节点的next指向新节点?链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。通过下标获取某个node 的时候,并不是只能从头到尾查询,因为同时存储了头节点和尾节点,会根据index处于前半段还是后半段进行一个折半查找,以提升查询效率2 插入单个节点add//在尾部插入一个节点: addpublic boolean add(E e) { linkLast(e); return true;} //在指定下标,index处,插入一个节点 public void add(int index, E element) { checkPositionIndex(index);//检查下标是否越界[0,size] if (index == size)//在尾节点后插入 linkLast(element); else//在中间插入 linkBefore(element, node(index)); }//生成新节点 并插入到 链表尾部, 更新 last/first 节点。void linkLast(E e) { final Node<E> l = last; //记录原尾部节点 final Node<E> newNode = new Node<>(l, e, null);//以原尾部节点为新节点的前置节点 last = newNode;//更新尾部节点 if (l == null)//若原链表为空链表,需要额外更新头结点 first = newNode; else//否则更新原尾节点的后置节点为现在的尾节点(新节点) l.next = newNode; size++;//修改size modCount++;//修改modCount} //在succ节点前,插入一个新节点e void linkBefore(E e, Node<E> succ) { // assert succ != null; //保存后置节点的前置节点 final Node<E> pred = succ.prev; //以前置和后置节点和元素值e 构建一个新节点 final Node<E> newNode = new Node<>(pred, e, succ); //新节点new是原节点succ的前置节点 succ.prev = newNode; if (pred == null)//如果之前的前置节点是空,说明succ是原头结点。所以新节点是现在的头结点 first = newNode; else//否则构建前置节点的后置节点为new pred.next = newNode; size++;//修改数量 modCount++;//修改modCount } private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }删除 //删:remove目标节点 public E remove(int index) { checkElementIndex(index);//检查是否越界 下标[0,size) return unlink(node(index));//从链表上删除某节点 } //删: remove元素值 public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } //从链表上删除x节点 E unlink(Node<E> x) { // assert x != null; final E element = x.item; //当前节点的元素值 final Node<E> next = x.next; //当前节点的后置节点 final Node<E> prev = x.prev;//当前节点的前置节点 if (prev == null) { //如果前置节点为空(说明当前节点原本是头结点) first = next; //则头结点等于后置节点 } else { prev.next = next; x.prev = null; //将当前节点的 前置节点置空 } if (next == null) {//如果后置节点为空(说明当前节点原本是尾节点) last = prev; //则 尾节点为前置节点 } else { next.prev = prev; x.next = null;//将当前节点的 后置节点置空 } x.item = null; //将当前元素值置空 size–; //修改数量 modCount++; //修改modCount return element; //返回取出的元素值} private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } //下标[0,size) private boolean isElementIndex(int index) { return index >= 0 && index < size; } public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size–; modCount++; return element; } public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size–; modCount++; return element; }修改public E set(int index, E element) { checkElementIndex(index); //检查越界[0,size) Node<E> x = node(index);//取出对应的Node E oldVal = x.item;//保存旧值 供返回 x.item = element;//用新值覆盖旧值 return oldVal;//返回旧值}改也是先根据index找到Node,然后替换值,改不修改modCount查找//根据index查询节点public E get(int index) { checkElementIndex(index);//判断是否越界 [0,size) return node(index).item; //调用node()方法 取出 Node节点,}//根据节点对象,查询下标 public int indexOf(Object o) { int index = 0; if (o == null) {//如果目标对象是null //遍历链表 for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else {////遍历链表 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; }从尾至头遍历链表,找到目标元素值为o的节点 public int lastIndexOf(Object o) { int index = size; if (o == null) { for (Node<E> x = last; x != null; x = x.prev) { index–; if (x.item == null) return index; } } else { for (Node<E> x = last; x != null; x = x.prev) { index–; if (o.equals(x.item)) return index; } } return -1; }toArray() public Object[] toArray() { //new 一个新数组 然后遍历链表,将每个元素存在数组里,返回 Object[] result = new Object[size]; int i = 0; for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; return result; }队列操作 //返回头结点,但不删除 public E peek() { final Node<E> f = first; return (f == null) ? null : f.item; } //返回头结点,但不删除 public E element() { return getFirst(); } //返回头结点并移除 public E poll() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } //删除头结点并返回 public E remove() { return removeFirst(); } //添加指定元素在集合末尾 public boolean offer(E e) { return add(e); }双端队列操作 //在集合头部插入元素 public boolean offerFirst(E e) { addFirst(e); return true; } //在集合尾部插入元素 public boolean offerLast(E e) { addLast(e); return true; } //得到集合第一个元素 public E peekFirst() { final Node<E> f = first; return (f == null) ? null : f.item; } //得到集合最后一个元素但不删除 public E peekLast() { final Node<E> l = last; return (l == null) ? null : l.item; } //得到并移除第一个元素 public E pollFirst() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); } //得到并移除最后一个元素 public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); } //在集合头部插入元素 public void push(E e) { addFirst(e); } //得到并删除第一个元素 ,如果为空抛出异常 public E pop() { return removeFirst(); }迭代器操作public Iterator<E> iterator() { return listIterator(); }public ListIterator<E> listIterator() { return listIterator(0); } public ListIterator<E> listIterator(int index) { checkPositionIndex(index); return new ListItr(index); }从上面可以看到三者的关系是iterator()——>listIterator(0)——>listIterator(int index)。最终都会调用listIterator(int index)方法,其中参数表示迭代器开始的位置。ListIterator是一个可以指定任意位置开始迭代,并且有两个遍历方法。下面直接看ListItr的实现: private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount;//保存当前modCount,确保fail-fast机制 ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index);//得到当前索引指向的next节点 nextIndex = index; } public boolean hasNext() { return nextIndex < size; } //获取下一个节点 public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex > 0; } //获取前一个节点,将next节点向前移 public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex–; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } public void remove() { checkForComodification(); if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; unlink(lastReturned); if (next == lastReturned) next = lastNext; else nextIndex–; lastReturned = null; expectedModCount++; } public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; }}在ListIterator的构造器中,得到了当前位置的节点,就是变量next。next()方法返回当前节点的值并将next指向其后继节点,previous()方法返回当前节点的前一个节点的值并将next节点指向其前驱节点。由于Node是一个双端节点,所以这儿用了一个节点就可以实现从前向后迭代和从后向前迭代。另外在ListIterator初始时,exceptedModCount保存了当前的modCount,如果在迭代期间,有操作改变了链表的底层结构,那么再操作迭代器的方法时将会抛出ConcurrentModificationException。其他public boolean contains(Object o) { return indexOf(o) != -1; } //返回一个浅拷贝LinkedList对象 public Object clone() { LinkedList<E> clone = superClone(); // Put clone into “virgin” state clone.first = clone.last = null; clone.size = 0; clone.modCount = 0; // Initialize clone with our elements for (Node<E> x = first; x != null; x = x.next) clone.add(x.item); return clone; } private LinkedList<E> superClone() { try { return (LinkedList<E>) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e); } } public void clear() { for (Node<E> x = first; x != null; ) { Node<E> next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; size = 0; modCount++; }2.2 ArrayListArrayList 是一个动态数组,它是线程不安全的,允许元素为null。因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length),所以它也有数组的缺点,空间效率不高。由于数组的内存连续,可以根据下标以O(1)的时间读写(改查)元素,因此时间效率很高。当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList 的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}构造方法,指定集合的大小,去构建ArrayList实例,以减少扩容次数,提高效率。或者在需要扩容的时候,手动调用public void ensureCapacity(int minCapacity) {}方法扩容。构造方法//默认构造函数里的空数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //存储集合元素的底层实现:真正存放元素的数组 transient Object[] elementData; //当前元素数量 private int size; //默认构造方法 public ArrayList() { //默认构造方法只是简单的将 空数组赋值给了elementData this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //空数组 private static final Object[] EMPTY_ELEMENTDATA = {}; //带初始容量的构造方法 public ArrayList(int initialCapacity) { //如果初始容量大于0,则新建一个长度为initialCapacity的Object数组. //注意这里并没有修改size(对比第三个构造函数) if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) {//如果容量为0,直接将EMPTY_ELEMENTDATA赋值给elementData this.elementData = EMPTY_ELEMENTDATA; } else {//容量小于0,直接抛出异常 throw new IllegalArgumentException(“Illegal Capacity: “+ initialCapacity); } } //利用别的集合类来构建ArrayList的构造函数 public ArrayList(Collection<? extends E> c) { //直接利用Collection.toArray()方法得到一个对象数组,并赋值给elementData elementData = c.toArray(); //因为size代表的是集合元素数量,所以通过别的集合来构造ArrayList时,要给size赋值 if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class)//这里是当c.toArray出错,没有返回Object[]时,利用Arrays.copyOf 来复制集合c中的元素到elementData数组中 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { //如果集合c元素数量为0,则将空数组EMPTY_ELEMENTDATA赋值给elementData this.elementData = EMPTY_ELEMENTDATA; } } public Object[] toArray() { return Arrays.copyOf(elementData, size); }常用API1 增加每次 add之前,都会判断add后的容量,是否需要扩容。public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e;//在数组末尾追加一个元素,并修改size return true;}private static final int DEFAULT_CAPACITY = 10;//默认扩容容量 10 private void ensureCapacityInternal(int minCapacity) { //利用 == 可以判断数组是否是用默认构造函数初始化的 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);}private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity);}//需要扩容的话,默认扩容一半private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//默认扩容一半 if (newCapacity - minCapacity < 0)//如果还不够,那么就用 能容纳的最小的数量。(add后的容量) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) //若newCapacity 大于最大存储容量,则进行大容量分配 newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity);//拷贝,扩容,构建一个新数组,}//大容量分配,最大分配 Integer.MAX_VALUEprivate static int hugeCapacity(int minCapacity) { if (minCapacity < 0) throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); //确认是否需要扩容 System.arraycopy(a, 0, elementData, size, numNew);// 复制数组完成复制 size += numNew; return numNew != 0;}public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index);//越界判断 Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew);//确认是否需要扩容 int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved);//移动(复制)数组 System.arraycopy(a, 0, elementData, index, numNew);//复制数组完成批量赋值 size += numNew; return numNew != 0;} add、addAll 先判断是否越界,是否需要扩容,如果扩容, 就复制数组,然后设置对应下标元素值。 值得注意的是: 如果需要扩容的话,默认扩容一半。如果扩容一半不够,就用目标的size作为扩容后的容量。在扩容成功后,会修改modCount。扩容操作会导致数组复制,批量删除会导致找出两个集合的交集,以及数组复制操作,因此,增、删都相对低效。 而 改、查都是很高效的操作。Vector的内部也是数组做的,区别在于Vector在API上都加了synchronized所以它是线程安全的,以及Vector扩容时,是翻倍size,而ArrayList是扩容50%。2 删除public E remove(int index) { rangeCheck(index);//判断是否越界 modCount++;//修改modeCount 因为结构改变了 E oldValue = elementData(index);//读出要删除的值 int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved);//用复制 覆盖数组数据 elementData[–size] = null;//置空原尾部数据 不再强引用 return oldValue;} //根据下标从数组取值 并强转 E elementData(int index) { return (E) elementData[index]; }//删除该元素在数组中第一次出现的位置上的数据。 如果有该元素返回true,如果false。public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index);//根据index删除元素 return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false;}//不会越界 不用判断 ,也不需要取出该元素。private void fastRemove(int index) { modCount++;//修改modCount int numMoved = size - index - 1;//计算要移动的元素数量 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved);//以复制覆盖元素 完成删除 elementData[–size] = null;//置空 不再强引用}//批量删除public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c);//判空 return batchRemove(c, false);}//批量移动private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0;//w 代表批量删除后 数组还剩多少元素 boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) // 如果c里不包含当前下标元素, elementData[w++] = elementData[r];//则保留 } finally { if (r != size) { //出现异常会导致 r !=size , 则将出现异常处后面的数据全部复制覆盖到数组里。 System.arraycopy(elementData, r, elementData, w, size - r); w += size - r;//修改 w数量 } if (w != size) {//置空数组后面的元素 for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w;//修改modCount size = w;// 修改size modified = true; } } return modified;}可以看出,当用来作为删除元素的集合里的元素多余被删除集合时,也没事,只会删除它们共同拥有的元素。小结:删除操作一定会修改modCount,且可能涉及到数组的复制,相对低效。3 修改不会修改modCount,相对增删是高效的操作。public E set(int index, E element) { rangeCheck(index);//越界检查 E oldValue = elementData(index); //取出元素 elementData[index] = element;//覆盖元素 return oldValue;//返回元素}4 查询不会修改modCount,相对增删是高效的操作。public E get(int index) { rangeCheck(index);//越界检查 return elementData(index); //下标取数据}E elementData(int index) { return (E) elementData[index];}5 清空 clear会修改modCount。public void clear() { modCount++;//修改modCount for (int i = 0; i < size; i++) //将所有元素置null elementData[i] = null; size = 0; //修改size }6 包含 contain//普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。public boolean contains(Object o) { return indexOf(o) >= 0;}//普通的for循环寻找值,只不过会根据目标对象是否为null分别循环查找。public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1;}7 迭代器 Iterator.public Iterator<E> iterator() { return new Itr();}private class Itr implements Iterator<E> { int cursor; // 要返回的下一个元素的索引,默认是0 int lastRet = -1; //上一次返回的元素 (删除的标志位) int expectedModCount = modCount; //用于判断集合是否修改过结构的标志 public boolean hasNext() { return cursor != size;//游标是否移动至尾部 } @SuppressWarnings(“unchecked”) public E next() { checkForComodification(); int i = cursor; if (i >= size)//判断是否越界 throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length)//再次判断是否越界,在 我们这里的操作时,有异步线程修改了List throw new ConcurrentModificationException(); cursor = i + 1;//游标+1 return (E) elementData[lastRet = i];//返回元素 ,并设置上一次返回的元素的下标 } public void remove() {//remove 掉 上一次next的元素 if (lastRet < 0)//先判断是否next过 throw new IllegalStateException(); checkForComodification();//判断是否修改过 try { ArrayList.this.remove(lastRet);//删除元素 remove方法内会修改 modCount 所以后面要更新Iterator里的这个标志值 cursor = lastRet; //要删除的游标 lastRet = -1; //不能重复删除 所以修改删除的标志位 expectedModCount = modCount;//更新 判断集合是否修改的标志, } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }//判断是否修改过了List的结构,如果有修改,抛出异常 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }}总结 ArrayList和Vectorvector是线程同步的,所以它也是线程安全的,而arraylist是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用arraylist效率比较高。如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%。如果在集合中使用数据量比较大的数据,用vector有一定的优势。如果查找一个指定位置的数据,vector和arraylist使用的时间是相同的,如果频繁的访问数据,这个时候使用vector和arraylist都可以。而如果移动一个指定位置会导致后面的元素都发生移动,这个时候就应该考虑到使用linklist,因为它移动一个指定位置的数据时其它元素不移动。ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据快,插入数据慢,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快。 ArrayList和LinkedListArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。 ...

March 14, 2019 · 11 min · jiezi

String, StringBuffer,StringBuilder的区别

String, StringBuffer,StringBuilder的区别java中String、StringBuffer、StringBuilder是编程中经常使用的字符串类,他们之间的区别也是经常在面试中会问到的问题。现在总结一下,看看他们的不同与相同。1.可变与不可变String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的。private final char value[]; String 为不可变对象,一旦被创建,就不能修改它的值. . 对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。char[] value; StringBuffer:是一个可变对象,当对他进行修改的时候不会像String那样重新建立对象 , 它只能通过构造函数来建立, 如: StringBuffer sb = new StringBuffer();不能通过赋值符号对他进行付值. , 如 sb = “welcome to here!”;//error 对象被建立以后,在内存中就会分配内存空间,并初始保存一个null.向StringBuffer中赋值的时候可以通过它的append方法. sb.append(“hello”);2.是否多线程安全String中的对象是不可变的,也就可以理解为常量, 显然线程安全 。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是 线程安全的 。看如下源码: public synchronized StringBuffer reverse() { super .reverse(); return this ;}StringBuilder并没有对方法进行加同步锁,所以是 非线程安全的 。 3.StringBuilder与StringBuffer共同点StringBuilder与StringBuffer有公共父类AbstractStringBuilder( 抽象类 )。抽象类与接口的其中一个区别是:抽象类中可以定义一些子类的公共方法,子类只需要增加新的功能,不需要重复写已经存在的方法;而接口中只是对方法的申明和常量的定义。StringBuilder、StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如super.append(…)。只是StringBuffer会在方法上加synchronized关键字,进行同步。最后,如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。

March 14, 2019 · 1 min · jiezi

通过Spring Boot中的手动Bean定义提高启动性能

原文:https://blog.csdn.net/qq_4288…使用Spring Boot时你不想使用@EnableAutoConfiguration。你应该怎么做?Spring本质上是快速且轻量级的,但是如何让Spring更快?其中一条建议是可以改善启动时间,那就是考虑手动导入Spring Boot配置,而不是自动全部配置。对所有应用程序来说,它不是正确的做法,但它可能会有所帮助,理解选项是什么肯定不会有害。在本文中,我们将探讨各种手动配置方法并评估其影响。完全自动配置:Hello World WebFlux作为基准,让我们看一下具有单个HTTP端点的Spring Boot应用程序:@SpringBootApplication@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}这个应用启动大约一秒钟,或者更长一些,具体取决于您的硬件。它在这段时间内做了很多工作 - 设置日志系统,读取和绑定配置文件,启动Netty并侦听端口8080,提供到@GetMapping应用程序的路由,还提供默认的错误处理。如果Spring Boot Actuator在类路径上,你还会得到一个/ health和/ info端点(由于这个原因,启动它需要更长的时间)。@SpringBootApplication注释实际包含@EnableAutoConfiguration功能,能够自动提供所有有用的功能。这就是Spring Boot流行的原因,所以我们不想丢弃这个功能,但我们可以仔细看看实际发生的事情,也许可以手动完成一些,看看我们是否学到了什么。注意:如果你想尝试这个代码,很容易从Spring Initializr获得一个空的WebFlux应用程序。只需选中“Reactive Web”复选框并下载项目即可。手动导入自动配置虽然@EnableAutoConfiguration可以轻松地为应用程序添加功能,但它也可以控制启用哪些功能。大多数人都乐意做出妥协 ,但是过度追求易用性也会失控,可能存在性能损失 , 应用程序可能会启动时慢一点,因为Spring Boot必须做一些工作才能找到所有这些功能并安装它们。事实上,找到正确的功能并没有太大的努力:首先类路径扫描,经过仔细优化后,实现有条件的评估非常快。其中应用程序的批量启动时间(80%左右)其实是由JVM加载类时间,因此实际上使其启动更快的唯一方法是通过安装更少的功能来让JVM加载更少。我们可以在注释@EnableAutoConfiguration中使用exclude属性禁用自动配置。一些单独的自动配置也有自己的布尔配置标志,可以在外部设置,例如我们可以使用的JMX spring.jmx.enabled=false(例如, 作为系统属性或在属性文件中)。我们可以走这条路并手动关闭我们不想使用的所有东西,但是这有点笨拙,并且如果类路径改变也不会阻碍其他组件功能被发现。现在我们只是使用我们想要使用的那些功能,我们可以将其称为“点菜”方式,而不是“完全自动配置autoconfiguration”中的“所有的你必须吃进去”。自动配置也其实自动寻找@Configuration标注的类,我们可以使用@Import替代@EnableAutoConfiguration,例如,以下是上面的应用程序,具有我们想要的所有功能(不包括执行器):@SpringBootConfiguration@Import({ WebFluxAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, ErrorWebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, ConfigurationPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}此版本的应用程序仍将具有我们上面描述的所有功能,但启动速度更快(可能大约30%左右)。那么我们为了更快的启动而放弃了什么呢?这是一个快速的概述:Spring Boot自动配置的完整功能包括实际应用程序中可能实际需要的其他内容,而不是特定的小样本。换句话说,30%的加速并不适用于所有应用程序,您的情况可能会有所不同。手动配置很脆弱,很难猜到。如果您编写了另一个执行稍微不同的应用程序,则需要进行不同的配置导入。您可以通过将其提取到便利类或注释中并重新使用它来缓解此问题。@Import行为方式与 @EnableAutoConfiguration配置类的排序方式不同。@Import在某些类具有依赖于早期类的条件行为的情况下,顺序很重要,如果你的配置类有前后依赖顺序关系,你必须要小心。在典型的实际应用中存在另一个排序问题。要模仿@EnableAutoConfiguration,首先需要处理用户配置,以便它们可以覆盖Spring Boot中的条件配置。如果使用@ComponentScan而不是@Imports,则无法控制扫描的顺序,或者处理这些类的处理顺序,您可以使用不同的注释来缓解这种情况(参见下文)。Spring Boot自动配置实际上从未被设计为以这种方式使用,使用这种方式可能会在您的应用程序中引入细微的错误。对此的唯一缓解方式就是是详尽的测试,让它以您期望的方式工作,并且对升级持谨慎态度。增加Actuators如果我们也可以在类路径上添加Actuator:@SpringBootConfiguration@Import({ WebFluxAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, ErrorWebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, HealthIndicatorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, ReactiveManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class, ConfigurationPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}与完整@EndpointAutoConfiguration应用程序相比,此应用程序启动速度更快 (甚至可能快50%),因为我们只包含与两个默认端点相关的配置。Spring Boot使用自动配置则默认地会激活所有端点,但不会将它们暴露给HTTP,如果我们只关心/ health和/ info,使用自动配置可能有些浪费,但自动配置也会在表中留下许多非常有用的功能。Spring Boot可能会在将来做更多工作,以禁用未曝光或未使用过的执行器。有什么不同?手动配置的应用程序有51个bean,而完全引导的自动配置应用程序有107个bean(不计算执行器)。所以它启动起来可能并不令人意外。在我们采用不同的方式实现示例应用程序之前,让我们先看看我们遗漏了什么,这样才能更快地启动它。如果在两个应用程序中都列出bean定义,您将看到所有差异来自我们遗漏的自动配置,以及Spring Boot不会有条件地排除这些自动配置。这是列表(假设您使用spring-boot-start-webflux时没有手动排除):AutoConfigurationPackagesCodecsAutoConfigurationJacksonAutoConfigurationJmxAutoConfigurationProjectInfoAutoConfigurationReactorCoreAutoConfigurationTaskExecutionAutoConfigurationTaskSchedulingAutoConfigurationValidationAutoConfigurationHttpMessageConvertersAutoConfigurationRestTemplateAutoConfigurationWebClientAutoConfiguration这就是我们不需要的12个自动配置(无论如何),并且在自动配置的应用程序中导致了56个额外的bean。它们都提供了有用的功能,所以我们可能希望有一天再次将它们包括在内,但是现在让我们假设我们更愿意在没有他们的情况下使用。spring-boot-autoconfigure有122个自动配置(还有更多spring-boot-actuator-autoconfigure)和完全引导的自动配置的示例应用程序上面只使用了其中的18个。计算使用哪些是非常早的,并且在任何类甚至加载之前,大多数都被Spring Boot丢弃,这种计算非常快(几毫秒)Spring Boot自动配置导入可以通过使用不同的注释来部分地解决与用户配置(必须最后应用)和自动配置之间的差异相关联的排序问题。Spring Boot为此提供了一个注释:@ImportAutoConfiguration来自Spring Boot Test附带spring-boot-autoconfigure的 测试切片功能。因此,您可以替换上面示例中的注释@Import, @ImportAutoConfiguration效果是推迟自动配置的处理,直到所有用户配置加载(例如,通过@ComponentScan或接收@Import)。如果我们准备将自动配置列表整理成自定义注释,我们甚至可以更进一步。不仅仅是直接使用@ImportAutoConfiguration,我们可以写一个自定义注释:@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@ImportAutoConfigurationpublic @interface EnableWebFluxAutoConfiguration {}此注释的主要特征是它带有元注释 @ImportAutoConfiguration。有了这个,我们可以在我们的应用程序中添加新的注释:@SpringBootConfiguration@EnableWebFluxAutoConfiguration@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}并列出实际的配置类/META-INF/spring.factories:com.example.config.EnableWebFluxAutoConfiguration=\org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration这样做的好处是应用程序代码不再需要手动枚举配置,而且现在由Spring Boot处理排序(属性文件中的条目在使用之前会进行排序)。缺点是它仅对需要精确这些特征的应用程序有用,并且需要在想要做一些不同的事情的任何应用程序中进行替换或扩充,当然它仍然会很快 - Spring Boot为排序做了一些额外的工作,但实际上并不是很多。在合适的硬件上,它可能仍然会在不到700毫秒的时间内启动,并带有正确的JVM标志。函数Bean定义在前面的文章中,我提到函数bean定义将是使用Spring启动应用程序的最有效方法。我们可以通过重新编写所有Spring Boot自动配置为ApplicationContextInitializers来将这个应用程序额外挤出10%左右。您可以手动执行此操作,或者您可以使用已为您准备的一些初始化程序,只要您不介意尝试某些实验性功能即可。目前有2个项目正在积极探索基于函数bean定义的新工具和新编程模型的概念:Spring Fu和 Spring Init。两者都提供至少一组函数bean定义来替换或包装Spring Boot自动配置。Spring Fu是基于API(DSL)的,不使用反射或注释;Spring Init具有函数bean定义,并且还具有用于“单点”配置的基于注释的编程模型的原型。其他地方都有更详细的介绍。这里要注意的要点是函数bean定义更快,但如果这是你主要考虑的问题,请记住它只有10%的效果。只要将所有功能放回到我们上面剥离的应用程序中,您就可以重新加载所有必需的类,并重新回到大致相同的启动时间。换句话说,@Configuration本身运行时处理的成本 并不是完全可以忽略不计,但它也不是很高(在这些小应用程序中可能只有10%左右,或者可能是100毫秒)。总结Spring Boot自动配置非常方便,但可以称之为“吃进所有你可以吃的东西”。目前(从2.1.x开始)它可能提供比某些应用程序使用或要求更多的功能。在“菜单单点”方法中,您可以使用Spring Boot作为准备和预测试配置的便捷集合,并选择您使用的部件。如果你这样做,那么@ImportAutoConfiguration是工具包的一个重要部分,但是当我们进一步研究这个主题时,你应该如何最好地使用它。Spring Boot的未来版本以及可能的其他新项目(如Spring Fu或Spring Init)将使得在运行时使用的配置选择变得更加容易,无论是自动还是通过显式选择。注意,@Configuration本身在运行时处理并不是免费的,但它也不是特别昂贵(特别是使用Spring Boot 2.1.x)。您使用的功能数量越少,加载的类越少,启动速度越快。最后,我们不希望 @EnableAutoConfiguration失去其价值或受欢迎程度。写在最后:既然看到这里了,觉得笔者写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!! ...

February 28, 2019 · 1 min · jiezi

亚洲第一届 Rust 大会将于 4 月 20 日在 [北京] 开启

RustCon Asia 来了!由秘猿科技与 PingCAP 联合主办,亚洲第一届 Rust 大会将于 4 月 20 日在中国北京开启。大会为期 4 天,包括 20 日全天和 21 日上午的主题演讲以及 22-23 日的多个主题 workshop 环节。其中主题演讲讲师来自于国内外资深 Rust 开发者和社区活跃贡献者;workshop 主题将覆盖到 Rust 开发入门和成熟技术栈或产品的实战操作和演示。关于 RustCon Asia我们受欧洲 RustFest、美国东部 Rust Belt Rust、俄罗斯 RustRush、美国西部 RustConf 和拉美 Rust LatAm 的影响和激励,开启亚洲的第一场 Rust 大会,并期望 RustCon Asia 未来能够周期性持续举办,连接亚洲的 Rust 的开发者与全球的 Rust 社区,相互扶持,共同布道 Rust 开发语言。在亚洲,我们已有不少 Rust 开发的优秀案例。一些 Rust 项目已经在生产环境中使用多年,包括中国的银行核心系统、信任链、分布式系统、网络和云服务基础设施等。我们选择北京作为 RustCon Asia 的第一站,首先因为我们的组织者秘猿科技和 PingCAP 都来自中国;其次也因为我们对中国的开发者和开发社区文化特别熟悉。秘猿科技和 PingCAP 都非常重视开发者社区,除了产品本身的号召力之外,核心团队的开发者也在各种开发者社区特别活跃,持续贡献技术知识和组织多种开发者活动。未来,我们将 RustCon Asia 推进到亚洲的其他国家,更好的促进当地社区与全球社区的合作和互助。RustCon Asia 目前已开启讲师席位,欢迎关注官网信息,并通过 CFP 提交您的议题信息,支持中英文双语。会议其它细节我们还在逐步确定,请随时关注我们的动态。此次大会期望能够满足你的以下期待:与国内社区的老友面基,与国际社区的开发者见面;中英文主题演讲,并有双向同传支持;实操 workshop(无同传);涵盖从新人友好到高级的技术内容;匿名议题提交和筛选,以便将最优秀内容呈现给大家;与生产环境中使用 Rust 的项目成员交流;开放、温馨的氛围;有机会与新、老朋友一起探索北京城!讲师席位和研讨会席位还在接受报名中,请于官网 CFP 处提交(支持中英文双语):https://cfp.rustcon.asia/events/rustcon-asia大会官网:https://rustcon.asia/中文直达:http://www.huodongxing.com/event/6479456003900Twitter @RustConAsia合作咨询:aimee@cryptape.com关于秘猿科技杭州秘猿科技有限公司(Cryptape Co.,Ltd.)的使命是用技术创造信任,为加密经济提供基础设施和服务。公司成立于 2016 年 ,核心团队从 2011 年开始参与或主导各种区块链项目,实践经验丰富。秘猿科技具备深厚的区块链技术研发和工程实力,核心技术人员均有超过 10 年以上开发经验。公司完全自主研发了区块链基础平台 CITA,并于 2017 年开源,其创新的架构设计解决了区块链底层扩展性问题。关于 PingCAPPingCAP 是一家开源的新型分布式数据库公司,秉承开源是基础软件的未来这一理念,PingCAP 持续扩大社区影响力,致力于前沿技术领域的创新实现。其研发的分布式关系型数据库 TiDB 项目,具备「分布式强一致性事务、在线弹性水平扩展、故障自恢复的高可用、跨数据中心多活」等核心特性,是大数据时代理想的数据库集群和云数据库解决方案。 ...

February 22, 2019 · 1 min · jiezi

指明方向与趋势!2019开发者技能报告出炉!!!

近日国外开发者平台 HankerRank 发布了 2019 年开发者技能调查报告( https://research.hackerrank.com/developer-skills/2019 ),该报告根据对71,281开发者的调查得出。2018 年最受欢迎的开发语言经过调查,2018年的所有开发语言中,JavaScript是最受欢迎的语言,2017年最受欢迎的语言是Java,今年被JavaScript超越,位居第二。2019年开发者最想学的语言报告调查了开发者最想学习的开发语言,结果显示,Go语言、Kotlin语言和Python语言位列前三。 Go语言 Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。Kotlin Kotlin 是一个用于现代多平台应用的静态编程语言 ,由 JetBrains 开发。Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。Kotlin已正式成为Android官方支持开发语言。Python Python是一种计算机程序设计语言。是一种动态的、面向对象的脚本语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的、大型项目的开发。2018年最闻名的开发框架2018年,最闻名的开发框架是AngularJS、其次是Spring。AngularJS AngularJS 是一个 JavaScript框架。它是一个以 JavaScript 编写的库。它可通过 标签添加到HTML 页面。Spring Spring是一个开放源代码的设计层面框架,它解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。2019最想学习的框架2019年,开发者最想学洗的框架是React,Java系的Spring排名第七。React React主要用于构建UI。你可以在React里传递多种类型的参数,如声明代码,帮助你渲染出UI、也可以是静态的HTML DOM元素、也可以传递动态变量、甚至是可交互的应用组件。最容易落地的新技术是什么最近几年,新技术层出不穷,如IoT(物联网)、深度学习、机器学习、计算机视觉、区块链、量子计算、AR(增强现实)、VR(虚拟现实)等。这些新技术,到底哪个在开发者心目中是最接近现实,目前看来最容易落地的呢。经过调查,IoT以53%占比获得第一名、量子计算排名最后,区块链倒数第二。找工作最看重什么不同程序员找工作的时候,会看重不同的东西,比如薪资、成长等。那么报告结果是如何的呢?初级开发者和高级开发者找工作最看重的东西排名前三名是一致的:个人成长和学习空间、其次是工作与生活的平衡,也就是加班的多少、排名第三的是有竞争力的薪酬。总结以上就是2018开发者技能报告的所有主要内容。报告中分别围绕开发者、编程语言等展开。涉及到多个方面。希望能对所有读者有所启发。活在当下,既要脚踏实地,也要仰望星空。作为一名程序员,我们也要时不时的抬起头,看一看自己所在的行业。本文作者:阿里高级开发工程师 洪亮阅读原文本文首发自微信公众号“Hollis”,如需转载请联系原作者。

February 19, 2019 · 1 min · jiezi

如何合理的规划jvm性能调优

JVM性能调优涉及到方方面面的取舍,往往是牵一发而动全身,需要全盘考虑各方面的影响。但也有一些基础的理论和原则,理解这些理论并遵循这些原则会让你的性能调优任务将会更加轻松。为了更好的理解本篇所介绍的内容。你需要已经了解和遵循以下内容:1、已了解jvm 垃圾收集器2、已了解jvm 性能监控常用工具3、能够读懂gc日志4、确信不为了调优而调优,jvm调优不能解决一切性能问题如果对这些不了解不建议读本篇文章。本篇文章基于jvm性能调优,结合jvm的各项参数对应用程序调优,主要内容有以下几个方面:1、jvm调优的一般流程2、jvm调优所要关注的几个性能指标3、jvm调优需要掌握的一些原则4、调优策略&示例一、性能调优的层次为了提升系统性能,我们需要对系统的各个角度和层次来进行优化,以下是需要优化的几个层次。从上面我们可以看到,除了jvm调优以外,还有其他几个层面需要来处理,所以针对系统的调优不是只有jvm调优一项,而是需要针对系统来整体调优,才能提升系统的性能。本篇只针对jvm调优来讲解,其他几个方面,后续再介绍。在进行jvm调优之前,我们假设项目的架构调优和代码调优已经进行过或者是针对当前项目是最优的。这两个是jvm调优的基础,并且架构调优是对系统影响最大的 ,我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过jvm调优令其达到一个质的飞跃,这是不可能的。另外,在调优之前,必须得有明确的性能优化目标, 然后找到其性能瓶颈。之后针对瓶颈的优化,还需要对应用进行压力和基准测试,通过各种监控和统计工具,确认调优后的应用是否已经达到相关目标。二、jvm调优流程调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm的调优也不例外,jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。当然这里的最少是最优的选择,而不是越少越好。1、性能定义要查找和评估器性能瓶颈,首先要知道性能定义,对于jvm调优来说,我们需要知道以下三个定义属性,依作为评估基础:吞吐量:重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。延迟:其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。内存占用:垃圾收集器流畅运行所需要 的内存数量。这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。2、性能调优原则在调优过程中,我们应该谨记以下3个原则,以便帮助我们更轻松的完成垃圾收集的调优,从而达到应用程序的性能要求。1. MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。2. GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。3. GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。3、性能调优流程以上就是对应用程序进行jvm调优的基本流程,我们可以看到,jvm调优是根据性能测试结果不断优化配置而多次迭代的过程。在达到每一个系统需求指标之前,之前的每个步骤都有可能经历多次迭代。有时候为了达到某一方面的指标,有可能需要对之前的参数进行多次调整,进而需要把之前的所有步骤重新测试一遍。另外调优一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求,要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。以下我们针对每个步骤进行详细的示例讲解。在JVM的运行模式方面,我们直接选择server模式,这也是jdk1.6以后官方推荐的模式。在垃圾收集器方面,我们直接采用了jdk1.6-1.8 中默认的parallel收集器(新生代采用parallelGC,老生代采用parallelOldGC)。三、确定内存占用在确定内存占用之前,我们需要知道两个知识点:应用程序的运行阶段jvm内存分配1、运行阶段应用程序的运行阶段,我可以划分为以下三个阶段:1、初始化阶段 : jvm加载应用程序,初始化应用程序的主要模块和数据。2、稳定阶段:应用在此时运行了大多数时间,经历过压力测试的之后,各项性能参数呈稳定状态。核心函数被执行,已经被jit编译预热过。3、总结阶段:最后的总结阶段,进行一些基准测试,生成响应的策报告。这个阶段我们可以不关注。确定内存占用以及活跃数据的大小,我们应该是在程序的稳定阶段来进行确定,而不是在项目起初阶段来进行确定,如何确定,我们先看以下jvm的内存分配。2、jvm内存分配&参数jvm堆中主要的空间,就是以上新生代、老生代、永久代组成,整个堆大小=新生代大小 + 老生代大小 + 永久代大小。 具体的对象提升方式,这里不再过多介绍了,我们看下一些jvm命令参数,对堆大小的指定。如果不采用以下参数进行指定的话,虚拟机会自动选择合适的值,同时也会基于系统的开销自动调整。在设置的时候,如果关注性能开销的话,应尽量把永久代的初始值与最大值设置为同一值,因为永久代的大小调整需要进行FullGC 才能实现。3、计算活跃数据大小计算活跃数据大小应该遵循以下流程:如前所述,活跃数据应该是基于应用程序稳定阶段时,观察长期存活与对象在java堆中占用的空间大小。计算活跃数据时应该确保以下条件发生:1.测试时,启动参数采用jvm默认参数,不人为设置。2.确保Full GC 发生时,应用程序正处于稳定阶段。采用jvm默认参数启动,是为了观察应用程序在稳定阶段的所需要的内存使用。如何才算稳定阶段?一定得需要产生足够的压力,找到应用程序和生产环境高峰符合状态类似的负荷,在此之后达到峰值之后,保持一个稳定的状态,才算是一个稳定阶段。所以要达到稳定阶段,压力测试是必不可少的,具体如何如何对应用压力测试,本篇不过多说明,后期会有专门介绍的篇幅。在确定了应用出于稳定阶段的时候,要注意观察应用的GC日志,特别是Full GC 日志。GC日志指令: -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:<filename>GC日志是收集调优所需信息的最好途径,即便是在生产环境,也可以开启GC日志来定位问题,开启GC日志对性能的影响极小,却可以提供丰富数据。必须得有FullGC 日志,如果没有的话,可以采用监控工具强制调用一次,或者采用以下命令,亦可以触发jmap -histo:live pid在稳定阶段触发了FullGC我们一般会拿到如下信息:从以上gc日志中,我们大概可以分析到,在发生fullGC之时,整个应用的堆占用以及GC时间,当然了,为了更加精确,应该多收集几次,获取一个平均值。或者是采用耗时最长的一次FullGC来进行估算。在上图中,fullGC之后,老年代空间占用在93168kb(约93MB),我们以此定为老年代空间的活跃数据。其他堆空间的分配,基于以下规则来进行。基于以上规则和上图中的FullGC信息,我们现在可以规划的该应用堆空间为:java 堆空间: 373Mb (=老年代空间93168kb4)新生代空间:140Mb(=老年代空间93168kb1.5)永久代空间:5Mb(=永久代空间3135kb1.5)老年代空间: 233Mb=堆空间-新生代看空间=373Mb-140Mb对应的应用启动参数应该为:java -Xms373m -Xmx373m -Xmn140m -XX:PermSize=5m -XX:MaxPermSize=5m四、延迟调优在确定了应用程序的活跃数据大小之后,我们需要再进行延迟性调优,因为对于此时堆内存大小,延迟性需求无法达到应用的需要,需要基于应用的情况来进行调试。在这一步进行期间,我们可能会再次优化堆大小的配置,评估GC的持续时间和频率、以及是否需要切换到不同的垃圾收集器上。1、系统延迟需求在调优之前,我们需要知道系统的延迟需求是那些,以及对应的延迟可调优指标是那些。应用程序可接受的平均停滞时间: 此时间与测量的Minor GC持续时间进行比较。可接受的Minor GC频率:Minor GC的频率与可容忍的值进行比较。可接受的最大停顿时间: 最大停顿时间与最差情况下FullGC的持续时间进行比较。可接受的最大停顿发生的频率:基本就是FullGC的频率。以上中,平均停滞时间和最大停顿时间,对用户体验最为重要,可以多关注。基于以上的要求,我们需要统计以下数据:MinorGC的持续时间;统计MinorGC的次数;FullGC的最差持续时间;最差情况下,FullGC的频率;2、优化新生代的大小比如如上的gc日志中,我们可以看到Minor GC的平均持续时间=0.069秒,MinorGC 的频率为0.389秒一次。如果,我们系统的设置的平均停滞时间为50ms,当前的69ms明显是太长了,就需要调整。我们知道新生代空间越大,Minor GC的GC时间越长,频率越低。如果想减少其持续时长,就需要减少其空间大小。如果想减小其频率,就需要加大其空间大小。为了降低改变新生代的大小对其他区域的最小影响。在改变新生代空间大小的时候,尽量保持老年代空间的大小。比如此次减少了新生代空间10%的大小,应该保持老年代和持代的大小不变化,第一步调优后的参数如下变化:java -Xms359m -Xmx359m -Xmn126m -XX:PermSize=5m -XX:MaxPermSize=5m新生代的大小有140m变为126,堆大小顺应变化,此时老年代是没有变化的。3、优化老年代的大小同上一步一样,在优化之前,也需要采集gc日志的数据。此次我们关注的是FullGC的持续时间和频率。上图中,我们可以看到FullGC 平均频率 =5.8sFullGC 平均持续时间=0.14s(以上为了测试,真实项目的fullGC 没有这么快)如果没有FullGC的日志,有办法可以评估么?我们可以通过对象提升率进行计算。对象提升率比如上述中启动参数中,我们的老年代大小=233Mb。那么需要多久才能填满老年代中这233Mb的空闲空间取决于新生代到老年代的提升率。每次提升老年代占用量=每次MinorGC 之后 java堆占用情况 减去 MinorGC后新生代的空间占用对象提升率=平均值(每次提升老年代占用量) 除以 老年代空间有了对象提升率,我们就可以算出填充满老年代空间需要多少次minorGC,大概一次fullGC的时间就可以计算出来了。比如:上图中:第一次minor GC 之后,老年代空间:13740kb - 13732kb =8kb第二次minor GC 之后,老年代空间:22394kb - 17905kb =4489kb第三次minor GC 之后,老年代空间:34739kb - 17917kb =16822kb第四次minor GC 之后,老年代空间:48143kb - 17913kb =30230kb第五次minor GC 之后,老年代空间:62112kb - 17917kb =44195kb老年代每次minorGC提升率4481kb 第二次和第一次minorGC之间12333kb 第3次和第2次minorGC之间13408kb 第4次和第3次minorGC之间13965kb 第5次和第4次minorGC之间我们可以测算出:每次minorGC 的平均提升为12211kb,约为12Mb上图中,平均minorGC的频率为 213ms/次提升率=12211kb/213ms=57kb/ms老年代空间233Mb ,占满大概需要2331024/57=4185ms 约为4.185s。FullGC的预期最差频率时长可以通过以上两种方式估算出来,可以调整老年代的大小来调整FullGC的频率,当然了,如果FullGC持续时间过长,无法达到应用程序的最差延迟要求,就需要切换垃圾处理器了。具体如何切换,下篇再讲,比如切换为CMS,针对CMS的调优方式又有会细微的差别。五、吞吐量调优经过上述漫长 调优过程,最终来到了调优的最后一步,这一步对上述的结果进行吞吐量测试,并进行微调。吞吐量调优主要是基于应用程序的吞吐量要求而来的,应用程序应该有一个综合的吞吐指标,这个指标基于真个应用的需求和测试而衍生出来的。当有应用程序的吞吐量达到或者超过预期的吞吐目标,整个调优过程就可以圆满结束了。如果出现调优后依然无法达到应用程序的吞吐目标,需要重新回顾吞吐要求,评估当前吞吐量和目标差距是否巨大,如果在20%左右,可以修改参数,加大内存,再次从头调试,如果巨大就需要从整个应用层面来考虑,设计以及目标是否一致了,重新评估吞吐目标。对于垃圾收集器来说,提升吞吐量的性能调优的目标就是就是尽可能避免或者很少发生FullGC 或者Stop-The-World压缩式垃圾收集(CMS),因为这两种方式都会造成应用程序吞吐降低。尽量在MinorGC 阶段回收更多的对象,避免对象提升过快到老年代。六、最后据Plumbr公司对特定垃圾收集器使用情况进行了一次调查研究,研究数据使用了84936个案例。在明确指定垃圾收集器的13%的案例中,并发收集器(CMS)使用次数最多;但大多数案例没有选择最佳垃圾收集器。这个比例占用在87%左右。JVM调优是一个系统而又复杂的工作,目前jvm下的自动调整已经做的比较优秀,基本的一些初始参数都可以保证一般的应用跑的比较稳定了,对部分团队来说,程序性能可能优先级不高,默认垃圾收集器已经够用了。调优要基于自己的情况而来。本文作者:wier_ali阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 18, 2019 · 1 min · jiezi

C语言权威指南和书单 - 中等级别

注:点击标题免费下载电子书1. Object-oriented Programming with ANSI-C2. C Interfaces and Implementations3. 21st Century C: C Tips from the New School4. Algorithms in C (Computer Science Series)5. Pointers on CProblem Solving and Program Design in CModern C更多免费电子书资源搜索 | Maning参考The Definitive C Book Guide and List

January 24, 2019 · 1 min · jiezi

阿里毕玄:程序员如何提升自己的硬实力

从业余程序员到职业程序员程序员刚入行时,我觉得最重要的是把自己培养成职业的程序员。我的程序员起步比同龄人都晚了很多,更不用说现在的年轻人了。我大学读的是生物专业,在上大学前基本算是完全没接触过计算机。军训的时候因为很无聊,我和室友每天跑去学校的机房玩,我现在还印象很深刻,我第一次走进机房的时候,别人问,你是要玩windows,还是dos,我那是完全的一抹黑。后来就只记得在机房一堆人都是在练习盲打,军训完,盲打倒是练的差不多了,对计算机就这么产生了浓厚的兴趣,大一的时候都是玩组装机,捣鼓了一些,对计算机的硬件有了那么一些了解。到大二后,买了一些书开始学习当时最火的网页三剑客,学会了手写HTML、PS的基本玩法之类的,课余、暑假也能开始给人做做网站什么的(那个时候做网站真的好赚钱),可能那样过了个一年左右,做静态的网页就不好赚钱了,也不好找实习工作,于是就开始学asp,写些简单的CRUD,做做留言板、论坛这些动态程序,应该算是在这个阶段接触编程了。毕业后加入了深圳的一家做政府行业软件的公司,一个非常靠谱和给我空间的Leader,使得自己在那几年有了不错的成长,终于成了一个职业的程序员。通常来说,业余或半职业的程序员,多数是1个人,或者很小的一个团队一起开发,使得在开发流程、协作工具(例如jira、cvs/svn/git等)、测试上通常会有很大的欠缺,而职业的程序员在这方面则会专业很多。另外,通常职业的程序员做的系统都要运行较长的时间,所以在可维护性上会特别注意,这点我是在加入阿里后理解更深的。一个运行10年的系统,和一个写来玩玩的系统显然是有非常大差别的。这块自己感觉也很难讲清楚,只能说模模糊糊有个这样的概念。通常在有兴趣的基础上,从业余程序员跨越到成为职业程序员我觉得不会太难。编程能力的成长作为程序员,最重要的能力始终是编程能力,就我自己的感受而言,我觉得编程能力的成长主要有这么几个部分:1、编程能力初级:会用编程,首先都是从学习编程语言的基本知识学起的,不论是什么编程语言,有很多共同的基本知识,例如怎么写第一个Hello World、if/while/for、变量等,因此我比较建议在刚刚开始学一门编程语言的时候,看看编程语言自己的一些文档就好,不要上来就去看一些高阶的书。我当年学Java的时候上来就看Think in Java、Effective Java之类的,真心好难懂。除了看文档以外,编程是个超级实践的活,所以一定要多写代码,只有这样才能真正熟练起来。这也是为什么我还是觉得在面试的时候让面试者手写代码是很重要的,这个过程是非常容易判断写代码的熟悉程度的。很多人会说由于写代码都是高度依赖IDE的,导致手写很难,但我绝对相信写代码写了很多的人,手写一段不太复杂的、可运行的代码是不难的。即使像我这种三年多没写过代码的人,让我现在手写一段不太复杂的可运行的Java程序,还是没问题的,前面N年的写代码生涯使得很多东西已经深入骨髓了。我觉得编程能力初级这个阶段对于大部分程序员来说都不会是问题,勤学苦练,是这个阶段的核心。2、编程能力中级:会查和避免问题除了初级要掌握的会熟练的使用编程语言去解决问题外,中级我觉得首先是提升查问题的能力。在写代码的过程中,出问题是非常正常的,怎么去有效且高效的排查问题,是程序员群体中通常能感受到的大家在编程能力上最大的差距。解决问题能力强的基本很容易在程序员群体里得到很高的认可。在查问题的能力上,首先要掌握的是一些基本的调试技巧,好用的调试工具,在Java里有JDK自带的jstat、jmap、jinfo,不在JDK里的有mat、gperf、btrace等。工欲善其事必先利其器,在查问题上是非常典型的,有些时候大家在查问题时的能力差距,有可能仅仅是因为别人比你多知道一个工具而已。除了调试技巧和工具外,查问题的更高境界就是懂原理。一个懂原理的程序员在查问题的水平上和其他程序员是有明显差距的。我想很多的同学应该能感受到,有些时候查出问题的原因仅仅是因为有效的工具,知其然不知其所以然。我给很多阿里的同学培训过Java排查问题的方法,在这个培训里,我经常也会讲到查问题的能力的培养最主要的也是熟练,多尝试给自己写一些会出问题的程序,多积极的看别人是怎么查问题的,多积极的去参与排查问题,很多最后查问题能力强的人多数仅仅是因为“无他,但手熟尔”。我自己排查问题能力的提升主要是在2009年和2010年。那两年作为淘宝消防队(处理各种问题和故障的虚拟团队)的成员,处理了很多的故障和问题。当时消防队还有阿里最公认的技术大神——多隆,我向他学习到了很多排查问题的技巧。和他比,我排查问题的能力就是初级的那种。印象最深刻的是一次我们一起查一个应用cpu us高的问题,我们两定位到是一段代码在某种输入参数的时候会造成cpu us高的原因后,我能想到的继续查的方法是去生产环境抓输入参数,然后再用参数来本地debug看是什么原因。但多隆在看了一会那段代码后,给了我一个输入参数,我拿这个参数一运行,果然cpu us很高!这种case不是一次两次。所以我经常和别人说,我是需要有问题场景才能排查出问题的,但多隆是完全有可能直接看代码就能看出问题的,这是本质的差距。除了查问题外,更厉害的程序员是在写代码的过程就会很好的去避免问题。大家最容易理解的就是在写代码时处理各种异常情况,这里通常也是造成程序员们之间很大的差距的地方。写一段正向逻辑的代码,大部分情况下即使有差距,也不会太大,但在怎么很好的处理这个过程中有可能出现的异常上,这个时候的功力差距会非常明显。很多时候一段代码里处理异常逻辑的部分都会超过正常逻辑的代码量。我经常说,一个优秀程序员和普通程序员的差距,很多时候压根就不需要看什么满天飞的架构图,而只用show一小段的代码就可以。举一个小case大家感受下。当年有一个严重故障,最后查出的原因是输入的参数里有一个是数组,把这个数组里的值作为参数去查数据库,结果前面输入了一个很大的数组,导致从数据库查了大量的数据,内存溢出了,很多程序员现在看都会明白对入参、出参的保护check,但类似这样的case我真的碰到了很多。在中级这个阶段,我会推荐大家尽可能的多刻意的去培养下自己这两个方面的能力,成为一个能写出高质量代码、有效排查问题的优秀程序员。3、编程能力高级:懂高级API和原理就我自己的经历而言,我是在写了多年的Java代码后,才开始真正更细致的学习和掌握Java的一些更高级的API,我相信多数Java程序员也是如此。我算是从2003年开始用Java写商业系统的代码,但直到在2007年加入淘宝后,才开始非常认真地学习Java的IO通信、并发这些部分的API。尽管以前也学过也写过一些这样的代码,但完全就是皮毛。当然,这些通常来说有很大部分的原因会是工作的相关性,多数的写业务系统的程序员可能基本就不需要用到这些,所以导致会很难懂这些相对高级一些的API,但这些API对真正的理解一门编程语言,我觉得至关重要。在之前的程序员成长路线的文章里我也讲到了这个部分,在没有场景的情况下,只能靠自己去创造场景来学习好。我觉得只要有足够的兴趣,这个问题还是不大的,毕竟现在有各种开源,这些是可以非常好的帮助自己创造机会学习的,例如学Java NIO,可以自己基于NIO包一个框架,然后对比Netty,看看哪些写的是不如Netty的,这样会非常有助于真正的理解。在学习高级API的过程中,以及排查问题的过程中,我自己越来越明白懂编程语言的运行原理是非常重要的,因此我到了后面的阶段开始学习Java的编译机制、内存管理、线程机制等。对于我这种非科班出身的而言,学这些会因为缺乏基础更难很多,但这些更原理性的东西学会了后,对自己的编程能力会有质的提升,包括以后学习其他编程语言的能力,学这些原理最好的方法我觉得是先看看一些讲相关知识的书,然后去翻看源码,这样才能真正的更好的掌握,最后是在以后写代码的过程中、查问题的过程中多结合掌握的原理,才能做到即使在N年后也不会忘。在编程能力的成长上,我觉得没什么捷径。我非常赞同1万小时理论,在中级、高级阶段,如果有人指点或和优秀的程序员们共事,会好非常多。不过我觉得这个和读书也有点像,到了一定阶段后(例如高中),天分会成为最重要的分水岭,不过就和大部分行业一样,大部分的情况下都还没到拼天分的时候,只需要拼勤奋就好。系统设计能力的成长除了少数程序员会进入专深的领域,例如Linux Kernel、JVM,其他多数的程序员除了编程能力的成长外,也会越来越需要在系统设计能力上成长。通常一个编程能力不错的程序员,在一定阶段后就会开始承担一个模块的工作,进而承担一个子系统、系统、跨多领域的更大系统等。我自己在工作的第三年开始承担一个流程引擎的设计和实现工作,一个不算小的系统,并且也是当时那个项目里的核心部分。那个阶段我学会了一些系统设计的基本知识,例如需要想清楚整个系统的目标、模块的划分和职责、关键的对象设计等,而不是上来就开始写代码。但那个时候由于我是一个人写整个系统,所以其实对设计的感觉并还没有那么强力的感觉。在那之后的几年也负责过一些系统,但总体感觉好像在系统设计上的成长没那么多,直到在阿里的经历,在系统设计上才有了越来越多的体会。(点击文末阅读原文,查看:我在系统设计上犯过的14个错,可以看到我走的一堆的弯路)。在阿里有一次做分享,讲到我在系统设计能力方面的成长,主要是因为三段经历,负责专业领域系统的设计 -> 负责跨专业领域的专业系统的设计 -> 负责阿里电商系统架构级改造的设计。第一段经历,是我负责HSF。HSF是一个从0开始打造的系统,它主要是作为支撑服务化的框架,是个非常专业领域的系统,放在整个淘宝电商的大系统来看,其实它就是一个很小的子系统,这段经历里让我最深刻的有三点:1).要设计好这种非常专业领域的系统,专业的知识深度是非常重要的。我在最早设计HSF的几个框的时候,是没有设计好服务消费者/提供者要怎么和现有框架结合的,在设计负载均衡这个部分也反复了几次,这个主要是因为自己当时对这个领域掌握不深的原因造成的;2). 太技术化。在HSF的阶段,出于情怀,在有一个版本里投入了非常大的精力去引进OSGi以及去做动态化,这个后来事实证明是个非常非常错误的决定,从这个点我才真正明白在设计系统时一定要想清楚目标,而目标很重要的是和公司发展阶段结合;3). 可持续性。作为一个要在生产环境持续运行很多年的系统而言,怎么样让其在未来更可持续的发展,这个对设计阶段来说至关重要。这里最low的例子是最早设计HSF协议的时候,协议头里竟然没有版本号,导致后来升级都特别复杂;最典型的例子是HSF在早期缺乏了缺乏了服务Tracing这方面的设计,导致后面发现了这个地方非常重要后,全部落地花了长达几年的时间;又例如HSF早期缺乏Filter Chain的设计,导致很多扩展、定制化做起来非常不方便。第二段经历,是做T4。T4是基于LXC的阿里的容器,它和HSF的不同是,它其实是一个跨多领域的系统,包括了单机上的容器引擎,容器管理系统,容器管理系统对外提供API,其他系统或用户通过这个来管理容器。这个系统发展过程也是各种犯错,犯错的主要原因也是因为领域掌握不深。在做T4的日子里,学会到的最重要的是怎么去设计这种跨多个专业领域的系统,怎么更好的划分模块的职责,设计交互逻辑,这段经历对我自己更为重要的意义是我有了做更大一些系统的架构的信心。第三段经历,是做阿里电商的异地多活。这对我来说是真正的去做一个巨大系统的架构师,尽管我以前做HSF的时候参与了淘宝电商2.0-3.0的重大技术改造,但参与和自己主导是有很大区别的,这个架构改造涉及到了阿里电商众多不同专业领域的技术团队。在这个阶段,我学会的最主要的:1). 子系统职责划分。在这种超大的技术方案中,很容易出现某些部分的职责重叠和冲突,这个时候怎么去划分子系统,就非常重要了。作为大架构师,这个时候要从团队的职责、团队的可持续性上去选择团队;2). 大架构师最主要的职责是控制系统风险。对于这种超大系统,一定是多个专业领域的架构师和大架构师共同设计,怎么确保在执行的过程中对于系统而言最重要的风险能够被控制住,这是我真正的理解什么叫系统设计文档里设计原则的部分。设计原则我自己觉得就是用来确保各个子系统在设计时都会遵循和考虑的,一定不能是虚的东西,例如在异地多活架构里,最重要的是如何控制数据风险,这个需要在原则里写上,最基本的原则是可接受系统不可用,但也要保障数据一致,而我看过更多的系统设计里设计原则只是写写的,或者千篇一律的,设计原则切实的体现了架构师对目标的理解(例如当时异地多活这个其实开始只是个概念,但做到什么程度才叫做到异地多活,这是需要解读的,也要确保在技术层面的设计上是达到了目标的),技术方案层面上的选择原则,并确保在细节的设计方案里有对于设计原则的承接以及执行;3). 考虑问题的全面性。像异地多活这种大架构改造,涉及业务层面、各种基础技术层面、基础设施层面,对于执行节奏的决定要综合考虑人力投入、机器成本、基础设施布局诉求、稳定性控制等,这会比只是做一个小的系统的设计复杂非常多。系统设计能力的成长,我自己觉得最重要的一是先在一两个技术领域做到专业,然后尽量扩大自己的知识广度。例如除了自己的代码部分外,还应该知道具体是怎么部署的,部署到哪去了,部署的环境具体是怎么样的,和整个系统的关系是什么样的。像我自己,是在加入基础设施团队后才更加明白有些时候软件上做的一个决策,会导致基础设施上巨大的硬件、网络或机房的投入,但其实有可能只需要在软件上做些调整就可以避免,做做研发、做做运维可能是比较好的把知识广度扩大的方法。第二点是练习自己做tradeoff的能力,这个比较难,做tradeoff这事需要综合各种因素做选择,但这也是所有的架构师最关键的,可以回头反思下自己在做各种系统设计时做出的tradeoff是什么。这个最好是亲身经历,听一些有经验的架构师分享他们选择背后的逻辑也会很有帮助,尤其是如果恰好你也在同样的挑战阶段,光听最终的架构结果其实大多数时候帮助有限。技术Leader我觉得最好是能在架构师的基础上,后续注重成长的方面还是有挺大差别,就不在这篇里写了,后面再专门来写一篇。程序员金字塔我认为程序员的价值关键体现在作品上,被打上作品标签是一种很大的荣幸,作品影响程度的大小我觉得决定了金字塔的层次,所以我会这么去理解程序员的金字塔。当然,要打造一款作品,仅有上面的两点能力是不够的,作品里很重要的一点是对业务、技术趋势的判断。希望作为程序员的大伙,都能有机会打造一款世界级的作品,去为技术圈的发展做出贡献。由于目前IT技术更新速度还是很快的,程序员这个行当是特别需要学习能力的。我一直认为,只有对程序员这个职业真正的充满兴趣,保持自驱,才有可能在这个职业上做好,否则的话是很容易淘汰的。作者简介:毕玄,2007年加入阿里,十多年来主要从事在软件基础设施领域,先后负责阿里的服务框架、Hbase、Sigma、异地多活等重大的基础技术产品和整体架构改造。本文作者:云效鼓励师阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 24, 2019 · 1 min · jiezi

C语言权威指南和书单 - 适用于所有级别

注:点击标题免费下载电子书所有级别1. The C Programming Language (2nd Edition)2. C: A Reference Manual (5th Edition)3. C语言常见问题4. C标准5. The new C standard - an annotated reference6. Rationale for C99 Standard7. The new C standard - an annotated reference更多免费电子书资源搜索 | Maning参考The Definitive C Book Guide and List

January 23, 2019 · 1 min · jiezi

C语言权威指南和书单 - 初学者

注:点击标题免费下载电子书1. C Primer Plus (5th Edition)2. A Book on CC Programming: A Modern Approach (2nd Edition)3. The C Book4. Beginning C (5th Edition)5. Sams Teach Yourself C in 21 Days6. Head First C7. C How to Program更多免费电子书资源搜索 | Maning参考The Definitive C Book Guide and List

January 23, 2019 · 1 min · jiezi

一份关于机器学习端到端学习指南

摘要: 本文是一份关于机器学习端到端过程的指导指南,其中列出了实现机器学习模型所需遵循的步骤及对应的参考文章;人工智能、机器学习已经火了有一阵了,很多程序员也想换到这方向,目前有关于深度学习基础介绍的材料很多,但很难找到一篇简洁的文章提供实施机器学习项目端到端的指南,从头到尾整个过程的相关指南介绍。因此,个人在网上搜集到了许多有关于实施机器学习项目过程的文章,深入介绍了如何实现机器学习/数据科学项目的各个部分,但更多时候,我们只需要一些概括性的经验指导。在我不熟悉机器学习和数据科学的时候,我曾经寻找一些指导性的文章,这些文章清楚地阐述了在项目的某些步骤时候我需要做什么才能很好地完成我的项目。本文将介绍一些文章,旨在为成功实现机器学习项目提供一份端到端的指南。基于此,闲话少叙,下面让我们开始吧简而言之,机器学习项目有三个主要部分:第一部分是数据理解、数据收集和清理,第二部分是模型的实现,第三部分是进行模型优化。一般而言,数据理解、收集和清理需要花费整个项目60-70%的时间。为此,我们需要该领域专家。场景假设现在假设我们正在尝试一个机器学习项目。本文将为你提供实施项目可以遵循的步骤指南,确保项目成功。在项目开始时,我们的大脑中肯定会出现多个问题:比如:如何开始这个项目?需要开发者或者统计学者?选择何种语言进行开发?数据集是否干净?各种依赖包是否安装齐全正确?项目问题是回归还是分类问题?应该采取何种机器学习方法?如何调参?机器学习项目简单来讲是一种试错过程,整个研究过程和递归过程比较类似,是一种不断试错寻找更优解法的过程。该过程同时也是实践和理论的结合,对相关研究领域的专业知识有所要求,完成每个项目后也会提升个人的战略技能,从事该领域的研究人员需要掌握统计相关的知识以及具备一定的编程能力。最重要的是,机器项目会教会你保持耐心,每做完一次实验后,都会分析实验结果,进而寻找到更优的答案(调参)。步骤必须进行的两个步骤:1.确保你了解机器学习是什么以及它的三个关键领域。可以阅读下面这篇文章:8分钟了解机器学习机器学习是现在,也是未来。所有的技术人员、数据科学家和金融专家可以从中受益,同时,如果上述这些人员在之后的日子不对该项技术有所涉猎的话,很可能会被时代所淘汰。2.选择合适的编程语言。需要熟练掌握Python,请点击阅读:从零开始学pythonPython是数据分析和机器学习最流行的编程语言中的一种,并且有很多封装好的工具包可供我们调用,实现起来相对而言比较简单。开始实施1.选择合适的机器学习算法。如何选择合适的算法可以参考下面这篇文章:机器学习算法大乱斗 现实中,不管是工业界,亦或是学业界,都有大量的机器学习算法可供使用。上述文章中将分析典型的机器学习算法各自的优缺点,及针对的具体问题。 到目前为止,你可能已经理解了你需要解决的项目问题是有监督问题还是无监督问题。 然而,机器学习不像经典的程序设计一样(给定一个输入,其输出是固定), 机器学习总有可能找到另外一个正确的答案。比如,预测问题中通常有多个正确的答案。2.如果这是一个有监督的机器学习问题,那么请确保你了解该项目是回归还是分类问题。想弄清楚这点可以阅读下面这篇文章:有监督的机器学习:回归与分类在上述文章中,将阐述有监督学习中回归问题和分类问题之间的关键差异。3.如果是时间序列回归问题,则在预测时间之前使时间序列数据保持不变。具体做法参考下面这篇文章:我是如何预测时间序列?预测、建模和推导时间序列在许多领域越来越受欢迎。时间序列一般用于预测未来。4.找出一种预先测量算法性能的方法。如何确定评测指标可以参考下面这篇文章:每个数据科学家必须知道的数学度量方法每个数据科学家都需要了解大量的数学度量方法,比如准确度、AUC。5.测量时间序列回归模型的性能。可以参考下面这篇文章:搭建的预测模型性能有多好——回归分析预测是计量经济学和数据科学中的一个重要概念,它也广泛用于人工智能中。6.调查是否需要使用ARIMA模型。详细内容请参考下面这篇文章:了解差分整合移动平均自回归模型——ARIMA 在文章“如何预测时间序列?”中,提供了关于时间序列分析的相关概述。这篇文章的核心是了解ARIMA模型。7.如果是无监督的机器学习问题,那么需要了解群集是如何工作和实施的。详细内容请参考下面这篇文章:无监督机器学习:聚类和K均值算法上述文章解释了聚类在无监督机器学习中的工作原理。8.探索神经网络和深度学习,看看它是否适用于你的问题。详细内容请参考下面这篇文章:了解神经网络:从激活函数到反向传播上述文章旨在阐述神经网络的基本概述,讲解基本概念,包含激活函数、反向传播算法。9.丰富你的特征集合,对其进行缩放、标准化和归一化等。详细内容请参考下面这篇文章:处理数据以提高机器学习模型的准确性有时我们会建立一个机器学习模型,用我们的训练数据训练它,当我们训练好后进行预测时,效果并不是很理想,有部分原因是数据集存在脏数据或不够全面,因此需要对数据进行进一步的处理,比如数据清洗、增强等。干净的数据=良好的结果。10.减少特征尺寸空间。详细内容请参考下面这篇文章:在数据科学中,什么是降维?虽然现在是大数据时代,有很多的数据可供使用,大量数据可以促使我们创建一个预测模型,但数据量不是越大越好,而是越精越好。如果在丰富特征并减小尺寸后,模型没有产生准确的结果,那么需要重新调整模型的参数。11.微调机器学习模型参数。详细内容请参考下面这篇文章:如何微调机器学习模型以提高模型的预测准确性?微调机器学习预测模型是提高预测结果准确性的关键步骤。这个过程有些枯燥,需要一些耐心和运气。始终确保模型不会过拟合或欠拟合。12.最后,重复这些步骤,直到获得准确的结果:丰富模型特征;微调模型参数;始终对数据集进行分析,看看是否缺少任何的重要信息,在看到问题时解决问题,但在开始进行新的实验前,始终需要备份并保存你前一份的工作内容,这是一个好的习惯么,因为你可能需要返回上一步再一次进行其它的实验。机器学习在本质上是回溯过程。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 18, 2019 · 1 min · jiezi

如何设计一个 RPC 系统

本文由云+社区发表RPC是一种方便的网络通信编程模型,由于和编程语言的高度结合,大大减少了处理网络数据的复杂度,让代码可读性也有可观的提高。但是RPC本身的构成却比较复杂,由于受到编程语言、网络模型、使用习惯的约束,有大量的妥协和取舍之处。本文就是通过分析几种流行的RPC实现案例,提供大家在设计RPC系统时的参考。由于RPC底层的网络开发一般和具体使用环境有关,而编程实现手段也非常多样化,但不影响使用者,因此本文基本涉及如何实现一个RPC系统。认识 RPC (远程调用)我们在各种操作系统、编程语言生态圈中,多少都会接触过“远程调用”的概念。一般来说,他们指的是用简单的一行代码,通过网络调用另外一个计算机上的某段程序。比如:RMI——Remote Method Invoke:调用远程的方法。“方法”一般是附属于某个对象上的,所以通常RMI指对在远程的计算机上的某个对象,进行其方法函数的调用。RPC——Remote Procedure Call:远程过程调用。指的是对网络上另外一个计算机上的,某段特定的函数代码的调用。远程调用本身是网络通信的一种概念,他的特点是把网络通信封装成一个类似函数的调用。网络通信在远程调用外,一般还有其他的几种概念:数据包处理、消息队列、流过滤、资源拉取等待。下面比较一下他们差异:方案编程方式信息封装传输模型典型应用远程调用调用函数,输入参数,获得返回值。使用编程语言的变量、类型、函数发出请求,获得响应Java RMI数据包处理调用Send()/Recv(),使用字节码数据,编解码,处理内容把通信内容构造成二进制的协议包发送/接收UDP编程消息队列调用Put()/Get(),使用“包”对象,处理其包含的内容消息被封装成语言可用的对象或结构对某队列,存入一个消息;取出一个消息ActiveMQ流过滤读取一个流,或写出一个流,对流中的单元包即刻处理单元长度很小的统一数据结构连接;发送/接收;处理网络视频资源拉取输入一个资源ID,获得资源内容请求或响应都包含:头部+正文请求后等待响应WWW针对远程调用的特点——调用函数。业界在各种语言下都开发过类似的方案,同时也有些方案是试图做到跨语言的。尽管远程调用在编程方式上,看起来似乎是最简单易用的,但是也有明显的缺点。所以了解清楚远程调用的优势和缺点,是决定是否要开发、或者使用远程调用这种模型的关键问题。远程调用的优势有:屏蔽了网络层。因此在传输协议和编码协议上,我们可以选择不同的方案。比如WebService方案就是用的HTTP传输协议+SOAP编码协议;而REST的方案往往使用HTTP+JSON协议。Facebook的Thrift甚至可以定制任何不同的传输协议和编码协议,你可以用TCP+Google Protocol Buffer,也可以用UDP+JSON……。由于屏蔽了网络层,你可以根据实际需要来独立的优化网络部分,而无需涉及业务逻辑的处理代码,这对于需要在各种网络环境下运行的程序来说,非常有价值。函数映射协议。你可以直接用编程语言来书写数据结构和函数定义,取代编写大量的编码协议格式和分包处理逻辑。对于那些业务逻辑非常复杂的系统,比如网络游戏,可以节省大量定义消息格式的时间。而且函数调用模型非常容易学习,不需要学习通信协议和流程,让经验较浅的程序员也能很容易的开始使用网络编程。远程调用的缺点:增加了性能消耗。由于把网络通信包装成“函数”,需要大量额外的处理。比如需要预生产代码,或者使用反射机制。这些都是额外消耗CPU和内存的操作。而且为了表达复杂的数据类型,比如变长的类型string/map/list,这些都要数据包中增加更多的描述性信息,则会占用更多的网络包长度。不必要的复杂化。如果你仅仅是为了某些特定的业务需求,比如传送一个固定的文件,那么你应该用HTTP/FTP协议模型。如果为了做监控或者IM软件,用简单的消息编码收发会更快速高效。如果是为了做代理服务器,用流式的处理会很简单。另外,如果你要做数据广播,那么消息队列会很容易做到,而远程调用这几乎无法完成。因此,远程调用最适合的场景是:业务需求多变,网络环境多变。RPC方案的核心问题由于远程调用的使用接口是“函数”,所以要如何构建这个“函数”,就产生了三个方面需要决策的问题:1 . 如何表示“远程”的信息所谓远程,就是指网络上另外一个位置,那么网络地址就是必须要输入的部分。在TCP/IP网络下,IP地址和端口号代表了运行中程序的一个入口。所以指定IP地址和端口是发起远程调用所必需的。然而,一个程序可能会运行很多个功能,可以接收多个不同含义的远程调用。这样如何去让用户指定这些不同含义的远程调用入口,就成为了另外一个问题。当然最简单的是每个端口一种调用,但是一个IP最多支持65535个端口,而且别的网络功能也可能需要端口,所以这种方案可能会不够用,同时一个数字代表一个功能也不太好理解,必须要查表才能明白。所以我们必须想别的方法。在面向对象的思想下,有些方案提出了:以不同的对象来归纳不同的功能组合,先指定对象,再指定方法。这个想法非常符合程序员的理解方式,EJB就是这种方案的。一旦你确定了用对象这种模型来定义远程调用的地址,那么你就需要有一种指定远程对象的方法,为了指定对象,你必须要能把对象的一些信息,从被调用方(服务器端)传输给调用方(客户端)。最简单的方案就是客户端输入一串字符串作为对象的“名字”,发给服务器端,查找注册了这个“名字”的对象,如果找到了,服务器端就会用某种技术“传输”这个对象给客户端,然后客户端就可以调用他的方法了。当然这种传输不可能是把整个服务器上的对象数据拷贝给客户端,而是用一些符号或者标志的方法,来代表这个服务器上的对象,然后发给客户端。如果你不是使用面向对象的模型,那么远程的一个函数,也是必须要定位和传输的,因为你调用的函数必须先能找到,然后成为客户端侧的一个接口,才能调用。针对“远程对象”(这里说的对象包括面向对象的对象或者仅仅是 函数)如何表达才能在网络上定位;以及定位成功之后以什么形式供客户端调用,都是“远程调用”设计方案中第一个重要的问题。2 . 函数的接口形式应该如何表示远程调用由于受到网络通信的约束,所以往往不能完全的支持编程语言的所有特性。比如C语言函数中的指针类型参数,就无法通过网络传递出去。因此远程调用的函数定义,能用语言中的什么特性,不能用什么特性,是需要在设计方案是规定下来的。这种规定如果太严格,会影响使用者的易用性;如果太宽泛,则可能导致远程调用的性能低下。如何去设计一种方式,把编程语言中的函数,描述成一个远程调用的函数,也是需要考虑的问题。很多方案采用了配置文件这种通用的方式,而另外一些方案可以直接在源代码中里面加特殊的注释。一般来说,编译型语言如C/C++只能采用源代码根据配置文件生成的方案,虚拟机型语言如C#/JAVA可以采用反射机制结合配置文件(设置是在源代码中用特殊注释来代替配置文件)的方案,如果是脚本语言就更简单,有时候连配置文件都不需要,因为脚本自己就可以充当。总之远程调用的接口要满足怎样的约束,也是一个需要仔细考虑的问题。3. 用什么方法来实现网络通信远程调用最重要的实现细节,就是关于网络通信。用何种通信方式来承载远程调用的问题,细化下来就是两个子问题:用什么样的服务程序提供网络功能?用什么样的通信协议?远程调用系统可以自己直接对TCP/IP编程来实现通信,也可以委托一些其他软件,比如Web服务器、消息队列服务器等等……也可以使用不同的网络通信框架,如Netty/Mina这些开源框架。通信协议则一般有两层:一个是传输协议,比如TCP/UDP或者高层一点的HTTP,或者自己定义的传输协议;另外一个是编码协议,就是如何把一个编程语言中的对象,序列化和反序列化成为二进制字节流的方案,流行的方案有JSON、Google Protocol Buffer等等,很多开发语言也有自己的序列化方案,如JAVA/C#都自带。以上这些技术细节,应该选择使用哪些,直接关系到远程调用系统的性能和环境兼容性。以上三个问题,就是远程调用系统必须考虑的核心选型。根据每个方案所面对的约束不同,他们都会在这三个问题上做出取舍,从而适应其约束。但是现在并不存在一个“万能”或者“通用”的方案,其原因就是:在如此复杂的一个系统中,如果要照顾的特性越多,需要付出的成本(易用性代价、性能开销)也会越多。下面,我们可以研究下业界现存的各种远程调用方案,看他们是如何在这三个方面做平衡和选择的。业界方案举例1. CORBACORBA是一个“古老”的,雄心勃勃的方案,他试图在完成远程调用的同时,还完成跨语言的通信的任务,因此其复杂程度是最高的,但是它的设计思想,也被后来更多的其他方案所学习。在通信对象的定位上,它使用URL来定义一个远程对象,这是在互联网时代非常容易接受的。其对象的内容则限定在C语言类型上,并且只能传递值,这也是非常容易理解的。为了能让不同语言的程序通信,所以就必须要在各种编程语言之外独立设计一种仅仅用于描述远程接口的语言,这就是所谓的IDL:Interface Description Language 接口描述语言。用这个方法,你就可以先用一种超然于所有语言之外的语言来定义接口,然后使用工具自动生成各种编程语言的代码。这种方案对于编译型语言几乎是唯一选择。CORBA并没有对通信问题有任何约定,而是留给具体语言的实现者去处理,这也许是他没有广泛流行的原因之一。实际上CORBA有一个非常著名的继承者,他就是Facebook公司的Thrift框架。Thrift也是使用一种IDL编译生成多种语言的远程调用方案,并且用C++/JAVA等多种语言完整的实现了通信承载,所以在开源框架中是特别有号召力的一个。Thrfit的通信承载还有个特点,就是能组合使用各种不同的传输协议和编码协议,比如TCP/UDP/HTTP配合JSON/BIN/PB……这让它几乎可以选择任何的网络环境。Thrift的模型类似下图,这里有的stub表示“桩代码”,就是客户端直接使用的函数形式程序;skeleton表示“骨架代码”,是需要程序员编写具体提供远程服务功能的模板代码,一般对模版做填空或者继承(扩展)即可。这个stub-skeleton模型几乎是所有远程调用方案的标配。2. JAVA RMIJAVA RMI是JAVA虚拟机自带的一个远程调用方案。它也是可以使用URL来定位远程对象,使用JAVA自带的序列化编码协议传递参数值。在接口描述上,由于这是一个仅限于JAVA环境下的方案,所以直接用JAVA语言的Interface类型作为定义语言。用户通过实现这个接口类型来提供远程服务,同时JAVA会根据这个接口文件自动生成客户端的调用代码供调用者使用。他的底层通信实现,还是用TCP协议实现的。在这里,Interface文件就是JAVA语言的IDL,同时也是skeleton模板,供开发者来填写远程服务内容。而stub代码则由于JAVA的反射功能,由虚拟机直接包办了。这个方案由于JAVA虚拟机的支持,使用起来非常简单,完全按照标志的JAVA编程方法就可以轻松解决问题,但是这也仅仅能在JAVA环境下运行,限制了其适用的范围。鱼与熊掌不可兼得,易用性和适用性往往是互相冲突的。这和CORBA/Thrift追求最大范围的适用性有很大的差别,也导致了两者在易用性上的不同。3. Windows RPCWindows中对RPC支持是比较早和比较完善的。首先它通过GUID来查询对象,然后使用C语言类型作为参数值的传递。由于Windows的API主要是C语言的,所以对于RPC功能来说,还是要用一种IDL来描述接口,最后生成.h和.c文件来生产RPC的stub和skeleton代码。而通信机制,由于是操作系统自带的,所以使用内核LPC机制承载,这一点还是对使用者来说比较方便的。但是也限制了只能用于Windows程序之间做调用。4. WebService & REST在互联网时代,程序需要通过互联网来互相调用。而互联网上最流行的协议是HTTP协议和WWW服务,因此使用HTTP协议的Web Service就顺理成章的成为跨系统调用的最流行方案。由于可以使用大多数互联网的基础设施,所以Web Service的开发和实现几乎是毫无难度的。一般来说,它都会使用URL来定位远程对象,而参数则通过一系列预定义的类型(主要是C语言基础类型),以及对象序列化方式来传递。接口生成方面,你可以自己直接对HTTP做解析,也可以使用诸如WSDL或者SOAP这样的规范。在REST的方案中,则限定了只有PUT/GET/DELETE/POST四种操作函数,其他都是参数。总结一下上面的这些RPC方案,我们发现,针对远程调用的三个核心问题,一般业界有以下几个选择:远程对象定位:使用URL;或者使用名字服务来查找远程调用参数传递:使用C的基本类型定义;或者使用某种预订的序列化(反序列化)方案接口定义:使用某种特定格式的技术,直接按预先约定一种接口定义文件;或者使用某种描述协议IDL来生成这些接口文件通信承载:有使用特定TCP/UDP之类的服务器,也有可以让用户自己开发定制的通信模型;还有使用HTTP或者消息队列这一类更加高级的传输协议方案选型在我们确定了远程调用系统方案几个可行选择后,自然就要明确一下各个方案的优缺点,这样才能选择真正合适需求的设计:1. 对于远程对象的描述:使用URL是互联网通行的标准,比较方便用户理解,也容易添加日后需要扩展到内容,因为URL本身是一个由多个部分组合的字符串;而名字服务则老式一些,但是依然有他的好处,就是名字服务可以附带负载均衡、容灾扩容、自定义路由等一系列特性,对于需求复杂的定位比较容易实现。2. 远程调用的接口描述:如果只限制于某个语言、操作系统、平台上,直接利用“隐喻”方式的接口描述,或者以“注解”类型注释手段来标注源代码,实现远程调用接口的定义,是最方便不过的。但是,如果需要兼容编译型语言,如C/C++,就一定要用某种IDL来生成这些编译语言的源代码了。3.通信承载:给用户自己定制通信模块,能提供最好的适用性,但是也让用户增加了使用的复杂程度。而HTTP/消息队列这种承载方式,在系统的部署、运维、编程上都会比较简单,缺点就是对于性能、传输特性的定制空间就比较小。分析完核心问题,我们还需要考虑一些适用性场景:1. 面向对象还是面向过程:如果我们只是考虑做面向过程的远程调用,只需要定位到“函数”即可。而如果是面向对象的,则需要定位到“对象”。由于函数是无状态的,所以其定位过程可以简单到一个名字即可,而对象则需要动态的查找到其ID或句柄。2.跨语言还是单一语言:单一语言的方案中,头文件或接口定义完全用一种语言处理即可,如果是跨语言的,就少不免要IDL3. 混合式通信承载还是使用HTTP服务器承载:混合式承载可能可以用到TCP/UDP/共享内存等底层技术,可以提供最优的性能,但是使用起来必然非常麻烦。使用HTTP服务器的话,则非常简单,因为WWW服务的开源软件、库众多,而且客户端使用浏览器或者一些JS页面即可调试,缺点是其性能较低。假设我们现在要为某种业务逻辑非常多变的领域,如企业业务应用领域,或游戏服务器端领域,去设计一个远程调用系统,我们可能应该如下选择:1. 使用名字服务定位远程对象:由于企业服务是需要高可用性的,使用名字服务能在查询名字时识别和选择可用性服务对象。J2EE方案中的EJB(企业JavaBean)就是用名字服务的。2. 使用IDL来生成接口定义:由于企业服务或游戏服务,其开发语言可能不是统一的,又或者需要高性能的编程语言如C/C++,所以只能使用IDL。3.使用混合式通信承载:虽然企业服务看起来无需在很复杂的网络下运行,但是不同的企业的网络环境又可能是千差万别的,所以要做一个通用的系统,最好还是不怕麻烦提供混合式的通信承载,这样可以在TCP/UDP等各种协议中选择。此文已由作者授权腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

January 3, 2019 · 1 min · jiezi

自己动手写事件总线(EventBus)

本文由云+社区发表事件总线核心逻辑的实现。<!–more–>EventBus的作用Android中存在各种通信场景,如Activity之间的跳转,Activity与Fragment以及其他组件之间的交互,以及在某个耗时操作(如请求网络)之后的callback回调等,互相之之间往往需要持有对方的引用,每个场景的写法也有差异,导致耦合性较高且不便维护。以Activity和Fragment的通信为例,官方做法是实现一个接口,然后持有对方的引用,再强行转成接口类型,导致耦合度偏高。再以Activity的返回为例,一方需要设置setResult,而另一方需要在onActivityResult做对应处理,如果有多个返回路径,代码就会十分臃肿。而SimpleEventBus(本文最终实现的简化版事件总线)的写法如下:public class MainActivity extends AppCompatActivity { TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.tv_demo); mTextView.setText(“MainActivity”); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); } }); EventBus.getDefault().register(this); } @Subscribe(threadMode = ThreadMode.MAIN) public void onReturn(Message message) { mTextView.setText(message.mContent); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); }}来源Activity:public class SecondActivity extends AppCompatActivity { TextView mTextView; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.tv_demo); mTextView.setText(“SecondActivity,点击返回”); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Message message = new Message(); message.mContent = “从SecondActivity返回”; EventBus.getDefault().post(message); finish(); } }); }}效果如下:似乎只是换了一种写法,但在场景愈加复杂后,EventBus能够体现出更好的解耦能力。背景知识主要涉及三方面的知识:观察者模式(or 发布-订阅模式)Android消息机制Java并发编程本文可以认为是greenrobot/EventBus这个开源库的源码阅读指南,笔者在看设计模式相关书籍的时候了解到这个库,觉得有必要实现一下核心功能以加深理解。实现过程EventBus的使用分三个步骤:注册监听、发送事件和取消监听,相应本文也将分这三步来实现。注册监听定义一个注解:@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Subscribe { ThreadMode threadMode() default ThreadMode.POST;}greenrobot/EventBus还支持优先级和粘性事件,这里只支持最基本的能力:区分线程,因为如更新UI的操作必须放在主线程。ThreadMode如下:public enum ThreadMode { MAIN, // 主线程 POST, // 发送消息的线程 ASYNC // 新开一个线程发送}在对象初始化的时候,使用register方法注册,该方法会解析被注册对象的所有方法,并解析声明了注解的方法(即观察者),核心代码如下:public class EventBus { … public void register(Object subscriber) { if (subscriber == null) { return; } synchronized (this) { subscribe(subscriber); } } … private void subscribe(Object subscriber) { if (subscriber == null) { return; } // TODO 巨踏马难看的缩进 Class<?> clazz = subscriber.getClass(); while (clazz != null && !isSystemClass(clazz.getName())) { final Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { Subscribe annotation = method.getAnnotation(Subscribe.class); if (annotation != null) { Class<?>[] paramClassArray = method.getParameterTypes(); if (paramClassArray != null && paramClassArray.length == 1) { Class<?> paramType = convertType(paramClassArray[0]); EventType eventType = new EventType(paramType); SubscriberMethod subscriberMethod = new SubscriberMethod(method, annotation.threadMode(), paramType); realSubscribe(subscriber, subscriberMethod, eventType); } } } clazz = clazz.getSuperclass(); } } … private void realSubscribe(Object subscriber, SubscriberMethod method, EventType eventType) { CopyOnWriteArrayList<Subscription> subscriptions = mSubscriptionsByEventtype.get(subscriber); if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); } Subscription subscription = new Subscription(subscriber, method); if (subscriptions.contains(subscription)) { return; } subscriptions.add(subscription); mSubscriptionsByEventtype.put(eventType, subscriptions); } …}执行过这些逻辑后,该对象所有的观察者方法都会被存在一个Map中,其Key是EventType,即观察事件的类型,Value是订阅了该类型事件的所有方法(即观察者)的一个列表,每个方法和对象一起封装成了一个Subscription类:public class Subscription { public final Reference<Object> subscriber; public final SubscriberMethod subscriberMethod; public Subscription(Object subscriber, SubscriberMethod subscriberMethod) { this.subscriber = new WeakReference<>(subscriber);// EventBus3 没用弱引用? this.subscriberMethod = subscriberMethod; } @Override public int hashCode() { return subscriber.hashCode() + subscriberMethod.methodString.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof Subscription) { Subscription other = (Subscription) obj; return subscriber == other.subscribe && subscriberMethod.equals(other.subscriberMethod); } else { return false; } }}如此,便是注册监听方法的核心逻辑了。消息发送消息的发送代码很简单:public class EventBus { … private EventDispatcher mEventDispatcher = new EventDispatcher(); private ThreadLocal<Queue<EventType>> mThreadLocalEvents = new ThreadLocal<Queue<EventType>>() { @Override protected Queue<EventType> initialValue() { return new ConcurrentLinkedQueue<>(); } }; … public void post(Object message) { if (message == null) { return; } mThreadLocalEvents.get().offer(new EventType(message.getClass())); mEventDispatcher.dispatchEvents(message); } …}比较复杂一点的是需要根据注解声明的线程模式在对应的线程进行发布:public class EventBus { … private class EventDispatcher { private IEventHandler mMainEventHandler = new MainEventHandler(); private IEventHandler mPostEventHandler = new DefaultEventHandler(); private IEventHandler mAsyncEventHandler = new AsyncEventHandler(); void dispatchEvents(Object message) { Queue<EventType> eventQueue = mThreadLocalEvents.get(); while (eventQueue.size() > 0) { handleEvent(eventQueue.poll(), message); } } private void handleEvent(EventType eventType, Object message) { List<Subscription> subscriptions = mSubscriptionsByEventtype.get(eventType); if (subscriptions == null) { return; } for (Subscription subscription : subscriptions) { IEventHandler eventHandler = getEventHandler(subscription.subscriberMethod.threadMode); eventHandler.handleEvent(subscription, message); } } private IEventHandler getEventHandler(ThreadMode mode) { if (mode == ThreadMode.ASYNC) { return mAsyncEventHandler; } if (mode == ThreadMode.POST) { return mPostEventHandler; } return mMainEventHandler; } }// end of the class …}三种线程模式分别如下,DefaultEventHandler(在发布线程执行观察者放方法):public class DefaultEventHandler implements IEventHandler { @Override public void handleEvent(Subscription subscription, Object message) { if (subscription == null || subscription.subscriber.get() == null) { return; } try { subscription.subscriberMethod.method.invoke(subscription.subscriber.get(), message); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } }}MainEventHandler(在主线程执行):public class MainEventHandler implements IEventHandler { private Handler mMainHandler = new Handler(Looper.getMainLooper()); DefaultEventHandler hanlder = new DefaultEventHandler(); @Override public void handleEvent(final Subscription subscription, final Object message) { mMainHandler.post(new Runnable() { @Override public void run() { hanlder.handleEvent(subscription, message); } }); }}AsyncEventHandler(新开一个线程执行):public class AsyncEventHandler implements IEventHandler { private DispatcherThread mDispatcherThread; private IEventHandler mEventHandler = new DefaultEventHandler(); public AsyncEventHandler() { mDispatcherThread = new DispatcherThread(AsyncEventHandler.class.getSimpleName()); mDispatcherThread.start(); } @Override public void handleEvent(final Subscription subscription, final Object message) { mDispatcherThread.post(new Runnable() { @Override public void run() { mEventHandler.handleEvent(subscription, message); } }); } private class DispatcherThread extends HandlerThread { // 关联了AsyncExecutor消息队列的Handle Handler mAsyncHandler; DispatcherThread(String name) { super(name); } public void post(Runnable runnable) { if (mAsyncHandler == null) { throw new NullPointerException(“mAsyncHandler == null, please call start() first.”); } mAsyncHandler.post(runnable); } @Override public synchronized void start() { super.start(); mAsyncHandler = new Handler(this.getLooper()); } }}以上便是发布消息的代码。注销监听最后一个对象被销毁还要注销监听,否则容易导致内存泄露,目前SimpleEventBus用的是WeakReference,能够通过GC自动回收,但不知道greenrobot/EventBus为什么没这样实现,待研究。注销监听其实就是遍历Map,拿掉该对象的订阅即可:public class EventBus { … public void unregister(Object subscriber) { if (subscriber == null) { return; } Iterator<CopyOnWriteArrayList<Subscription>> iterator = mSubscriptionsByEventtype.values().iterator(); while (iterator.hasNext()) { CopyOnWriteArrayList<Subscription> subscriptions = iterator.next(); if (subscriptions != null) { List<Subscription> foundSubscriptions = new LinkedList<>(); for (Subscription subscription : subscriptions) { Object cacheObject = subscription.subscriber.get(); if (cacheObject == null || cacheObject.equals(subscriber)) { foundSubscriptions.add(subscription); } } subscriptions.removeAll(foundSubscriptions); } // 如果针对某个Event的订阅者数量为空了,那么需要从map中清除 if (subscriptions == null || subscriptions.size() == 0) { iterator.remove(); } } } …}以上便是事件总线最核心部分的代码实现,完整代码见vimerzhao/SimpleEventBus,后面发现问题更新或者进行升级也只会改动仓库的代码。局限性由于时间关系,目前只研究了EventBus的核心部分,还有几个值得深入研究的点,在此记录一下,也欢迎路过的大牛指点一二。性能问题实际使用时,注解和反射会导致性能问题,但EventBus3已经通过Subscriber Index基本解决了这一问题,实现也非常有意思,是通过注解处理器(Annotation Processor)把耗时的逻辑从运行期提前到了编译期,通过编译期生成的索引来给运行期提速,这也是这个名字的由来。可用性问题如果订阅者很多会不会影响体验,毕竟原始的方法是点对点的消息传递,不会有这种问题,如果部分订阅者尚未初始化怎么办。等等。目前EventBus3提供了优先级和粘性事件的属性来进一步满足开发需求。但是否彻底解决问题了还有待验证。跨进程问题EventBus是进程内的消息管理机制,并且从开源社区的反馈来看这个项目是非常成功的,但稍微有点体量的APP都做了进程拆分,所以是否有必要支持多进程,能否在保证性能的情况下提供同等的代码解耦能力,也值得继续挖掘。目前有lsxiao/Apollo和Xiaofei-it/HermesEventBus等可供参考。参考greenrobot/EventBushehonghui/AndroidEventBus《Android开发艺术探索》第十章此文已由作者授权腾讯云+社区发布 ...

December 29, 2018 · 4 min · jiezi

foo、bar到底是什么意思

在学习编程语言的过程中,尤其使用的是英文的书籍,我们经常发现一些foo、bar、baz之类的名字,要么是变量的名字,要么是文件的名字。。。深究起来完全不明所以。这到底是什么意思呢?示例下面是《C++17 STL Cookbook》这本书的一些用例://foo用做类名class foo {public: static std::string &standard_string() { static std::string s{“some standard string”}; return s; }};还有://用作文件名path p {“testdir/foobar.txt”};同样其他语言里也有这种情况,《The Go Programming Language》这本书使用了大量此类名字:f, err = os.Open(“foo.txt”) // 用作文件名//用做字符串fmt.Println(Equal([]string{“foo”}, []string{“bar”}))解释那这些单词传达给我们的意思是什么呢?实际上这些东西跟我们中文里的张三、李四、王五这类词属于一类,表示当前当前命名“无关紧要”,不影响当前概念的表达。其他资料维基百科里有比较专业的解释:The terms foobar (/fubr/), or foo and others are used as placeholder names (also referred to as metasyntactic variables) in computer programming or computer-related documentation.[1] They have been used to name entities such as variables, functions, and commands whose exact identity is unimportant and serve only to demonstrate a concept.直译过来就是:foobar或者foo等诸如此类的措辞在计算机编程或计算机相关文档中被用作占位符名字(也称为元语法变量)。它们通常被用来命名一些变量、函数或命令等此类实体,而这些实体通常不重要,而且仅仅用来演示一个概念。请关注我的公众号哦。 ...

December 25, 2018 · 1 min · jiezi

JavaScript基础——深入学习async/await

本文由云+社区发表本篇文章,小编将和大家一起学习异步编程的未来——async/await,它会打破你对上篇文章Promise的认知,竟然异步代码还能这么写! 但是别太得意,你需要深入理解Promise后,才能更好的的驾驭async/await,因为async/await是基于Promise的。关于async / await用于编写异步程序代码书写方式和同步编码十分相似,因此代码十分简洁易读基于Promise您可以使用try-catch常规的方法捕获异常ES8中引入了async/await,目前几乎所有的现代浏览器都已支持这个特性(除了IE和Opera不支持)你可以轻松设置断点,调试更容易。从async开始学起让我们从async关键字开始吧,这个关键字可以放在函数之前,如下所示:async function f() { return 1; }在函数之间加上async意味着:函数将返回一个Promise,虽然你的代码里没有显示的声明返回一个Promise,但是编译器会自动将其转换成一个Promise,不信你可以使用Promise的then方法试试:async function f() { return 1; } f().then(alert); // 1…如果你不放心的话,你可以在代码里明确返回一个Promise,输出结果是相同的。async function f() { return Promise.resolve(1); } f().then(alert); // 1很简单吧,小编之所以说 async/await 是基于Promise是没毛病的,async函数返回一个Promise,很简单吧,不仅如此,还有一个关键字await,await只能在async中运行。等待——awaitawait的基本语法:let value=await promise;该关键字的await的意思就是让JS编译器等待Promise并返回结果。接下来我们看一段简单的示例:async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve(“done!”), 1000) }); let result = await promise; // wait till the promise resolves (*) alert(result); // “done!” } f();函数执行将会在 let result = await promise 这一行暂停,直到Promise返回结果,因此上述代码将会1秒后,在浏览器弹出“done”的提示框。小编在此强调下:await的字面意思就是让JavaScript等到Promise结束,然后输出结果。这里并不会占用CPU资源,因为引擎可以同时执行其他任务:其他脚本或处理事件。不能单独使用await,必须在async函数作用域下使用,否则将会报出异常“Error: await is only valid in async function”,示例代码如下:function f() { let promise = Promise.resolve(1); let result = await promise; // Syntax error }接下来,小编将和大家一起来亲自动手实践以下内容:async与Promise.then的结合,依次处理多个结果使用await替代Promise.then,依次处理多个结果同时等待多个结果使用Promise.all收集多个结果使用try-catch捕获异常如何捕获Promise.all中的异常使用finally确保函数执行一起动手之前,确保你安装了Node,NPM相关工具,谷歌浏览器,为了预览代码效果,小编使用 npm install http-server -g 命令快速部署了web服务环境,方便我们运行代码。接下来,我们写一个火箭发射场景的小例子(不是真的发射火箭????)。async与Promise.then的结合,依次处理多个结果通过控制台命令切换至工作区创建一个async-function-Promise-chain的文件夹在main.js中用创建一个返回随机函数的async函数getRandomNumber:async function getRandomNumber() { console.log(‘Getting random number.’); return Math.random(); } 再创建一个async函数determinReadyToLaunch:如果传入参数大于0.5将返回Trueasync function deteremineReadyToLaunch(percentage) { console.log(‘Determining Ready to launch.’); return percentage>0.5; } 创建第三个async函数reportResults,如果传入参数为True将进入倒计时发射async function reportResults(isReadyToLaunch) { if (isReadyToLaunch) { console.log(‘Rocket ready to launch. Initiate countdown: ????’); } else { console.error(‘Rocket not ready. Abort mission: ‘); } } 创建一个main函数,调用getRandomNumber函数,并且通过Promise.then方法相继调用determineReadyToLaunch和reportResults函数export function main() { console.log(‘Before Promise created’); getRandomNumber() .then(deteremineReadyToLaunch) .then(reportResults) console.log(‘After Promise created’); } 新建一个html文件引入main.js<html> <script type=“module”> import {main} from ‘./main.js’; main(); </script> <body> </body> </html>在工作区域运行 http-server 命令,你将会看到如下输出使用await替代Promise.then,依次处理多个结果上一小节,我们使用Promise.then依次处理了多个结果,本小节,小编将使用await实现同样的功能,具体操作如下:通过控制台命令切换至工作区创建一个async-function-Promise-chain的文件夹在main.js中用创建一个返回随机函数的async函数getRandomNumber:async function getRandomNumber() { console.log(‘Getting random number.’); return Math.random(); } 再创建一个async函数determinReadyToLaunch:如果传入参数大于0.5将返回Trueasync function deteremineReadyToLaunch(percentage) { console.log(‘Determining Ready to launch.’); return percentage>0.5; } 创建第三个async函数reportResults,如果传入参数为True将进入倒计时发射async function reportResults(isReadyToLaunch) { if (isReadyToLaunch) { console.log(‘Rocket ready to launch. Initiate countdown: ????’); } else { console.error(‘Rocket not ready. Abort mission: ‘); } } 创建一个main函数,调用getRandomNumber函数,并且通过Promise.then方法相继调用determineReadyToLaunch和reportResults函数export async function main() { const randomNumber = await getRandomNumber(); const ready = await deteremineReadyToLaunch(randomNumber); await reportResults(ready); } 在工作区域运行 http-server 命令,你将会看到如下输出同时等待多个结果有时候我们需要同时启动多个异步,无需依次等待结果消耗时间,接下来的例子可以使用await同时启动多个异步函数,等待多个结果。通过控制台命令切换至工作区创建一个await-concurrently的文件夹创建三个函数checkEngines,checkFlightPlan,和checkNavigationSystem用来记录信息时,这三个函数都返回一个Promise,示例代码如下:function checkEngines() { console.log(‘checking engine’); return new Promise(function (resolve) { setTimeout(function() { console.log(’engine check completed’); resolve(Math.random() < 0.9) }, 250) }); } function checkFlightPlan() { console.log(‘checking flight plan’); return new Promise(function (resolve) { setTimeout(function() { console.log(‘flight plan check completed’); resolve(Math.random() < 0.9) }, 350) }); } function checkNavigationSystem() { console.log(‘checking navigation system’); return new Promise(function (resolve) { setTimeout(function() { console.log(’navigation system check completed’); resolve(Math.random() < 0.9) }, 450) }); } 创建一个async的main函数调用上一步创建函数。将每个返回的值分配给局部变量。然后等待Promise的结果,并输出结果: export async function main() { const enginePromise = checkEngines(); const flighPlanPromise = checkFlightPlan(); const navSystemPromise = checkNavigationSystem(); const enginesOk = await enginePromise; const flighPlanOk = await flighPlanPromise; const navigationOk = await navSystemPromise; if (enginesOk && flighPlanOk && navigationOk) { console.log(‘All systems go, ready to launch: ????’); } else { console.error(‘Abort the launch: ‘); if (!enginesOk) { console.error(’engines not ready’); } if (!flighPlanOk) { console.error(’error found in flight plan’); } if (!navigationOk) { console.error(’error found in navigation systems’); } } } 在工作区域运行 http-server 命令,你将会看到如下输出使用Promise.all收集多个结果在上一小节中,我们一起学习了如何触发多个异步函数并等待多个异步函数结果。上一节我们只使用了asyc/await,本节小编和大家一起使用Promise.all来收集多个异步函数的结果,在某些情况下,尽量使用Promise相关的API,具体的代码如下:通过控制台命令切换至工作区创建一个Promise-all-collect-concurrently的文件夹创建三个函数功能checkEngines,checkFlightPlan,和checkNavigationSystem用来记录信息时,这三个函数都返回一个Promise,示例代码如下:function checkEngines() { console.log(‘checking engine’); return new Promise(function (resolve) { setTimeout(function() { console.log(’engine check completed’); resolve(Math.random() < 0.9) }, 250) }); } function checkFlightPlan() { console.log(‘checking flight plan’); return new Promise(function (resolve) { setTimeout(function() { console.log(‘flight plan check completed’); resolve(Math.random() < 0.9) }, 350) }); } function checkNavigationSystem() { console.log(‘checking navigation system’); return new Promise(function (resolve) { setTimeout(function() { console.log(’navigation system check completed’); resolve(Math.random() < 0.9) }, 450) }); } 创建一个async的main函数调用上一步创建的函数。使用Promise.all收集多个结果,将结果返回给变量,代码实现如下:export async function main() { const prelaunchChecks = [ checkEngines(), checkFlightPlan(), checkNavigationSystem() ]; const checkResults = await Promise.all(prelaunchChecks); const readyToLaunch = checkResults.reduce((acc, curr) => acc && curr); if (readyToLaunch) { console.log(‘All systems go, ready to launch: ????’); } else { console.error(‘Something went wrong, abort the launch: ‘); } } } 在工作区域运行 http-server 命令,你将会看到如下输出:Promise.all接收多个promise的数组,并整体返回一个Promise,如果和上一小节的代码进行比较,代码量少了不少。使用try-catch捕获异常并非所有的async都能成功返回,我们需要处理程序的异常,在本小节中,你将会看到如何使用try-catch捕获async函数引发的异常,具体操作的流程如下:通过控制台命令切换至工作区创建一个async-errors-try-catch的文件夹创建一个抛出错误的async函数addBoostersasync function addBoosters() { throw new Error(‘Unable to add Boosters’); } 创建一个async函数,performGuidanceDiagnostic它也会抛出一个错误:async function performGuidanceDiagnostic (rocket) { throw new Error(‘Unable to finish guidance diagnostic’)); } 创建一个async的main函数调用addBosters、performGuidanceDiagnostic两个函数 ,使用try-catch捕获异常: export async function main() { console.log(‘Before Check’); try { await addBosters(); await performGuidanceDiagnostic(); } catch (e) { console.error(e); } } console.log(‘After Check’); 在工作区域运行 http-server 命令,你将会看到如下输出:从输出看出,我们使用熟悉的try-catch捕获到了异常,如果第一个发生异常,第二个就不会执行,同时将会被记录到,并输出到控制台,在下一小节,我们将一起学习如何使用try-catch捕获Promise.all中运行的多个Promise的异常。如何捕获Promise.all中的异常在上一小节,我们使用了Promise.all来收集多个异步函数的结果。在收集异常方面,Promise.all更有趣。通常,我们在处理多个错误时,同时显示多个错误信息,我们必须编写相关的业务逻辑。但是,在这小节,你将会使用Promise.all和try-catch捕获异常,无需编写复杂的布尔逻辑处理业务,具体如何实现示例如下:通过控制台命令切换至工作区创建一个Promise-all-collect-concurrently的文件夹创建三个async函数checkEngines,checkFlightPlan以及checkNavigationSystem用来记录信息时,返回Promise,一个成功的值的信息和一个失败值的信息:function checkEngines() { console.log(‘checking engine’); return new Promise(function (resolve, reject) { setTimeout(function () { if (Math.random() > 0.5) { reject(new Error(‘Engine check failed’)); } else { console.log(‘Engine check completed’); resolve(); } }, 250) }); } function checkFlightPlan() { console.log(‘checking flight plan’); return new Promise(function (resolve, reject) { setTimeout(function () { if (Math.random() > 0.5) { reject(new Error(‘Flight plan check failed’)); } else { console.log(‘Flight plan check completed’); resolve(); } }, 350) }); } function checkNavigationSystem() { console.log(‘checking navigation system’); return new Promise(function (resolve, reject) { setTimeout(function () { if (Math.random() > 0.5) { reject(new Error(‘Navigation system check failed’)); } else { console.log(‘Navigation system check completed’); resolve(); } }, 450) }); } 创建一个async的main函数调用每个在上一步中创建的功能函数。等待结果,捕获并记录引发的异常。如果没有抛出异常,则记录成功: export async function main() { try { const prelaunchChecks = [ checkEngines, checkFlightPlan, checkNavigationSystem ]; await Promise.all(prelauchCheck.map((check) => check()); console.log(‘All systems go, ready to launch: ????’); } catch (e) { console.error(‘Aborting launch: ‘); console.error(e); } } } 在工作区域运行 http-server 命令,你将会看到如下输出Promise.all返回一个Promise,当await在错误状态下,会抛出异常。三个异步promise同时执行,如果其中一个或多个错误得到满足,则会抛出一个或多个错误;你会发现只有一个错误会被记录下来,与同步代码一样,我们的代码可能会抛出多个异常,但只有一个异常会被catch捕获并记录。使用finally确保函数执行错误处理可能会变得相当复杂。有些情况,其中你希望发生错误时继续冒泡调用堆栈以便执行其它更高级别处理。在这些情况下,您可能还需要执行一些清理任务。本小节,你将了解如何使用finally以确保执行某些代码,而不管错误状态如何,具体如何实现示例如下:通过控制台命令切换至工作区创建一个Promise-all-collect-concurrently的文件夹创建三个async函数checkEngines,checkFlightPlan、checkNavigationSystem用来记录信息,返回Promise,一个成功的值的信息和一个失败值的信息:function checkEngines() { console.log(‘checking engine’); return new Promise(function (resolve, reject) { setTimeout(function () { if (Math.random() > 0.5) { reject(new Error(‘Engine check failed’)); } else { console.log(‘Engine check completed’); resolve(); } }, 250) }); } function checkFlightPlan() { console.log(‘checking flight plan’); return new Promise(function (resolve, reject) { setTimeout(function () { if (Math.random() > 0.5) { reject(new Error(‘Flight plan check failed’)); } else { console.log(‘Flight plan check completed’); resolve(); } }, 350) }); } function checkNavigationSystem() { console.log(‘checking navigation system’); return new Promise(function (resolve, reject) { setTimeout(function () { if (Math.random() > 0.5) { reject(new Error(‘Navigation system check failed’)); } else { console.log(‘Navigation system check completed’); resolve(); } }, 450) }); } 创建一个asyncperformCheck函数,调用上一步中创建的每个函数。等待结果,并用于finally记录完整的消息:async function performChecks() { console.log(‘Starting Pre-Launch Checks’); try { const prelaunchChecks = [ checkEngines, checkFlightPlan, checkNavigationSystem ]; return Promise.all(prelauchCheck.map((check) => check()); } finally { console.log(‘Completed Pre-Launch Checks’); } } 创建一个async的main函数调该函数performChecks。等待结果,捕获并记录引发的异常。export async function main() { try { await performChecks(); console.log(‘All systems go, ready to launch: ????’); } catch (e) { console.error(‘Aborting launch: ‘); console.error(e); } } 在工作区域运行 http-server 命令,你将会看到如下输出与上一小节一样,异常在main函数中进行捕获,由于finally的存在,让我清楚的知道performChecks确保需要执行的输出完成。你可以设想,处理错误是一个重要的任务,并且async/await允许我们使用try/catch的方式同时处理异步和同步代码的错误,大大简化了我们处理错误的工作量,让代码更加简洁。用async/await改写上篇文章Promise的例子上篇文章《JavaScript基础——Promise使用指南》的最后,我们使用Promise的方法改写了回调的例子,本文的最后,我们将用今天学到的内容async/await改写这个例子,如何实现呢,代码如下:const fs = require(‘fs’); const path = require(‘path’); const postsUrl = path.join(__dirname, ‘db/posts.json’); const commentsUrl = path.join(__dirname, ‘db/comments.json’); //return the data from our file function loadCollection(url) { return new Promise(function(resolve, reject) { fs.readFile(url, ‘utf8’, function(error, data) { if (error) { reject(’error’); } else { resolve(JSON.parse(data)); } }); }); } //return an object by id function getRecord(collection, id) { return new Promise(function(resolve, reject) { const data = collection.find(function(element){ return element.id == id; }); resolve(data); }); } //return an array of comments for a post function getCommentsByPost(comments, postId) { return comments.filter(function(comment){ return comment.postId == postId; }); } async function getPost(){ try { const posts = await loadCollection(postsUrl); const post = await getRecord(posts, “001”); const comments = await loadCollection(commentsUrl); const postComments = await getCommentsByPost(comments, post.id); console.log(post); console.log(postComments); } catch (error) { console.log(error); } } getPost(); 和Promise的方式相比,async/await 的实现方式是不是更直观更容易理解呢,让我几乎能用同步的方式编写异步代码。此文已由作者授权腾讯云+社区发布 ...

December 20, 2018 · 5 min · jiezi

编程语言的心智负担

很多编程语言对比的文章,总喜欢比较各种编程语言的性能、语法、IO模型。本文将从心智负担这个角度去比较下不同的编程语言和技术。因本人所擅长的编程语言有限,如有不对的地方,欢迎指正。内存越界如:C语言、C++(C with class)C/C++可以直接操作内存,但编程必须要面对内存越界问题。发生内存越界后,程序会直接core dump,开发者需要使用gdb工具分析内存错误的原因,如果内存越界是偶发的,比如由于数据同步问题造成,数亿次中会出现一次,解决起来非常困难,甚至需要顶级专家才能找到问题原因。心智负担:10现代C++提供了STL库包含大量容器,另外C++支持引用语法,不再需要直接操作指针,降低了内存错误读写的风险。使用现代C++的编程风格可以避免此问题。但由于C++没有完全从语法层面移除指针,不够彻底。宏C/C++程序中经常使用预定义宏实现一些逻辑,导致可读性变差。有些情况下会嵌套多次宏的使用,展开后变得极其难读。心智负担:6因此在C/C++中建议使用enum或static inline函数代替宏。内存管理如:C语言、C++C/C++语言,需要手工管理内存,malloc/new申请的内存要与free/delete成对使用。申请的内存忘记释放,就会出现内存泄漏。心智负担:8Java/PHP/Go等有GC的编程语言,不需要手工管理内存,不会因为代码错误引起内存泄漏。心智负担:0数值类型C/C++/GO等编程语言,提供了有符号、无符号整型和浮点型,8/16/32/64不同尺寸的整型。编程时需要额外处理,避免数值溢出。心智负担:6PHP/Java等编程语言,默认整数为有符号int64,降低了心智负担。一般业务项目中很难有超过2^63的数字,不会遇到问题。但如果是做科学计算,int64就难以满足需求了。在PHP中超过2^63底层会转为浮点型,计算将丢失精度。心智负担:1而Python整数是不限长度的,可以做任意位数的数值计算。心智负担:0类型约束Java是静态强类型编程语言,因此在编程中存在类型约束,某些情况下可能不是特别方便。如JSON序列化。不同类型的变量互相操作时可能需要进行显式类型转换。心智负担:2PHP/JS是动态弱类型编程语言,底层自动进行隐式类型转换。编程更方便。心智负担:0。但在大型项目,或对项目进行代码重构,以及项目代码更换开发者时,弱类型带来可维护性、可读性的难题,反而与Java/Go/C++这样的静态强类型编程语言,增加了大量心智负担。心智负担:5多线程编程Java/C++/Go提供了多线程并行编程、无锁编程,在编程中会存在数据同步问题。因此需要对临界资源进行加锁。而错误的锁操作又会带来,死锁和热点争抢问题。需要开发者具备极高的素质,否则难以做到正确无误并性能良好,这可能需要耗费大量心智。心智负担:10内存泄漏除PHP(php-fpm)之外的其他编程语言和技术(包括PHP + Swoole),在服务器端程序中均为长生命周期。对全局/静态变量操作可能会导致内存或资源句柄泄漏。编程时需要注意。心智负担:3而PHP(php-fpm)是短生命周期的,在请求结束后会立即释放所有内存和句柄,无需担心泄漏。心智负担:0IO 超时同步阻塞IO模型的编程语言和技术,在遇到某个慢IO会导致整个进程或线程挂起。极端情况下会出现所有进程/线程挂起,引起线上服务不可用。开发者需要格外注意设置IO操作的超时时间,避免慢请求带来进程/线程阻塞。心智负担:2而且异步IO的Go/Node.js/Swoole等无需担心此问题。心智负担:0汇总

December 18, 2018 · 1 min · jiezi

Spring Cloud Config 规范

Spring Cloud Config 规范首先Spring Cloud 是基于 Spring 来扩展的,Spring 本身就提供当创建一个Bean时可从Environment 中将一些属性值通过@Value的形式注入到业务代码中的能力。那Spring Cloud Config 要解决的问题就是:如何将配置加载到 Environment 。配置变更时,如何控制 Bean 是否需要 create,重新触发一次 Bean 的初始化,才能将 @Value 注解指定的字段从 Environment 中重新注入。配置变更时,如何控制新的配置会更新到 Environment 中,才能保证配置变更时可注入最新的值。要解决以上三个问题:Spring Cloud Config 规范中刚好定义了核心的三个接口:PropertySourceLocator:抽象出这个接口,就是让用户可定制化的将一些配置加载到 Environment。这部分的配置获取遵循了 Spring Cloud Config 的理念,即希望能从外部储存介质中来 loacte。RefreshScope: Spring Cloud 定义这个注解,是扩展了 Spring 原有的 Scope 类型。用来标识当前这个 Bean 是一个refresh 类型的 Scope。其主要作用就是可以控制 Bean 的整个生命周期。ContextRefresher:抽象出这个 Class,是让用户自己按需来刷新上下文(比如当有配置刷新时,希望可以刷新上下文,将最新的配置更新到 Environment,重新创建 Bean 时,就可以从 Environment 中注入最新的配置)。Spring Cloud Config 原理Spring Cloud Config 的启动过程1、如何将配置加载到Environment:PropertySourceLocator在整个 Spring Boot 启动的生命周期过程中,有一个阶段是 prepare environment。在这个阶段,会publish 一个 ApplicationEnvironmentPreparedEvent,通知所有对这个事件感兴趣的 Listener,提供对 Environment 做更多的定制化的操作。Spring Cloud 定义了一个BootstrapApplicationListener,在 BootstrapApplicationListener 的处理过程中有一步非常关键的操作如下所示:private ConfigurableApplicationContext bootstrapServiceContext( ConfigurableEnvironment environment, final SpringApplication application, String configName) { //省略 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // Use names and ensure unique to protect against duplicates List<String> names = new ArrayList<>(SpringFactoriesLoader .loadFactoryNames(BootstrapConfiguration.class, classLoader)); //省略 }这是 Spring 的工厂加载机制,可通过在 META-INF/spring.factories 文件中配置一些程序中预定义的一些扩展点。比如 Spring Cloud 这里的实现,可以看到 BootstrapConfiguration 不是一个具体的接口,而是一个注解。通过这种方式配置的扩展点好处是不局限于某一种接口的实现,而是同一类别的实现。可以查看 spring-cloud-context 包中的 spring.factories 文件关于BootstrapConfiguration的配置,有一个比较核心入口的配置就是:org.springframework.cloud.bootstrap.BootstrapConfiguration=\org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration可以发现 PropertySourceBootstrapConfiguration 实现了 ApplicationContextInitializer 接口,其目的就是在应用程序上下文初始化的时候做一些额外的操作。在 Bootstrap 阶段,会通过 Spring Ioc 的整个生命周期来初始化所有通过key为_org.springframework.cloud.bootstrap.BootstrapConfiguration_ 在 spring.factories 中配置的 Bean。Spring Cloud Alibaba Nacos Config 的实现就是通过该key来自定义一些在Bootstrap 阶段需要初始化的一些Bean。在该模块的 spring.factories 配置文件中可以看到如下配置:org.springframework.cloud.bootstrap.BootstrapConfiguration=\org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration在 Bootstrap 阶段初始化的过程中,会获取所有 ApplicationContextInitializer 类型的 Bean,并设置回SpringApplication主流程当中。如下 BootstrapApplicationListener 类中的部分代码所示:private void apply(ConfigurableApplicationContext context, SpringApplication application, ConfigurableEnvironment environment) { @SuppressWarnings(“rawtypes”) //这里的 context 是一个 bootstrap 级别的 ApplicationContext,这里已经含有了在 bootstrap阶段所有需要初始化的 Bean。 //因此可以获取 ApplicationContextInitializer.class 类型的所有实例 List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context, ApplicationContextInitializer.class); //设置回 SpringApplication 主流程当中 application.addInitializers(initializers .toArray(new ApplicationContextInitializer[initializers.size()])); //省略…}这样一来,就可以通过在 SpringApplication 的主流程中来回调这些ApplicationContextInitializer 的实例,做一些初始化的操作。如下 SpringApplication 类中的部分代码所示:private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); //回调在BootstrapApplicationListener中设置的ApplicationContextInitializer实例 applyInitializers(context); listeners.contextPrepared(context); //省略…}protected void applyInitializers(ConfigurableApplicationContext context) { for (ApplicationContextInitializer initializer : getInitializers()) { Class<?> requiredType = GenericTypeResolver.resolveTypeArgument( initializer.getClass(), ApplicationContextInitializer.class); Assert.isInstanceOf(requiredType, context, “Unable to call initializer.”); initializer.initialize(context); }}在 applyInitializers 方法中,会触发 PropertySourceBootstrapConfiguration 中的 initialize 方法。如下所示:@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) { CompositePropertySource composite = new CompositePropertySource( BOOTSTRAP_PROPERTY_SOURCE_NAME); AnnotationAwareOrderComparator.sort(this.propertySourceLocators); boolean empty = true; ConfigurableEnvironment environment = applicationContext.getEnvironment(); for (PropertySourceLocator locator : this.propertySourceLocators) { PropertySource<?> source = null; //回调所有实现PropertySourceLocator接口实例的locate方法, source = locator.locate(environment); if (source == null) { continue; } composite.addPropertySource(source); empty = false; } if (!empty) { //从当前Enviroment中获取 propertySources MutablePropertySources propertySources = environment.getPropertySources(); //省略… //将composite中的PropertySource添加到当前应用上下文的propertySources中 insertPropertySources(propertySources, composite); //省略… }在这个方法中会回调所有实现 PropertySourceLocator 接口实例的locate方法,locate 方法返回一个 PropertySource 的实例,统一add到CompositePropertySource实例中。如果 composite 中有新加的PropertySource,最后将composite中的PropertySource添加到当前应用上下文的propertySources中。Spring Cloud Alibaba Nacos Config 在 Bootstrap 阶段通过Java配置的方式初始化了一个 NacosPropertySourceLocator 类型的Bean。从而在 locate 方法中将存放在Nacos中的配置信息读取出来,将读取结果存放到 PropertySource 的实例中返回。具体如何从Nacos中读取配置信息可参考 NacosPropertySourceLocator 类的实现。Spring Cloud Config 正是提供了PropertySourceLocator接口,来提供应用外部化配置可动态加载的能力。Spring Ioc 容器在初始化 Bean 的时候,如果发现 Bean 的字段上含有 @Value 的注解,就会从 Enviroment 中的PropertySources 来获取其值,完成属性的注入。Spring Cloud Config 外部化配置可动态刷新感知到外部化配置的变更这部分代码的操作是需要用户来完成的。Spring Cloud Config 只提供了具备外部化配置可动态刷新的能力,并不具备自动感知外部化配置发生变更的能力。比如如果你的配置是基于Mysql来实现的,那么在代码里面肯定要有能力感知到配置发生变化了,然后再显示的调用 ContextRefresher 的 refresh方法,从而完成外部化配置的动态刷新(只会刷新使用RefreshScope注解的Bean)。例如在 Spring Cloud Alibaba Nacos Config 的实现过程中,Nacos 提供了对dataid 变更的Listener 回调。在对每个dataid 注册好了相应的Listener之后,如果Nacos内部通过长轮询的方式感知到数据的变更,就会回调相应的Listener,在 Listener 的实现过程中,就是通过调用 ContextRefresher 的 refresh方法完成配置的动态刷新。具体可参考 NacosContextRefresher 类的实现。Sring Cloud Config的动态配置刷新原理图如下所示:ContextRefresher的refresh的方法主要做了两件事:触发PropertySourceLocator的locator方法,需要加载最新的值,并替换 Environment 中旧值Bean中的引用配置值需要重新注入一遍。重新注入的流程是在Bean初始化时做的操作,那也就是需要将refresh scope中的Bean 缓存失效,当再次从refresh scope中获取这个Bean时,发现取不到,就会重新触发一次Bean的初始化过程。这两个操作所对应的代码如下所示:public synchronized Set refresh() { Map<String, Object> before = extract( this.context.getEnvironment().getPropertySources()); //1、加载最新的值,并替换Envrioment中旧值 addConfigFilesToEnvironment(); Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); this.context.publishEvent(new EnvironmentChangeEvent(context, keys)); //2、将refresh scope中的Bean 缓存失效: 清空 this.scope.refreshAll(); return keys;}addConfigFilesToEnvironment 方法中发生替换的代码如下所示:ConfigurableApplicationContext addConfigFilesToEnvironment() { ConfigurableApplicationContext capture = null; try { //省略… //1、这里会重新触发PropertySourceLoactor的locate的方法,获取最新的外部化配置 capture = (SpringApplicationBuilder)builder.run(); MutablePropertySources target = this.context.getEnvironment() .getPropertySources(); String targetName = null; for (PropertySource<?> source : environment.getPropertySources()) { String name = source.getName(); //省略.. //只有不是标准的 Source 才可替换 if (!this.standardSources.contains(name)) { if (target.contains(name)) { //开始用新的PropertySource替换旧值 target.replace(name, source); } // } } } // return capture;}this.scope.refreshAll() 清空缓存的操作代码如下所示:@Override public void destroy() { List<Throwable> errors = new ArrayList<Throwable>(); //清空Refresh Scope 中的缓存 Collection<BeanLifecycleWrapper> wrappers = this.cache.clear(); //省略… }为了验证每次配置刷新时,Bean 是新创建的,特意写了一个Demo 验证了下,如下所示:Acm Properties: beijing-region//刷新前Object Instance is :com.alibaba.demo.normal.ConfigProperties@1be96342018-11-01 19:16:32.535 INFO 27254 — [gPullingdefault] startup date [Thu Nov 01 19:16:32 CST 2018]; root of context hierarchyAcm Properties: qingdao-region//刷新后Object Instance is :com.alibaba.demo.normal.ConfigProperties@2c6965e0Spring Cloud Config 扩展Scope的核心类:RefreshScope可以看到上面的代码中有 this.scope.refreshAll(),其中的scope就是RefreshScope。是用来存放scope类型为refresh类型的Bean(即使用RefreshScope注解标识的Bean),也就是说当一个Bean既不是singleton也不是prototype时,就会从自定义的Scope中去获取(Spring 允许自定义Scope),然后调用Scope的get方法来获取一个实例,Spring Cloud 正是扩展了Scope,从而控制了整个 Bean 的生命周期。当配置需要动态刷新的时候, 调用this.scope.refreshAll()这个方法,就会将整个RefreshScope的缓存清空,完成配置可动态刷新的可能。更多关于Scope的分析请参考 这里后续关于ContextRefresh 和 RefreshScope的初始化配置是在RefreshAutoConfiguration类中完成的。而RefreshAutoConfiguration类初始化的入口是在spring-cloud-context中的META-INF/spring.factories中配置的。从而完成整个和动态刷新相关的Bean的初始化操作。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 17, 2018 · 3 min · jiezi

为什么要学习Python?这10个理由足够了!

摘要: 看完这十个理由,我决定买本python从入门到精通!如果你定期关注现今的科技发展,那么你可能想知道我为什么要写这篇文章告诉人们学习Python?因为几年前我提倡Java而不是Python。在2016年,Python取代Java成为高校中最受欢迎的语言,从那时起它受欢迎的程度就没有减退过。但是,随着新时代的到来,Python正在不断发展壮大。如果你阅读编程和技术新闻或博客文章,那么你可能已经发现了Python的兴起,因为许多流行的开发人员社区,包括StackOverFlow和CodeAcademy都提到了Python作为主要编程语言的兴起。但是,最大的问题是为什么程序员应该学习Python?作为一名Java开发人员,我很喜欢它,并且始终会去学习它。但是,这并不能阻止我们学习潜在的新工具和编程语言,这将使我们成为一个更好的程序员。对于初学者来说很简单,从Python开始是最好的选择。因为它易于学习,功能强大,足以构建Web应用程序并自动化无聊的东西。实际上,几年前,脚本编写是学习Python的主要原因,这也是我被Python吸引并首选Perl的原因,而Perl是当时另一种流行的脚本语言。对于有经验的程序员或已经了解Ruby,Java或JavaScript的人来说,学习Python意味着在你的工具库中获得一个新的强大工具,我还没有想出一个对工具说“不”的程序员,这是你学习一门新的编程语言时的正确查找方式。正如经典的Automate the Boring Stuff with Python一书中所提到的,Python让你能够自动化琐碎的东西,让你专注于更多令人兴奋和有用的东西。如果你是Java开发人员,那么也可以使用Groovy来实现这一点,但Groovy并未提供Python在API、库、框架和数据科学、机器学习以及Web开发等领域的广泛应用。为什么2018年程序员应该学习Python?如果你正在考虑学习Python但不确定为什么要学,那么这里有10个理由强调了2018年学习Python的好处。虽然,问题的答案取决于问问题的对象,对于初学者,学习Python是有道理的,因为学习Python主要原因是简便性。同样,对于想要进入数据科学和机器学习的有经验的程序员来说,学习Python是有意义的,因为它很快成为最常用的编程语言,并且有强大的API和可用于AI、数据科学和机器学习的库。以下是在2018年学习Python的10个理由:1.数据科学这是许多程序员在2018年学习Python的一个最大的原因。我知道很多对投资银行的Java编程工作感到厌倦的朋友,由于令人兴奋的工作和高回报,他们正在Udemy学习Python来从事数据科学工作。但是,是什么让Python成为数据科学和机器学习的首选语言?不久之前R语言不是最好的吗?我认为Python提供关于AI、DataScience、机器学习PyBrain,NumPy和PyMySQL的库和框架就是其中一个原因。另一个原因是多样性,Python体验允许你比R做更多的事情,例如你可以创建脚本来自动化内容,进入Web开发等等。如果你有兴趣在2018年成为数据科学家,我建议你查看关于Udemy的数据科学,深度学习和机器学习与Python课程。我已经购买了这门课程,这是一个非常棒的资源,有时花费不到10美元。2.机器学习这也是为什么程序员在2018年学习Python的另一个原因。机器学习的发展在过去的几年中是惊人的,它正在迅速改变我们周围的一切。算法日渐复杂,最好的例子是谷歌的搜索算法,它现在可以通过聊天机器人来回答你所期望的答案,其中 Uber完全由算法驱动。如果你对机器学习感兴趣,想要做一个宠物项目或只是想玩一玩,Python是唯一能让它变得简单的主要编程语言。尽管Java中提供了机器学习库,但你会在Python中发现更多内容,因为开发人员社区更喜欢Python而不是其他任何关于数据科学和机器学习的内容。如果你有兴趣使用Python进行Web开发,我建议你进一步在Udemy上查阅数据科学课程中的机器学习A-Z™:Hands-On Python和R。3.网站开发优质的开发是学习Python的另一个原因。它提供了许多好的库和框架,例如Django和Flask使Web开发变得非常简单。PHP中需要花费数小时的任务可以在几分钟内完成。Python也被用于网络爬虫。像Reddit这样互联网上的一些流行网站是使用Python构建的。如果你对使用Python进行Web开发感兴趣,我建议你加入JoshPortilla的Python和Django Full Stack Web Developer Bootcamp课程。事实上,Udemy上有一个免费学习Python的课程。4.简便这是初学者学习Python的最大原因。当你第一次开始编程时,你肯定不希望从具有严格语法和奇怪规则的编程语言开始。 Python既可读又简单,它也更容易设置,而不需要处理任何类路径问题,如Java或C++等编译器问题。只需安装Python就可以了。安装它时还会要求你在PATH中添加Python,这意味着你可以从计算机上的任何位置运行Python。5.强大的社区你需要一个社区来学习新技术,而在学习编程语言方面,朋友是你最大的资产,当你遇到问题时,就需要朋友的援助之手。 由于Google,你可以在几分钟内找到任何问题的解决方案,像StackOverflow这样的社区也将许多Python专家聚集在一起,来帮助新手。6.库和框架Python和Java之间的一个相似之处是可以执行任何操作的开源库,框架和模块的数量,它使应用程序开发变得非常容易。想象一下,不用Java的Spring或Django和Flask创建一个Web应用程序,工作变得简单,因为只需要关注业务逻辑。Python有满足不同需求的库。Django和Flask是Web开发中最受欢迎的两个,而NumPy和SciPy是用于数据科学的。实际上,Python拥有最好的机器学习和数据科学库集合,如TensorFlow,Scikit-Learn,Keras,Pandas等等。如果你想了解有关Python机器学习库的更多信息,我建议你加入Python for Data Science和Machine Learning Bootcamp课程,这是我最喜欢的课程之一。7.自动化由于某一次的脚本需求,我第一次了解了Python。我在使用通过UDP接收消息的应用程序时出现了问题,但我没有在日志中看到消息。我想检查是否在该盒子和端口上接收了任何UDP流量,但我找不到一个方便的UNIX命令来做到这一点。我的一位坐在我旁边的朋友正在学习Python,他在短短5分钟内编写了一个实用工具,使用了其中一个Python模块拦截UDP消息。显然,我对他编写这样一个工具所花费的时间印象深刻,但这只是在编写脚本、工具和自动化时显示出了Python的强大功能。如果你真的想知道Python对自动化有多大帮助,那么我最喜欢的就是使用Automate boring stuff with Python这本书,简直太棒了。8.多用途我喜欢Python的一个原因是它的瑞士军刀性质。它关系着方方面面,例如R在数据科学和机器学习方面表现得很好,而且在Web开发方面也无处不在。学习Python意味着你可以做很多事情。 你可以使用Django和Flask创建Web应用程序,可以使用NumPy,Scipy,Scikit-Learn和NLTK进行数据分析。至少,你可以使用Python编写脚本来自动完成许多日常任务。9.工作与成长Python的发展速度非常快,持续时间很长,如果你刚刚开始编程生涯,那么学习一门不断发展的编程语言会很有意义。它不仅可以帮助你快速找到工作,而且还可以加速你的职业发展。恕我直言,对于初学者来说,除了它的简便性,这应该是学习Python的最重要原因。10.薪水Python开发人员是收入最高的开发人员之一,特别是在数据科学,机器学习和Web开发方面。平均而言,它们的报酬非常高,从70,000美元到150,000美元不等,具体取决于他们的经验,定位。如果你有兴趣了解有关高薪技术工作的更多信息,我建议你查看一下2018年发布的关于软件工程师的10个最高薪技术工作的帖子。学习Python的有用资源如果你决定在2018年学习Python,那么这里有一些有用的Python书籍,课程和教程,可以在Python的美丽世界中开始你的旅程。1. The Complete Python MasterClass2.Python圣经 - 你需要用Python编程的一切3.Pluralsight的Python基础知识4.5门课程学习数据科学和机器学习5.10个免费的Python编程电子书和PDF6.2018年学习Python的五大课程如果你仍然不确定学习Python,那么看看这个图像,它正确地展示了Python开发人员的生活:这就是2018年学习Python的一些重要原因。正如我所说,了解当今世界的编程非常重要,如果你不知道编程,你就会遗漏一些东西,学习Python是开始学习编程的好方法。对于已经了解Java或C++的程序员来说,学习Python不仅可以让你成为Polyglot程序员,还可以在工具库中为你提供一个强大的工具来编写脚本、创建一个Web应用程序,并在激动人心的数据科学和机器学习领域打开大门。简而言之,如果你在2018年只学习一种编程语言,并且开始学习Python,The Complete Python MasterClass是最好的课程。你可能喜欢的其他编程文章前8个Python数据科学和机器学习库10个面向程序员的机器学习和数据科学课程你可以在2018年学习的10种编程语言学习Scala编程语言的10个理由5个面向数据科学家的免费R编程课程Python程序员的前5个TensorFlow课程学习数据科学和机器学习的五门课程50+数据结构和算法面试问题总结感谢你看到了文章的最后…祝你的Python旅程好运!这肯定是一个很好的决定,并在不久的将来会得到回报。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 10, 2018 · 1 min · jiezi

MQ消息队列产品测试

一、产品背景消息队列是阿里巴巴集团自主研发的专业消息中间件。 产品基于高可用分布式集群技术,提供消息订阅和发布、消息轨迹查询、定时(延时)消息、资源统计、监控报警等一系列消息云服务,是企业级互联网架构的核心产品。 MQ 目前提供 TCP 、MQTT 两种协议层面的接入方式,支持 Java、C++ 以及 .NET 不同语言,方便不同编程语言开发的应用快速接入 MQ 消息云服务。 用户可以将应用部署在阿里云 ECS、企业自建云,或者嵌入到移动端、物联网设备中与 MQ 建立连接进行消息收发,同时本地开发者也可以通过公网接入 MQ 服务进行消息收发。从官方文档中看到MQ消息队列的产品为一个提供消息服务的中间件,可以提供端到云的消息服务,这个端的覆盖面包括了移动端和IOT物联网设备,并且为了支持IOT的需要除TCP协议外提供了MQTT来支持物联网设备的消息服务,在云上的支持不止包括阿里云,可以支持用户将服务部署在企业自建云上。作为PAAS层的服务支持用户通过API的方式将消息队列服务集成在自己的平台上,目前在产品的结构上分成两部分,移动端和物联网的消息队列服务单独作为一个子产品MQ FOR IOT提供服务,这项服务和MQ主服务比主要的区别就是增加了对MQTT通讯协议的支持。从编程语言来看,因为MQ FOR IOT是面向移动端和物联网,所以需要支持的编程语言更多,包括ANDROID、IOS和PYTHON环境在消息队列服务中都已经支持。二、消息队列MQ产品测试开通服务进入控制台后看到菜单将消息队列服务清晰的分成两部分,支持MQTT的微消息服务单独列出子菜单,菜单选项按照功能分成三大部分,生产管理类子菜单,消息查询追踪类子菜单和监控报警类子菜单。TOPIC是消息队列服务中一个重要概念,用于区分消息的不同类型,比如在一次交易中,用户对于商品所下的订单和支付的订单虽然针对的是同一件事情,但是对于消息队列来说,这两种消息的功能和类型有明显的不同,可以用不同的TOPIC来区分,在TOPIC下还有个标签TAG用于二级分类,如一个用户对不同商品的购买订单可以作为不同的TAG。针对消息的配置来讲,需要定义消息的名字和消息的类型。在类型上普通消息、事务消息、定时消息、分区消息等都可以将不同类型的TOPIC根据类型区分。将TOPIC按什么类型进行分类及归入哪个分类需要用户根据实际情况进行确定。除了TOPIC外,对于一条消息,还有三个独特的属性可以为查询提供方便,生产者的编号(PRODUCT ID)、消费者的编号(CONSUMER ID)和消息编号(MESSAGE ID),加上TOPIC的配置,可以准确定义海量消息中的每一条,方便查询和监控等功能的支持。消息路由是指的在不同地域间的消息同步,需要配置源地域和TOPIC、目标地域和TOPIC,从最新写入源的消息开始进行同步。资源报表分成两个子项,生产者和消费者,可以对于消息的两个源头的情况进行查看,如果需要对于消息服务的可以在监控报警设置中进行配置,对于消息的报警项,有两个重要指标堆积量和消息延迟,分别从数量和时间对于消息服务的异常情况进行报警,通过短信方式通知用户。三、微消息队列MQ FOR IOT产品测试从微消息队列的按量付费的计费项目就可以看出物联网在消息通讯上的几个主要特征,即时连接数、订阅消息数和消息收发量。万物互联后物联网设备的消息数在这三个维度都会到达海量的程度,特别是即时连接这个特点和一般的MQ服务有很大不同,可以代表物联网中消息传递的特征。此外,微消息队列服务对于消息的分类同一般MQ服务不同的是,将TOPIC分成父TOPIC和子TOPIC的方式而不是TOPIC和TAG的分类方式,子TOPIC从属于父TOPIC,这个特点我想也是因为需要支持物联网的关系,因为传统下的消息都是针对应用比较多,但是物联网情况下,消息的类型如设备的状态、工业监测数据等会比一般情况多的多,并且消息服务的实时性要求更高,所以将TOPIC设置成父子从属关系更有利于对海量不同类型的消息进行区分。本文作者:朱祺阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 6, 2018 · 1 min · jiezi

全图化引擎(AI·OS)中的编译技术

全图化引擎又称算子执行引擎,它的介绍可以参考从HA3到AI OS - 全图化引擎破茧之路。本文从算子化的视角介绍了编译技术在全图化引擎中的运用主要内容有:1.通过脚本语言扩展通用算子上的用户订制能力,目前这些通用算子包括得分算子,过滤算子等。这一方面侧重于编译前端,我们开发了一种嵌入引擎的脚本语言cava ,解决了用户扩展引擎功能的一些痛点,包括插件的开发测试效率,兼容性,引擎版本升级效率等。2.通过代码技术优化全图化引擎性能,由于全图化引擎是基于tensorflow开发,它天生具备tensorflow xla编译能力,利用内核的熔丝提升性能,这部分内容可以参考XLA概述 .xla主要面向tensorflow内置的内核,能发挥的场景是在线预测模型算分。但是对于用户自己开发的算子,XLA很难发挥作用。本文第二部分主要介绍对于自定义算子我们是如何做的代码生成优化的。通用算子上的脚本语言静脉由于算子开发和组图逻辑对普通用户来说成本较高,全图化引擎内置了一些通用算子,比如说射手算子,过滤器算子。这些通用算子能加载C ++插件,也支持用静脉脚本写的插件。关于静脉参考可以这篇文章了解一下。和C ++插件相比,静脉插件有如下特点:1.类java的语法。扩大了插件开发的受众,让熟悉java的同学能快速上手使用引擎。2.性能高.cava是强类型,编译型语言,它能和c ++无损交互。这保证了cava插件的执行性能,在单值场景使用cava写的插件和c ++的插件性能相当。3.使用池管理内存.cava的内存管理可定制,服务端应用每个请求一个池是最高效的内存使用策略。4.安全。对数组越界,对象访问,除零异常做了保护。5.支持jit,编译快。支持upc时编译代码,插件的上线就和上线普通配置一样,极大的提升迭代效率6.兼容性:由于cava的编译过程和引擎版本是强绑定的,只要引擎提供的cava类库接口不变,cava的插件的兼容性很容易得到保证。而c ++插件兼容性很难保证,任何引擎内部对象内存布局的变动就可能带来兼容性问题。射手算子中的静脉插件cava scorer目前有如下场景在使用1.主搜海选场景,算法逻辑可以快速上线验证2.赛马引擎2.0的算分逻辑,赛马引擎重构后引入cava算分替代原先的战马算分样例如下:package test;import ha3.;/ * 将多值字段值累加,并乘以query里面传递的ratio,作为最后的分数 * /class DefaultScorer { MInt32Ref mref; double ratio; boolean init(IApiProvider provider) { IRefManager refManger = provider.getRefManager(); mref = refManger.requireMInt32(“ids”); KVMapApi kv = provider.getKVMapApi(); ratio = kv.getDoubleValue(“ratio”);//获取kvpair内参数 return true; } double process(MatchDoc doc) { int score = 0; MInt32 mint = mref.get(doc); for (int i = 0; i < mint.size(); i++) { score = score + mint.get(i); } return score * ratio; }}其中cava scorer的算分逻辑(process函数)调用次数是doc级别的,它的执行性能和c ++相比唯一的差距是多了安全保护(数组越界,对象访问,除零异常)。可以说cava是目前能嵌入C ++系统执行的性能最好的脚本语言。过滤算子中静脉插件filter算子中主要是表达式逻辑,例如filter =(0.5 * a + b)> 10.以前表达式的能力较弱,只能使用算术,逻辑和关系运算符。使用cava插件可进一步扩展表达式的能力,它支持类的Java语法,可以定义变量,使用分支循环等。计算 filter = (0.5 * a + b) > 10,用cava可定义如下:class MyFunc { public boolean init(FunctionProvider provider) { return true; } public boolean process(MatchDoc doc, double a, double b) { return (0.5 * a + b) > 10; }}filter = MyFunc(a, b)另外由于静脉是编译执行的,和原生的解释执行的表达式相比有天然的性能优势。关于静脉前端的展望静脉是全图化引擎上面向用户需求的语言,有用户定制扩展逻辑的需求都可以考虑用通用算子+静脉插件配合的模式来支持,例如全图化SQL上的UDF,规则引擎的匹配需求等等。后续静脉会进一步完善语言前端功能,完善类库,尽可能兼容的Java。依托苏伊士和全图化引擎支持更多的业务需求。自定义算子的代码生成优化过去几年,在OLAP领域codegen一直是一个比较热门的话题。原因在于大多数数据库系统采用的是Volcano Model模式。其中的下一个()通常为虚函数调用,开销较大。全图化引擎中也有类似的代码生成场景,例如统计算子,过滤算子等。此外,和XLA类似,全图化引擎中也有一些场景可以通过算子融合优化性能。目前我们的代码生成工作主要集中在CPU上对局部算子做优化,未来期望能在SQL场景做全图编译,并且在异构计算的编译器领域有所发展。单算子的代码生成优化统计算子例如统计语句:group_key:键,agg_fun:总和(VAL)#COUNT(),按键分组统计键出现的次数和缬氨酸的和在统计算子的实现中,键的取值有一次虚函数调用, sum和count的计算是两次虚函数调用,sum count计算出来的值和需要通过matchdoc存取,而matchdoc的访问有额外的开销:一次是定位到matchdoc storage,一次是通过偏移定位到存取位置。那么统计代码生成是怎么去除虚函数调用和matchdoc访问的呢?在运行时,我们可以根据用户的查询获取字段的类型,需要统计的功能等信息,根据这些信息我们可以把通用的统计实现特化成专用的统计实现。例如统计sum和count只需定义包含sum count字段的AggItem结构体,而不需要matchdoc;统计函数sum和count变成了结构体成员的+ =操作。假设键和VAL字段的类型都是整型,那么上面的统计语句最终的代码生成成的静脉代码如下:class AggItem { long sum0; long count1; int groupKey;}class JitAggregator { public AttributeExpression groupKeyExpr; public IntAggItemMap itemMap; public AggItemAllocator allocator; public AttributeExpression sumExpr0; … static public JitAggregator create(Aggregator aggregator) { …. } public void batch(MatchDocs docs, uint size) { for (uint i = 0; i < size; ++i) { MatchDoc doc = docs.get(i); //由c++实现,可被inline int key = groupKeyExpr.getInt32(doc); AggItem item = (AggItem)itemMap.get(key); if (item == null) { item = (AggItem)allocator.alloc(); item.sum0 = 0; item.count1 = 0; item.groupKey = key; itemMap.add(key, (Any)item); } int sum0 = sumExpr0.getInt32(doc); item.sum0 += sum0; item.count1 += 1; } }}这里总计数的虚函数被替换成和+ +和计数+ =,matchdoc的存取变成结构体成员的读写item.sum0和item.count0。经过llvm jit编译优化之后生成的ir如下:define void @_ZN3ha313JitAggregator5batchEP7CavaCtxPN6unsafe9MatchDocsEj(%“class.ha3::JitAggregator”* %this, %class.CavaCtx* %"@cavaCtx@", %“class.unsafe::MatchDocs”* %docs, i32 %size){entry: %lt39 = icmp eq i32 %size, 0 br i1 %lt39, label %for.end, label %for.body.lr.phfor.body.lr.ph: ; preds = %entry %wide.trip.count = zext i32 %size to i64 br label %for.bodyfor.body: ; preds = %for.inc, %for.body.lr.ph %lsr.iv42 = phi i64 [ %lsr.iv.next, %for.inc ], [ %wide.trip.count, %for.body.lr.ph ] %lsr.iv = phi %“class.unsafe::MatchDocs”* [ %scevgep, %for.inc ], [ %docs, %for.body.lr.ph ] %lsr.iv41 = bitcast %“class.unsafe::MatchDocs”* %lsr.iv to i64* // … prepare call for groupKeyExpr.getInt32 %7 = tail call i32 %5(%“class.suez::turing::AttributeExpressionTyped.64”* %1, i64 %6) // … prepare call for itemMap.get %9 = tail call i8* @_ZN6unsafe13IntAggItemMap3getEP7CavaCtxi(%“class.unsafe::IntAggItemMap”* %8, %class.CavaCtx* %"@cavaCtx@", i32 %7) %eq = icmp eq i8* %9, null br i1 %eq, label %if.then, label %if.end10// if (item == null) {if.then: ; preds = %for.body // … prepare call for allocator.alloc %15 = tail call i8* @_ZN6unsafe16AggItemAllocator5allocEP7CavaCtx(%“class.unsafe::AggItemAllocator”* %14, %class.CavaCtx* %"@cavaCtx@") // item.groupKey = key; %groupKey = getelementptr inbounds i8, i8* %15, i64 16 %16 = bitcast i8* %groupKey to i32* store i32 %7, i32* %16, align 4 // item.sum0 = 0; item.count1 = 0; call void @llvm.memset.p0i8.i64(i8* %15, i8 0, i64 16, i32 8, i1 false) // … prepare call for itemMap.add tail call void @_ZN6unsafe13IntAggItemMap3addEP7CavaCtxiPNS_3AnyE(%“class.unsafe::IntAggItemMap”* %17, %class.CavaCtx* %"@cavaCtx@", i32 %7, i8* %15) br label %if.end10if.end10: ; preds = %if.end, %for.body %item.0.in = phi i8* [ %15, %if.end ], [ %9, %for.body ] %18 = bitcast %“class.unsafe::MatchDocs”* %lsr.iv to i64* // … prepare call for sumExpr0.getInt32 %26 = tail call i32 %24(%“class.suez::turing::AttributeExpressionTyped.64”* %20, i64 %25) // item.sum0 += sum0; item.count1 += 1; %27 = sext i32 %26 to i64 %28 = bitcast i8* %item.0.in to <2 x i64>* %29 = load <2 x i64>, <2 x i64>* %28, align 8 %30 = insertelement <2 x i64> undef, i64 %27, i32 0 %31 = insertelement <2 x i64> %30, i64 1, i32 1 %32 = add <2 x i64> %29, %31 %33 = bitcast i8* %item.0.in to <2 x i64>* store <2 x i64> %32, <2 x i64>* %33, align 8 br label %for.incfor.inc: ; preds = %if.then, %if.end10 %scevgep = getelementptr %“class.unsafe::MatchDocs”, %“class.unsafe::MatchDocs”* %lsr.iv, i64 8 %lsr.iv.next = add nsw i64 %lsr.iv42, -1 %exitcond = icmp eq i64 %lsr.iv.next, 0 br i1 %exitcond, label %for.end, label %for.bodyfor.end: ; preds = %for.inc, %entry ret void}代码生成的代码中有不少函数是通过C ++实现的,如docs.get(i)中,itemMap.get(键)等。但是优化后的IR中并没有docs.get(I)的函数调用,这是由于经常调用的c ++中实现的函数会被提前编译成bc,由cava编译器加载,经过llvm inline优化pass后被消除。可以认为cava代码和llvm ir基本能做到无损映射(cava中不容易实现逻辑可由c ++实现,预编译成bc加载后被内联),有了cava这一层我们可以用常规面向对象的编码习惯来做codegen,不用关心llvm api细节,让codegen门槛进一步降低。这个例子中,统计规模是100瓦特文档1瓦特个键时,线下测试初步结论是延迟大约能降1倍左右(54ms-> 27ms),有待表达式计算进一步优化。2.过滤算子在通用过滤算子中,表达式计算是典型的可被codegen优化的场景。例如ha3的过滤语句:filter =(a + 2 * b - c)> 0:表达式计算是通过AttributeExpression实现的,AttributeExpression的评价是虚函数。对于单文档接口我们可以用和统计类似的方式,使用静脉对表达式计算做代码生成。对于批量接口,和统计不同的是,表达式的批量计算更容易运用向量化优化,利用CPU的SIMD指令,使计算效率有成倍的提升。但是并不是所有的表达式都能使用一致的向量化优化方法,比如filter = a> 0 AND b <0这类表达式,有短路逻辑,向量化会带来不必要的计算。因此表达式的编译优化需要有更好的codegen 抽象,我们发现Halide能比较好的满足我们的需求.Halide的核心思想:算法描述(做什么ir)和性能优化(怎么做schedule)解耦。种解耦能让我们更灵活的定制优化策略,比如某些场景走向量化,某些场景走普通的代码生成;更进一步,不同计算平台上使用不同的优化策略也成为可能。3.倒排召回算子在寻求算子中,倒排召回是通过QueryExecutor实现的,QueryExecutor的seek是虚函数。例如query = a AND b OR c。QueryExecutor的和或ANDNOT有比较复杂的逻辑,虚函数的开销相对占比没有表达式计算那么大,之前用VTUNE做过预估,求虚函数调用开销占比约10%(数字不一定准确,内联效果没法评估)和精确统计,表达式计算相比,查询的组合空间巨大,寻求的代码生成得更多的考虑对高性价比的查询做编译优化。海选与排序算子在HA3引擎中海选和精排逻辑中有大量比较操作例如排序= + RANK; ID字句,对应的比较函数是秩Compartor和标识Compartor的联合比较.compare的函数调用可被代码生成掉,并且还可和STL算法联合inline.std ::排序使用非在线的补偿函数带来的开销可以参考如下例子:bool myfunction (int i,int j) { return (i<j); }int docCount = 200000;std::random_device rd;std::mt19937_64 mt(rd());std::uniform_int_distribution<int> keyDist(0, 200000);std::vector<int> myvector1;for (int i = 0 ; i < docCount; i++) { myvector1.push_back(keyDist(mt));}std::vector<int> myvector2 = myvector1;std::sort (myvector1.begin(), myvector1.end()); // cost 15.475msstd::sort (myvector2.begin(), myvector2.end(), myfunction); // cost 19.757ms对20瓦特随机数排序,简单的比较直列带来30%的提升。当然在引擎场景,由于比较逻辑复杂,这部分收益可能不会太多。算子的保险丝和代码生成算子的fuse是tensorflow xla编译的核心思想,在全图化场景我们有一些自定义算子也可以运用这个思想,例如特征生成器。FG生成特征的英文模型训练中很重要的一个环节。在线FG是以子图+配置形式描述计算,这部分的代码生成能使数据从索引直接计算到张量上,省去了很多环节中间数据的拷贝。目前这部分的代码生成可以工作参考这篇文章关于编译优化的展望SQL场景全图的编译执行数据库领域全阶段代码生成早被提出并应用,例如Apache的火花作为编译器 ;还有现在比较火的GPU数据库MAPD,把整个执行计划编译成架构无关的中间表示(LLVM IR),借助LLVM编译到不同的目标执行。从实现上看,SQL场景的全图编译执行对全图化引擎还有更多意义,比如可以省去tensorflow算子执行带来的线程切换的开销,可以去除算子间matchdoc传递(matchdoc作为通用的数据布局性能较差)带来的性能损耗。面向异构计算的编译器随着摩尔定律触及天花板,未来异构计算一定是一个热门的领域.SQL大规模数据分析和在线预测就是异构计算可以发挥作用的典型场景,比如分析场景大数据量统计,在线预测场景深度模型大规模并行计算.cpu驱动其他计算平台如gpu fpga,相互配合各自做自己擅长的事情,在未来有可能是常态。这需要为开发人员提供更好的编程接口。全图化引擎已经领先了一步,集成了tensorflow计算框架,天生具备了异构计算的能力。但在编译领域,通用的异构计算编程接口还远未到成熟的地步。工业界和学术界有不少尝试,比如tensorflow的xla编译框架,TVM,Weld等等。借用焊接的概念图表达一下异构计算编译器设计的愿景:让数据分析,深度学习,图像算法等能用统一易用的编程接口充分发挥异构计算平台的算力。总结编译技术已经开始在引擎的用户体验,迭代效率,性能优化中发挥作用,后续会跟着全图化引擎的演进不断发展。能做的事情很多,挑战很大,感兴趣有同学的可以联系我们探讨交流。参考使用带宽优化存储平衡向量化查询执行,第3章有效地编译现代硬件的高效查询计划TensorFlow编译优化策略 - XLAWeld:重新思考数据密集型库之间的接口TVM:用于深度学习的自动化端到端优化编译器本文作者:sance阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 4 min · jiezi

想成为数据科学家?先做到这6点吧!

摘要: 想成为数据科学家?先做到这6点吧!世界变化很快,各行各业在大环境的变化也发生着非常大的改动。传统的行业比如会计师、审计师、同声翻译、点餐员等许多职业逐渐被机器所替代。根据相关报道,未来top10的行业都与数据有关,比如数据分析科学家、AI专家等。最近一则新闻——校招薪资超80万让一些工作数年的员工羡慕不已,该类职业大多都与数据相关。因此,很多相关行业的人都在思考是否应该转行进入与数据相关的职业。在这里,我给的建议是,大的趋势是趋向数字化、智能化,那些在以技术为中心领域工作的人不管是否转行成为一名数据科学家,都应该自学相关的知识,以便在未来的时代里不被淘汰。《未来简史》中说道“未来,数据为王,谁掌握了数据,谁就处于领先。”2018年即将过去,2019年即将到来。如果你想成为一名数据科学家,本文提出6点建议,希望你在新的一年里能够尽量完成。1.成为相关组织的成员与对数据科学感兴趣的其他人建立联系可以让自己了解到现有的教育选择,了解哪些工具在数据科学行业中最为突出,并从曾经也想成为数据科学家的人那里得到鼓励。运营研究和管理科学研究所(INFORMS)是最大的此类国际组织,拥有数千名成员。该组织有不仅组织线下活动,也有一个仅限会员在线讨论的论坛。然而,有抱负的数据科学家也可能会在他们各自的社区中进行探索。MeetUp.com提供全球各类会有议,并有超过https://www.meetup.com/topics/data-science/个与数据科学相关的活动。2.熟悉新兴趋势并将其应用于职业目标数据科学是一个快速发展的行业,能够很好地跟上不断变化的环境的专业人士通常是那些有意识地努力实现这一目标的人。物联网、开源工具和预测分析是2019年可能突出的趋势。寻求成为数据科学家的人不仅要了解趋势并及时了解相关的最新消息,还需要研究如何将这些趋势应用于他们的职业目标中去。例如,一个人可以探索新的开源数据科学软件,并尽快开始使用它以熟悉其工作原理。或者,参加有关预测分析基础知识的在线课程,并了解为什么该领域知识对于雇用数据科学家的公司如此重要,掌握好基础,为成为一名数据科学家打下了很好的基础。3.制定具体目标以促进数据科学项目的进展许多数据科学家或想在该领域工作的人都有自己学习的时期,这意味着即使还没有正式的数据科学训练,也可以独立启动数据科学项目,这些是可以通过好奇心和自我技能提升的渴望来推动的。那些着眼于数据科学职业的人应该尝试一个具体的目标设定系统,比如阿里、谷歌和其他知名公司的团队开发的数据科学项目,一般涉及提出的目标和关键结果(OKR)。个人目标与项目的目标相关,关键结果代表了一个人如何实现目标。一个人,如果能够满足70%的关键结果,OKR就是成功的。一个人可以通过选择与之相关的最有意义的指标,将OKR应用于数据科学项目。这类指标塑造目标,个人必须经历一些过程才能使项目富有成效,取得一些关键性结果,在每次取得关键结果时最好都记录一个与之关联的日期,便于掌握自己的成长以及项目的进程。4.考虑获得高级数据科学学位一个人如果希望在未来的数据科学职业中大幅提升收入,一种快速的方法就是取得高级数据科学学位,目前有许多学校提供数据科学专业的工商管理硕士(MBA)学位。收集相关学校和课程信息,并将其列入候选名单,每周抽出一个晚上探索一所学校的课程。采用这种方法每月大约可以获得20所学校的详细信息,并且获取的信息都比较详细,收集得也不仓促。MBA毕业生的平均工资取决于所选择的岗位的专注度和个人工作年限等因素。由于数据科学技能需求非常高,因此专注数据科学MBA可能会使求职者脱颖而出。最近调查的统计数据显示,国内外大多数行业对数据科学人才的需求还有很大的缺口。高等学位可以使一个人有足够的能力投身于这个行业,并使他们能够获得高于平均水平的工资。5.提高数据讲故事能力在数据集合中查找有意义的信息是数据科学家必备的技能,但该人员也必须是一位出色的讲故事者,能够将分析信息生动且清晰传递给别人。否则,企业的决策者将无法理解为什么从数据中得出的特定结论是有价值的。如果只是自己明白数据所传递的信息,而无法将信息传递给听众的话,且没有引起他们的注意,他们就不会根据信息做出改变,影响公司的决断。在2019年里,一个好的练习方法是将得到的数据科学结果传达给没有数据科学背景的朋友,并听取他们的意见,进而做出调整与改变。6.学习一些新的编程语言数据科学家在其工作中会使用各种编程语言。每年积极学习一些新的编程语言,以获得必要的知识,进而在未来的职业生涯中取得优异成绩。如果想要从事数据科学工作的人还不没有掌握任何一门编程语言,那么2019年是扩充自己知识的重要时刻。Python是一种容易快速上手且流行的编程语言,在数据科学领域非常流行,可以从该语言学起,但如果是想从事金融或游戏行业,可以从R语言开始。另外,也需要学习一些数据相关知识,比如SQL、Hadoop等工具。但是当努力提高编程语言能力时,要注意一点,不要贪求了解其它许多编程语言,最好是能够熟练掌握一两种编程语言。拥有正确的心态至关重要除了上述这些目标之外,还要保持良好的心态。自学的道路是比较坎坷的,摸着石头过河,即使遇到挫折,也应该保持自己的积极性。此外,发展投身于数据科学工作的精神可以使未来的数据科学家为他们工作的公司提供更大的资本。本文作者:【方向】阅读原文本文为云栖社区原创内容,未经允许不得转载。

November 26, 2018 · 1 min · jiezi

学习编程并不是学习编程语言

作者:zooboole英文原文:《Learning programming is different from learning a programming language》我们都是程序员,也是学习者。令人惊讶的是,如此多的人以为自己在学习编程,却已经步入歧途。你可能正在学习编程语言,而不是编程本身大家都知道计算机科学不是研究计算机,它反倒是利用计算机研究自动解决问题的。问题解决是计算机科学,不是编程。这就许多计算机科学专业的学生似乎不理解他们为什么要学习算法或数学的原因。如果你以前上过计算机科学课,你就应该知道我在说什么。因为你会注意到编程与编程语言几乎没有关系。问问自己为什么伪代码在这些课程中如此常见。但是,大多数自以为是的程序员总是落入陷阱。在意识到进行编程时到底什么是应该要做的之前,我们学习了几十年的编程语言。我自己也是受害者。我花了十多年的时间一点一点地学习各种编程语言。我学的越多,就越难以简单的方式解决问题。我以为是没有找到合适的工具。但问题是,当我甚至还不知道这个工作要做什么时,就去寻找合适的工具,而忘记了找出真正的工作是该做什么。编程语言的奇怪之处在于它们总是在不断发展。编程语言几乎每天都在变化,跟进很难。而大多数优秀的程序只使用了编程语言的一小部分。首先,学习编程语言的问题就像在学习木工之前学习如何使用木工锯,锤子和各种切割机器。木工需要注意:想法,可行性分析,测量,测试,客户行为。资深木匠感兴趣的事物不止于锤子和钉子。在他对这项工作的研究中,还需要时间来检查钉子、着色剂、木材等的质量。学习编程和学习编程语言的区别是什么呢?编程是通过一次下达指令来设置一个系统自动运行。我们每天都这样做。我们教我们的孩子,命令我们的士兵,服务我们的客户。我们给予或收到指示,以自由/独立的方式生活。父母不需要跟随并指导你在生活中所做的每一个动作。他们可能已经在生活的许多方面为你编程了。大多数学校和教学网站都会教授编程语言的语法。他们可以添加一些设计模式(当你忽略究竟是什么设计时)、一些算术计算。教你如何声明变量以及如何使用它们;教你如何声明数据类型以及创建它们。这并不能教你推理。但后来,您将会遇见推理方法。使用那些方法来学习,会让你觉得是浪费生命或者花了很多时间来学习编程。我们用编程来解决问题,编程语言是帮助我们达到目的工具。它们就象工具箱,我们称之为框架,帮助你组织你的思维。如果你正在学习编程且仍然无法设计和编写真实应用程序,那么这就意味着你正在学习编程语言而不是编程。我们经常会遇到想知道如何创建程序的学习者。对于程序员来说,程序是一个问题求解。在使用任何编程语言之前,他通过关键分析解决了问题。当你解决任何问题时,你可以用任何编程语言来编码。我们来看看平方求解的案例。为了求解平方,我们将它与自己相乘。我们可以用各种语言实现它,例如:C语言function square(int * x) { return x * x;}PHP语言function square ($x){ return $x * $x;}Javascript语言function square(x){ return x * x}Scheme(a Lisp dialect)语言(define (square x) (* x x))您应该注意到实现中只有语法是不一样的,解决方案是一样的。这也是你几乎可以使用任何编程语言的主要原因之一,在这种语言中你可以更轻松地构建任何类型的软件。编程可以让你更容易理解一门语言通常,问题出在人类语言,它充满了局限和错误。人类语言不能用来指令机器,因为它们不理解。你学习编程时,是在学习一种新术语和工具,来帮助你以计算机或其他程序员可以理解和同意的方式编写逻辑。通常,你将从简单且类似人类语言的符号–伪代码开始。它是从人类语言到计算机编程语言的良好过渡工具。这通常是为了避免浪费时间在具体的编程语言上,这样你可以完全专注于推理。通过它,你将发现构成良好编程工具(语言)的核心部分。你知道了真正需要的是什么、掌握了编程语言的核心目标。在编程实践过程中,你会不知不觉地就学会了这门编程语言。相关文章如何学习一门计算机编程语言

November 20, 2018 · 1 min · jiezi

【基本功】深入剖析Swift性能优化

简介2014年,苹果公司在WWDC上发布Swift这一新的编程语言。经过几年的发展,Swift已经成为iOS开发语言的“中流砥柱”,Swift提供了非常灵活的高级别特性,例如协议、闭包、泛型等,并且Swift还进一步开发了强大的SIL(Swift Intermediate Language)用于对编译器进行优化,使得Swift相比Objective-C运行更快性能更优,Swift内部如何实现性能的优化,我们本文就进行一下解读,希望能对大家有所启发和帮助。针对Swift性能提升这一问题,我们可以从概念上拆分为两个部分:编译器:Swift编译器进行的性能优化,从阶段分为编译期和运行期,内容分为时间优化和空间优化。开发者:通过使用合适的数据结构和关键字,帮助编译器获取更多信息,进行优化。下面我们将从这两个角度切入,对Swift性能优化进行分析。通过了解编译器对不同数据结构处理的内部实现,来选择最合适的算法机制,并利用编译器的优化特性,编写高性能的程序。理解Swift的性能理解Swift的性能,首先要清楚Swift的数据结构,组件关系和编译运行方式。数据结构Swift的数据结构可以大体拆分为:Class,Struct,Enum。组件关系组件关系可以分为:inheritance,protocols,generics。方法分派方式方法分派方式可以分为Static dispatch和Dynamic dispatch。要在开发中提高Swift性能,需要开发者去了解这几种数据结构和组件关系以及它们的内部实现,从而通过选择最合适的抽象机制来提升性能。首先我们对于性能标准进行一个概念陈述,性能标准涵盖三个标准:AllocationReference countingMethod dispatch接下来,我们会分别对这几个指标进行说明。Allocation内存分配可以分为堆区栈区,在栈的内存分配速度要高于堆,结构体和类在堆栈分配是不同的。Stack基本数据类型和结构体默认在栈区,栈区内存是连续的,通过出栈入栈进行分配和销毁,速度很快,高于堆区。我们通过一些例子进行说明://示例 1// Allocation// Structstruct Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //进行point1初始化,开辟栈内存var point2 = point1 //初始化point2,拷贝point1内容,开辟新内存point2.x = 5 //对point2的操作不会影响point1// use point1// use point2以上结构体的内存是在栈区分配的,内部的变量也是内联在栈区。将point1赋值给point2实际操作是在栈区进行了一份拷贝,产生了新的内存消耗point2,这使得point1和point2是完全独立的两个实例,它们之间的操作互不影响。在使用point1和point2之后,会进行销毁。Heap高级的数据结构,比如类,分配在堆区。初始化时查找没有使用的内存块,销毁时再从内存块中清除。因为堆区可能存在多线程的操作问题,为了保证线程安全,需要进行加锁操作,因此也是一种性能消耗。// Allocation// Classclass Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //在堆区分配内存,栈区只是存储地址指针let point2 = point1 //不产生新的实例,而是对point2增加对堆区内存引用的指针point2.x = 5 //因为point1和point2是一个实例,所以point1的值也会被修改// use point1// use point2以上我们初始化了一个Class类型,在栈区分配一块内存,但是和结构体直接在栈内存储数值不同,我们只在栈区存储了对象的指针,指针指向的对象的内存是分配在堆区的。需要注意的是,为了管理对象内存,在堆区初始化时,除了分配属性内存(这里是Double类型的x,y),还会有额外的两个字段,分别是type和refCount,这个包含了type,refCount和实际属性的结构被称为blue box。内存分配总结从初始化角度,Class相比Struct需要在堆区分配内存,进行内存管理,使用了指针,有更强大的特性,但是性能较低。优化方式:对于频繁操作(比如通信软件的内容气泡展示),尽量使用Struct替代Class,因为栈内存分配更快,更安全,操作更快。Reference countingSwift通过引用计数管理堆对象内存,当引用计数为0时,Swift确认没有对象再引用该内存,所以将内存释放。对于引用计数的管理是一个非常高频的间接操作,并且需要考虑线程安全,使得引用计数的操作需要较高的性能消耗。对于基本数据类型的Struct来说,没有堆内存分配和引用计数的管理,性能更高更安全,但是对于复杂的结构体,如:// Reference Counting// Struct containing referencesstruct Label { var text:String var font:UIFont func draw() { … }}let label1 = Label(text:“Hi”, font:font) //栈区包含了存储在堆区的指针let label2 = label1 //label2产生新的指针,和label1一样指向同样的string和font地址// use label1// use label2这里看到,包含了引用的结构体相比Class,需要管理双倍的引用计数。每次将结构体作为参数传递给方法或者进行直接拷贝时,都会出现多份引用计数。下图可以比较直观的理解:备注:包含引用类型的结构体出现Copy的处理方式Class在拷贝时的处理方式:引用计数总结Class在堆区分配内存,需要使用引用计数器进行内存管理。基本类型的Struct在栈区分配内存,无引用计数管理。包含强类型的Struct通过指针管理在堆区的属性,对结构体的拷贝会创建新的栈内存,创建多份引用的指针,Class只会有一份。优化方式在使用结构体时:通过使用精确类型,例如UUID替代String(UUID字节长度固定128字节,而不是String任意长度),这样就可以进行内存内联,在栈内存储UUID,我们知道,栈内存管理更快更安全,并且不需要引用计数。Enum替代String,在栈内管理内存,无引用计数,并且从语法上对于开发者更友好。Method Dispatch我们之前在Static dispatch VS Dynamic dispatch中提到过,能够在编译期确定执行方法的方式叫做静态分派Static dispatch,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派Dynamic dispatch。Static dispatch更快,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了Dynamic dispatch动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于block了编译器的一些后期优化。所以速度慢于Static dispatch。下面看一段多态代码,以及分析实现方式://引用语义实现的多态class Drawable { func draw() {} }class Point :Drawable { var x, y:Double override func draw() { … }}class Line :Drawable { var x1, y1, x2, y2:Double override func draw() { … }}var drawables:[Drawable]for d in drawables { d.draw()}Method Dispatch总结Class默认使用Dynamic dispatch,因为在编译期几乎每个环节的信息都无法确定,所以阻碍了编译器的优化,比如inline和whole module inline。使用Static dispatch代替Dynamic dispatch提升性能我们知道Static dispatch快于Dynamic dispatch,如何在开发中去尽可能使用Static dispatch。inheritance constraints继承约束我们可以使用final关键字去修饰Class,以此生成的Final class,使用Static dispatch。access control访问控制private关键字修饰,使得方法或属性只对当前类可见。编译器会对方法进行Static dispatch。编译器可以通过whole module optimization检查继承关系,对某些没有标记final的类通过计算,如果能在编译期确定执行的方法,则使用Static dispatch。Struct默认使用Static dispatch。Swift快于OC的一个关键是可以消解动态分派。总结Swift提供了更灵活的Struct,用以在内存、引用计数、方法分派等角度去进行性能的优化,在正确的时机选择正确的数据结构,可以使我们的代码性能更快更安全。延伸你可能会问Struct如何实现多态呢?答案是protocol oriented programming。以上分析了影响性能的几个标准,那么不同的算法机制Class,Protocol Types和Generic code,它们在这三方面的表现如何,Protocol Type和Generic code分别是怎么实现的呢?我们带着这个问题看下去。Protocol Type这里我们会讨论Protocol Type如何存储和拷贝变量,以及方法分派是如何实现的。不通过继承或者引用语义的多态:protocol Drawable { func draw() }struct Point :Drawable { var x, y:Double func draw() { … }}struct Line :Drawable { var x1, y1, x2, y2:Double func draw() { … }}var drawables:[Drawable] //遵守了Drawable协议的类型集合,可能是point或者linefor d in drawables { d.draw()}以上通过Protocol Type实现多态,几个类之间没有继承关系,故不能按照惯例借助V-Table实现动态分派。如果想了解Vtable和Witness table实现,可以进行点击查看,这里不做细节说明。因为Point和Line的尺寸不同,数组存储数据实现一致性存储,使用了Existential Container。查找正确的执行方法则使用了 Protoloc Witness Table 。Existential ContainerExistential Container是一种特殊的内存布局方式,用于管理遵守了相同协议的数据类型Protocol Type,这些数据类型因为不共享同一继承关系(这是V-Table实现的前提),并且内存空间尺寸不同,使用Existential Container进行管理,使其具有存储的一致性。结构如下:三个词大小的valueBuffer这里介绍一下valueBuffer结构,valueBuffer有三个词,每个词包含8个字节,存储的可能是值,也可能是对象的指针。对于small value(空间小于valueBuffer),直接存储在valueBuffer的地址内, inline valueBuffer,无额外堆内存初始化。当值的数量大于3个属性即large value,或者总尺寸超过valueBuffer的占位,就会在堆区开辟内存,将其存储在堆区,valueBuffer存储内存指针。value witness table的引用因为Protocol Type的类型不同,内存空间,初始化方法等都不相同,为了对Protocol Type生命周期进行专项管理,用到了Value Witness Table。protocol witness table的引用管理Protocol Type的方法分派。内存分布如下:1. payload_data_0 = 0x0000000000000004,2. payload_data_1 = 0x0000000000000000,3. payload_data_2 = 0x0000000000000000,4. instance_type = 0x000000010d6dc408 ExistentialContainers`type metadata for ExistentialContainers.Car,5. protocol_witness_0 = 0x000000010d6dc1c0 ExistentialContainers protocol witness table for ExistentialContainers.Car:ExistentialContainers.Drivable in ExistentialContainersProtocol Witness Table(PWT)为了实现Class多态也就是引用语义多态,需要V-Table来实现,但是V-Table的前提是具有同一个父类即共享相同的继承关系,但是对于Protocol Type来说,并不具备此特征,故为了支持Struct的多态,需要用到protocol oriented programming机制,也就是借助Protocol Witness Table来实现(细节可以点击Vtable和witness table实现,每个结构体会创造PWT表,内部包含指针,指向方法具体实现)。Value Witness Table(VWT)用于管理任意值的初始化、拷贝、销毁。Value Witness Table的结构如上,是用于管理遵守了协议的Protocol Type实例的初始化,拷贝,内存消减和销毁的。Value Witness Table在SIL中还可以拆分为%relative_vwtable和%absolute_vwtable,我们这里先不做展开。Value Witness Table和Protocol Witness Table通过分工,去管理Protocol Type实例的内存管理(初始化,拷贝,销毁)和方法调用。我们来借助具体的示例进行进一步了解:// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)在Swift编译器中,通过Existential Container实现的伪代码如下:// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)//existential container的伪代码结构struct ExistContDrawable { var valueBuffer:(Int, Int, Int) var vwt:ValueWitnessTable var pwt:DrawableProtocolWitnessTable}// drawACopy方法生成的伪代码func drawACopy(val:ExistContDrawable) { //将existential container传入 var local = ExistContDrawable() //初始化container let vwt = val.vwt //获取value witness table,用于管理生命周期 let pwt = val.pwt //获取protocol witness table,用于进行方法分派 local.type = type local.pwt = pwt vwt.allocateBufferAndCopyValue(&local, val) //vwt进行生命周期管理,初始化或者拷贝 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,这里说一下projectBuffer,因为不同类型在内存中是不同的(small value内联在栈内,large value初始化在堆内,栈持有指针),所以方法的确定也是和类型相关的,我们知道,查找方法时是通过当前对象的地址,通过一定的位移去查找方法地址。 vwt.destructAndDeallocateBuffer(temp) //vwt进行生命周期管理,销毁内存}Protocol Type 存储属性我们知道,Swift中Class的实例和属性都存储在堆区,Struct实例在栈区,如果包含指针属性则存储在堆区,Protocol Type如何存储属性?Small Number通过Existential Container内联实现,大数存在堆区。如何处理Copy呢?Protocol大数的Copy优化在出现Copy情况时:let aLine = Line(1.0, 1.0, 1.0, 3.0)let pair = Pair(aLine, aLine)let copy = pair会将新的Exsitential Container的valueBuffer指向同一个value即创建指针引用,但是如果要改变值怎么办?我们知道Struct值的修改和Class不同,Copy是不应该影响原实例的值的。这里用到了一个技术叫做Indirect Storage With Copy-On-Write,即优先使用内存指针。通过提高内存指针的使用,来降低堆区内存的初始化。降低内存消耗。在需要修改值的时候,会先检测引用计数检测,如果有大于1的引用计数,则开辟新内存,创建新的实例。在对内容进行变更的时候,会开启一块新的内存,伪代码如下:class LineStorage { var x1, y1, x2, y2:Double }struct Line :Drawable { var storage :LineStorage init() { storage = LineStorage(Point(), Point()) } func draw() { … } mutating func move() { if !isUniquelyReferencedNonObjc(&storage) { //如何存在多份引用,则开启新内存,否则直接修改 storage = LineStorage(storage) } storage。start = … }}这样实现的目的:通过多份指针去引用同一份地址的成本远远低于开辟多份堆内存。以下对比图:Protocol Type多态总结支持Protocol Type的动态多态(Dynamic Polymorphism)行为。通过使用Witness Table和Existential Container来实现。对于大数的拷贝可以通过Indirect Storage间接存储来进行优化。说到动态多态Dynamic Polymorphism,我们就要问了,什么是静态多态Static Polymorphism,看看下面示例:// Drawing a copyprotocol Drawable { func draw()}func drawACopy(local :Drawable) { local.draw()}let line = Line()drawACopy(line)// …let point = Point()drawACopy(point)这种情况我们就可以用到泛型Generic code来实现,进行进一步优化。泛型我们接下来会讨论泛型属性的存储方式和泛型方法是如何分派的。泛型和Protocol Type的区别在于:泛型支持的是静态多态。每个调用上下文只有一种类型。查看下面的示例,foo和bar方法是同一种类型。在调用链中会通过类型降级进行类型取代。对于以下示例:func foo<T:Drawable>(local :T) { bar(local)}func bar<T:Drawable>(local:T) { … }let point = Point()foo(point)分析方法foo和bar的调用过程://调用过程foo(point)–>foo<T = Point>(point) //在方法执行时,Swift将泛型T绑定为调用方使用的具体类型,这里为Point bar(local) –>bar<T = Point>(local) //在调用内部bar方法时,会使用foo已经绑定的变量类型Point,可以看到,泛型T在这里已经被降级,通过类型Point进行取代泛型方法调用的具体实现为:同一种类型的任何实例,都共享同样的实现,即使用同一个Protocol Witness Table。使用Protocol/Value Witness Table。每个调用上下文只有一种类型:这里没有使用Existential Container, 而是将Protocol/Value Witness Table作为调用方的额外参数进行传递。变量初始化和方法调用,都使用传入的VWT和PWT来执行。看到这里,我们并不觉得泛型比Protocol Type有什么更快的特性,泛型如何更快呢?静态多态前提下可以进行进一步的优化,称为特定泛型优化。泛型特化静态多态:在调用站中只有一种类型Swift使用只有一种类型的特点,来进行类型降级取代。类型降级后,产生特定类型的方法为泛型的每个类型创造对应的方法这时候你可能会问,那每一种类型都产生一个新的方法,代码空间岂不爆炸?静态多态下进行特定优化specialization因为是静态多态。所以可以进行很强大的优化,比如进行内联实现,并且通过获取上下文来进行更进一步的优化。从而降低方法数量。优化后可以更精确和具体。例如:func min<T:Comparable>(x:T, y:T) -> T { return y < x ? y : x}从普通的泛型展开如下,因为要支持所有类型的min方法,所以需要对泛型类型进行计算,包括初始化地址、内存分配、生命周期管理等。除了对value的操作,还要对方法进行操作。这是一个非常复杂庞大的工程。func min<T:Comparable>(x:T, y:T, FTable:FunctionTable) -> T { let xCopy = FTable.copy(x) let yCopy = FTable.copy(y) let m = FTable.lessThan(yCopy, xCopy) ? y :x FTable.release(x) FTable.release(y) return m}在确定入参类型时,比如Int,编译器可以通过泛型特化,进行类型取代(Type Substitute),优化为:func min<Int>(x:Int, y:Int) -> Int { return y < x ? y :x}泛型特化specilization是何时发生的?在使用特定优化时,调用方需要进行类型推断,这里需要知晓类型的上下文,例如类型的定义和内部方法实现。如果调用方和类型是单独编译的,就无法在调用方推断类型的内部实行,就无法使用特定优化,保证这些代码一起进行编译,这里就用到了whole module optimization。而whole module optimization是对于调用方和被调用方的方法在不同文件时,对其进行泛型特化优化的前提。泛型进一步优化特定泛型的进一步优化:// Pairs in our program using generic typesstruct Pair<T :Drawable> { init(_ f:T, _ s:T) { first = f ; second = s } var first:T var second:T}let pairOfLines = Pair(Line(), Line())// …let pairOfPoint = Pair(Point(), Point())在用到多种泛型,且确定泛型类型不会在运行时修改时,就可以对成对泛型的使用进行进一步优化。优化的方式是将泛型的内存分配由指针指定,变为内存内联,不再有额外的堆初始化消耗。请注意,因为进行了存储内联,已经确定了泛型特定类型的内存分布,泛型的内存内联不能存储不同类型。所以再次强调此种优化只适用于在运行时不会修改泛型类型,即不能同时支持一个方法中包含line和point两种类型。whole module optimizationwhole module optimization是用于Swift编译器的优化机制。可以通过-whole-module-optimization (或 -wmo)进行打开。在XCode 8之后默认打开。 Swift Package Manager在release模式默认使用whole module optimization。module是多个文件集合。编译器在对源文件进行语法分析之后,会对其进行优化,生成机器码并输出目标文件,之后链接器联合所有的目标文件生成共享库或可执行文件。whole module optimization通过跨函数优化,可以进行内联等优化操作,对于泛型,可以通过获取类型的具体实现来进行推断优化,进行类型降级方法内联,删除多余方法等操作。全模块优化的优势编译器掌握所有方法的实现,可以进行内联和泛型特化等优化,通过计算所有方法的引用,移除多余的引用计数操作。通过知晓所有的非公共方法,如果这写方法没有被使用,就可以对其进行消除。如何降低编译时间和全模块优化相反的是文件优化,即对单个文件进行编译。这样的好处在于可以并行执行,并且对于没有修改的文件不会再次编译。缺点在于编译器无法获知全貌,无法进行深度优化。下面我们分析下全模块优化如何避免没修改的文件再次编译。编译器内部运行过程分为:语法分析,类型检查,SIL优化,LLVM后端处理。语法分析和类型检查一般很快,SIL优化执行了重要的Swift特定优化,例如泛型特化和方法内联等,该过程大概占用整个编译时间的三分之一。LLVM后端执行占用了大部分的编译时间,用于运行降级优化和生成代码。进行全模块优化后,SIL优化会将模块再次拆分为多个部分,LLVM后端通过多线程对这些拆分模块进行处理,对于没有修改的部分,不会进行再处理。这样就避免了修改一小部分,整个大模块进行LLVM后端的再次执行,除此外,使用多线程并行操作也会缩短处理时间。扩展:Swift的隐藏“Bug”Swift因为方法分派机制问题,所以在设计和优化后,会产生和我们常规理解不太一致的结果,这当然不能算Bug。但是还是要单独进行说明,避免在开发过程中,因为对机制的掌握不足,造成预期和执行出入导致的问题。Message dispatch我们通过上面说明结合Static dispatch VS Dynamic dispatch对方法分派方式有了了解。这里需要对Objective-C的方法分派方式进行说明。熟悉OC的人都知道,OC采用了运行时机制使用obj_msgSend发送消息,runtime非常的灵活,我们不仅可以对方法调用采用swizzling,对于对象也可以通过isa-swizzling来扩展功能,应用场景有我们常用的hook和大家熟知的KVO。大家在使用Swift进行开发时都会问,Swift是否可以使用OC的运行时和消息转发机制呢?答案是可以。Swift可以通过关键字dynamic对方法进行标记,这样就会告诉编译器,此方法使用的是OC的运行时机制。注意:我们常见的关键字@ObjC并不会改变Swift原有的方法分派机制,关键字@ObjC的作用只是告诉编译器,该段代码对于OC可见。总结来说,Swift通过dynamic关键字的扩展后,一共包含三种方法分派方式:Static dispatch,Table dispatch和Message dispatch。下表为不同的数据结构在不同情况下采取的分派方式:如果在开发过程中,错误的混合了这几种分派方式,就可能出现Bug,以下我们对这些Bug进行分析:SR-584此情况是在子类的extension中重载父类方法时,出现和预期不同的行为。class Base:NSObject { var directProperty:String { return “This is Base” } var indirectProperty:String { return directProperty }}class Sub:Base { }extension Sub { override var directProperty:String { return “This is Sub” }}执行以下代码,直接调用没有问题:Base().directProperty // “This is Base”Sub().directProperty // “This is Sub”间接调用结果和预期不同:Base()。indirectProperty // “This is Base”Sub()。indirectProperty // expected “this is Sub”,but is “This is Base” <- Unexpected!在Base.directProperty前添加dynamic关键字就可以获得"this is Sub"的结果。Swift在extension 文档中说明,不能在extension中重载已经存在的方法。“Extensions can add new functionality to a type, but they cannot override existing functionality.”会出现警告:Cannot override a non-dynamic class declaration from an extension。出现这个问题的原因是,NSObject的extension是使用的Message dispatch,而Initial Declaration使用的是Table dispath(查看上图 Swift Dispatch Method)。extension重载的方法添加在了Message dispatch内,没有修改虚函数表,虚函数表内还是父类的方法,故会执行父类方法。想在extension重载方法,需要标明dynamic来使用Message dispatch。SR-103协议的扩展内实现的方法,无法被遵守类的子类重载:protocol Greetable { func sayHi()}extension Greetable { func sayHi() { print(“Hello”) }}func greetings(greeter:Greetable) { greeter.sayHi()}现在定义一个遵守了协议的类Person。遵守协议类的子类LoudPerson:class Person:Greetable {}class LoudPerson:Person { func sayHi() { print(“sub”) }}执行下面代码结果为:var sub:LoudPerson = LoudPerson()sub.sayHi() //sub不符合预期的代码:var sub:Person = LoudPerson()sub.sayHi() //HellO <-使用了protocol的默认实现注意,在子类LoudPerson中没有出现override关键字。可以理解为LoudPerson并没有成功注册Greetable在Witness table的方法。所以对于声明为Person实际为LoudPerson的实例,会在编译器通过Person去查找,Person没有实现协议方法,则不产生Witness table,sayHi方法是直接调用的。解决办法是在base类内实现协议方法,无需实现也要提供默认方法。或者将基类标记为final来避免继承。进一步通过示例去理解:// Defined protocol。protocol A { func a() -> Int}extension A { func a() -> Int { return 0 }}// A class doesn’t have implement of the function。class B:A {}class C:B { func a() -> Int { return 1 }}// A class has implement of the function。class D:A { func a() -> Int { return 1 }}class E:D { override func a() -> Int { return 2 }}// Failure cases。B().a() // 0C().a() // 1(C() as A).a() // 0 # We thought return 1。 // Success cases。D().a() // 1(D() as A).a() // 1E().a() // 2(E() as A).a() // 2其他我们知道Class extension使用的是Static Dispatch:class MyClass {}extension MyClass { func extensionMethod() {}} class SubClass:MyClass { override func extensionMethod() {}}以上代码会出现错误,提示Declarations in extensions can not be overridden yet。总结影响程序的性能标准有三种:初始化方式, 引用指针和方法分派。文中对比了两种数据结构:Struct和Class的在不同标准下的性能表现。Swift相比OC和其它语言强化了结构体的能力,所以在了解以上性能表现的前提下,通过利用结构体可以有效提升性能。在此基础上,我们还介绍了功能强大的结构体的类:Protocol Type和Generic。并且介绍了它们如何支持多态以及通过使用有条件限制的泛型如何让程序更快。参考资料swift memorylayoutwitness table videoprotocol types pdfprotocol and value oriented programming in UIKit apps videooptimizing swift performancewhole module optimizaitonincreasing performance by reducing dynamic dispatchprotocols generics existential containerprotocols and genericswhy swift is swiftswift method dispatchswift extensionuniversal dynamic dispatch for method callscompiler performance.mdstructures and classes作者简介亚男,美团点评iOS工程师。2017年加入美团点评,负责专业版餐饮管家开发,研究编译器原理。目前正积极推动Swift组件化建设。招聘信息我们餐饮生态技术部是一个技术氛围活跃,大牛聚集的地方。新到店紧握真正的大规模SaaS实战机会,多租户、数据、安全、开放平台等全方位的挑战。业务领域复杂技术挑战多,技术和业务能力迅速提升,最重要的是,加入我们,你将实现真正通过代码来改变行业的梦想。我们欢迎各端人才加入,Java优先。感兴趣的同学赶紧发送简历至 zhaoyanan02@meituan.com,我们期待你的到来。 ...

November 2, 2018 · 5 min · jiezi