<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0">
  <channel>
    <title>IT瘾编程推荐</title>
    <link>https://itindex.net/categories/编程</link>
    <description>IT社区推荐资讯 - ITIndex.net</description>
    <language>zh</language>
    <copyright>https://itindex.net/</copyright>
    <generator>https://itindex.net/</generator>
    <docs>http://backend.userland.com/rss</docs>
    <image>
      <url>https://itindex.net/images/logo.gif</url>
      <title>IT社区推荐资讯 - ITIndex.net</title>
      <link>https://itindex.net/categories/编程</link>
    </image>
    <item>
      <title>大模型真的加快了程序员的编程速度？</title>
      <link>https://itindex.net/detail/63127-%E6%A8%A1%E5%9E%8B-%E7%A8%8B%E5%BA%8F%E5%91%98-%E7%BC%96%E7%A8%8B</link>
      <description>MIT Technology Review 采访逾 30 名开发者、科技公司高管、分析师和研究人员后发现，基于大模型的 AI 工具是否加快程序员编程速度不是一个一锤定音的问题。随着一线程序员认识到大模型的局限性，他们对 AI 工具的狂热开始消退。众多研究表明，AI 工具所宣称的生产力提升可能只是一种假象。GitClear 的数据显示 2022 年以来工程师所写代码的持久性——数周内代码不会被删除或重写——提高约 10%，这一改进可能需要归功于 AI。但与此同时，代码的多项质量指标在快速下降。编程问答平台 Stack Overflow 的调查首次显示对 AI 工具的信任度和好感度显著下降。程序员普遍认同 AI 工具的优势在于生成“样板代码”，编写测试、修 bug 以及向新手解释不熟悉的代码。但对于经验丰富的程序员而言，此类任务只占工作量的一小部分，AI 工具对于解决复杂难题帮助不大。基于大模型的 AI 工具也不可避免存在幻觉，它们生成的代码看起来完美，因此很难发现错误。所以使用 AI 工具就像是玩老虎机，有的时候大有帮助，但其它情况可能完全不可靠。
 &lt;p&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/63127-%E6%A8%A1%E5%9E%8B-%E7%A8%8B%E5%BA%8F%E5%91%98-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Wed, 24 Dec 2025 00:58:19 CST</pubDate>
    </item>
    <item>
      <title>AI 编程带来的新变革</title>
      <link>https://itindex.net/detail/63074-ai-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;div&gt;今天看到一篇文章，作者（Joe Mag）是亚马逊的一位工程师，他所在的一个资深工程师团队，过去三个月里借助 Coding Agent，代码产出效率是普通高效团队的 10 倍。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;但作者可不像傅老板那样说人类程序员要被替代，毕竟人家是专业程序员，他反而是来浇冷水的。他用亲身经历告诉我们：当你的速度提升 10 倍时，你面临的风险和瓶颈，可能也放大了 10 倍。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;并且“AI Coding”带来了新的变革”——它彻底改变了软件工程中关于“成本”和“收益”的传统计算方式。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;他们的用法是这样的：&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;他自己提交的代码里，有 80% 是 AI 写的。但这绝不是“Vibe Coding”。他们管自己的工作流叫 “Agentic Coding”（智能体编码）。AI 在这里扮演一个“能力超强、但没有责任感”的初级程序员。人类工程师则是那个经验丰富的“技术主管”或“架构师”。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;工程师的工作流变成了：&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;1. 拆解任务（自己先想明白，或者和 AI 一起头脑风暴）。&lt;/div&gt; &lt;div&gt;2. 给 AI 下指令。&lt;/div&gt; &lt;div&gt;3. 逐行审查 AI 的输出。&lt;/div&gt; &lt;div&gt;4. 反复迭代，直到对质量完全满意。&lt;/div&gt; &lt;div&gt;5. 最终提交PR，并为这些代码负全责。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;这里面人的作用是最重要的，并没有闲着，只是把工作重心从手写代码转移到了“提要求”和“代码审查”上。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;我以前打过一个比方：如果说 AI 编程就像汽车引擎一样，可以越来越快，但是软件项目交付速度并不会跟着一起变快，因为汽车跑得快，光引擎快还不够，还要考虑路况、司机的能力。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;作者在文章中也用了一个类似的比喻：&lt;/div&gt; &lt;div&gt;&amp;gt; 当你在赛道上以 200 迈的速度狂飙时，你需要巨大的“下压力”来把车死死按在地面上，否则你会在第一个弯道就飞出去。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;在软件工程里，“飞出去”指的就是 Bug 和系统崩溃。编码的速度上去了，相应的 Bug 出现的概率和系统奔溃的概率也会跟着上升：&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;- 过去：一个团队可能一年才会遇到一两次严重的生产环境 Bug。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;- 现在：当你以 10 倍的速度提交代码时，哪怕 Bug 出现的 概率 不变，你遇到 Bug 的 绝对数量 也会翻 10 倍。以前一年一次的事故，现在可能每周都来一次。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;这种“事故率”是任何团队都难以承受的。而很多吹捧 Vibe Coding 的人有意无意都忽略了这些问题。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;要享受 AI 带来的 10 倍编码速度提升，相应的你也必须要想办法把“出问题的概率”降低 10 倍，甚至更多。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;当然作者也给了一些具体建议：&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;1. 借助 AI 降低本级搭建开发测试环境的成本，做好“风洞测试”&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;提高代码质量最好的办法就是加强测试。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;作者一直很推崇一种“风洞测试”的思路。就像造飞机一样，在真正上天前，会把模型放进风洞里吹。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;在软件里，这就是指在本地搭建一个“高仿真模拟环境”。比如，你的系统依赖了 10 个外部服务（数据库、认证、支付等），你就把这 10 个服务全本地运行或者模拟出来。然后，在你的电脑上就能完整地跑一遍端到端的测试，甚至可以模拟各种极端失败情况。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;这种测试能抓出大量在“组件”之间缝隙中隐藏的 Bug。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;以前为啥不做？太贵了！模拟和维护这些服务的工作量太大，大多数团队都放弃了。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;现在为什么能做了？AI 擅长干这个！ AI 智能体非常擅长写这种逻辑清晰、行为明确的模拟服务。作者的团队在 AI 的帮助下，只花了几天时间，就为他们那个相当复杂的系统搭建起了一套完整的本地“风洞”。过去要几周甚至几个月的活，现在几天搞定。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;2. 做好持续集成部署（CI/CD）&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;早些年瀑布开发的时候，模块划分好后，都是各自开发，开发后再集成，集成时会很多问题，要花很长时间才能稳定下来。后来持续集成开始流行：&lt;/div&gt; &lt;div&gt;&amp;gt; 越早集成越早可以得到反馈，越频繁集成越是可以减少问题复杂度。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;到现在 CI/CD 已经是公认的软件工程最佳实践，但真正做好的团队也不算太多，原因同样也是搭建和维护这样一套流程成本不低。另外还有一个问题就是很多团队虽然做了 CI/CD，但是流程耗时巨长，一个 PR 等所有各种测试、发布跑一遍，少则十分钟，多则上个小时。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;以前没有 AI 的时候，这些问题还不明显，现在 AI 能力强了反而成了障碍。特别像汽车引擎速度上来了，还是跑在土坑路上。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;所以 CI/CD 也需要跟着一起升级，把反馈循环从“小时级”压缩到“分钟级”。 你需要一套快到夸张的基础设施，能在几分钟内发现、隔离并回滚有问题的改动，让其他没有问题的改动继续前进。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;3. 决策和沟通系统也要升级&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;10 倍的代码产出，意味着也要 10 倍以上的沟通和决策效率。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;以前开发个系统，需要各种开会，讨论很久，最后才开工干活，毕竟要依赖其他人的模块，要先定义好协议，否则后面都没法集成。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;另外各种技术决策也要反复讨论很久，毕竟那时候开发成本高，如果决策错了返工代价太大。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;而现在如果还是以前的沟通效率，会极大的拉低整体效率，也许最高效的是尽可能的减少沟通，每个人做的事情尽可能不依赖于其他人。包括我以前提到过的微服务方案，也许在 AI 时代是个不错的选择。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;对技术决策来说，现在反倒是可以有更多机会试错了，不必像以前一样过于严谨的去反复论证技术决策。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;AI 带来的 10 倍编码速度，它更像是一个给汽车升级了一个全新的“引擎”。如果你只是把它装在原来的“老爷车”上，你得到的不会是 10 倍的速度，而是 10 倍的问题。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;AI Coding 的真正价值，不仅仅是写得快，更是它让那些过去“好是好，但太贵”的最佳软件工程实践（比如CI/CD、自动化测试）变得便宜可行了。就像你不仅仅是升级汽车的引擎，还把汽车经常跑的马路一起升级一下，以前的老司机不是让 AI 替代，而是让他们能适应新的高速引擎，给他们舒服和安全的驾驶环境。&lt;/div&gt; &lt;div&gt;  &lt;br /&gt;&lt;/div&gt; &lt;div&gt;---&lt;/div&gt; &lt;div&gt;原文：The New Calculus of AI-based Coding http://t.cn/AXAZcFaD&lt;/div&gt; &lt;div&gt;译文《AI 编程带来的新变革》： http://t.cn/AXAZcFak&lt;/div&gt;
     
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/63074-ai-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Wed, 29 Oct 2025 12:27:07 CST</pubDate>
    </item>
    <item>
      <title>上手使用AI辅助编程Cursor的感受</title>
      <link>https://itindex.net/detail/62993-%E4%B8%8A%E6%89%8B-ai-%E7%BC%96%E7%A8%8B</link>
      <description>Cursor 优化了一系列的小功能。Themes，Checkpoints，自动修复错误，Navbar，Previews，At功能，历史记录、Code Blocks（分别在图2-图9） &lt;br /&gt; &lt;br /&gt;这段时间用Cursor的感受： &lt;br /&gt;1) 对于不熟悉的领域有&amp;quot;10x工程师&amp;quot;的感觉，能让你快速熟悉一个新的项目或语言。最近干了一个Android项目（以前没真正写过线上代码），对于不熟悉的UI布局的优化，基础框架/Util的搭建，错误处理，ViewModel的逻辑，能极大加快上手开发的速度。 &lt;br /&gt; &lt;br /&gt;2）对于自己不熟悉的语言，AI容易给你埋坑，最终如果你不了解里面的原理，这些坑有天迟早会炸了（让你一团糟，需要用更长的时间来查bug或者返工） &lt;br /&gt; &lt;br /&gt;3) 对于自己熟悉的项目和语言，反而帮助没那么大。最大的帮助是刚建好数据库表的时候，让cursor帮忙生成model层，DAO层和grpc的接口协议，省去了很多重复工作。 &lt;br /&gt; &lt;br /&gt;4) 感觉马上要变成Tab工程师了
     
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62993-%E4%B8%8A%E6%89%8B-ai-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Mon, 17 Mar 2025 08:45:39 CST</pubDate>
    </item>
    <item>
      <title>成为超级个体：AI时代的研发人员编程技巧最佳实践  - Phodal  | Phodal - A Growth Engineer</title>
      <link>https://itindex.net/detail/62950-%E4%B8%AA%E4%BD%93-ai-%E6%97%B6%E4%BB%A3</link>
      <description>&lt;div&gt;Posted by:    &lt;a href="https://www.phodal.com/blog/author/root/"&gt;Phodal Huang&lt;/a&gt;Oct. 26, 2024, 2:31 p.m.    &lt;p&gt;&lt;/p&gt;    &lt;p&gt;随着AI技术在软件开发中的普及，开发人员的角色正在从单一技术专家逐渐转向“超级个体”——即具备跨界、创新和自我迭代能力的多面手。生成式AI不仅简化了传统开发流程，更通过不断迭代的反馈机制让开发人员掌握新技术栈、快速适应需求变化，并能将复杂的业务知识融入代码。本文结合生成式AI的实践场景，解析如何高效运用AI辅助工具并适应技术转型。&lt;/p&gt;    &lt;h2&gt;1. 生成式AI的能力与局限性&lt;/h2&gt;    &lt;p&gt;生成式AI工具，例如GitHub的Copilot，通过学习和理解代码片段生成代码或提供自动补全，显著提升了开发速度。例如，开发人员只需描述想要的功能，AI便可以生成初始代码，甚至包括架构和代码结构。然而，生成式AI的效果参差不齐，并非所有代码都符合项目需求。在GitHub的案例中，Copilot生成的代码的平均接受率为30%左右，主要原因在于AI生成的代码质量不一，有时会生成冗余代码或错误的逻辑，甚至出现“幻觉”代码（无依据的内容），这增加了代码审查的负担。&lt;/p&gt;    &lt;h3&gt;AI 体验的“曲折”过程&lt;/h3&gt;    &lt;p&gt;在实际操作中，生成式AI工具的使用体验并非一帆风顺。开发人员在首次接触时，通常会因AI生成的代码质量问题和提示词调试过程所困扰。尽管如此，通过反复迭代提示词和适应AI的特定模式，开发人员可以逐渐掌握生成式AI的应用，从而在实践中有效发挥AI的优势。&lt;/p&gt;    &lt;p&gt;然而，生成式AI在实际应用中也面临诸多限制，包括：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;strong&gt;上下文不完整&lt;/strong&gt;：生成内容缺乏上下文，特别是在业务逻辑复杂的情况下，往往需要开发者进行额外的修改和调试。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;生成质量不稳定&lt;/strong&gt;：生成内容可能包含冗余信息或“幻觉”（hallucinations），如生成与需求无关的代码或错误的逻辑。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;工具链不完善&lt;/strong&gt;：现有工具在生成内容质量、与现有IDE集成等方面仍有改进空间。例如，生成代码后还需在IDE中手动进行优化和测试，影响流畅度。&lt;/li&gt;&lt;/ul&gt;    &lt;h2&gt;2. AI辅助编程工具的实用技巧&lt;/h2&gt;    &lt;p&gt;为充分发挥AI辅助编程工具的效果，开发人员需要掌握一些实用的技巧，以便有效运用AI生成的代码和提示。AI辅助编程的三种主要模式为：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;strong&gt;聊天模式&lt;/strong&gt;：通过与AI进行多轮对话，开发人员可以从需求分析到代码示例、架构设计等方面获取全面支持。工具如GitHub Copilot Chat、Genie等允许用户通过自然语言输入获得上下文相关的代码块和最佳实践。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;伴随模式&lt;/strong&gt;：伴随模式工具如JetBrains AI Assistant或AutoDev，支持在开发过程中生成实时建议，包括错误分析、提交信息生成和文档撰写。这样开发人员可以在代码编辑时获得即时反馈，提升编程效率。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;实时模式&lt;/strong&gt;：在编码过程中提供自动完成、错误检测和代码优化建议等，如自动化地识别并修复代码中的潜在错误。这种方式确保了编码的实时反馈，减少了开发过程中的反复修改。&lt;/li&gt;&lt;/ul&gt;    &lt;h3&gt;如何构建有效的提示词&lt;/h3&gt;    &lt;p&gt;构建高质量提示词是提升AI生成代码质量的关键。好的提示词应该包括明确的上下文信息、需求描述和预期输出格式。在复杂需求场景中，开发人员可以通过分步提问，将需求拆分为具体的子任务。比如，如果需要生成一个面向API的提交信息，除了代码片段差异信息，还可以增加描述性内容，使提交信息更符合规范。&lt;/p&gt;    &lt;p&gt;不同的AI工具在不同的场景下需要不同的提示词策略。例如，在Copilot中，注释、函数名称和代码片段都是提示词的一部分，而在聊天模式下则需要明确地提出背景、需求和输出形式，以避免生成不符合预期的代码。&lt;/p&gt;    &lt;h2&gt;3. 融入软件工程实践，降低校验成本&lt;/h2&gt;    &lt;p&gt;在生成式AI辅助下的开发流程中，生成内容的校验与验证成为重要一环。传统的软件工程校验方法仍然适用于AI生成的代码，以确保代码符合业务逻辑并降低验证成本。以下是几种常见的校验策略：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;strong&gt;自动化测试&lt;/strong&gt;：通过单元测试、集成测试、接口测试等方法验证生成代码的准确性。例如在API开发过程中，可以使用RestAssured生成API测试，或通过WireMock模拟服务请求，以此来验证生成代码的稳定性和正确性。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;黑盒测试&lt;/strong&gt;：通过测试输入和输出，而无需关注代码的具体实现，确保生成的功能模块符合预期。例如在界面开发中，可以通过UI测试验证按钮交互和数据渲染效果。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;运行时反馈&lt;/strong&gt;：在IDE中构建实时反馈机制，结合代码生成平台和云端智能体（如Dify智能体）减少工具间的切换，从而实时校验生成内容是否符合业务需求。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;通过这些方法，开发者不仅能快速校验生成内容，还能在验证过程中积累反馈数据，帮助AI不断改进生成质量。&lt;/p&gt;    &lt;h2&gt;4. 重构开发流程：加速产出，优化知识转化&lt;/h2&gt;    &lt;p&gt;AI辅助编程带来了重构开发流程的机会，通过更流畅的资产生成与知识转化机制，可以大幅提升产出质量。在软件开发生命周期中，许多隐性知识如业务需求、架构设计等，往往会影响开发过程的效率。在AI的帮助下，这些知识可以系统化地转化为显性资产：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;strong&gt;知识积累与转化&lt;/strong&gt;：将业务逻辑、架构知识转化为规范文档或代码模板，为团队中的新成员提供了学习和上手的便利。例如，开发人员可以将API设计规范和测试用例以模板形式保存，通过AI辅助的提示在新项目中复用。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;流程自动化&lt;/strong&gt;：整合API开发工具链，通过自动生成API规范、测试用例和前端代码，使开发流程中的每个步骤都紧密相连，减少人为干预。例如，在前端代码开发中，通过AI生成与后端服务对接的API，避免手动集成可能导致的错误。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;实时反馈机制&lt;/strong&gt;：构建反馈闭环，通过自动化测试和性能测试结果及时反哺开发过程。例如，通过Apache Benchmark或Gatling进行性能测试，生成性能分析报告并反馈至开发阶段，提升生成代码的实际运行效果。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;这种知识转化和流程优化不仅减少了开发过程中的冗余工作，还能显著提高团队整体的响应速度。&lt;/p&gt;    &lt;h2&gt;5. 面向未来：适应技能变化，放大专业能力&lt;/h2&gt;    &lt;p&gt;AI辅助编程已成为开发人员扩展专业能力的利器，但未来的竞争力还需要建立在持续适应技能变化的基础上。AI可以提升开发效率，但设计思维、结构化思维和任务分解能力仍然是高效开发的核心。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;strong&gt;重新定义技能框架&lt;/strong&gt;：生成式AI工具促使开发人员重新评估自己的技能架构。通过不断学习和适应，开发人员可以掌握从需求分析到系统设计的全面知识，同时熟练掌握如何与AI工具互动，最大化AI对开发工作的促进作用。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;构建专业上下文&lt;/strong&gt;：AI工具生成内容的质量依赖于上下文的完整性。开发者可以通过精炼的上下文表达（如注释、函数命名等），确保AI理解需求，提高生成质量。例如，通过在任务中详细描述业务需求和技术背景，使生成内容更贴近实际需求。&lt;/li&gt;      &lt;li&gt;        &lt;strong&gt;快速验证与适应AI流&lt;/strong&gt;：未来的开发人员需要具备快速验证AI内容的专业能力。特别是在需求迭代和验证过程中，开发者应能够灵活应用AI生成的代码，通过实践优化生成的代码段，以提高AI生成内容的专业性和适用性。&lt;/li&gt;&lt;/ul&gt;    &lt;h2&gt;结语&lt;/h2&gt;    &lt;p&gt;AI时代的研发人员已不再仅仅是“执行者”，而是通过AI赋能，掌握全栈开发和跨界思维的“超级个体”。借助生成式AI工具，开发者能够在高效生成代码的同时，保证生成内容的质量，并通过自动化测试和反馈闭环不断优化工作流程。通过在实际项目中不断应用和改进AI辅助策略，未来的开发者将能够充分发挥AI的潜力，持续提升软件开发的整体效率和产品质量。&lt;/p&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62950-%E4%B8%AA%E4%BD%93-ai-%E6%97%B6%E4%BB%A3</guid>
      <pubDate>Sun, 27 Oct 2024 10:12:53 CST</pubDate>
    </item>
    <item>
      <title>编程语言是如何实现并发的之操作系统篇 · BMPI</title>
      <link>https://itindex.net/detail/62933-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%B9%B6%E5%8F%91-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F</link>
      <description>&lt;div&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E4%BB%8E%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%BF%90%E8%A1%8C%E7%A8%8B%E5%BA%8F%E8%AF%B4%E8%B5%B7"&gt;从操作系统运行程序说起&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%94%AF%E6%8C%81"&gt;操作系统的支持&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E8%B0%83%E5%BA%A6scheduling"&gt;调度(Scheduling)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E7%BA%BF%E7%A8%8Bthread"&gt;线程(Thread)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E7%94%A8%E6%88%B7%E7%BA%BF%E7%A8%8Buser-level-thread"&gt;用户线程(User-level Thread)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8Bthread-model"&gt;线程模型(Thread Model)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2context-switching"&gt;上下文切换(Context switching)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#io%E6%A8%A1%E5%9E%8Bio-model"&gt;I/O模型(I/O Model)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E5%B9%B6%E5%8F%91%E8%BF%98%E6%98%AF%E5%B9%B6%E8%A1%8C"&gt;并发还是并行&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/?continueFlag=9c9ca4836bf40544b491dee6be45203d#%E6%80%BB%E7%BB%93"&gt;总结&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h2&gt;从操作系统运行程序说起&lt;/h2&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;这是一台拥有2个虚拟CPU核心的      &lt;u&gt;Linux&lt;/u&gt;服务器的      &lt;u&gt;系统监控界面&lt;/u&gt;。其中红框①中      &lt;code&gt;PPID&lt;/code&gt;代表父进程ID，      &lt;code&gt;PID&lt;/code&gt;代表进程或线程ID。红框②中      &lt;code&gt;CPU&lt;/code&gt;代表当前线程运行的CPU核心编号。红框③中是程序的运行命令，其中绿色代表的是      &lt;u&gt;线程，白色为进程&lt;/u&gt;。&lt;/p&gt;    &lt;p&gt;以PID为1375的进程为例，它的父进程为1086，可以通过PPID不断追溯至PID为1的      &lt;u&gt;        &lt;code&gt;init&lt;/code&gt;&lt;/u&gt;进程。从这可以看出Linux通过      &lt;u&gt;        &lt;code&gt;fork()&lt;/code&gt;&lt;/u&gt;的系统调用不断的复制出大量的需要被执行的程序进程。&lt;/p&gt;    &lt;p&gt;进程是操作系统进行      &lt;u&gt;资源分配&lt;/u&gt;的一个独立单位，而实际在CPU运行调度的是线程。以进程1375为例，它又创建了7个线程，如下：&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;$ ls /proc/1375/task/
1375  1429  1430  1431  1432  1433  1488
$ ls /proc/1375/task/1429
arch_status  cgroup      cmdline             cpuset   exe     gid_map  loginuid  mountinfo  ns         oom_score      patch_state  root       sessionid  smaps_rollup  statm    uid_map
attr         children    comm                cwd      fd      io       maps      mounts     numa_maps  oom_score_adj  personality  sched      setgroups  stack         status   wchan
auxv         clear_refs  cpu_resctrl_groups  environ  fdinfo  limits   mem       net        oom_adj    pagemap        projid_map   schedstat  smaps      stat          syscall&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;其中线程1430与1432是调度运行在2号CPU核心上的，如果持续观察这个监控界面，会发现同一个线程会不定时在两个CPU核心之间来回切换，这实际正是操作系统对这些线程在多核CPU上进行抢占式调度。&lt;/p&gt;    &lt;p&gt;操作系统之所以能用有限的CPU核心去运行非常多的程序，并且用户感觉这些程序是在同时运行。一方面操作系统（内核）可以通过一些方法实现并发处理任务（程序），另外一方面得益于多个CPU核心，操作系统还可以并行处理任务。&lt;/p&gt;    &lt;p&gt;本文并不是研究操作系统是怎么实现并发的，但在搞清楚编程语言是怎么实现并发处理之前，很有必要提前对操作系统支持并发提供的一些重要特性做一个全面的介绍。操作系统为了支持多任务处理，提供了进程管理与调度，同时在I/O上提供了多种访问文件或网络的系统调用方式。&lt;/p&gt;    &lt;h2&gt;操作系统的支持&lt;/h2&gt;    &lt;pre&gt;      &lt;code&gt;# 操作系统
## 进程(Process)
### 调度方式
- 抢占式(Preemptive)
- 协作式(Cooperative)
### 执行方式
- 用户线程(User-level Thread)
  - Coroutine
    - Verticle
  - Goroutine
  - Erlang process
  - Green Thread(Java)
- 内核线程(Kernel-level Thread)
- 纤程(Fiber)
## I/O
- 同步(Synchronous)
  - 阻塞式(Blocking)
  - 非阻塞式(Non-blocking)
  - 多路复用(Multiplexing)
  - 信号驱动(Signal Driven)
- 异步(Asynchronous)&lt;/code&gt;&lt;/pre&gt;    &lt;h3&gt;调度(Scheduling)&lt;/h3&gt;    &lt;p&gt;进程调度主要有抢占式调度和协作式调度两种：抢占式(Preemptive)与协作式(Cooperative)。&lt;/p&gt;    &lt;a href="https://www.slanglabs.in/blog/python-microservices-01-tornado-asyncio-lint-test-coverage-project-setup"&gt;      &lt;img alt="&amp;#25250;&amp;#21344;&amp;#24335;&amp;#19982;&amp;#21327;&amp;#20316;&amp;#24335;&amp;#20219;&amp;#21153;&amp;#35843;&amp;#24230;(Preemptive Multitasking vs. Cooperative Multitasking)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;抢占式与协作式任务调度(Preemptive Multitasking vs. Cooperative Multitasking)&lt;/p&gt;    &lt;p&gt;抢占式调度往往在一些重要位置（Sleep Call，Timer Tick）放置了中断信号，通过这个信号通知操作系统调度器(Scheduler)进行进程切换。在抢占式模型中，正在运行的进程可能会被强行挂起，这是由于这些中断信号引发的。&lt;/p&gt;    &lt;p&gt;协作式调度也叫非抢占式调度，是指当前运行的进程通过自身代码逻辑出让CPU控制权。与抢占式调度的区别在于进程运行不会被中断信号打断，除非其主动出让控制权给其他进程。&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;以上描述并没有明确区分进程还是线程，实际在        &lt;u&gt;Linux系统的内核态上&lt;/u&gt;，用户态的线程统一按        &lt;u&gt;轻量级进程(Light-weight process)&lt;/u&gt;来处理，它们与真正的进程的区别是在一个用户态进程中的线程共享了相同的地址空间和其他资源（如打开的文件描述符），但在内核调度上并没有什么区别。&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;线程是进程的运行实例，哪怕在非多线程的进程中，在内核态实际运行进程的还是内核线程，所以接下来介绍下线程的分类。&lt;/p&gt;    &lt;h3&gt;线程(Thread)&lt;/h3&gt;    &lt;p&gt;我们在编程语言中见到的线程，一般指的是与内核线程一一映射的用户态线程，比如Java中的Thread其实就是内核线程。但一些编程语言如Erlang与Go都实现了更轻量级的用户线程。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;用户线程(User-level Thread)        &lt;ul&gt;          &lt;li&gt;协程(Coroutines - Cooperative User-Level Threads)：应用程序通过线程库自行实现的            &lt;strong&gt;协作式调度&lt;/strong&gt;的用户线程，代表性的有Go语言的Goroutine（1.14之前的版本），Vert.x框架中的Verticle。&lt;/li&gt;          &lt;li&gt;Go Goroutine：Go语言的Goroutine在1.14之前是协程的机制，之后的版本采用了            &lt;a href="https://go.dev/doc/go1.14"&gt;异步抢占式调度(asynchronously preemptible)&lt;/a&gt;。&lt;/li&gt;          &lt;li&gt;Erlang process：Erlang VM(BEAM)管理的用户线程，与协程相比的优势在于它可以做到            &lt;strong&gt;公平调度&lt;/strong&gt;，不会出现协程中某个用户线程占用过多CPU周期。&lt;/li&gt;          &lt;li&gt;绿色线程(Green Thread)：类似于协程，是由            &lt;u&gt;Java JDK实现&lt;/u&gt;的，但因为            &lt;u&gt;早期Linux系统没实现内核态的抢占式调度&lt;/u&gt;，Green Thread只能在Solaris系统上发挥它的威力，最终在JDK 1.3之后被Native Thread取代。所以现行的JDK的线程实际是非常重量级的内核态线程，Java的            &lt;a href="https://github.com/openjdk/loom"&gt;Project Loom&lt;/a&gt;会尝试实现新的Green Thread方案，并且是抢占式的调度方案。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;内核线程(Kernel-level Thread)：操作系统内核管理的        &lt;strong&gt;抢占式调度&lt;/strong&gt;的线程，是最终运行在CPU上实际执行任务的最小单元。&lt;/li&gt;      &lt;li&gt;纤程(Fibers)：操作系统内核管理的        &lt;strong&gt;协作式调度&lt;/strong&gt;的线程。这一系统级别的方案最终因硬件和软件的发展        &lt;u&gt;逐渐式微&lt;/u&gt;，现在的用户线程也能达到协作式调度的效果。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;用户线程与内核线程的区别在于用户线程的调度是发生在用户态，内核中无法感知到用户线程的存在，并且调度也发生在用户态，一般是由线程库或编程语言运行时自行实现的。而内核线程的调度是由内核完成的，一般是抢占式调度。&lt;/p&gt;    &lt;h3&gt;用户线程(User-level Thread)&lt;/h3&gt;    &lt;p&gt;用户线程大多是采用协作调度的方式实现，本质上是      &lt;strong&gt;同步执行在与CPU核心数量相同的内核线程上的&lt;/strong&gt;，不仅能极大的降低了上下文开销，还能最佳的利用多核CPU的计算能力。&lt;/p&gt;    &lt;p&gt;用户线程的轻量除了体现在调度的上下文切换开销上，还体现了在对内存的需求上。如果要在4核心4GB内存的笔记本电脑中测试同时生成100万个线程的话，不同编程语言对内存的需求：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;Elixir: 0.48 GB (Process)&lt;/li&gt;      &lt;li&gt;Golang: 1.91 GB (Goroutine)&lt;/li&gt;      &lt;li&gt;Java: 977 GB (Thread)&lt;/li&gt;      &lt;li&gt;PHP: 6836 GB (Laravel Request)&lt;/li&gt;&lt;/ul&gt;    &lt;blockquote&gt;      &lt;p&gt;数据来源：Programming Elixir        &lt;sup&gt;Chapter 15&lt;/sup&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;但用户线程的执行最终是由内核线程来完成，所以存在一个从用户线程到内核线程的映射模型。&lt;/p&gt;    &lt;h3&gt;线程模型(Thread Model)&lt;/h3&gt;    &lt;p&gt;可能有人会疑惑，用户线程与内核线程是一一映射的吗？总的来说有以下三种线程模型：&lt;/p&gt;    &lt;a href="https://medium.com/swlh/different-threading-models-why-i-feel-goroutine-is-better-though-with-some-limitations-b73863ba4dae"&gt;      &lt;img alt="&amp;#19981;&amp;#21516;&amp;#30340;&amp;#32447;&amp;#31243;&amp;#27169;&amp;#22411;(Different Threading Models)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;不同的线程模型(Different Threading Models)&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;1x1 (kernel-level threading)：用户线程与内核线程是一一映射的。这是最简单的模型。在这种语境下，用户线程中的线程就是我们常规意义上说的线程，当程序创建一个线程时，也会在内核中创建一个内核线程。目前大多数操作系统如Linux、Solaris、FreeBSD、macOS与iOS内核的线程模型就是这种。&lt;/li&gt;      &lt;li&gt;Nx1 (user-level threading)：多个用户线程与一个内核线程映射。在这种模型中，内核线程只有一个，在应用内部不存在内核线程切换的开销，程序的并发能力是很高的。但一旦某个用户线程被阻塞（发生网络或文件I/O系统调用），其他用户线程也会被阻塞。另外应用也无法从多核CPU上获得更好的并发性。所以实际的使用场景中，这种模型并不常见。&lt;/li&gt;      &lt;li&gt;MxN (hybrid threading)：多个用户线程与多个内核线程映射。这种模型最为复杂，但也是最强大的线程模型，不仅有着很好的并发能力，同时还能获得多核CPU的处理能力。早期的Solaris内核中也支持这种线程模型，但因为其会导致内核调度过于复杂，逐渐被放弃。但在Erlang VM和Go语言的运行时就又实现了这种线程模型。&lt;/li&gt;&lt;/ul&gt;    &lt;a href="https://docs.oracle.com/cd/E19620-01/805-3024/6j2sumi1a/index.html"&gt;      &lt;img alt="&amp;#26089;&amp;#26399;Solaris&amp;#20869;&amp;#26680;&amp;#30340;&amp;#32447;&amp;#31243;&amp;#21644;&amp;#36731;&amp;#37327;&amp;#32423;&amp;#36827;&amp;#31243;(Threads and Lightweight Processes)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;早期Solaris内核的线程和轻量级进程(Threads and Lightweight Processes)&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;轻量级进程(Light-weight process)：Solaris内核中的概念，但也会在其他系统内核中看到类似的概念。指的是用户线程和内核线程之间的接口，也可被认为是一个调度用户线程执行的虚拟CPU。当用户线程发出系统调用时，运行该线程的LWP调用内核并保持绑定到该内核线程至少直到系统调用完成。当LWP在内核中为用户线程执行系统调用时，它会运行一个内核线程。因此，每个LWP都与一个内核线程相关联。只有在用户线程完全由轻量级进程构成时，才可以说轻量级进程就是线程。Go语言中Goroutine的G-P-M调度模型中，P承担了类似LWP的角色。&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;应用程序为什么会费劲的设计出用户线程？操作系统提供的内核线程不香吗？这是因为操作系统内核的线程切换是重量级的操作，它需要进行上下文切换，而这会很耗时。&lt;/p&gt;    &lt;h3&gt;上下文切换(Context switching)&lt;/h3&gt;    &lt;p&gt;在进程间切换需要消耗一定的CPU时钟周期进行相关的状态管理工作，包括寄存器和内存映射的保存与读取、更新各种内部的表等。比如在Linux内核中，上下文切换需要涉及寄存器、栈指针、程序计数器的切换。&lt;/p&gt;    &lt;p&gt;在这篇      &lt;a href="https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html"&gt;How long does it take to make a context switch?&lt;/a&gt;中可以看到一个结论是：&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;Context switching is expensive. My rule of thumb is that it’ll cost you about 30µs of CPU overhead…Applications that create too many threads that are constantly fighting for CPU time (such as Apache’s HTTPd or many Java applications) can waste considerable amounts of CPU cycles just to switch back and forth between different threads…I think the sweet spot for optimal CPU use is to have the same number of worker threads as there are hardware threads, and write code in an asynchronous / non-blocking fashion.&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;因为线程调度的上下文切换成本非常昂贵，所以最佳的做法是应用程序使用和CPU核心相同的线程数，这样每个线程都能充分利用CPU核心的时间片，避免了应用内部多个线程的上下文切换开销。&lt;/p&gt;    &lt;p&gt;用户线程的轻量级让应用程序能够极大的提高并发性，但也会有一些问题，比如某个用户线程中发起一个网络请求导致底层的内核线程被阻塞，因为多个用户线程在共用这个内核线程，最终导致大量的用户线程被阻塞。&lt;/p&gt;    &lt;p&gt;解决这个问题需要了解操作系统的I/O模型。&lt;/p&gt;    &lt;h3&gt;I/O模型(I/O Model)&lt;/h3&gt;    &lt;p&gt;当应用程序需要访问文件或者网络资源时，应用内的线程将会花费大量的时间来等待数据的到来。如下图所示：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;如果把CPU处理计算机指令的速度类比成高铁的速度，那线程一次文件或网络的访问将会和蜗牛一样慢，这相当于你在高铁上让蜗牛去帮你取快递，对CPU来说是巨大的浪费。现代操作系统已经提供了多种I/O模型来解决这个问题。常见的I/O模型有：&lt;/p&gt;    &lt;a href="https://www.4e00.com/blog/linux/2017/09/29/unix-network-programming-charpter-6-io-multiplexing.html"&gt;      &lt;img alt="&amp;#20116;&amp;#31181;I/O&amp;#27169;&amp;#22411;&amp;#23545;&amp;#27604;(Comparison of the five I/O models)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;五种I/O模型对比(Comparison of the five I/O models)&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;同步(Synchronous)        &lt;ul&gt;          &lt;li&gt;阻塞式(Blocking)：同步阻塞式是最常见的I/O模型。线程在访问文件或网络资源时，会因发起了内核的系统调用被挂起，内核会检查文件描述符是否可读，当文件描述符中存在数据时，内核会将数据复制给线程并交回控制权，线程从挂起状态切换成可运行状态等到内核调度运行。&lt;/li&gt;          &lt;li&gt;非阻塞式(Non-blocking)：线程在访问文件或网络资源时，因文件描述符是非阻塞的，线程在检查数据是否可读的阶段是非阻塞的。此模型需要线程不停的通过轮询(polling)的方式检查文件描述符是否可读。但之所以属于同步I/O，是因为在最终读取数据(            &lt;code&gt;recvfrom&lt;/code&gt;)时需要从内核态中拷贝数据(            &lt;code&gt;recvfrom&lt;/code&gt;)到用户态中，这个阶段线程依旧被阻塞住无法处理其他指令。&lt;/li&gt;          &lt;li&gt;多路复用(Multiplexing)：和非阻塞式的区别在于，多路复用模型的线程可以同时访问多个文件描述符，这很适合构建高并发的Web服务器或中间件。但此模型会在检查文件描述符时会被阻塞(            &lt;u&gt;              &lt;code&gt;select&lt;/code&gt;&lt;/u&gt;)，并且在读取数据(            &lt;code&gt;recvfrom&lt;/code&gt;)时也会被阻塞。和多路复用模型相似的是使用多线程和阻塞I/O，但当线程产生很多时会消耗大量的内存资源以及线程调度产生的上下文切换开销，所以多路复用模型一般只使用单线程模型。&lt;/li&gt;          &lt;li&gt;信号驱动(Signal Driven)：和上面的模型区别在于，之前的模型都需要线程主动轮询，信号驱动模型需要监听内核的            &lt;code&gt;SIGIO&lt;/code&gt;事件，通过注册事件处理函数，之后线程可以继续执行其他任务。当数据可读时，线程处理函数会以阻塞的方式从内核态复制数据到用户态。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;异步(Asynchronous)：此模型和同步模型最大的区别在于，不仅在获取文件操作符时不会被阻塞，数据从内核态复制到用户态也不会被阻塞，因为内核会去做这个复制数据的工作，线程只需要在回调函数中使用数据即可。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;关于I/O模型的更多细节，请参考：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://rickhw.github.io/2019/02/27/ComputerScience/IO-Models/"&gt;Study Notes - I/O Models&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html"&gt;I/O Models&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;blockquote&gt;      &lt;p&gt;思考一个问题：如果用户线程不可避免的需要被挂起进（如访问共享资源但没有获得锁）而导致内核线程被挂起，如何调度可以让此内核线程上的用户线程可以继续运行？&lt;/p&gt;&lt;/blockquote&gt;    &lt;h2&gt;并发还是并行&lt;/h2&gt;    &lt;p&gt;前面讨论了很多和I/O相关的概念，并不是所有的计算机任务都是和I/O相关(I/O bound)的，比如很多算法都需要CPU做大量的计算，这时候CPU核心根本不会因为等待外部资源而空转，如果在这种计算密集型(CPU bound)任务中使用多线程技术，那么就会产生大量的线程上下文切换开销，最终会导致处理任务的性能变慢。&lt;/p&gt;    &lt;p&gt;在这篇      &lt;a href="https://www.ardanlabs.com/blog/2018/12/scheduling-in-go-part3.html"&gt;Scheduling In Go : Part III - Concurrency&lt;/a&gt;文章中，作者对比了两种类型工作在并发和并行模式的对比，并提供了一些经验总结：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;CPU密集型(CPU bound)：如果任务是CPU密集型的，那么使用并行的方式来解决问题，这样可以最大的利用CPU核心的算力。如果使用并发的方式去处理，反而会增加        &lt;u&gt;复杂度&lt;/u&gt;。&lt;/li&gt;      &lt;li&gt;I/O密集型(I/O bound)：如果任务时I/O密集型的，那么使用并发的方式来解决问题，这样可以提升单个CPU核心的吞吐量(Throughput)。&lt;/li&gt;&lt;/ul&gt;    &lt;blockquote&gt;      &lt;p&gt;并发(Concurrent)与并行(Parallel)：并发是指同时处理(dealing with)很多事情，并行是指同时做(doing)很多事情。并行是并发的特殊情况，只能在计算机多核环境中实现。&lt;/p&gt;      &lt;p&gt;同步(Sync)与异步(Async)：一种编程模型。区别在于同步是可预测(predictable)的，异步是不可预测(unpredictable)的编程模型。&lt;/p&gt;&lt;/blockquote&gt;    &lt;h2&gt;总结&lt;/h2&gt;    &lt;p&gt;本篇从操作系统的视角介绍编程语言实现并发的底层概念，包括进程调度与I/O模型等。下篇开始介绍常见的      &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/"&gt;并发模型&lt;/a&gt;。&lt;/p&gt;    &lt;div&gt;      &lt;p&gt;        &lt;strong&gt;更新日志&lt;/strong&gt;2022-04-15：根据此        &lt;a href="https://www.v2ex.com/t/846178#reply20"&gt;讨论帖&lt;/a&gt;修改更新。（感谢        &lt;a href="https://www.v2ex.com/member/lxdlam"&gt;@lxdlam&lt;/a&gt;）
2022-04-10：初稿发布。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62933-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%B9%B6%E5%8F%91-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F</guid>
      <pubDate>Sat, 07 Sep 2024 07:31:49 CST</pubDate>
    </item>
    <item>
      <title>我问了chatgpt几个如何使用nodejs编程的问题，看看如何？</title>
      <link>https://itindex.net/detail/62717-chatgpt-nodejs-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;div&gt;  &lt;div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;p&gt;如何使用nodejs开发一个API服务，请给出代码&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;div&gt;重试&lt;/div&gt;     &lt;div&gt;复制&lt;/div&gt;&lt;/div&gt;    &lt;div&gt;     &lt;p&gt;可以使用Node.js和Express框架来快速编写和部署API服务。&lt;/p&gt;     &lt;p&gt;下面是一个简单的例子代码，该代码基于Express框架，实现了一个简单的API服务：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;const express = require(&amp;apos;express&amp;apos;);
const app = express();

// 设置路由：获取用户列表
app.get(&amp;apos;/users&amp;apos;, (req, res) =&amp;gt; {
  const users = [
    { id: 1, name: &amp;apos;张三&amp;apos; },
    { id: 2, name: &amp;apos;李四&amp;apos; },
    { id: 3, name: &amp;apos;王五&amp;apos; }
  ];

  res.json(users);
});

// 设置路由：获取单个用户信息
app.get(&amp;apos;/users/:id&amp;apos;, (req, res) =&amp;gt; {
  const user = { id: req.params.id, name: &amp;apos;xxx&amp;apos; };

  res.json(user);
});

// 启动服务器
app.listen(3000, () =&amp;gt; {
  console.log(&amp;apos;Server started on port 3000&amp;apos;);
});
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在这个例子中，我们首先要安装Node.js和Express：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;npm install express --save
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;然后，我们在代码中导入并创建一个Express应用对象。在这个应用中，我们定义了两个API路由：&lt;/p&gt;     &lt;ul&gt;      &lt;li&gt;       &lt;code&gt;/users&lt;/code&gt;：用于获取用户列表；&lt;/li&gt;      &lt;li&gt;       &lt;code&gt;/users/:id&lt;/code&gt;：用于获取单个用户信息，其中       &lt;code&gt;:id&lt;/code&gt;表示动态的路径参数。&lt;/li&gt;&lt;/ul&gt;     &lt;p&gt;在这里，我们使用      &lt;code&gt;req.params&lt;/code&gt;对象来获取动态路径参数的值。同时，我们也可以定义其他类型的请求（比如      &lt;code&gt;POST&lt;/code&gt;、      &lt;code&gt;PUT&lt;/code&gt;、      &lt;code&gt;DELETE&lt;/code&gt;等），以便实现更复杂的API操作。&lt;/p&gt;     &lt;p&gt;最后，我们启动了一个基于Express的Web服务器，监听在3000端口上。&lt;/p&gt;     &lt;p&gt;完成以上步骤后，我们可以使用Postman或其他HTTP客户端工具来测试我们的API服务了。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;2023/3/30 22:42:51&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;img alt="smiley" src="https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f603.png"&gt;&lt;/img&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;p&gt;在nodejs里如何操作数据库，例如mysql&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;div&gt;重试&lt;/div&gt;     &lt;div&gt;复制&lt;/div&gt;&lt;/div&gt;    &lt;div&gt;     &lt;p&gt;在Node.js应用中连接和操作MySQL数据库，需要使用第三方模块。常见的MySQL模块包括mysql和mysql2，这两个模块都可以用来连接和操作MySQL数据库。&lt;/p&gt;     &lt;p&gt;下面是一个使用mysql模块的例子，演示如何连接MySQL数据库并进行查询操作：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;const mysql = require(&amp;apos;mysql&amp;apos;);

// 建立连接
const db = mysql.createConnection({
  host: &amp;apos;localhost&amp;apos;,
  user: &amp;apos;root&amp;apos;,
  password: &amp;apos;mypassword&amp;apos;,
  database: &amp;apos;mydb&amp;apos;
});

// 连接数据库
db.connect((err) =&amp;gt; {
  if (err) throw err;
  console.log(&amp;apos;MySQL Connected...&amp;apos;);
});

// 查询数据
db.query(&amp;apos;SELECT * FROM users&amp;apos;, (err, result) =&amp;gt; {
  if (err) throw err;
  console.log(result);
});

// 关闭连接
db.end((err) =&amp;gt; {
  if (err) throw err;
  console.log(&amp;apos;MySQL Closed&amp;apos;);
});
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在这个例子中，我们首先使用mysql模块的      &lt;code&gt;createConnection&lt;/code&gt;方法来创建MySQL连接，并指定连接参数（例如host、user、password和database）。&lt;/p&gt;     &lt;p&gt;然后，我们使用      &lt;code&gt;connect&lt;/code&gt;方法来连接到MySQL数据库。如果连接成功，我们将在控制台输出“MySQL Connected...”字符串。&lt;/p&gt;     &lt;p&gt;接下来，我们使用      &lt;code&gt;query&lt;/code&gt;方法来运行查询，以获取MySQL数据库中的数据。在这里，我们运行了一个简单的“SELECT * FROM users”查询，以获取      &lt;code&gt;users&lt;/code&gt;表中的所有数据，查询成功后，我们就可以在控制台输出该查询的结果了。&lt;/p&gt;     &lt;p&gt;最后，我们调用      &lt;code&gt;end&lt;/code&gt;方法来关闭连接。如果在连接过程中出现任何错误，我们将在控制台输出错误信息。如果一切都顺利，我们将在控制台输出“MySQL Closed”字符串。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;2023/3/30 22:44:17&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;img alt="smiley" src="https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f603.png"&gt;&lt;/img&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;p&gt;上面你说的express是什么？&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;div&gt;重试&lt;/div&gt;     &lt;div&gt;复制&lt;/div&gt;&lt;/div&gt;    &lt;div&gt;     &lt;p&gt;Express是一个基于Node.js平台的Web应用开发框架。它为Web应用开发提供了许多工具和一致的解决方案，使得开发Web应用变得更加容易、简单和快速。&lt;/p&gt;     &lt;p&gt;使用Express，可以轻松地设置路由、中间件以及处理HTTP请求和响应等。Express还具有灵活性和可扩展性，允许开发人员轻松扩展和自定义应用。&lt;/p&gt;     &lt;p&gt;Express具有以下主要特点：&lt;/p&gt;     &lt;ul&gt;      &lt;li&gt;快速、灵活和简单：Express提供了许多功能强大和易于使用的功能和API，使Web应用开发变得简单、快速和灵活。&lt;/li&gt;      &lt;li&gt;轻量级：Express是一个轻量级的Web应用框架，非常适合快速开发小型或中小型的Web应用。&lt;/li&gt;      &lt;li&gt;中间件支持：Express的中间件架构使HTTP请求的处理变得非常简单和灵活，可以基于条件处理请求、响应等。&lt;/li&gt;      &lt;li&gt;可扩展性：Express的模块化架构允许开发人员在应用程序中使用自定义模块，以进行扩展和自定义。&lt;/li&gt;&lt;/ul&gt;     &lt;p&gt;使用Express，可以方便地创建RESTful API、实现路由、处理HTTP请求和响应、设置Cookie和Session等。在Node.js应用开发中，Express是非常常用的一个框架。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;2023/3/30 22:45:10&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;img alt="smiley" src="https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f603.png"&gt;&lt;/img&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;p&gt;在express里如何操作数据库&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;div&gt;重试&lt;/div&gt;     &lt;div&gt;复制&lt;/div&gt;&lt;/div&gt;    &lt;div&gt;     &lt;p&gt;在Express中操作数据库需要使用数据库操作的中间件。常用的中间件包括：&lt;/p&gt;     &lt;ul&gt;      &lt;li&gt;Sequelize：一款支持多种数据库（MySQL、PostgreSQL、SQLite、MSSQL）的ORM（对象关系映射）框架。&lt;/li&gt;      &lt;li&gt;Mongoose：面向对象的ODM（对象文档映射）框架，支持MongoDB数据库。&lt;/li&gt;      &lt;li&gt;mysql2、pg、mongodb等Node.js模块，直接连接对应的数据库进行操作。&lt;/li&gt;&lt;/ul&gt;     &lt;p&gt;以Sequelize为例，下面给出一个使用Sequelize连接MySQL数据库的示例：&lt;/p&gt;     &lt;p&gt;安装Sequelize和mysql2模块：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;npm install sequelize mysql2
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在Node.js中连接和操作MySQL数据库的示例代码如下：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;const Sequelize = require(&amp;apos;sequelize&amp;apos;);

// 连接MySQL数据库
const sequelize = new Sequelize(&amp;apos;database_name&amp;apos;, &amp;apos;username&amp;apos;, &amp;apos;password&amp;apos;, {
  host: &amp;apos;localhost&amp;apos;,
  dialect: &amp;apos;mysql&amp;apos;
});

// 定义模型
const User = sequelize.define(&amp;apos;user&amp;apos;, {
  id: {
    type: Sequelize.INTEGER,
    primaryKey: true
  },
  username: {
    type: Sequelize.STRING
  },
  password: {
    type: Sequelize.STRING
  },
  email: {
    type: Sequelize.STRING
  }
});

// 执行查询操作
User.findAll().then(users =&amp;gt; {
  console.log(users);
});

// 关闭数据库连接
sequelize.close();
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在这个示例中，我们首先使用Sequelize的      &lt;code&gt;define&lt;/code&gt;方法定义了一个名为“user”的模型，并指定了该模型的字段和数据类型。然后通过      &lt;code&gt;findAll&lt;/code&gt;方法查询了数据库中的所有用户数据，并输出到控制台中。&lt;/p&gt;     &lt;p&gt;此外，我们还创建了一个Sequelize实例，使用      &lt;code&gt;Sequelize&lt;/code&gt;构造函数来连接到MySQL数据库，并在最后使用      &lt;code&gt;close&lt;/code&gt;方法关闭该连接。&lt;/p&gt;     &lt;p&gt;总之，使用Sequelize等中间件能够极大地简化Node.js应用中数据库的连接和操作，提高了开发的效率和代码的可维护性。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;2023/3/30 22:46:07&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;img alt="smiley" src="https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f603.png"&gt;&lt;/img&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;p&gt;有连接池模块吗？可以自动关闭数据库连接吗？如何在异常出现时自动关闭或回收连接&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;div&gt;重试&lt;/div&gt;     &lt;div&gt;复制&lt;/div&gt;&lt;/div&gt;    &lt;div&gt;     &lt;p&gt;是的，Sequelize具备连接池的功能，可以通过配置连接池的方式来管理数据库连接的数量和使用情况。&lt;/p&gt;     &lt;p&gt;下面以连接池模块      &lt;code&gt;generic-pool&lt;/code&gt;为例，给出一个使用Sequelize连接池连接MySQL数据库的示例：&lt;/p&gt;     &lt;p&gt;安装      &lt;code&gt;generic-pool&lt;/code&gt;和      &lt;code&gt;mysql2&lt;/code&gt;模块：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;npm install generic-pool mysql2
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在Node.js中连接和操作MySQL数据库的示例代码如下：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;const Sequelize = require(&amp;apos;sequelize&amp;apos;);
const { createPool } = require(&amp;apos;generic-pool&amp;apos;);
const mysql = require(&amp;apos;mysql2&amp;apos;);

// 创建连接池
const pool = createPool({
  create: function() {
    return mysql.createConnection({
      host: &amp;apos;localhost&amp;apos;,
      user: &amp;apos;username&amp;apos;,
      password: &amp;apos;password&amp;apos;,
      database: &amp;apos;database_name&amp;apos;
    });
  },
  destroy: function(connection) {
    connection.end();
  }
}, {
  min: 2,
  max: 10
});

// 获取连接
pool.acquire().then(connection =&amp;gt; {
  // 定义模型
  const User = connection.define(&amp;apos;user&amp;apos;, {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true
    },
    username: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    }
  });

  // 执行查询操作
  User.findAll().then(users =&amp;gt; {
    console.log(users);
  });

  // 释放连接
  pool.release(connection);
});


// 关闭连接池
pool.drain().then(() =&amp;gt; {
  pool.clear();
});

&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在这个示例中，我们使用了      &lt;code&gt;generic-pool&lt;/code&gt;模块来创建连接池，在连接池中创建了2个连接，最大连接数为10个。通过      &lt;code&gt;acquire&lt;/code&gt;方法获取连接，使用连接执行查询操作后，调用      &lt;code&gt;release&lt;/code&gt;方法释放连接。最后通过      &lt;code&gt;drain&lt;/code&gt;方法和      &lt;code&gt;clear&lt;/code&gt;方法来关闭连接池。&lt;/p&gt;     &lt;p&gt;在使用Sequelize连接池时，为了避免长时间占用连接，需要在执行完操作后及时释放连接资源。可以使用Promise的      &lt;code&gt;finally&lt;/code&gt;方法、try...catch语句中的finally块等来自动关闭连接：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;// 获取连接
let conn;
try {
  conn = await pool.acquire();
  // 使用连接执行查询操作，例如：
  const result = await conn.query(&amp;apos;select * from users&amp;apos;);
  // 处理查询结果
} catch (err) {
  // 处理异常
} finally {
  if (conn) {
    // 释放连接
    await pool.release(conn);
  }
}
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;在这个示例中，使用      &lt;code&gt;try...catch&lt;/code&gt;语句捕获异常，并在      &lt;code&gt;finally&lt;/code&gt;块中释放连接资源。如果有多个查询操作，也可以在每次查询操作后释放连接资源：&lt;/p&gt;     &lt;pre&gt;      &lt;code&gt;let conn;
try {
  conn = await pool.acquire();
  const result1 = await conn.query(&amp;apos;select * from users&amp;apos;);
  // 处理查询结果1
  const result2 = await conn.query(&amp;apos;select * from products&amp;apos;);
  // 处理查询结果2
} catch (err) {
  // 处理异常
} finally {
  if (conn) {
    // 释放连接
    await pool.release(conn);
  }
}
&lt;/code&gt;&lt;/pre&gt;     &lt;p&gt;总之，使用连接池可以更好地管理数据库连接资源，提高系统的可伸缩性和稳定性，同时在编写代码时要及时释放连接资源，避免出现过度占用连接而导致连接池内没有可用连接的问题。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;2023/3/30 22:51:59&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;img alt="smiley" src="https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f603.png"&gt;&lt;/img&gt;&lt;/div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;p&gt;使用nodejs开发最好的开发工具是什么&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div&gt;  &lt;div&gt;   &lt;div&gt;&lt;/div&gt;   &lt;div&gt;    &lt;div&gt;     &lt;div&gt;重试&lt;/div&gt;     &lt;div&gt;复制&lt;/div&gt;&lt;/div&gt;    &lt;div&gt;     &lt;p&gt;开发工具这个问题真的很主观，每个开发者都有自己喜欢的工具和习惯，不存在唯一的最好工具。不过，我可以列出一些在Node.js开发中比较流行和便利的工具供你参考。&lt;/p&gt;     &lt;ol&gt;      &lt;li&gt;Visual Studio Code：&lt;/li&gt;&lt;/ol&gt;     &lt;p&gt;Visual Studio Code是一款轻量级的、免费、开源的代码编辑器，它支持多种编程语言包括JavaScript和Node.js。Visual Studio Code提供了很多有用的扩展，可以帮助开发者在Node.js项目中更加高效地工作，例如JavaScript调试器、代码段提示、类型定义文件支持、Git集成等等。&lt;/p&gt;     &lt;ol start="2"&gt;      &lt;li&gt;WebStorm：&lt;/li&gt;&lt;/ol&gt;     &lt;p&gt;WebStorm是一款专业的JavaScript IDE，它支持多种编程语言包括JavaScript和Node.js，并内置了Node.js的调试器。WebStorm提供了很多有用的功能，例如代码智能提示、自动重构、版本控制、开发者工具等等。&lt;/p&gt;     &lt;ol start="3"&gt;      &lt;li&gt;Sublime Text：&lt;/li&gt;&lt;/ol&gt;     &lt;p&gt;Sublime Text是一款轻量级的、高性能的代码编辑器，它支持多种编程语言包括JavaScript和Node.js。Sublime Text具有快速导航、快速模式、代码片段、语法高亮、多行编辑和大量插件等有用功能。&lt;/p&gt;     &lt;ol start="4"&gt;      &lt;li&gt;Atom：&lt;/li&gt;&lt;/ol&gt;     &lt;p&gt;Atom是一款免费和开源的文本编辑器，它支持多种编程语言包括JavaScript和Node.js。Atom具有扩展性强、颜色方案自定义、Git集成等功能，同时内置有一些有用的插件。&lt;/p&gt;     &lt;ol start="5"&gt;      &lt;li&gt;Brackets：&lt;/li&gt;&lt;/ol&gt;     &lt;p&gt;Brackets是一款免费的代码编辑器，它由Adobe公司开发，支持多种编程语言包括JavaScript和Node.js。Brackets具有代码补全、语法高亮、实时预览、括号匹配和扩展性等功能，同时内置有一些有用的插件。&lt;/p&gt;     &lt;p&gt;总之，在选择开发工具时可以根据自己的习惯和需要进行选择，可以尝试不同的工具来找到自己喜欢的工具。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
     
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62717-chatgpt-nodejs-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Sat, 01 Apr 2023 20:18:29 CST</pubDate>
    </item>
    <item>
      <title>面向GPT-4编程的时代来了：Github Copilot大升级，首次集成聊天功能</title>
      <link>https://itindex.net/detail/62700-gpt-%E7%BC%96%E7%A8%8B-%E6%97%B6%E4%BB%A3</link>
      <description>&lt;blockquote data-author-name="" data-content-utf8-length="36" data-source-title="" data-type="2" data-url=""&gt;&lt;section&gt;&lt;section&gt;&lt;p&gt;从今天开始，面向 GPT-4 编程。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;/blockquote&gt;&lt;p&gt;面向 GPT-4 编程，程序员们终于等到了这一天。&lt;/p&gt;&lt;p&gt;作为 Copilot 代码补全工具的升级，GitHub 刚刚宣布了 Copilot X 计划，正式接入 GPT-4，并放出了一系列全新功能。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.775" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_png/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoFynOL5vZRITyjHvIiaPZwibGq5zMrMOEJkOltsMh30Siajn64eGDLKzLw/640?wx_fmt=png" data-type="png" data-w="1080" data-index="1" src="https://image.jiqizhixin.com/uploads/editor/8a346635-bb17-4438-8221-288e90924fec/640.png" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;Copilot X 搭建了一种以代码为中心的聊天模式。升级后的 Copilot 在整个开发生命周期中均可应用，而不仅仅是代码补全。通过整合聊天和语音功能，开发人员可以更自然地与 Copilot 进行交流。此外，Copilot X 将集成到拉取请求、命令行和文档中，为有关项目的问题提供即时答案。&lt;/p&gt;&lt;p&gt;毫不意外，新功能是由 OpenAI 的 GPT-4 驱动的。但出于延迟的原因，来自 GPT-3 的代码补全工具仍然在 GitHub Codex 模型之中。&lt;/p&gt;&lt;p&gt;GitHub Copilot 推出至今还不到两年，已经对软件开发领域产生了颠覆性的影响。据 GitHub 调查报告显示，这款基于 OpenAI &amp;nbsp;Codex 模型构建的 AI 工具，目前已编写了该平台上 46% 的代码，帮助开发人员将编码速度提高了 55%。通过自动完成 comments 和代码，Copilot 能够作为程序员的 AI 助手，让开发人员保持专注和高效。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.525" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_png/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2Ado7YX8eJERWSk0YzciaPurP5uebgAFAVpCeTRW5e4Ijz1Q8m1Y2pSUrnA/640?wx_fmt=png" data-type="png" data-w="1080" data-index="2" src="https://image.jiqizhixin.com/uploads/editor/5b121ca1-ffd9-438a-81e4-41607b5d5930/640.png" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;Copilot X 的诞生，同样延续了这一愿景。让我们来具体看看最新的功能。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Copilot X 功能展示&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Copilot X 在开发者体验中添加了聊天功能「Copilot Chat」。Copilot Chat 建立在 OpenAI 和微软在 ChatGPT 和新 Bing 上所做的工作之上。它不仅仅是一个聊天窗口，还可以识别开发人员键入的代码、显示错误消息，并将其嵌入到 IDE 中。开发人员可以深入分析和解释代码块的用途，生成单元测试，甚至获得针对错误的建议修复。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.5625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_gif/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoIqcQF3pZ5u4lfSLibeye9U5okcu1xXte1EcZibFqmMnN54KI3bEMoxpA/640?wx_fmt=gif" data-type="gif" data-w="800" data-index="3" src="https://image.jiqizhixin.com/uploads/editor/92244842-b6d8-4808-9c46-178062bcfef5/640.gif" data-order="0" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;Copilot Chat 不仅仅是将 ChatGPT 带入 IDE，同时也将 IDE 的上下文带入了 ChatGPT。GitHub 至少通过一些巧妙的 prompt 工程实现了这一点，在 prompt 中尽可能多地加入了上下文。今天推出预览版的 Copilot Chat 将在 Visual Studio 和 VS Code 中提供，晚些将支持其他 IDE。&lt;/p&gt;&lt;p&gt;当然，你也可以要求 Copilot Chat 为你写代码。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.5625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_gif/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoKHZOcPOWRTk5HKibiaZW9WNj3bEbWnbV7VOgvvZgrSRs5xicFbuqmCsvA/640?wx_fmt=gif" data-type="gif" data-w="800" data-index="4" src="https://image.jiqizhixin.com/uploads/editor/e71aa945-fc05-47f9-b61d-b0d49b8bc764/640.gif" data-order="1" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;「它带回了编程本身的乐趣，因为你不必找到一个教程，也不必弄清楚哪一个是正确的教程，你只需继续下去，」Dohmke 说。「我确信，当你让学生和孩子们使用时，他们会大吃一惊。它变成了一种令人上瘾的工具，因为你有一个可以不断提问的东西。」&lt;/p&gt;&lt;p&gt;Copilot Chat 只专注于编程问题，如果用户提问了与编程无关的事项，它将迅速关闭任何不相关的内容。&lt;/p&gt;&lt;p&gt;GitHub CEO Thomas Dohmke 认为，虽然自动补全功能已经让开发者大大提高了生产力，但这一新升级的 Copilot 功能将使开发者的生产力提高 10 倍。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.5625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_gif/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoTJHPBCicicXMsRZk44wVF4AeZy3YcbTp7Azz7aaYV3iaVUicbCV6p7tDbg/640?wx_fmt=gif" data-type="gif" data-w="800" data-index="5" src="https://image.jiqizhixin.com/uploads/editor/e50711b2-22f7-4ab9-bd2d-3368cc02cb83/640.gif" data-order="2" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;终端是开发人员花费最多时间的地方，即使是最熟练的开发人员也需要滚动浏览许多页面才能记住许多命令的准确语法。作为整体 Copilot X 计划的一部分，GitHub 也将 Copilot 带到了命令行。Copilot CLI 可以组合命令和循环，并通过模糊的查找标志来满足用户的查询。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_png/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoJEM9GmvyLAydzYmU7xnKRKIelk0mrmt16o9W3F3YZpUEQdPhWLkj6g/640?wx_fmt=png" data-type="png" data-w="1080" data-index="6" src="https://image.jiqizhixin.com/uploads/editor/cc2725ab-3162-4853-a2b5-fcd976e06e0a/640.png" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;与 Warp 在终端上所做的类似，GitHub 会帮助开发者编写命令和循环，并找到那些晦涩难懂的特征标志，这些标志往往深藏在手册中（或至少要在 Stack Overflow 上搜索一两次）。&lt;/p&gt;&lt;p&gt;此外，开发人员现在还可以申请参与「Copilot for Pull Requests」的技术预览，该功能可以生成拉取请求的描述并自动标记。这项新功能由 OpenAI 的新 GPT-4 模型提供支持。&lt;/p&gt;&lt;p&gt;Copilot for Pull Requests 减少了样板文件和手动任务，开发人员可以专注于更复杂和更具创新性的工作。毕竟很少有人喜欢模板，这是一项无聊、琐碎的工作，无论是在代码层还是拉取请求层。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.5625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_gif/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoLlpUggUMXic5A4gQibWxjLv049Amoiarq1tax0gITT2eFC9RibXRiaVajNg/640?wx_fmt=gif" data-type="gif" data-w="960" data-index="7" src="https://image.jiqizhixin.com/uploads/editor/b862688a-9dea-4076-a3f0-2c41df390f47/640.gif" data-order="3" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;同时，GitHub 还在准备一项新功能：如果开发人员缺少足够的拉取请求测试，Copilot 会自动发出警告，然后建议根据项目需要编辑、接受或拒绝的潜在测试。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.5625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_gif/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2Ado6FHGfHakJIh8RzNMibZoMrl0KDrWbeOSlg5mE0MV5WhzR2GVrVgGYyA/640?wx_fmt=gif" data-type="gif" data-w="960" data-index="8" src="https://image.jiqizhixin.com/uploads/editor/d50a6426-edc2-4d68-b4c8-61f9d69159df/640.gif" data-order="4" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;「Copilot for doc」的功能也是刚刚推出的。Github 计划将该功能引入任何组织的存储库和内部文档，因此开发人员可以通过类似 ChatGPT 的界面提问并获得即时答复。&lt;/p&gt;&lt;p&gt;&lt;img data-ratio="0.5625" data-s="300,640" data-src="https://mmbiz.qpic.cn/mmbiz_gif/KmXPKA19gW9Ba64DmfTk7hAKkG1ic2AdoQpmLJtFJyGNOzLPUpTmRVGX23lZPibxic5ETjOjgn0P0SKsloSj4TmaA/640?wx_fmt=gif" data-type="gif" data-w="960" data-index="9" src="https://image.jiqizhixin.com/uploads/editor/e4a3d7e4-13d5-49c0-9257-361d58b2df61/640.gif" data-order="5" alt="图片" data-fail="0" class="fr-fic fr-dib" style="width: 700%;"&gt;&lt;/p&gt;&lt;p&gt;不过，ChatGPT 数据库的截止日期是 2021 年，这意味着这些信息并不总是最新的，GitHub 正在用更多的最新数据来充实它，类似于微软的 Bing 用其 Prometheus 模型为搜索所做的那样。&lt;/p&gt;&lt;p&gt;可以想象的是，当你在每个应用程序中都拥有一个 Copilot，无论是 Outlook、Word 还是代码编辑器中，这意味着你都可以利用所有在这些环境中写的东西，也可以利用组织中其他工作人员的信息。&lt;/p&gt;&lt;p&gt;某种意义上来看，它消除了公司中长久以来存在的「孤岛」。从开发人员到营销人员，所有人现在可以更轻松地写出公告博文，因为他们在 Copilot 中可以直接看到开发人员放在 git repo 中的发布说明。这或许就是将释放 10 倍以上潜在生产力的机会。&lt;/p&gt;&lt;p&gt;开发效率的大幅提升，你准备好迎接了吗？&lt;/p&gt;&lt;p&gt;&lt;em&gt;参考链接：https://github.blog/2023-03-22-github-copilot-x-the-ai-powered-developer-experience/&lt;/em&gt;&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62700-gpt-%E7%BC%96%E7%A8%8B-%E6%97%B6%E4%BB%A3</guid>
      <pubDate>Thu, 23 Mar 2023 23:10:52 CST</pubDate>
    </item>
    <item>
      <title>编程语言是如何实现并发的之操作系统篇 · 构建我的被动收入</title>
      <link>https://itindex.net/detail/62403-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%B9%B6%E5%8F%91-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F</link>
      <description>&lt;div&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E4%BB%8E%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%BF%90%E8%A1%8C%E7%A8%8B%E5%BA%8F%E8%AF%B4%E8%B5%B7"&gt;从操作系统运行程序说起&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%94%AF%E6%8C%81"&gt;操作系统的支持&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E8%B0%83%E5%BA%A6scheduling"&gt;调度(Scheduling)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E7%BA%BF%E7%A8%8Bthread"&gt;线程(Thread)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E7%94%A8%E6%88%B7%E7%BA%BF%E7%A8%8Buser-level-thread"&gt;用户线程(User-level Thread)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8Bthread-model"&gt;线程模型(Thread Model)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2context-switching"&gt;上下文切换(Context switching)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#io%E6%A8%A1%E5%9E%8Bio-model"&gt;I/O模型(I/O Model)&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E5%B9%B6%E5%8F%91%E8%BF%98%E6%98%AF%E5%B9%B6%E8%A1%8C"&gt;并发还是并行&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E6%80%BB%E7%BB%93"&gt;总结&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h2&gt;从操作系统运行程序说起&lt;/h2&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;这是一台拥有2个虚拟CPU核心的      &lt;u&gt;Linux&lt;/u&gt;服务器的      &lt;u&gt;系统监控界面&lt;/u&gt;。其中红框①中      &lt;code&gt;PPID&lt;/code&gt;代表父进程ID，      &lt;code&gt;PID&lt;/code&gt;代表进程或线程ID。红框②中      &lt;code&gt;CPU&lt;/code&gt;代表当前线程运行的CPU核心编号。红框③中是程序的运行命令，其中绿色代表的是      &lt;u&gt;线程，白色为进程&lt;/u&gt;。&lt;/p&gt;    &lt;p&gt;以PID为1375的进程为例，它的父进程为1086，可以通过PPID不断追溯至PID为1的      &lt;u&gt;        &lt;code&gt;init&lt;/code&gt;&lt;/u&gt;进程。从这可以看出Linux通过      &lt;u&gt;        &lt;code&gt;fork()&lt;/code&gt;&lt;/u&gt;的系统调用不断的复制出大量的需要被执行的程序进程。&lt;/p&gt;    &lt;p&gt;进程是操作系统进行      &lt;u&gt;资源分配&lt;/u&gt;的一个独立单位，而实际在CPU运行调度的是线程。以进程1375为例，它又创建了7个线程，如下：&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;$ ls /proc/1375/task/
1375  1429  1430  1431  1432  1433  1488
$ ls /proc/1375/task/1429
arch_status  cgroup      cmdline             cpuset   exe     gid_map  loginuid  mountinfo  ns         oom_score      patch_state  root       sessionid  smaps_rollup  statm    uid_map
attr         children    comm                cwd      fd      io       maps      mounts     numa_maps  oom_score_adj  personality  sched      setgroups  stack         status   wchan
auxv         clear_refs  cpu_resctrl_groups  environ  fdinfo  limits   mem       net        oom_adj    pagemap        projid_map   schedstat  smaps      stat          syscall&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;其中线程1430与1432是调度运行在2号CPU核心上的，如果持续观察这个监控界面，会发现同一个线程会不定时在两个CPU核心之间来回切换，这实际正是操作系统对这些线程在多核CPU上进行抢占式调度。&lt;/p&gt;    &lt;p&gt;操作系统之所以能用有限的CPU核心去运行非常多的程序，并且用户感觉这些程序是在同时运行。一方面操作系统（内核）可以通过一些方法实现并发处理任务（程序），另外一方面得益于多个CPU核心，操作系统还可以并行处理任务。&lt;/p&gt;    &lt;p&gt;本文并不是研究操作系统是怎么实现并发的，但在搞清楚编程语言是怎么实现并发处理之前，很有必要提前对操作系统支持并发提供的一些重要特性做一个全面的介绍。操作系统为了支持多任务处理，提供了进程管理与调度，同时在I/O上提供了多种访问文件或网络的系统调用方式。&lt;/p&gt;    &lt;h2&gt;操作系统的支持&lt;/h2&gt;    &lt;pre&gt;      &lt;code&gt;# 操作系统
## 进程(Process)
### 调度方式
- 抢占式(Preemptive)
- 协作式(Cooperative)
### 执行方式
- 用户线程(User-level Thread)
  - Coroutine
    - Verticle
  - Goroutine
  - Erlang process
  - Green Thread(Java)
- 内核线程(Kernel-level Thread)
- 纤程(Fiber)
## I/O
- 同步(Synchronous)
  - 阻塞式(Blocking)
  - 非阻塞式(Non-blocking)
  - 多路复用(Multiplexing)
  - 信号驱动(Signal Driven)
- 异步(Asynchronous)&lt;/code&gt;&lt;/pre&gt;    &lt;h3&gt;调度(Scheduling)&lt;/h3&gt;    &lt;p&gt;进程调度主要有抢占式调度和协作式调度两种：抢占式(Preemptive)与协作式(Cooperative)。&lt;/p&gt;    &lt;a href="https://www.slanglabs.in/blog/python-microservices-01-tornado-asyncio-lint-test-coverage-project-setup"&gt;      &lt;img alt="&amp;#25250;&amp;#21344;&amp;#24335;&amp;#19982;&amp;#21327;&amp;#20316;&amp;#24335;&amp;#20219;&amp;#21153;&amp;#35843;&amp;#24230;(Preemptive Multitasking vs. Cooperative Multitasking)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;抢占式与协作式任务调度(Preemptive Multitasking vs. Cooperative Multitasking)&lt;/p&gt;    &lt;p&gt;抢占式调度往往在一些重要位置（Sleep Call，Timer Tick）放置了中断信号，通过这个信号通知操作系统调度器(Scheduler)进行进程切换。在抢占式模型中，正在运行的进程可能会被强行挂起，这是由于这些中断信号引发的。&lt;/p&gt;    &lt;p&gt;协作式调度也叫非抢占式调度，是指当前运行的进程通过自身代码逻辑出让CPU控制权。与抢占式调度的区别在于进程运行不会被中断信号打断，除非其主动出让控制权给其他进程。&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;以上描述并没有明确区分进程还是线程，实际在        &lt;u&gt;Linux系统的内核态上&lt;/u&gt;，用户态的线程统一按        &lt;u&gt;轻量级进程(Light-weight process)&lt;/u&gt;来处理，它们与真正的进程的区别是在一个用户态进程中的线程共享了相同的地址空间和其他资源（如打开的文件描述符），但在内核调度上并没有什么区别。&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;线程是进程的运行实例，哪怕在非多线程的进程中，在内核态实际运行进程的还是内核线程，所以接下来介绍下线程的分类。&lt;/p&gt;    &lt;h3&gt;线程(Thread)&lt;/h3&gt;    &lt;p&gt;我们在编程语言中见到的线程，一般指的是与内核线程一一映射的用户态线程，比如Java中的Thread其实就是内核线程。但一些编程语言如Erlang与Go都实现了更轻量级的用户线程。&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;用户线程(User-level Thread)        &lt;ul&gt;          &lt;li&gt;协程(Coroutines - Cooperative User-Level Threads)：应用程序通过线程库自行实现的            &lt;strong&gt;协作式调度&lt;/strong&gt;的用户线程，代表性的有Go语言的Goroutine（1.14之前的版本），Vert.x框架中的Verticle。&lt;/li&gt;          &lt;li&gt;Go Goroutine：Go语言的Goroutine在1.14之前是协程的机制，之后的版本采用了            &lt;a href="https://go.dev/doc/go1.14"&gt;异步抢占式调度(asynchronously preemptible)&lt;/a&gt;。&lt;/li&gt;          &lt;li&gt;Erlang process：Erlang VM(BEAM)管理的用户线程，与协程相比的优势在于它可以做到            &lt;strong&gt;公平调度&lt;/strong&gt;，不会出现协程中某个用户线程占用过多CPU周期。&lt;/li&gt;          &lt;li&gt;绿色线程(Green Thread)：类似于协程，是由            &lt;u&gt;Java JDK实现&lt;/u&gt;的，但因为            &lt;u&gt;早期Linux系统没实现内核态的抢占式调度&lt;/u&gt;，Green Thread只能在Solaris系统上发挥它的威力，最终在JDK 1.3之后被Native Thread取代。所以现行的JDK的线程实际是非常重量级的内核态线程，Java的            &lt;a href="https://github.com/openjdk/loom"&gt;Project Loom&lt;/a&gt;会尝试实现新的Green Thread方案，并且是抢占式的调度方案。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;内核线程(Kernel-level Thread)：操作系统内核管理的        &lt;strong&gt;抢占式调度&lt;/strong&gt;的线程，是最终运行在CPU上实际执行任务的最小单元。&lt;/li&gt;      &lt;li&gt;纤程(Fibers)：操作系统内核管理的        &lt;strong&gt;协作式调度&lt;/strong&gt;的线程。这一系统级别的方案最终因硬件和软件的发展        &lt;u&gt;逐渐式微&lt;/u&gt;，现在的用户线程也能达到协作式调度的效果。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;用户线程与内核线程的区别在于用户线程的调度是发生在用户态，内核中无法感知到用户线程的存在，并且调度也发生在用户态，一般是由线程库或编程语言运行时自行实现的。而内核线程的调度是由内核完成的，一般是抢占式调度。&lt;/p&gt;    &lt;h3&gt;用户线程(User-level Thread)&lt;/h3&gt;    &lt;p&gt;用户线程大多是采用协作调度的方式实现，本质上是      &lt;strong&gt;同步执行在与CPU核心数量相同的内核线程上的&lt;/strong&gt;，不仅能极大的降低了上下文开销，还能最佳的利用多核CPU的计算能力。&lt;/p&gt;    &lt;p&gt;用户线程的轻量除了提现在调度的上下文切换开销上，还体现了在对内存的需求上。如果要在4核心4GB内存的笔记本电脑中测试同时生成100万个线程的话，不同编程语言对内存的需求：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;Elixir: 0.48 GB (Process)&lt;/li&gt;      &lt;li&gt;Golang: 1.91 GB (Goroutine)&lt;/li&gt;      &lt;li&gt;Java: 977 GB (Thread)&lt;/li&gt;      &lt;li&gt;PHP: 6836 GB (Laravel Request)&lt;/li&gt;&lt;/ul&gt;    &lt;blockquote&gt;      &lt;p&gt;数据来源：Programming Elixir        &lt;sup&gt;Chapter 15&lt;/sup&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;但用户线程的执行最终是由内核线程来完成，所以存在一个从用户线程到内核线程的映射模型。&lt;/p&gt;    &lt;h3&gt;线程模型(Thread Model)&lt;/h3&gt;    &lt;p&gt;可能有人会疑惑，用户线程与内核线程是一一映射的吗？总的来说有以下三种线程模型：&lt;/p&gt;    &lt;a href="https://medium.com/swlh/different-threading-models-why-i-feel-goroutine-is-better-though-with-some-limitations-b73863ba4dae"&gt;      &lt;img alt="&amp;#19981;&amp;#21516;&amp;#30340;&amp;#32447;&amp;#31243;&amp;#27169;&amp;#22411;(Different Threading Models)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;不同的线程模型(Different Threading Models)&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;1x1 (kernel-level threading)：用户线程与内核线程是一一映射的。这是最简单的模型。在这种语境下，用户线程中的线程就是我们常规意义上说的线程，当程序创建一个线程时，也会在内核中创建一个内核线程。目前大多数操作系统如Linux、Solaris、FreeBSD、macOS与iOS内核的线程模型就是这种。&lt;/li&gt;      &lt;li&gt;Nx1 (user-level threading)：多个用户线程与一个内核线程映射。在这种模型中，内核线程只有一个，在应用内部不存在内核线程切换的开销，程序的并发能力是很高的。但一旦某个用户线程被阻塞（发生网络或文件I/O系统调用），其他用户线程也会被阻塞。另外应用也无法从多核CPU上获得更好的并发性。所以实际的使用场景中，这种模型并不常见。&lt;/li&gt;      &lt;li&gt;MxN (hybrid threading)：多个用户线程与多个内核线程映射。这种模型最为复杂，但也是最强大的线程模型，不仅有着很好的并发能力，同时还能获得多核CPU的处理能力。早期的Solaris内核中也支持这种线程模型，但因为其会导致内核调度过于复杂，逐渐被放弃。但在Erlang VM和Go语言的运行时就又实现了这种线程模型。&lt;/li&gt;&lt;/ul&gt;    &lt;a href="https://docs.oracle.com/cd/E19620-01/805-3024/6j2sumi1a/index.html"&gt;      &lt;img alt="&amp;#26089;&amp;#26399;Solaris&amp;#20869;&amp;#26680;&amp;#30340;&amp;#32447;&amp;#31243;&amp;#21644;&amp;#36731;&amp;#37327;&amp;#32423;&amp;#36827;&amp;#31243;(Threads and Lightweight Processes)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;早期Solaris内核的线程和轻量级进程(Threads and Lightweight Processes)&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;轻量级进程(Light-weight process)：Solaris内核中的概念，但也会在其他系统内核中看到类似的概念。指的是用户线程和内核线程之间的接口，也可被认为是一个调度用户线程执行的虚拟CPU。当用户线程发出系统调用时，运行该线程的LWP调用内核并保持绑定到该内核线程至少直到系统调用完成。当LWP在内核中为用户线程执行系统调用时，它会运行一个内核线程。因此，每个LWP都与一个内核线程相关联。只有在用户线程完全由轻量级进程构成时，才可以说轻量级进程就是线程。Go语言中Goroutine的G-P-M调度模型中，P承担了类似LWP的角色。&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;应用程序为什么会费劲的设计出用户线程？操作系统提供的内核线程不香吗？这是因为操作系统内核的线程切换是重量级的操作，它需要进行上下文切换，而这会很耗时。&lt;/p&gt;    &lt;h3&gt;上下文切换(Context switching)&lt;/h3&gt;    &lt;p&gt;在进程间切换需要消耗一定的CPU时钟周期进行相关的状态管理工作，包括寄存器和内存映射的保存与读取、更新各种内部的表等。比如在Linux内核中，上下文切换需要涉及寄存器、栈指针、程序计数器的切换。&lt;/p&gt;    &lt;p&gt;在这篇      &lt;a href="https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html"&gt;How long does it take to make a context switch?&lt;/a&gt;中可以看到一个结论是：&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;Context switching is expensive. My rule of thumb is that it’ll cost you about 30µs of CPU overhead…Applications that create too many threads that are constantly fighting for CPU time (such as Apache’s HTTPd or many Java applications) can waste considerable amounts of CPU cycles just to switch back and forth between different threads…I think the sweet spot for optimal CPU use is to have the same number of worker threads as there are hardware threads, and write code in an asynchronous / non-blocking fashion.&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;因为线程调度的上下文切换成本非常昂贵，所以最佳的做法是应用程序使用和CPU核心相同的线程数，这样每个线程都能充分利用CPU核心的时间片，避免了应用内部多个线程的上下文切换开销。&lt;/p&gt;    &lt;p&gt;用户线程的轻量级让应用程序能够极大的提高并发性，但也会有一些问题，比如某个用户线程中发起一个网络请求导致底层的内核线程被阻塞，因为多个用户线程在共用这个内核线程，最终导致大量的用户线程被阻塞。&lt;/p&gt;    &lt;p&gt;解决这个问题需要了解操作系统的I/O模型。&lt;/p&gt;    &lt;h3&gt;I/O模型(I/O Model)&lt;/h3&gt;    &lt;p&gt;当应用程序需要访问文件或者网络资源时，应用内的线程将会花费大量的时间来等待数据的到来。如下图所示：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;如果把CPU处理计算机指令的速度类比成高铁的速度，那线程一次文件或网络的访问将会和蜗牛一样慢，这相当于你在高铁上让蜗牛去帮你取快递，对CPU来说是巨大的浪费。现代操作系统已经提供了多种I/O模型来解决这个问题。常见的I/O模型有：&lt;/p&gt;    &lt;a href="https://www.4e00.com/blog/linux/2017/09/29/unix-network-programming-charpter-6-io-multiplexing.html"&gt;      &lt;img alt="&amp;#20116;&amp;#31181;I/O&amp;#27169;&amp;#22411;&amp;#23545;&amp;#27604;(Comparison of the five I/O models)"&gt;&lt;/img&gt;&lt;/a&gt;    &lt;p&gt;五种I/O模型对比(Comparison of the five I/O models)&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;同步(Synchronous)        &lt;ul&gt;          &lt;li&gt;阻塞式(Blocking)：同步阻塞式是最常见的I/O模型。线程在访问文件或网络资源时，会因发起了内核的系统调用被挂起，内核会检查文件描述符是否可读，当文件描述符中存在数据时，内核会将数据复制给线程并交回控制权，线程从挂起状态切换成可运行状态等到内核调度运行。&lt;/li&gt;          &lt;li&gt;非阻塞式(Non-blocking)：线程在访问文件或网络资源时，因文件描述符是非阻塞的，线程在检查数据是否可读的阶段是非阻塞的。此模型需要线程不停的通过轮询(polling)的方式检查文件描述符是否可读。但之所以属于同步I/O，是因为在最终读取数据(            &lt;code&gt;recvfrom&lt;/code&gt;)时需要从内核态中拷贝数据(            &lt;code&gt;recvfrom&lt;/code&gt;)到用户态中，这个阶段线程依旧被阻塞住无法处理其他指令。&lt;/li&gt;          &lt;li&gt;多路复用(Multiplexing)：和非阻塞式的区别在于，多路复用模型的线程可以同时访问多个文件描述符，这很适合构建高并发的Web服务器或中间件。但此模型会在检查文件描述符时会被阻塞(            &lt;u&gt;              &lt;code&gt;select&lt;/code&gt;&lt;/u&gt;)，并且在读取数据(            &lt;code&gt;recvfrom&lt;/code&gt;)时也会被阻塞。和多路复用模型相似的是使用多线程和阻塞I/O，但当线程产生很多时会消耗大量的内存资源以及线程调度产生的上下文切换开销，所以多路复用模型一般只使用单线程模型。&lt;/li&gt;          &lt;li&gt;信号驱动(Signal Driven)：和上面的模型区别在于，之前的模型都需要线程主动轮询，信号驱动模型需要监听内核的            &lt;code&gt;SIGIO&lt;/code&gt;事件，通过注册事件处理函数，之后线程可以继续执行其他任务。当数据可读时，线程处理函数会以阻塞的方式从内核态复制数据到用户态。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;异步(Asynchronous)：此模型和同步模型最大的区别在于，不仅在获取文件操作符时不会被阻塞，数据从内核态复制到用户态也不会被阻塞，因为内核会去做这个复制数据的工作，线程只需要在回调函数中使用数据即可。&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;关于I/O模型的更多细节，请参考：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://rickhw.github.io/2019/02/27/ComputerScience/IO-Models/"&gt;Study Notes - I/O Models&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html"&gt;I/O Models&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;blockquote&gt;      &lt;p&gt;思考一个问题：如果用户线程不可避免的需要被挂起进（如访问共享资源但没有获得锁）而导致内核线程被挂起，如何调度可以让此内核线程上的用户线程可以继续运行？&lt;/p&gt;&lt;/blockquote&gt;    &lt;h2&gt;并发还是并行&lt;/h2&gt;    &lt;p&gt;前面讨论了很多和I/O相关的概念，并不是所有的计算机任务都是和I/O相关(I/O bound)的，比如很多算法都需要CPU做大量的计算，这时候CPU核心根本不会因为等待外部资源而空转，如果在这种计算密集型(CPU bound)任务中使用多线程技术，那么就会产生大量的线程上下文切换开销，最终会导致处理任务的性能变慢。&lt;/p&gt;    &lt;p&gt;在这篇      &lt;a href="https://www.ardanlabs.com/blog/2018/12/scheduling-in-go-part3.html"&gt;Scheduling In Go : Part III - Concurrency&lt;/a&gt;文章中，作者对比了两种类型工作在并发和并行模式的对比，并提供了一些经验总结：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;CPU密集型(CPU bound)：如果任务是CPU密集型的，那么使用并行的方式来解决问题，这样可以最大的利用CPU核心的算力。如果使用并发的方式去处理，反而会增加        &lt;u&gt;复杂度&lt;/u&gt;。&lt;/li&gt;      &lt;li&gt;I/O密集型(I/O bound)：如果任务时I/O密集型的，那么使用并发的方式来解决问题，这样可以提升单个CPU核心的吞吐量(Throughput)。&lt;/li&gt;&lt;/ul&gt;    &lt;blockquote&gt;      &lt;p&gt;并发(Concurrent)与并行(Parallel)：并发是指同时处理(dealing with)很多事情，并行是指同时做(doing)很多事情。并行是并发的特殊情况，只能在计算机多核环境中实现。&lt;/p&gt;      &lt;p&gt;同步(Sync)与异步(Async)：一种编程模型。区别在于同步是可预测(predictable)的，异步是不可预测(unpredictable)的编程模型。&lt;/p&gt;&lt;/blockquote&gt;    &lt;h2&gt;总结&lt;/h2&gt;    &lt;p&gt;本篇从操作系统的视角介绍编程语言实现并发的底层概念，包括进程调度与I/O模型等。下篇开始介绍常见的      &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/"&gt;并发模型&lt;/a&gt;。&lt;/p&gt;    &lt;div&gt;      &lt;p&gt;        &lt;strong&gt;更新日志&lt;/strong&gt;2022-04-15：根据此        &lt;a href="https://www.v2ex.com/t/846178#reply20"&gt;讨论帖&lt;/a&gt;修改更新。（感谢        &lt;a href="https://www.v2ex.com/member/lxdlam"&gt;@lxdlam&lt;/a&gt;）
2022-04-10：初稿发布。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62403-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%B9%B6%E5%8F%91-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F</guid>
      <pubDate>Sun, 04 Sep 2022 14:26:12 CST</pubDate>
    </item>
    <item>
      <title>编程语言是如何实现并发的之并发模型篇 · 构建我的被动收入</title>
      <link>https://itindex.net/detail/62279-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%B9%B6%E5%8F%91-%E5%B9%B6%E5%8F%91</link>
      <description>&lt;div&gt;    &lt;div&gt;      &lt;p&gt;        &lt;strong&gt;初稿征集意见中&lt;/strong&gt;本文处于初稿状态，可能存在很多错误，如果你有不同的看法，欢迎不吝赐教，先行感谢！&lt;/p&gt;&lt;/div&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E5%B8%B8%E8%A7%81%E7%9A%84%E5%B9%B6%E5%8F%91%E6%A8%A1%E5%9E%8B"&gt;常见的并发模型&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E5%A4%9A%E8%BF%9B%E7%A8%8Bmultiprocessing"&gt;多进程(Multiprocessing)&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E5%A4%9A%E7%BA%BF%E7%A8%8Bmultithreaded"&gt;多线程(Multithreaded)&lt;/a&gt;            &lt;ul&gt;              &lt;li&gt;                &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E9%80%9A%E4%BF%A1shared-memory-communication"&gt;共享内存通信(Shared memory communication)&lt;/a&gt;                &lt;ul&gt;                  &lt;li&gt;                    &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#lock"&gt;Lock&lt;/a&gt;&lt;/li&gt;                  &lt;li&gt;                    &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#stm"&gt;STM&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;              &lt;li&gt;                &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E6%B6%88%E6%81%AF%E4%BC%A0%E9%80%92%E9%80%9A%E4%BF%A1message-passing-communication"&gt;消息传递通信(Message passing communication)&lt;/a&gt;                &lt;ul&gt;                  &lt;li&gt;                    &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#csp"&gt;CSP&lt;/a&gt;&lt;/li&gt;                  &lt;li&gt;                    &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#actor"&gt;Actor&lt;/a&gt;                    &lt;ul&gt;                      &lt;li&gt;                        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#vertx"&gt;Vert.x&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E4%BA%8B%E4%BB%B6%E9%A9%B1%E5%8A%A8event-driven"&gt;事件驱动(Event Driven)&lt;/a&gt;            &lt;ul&gt;              &lt;li&gt;                &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#event-loop-with-multiplexing"&gt;Event Loop with Multiplexing&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%9A%84%E5%AE%9E%E7%8E%B0"&gt;编程语言的实现&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#java"&gt;Java&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#go"&gt;Go&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#erlangelixir"&gt;Erlang/Elixir&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#clojure"&gt;Clojure&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E8%B5%B0%E5%90%91%E9%AB%98%E5%B9%B6%E5%8F%91"&gt;走向高并发&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E6%80%BB%E7%BB%93"&gt;总结&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/concurrency-model/#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99"&gt;参考资料&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;在      &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/"&gt;操作系统篇&lt;/a&gt;中介绍了从操作系统的视角中不同编程语言实现并发的共同的一些概念。本文将会介绍常见的并发模型及不同编程语言是如何实现的。&lt;/p&gt;    &lt;h2&gt;常见的并发模型&lt;/h2&gt;    &lt;pre&gt;      &lt;code&gt;## 并发模型
### 多进程(Multiprocessing)
- Fork
### 多线程(Multithreaded)
- 共享内存通信
  - Lock
    - Java/Go/Clojure/C
  - STM
    - Clojure
- 消息传递通信
  - CSP
    - Go
  - Actor
    - Erlang/OTP
    - Scala/Akka
    - Vert.x    
### 事件驱动(Event Driven)
- Event Loop 
  with Multiplexing
  - Nginx
  - Node.js
  - Redis
  - Twisted(Python)
  - Netty(Java)&lt;/code&gt;&lt;/pre&gt;    &lt;h3&gt;多进程(Multiprocessing)&lt;/h3&gt;    &lt;p&gt;多进程模型是利用操作系统的进程模型来实现并发的。典型的是Apache Web Server，每个用户请求接入的时候都会创建一个进程，这样应用就可以同时支持多个用户。&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;如上图，在Linux系统中PID为1的名为P0的进程通过      &lt;code&gt;fork()&lt;/code&gt;系统调用创建了3个子进程。在多处理器的系统中，这些子进程可以并行运行在多个处理器上，当然这些进程也可以被同一个处理器通过      &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2context-switching"&gt;上下文切换&lt;/a&gt;进行CPU分时共享。&lt;/p&gt;    &lt;p&gt;在图中M1、M2与M3都代表内存资源，在多进程中如果不同进程想共享内存中的数据必须通过      &lt;a href="https://en.wikipedia.org/wiki/Inter-process_communication"&gt;进程间通信&lt;/a&gt;的方式来实现。&lt;/p&gt;    &lt;p&gt;多进程模型的缺点在于创建进程的开销非常高，如果进程过多会很快消耗光系统资源，并且上下文切换在进程间的开销也很高，在      &lt;a href="https://www.bmpi.dev/dev/deep-in-program-language/how-to-implement-concurrency/os-scheduling/#%E5%B9%B6%E5%8F%91%E8%BF%98%E6%98%AF%E5%B9%B6%E8%A1%8C"&gt;I/O密集型任务&lt;/a&gt;的应用中，此并发模型很难满足需求。&lt;/p&gt;    &lt;h3&gt;多线程(Multithreaded)&lt;/h3&gt;    &lt;p&gt;在操作系统的视角看，比如Linux中，在进程中创建线程是通过      &lt;code&gt;clone()&lt;/code&gt;系统调用来实现，这和创建子进程的区别不大。线程与进程的区别在于同一个进程内的线程共享着进程分配的资源，线程不被分配资源，只是操作系统调度执行任务的抽象的最小单元。&lt;/p&gt;    &lt;p&gt;比如下图中，PID为10的进程P0通过      &lt;code&gt;clone()&lt;/code&gt;系统调用创建了3个线程，这些线程都可以访问进程分配的内存资源M0。&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;我们也可以通过      &lt;code&gt;htop&lt;/code&gt;命令在Linux中看到这些进程及线程的运行信息，比如下图中所示PID为339328进程（它是一个基于JVM的Clojure应用）及其创建的线程信息：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;也可以通过      &lt;code&gt;/proc&lt;/code&gt;文件系统获取进程创建的线程的信息：&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;        &lt;code&gt;$ ls /proc/339328/task/1043773339340339342339344339346339348339350339357339359339361339363339367339380339388788478975443991563339328339341339343339345339347339349339353339358339360339362339364339378339382424498975439991561&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;相比多进程模型来说，因为线程比进程创建的系统开销小，所以多线程模型是很常见的实现并发的方式。但此种模型存在一个必须解决的问题，就是线程间通信的问题。但线程为什么要通信呢？那是因为大部分业务系统问题的解空间在用冯·诺伊曼计算机去实现的时候，都存在并发计算时线程间数据共享的问题。要数据共享有两种方式：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;共享内存通信(Shared memory communication)：不同线程间可以访问同一内存地址空间，并可修改此地址空间的数据。&lt;/li&gt;      &lt;li&gt;消息传递通信(Message passing communication)：不同线程间只能通过收发消息的形式去通信，数据只能被拥有它的线程修改。&lt;/li&gt;&lt;/ul&gt;    &lt;h4&gt;共享内存通信(Shared memory communication)&lt;/h4&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;因为线程间共享内存资源，在访问临界区域时会出现数据竞争（发生竞态条件，即代码的行为取决于各操作的时序）的问题，如果不能正确的处理此问题，程序会产生线程不安全的问题，最终导致程序崩溃或无法正常运行。&lt;/p&gt;    &lt;p&gt;解决竞态条件的方式是对数据进行同步(      &lt;a href="https://en.wikipedia.org/wiki/Synchronization_(computer_science)"&gt;Synchronize&lt;/a&gt;)访问。要实现同步访问常见的方式有：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;锁(        &lt;a href="https://en.wikipedia.org/wiki/Lock_(computer_science)"&gt;Lock&lt;/a&gt;)：通过锁定临界区域来实现同步访问。&lt;/li&gt;      &lt;li&gt;信号量(        &lt;a href="https://en.wikipedia.org/wiki/Semaphore_(programming)"&gt;Semaphores&lt;/a&gt;)：可以通过信号量的增减控制对一个或多个线程对临界区域的访问。&lt;/li&gt;      &lt;li&gt;同步屏障(        &lt;a href="https://en.wikipedia.org/wiki/Barrier_(computer_science)"&gt;Barriers&lt;/a&gt;)：通过设置屏障控制不同线程执行周期实现同步访问。&lt;/li&gt;&lt;/ul&gt;    &lt;h5&gt;Lock&lt;/h5&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;锁(Lock)，也叫互斥量(Mutex)。线程在操作临界区域资源时，需要先获取锁，然后才能操作，当操作完成后，需要释放锁。此模型利用了对底层硬件运行过程的形式化，这让其即简单又复杂。从锁的种类就可以看出来其复杂性：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;自旋锁&lt;/li&gt;      &lt;li&gt;递归锁&lt;/li&gt;      &lt;li&gt;乐观/悲观锁&lt;/li&gt;      &lt;li&gt;公平/非公平锁&lt;/li&gt;      &lt;li&gt;独享/共享锁&lt;/li&gt;      &lt;li&gt;偏向/轻量级/重量锁&lt;/li&gt;      &lt;li&gt;分段锁&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;对锁的使用不当还会产生死锁问题(      &lt;a href="https://en.wikipedia.org/wiki/Deadlock"&gt;Deadlock&lt;/a&gt;)。在实际开发过程中，能不用锁就不用锁，可以考虑使用一些轻量级的替代方案如原子变量(Atomic)，或      &lt;a href="https://www.baeldung.com/lock-free-programming"&gt;无锁(lock-free)非阻塞(non-blocking)算法&lt;/a&gt;实现的数据结构。&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;原子变量的更新为何是线程安全的？因为CPU提供了CAS(        &lt;a href="https://en.wikipedia.org/wiki/Compare-and-swap"&gt;Compare-and-swap&lt;/a&gt;)的指令来更新原子变量，这条指令从硬件上确保了此操作是线程安全的。&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;此模型的优点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;大多编程语言都支持此模型；&lt;/li&gt;      &lt;li&gt;贴近硬件架构，使用得当性能很高；&lt;/li&gt;      &lt;li&gt;是其他并发模型的基础；&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;此模型的缺点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;不支持分布式内存模型，只解决了进程内的并发同步；&lt;/li&gt;      &lt;li&gt;不好调试与测试，想用好不容易；&lt;/li&gt;&lt;/ul&gt;    &lt;h5&gt;STM&lt;/h5&gt;    &lt;p&gt;锁模型是一种悲观的并发同步机制，但实际上冲突发生的概率并不高，所以乐观的并发同步机制性能更好。STM(Software transactional memory)就是这样一种用来代替锁模型的乐观并发同步机制。STM是用软件的方式去实现事务内存(Transactional memory)，而事务内存中的事务(Transactional)正是关系型数据库中的概念，一个事务必须满足      &lt;a href="https://en.wikipedia.org/wiki/ACID"&gt;ACID&lt;/a&gt;性质，如下图所示：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;在t0时刻，T1、T2与T3线程同时获取要操作的同一数据的快照v0，之后T1线程在自己的事务里于t1时刻提交自己的写入值v1，之后T2线程在自己的事务里提交自己的写入值v2，由于在提交时刻会做冲突检测，此事务发现操作数据的快照已经发生变化，于是回滚自己的提交。之后开启新的事务重新获取最新的快照v1，并于时刻t2成功提交自己的写入值v2。在线程v3中由于是读取操作，并没有数据修改，所以在它的事务中使用的是最早的快照v0。&lt;/p&gt;&lt;/blockquote&gt;    &lt;p&gt;在STM的事务中尽可能避免副作用，比如在事务中去修改原子变量这种操作，可能会导致事务回滚失败。&lt;/p&gt;    &lt;p&gt;STM实现的一种方式是基于MVCC(      &lt;a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"&gt;Multiversion concurrency control&lt;/a&gt;)。很多编程语言提供了这种并发模型的实现。&lt;/p&gt;    &lt;p&gt;此模型的优点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;相比锁模型更简单；&lt;/li&gt;      &lt;li&gt;大部分情况下更高效；&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;此模型的缺点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;在事务内需要避免产生副作用；&lt;/li&gt;      &lt;li&gt;不支持分布式内存模型，只解决了进程内的并发同步；&lt;/li&gt;&lt;/ul&gt;    &lt;h4&gt;消息传递通信(Message passing communication)&lt;/h4&gt;    &lt;p&gt;在锁模型中，生产者和消费者之间的通信是通过共享内存而完成的，要实现安全通信，必须给共享内存所属的临界区加锁。那如果生产者和消费者是通过消息传递完成通信的呢？那样我们就可以摆脱锁模型的限制了。&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;Do not communicate by sharing memory. Instead, share memory by communicating. Communication forces coordination. (Ivo Balbaert)&lt;/p&gt;&lt;/blockquote&gt;    &lt;h5&gt;CSP&lt;/h5&gt;    &lt;p&gt;通信顺序进程(      &lt;a href="https://en.wikipedia.org/wiki/Communicating_sequential_processes"&gt;CSP(Communicating sequential processes)&lt;/a&gt;)是一种形式语言，用来描述基于消息传递通信的安全并发模型。如下图所示：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;这些任务块之间的通信是基于通道(Channel)来完成的，当创建了一个通道之后，不同的任务块就可以通过持有这个通道来通信，通道可以被不同的任务块共享。通道两端任务块的通信可以是同步的，也可以是异步的。&lt;/p&gt;    &lt;p&gt;在这里的任务块不是如Java里重量级的线程类，在运行时是非常轻量级的代码块。这些代码块可以被调度到不同的线程中，最终被多个CPU内核并发执行。&lt;/p&gt;    &lt;p&gt;此模型的优点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;相比锁模型更简单；&lt;/li&gt;      &lt;li&gt;很容易实现高并发；&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;此模型的缺点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;不支持分布式内存模型，只解决了进程内的并发同步；&lt;/li&gt;&lt;/ul&gt;    &lt;h5&gt;Actor&lt;/h5&gt;    &lt;p&gt;演员模型(      &lt;a href="https://en.wikipedia.org/wiki/Actor_model"&gt;Actor&lt;/a&gt;)是一种类似面向对象编程思想的安全并发模型。在面向对象的世界里，对象是一种封装了状态及行为的实体，对象间通过消息去通信（通过对象调用其方法）。而在Actor模型中，一切皆Actor，每个Actor中都有自己的状态，其他Actor只能通过通信的方式来获取或修改被通信Actor的状态。Actor通信的方式类似收发邮件，它有自己的收件箱，如下图所示：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;在上述图中，我们可以看到相比CSP模型，Actor模型可以跨节点在分布式集群中运行。实际上Actor模型的代表Erlang正是天然分布式容错的编程语言。&lt;/p&gt;    &lt;p&gt;此模型的优点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;相比锁模型更简单；&lt;/li&gt;      &lt;li&gt;很容易实现高并发；&lt;/li&gt;      &lt;li&gt;支持分布式内存模型，能实现跨节点的并发同步；&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;此模型的缺点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;存在信箱满后消息丢失的问题;&lt;/li&gt;&lt;/ul&gt;    &lt;h6&gt;Vert.x&lt;/h6&gt;    &lt;p&gt;      &lt;a href="https://vertx.io/"&gt;Vert.x&lt;/a&gt;是一个基于JVM的反应式模型的工具包，在解决并发同步的问题上它采用了类似Actor模型的方式，如下图所示：&lt;/p&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;在它的架构里最基本的计算单元名为Verticle，这些Verticle之间通过事件总线(Event Bus)进行异步通信，当然也可以像Actor一样跨节点通信。和Actor模型不同的地方在于Vert.x支持多种语言，因为Event Bus解耦了实现Verticle的语言限制。&lt;/p&gt;    &lt;h3&gt;事件驱动(Event Driven)&lt;/h3&gt;    &lt;p&gt;在多线程方式实现的并发模型中，我们解决问题的方式是通过创建更多的线程来提高系统的并发处理能力。但线程创建的开销及线程间上下文调度切换的开销并不是很小，所以纵使系统的硬件资源很充足，也存在一定的上限。那么有没有可能只创建一个线程，而且这个线程可以同时处理很多个任务呢？当然是可以的，这正是基于I/O多路复用的事件循环处理并发模型的解法，通过单线程来并发处理I/O密集型的任务。&lt;/p&gt;    &lt;blockquote&gt;      &lt;p&gt;Less is more.&lt;/p&gt;&lt;/blockquote&gt;    &lt;h4&gt;Event Loop with Multiplexing&lt;/h4&gt;    &lt;p&gt;      &lt;img alt=""&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;此模型巧妙的利用了系统内核提供的I/O多路复用系统调用，将多个socket连接转换成一个事件队列(event queue)，只需要单个线程即可循环处理这个事件队列。当然这个线程是有可能被阻塞或长期占用的，针对这种类型的任务处理可以单独使用一个      &lt;a href="https://www.nginx.com/blog/thread-pools-boost-performance-9x/"&gt;线程池&lt;/a&gt;去做，这样就不会阻塞Event Loop的线程了。&lt;/p&gt;    &lt;p&gt;此模型的优点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;单线程对系统资源的占用很小；&lt;/li&gt;      &lt;li&gt;很容易实现高并发；&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;此模型的缺点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;不支持分布式内存模型，只解决了进程内的并发同步；&lt;/li&gt;&lt;/ul&gt;    &lt;h2&gt;编程语言的实现&lt;/h2&gt;    &lt;p&gt;许多编程语言标准库或三方库都已支持上述大多数的并发模型，但因为一些历史原因带来的兼容性问题，开发者的使用体验好坏不一。以下仅简单介绍下各种编程语言标准库对并发模型的实现及流行三方库的扩展支持。&lt;/p&gt;    &lt;h3&gt;Java&lt;/h3&gt;    &lt;p&gt;Java是一门面向对象的编程语言，标准库对并发的支持是      &lt;strong&gt;基于共享内存通信的锁模型&lt;/strong&gt;，因此用Java的标准库来实现高并发是一件非常有挑战的事情，想不踩坑太难。&lt;/p&gt;    &lt;p&gt;想深入了解Java的并发模型，可以参考      &lt;a href="https://book.douban.com/subject/10484692/"&gt;《Java并发编程实战》&lt;/a&gt;。&lt;/p&gt;    &lt;p&gt;当然基于Java的三方库实现了很多并发模型，如：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;Actor:        &lt;a href="https://doc.akka.io/docs/akka/current/?language=java"&gt;Akka&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;Event Loop with Multiplexing        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://netty.io/"&gt;Netty&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://github.com/nginx-clojure/nginx-clojure/"&gt;Nginx-Clojure&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h3&gt;Go&lt;/h3&gt;    &lt;p&gt;在Go流行的时期流传着一个故事：一个PHP的普通开发者在一周内学会了Go语言，之后开发出了一个高并发的Web应用，要用Java实现同样的性能，至少需要多年的经验。&lt;/p&gt;    &lt;p&gt;暂且不论这个故事是否合理，但它展示了Go语言的两大亮点：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;语法简单易学；&lt;/li&gt;      &lt;li&gt;天然支持高并发模型；&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;Go在语言层面实现了      &lt;strong&gt;CSP并发模型&lt;/strong&gt;，因此能让开发者以非常低的成本写出高并发的Web应用。在对CSP并发模型的实现中，Go任务块一般是一个函数，这个函数的调度是由Go语言的调度器来完成，可以被调度在不同的线程中。如果在这个函数中出现了阻塞线程的如网络I/O的操作，调度器会委托给Netpoller去执行，而Netpoller的底层正是对操作系统I/O多路复用技术的封装。&lt;/p&gt;    &lt;p&gt;Go高并发的秘诀在于它的G-P-M运行时调度模型，详细的设计可参考这篇文章：      &lt;a href="https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html"&gt;Scheduling In Go : Part II - Go Scheduler&lt;/a&gt;。&lt;/p&gt;    &lt;h3&gt;Erlang/Elixir&lt;/h3&gt;    &lt;p&gt;Erlang是一门天然分布式、高并发、容错的编程语言，它是      &lt;strong&gt;Actor并发模型&lt;/strong&gt;的代表编程语言。Elixir是基于Erlang虚拟机(BEAM)的一种不纯粹的、动态类型的函数式语言。它们自然原生支持Actor并发模型，所以在开发高并发的分布式容错应用时，可以考虑使用Elixir，它强大的并发模型及富有表达力的语法可以提供非常好的开发体验。&lt;/p&gt;    &lt;p&gt;Erlang的虚拟机在运行时实现了      &lt;u&gt;软实时抢占式调度&lt;/u&gt;，详细的信息可参考这篇文章：      &lt;a href="https://jlouisramblings.blogspot.com/2013/01/how-erlang-does-scheduling.html"&gt;How Erlang does scheduling&lt;/a&gt;。&lt;/p&gt;    &lt;h3&gt;Clojure&lt;/h3&gt;    &lt;p&gt;Clojure是基于JVM平台的Lisp方言，是不纯粹的、动态类型的函数式语言（这点倒和Elixir类似）。Clojure可以直接调用Java的库，这让其可支持非常多的并发模型，但最有特色的就是它的标准库实现了STM的并发模型，官方提供的异步库      &lt;a href="https://github.com/clojure/core.async"&gt;core.async&lt;/a&gt;也实现了CSP的并发模型。当然还可以通过      &lt;a href="https://github.com/nginx-clojure/nginx-clojure/"&gt;Nginx-Clojure&lt;/a&gt;实现基于I/O多路复用的高并发模型。&lt;/p&gt;    &lt;p&gt;Clojure支持非常多的并发原语，想了解可参考这篇文章：      &lt;a href="https://ericnormand.me/guide/clojure-concurrency"&gt;Clojure Concurrency Tutorial&lt;/a&gt;。&lt;/p&gt;    &lt;h2&gt;走向高并发&lt;/h2&gt;    &lt;p&gt;1999年的时候      &lt;a href="https://en.wikipedia.org/wiki/C10k_problem"&gt;C10K&lt;/a&gt;的问题被提出，当时在单机上并发处理上万的连接是一件富有挑战的事情，最终借助操作系统提供的一些支持如进程调度与I/O模型，通过一些高并发的模型如I/O多路复用，我们可以在单机上支持上万甚至更多的并发连接。&lt;/p&gt;    &lt;p&gt;二十多年过去了，互联网从Web1走向了      &lt;a href="https://www.bmpi.dev/dev/glimpse-of-web3/"&gt;Web3&lt;/a&gt;，联网的设备从PC走向了IoT，我们已经到了万物互联的时代。C10K的问题已经转变为      &lt;strong&gt;C10M&lt;/strong&gt;的问题，如何在单机上支持百万乃至千万的连接呢？&lt;/p&gt;    &lt;p&gt;在这篇      &lt;a href="https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections"&gt;The Road to 2 Million Websocket Connections in Phoenix&lt;/a&gt;中我们可以看到如何用Elixir的Web框架Phoenix在一台40核128GB内存的机器上支撑两百万的Websocket长链接，最终因Linux内核的限制而无法进一步提高并发链接数，并没有达到服务器的极限。&lt;/p&gt;    &lt;p&gt;Migratorydata甚至使用Java在一台12核96GB内存的机器上支撑了上千万的Websocket长连接，更多细节见这两篇文章：&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://migratorydata.com/blog/migratorydata-solved-the-c10m-problem/"&gt;How MigratoryData solved the C10M problem: 10 Million Concurrent Connections on a Single Commodity Server&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://arxiv.org/pdf/1712.09876.pdf"&gt;Reliable Messaging to Millions of Users with MigratoryData&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;当单机并发达10K时，内核是解决方案，当单机并发达10M时，内核是瓶颈。比如Linux内核需要32GB内存来维护上千万的Socket连接。所以在单机千万级的解决方案需要在内核之外的应用层去做更多的事情，感兴趣的读者可以看这篇文章：      &lt;a href="http://highscalability.com/blog/2013/5/13/the-secret-to-10-million-concurrent-connections-the-kernel-i.html"&gt;The Secret to 10 Million Concurrent Connections -The Kernel is the Problem, Not the Solution&lt;/a&gt;。&lt;/p&gt;    &lt;h2&gt;总结&lt;/h2&gt;    &lt;p&gt;在软件开发过程中，安全至关重要。编程的两大安全难题是      &lt;u&gt;线程安全与内存安全&lt;/u&gt;。这个系列的两篇文章都是在尝试介绍不同编程语言是如何解决线程安全从而更容易的实现高并发。&lt;/p&gt;    &lt;p&gt;另外一个想借助这篇文章分享的一个观点是：问题可以被更复杂的方案去解决，当然也可以通过另一种截然不同的思路去更简单的解决。当你觉得一个方案很复杂的时候，试着去找一个完全不同方向的方案，也许会更容易一些。&lt;/p&gt;    &lt;h2&gt;参考资料&lt;/h2&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html"&gt;I/O Models&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://web.archive.org/web/20160423223903/http://concur.rspace.googlecode.com/hg/talk/concur.html#slide-5"&gt;Concurrency is not Parallelism (it’s better)&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.loginradius.com/blog/engineering/callback-vs-promises-vs-async-await/"&gt;Callback vs Promises vs Async Await&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://betterprogramming.pub/sync-vs-async-vs-concurrent-vs-parallel-5754cdb60f66"&gt;Sync vs. Async vs. Concurrent vs. Parallel&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.thegeekstuff.com/2013/11/linux-process-and-threads/"&gt;What are Linux Processes, Threads, Light Weight Processes, and Process State&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://gist.github.com/jboner/2841832"&gt;Latency Numbers Every Programmer Should Know&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cs.uaf.edu/2015/fall/cs301/lecture/11_16_thread_user.html"&gt;Cooperative User-Level Threads (Coroutines)&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://en.m.wikipedia.org/wiki/Green_threads"&gt;Green threads - Wikipedia&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://stackoverflow.com/questions/5878231/what-exactly-makes-erlang-process-green-thread-coroutine-lighter-than-kernel"&gt;multithreading&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://news.ycombinator.com/item?id=193440"&gt;Ask HN: Erlang and network connections?&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://en.wikipedia.org/wiki/Thread_(computing)#Threading_models"&gt;Thread (computing) - Wikipedia&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://berb.github.io/diploma-thesis/community/index.html"&gt;Concurrent Programming for Scalable Web Architectures&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="http://tutorials.jenkov.com/java-concurrency/concurrency-models.html"&gt;Concurrency Models&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="http://java.ociweb.com/mark/stm/article.html"&gt;Software Transactional Memory&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://naghavi.me/blog/vertx-concurrency-model/"&gt;Vert.x concurrency model&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop"&gt;The event loop - JavaScript | MDN&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://v8.dev/blog/fast-async"&gt;Faster async functions and promises · V8&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://mp.weixin.qq.com/s/9g0wVT-5PpmXRoKJZo-skA"&gt;透过 rust 探索系统的本原：并发篇&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://zhuanlan.zhihu.com/p/137339439"&gt;并发模型&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://book.douban.com/subject/26337939/"&gt;七周七并发模型&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;div&gt;      &lt;p&gt;        &lt;strong&gt;更新日志&lt;/strong&gt;2022-05-27：初稿发布。        &lt;br /&gt;2022-05-07：开始写作。&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62279-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%B9%B6%E5%8F%91-%E5%B9%B6%E5%8F%91</guid>
      <pubDate>Sun, 29 May 2022 07:15:54 CST</pubDate>
    </item>
    <item>
      <title>eBPF编程指北</title>
      <link>https://itindex.net/detail/62262-ebpf-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;div&gt;  &lt;em&gt;—&lt;/em&gt;  &lt;em&gt;   &lt;strong&gt;    &lt;strong&gt; &lt;/strong&gt;&lt;/strong&gt;&lt;/em&gt;  &lt;em&gt;   &lt;strong&gt;1&lt;/strong&gt;&lt;/em&gt;   &lt;em&gt;—&lt;/em&gt;  &lt;br /&gt;  &lt;strong&gt;开发环境&lt;/strong&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;这里以 Ubuntu 20.04 为例构建 eBPF 开发环境：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ uname -a    &lt;br /&gt;Linux VM-1-3-ubuntu 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux    &lt;br /&gt;$ sudo apt install build-essential git make libelf-dev clang llvm strace tar bpfcc-tools linux-headers-$(uname -r) gcc-multilib  flex  bison libssl-dev -y&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;主流的发行版在对 LLVM 打包的时候就默认启用了 BPF 后端，因此，在大部分发行版上安 装 clang 和 llvm 就可以将 C 代码编译为 BPF 对象文件了。&lt;/p&gt;  &lt;p&gt;典型的工作流是：&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;用 C 编写 BPF 程序&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;用 LLVM 将 C 程序编译成对象文件（ELF）&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;用户空间 BPF ELF 加载器（例如 iproute2）解析对象文件&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;加载器通过 bpf() 系统调用将解析后的对象文件注入内核&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;内核验证 BPF 指令，然后对其执行即时编译（JIT），返回程序的一个新文件描述符&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;利用文件描述符 attach 到内核子系统（例如网络子系统）&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;某些子系统还支持将 BPF 程序 offload 到硬件（例如网卡）。&lt;/p&gt;  &lt;p&gt;查看 LLVM 支持的 BPF target：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ llc --version    &lt;br /&gt;LLVM (http://llvm.org/):    &lt;br /&gt;  LLVM version 10.0.0    &lt;br /&gt;    &lt;br /&gt;  Optimized build.    &lt;br /&gt;  Default target: x86_64-pc-linux-gnu    &lt;br /&gt;  Host CPU: skylake    &lt;br /&gt;    &lt;br /&gt;  Registered Targets:    &lt;br /&gt;    # ...    &lt;br /&gt;    bpf        - BPF (host endian)    &lt;br /&gt;    bpfeb      - BPF (big endian)    &lt;br /&gt;    bpfel      - BPF (little endian)    &lt;br /&gt;    # ...&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;默认情况下，bpf target 使用编译时所在的 CPU 的大小端格式，即，如果 CPU 是小端，BPF 程序就会用小端表示；如果 CPU 是大端，BPF 程序就是大端。这也和 BPF 的运行时行为相匹配，这样的行为比较通用，而且大小端格式一致可以避免一些因为格式导致的架构劣势。&lt;/p&gt;  &lt;p&gt;BPF 程序可以在大端节点上编译，在小端节点上运行，或者相反，因此对于交叉编译， 引入了两个新目标 bpfeb 和 bpfel。注意前端也需要以相应的大小端方式运行。&lt;/p&gt;  &lt;p&gt;在不存在大小端混用的场景下，建议使用 bpf target。例如，在 x86_64 平台上（小端 ），指定 bpf 和 bpfel 会产生相同的结果，因此触发编译的脚本不需要感知到大小端 。&lt;/p&gt;  &lt;p&gt;下面是一个最小的完整 XDP 程序，实现丢弃包的功能（xdp-example.c）：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;#include &amp;lt;linux/bpf.h&amp;gt;    &lt;br /&gt;    &lt;br /&gt;#ifndef __section    &lt;br /&gt;# define __section(NAME)                  \    &lt;br /&gt;   __attribute__((section(NAME), used))    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;__section(&amp;quot;prog&amp;quot;)    &lt;br /&gt;int xdp_drop(struct xdp_md *ctx)    &lt;br /&gt;{    &lt;br /&gt;    return XDP_DROP;    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;char __license[] __section(&amp;quot;license&amp;quot;) = &amp;quot;GPL&amp;quot;;&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;用下面的命令编译并加载到内核：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ clang -O2 -Wall -target bpf -c xdp-example.c -o xdp-example.o    &lt;br /&gt;$ ip link set dev em1 xdp obj xdp-example.o&lt;/p&gt;&lt;/pre&gt;  &lt;em&gt;   &lt;br /&gt;—&lt;/em&gt;  &lt;em&gt;   &lt;strong&gt;    &lt;strong&gt; 2&lt;/strong&gt;&lt;/strong&gt;&lt;/em&gt;   &lt;em&gt;—&lt;/em&gt;  &lt;br /&gt;  &lt;strong&gt;编程限制&lt;/strong&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;用 C 语言编写 BPF 程序不同于用 C 语言做应用开发，有一些陷阱需要注意。本节列出了 二者的一些不同之处。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;所有函数都需要内联（inlined）、没有函数调用（对于老版本 LLVM）或共享库调用&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;BPF 不支持共享库（Shared libraries）。但是，可以将常规的库代码（library code）放到头文件中，然后在主程序中 include 这些头文件，例如 Cilium 就大量使用了这种方式 （可以查看 bpf/lib/ 文件夹）。&lt;/p&gt;  &lt;p&gt;另外，也可以 include 其他的一些头文件，例如内核或其他库中的头文件，复用其中的静态内联函数（static inline functions）或宏/定义（ macros / definitions）。&lt;/p&gt;  &lt;p&gt;内核 4.16+ 和 LLVM 6.0+ 之后已经支持 BPF-to-BPF 函数调用。对于任意给定的程序片段 ，在此之前的版本只能将全部代码编译和内联成一个扁平的 BPF 指令序列（a flat sequence of BPF instructions）。&lt;/p&gt;  &lt;p&gt;在这种情况下，最佳实践就是为每个库函数都使用一个 像 __inline 一样的注解（annotation ），下面的例子中会看到。推荐使用 always_inline，因为编译器可能会对只注解为 inline 的长函数仍然做 uninline 操 作。&lt;/p&gt;  &lt;p&gt;如果是后者，LLVM 会在 ELF 文件中生成一个重定位项（relocation entry），BPF ELF 加载器（例如 iproute2）无法解析这个重定位项，因此会产生一条错误，因为对加载器 来说只有 BPF maps 是合法的、能够处理的重定位项。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;#include &amp;lt;linux/bpf.h&amp;gt;    &lt;br /&gt;    &lt;br /&gt;#ifndef __section    &lt;br /&gt;# define __section(NAME)                  \    &lt;br /&gt;   __attribute__((section(NAME), used))    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;#ifndef __inline    &lt;br /&gt;# define __inline                         \    &lt;br /&gt;   inline __attribute__((always_inline))    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;static __inline int foo(void)    &lt;br /&gt;{    &lt;br /&gt;    return XDP_DROP;    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;__section(&amp;quot;prog&amp;quot;)    &lt;br /&gt;int xdp_drop(struct xdp_md *ctx)    &lt;br /&gt;{    &lt;br /&gt;    return foo();    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;char __license[] __section(&amp;quot;license&amp;quot;) = &amp;quot;GPL&amp;quot;;&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;多个程序可以放在同一 C 文件中的不同 section&lt;/strong&gt;  &lt;p&gt;BPF C 程序大量使用 section annotations。一个 C 文件典型情况下会分为 3 个或更 多个 section。BPF ELF 加载器利用这些名字来提取和准备相关的信息，以通过 bpf() 系统调用加载程序和 maps。&lt;/p&gt;  &lt;p&gt;例如，查找创建 map 所需的元数据和 BPF 程序的 license 信息 时，iproute2 会分别使用 maps 和 license 作为默认的 section 名字。注意在程序创建时 license section 也会加载到内核，如果程序使用的是兼容 GPL 的协议，这些信息就可以启用那些 GPL-only 的辅助函数，例如 bpf_ktime_get_ns() 和 bpf_probe_read() 。&lt;/p&gt;  &lt;p&gt;其余的 section 名字都是和特定的 BPF 程序代码相关的，例如，下面经过修改之后的代码包含两个程序 section：ingress 和 egress。这个非常简单的示例展示了不同 section （这里是 ingress 和 egress）之间可以共享 BPF map 和常规的静态内联辅助函数（例如 account_data()）。&lt;/p&gt;  &lt;p&gt;示例程序：&lt;/p&gt;  &lt;p&gt;这里将原来的 xdp-example.c 修改为 tc-example.c，然后用 tc 命令加载，attach 到 一个 netdevice 的 ingress 或 egress hook。该程序对传输的字节进行计数，存储在一 个名为 acc_map 的 BPF map 中，这个 map 有两个槽（slot），分别用于 ingress hook 和 egress hook 的流量统计。&lt;/p&gt;  &lt;pre&gt;#include &amp;lt;linux/bpf.h&amp;gt;   &lt;br /&gt;#include &amp;lt;linux/pkt_cls.h&amp;gt;   &lt;br /&gt;#include &amp;lt;stdint.h&amp;gt;   &lt;br /&gt;#include &amp;lt;iproute2/bpf_elf.h&amp;gt;   &lt;br /&gt;   &lt;br /&gt;#ifndef __section   &lt;br /&gt;# define __section(NAME)                  \   &lt;br /&gt;   __attribute__((section(NAME), used))   &lt;br /&gt;#endif   &lt;br /&gt;   &lt;br /&gt;#ifndef __inline   &lt;br /&gt;# define __inline                         \   &lt;br /&gt;   inline __attribute__((always_inline))   &lt;br /&gt;#endif   &lt;br /&gt;   &lt;br /&gt;#ifndef lock_xadd   &lt;br /&gt;# define lock_xadd(ptr, val)              \   &lt;br /&gt;   ((void)__sync_fetch_and_add(ptr, val))   &lt;br /&gt;#endif   &lt;br /&gt;   &lt;br /&gt;#ifndef BPF_FUNC   &lt;br /&gt;# define BPF_FUNC(NAME, ...)              \   &lt;br /&gt;   (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME   &lt;br /&gt;#endif   &lt;br /&gt;   &lt;br /&gt;static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);   &lt;br /&gt;   &lt;br /&gt;struct bpf_elf_map acc_map __section(&amp;quot;maps&amp;quot;) = {   &lt;br /&gt;    .type           = BPF_MAP_TYPE_ARRAY,   &lt;br /&gt;    .size_key       = sizeof(uint32_t),   &lt;br /&gt;    .size_value     = sizeof(uint32_t),   &lt;br /&gt;    .pinning        = PIN_GLOBAL_NS,   &lt;br /&gt;    .max_elem       = 2,   &lt;br /&gt;};   &lt;br /&gt;   &lt;br /&gt;static __inline int account_data(struct __sk_buff *skb, uint32_t dir)   &lt;br /&gt;{   &lt;br /&gt;    uint32_t *bytes;   &lt;br /&gt;   &lt;br /&gt;    bytes = map_lookup_elem(&amp;amp;acc_map, &amp;amp;dir);   &lt;br /&gt;    if (bytes)   &lt;br /&gt;            lock_xadd(bytes, skb-&amp;gt;len);   &lt;br /&gt;   &lt;br /&gt;    return TC_ACT_OK;   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;__section(&amp;quot;ingress&amp;quot;)   &lt;br /&gt;int tc_ingress(struct __sk_buff *skb)   &lt;br /&gt;{   &lt;br /&gt;    return account_data(skb, 0);   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;__section(&amp;quot;egress&amp;quot;)   &lt;br /&gt;int tc_egress(struct __sk_buff *skb)   &lt;br /&gt;{   &lt;br /&gt;    return account_data(skb, 1);   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;char __license[] __section(&amp;quot;license&amp;quot;) = &amp;quot;GPL&amp;quot;;&lt;/pre&gt;其他程序说明：  &lt;p&gt;这个例子还展示了其他一些很有用的东西，在开发过程中要注意。&lt;/p&gt;  &lt;p&gt;首先，include 了内核头文件、标准 C 头文件和一个特定的 iproute2 头文件 iproute2/bpf_elf.h，后者定义了struct bpf_elf_map。iproute2 有一个通用的 BPF ELF 加载器，因此 struct bpf_elf_map的定义对于 XDP 和 tc 类型的程序是完全一样的。&lt;/p&gt;  &lt;p&gt;其次，程序中每条 struct bpf_elf_map 记录（entry）定义一个 map，这个记录包含了生成一 个（ingress 和 egress 程序需要用到的）map 所需的全部信息（例如 key/value 大 小）。这个结构体的定义必须放在 maps section，这样加载器才能找到它。可以用这个 结构体声明很多名字不同的变量，但这些声明前面必须加上 __section(&amp;quot;maps&amp;quot;) 注解。&lt;/p&gt;  &lt;p&gt;结构体 struct bpf_elf_map 是特定于 iproute2 的。不同的 BPF ELF 加载器有不同的格式，例如，内核源码树中的 libbpf（主要是 perf 在用）就有一个不同的规范 （结构体定义）。iproute2 保证 struct bpf_elf_map 的后向兼容性。Cilium 采用的 是 iproute2 模型。&lt;/p&gt;  &lt;p&gt;另外，这个例子还展示了 BPF 辅助函数是如何映射到 C 代码以及如何被使用的。&lt;/p&gt;  &lt;p&gt;这里首先定义了一个宏 BPF_FUNC，接受一个函数名 NAME 以及其他的任意参数。然后用这个宏声明了一 个 NAME 为 map_lookup_elem 的函数，经过宏展开后会变成 BPF_FUNC_map_lookup_elem 枚举值，后者以辅助函数的形式定义在 uapi/linux/bpf.h。&lt;/p&gt;  &lt;p&gt;当随后这个程序被加载到内核时，校验器会检查传入的参数是否是期望的类型，如果是，就将辅助函数调用重新指向（re-points）某个真正的函数调用。另外，map_lookup_elem() 还展示了 map 是如何传递给 BPF 辅助函数的。这里，maps section 中的 &amp;amp;acc_map 作为第一个参数传递给 map_lookup_elem()。&lt;/p&gt;  &lt;p&gt;由于程序中定义的数组 map （array map）是全局的，因此计数时需要使用原子操作，这里 是使用了 lock_xadd()。LLVM 将 __sync_fetch_and_add() 作为一个内置函数映射到 BPF 原子加指令，即 BPF_STX | BPF_XADD | BPF_W（for word sizes）。&lt;/p&gt;  &lt;p&gt;另外，struct bpf_elf_map 中的 .pinning 字段初始化为 PIN_GLOBAL_NS，这意味 着 tc 会将这个 map 作为一个节点（node）钉（pin）到 BPF 伪文件系统。默认情况下， 这个变量 acc_map 将被钉到 /sys/fs/bpf/tc/globals/acc_map。&lt;/p&gt;  &lt;p&gt;如果指定的是 PIN_GLOBAL_NS，那 map 会被放到 /sys/fs/bpf/tc/globals/。globals 是一个跨对象文件的全局命名空间。&lt;/p&gt;  &lt;p&gt;如果指定的是 PIN_OBJECT_NS，tc 将会为对象文件创建一个它的本地目录（local to the object file）。例如，只要指定了 PIN_OBJECT_NS，不同的 C 文件都可以像上 面一样定义各自的 acc_map。在这种情况下，这个 map 会在不同 BPF 程序之间共享。&lt;/p&gt;  &lt;p&gt;PIN_NONE 表示 map 不会作为节点（node）钉（pin）到 BPF 文件系统，因此当 tc 退 出时这个 map 就无法从用户空间访问了。同时，这还意味着独立的 tc 命令会创建出独 立的 map 实例，因此后执行的 tc 命令无法用这个 map 名字找到之前被钉住的 map。在路径 /sys/fs/bpf/tc/globals/acc_map 中，map 名是 acc_map。&lt;/p&gt;  &lt;p&gt;因此，在加载 ingress 程序时，tc 会先查找这个 map 在 BPF 文件系统中是否存在，不存在就创建一个。创建成功后，map 会被钉（pin）到 BPF 文件系统，因此当 egress 程 序通过 tc 加载之后，它就会发现这个 map 存在了，接下来会复用这个 map 而不是再创建 一个新的。在 map 存在的情况下，加载器还会确保 map 的属性（properties）是匹配的， 例如 key/value 大小等等。&lt;/p&gt;  &lt;p&gt;就像 tc 可以从同一 map 获取数据一样，第三方应用也可以用 bpf 系统调用中的 BPF_OBJ_GET 命令创建一个指向某个 map 实例的新文件描述符，然后用这个描述 符来查看/更新/删除 map 中的数据。&lt;/p&gt;  &lt;p&gt;通过 clang 编译和 iproute2 加载：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ clang -O2 -Wall -target bpf -c tc-example.c -o tc-example.o    &lt;br /&gt;    &lt;br /&gt;$ tc qdisc add dev em1 clsact    &lt;br /&gt;$ tc filter add dev em1 ingress bpf da obj tc-example.o sec ingress    &lt;br /&gt;$ tc filter add dev em1 egress bpf da obj tc-example.o sec egress    &lt;br /&gt;    &lt;br /&gt;$ tc filter show dev em1 ingress    &lt;br /&gt;filter protocol all pref 49152 bpf    &lt;br /&gt;filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[ingress] direct-action id 1 tag c5f7825e5dac396f    &lt;br /&gt;    &lt;br /&gt;$ tc filter show dev em1 egress    &lt;br /&gt;filter protocol all pref 49152 bpf    &lt;br /&gt;filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[egress] direct-action id 2 tag b2fd5adc0f262714    &lt;br /&gt;    &lt;br /&gt;$ mount | grep bpf    &lt;br /&gt;sysfs on /sys/fs/bpf type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)    &lt;br /&gt;bpf on /sys/fs/bpf type bpf (rw,relatime,mode=0700)    &lt;br /&gt;    &lt;br /&gt;$ tree /sys/fs/bpf/    &lt;br /&gt;/sys/fs/bpf/    &lt;br /&gt;+-- ip -&amp;gt; /sys/fs/bpf/tc/    &lt;br /&gt;+-- tc    &lt;br /&gt;|   +-- globals    &lt;br /&gt;|       +-- acc_map    &lt;br /&gt;+-- xdp -&amp;gt; /sys/fs/bpf/tc/    &lt;br /&gt;    &lt;br /&gt;4 directories, 1 file&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;以上步骤指向完成后，当包经过 em 设备时，BPF map 中的计数器就会递增。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;不允许全局变量&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;出于第 1 条中提到的原因（只支持 BPF maps 重定位，译者注），BPF 不能使用全局变量 ，而常规 C 程序中是可以的。&lt;/p&gt;  &lt;p&gt;但是，我们有间接的方式实现全局变量的效果：BPF 程序可以使用一个 BPF_MAP_TYPE_PERCPU_ARRAY 类型的、只有一个槽（slot）的、可以存放任意类型数据（ arbitrary value size）的 BPF map。&lt;/p&gt;  &lt;p&gt;这可以实现全局变量的效果原因是，BPF 程序在执行期间不会被内核抢占，因此可以用单个 map entry 作为一个 scratch buffer 使用，存储临时数据，例如扩展 BPF 栈的限制（512 字节）。这种方式在尾调用中也是可 以工作的，因为尾调用执行期间也不会被抢占。&lt;/p&gt;  &lt;p&gt;另外，如果要在不同次 BPF 程序执行之间保持状态，使用常规的 BPF map 就可以了。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;不支持常量字符串或数组（const strings or arrays）&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;BPF C 程序中不允许定义 const 字符串或其他数组，原因和第 1 点及第 3 点一样，即 ，ELF 文件中生成的 重定位项（relocation entries）会被加载器拒绝，因为不符合加载器的 ABI（加载器也无法修复这些重定位项，因为这需要对已经编译好的 BPF 序列进行大范围的重写）。&lt;/p&gt;  &lt;p&gt;将来 LLVM 可能会检测这种情况，提前将错误抛给用户。现在可以用下面的辅助函数来作为短期解决方式（work around）：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);    &lt;br /&gt;    &lt;br /&gt;#ifndef printk    &lt;br /&gt;# define printk(fmt, ...)                                      \    &lt;br /&gt;    ({                                                         \    &lt;br /&gt;        char ____fmt[] = fmt;                                  \    &lt;br /&gt;        trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \    &lt;br /&gt;    })    &lt;br /&gt;#endif&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;有了上面的定义，程序就可以自然地使用这个宏，例如 printk(&amp;quot;skb len:%u\n&amp;quot;, skb-&amp;gt;len);。 输出会写到 trace pipe，用 tc exec bpf dbg 命令可以获取这些打印的消息。&lt;/p&gt;  &lt;p&gt;不过，使用 trace_printk() 辅助函数也有一些不足，因此不建议在生产环境使用。每次调用这个辅助函数时，常量字符串（例如 &amp;quot;skb len:%u\n&amp;quot;）都需要加载到 BPF 栈，但这个辅助函数最多只能接受 5 个参数，因此使用这个函数输出信息时只能传递三个参数。&lt;/p&gt;  &lt;p&gt;因此，虽然这个辅助函数对快速调试很有用，但（对于网络程序）还是推荐使用 skb_event_output() 或 xdp_event_output() 辅助函数。这两个函数接受从 BPF 程序传递自定义的结构体类型参数，然后将参数以及可选的包数据（packet sample）放到 perf event ring buffer。&lt;/p&gt;  &lt;p&gt;例如，Cilium monitor 利用这些辅助函数实现了一个调试框架，以及在发现违反网络策略时发出通知等功能。这些函数通过一个无锁的、内存映射的、 per-CPU 的 perf ring buffer 传递数据，因此要远快于 trace_printk()。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;使用 LLVM 内置的函数做内存操作&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;因为 BPF 程序除了调用 BPF 辅助函数之外无法执行任何函数调用，因此常规的库代码必须 实现为内联函数。另外，LLVM 也提供了一些可以用于特定大小（这里是 n）的内置函数 ，这些函数永远都会被内联：&lt;/p&gt;  &lt;pre&gt;#ifndef memset   &lt;br /&gt;# define memset(dest, chr, n)   __builtin_memset((dest), (chr), (n))   &lt;br /&gt;#endif   &lt;br /&gt;   &lt;br /&gt;#ifndef memcpy   &lt;br /&gt;# define memcpy(dest, src, n)   __builtin_memcpy((dest), (src), (n))   &lt;br /&gt;#endif   &lt;br /&gt;   &lt;br /&gt;#ifndef memmove   &lt;br /&gt;# define memmove(dest, src, n)  __builtin_memmove((dest), (src), (n))   &lt;br /&gt;#endif&lt;/pre&gt;  &lt;p&gt;LLVM 后端中的某个问题会导致内置的 memcmp() 有某些边界场景下无法内联，因此在这个问题解决之前不推荐使用这个函数。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;（目前还）不支持循环&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;内核中的 BPF 校验器除了对其他的控制流进行图验证（graph validation）之外，还会对所有程序路径执行深度优先搜索（depth first search），确保其中不存在循环。这样做的目的是确保程序永远会结束。&lt;/p&gt;  &lt;p&gt;但可以使用 #pragma unroll 指令实现常量的、不超过一定上限的循环。下面是一个例子：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;#pragma unroll    &lt;br /&gt;    for (i = 0; i &amp;lt; IPV6_MAX_HEADERS; i++) {    &lt;br /&gt;        switch (nh) {    &lt;br /&gt;        case NEXTHDR_NONE:    &lt;br /&gt;            return DROP_INVALID_EXTHDR;    &lt;br /&gt;        case NEXTHDR_FRAGMENT:    &lt;br /&gt;            return DROP_FRAG_NOSUPPORT;    &lt;br /&gt;        case NEXTHDR_HOP:    &lt;br /&gt;        case NEXTHDR_ROUTING:    &lt;br /&gt;        case NEXTHDR_AUTH:    &lt;br /&gt;        case NEXTHDR_DEST:    &lt;br /&gt;            if (skb_load_bytes(skb, l3_off + len, &amp;amp;opthdr, sizeof(opthdr)) &amp;lt; 0)    &lt;br /&gt;                return DROP_INVALID;    &lt;br /&gt;    &lt;br /&gt;            nh = opthdr.nexthdr;    &lt;br /&gt;            if (nh == NEXTHDR_AUTH)    &lt;br /&gt;                len += ipv6_authlen(&amp;amp;opthdr);    &lt;br /&gt;            else    &lt;br /&gt;                len += ipv6_optlen(&amp;amp;opthdr);    &lt;br /&gt;            break;    &lt;br /&gt;        default:    &lt;br /&gt;            *nexthdr = nh;    &lt;br /&gt;            return len;    &lt;br /&gt;        }    &lt;br /&gt;    }&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;另外一种实现循环的方式是：用一个 BPF_MAP_TYPE_PERCPU_ARRAY map 作为本地 scratch space（存储空间），然后用尾调用的方式调用函数自身。虽然这种方式更加动态，但目前最大只支持 32 层嵌套调用。&lt;/p&gt;  &lt;p&gt;将来 BPF 可能会提供一些更加原生、但有一定限制的循环。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;尾调用的用途&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;尾调用能够从一个程序调到另一个程序，提供了在运行时（runtime）原子地改变程序行为的灵活性。为了选择要跳转到哪个程序，尾调用使用了程序数组 map（ BPF_MAP_TYPE_PROG_ARRAY），将 map 及其索引（index）传递给将要跳转到的程序。跳转动作一旦完成，就没有办法返回到原来的程序；但如果给定的 map 索引中没有程序（无法跳转），执行会继续在原来的程序中执行。&lt;/p&gt;  &lt;p&gt;例如，可以用尾调用实现解析器的不同阶段，可以在运行时（runtime）更新这些阶段的新解析特性。&lt;/p&gt;  &lt;p&gt;尾调用的另一个用处是事件通知，例如，Cilium 可以在运行时（runtime）开启或关闭丢弃包的通知（packet drop notifications），其中对 skb_event_output() 的调用就是发 生在被尾调用的程序中。&lt;/p&gt;  &lt;p&gt;因此，在常规情况下，执行的永远是从上到下的路径（ fall-through path），当某个程序被加入到相关的 map 索引之后，程序就会解析元数据， 触发向用户空间守护进程（user space daemon）发送事件通知。&lt;/p&gt;  &lt;p&gt;程序数组 map 非常灵活， map 中每个索引对应的程序可以实现各自的动作（actions）。例如，attach 到 tc 或 XDP 的 root 程序执行初始的、跳转到程序数组 map 中索引为 0 的程序，然后执行流量抽样（traffic sampling），然后跳转到索引为 1 的程序，在那个程序中应用防火墙策略，然后就可以决定是丢地包还是将其送到索引为 2 的程序中继续处理，在后者中，可能可能会被 mangle 然后再次通过某个接口发送出去。&lt;/p&gt;  &lt;p&gt;在程序数据 map 之中是可以随意跳转的。当达到尾调用的最大调用深度时，内核最终会执行 fall-through path。&lt;/p&gt;  &lt;p&gt;一个使用尾调用的最小程序示例：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;[...]    &lt;br /&gt;    &lt;br /&gt;#ifndef __stringify    &lt;br /&gt;# define __stringify(X)   #X    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;#ifndef __section    &lt;br /&gt;# define __section(NAME)                  \    &lt;br /&gt;   __attribute__((section(NAME), used))    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;#ifndef __section_tail    &lt;br /&gt;# define __section_tail(ID, KEY)          \    &lt;br /&gt;   __section(__stringify(ID) &amp;quot;/&amp;quot; __stringify(KEY))    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;#ifndef BPF_FUNC    &lt;br /&gt;# define BPF_FUNC(NAME, ...)              \    &lt;br /&gt;   (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;#define BPF_JMP_MAP_ID   1    &lt;br /&gt;    &lt;br /&gt;static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,    &lt;br /&gt;                     uint32_t index);    &lt;br /&gt;    &lt;br /&gt;struct bpf_elf_map jmp_map __section(&amp;quot;maps&amp;quot;) = {    &lt;br /&gt;    .type           = BPF_MAP_TYPE_PROG_ARRAY,    &lt;br /&gt;    .id             = BPF_JMP_MAP_ID,    &lt;br /&gt;    .size_key       = sizeof(uint32_t),    &lt;br /&gt;    .size_value     = sizeof(uint32_t),    &lt;br /&gt;    .pinning        = PIN_GLOBAL_NS,    &lt;br /&gt;    .max_elem       = 1,    &lt;br /&gt;};    &lt;br /&gt;    &lt;br /&gt;__section_tail(JMP_MAP_ID, 0)    &lt;br /&gt;int looper(struct __sk_buff *skb)    &lt;br /&gt;{    &lt;br /&gt;    printk(&amp;quot;skb cb: %u\n&amp;quot;, skb-&amp;gt;cb[0]++);    &lt;br /&gt;    tail_call(skb, &amp;amp;jmp_map, 0);    &lt;br /&gt;    return TC_ACT_OK;    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;__section(&amp;quot;prog&amp;quot;)    &lt;br /&gt;int entry(struct __sk_buff *skb)    &lt;br /&gt;{    &lt;br /&gt;    skb-&amp;gt;cb[0] = 0;    &lt;br /&gt;    tail_call(skb, &amp;amp;jmp_map, 0);    &lt;br /&gt;    return TC_ACT_OK;    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;char __license[] __section(&amp;quot;license&amp;quot;) = &amp;quot;GPL&amp;quot;;&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;加载这个示例程序时，tc 会创建其中的程序数组（jmp_map 变量），并将其钉（pin）到 BPF 文件系统中全局命名空间下名为的 jump_map 位置。而且，iproute2 中的 BPF ELF 加载器也会识别出标记为 __section_tail() 的 section。 &lt;/p&gt;  &lt;p&gt;jmp_map 的 id 字段会 跟__section_tail() 中的 id 字段（这里初始化为常量 JMP_MAP_ID）做匹配，因此程 序能加载到用户指定的索引（位置），在上面的例子中这个索引是 0。&lt;/p&gt;  &lt;p&gt;然后，所有的尾调用 section 将会被 iproute2 加载器处理，关联到 map 中。这个机制并不是 tc 特有的， iproute2 支持的其他 BPF 程序类型（例如 XDP、lwt）也适用。&lt;/p&gt;  &lt;p&gt;生成的 elf 包含 section headers，描述 map id 和 map 内的条目：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ llvm-objdump -S --no-show-raw-insn prog_array.o | less    &lt;br /&gt;prog_array.o:   file format ELF64-BPF    &lt;br /&gt;    &lt;br /&gt;Disassembly of section 1/0:    &lt;br /&gt;looper:    &lt;br /&gt;       0:       r6 = r1    &lt;br /&gt;       1:       r2 = *(u32 *)(r6 + 48)    &lt;br /&gt;       2:       r1 = r2    &lt;br /&gt;       3:       r1 += 1    &lt;br /&gt;       4:       *(u32 *)(r6 + 48) = r1    &lt;br /&gt;       5:       r1 = 0 ll    &lt;br /&gt;       7:       call -1    &lt;br /&gt;       8:       r1 = r6    &lt;br /&gt;       9:       r2 = 0 ll    &lt;br /&gt;      11:       r3 = 0    &lt;br /&gt;      12:       call 12    &lt;br /&gt;      13:       r0 = 0    &lt;br /&gt;      14:       exit    &lt;br /&gt;Disassembly of section prog:    &lt;br /&gt;entry:    &lt;br /&gt;       0:       r2 = 0    &lt;br /&gt;       1:       *(u32 *)(r1 + 48) = r2    &lt;br /&gt;       2:       r2 = 0 ll    &lt;br /&gt;       4:       r3 = 0    &lt;br /&gt;       5:       call 12    &lt;br /&gt;       6:       r0 = 0    &lt;br /&gt;       7:       exi&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;在这个例子中，section 1/0 表示 looper() 函数位于 map 1 中，在 map 1 内的 位置是 0。&lt;/p&gt;  &lt;p&gt;被钉住（pinned）map 可以被用户空间应用（例如 Cilium daemon）读取，也可以被 tc 本 身读取，因为 tc 可能会用新的程序替换原来的程序，此时可能需要读取 map 内容。更新是原子的。&lt;/p&gt;  &lt;p&gt;tc 执行尾调用 map 更新（tail call map updates）的例子：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ tc exec bpf graft m:globals/jmp_map key 0 obj new.o sec foo&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;如果 iproute2 需要更新被钉住（pinned）的程序数组，可以使用 graft 命令。上面的 例子中指向的是 globals/jmp_map，那 tc 将会用一个新程序更新位于 index/key 为 0 的 map， 这个新程序位于对象文件 new.o 中的 foo section。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;BPF 最大栈空间 512 字节&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;BPF 程序的最大栈空间是 512 字节，在使用 C 语言实现 BPF 程序时需要考虑到这一点。但正如在第 3 点中提到的，可以通过一个只有一条记录（single entry）的 BPF_MAP_TYPE_PERCPU_ARRAY map 来绕过这限制，增大 scratch buffer 空间。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;尝试使用 BPF 内联汇编&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;LLVM 6.0 以后支持 BPF 内联汇编，在某些场景下可能会用到。下面这个玩具示例程序（ 没有实际意义）展示了一个 64 位原子加操作。&lt;/p&gt;  &lt;p&gt;由于文档不足，要获取更多信息和例子，目前可能只能参考 LLVM 源码中的 lib/Target/BPF/BPFInstrInfo.td 以及 test/CodeGen/BPF/。测试代码：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;#include &amp;lt;linux/bpf.h&amp;gt;    &lt;br /&gt;    &lt;br /&gt;#ifndef __section    &lt;br /&gt;# define __section(NAME)                  \    &lt;br /&gt;   __attribute__((section(NAME), used))    &lt;br /&gt;#endif    &lt;br /&gt;    &lt;br /&gt;__section(&amp;quot;prog&amp;quot;)    &lt;br /&gt;int xdp_test(struct xdp_md *ctx)    &lt;br /&gt;{    &lt;br /&gt;    __u64 a = 2, b = 3, *c = &amp;amp;a;    &lt;br /&gt;    /* just a toy xadd example to show the syntax */    &lt;br /&gt;    asm volatile(&amp;quot;lock *(u64 *)(%0+0) += %1&amp;quot; : &amp;quot;=r&amp;quot;(c) : &amp;quot;r&amp;quot;(b), &amp;quot;0&amp;quot;(c));    &lt;br /&gt;    return a;    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;char __license[] __section(&amp;quot;license&amp;quot;) = &amp;quot;GPL&amp;quot;;&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;上面的程序会被编译成下面的 BPF 指令序列：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;Verifier analysis:    &lt;br /&gt;    &lt;br /&gt;0: (b7) r1 = 2    &lt;br /&gt;1: (7b) *(u64 *)(r10 -8) = r1    &lt;br /&gt;2: (b7) r1 = 3    &lt;br /&gt;3: (bf) r2 = r10    &lt;br /&gt;4: (07) r2 += -8    &lt;br /&gt;5: (db) lock *(u64 *)(r2 +0) += r1    &lt;br /&gt;6: (79) r0 = *(u64 *)(r10 -8)    &lt;br /&gt;7: (95) exit    &lt;br /&gt;processed 8 insns (limit 131072), stack depth 8&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;用 #pragma pack 禁止结构体填充（struct padding）&lt;/strong&gt;  &lt;p&gt;现代编译器默认会对数据结构进行内存对齐（align），以实现更加高效的访问。结构体成员会被对齐到数倍于其自身大小的内存位置，不足的部分会进行填充（padding），因此结构体最终的大小可能会比预想中大。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;struct called_info {    &lt;br /&gt;    u64 start;  // 8-byte    &lt;br /&gt;    u64 end;    // 8-byte    &lt;br /&gt;    u32 sector; // 4-byte    &lt;br /&gt;}; // size of 20-byte ?    &lt;br /&gt;    &lt;br /&gt;printf(&amp;quot;size of %d-byte\n&amp;quot;, sizeof(struct called_info)); // size of 24-byte    &lt;br /&gt;    &lt;br /&gt;// Actual compiled composition of struct called_info    &lt;br /&gt;// 0x0(0)                   0x8(8)    &lt;br /&gt;//  ↓________________________↓    &lt;br /&gt;//  |        start (8)       |    &lt;br /&gt;//  |________________________|    &lt;br /&gt;//  |         end  (8)       |    &lt;br /&gt;//  |________________________|    &lt;br /&gt;//  |  sector(4) |  PADDING  | &amp;lt;= address aligned to 8    &lt;br /&gt;//  |____________|___________|     with 4-byte PADDING.&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;内核中的 BPF 校验器会检查栈边界（stack boundary），BPF 程序不会访问栈边界外的空间，或者是未初始化的栈空间。如果将结构体中填充出来的内存区域作为一个 map 值进行 访问，那调用 bpf_prog_load() 时就会报 invalid indirect read from stack 错误。&lt;/p&gt;  &lt;p&gt;示例代码：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;struct called_info {    &lt;br /&gt;    u64 start;    &lt;br /&gt;    u64 end;    &lt;br /&gt;    u32 sector;    &lt;br /&gt;};    &lt;br /&gt;    &lt;br /&gt;struct bpf_map_def SEC(&amp;quot;maps&amp;quot;) called_info_map = {    &lt;br /&gt;    .type = BPF_MAP_TYPE_HASH,    &lt;br /&gt;    .key_size = sizeof(long),    &lt;br /&gt;    .value_size = sizeof(struct called_info),    &lt;br /&gt;    .max_entries = 4096,    &lt;br /&gt;};    &lt;br /&gt;    &lt;br /&gt;SEC(&amp;quot;kprobe/submit_bio&amp;quot;)    &lt;br /&gt;int submit_bio_entry(struct pt_regs *ctx)    &lt;br /&gt;{    &lt;br /&gt;    char fmt[] = &amp;quot;submit_bio(bio=0x%lx) called: %llu\n&amp;quot;;    &lt;br /&gt;    u64 start_time = bpf_ktime_get_ns();    &lt;br /&gt;    long bio_ptr = PT_REGS_PARM1(ctx);    &lt;br /&gt;    struct called_info called_info = {    &lt;br /&gt;            .start = start_time,    &lt;br /&gt;            .end = 0,    &lt;br /&gt;            .bi_sector = 0    &lt;br /&gt;    };    &lt;br /&gt;    &lt;br /&gt;    bpf_map_update_elem(&amp;amp;called_info_map, &amp;amp;bio_ptr, &amp;amp;called_info, BPF_ANY);    &lt;br /&gt;    bpf_trace_printk(fmt, sizeof(fmt), bio_ptr, start_time);    &lt;br /&gt;    return 0;    &lt;br /&gt;}    &lt;br /&gt;    &lt;br /&gt;// On bpf_load_program    &lt;br /&gt;bpf_load_program() err=13    &lt;br /&gt;0: (bf) r6 = r1    &lt;br /&gt;...    &lt;br /&gt;19: (b7) r1 = 0    &lt;br /&gt;20: (7b) *(u64 *)(r10 -72) = r1    &lt;br /&gt;21: (7b) *(u64 *)(r10 -80) = r7    &lt;br /&gt;22: (63) *(u32 *)(r10 -64) = r1    &lt;br /&gt;...    &lt;br /&gt;30: (85) call bpf_map_update_elem#2    &lt;br /&gt;invalid indirect read from stack off -80+20 size 24&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;在 bpf_prog_load() 中会调用 BPF 校验器的 bpf_check() 函数，后者会调用 check_func_arg() -&amp;gt; check_stack_boundary() 来检查栈边界。&lt;/p&gt;  &lt;p&gt;从上面的错误可以看出 ，struct called_info 被编译成 24 字节，错误信息提示从 +20 位置读取数据是“非法的间接读取”（invalid indirect read）。从我们更前面给出的内存布局图中可以看到， 地址 0x14(20) 是填充（PADDING）开始的地方。这里再次画出内存布局图以方便对比：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;// Actual compiled composition of struct called_info    &lt;br /&gt;// 0x10(16)    0x14(20)    0x18(24)    &lt;br /&gt;//  ↓____________↓___________↓    &lt;br /&gt;//  |  sector(4) |  PADDING  | &amp;lt;= address aligned to 8    &lt;br /&gt;//  |____________|___________|     with 4-byte PADDING.&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;check_stack_boundary() 会遍历每一个从开始指针出发的 access_size (24) 字节，确保它们位于栈边界内部，并且栈内的所有元素都初始化了。因此填充的部分是不允许使用的，所以报了 “invalid indirect read from stack” 错误。要避免这种错误，需要将结构体中的填充去掉。这是通过 #pragma pack(n) 原语实现的：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;#pragma pack(4)    &lt;br /&gt;struct called_info {    &lt;br /&gt;    u64 start;  // 8-byte    &lt;br /&gt;    u64 end;    // 8-byte    &lt;br /&gt;    u32 sector; // 4-byte    &lt;br /&gt;}; // size of 20-byte ?    &lt;br /&gt;    &lt;br /&gt;printf(&amp;quot;size of %d-byte\n&amp;quot;, sizeof(struct called_info)); // size of 20-byte    &lt;br /&gt;    &lt;br /&gt;// Actual compiled composition of packed struct called_info    &lt;br /&gt;// 0x0(0)                   0x8(8)    &lt;br /&gt;//  ↓________________________↓    &lt;br /&gt;//  |        start (8)       |    &lt;br /&gt;//  |________________________|    &lt;br /&gt;//  |         end  (8)       |    &lt;br /&gt;//  |________________________|    &lt;br /&gt;//  |  sector(4) |             &amp;lt;= address aligned to 4    &lt;br /&gt;//  |____________|                 with no PADDING.&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;在 struct called_info 前面加上 #pragma pack(4) 之后，编译器会以 4 字节为单位进行对齐。上面的图可以看到，这个结构体现在已经变成 20 字节大小，没有填充了。&lt;/p&gt;  &lt;p&gt;但是，去掉填充也是有弊端的。例如，编译器产生的代码没有原来优化的好。去掉填充之后 ，处理器访问结构体时触发的是非对齐访问（unaligned access），可能会导致性能下降。并且，某些架构上的校验器可能会直接拒绝非对齐访问。&lt;/p&gt;  &lt;p&gt;不过，我们也有一种方式可以避免产生自动填充：手动填充。我们简单地在结构体中加入一 个 u32 pad 成员来显式填充，这样既避免了自动填充的问题，又解决了非对齐访问的问题。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;struct called_info {    &lt;br /&gt;    u64 start;  // 8-byte    &lt;br /&gt;    u64 end;    // 8-byte    &lt;br /&gt;    u32 sector; // 4-byte    &lt;br /&gt;    u32 pad;    // 4-byte    &lt;br /&gt;}; // size of 24-byte ?    &lt;br /&gt;    &lt;br /&gt;printf(&amp;quot;size of %d-byte\n&amp;quot;, sizeof(struct called_info)); // size of 24-byte    &lt;br /&gt;    &lt;br /&gt;// Actual compiled composition of struct called_info with explicit padding    &lt;br /&gt;// 0x0(0)                   0x8(8)    &lt;br /&gt;//  ↓________________________↓    &lt;br /&gt;//  |        start (8)       |    &lt;br /&gt;//  |________________________|    &lt;br /&gt;//  |         end  (8)       |    &lt;br /&gt;//  |________________________|    &lt;br /&gt;//  |  sector(4) |  pad (4)  | &amp;lt;= address aligned to 8    &lt;br /&gt;//  |____________|___________|     with explicit PADDING.&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;通过未验证的引用（invalidated references）访问包数据&lt;/strong&gt;  &lt;p&gt;某些网络相关的 BPF 辅助函数，例如 bpf_skb_store_bytes，可能会修改包的大小。校验器无法跟踪这类改动，因此它会将所有之前对包数据的引用都视为过期的（未验证的） 。因此，为避免程序被校验器拒绝，在访问数据之外需要先更新相应的引用。&lt;/p&gt;  &lt;p&gt;来看下面的例子：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;struct iphdr *ip4 = (struct iphdr *) skb-&amp;gt;data + ETH_HLEN;    &lt;br /&gt;    &lt;br /&gt;skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &amp;amp;new_saddr, 4, 0);    &lt;br /&gt;    &lt;br /&gt;if (ip4-&amp;gt;protocol == IPPROTO_TCP) {    &lt;br /&gt;    // do something    &lt;br /&gt;}&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;校验器会拒绝这段代码，因为它认为在 skb_store_bytes 执行之后，引用 ip4-&amp;gt;protocol 是未验证的（invalidated）：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;R1=pkt_end(id=0,off=0,imm=0) R2=pkt(id=0,off=34,r=34,imm=0) R3=inv0    &lt;br /&gt;R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff))    &lt;br /&gt;R8=inv4294967162 R9=pkt(id=0,off=0,r=34,imm=0) R10=fp0,call_-1    &lt;br /&gt;...    &lt;br /&gt;18: (85) call bpf_skb_store_bytes#9    &lt;br /&gt;19: (7b) *(u64 *)(r10 -56) = r7    &lt;br /&gt;R0=inv(id=0) R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=2,var_off=(0x0; 0x3))    &lt;br /&gt;R8=inv4294967162 R9=inv(id=0) R10=fp0,call_-1 fp-48=mmmm???? fp-56=mmmmmmmm    &lt;br /&gt;21: (61) r1 = *(u32 *)(r9 +23)    &lt;br /&gt;R9 invalid mem access &amp;apos;inv&amp;apos;&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;要解决这个问题，必须更新（重新计算） ip4 的地址：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;struct iphdr *ip4 = (struct iphdr *) skb-&amp;gt;data + ETH_HLEN;    &lt;br /&gt;    &lt;br /&gt;skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &amp;amp;new_saddr, 4, 0);    &lt;br /&gt;    &lt;br /&gt;ip4 = (struct iphdr *) skb-&amp;gt;data + ETH_HLEN;    &lt;br /&gt;    &lt;br /&gt;if (ip4-&amp;gt;protocol == IPPROTO_TCP) {    &lt;br /&gt;    // do something    &lt;br /&gt;}&lt;/p&gt;&lt;/pre&gt;  &lt;em&gt;   &lt;br /&gt;—&lt;/em&gt;  &lt;em&gt;   &lt;strong&gt;    &lt;strong&gt; &lt;/strong&gt;&lt;/strong&gt;&lt;/em&gt;  &lt;em&gt;   &lt;strong&gt;3&lt;/strong&gt;&lt;/em&gt;   &lt;em&gt;—&lt;/em&gt;  &lt;br /&gt;  &lt;strong&gt;开发工具链&lt;/strong&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;libbpf&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;bpftool&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;bpftool 是查看和调试 BPF 程序的主要工具。它随内核一起开发，在内核中的路径是 tools/bpf/bpftool/。&lt;/p&gt;  &lt;p&gt;这个工具可以完成：&lt;/p&gt;  &lt;ol&gt;   &lt;li&gt;    &lt;p&gt;dump 当前已经加载到系统中的所有 BPF 程序和 map&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;列出和指定程序相关的所有 BPF map&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;dump 整个 map 中的 key/value 对&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;查看、更新、删除特定 key&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;查看给定 key 的相邻 key（neighbor key）&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;  &lt;p&gt;要执行这些操作可以指定 BPF 程序、map ID，或者指定 BPF 文件系统中程序或 map 的位 置。另外，这个工具还提供了将 map 或程序钉（pin）到 BPF 文件系统的功能。&lt;/p&gt;  &lt;p&gt;查看系统当前已经加载的 BPF 程序：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog    &lt;br /&gt;398: sched_cls  tag 56207908be8ad877    &lt;br /&gt;   loaded_at Apr 09/16:24  uid 0    &lt;br /&gt;   xlated 8800B  jited 6184B  memlock 12288B  map_ids 18,5,17,14    &lt;br /&gt;399: sched_cls  tag abc95fb4835a6ec9    &lt;br /&gt;   loaded_at Apr 09/16:24  uid 0    &lt;br /&gt;   xlated 344B  jited 223B  memlock 4096B  map_ids 18    &lt;br /&gt;400: sched_cls  tag afd2e542b30ff3ec    &lt;br /&gt;   loaded_at Apr 09/16:24  uid 0    &lt;br /&gt;   xlated 1720B  jited 1001B  memlock 4096B  map_ids 17    &lt;br /&gt;401: sched_cls  tag 2dbbd74ee5d51cc8    &lt;br /&gt;   loaded_at Apr 09/16:24  uid 0    &lt;br /&gt;   xlated 3728B  jited 2099B  memlock 4096B  map_ids 17    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;类似地，查看所有的 active maps：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool map    &lt;br /&gt;5: hash  flags 0x0    &lt;br /&gt;    key 20B  value 112B  max_entries 65535  memlock 13111296B    &lt;br /&gt;6: hash  flags 0x0    &lt;br /&gt;    key 20B  value 20B  max_entries 65536  memlock 7344128B    &lt;br /&gt;7: hash  flags 0x0    &lt;br /&gt;    key 10B  value 16B  max_entries 8192  memlock 790528B    &lt;br /&gt;8: hash  flags 0x0    &lt;br /&gt;    key 22B  value 28B  max_entries 8192  memlock 987136B    &lt;br /&gt;9: hash  flags 0x0    &lt;br /&gt;    key 20B  value 8B  max_entries 512000  memlock 49352704B    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;bpftool 的每个命令都提供了以 json 格式打印的功能，在命令末尾指定 --json 就行了。另外，--pretty 会使得打印更加美观，看起来更清楚。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog --json --pretty&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;要 dump 特定 BPF 程序的 post-verifier BPF 指令镜像（instruction image），可以先 从查看一个具体程序开始，例如，查看 attach 到 tc ingress hook 上的程序：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ tc filter show dev cilium_host egress    &lt;br /&gt;filter protocol all pref 1 bpf chain 0    &lt;br /&gt;filter protocol all pref 1 bpf chain 0 handle 0x1 bpf_host.o:[from-netdev] \    &lt;br /&gt;                    direct-action not_in_hw id 406 tag e0362f5bd9163a0a jited&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;这个程序是从对象文件 bpf_host.o 加载来的，程序位于对象文件的 from-netdev section，程序 ID 为 406。基于以上信息 bpftool 可以提供一些关于这个程序的上层元数据：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog show id 406    &lt;br /&gt;406: sched_cls  tag e0362f5bd9163a0a    &lt;br /&gt;     loaded_at Apr 09/16:24  uid 0    &lt;br /&gt;     xlated 11144B  jited 7721B  memlock 12288B  map_ids 18,20,8,5,6,14&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;从上面的输出可以看到：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;程序 ID 为 406，类型是 sched_cls（BPF_PROG_TYPE_SCHED_CLS），有一个 tag 为 e0362f5bd9163a0a（指令序列的 SHA sum）&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;这个程序被 root uid 0 在 Apr 09/16:24 加载&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;BPF 指令序列有 11,144 bytes 长，JIT 之后的镜像有 7,721 bytes&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;程序自身（不包括 maps）占用了 12,288 bytes，这部分空间使用的是 uid 0 用户 的配额&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;BPF 程序使用了 ID 为 18、20 8 5 6 和 14 的 BPF map。可以用这些 ID 进一步 dump map 自身或相关信息&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;另外，bpftool 可以 dump 出运行中程序的 BPF 指令：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 406    &lt;br /&gt; 0: (b7) r7 = 0    &lt;br /&gt; 1: (63) *(u32 *)(r1 +60) = r7    &lt;br /&gt; 2: (63) *(u32 *)(r1 +56) = r7    &lt;br /&gt; 3: (63) *(u32 *)(r1 +52) = r7    &lt;br /&gt;[...]    &lt;br /&gt;47: (bf) r4 = r10    &lt;br /&gt;48: (07) r4 += -40    &lt;br /&gt;49: (79) r6 = *(u64 *)(r10 -104)    &lt;br /&gt;50: (bf) r1 = r6    &lt;br /&gt;51: (18) r2 = map[id:18]                    &amp;lt;-- BPF map id 18    &lt;br /&gt;53: (b7) r5 = 32    &lt;br /&gt;54: (85) call bpf_skb_event_output#5656112  &amp;lt;-- BPF helper call    &lt;br /&gt;55: (69) r1 = *(u16 *)(r6 +192)    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;如上面的输出所示，bpftool 将指令流中的 BPF map ID、BPF 辅助函数或其他 BPF 程序都 做了关联。&lt;/p&gt;  &lt;p&gt;和内核的 BPF 校验器一样，bpftool dump 指令流时复用了同一个使输出更美观的打印程序 （pretty-printer）。&lt;/p&gt;  &lt;p&gt;由于程序被 JIT，因此真正执行的是生成的 JIT 镜像（从上面 xlated 中的指令生成的 ），这些指令也可以通过 bpftool 查看：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump jited id 406    &lt;br /&gt; 0:        push   %rbp    &lt;br /&gt; 1:        mov    %rsp,%rbp    &lt;br /&gt; 4:        sub    $0x228,%rsp    &lt;br /&gt; b:        sub    $0x28,%rbp    &lt;br /&gt; f:        mov    %rbx,0x0(%rbp)    &lt;br /&gt;13:        mov    %r13,0x8(%rbp)    &lt;br /&gt;17:        mov    %r14,0x10(%rbp)    &lt;br /&gt;1b:        mov    %r15,0x18(%rbp)    &lt;br /&gt;1f:        xor    %eax,%eax    &lt;br /&gt;21:        mov    %rax,0x20(%rbp)    &lt;br /&gt;25:        mov    0x80(%rdi),%r9d    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;另外，还可以指定在输出中将反汇编之后的指令关联到 opcodes，这个功能主要对 BPF JIT 开发者比较有用：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump jited id 406 opcodes    &lt;br /&gt; 0:        push   %rbp    &lt;br /&gt;           55    &lt;br /&gt; 1:        mov    %rsp,%rbp    &lt;br /&gt;           48 89 e5    &lt;br /&gt; 4:        sub    $0x228,%rsp    &lt;br /&gt;           48 81 ec 28 02 00 00    &lt;br /&gt; b:        sub    $0x28,%rbp    &lt;br /&gt;           48 83 ed 28    &lt;br /&gt; f:        mov    %rbx,0x0(%rbp)    &lt;br /&gt;           48 89 5d 00    &lt;br /&gt;13:        mov    %r13,0x8(%rbp)    &lt;br /&gt;           4c 89 6d 08    &lt;br /&gt;17:        mov    %r14,0x10(%rbp)    &lt;br /&gt;           4c 89 75 10    &lt;br /&gt;1b:        mov    %r15,0x18(%rbp)    &lt;br /&gt;           4c 89 7d 18    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;同样，也可以将常规的 BPF 指令关联到 opcodes，有时在内核中进行调试时会比较有用：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 406 opcodes    &lt;br /&gt; 0: (b7) r7 = 0    &lt;br /&gt;    b7 07 00 00 00 00 00 00    &lt;br /&gt; 1: (63) *(u32 *)(r1 +60) = r7    &lt;br /&gt;    63 71 3c 00 00 00 00 00    &lt;br /&gt; 2: (63) *(u32 *)(r1 +56) = r7    &lt;br /&gt;    63 71 38 00 00 00 00 00    &lt;br /&gt; 3: (63) *(u32 *)(r1 +52) = r7    &lt;br /&gt;    63 71 34 00 00 00 00 00    &lt;br /&gt; 4: (63) *(u32 *)(r1 +48) = r7    &lt;br /&gt;    63 71 30 00 00 00 00 00    &lt;br /&gt; 5: (63) *(u32 *)(r1 +64) = r7    &lt;br /&gt;    63 71 40 00 00 00 00 00    &lt;br /&gt; [...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;此外，还可以用 graphviz 以可视化的方式展示程序的基本组成部分。bpftool 提供了一 个 visual dump 模式，这种模式下输出的不是 BPF xlated 指令文本，而是一张点图（ dot graph），后者可以转换成 png 格式的图片：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 406 visual &amp;amp;&amp;gt; output.dot    &lt;br /&gt;    &lt;br /&gt;$ dot -Tpng output.dot -o output.png&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;也可以用 dotty 打开生成的点图文件：dotty output.dot，bpf_host.o 程序的效果如 下图所示（一部分）：&lt;/p&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;注意，xlated 中 dump 出来的指令是经过校验器之后（post-verifier）的 BPF 指令镜 像，即和 BPF 解释器中执行的版本是一样的。&lt;/p&gt;  &lt;p&gt;在内核中，校验器会对 BPF 加载器提供的原始指令执行各种重新（rewrite）。一个例子就 是对辅助函数进行内联化（inlining）以提高运行时性能，下面是对一个哈希表查找的优化：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 3    &lt;br /&gt; 0: (b7) r1 = 2    &lt;br /&gt; 1: (63) *(u32 *)(r10 -4) = r1    &lt;br /&gt; 2: (bf) r2 = r10    &lt;br /&gt; 3: (07) r2 += -4    &lt;br /&gt; 4: (18) r1 = map[id:2]                      &amp;lt;-- BPF map id 2    &lt;br /&gt; 6: (85) call __htab_map_lookup_elem#77408   &amp;lt;-+ BPF helper inlined rewrite    &lt;br /&gt; 7: (15) if r0 == 0x0 goto pc+2                |    &lt;br /&gt; 8: (07) r0 += 56                              |    &lt;br /&gt; 9: (79) r0 = *(u64 *)(r0 +0)                &amp;lt;-+    &lt;br /&gt;10: (15) if r0 == 0x0 goto pc+24    &lt;br /&gt;11: (bf) r2 = r10    &lt;br /&gt;12: (07) r2 += -4    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;bpftool 通过 kallsyms 来对辅助函数或 BPF-to-BPF 调用进行关联。因此，确保 JIT 之 后的 BPF 程序暴露到了 kallsyms（bpf_jit_kallsyms），并且 kallsyms 地址是明确的 （否则调用显示的就是 call bpf_unspec#0）：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ echo 0 &amp;gt; /proc/sys/kernel/kptr_restrict    &lt;br /&gt;$ echo 1 &amp;gt; /proc/sys/net/core/bpf_jit_kallsyms&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;BPF-to-BPF 调用在解释器和 JIT 镜像中也做了关联。对于后者，子程序的 tag 会显示为 调用目标（call target）。在两种情况下，pc+2 都是调用目标的程序计数器偏置（ pc-relative offset），表示就是子程序的地址。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 1    &lt;br /&gt;0: (85) call pc+2#__bpf_prog_run_args32    &lt;br /&gt;1: (b7) r0 = 1    &lt;br /&gt;2: (95) exit    &lt;br /&gt;3: (b7) r0 = 2    &lt;br /&gt;4: (95) exit&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;对应的 JIT 版本：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 1    &lt;br /&gt;0: (85) call pc+2#bpf_prog_3b185187f1855c4c_F    &lt;br /&gt;1: (b7) r0 = 1    &lt;br /&gt;2: (95) exit    &lt;br /&gt;3: (b7) r0 = 2    &lt;br /&gt;4: (95) exit&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;在尾调用中，内核会将它们映射为同一个指令，但 bpftool 还是会将它们作为辅助函数进 行关联，以方便调试：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool prog dump xlated id 2    &lt;br /&gt;[...]    &lt;br /&gt;10: (b7) r2 = 8    &lt;br /&gt;11: (85) call bpf_trace_printk#-41312    &lt;br /&gt;12: (bf) r1 = r6    &lt;br /&gt;13: (18) r2 = map[id:1]    &lt;br /&gt;15: (b7) r3 = 0    &lt;br /&gt;16: (85) call bpf_tail_call#12    &lt;br /&gt;17: (b7) r1 = 42    &lt;br /&gt;18: (6b) *(u16 *)(r6 +46) = r1    &lt;br /&gt;19: (b7) r0 = 0    &lt;br /&gt;20: (95) exit    &lt;br /&gt;    &lt;br /&gt;$ bpftool map show id 1    &lt;br /&gt;1: prog_array  flags 0x0    &lt;br /&gt;      key 4B  value 4B  max_entries 1  memlock 4096B&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;map dump 子命令可以 dump 整个 map，它会遍历所有的 map 元素，输出 key/value。&lt;/p&gt;  &lt;p&gt;如果 map 中没有可用的 BTF 数据，那 key/value 会以十六进制格式输出：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ bpftool map dump id 5    &lt;br /&gt;key:    &lt;br /&gt;f0 0d 00 00 00 00 00 00  0a 66 00 00 00 00 8a d6    &lt;br /&gt;02 00 00 00    &lt;br /&gt;value:    &lt;br /&gt;00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;key:    &lt;br /&gt;0a 66 1c ee 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;01 00 00 00    &lt;br /&gt;value:    &lt;br /&gt;00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00    &lt;br /&gt;00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00    &lt;br /&gt;[...]    &lt;br /&gt;Found 6 elements&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;如果有 BTF 数据，map 就有了关于 key/value 结构体的调试信息。例如，BTF 信息加上 BPF map 以及 iproute2 中的 BPF_ANNOTATE_KV_PAIR() 会产生下面的输出（内核 selftests 中的 test_xdp_noinline.o）：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ cat tools/testing/selftests/bpf/test_xdp_noinline.c    &lt;br /&gt;  []    &lt;br /&gt;   struct ctl_value {    &lt;br /&gt;         union {    &lt;br /&gt;                 __u64 value;    &lt;br /&gt;                 __u32 ifindex;    &lt;br /&gt;                 __u8 mac[6];    &lt;br /&gt;         };    &lt;br /&gt;   };    &lt;br /&gt;    &lt;br /&gt;   struct bpf_map_def __attribute__ ((section(&amp;quot;maps&amp;quot;), used)) ctl_array = {    &lt;br /&gt;          .type  = BPF_MAP_TYPE_ARRAY,    &lt;br /&gt;          .key_size = sizeof(__u32),    &lt;br /&gt;          .value_size = sizeof(struct ctl_value),    &lt;br /&gt;          .max_entries = 16,    &lt;br /&gt;          .map_flags = 0,    &lt;br /&gt;   };    &lt;br /&gt;   BPF_ANNOTATE_KV_PAIR(ctl_array, __u32, struct ctl_value);    &lt;br /&gt;    &lt;br /&gt;   []&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;BPF_ANNOTATE_KV_PAIR() 宏强制每个 map-specific ELF section 包含一个空的 key/value，这样 iproute2 BPF 加载器可以将 BTF 数据关联到这个 section，因此在加载 map 时可用从 BTF 中选择响应的类型。&lt;/p&gt;  &lt;p&gt;使用 LLVM 编译，并使用 pahole 基于调试信息产生 BTF：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ clang [...] -O2 -target bpf -g -emit-llvm -c test_xdp_noinline.c -o - |    &lt;br /&gt;  llc -march=bpf -mcpu=probe -mattr=dwarfris -filetype=obj -o test_xdp_noinline.o    &lt;br /&gt;    &lt;br /&gt;$ pahole -J test_xdp_noinline.o&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;加载到内核，然后使用 bpftool dump 这个 map：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ ip -force link set dev lo xdp obj test_xdp_noinline.o sec xdp-test    &lt;br /&gt;$ ip a    &lt;br /&gt;1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 xdpgeneric/id:227 qdisc noqueue state UNKNOWN group default qlen 1000    &lt;br /&gt;    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00    &lt;br /&gt;    inet 127.0.0.1/8 scope host lo    &lt;br /&gt;       valid_lft forever preferred_lft forever    &lt;br /&gt;    inet6 ::1/128 scope host    &lt;br /&gt;       valid_lft forever preferred_lft forever    &lt;br /&gt;[...]    &lt;br /&gt;    &lt;br /&gt;$ bpftool prog show id 227    &lt;br /&gt;227: xdp  tag a85e060c275c5616  gpl    &lt;br /&gt;    loaded_at 2018-07-17T14:41:29+0000  uid 0    &lt;br /&gt;    xlated 8152B  not jited  memlock 12288B  map_ids 381,385,386,382,384,383    &lt;br /&gt;    &lt;br /&gt;$ bpftool map dump id 386    &lt;br /&gt; [{    &lt;br /&gt;      &amp;quot;key&amp;quot;: 0,    &lt;br /&gt;      &amp;quot;value&amp;quot;: {    &lt;br /&gt;          &amp;quot;&amp;quot;: {    &lt;br /&gt;              &amp;quot;value&amp;quot;: 0,    &lt;br /&gt;              &amp;quot;ifindex&amp;quot;: 0,    &lt;br /&gt;              &amp;quot;mac&amp;quot;: []    &lt;br /&gt;          }    &lt;br /&gt;      }    &lt;br /&gt;  },{    &lt;br /&gt;      &amp;quot;key&amp;quot;: 1,    &lt;br /&gt;      &amp;quot;value&amp;quot;: {    &lt;br /&gt;          &amp;quot;&amp;quot;: {    &lt;br /&gt;              &amp;quot;value&amp;quot;: 0,    &lt;br /&gt;              &amp;quot;ifindex&amp;quot;: 0,    &lt;br /&gt;              &amp;quot;mac&amp;quot;: []    &lt;br /&gt;          }    &lt;br /&gt;      }    &lt;br /&gt;  },{    &lt;br /&gt;[...]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;针对 map 的某个 key，也可用通过 bpftool 查看、更新、删除和获取下一个 key（’get next key’）。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;BPF sysctls&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;Linux 内核提供了一些 BPF 相关的 sysctl 配置。&lt;/p&gt;  &lt;p&gt;/proc/sys/net/core/bpf_jit_enable：启用或禁用 BPF JIT 编译器。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| Value | Description                                                       |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 0     | Disable the JIT and use only interpreter (kernel&amp;apos;s default value) |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 1     | Enable the JIT compiler                                           |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 2     | Enable the JIT and emit debugging traces to the kernel log        |    &lt;br /&gt;+-------+-------------------------------------------------------------------+&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;后面会介绍到，当 JIT 编译设置为调试模式（option 2）时，bpf_jit_disasm 工 具能够处理调试跟踪信息（debugging traces）。&lt;/p&gt;  &lt;p&gt;/proc/sys/net/core/bpf_jit_harden：启用会禁用 BPF JIT 加固。&lt;/p&gt;  &lt;p&gt;注意，启用加固会降低性能，但能够降低 JIT spraying（喷射）攻击，因为它会禁止 （blind）BPF 程序使用立即值（immediate values）。对于通过解释器处理的程序， 禁用（blind）立即值是没有必要的（也是没有去做的）。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| Value | Description                                                       |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 0     | Disable JIT hardening (kernel&amp;apos;s default value)                    |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 1     | Enable JIT hardening for unprivileged users only                  |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 2     | Enable JIT hardening for all users                                |    &lt;br /&gt;+-------+-------------------------------------------------------------------+&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;/proc/sys/net/core/bpf_jit_kallsyms：是否允许 JIT 后的程序作为内核符号暴露到 /proc/kallsyms。&lt;/p&gt;  &lt;p&gt;启用后，这些符号可以被 perf 这样的工具识别，使内核在做 stack unwinding 时 能感知到这些地址，例如，在 dump stack trace 的时候，符合名中会包含 BPF 程序 tag（bpf_prog_&amp;lt;tag&amp;gt;）。如果启用了 bpf_jit_harden，这个特性就会自动被禁用。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| Value | Description                                                       |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 0     | Disable JIT kallsyms export (kernel&amp;apos;s default value)              |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 1     | Enable JIT kallsyms export for privileged users only              |    &lt;br /&gt;+-------+-------------------------------------------------------------------+&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;/proc/sys/kernel/unprivileged_bpf_disabled：是否允许非特权用户使用 bpf(2) 系统调用。&lt;/p&gt;  &lt;p&gt;内核默认允许非特权用户使用 bpf(2) 系统调用，但一旦将这个开关关闭，必须重启 内核才能再次将其打开。因此这是一个一次性开关（one-time switch），一旦关闭， 不管是应用还是管理员都无法再次修改。这个开关不影响 cBPF 程序（例如 seccomp） 或 传统的没有使用 bpf(2) 系统调用的 socket 过滤器 加载程序到内核。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| Value | Description                                                       |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 0     | Unprivileged use of bpf syscall enabled (kernel&amp;apos;s default value)  |    &lt;br /&gt;+-------+-------------------------------------------------------------------+    &lt;br /&gt;| 1     | Unprivileged use of bpf syscall disabled                          |    &lt;br /&gt;+-------+-------------------------------------------------------------------+&lt;/p&gt;&lt;/pre&gt;  &lt;strong&gt;内核测试&lt;/strong&gt;  &lt;p&gt;Linux 内核自带了一个 selftest 套件，在内核源码树中的路径是 tools/testing/selftests/bpf/。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ cd tools/testing/selftests/bpf/    &lt;br /&gt;$ make    &lt;br /&gt;$ make run_tests&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;测试用例包括：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;BPF 校验器、程序 tags、BPF map 接口和 map 类型的很多测试用例&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;用于 LLVM 后端的运行时测试，用 C 代码实现&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;用于解释器和 JIT 的测试，运行在内核，用 eBPF 和 cBPF 汇编实现&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;   &lt;strong&gt;JIT Debugging&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;对于执行审计或编写扩展的 JIT 开发人员，每次编译运行都可以通过以下方式将生成的 JIT 镜像输出到内核日志中：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ echo 2 &amp;gt; /proc/sys/net/core/bpf_jit_enable&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;每当加载新的 BPF 程序时，JIT 编译器都会转储输出，然后可以使用 dmesg 检查，例如：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;[ 3389.935842] flen=6 proglen=70 pass=3 image=ffffffffa0069c8f from=tcpdump pid=20583    &lt;br /&gt;[ 3389.935847] JIT code: 00000000: 55 48 89 e5 48 83 ec 60 48 89 5d f8 44 8b 4f 68    &lt;br /&gt;[ 3389.935849] JIT code: 00000010: 44 2b 4f 6c 4c 8b 87 d8 00 00 00 be 0c 00 00 00    &lt;br /&gt;[ 3389.935850] JIT code: 00000020: e8 1d 94 ff e0 3d 00 08 00 00 75 16 be 17 00 00    &lt;br /&gt;[ 3389.935851] JIT code: 00000030: 00 e8 28 94 ff e0 83 f8 01 75 07 b8 ff ff 00 00    &lt;br /&gt;[ 3389.935852] JIT code: 00000040: eb 02 31 c0 c9 c3&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;flen 是 BPF 程序的长度（这里是 6 个 BPF 指令），proglen 告诉 JIT 为操作码图像生成的字节数（这里是 70 字节大小）。pass 意味着图像是在 3 次编译器 pass 中生成的，&lt;/p&gt;  &lt;p&gt;例如，x86_64 可以有各种优化 pass 以在可能的情况下进一步减小图像大小。image 包含生成的 JIT 镜像的地址，from 和 pid 分别是用户空间应用程序名称和 PID，它们触发了编译过程。eBPF 和 cBPF JIT 的转储输出格式相同。&lt;/p&gt;  &lt;p&gt;在 tools/bpf/ 下的内核树中，有一个名为 bpf_jit_disasm 的工具。它读出最新的转储并打印反汇编以供进一步检查：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ ./bpf_jit_disasm    &lt;br /&gt;70 bytes emitted from JIT compiler (pass:3, flen:6)    &lt;br /&gt;ffffffffa0069c8f + &amp;lt;x&amp;gt;:    &lt;br /&gt;   0:       push   %rbp    &lt;br /&gt;   1:       mov    %rsp,%rbp    &lt;br /&gt;   4:       sub    $0x60,%rsp    &lt;br /&gt;   8:       mov    %rbx,-0x8(%rbp)    &lt;br /&gt;   c:       mov    0x68(%rdi),%r9d    &lt;br /&gt;  10:       sub    0x6c(%rdi),%r9d    &lt;br /&gt;  14:       mov    0xd8(%rdi),%r8    &lt;br /&gt;  1b:       mov    $0xc,%esi    &lt;br /&gt;  20:       callq  0xffffffffe0ff9442    &lt;br /&gt;  25:       cmp    $0x800,%eax    &lt;br /&gt;  2a:       jne    0x0000000000000042    &lt;br /&gt;  2c:       mov    $0x17,%esi    &lt;br /&gt;  31:       callq  0xffffffffe0ff945e    &lt;br /&gt;  36:       cmp    $0x1,%eax    &lt;br /&gt;  39:       jne    0x0000000000000042    &lt;br /&gt;  3b:       mov    $0xffff,%eax    &lt;br /&gt;  40:       jmp    0x0000000000000044    &lt;br /&gt;  42:       xor    %eax,%eax    &lt;br /&gt;  44:       leaveq    &lt;br /&gt;  45:       retq&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;或者，该工具还可以将相关操作码与反汇编一起转储。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ ./bpf_jit_disasm -o    &lt;br /&gt;70 bytes emitted from JIT compiler (pass:3, flen:6)    &lt;br /&gt;ffffffffa0069c8f + &amp;lt;x&amp;gt;:    &lt;br /&gt;   0:       push   %rbp    &lt;br /&gt;    55    &lt;br /&gt;   1:       mov    %rsp,%rbp    &lt;br /&gt;    48 89 e5    &lt;br /&gt;   4:       sub    $0x60,%rsp    &lt;br /&gt;    48 83 ec 60    &lt;br /&gt;   8:       mov    %rbx,-0x8(%rbp)    &lt;br /&gt;    48 89 5d f8    &lt;br /&gt;   c:       mov    0x68(%rdi),%r9d    &lt;br /&gt;    44 8b 4f 68    &lt;br /&gt;  10:       sub    0x6c(%rdi),%r9d    &lt;br /&gt;    44 2b 4f 6c    &lt;br /&gt;  14:       mov    0xd8(%rdi),%r8    &lt;br /&gt;    4c 8b 87 d8 00 00 00    &lt;br /&gt;  1b:       mov    $0xc,%esi    &lt;br /&gt;    be 0c 00 00 00    &lt;br /&gt;  20:       callq  0xffffffffe0ff9442    &lt;br /&gt;    e8 1d 94 ff e0    &lt;br /&gt;  25:       cmp    $0x800,%eax    &lt;br /&gt;    3d 00 08 00 00    &lt;br /&gt;  2a:       jne    0x0000000000000042    &lt;br /&gt;    75 16    &lt;br /&gt;  2c:       mov    $0x17,%esi    &lt;br /&gt;    be 17 00 00 00    &lt;br /&gt;  31:       callq  0xffffffffe0ff945e    &lt;br /&gt;    e8 28 94 ff e0    &lt;br /&gt;  36:       cmp    $0x1,%eax    &lt;br /&gt;    83 f8 01    &lt;br /&gt;  39:       jne    0x0000000000000042    &lt;br /&gt;    75 07    &lt;br /&gt;  3b:       mov    $0xffff,%eax    &lt;br /&gt;    b8 ff ff 00 00    &lt;br /&gt;  40:       jmp    0x0000000000000044    &lt;br /&gt;    eb 02    &lt;br /&gt;  42:       xor    %eax,%eax    &lt;br /&gt;    31 c0    &lt;br /&gt;  44:       leaveq    &lt;br /&gt;    c9    &lt;br /&gt;  45:       retq    &lt;br /&gt;    c3&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;最近，bpftool 采用了相同的功能，即根据系统中已加载的给定 BPF 程序 ID 转储 BPF JIT 镜像。&lt;/p&gt;  &lt;p&gt;对于 JITed BPF 程序的性能分析，perf 可以照常使用。作为先决条件，需要通过 kallsyms 基础设施导出 JIT 程序。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ echo 1 &amp;gt; /proc/sys/net/core/bpf_jit_enable    &lt;br /&gt;$ echo 1 &amp;gt; /proc/sys/net/core/bpf_jit_kallsyms&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;启用或禁用 bpf_jit_kallsyms 不需要重新加载相关的 BPF 程序。接下来，提供了一个小型工作流示例来分析 BPF 程序。一个精心制作的 tc BPF 程序用于演示目的，其中 perf 在 bpf_clone_redirect() 帮助程序中记录了失败的分配。&lt;/p&gt;  &lt;p&gt;由于使用直接写入，bpf_try_make_head_writable() 失败，然后会再次释放克隆的 skb 并返回错误消息。因此 perf 记录了所有 kfree_skb 事件。&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ tc qdisc add dev em1 clsact    &lt;br /&gt;$ tc filter add dev em1 ingress bpf da obj prog.o sec main    &lt;br /&gt;$ tc filter show dev em1 ingress    &lt;br /&gt;filter protocol all pref 49152 bpf    &lt;br /&gt;filter protocol all pref 49152 bpf handle 0x1 prog.o:[main] direct-action id 1 tag 8227addf251b7543    &lt;br /&gt;    &lt;br /&gt;$ cat /proc/kallsyms    &lt;br /&gt;[...]    &lt;br /&gt;ffffffffc00349e0 t fjes_hw_init_command_registers    [fjes]    &lt;br /&gt;ffffffffc003e2e0 d __tracepoint_fjes_hw_stop_debug_err    [fjes]    &lt;br /&gt;ffffffffc0036190 t fjes_hw_epbuf_tx_pkt_send    [fjes]    &lt;br /&gt;ffffffffc004b000 t bpf_prog_8227addf251b7543    &lt;br /&gt;    &lt;br /&gt;$ perf record -a -g -e skb:kfree_skb sleep 60    &lt;br /&gt;$ perf script --kallsyms=/proc/kallsyms    &lt;br /&gt;[...]    &lt;br /&gt;ksoftirqd/0     6 [000]  1004.578402:    skb:kfree_skb: skbaddr=0xffff9d4161f20a00 protocol=2048 location=0xffffffffc004b52c    &lt;br /&gt;   7fffb8745961 bpf_clone_redirect (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffc004e52c bpf_prog_8227addf251b7543 (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffc05b6283 cls_bpf_classify (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb875957a tc_classify (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb8729840 __netif_receive_skb_core (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb8729e38 __netif_receive_skb (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb872ae05 process_backlog (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb872a43e net_rx_action (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb886176c __do_softirq (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb80ac5b9 run_ksoftirqd (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb80ca7fa smpboot_thread_fn (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb80c6831 kthread (/lib/modules/4.10.0+/build/vmlinux)    &lt;br /&gt;   7fffb885e09c ret_from_fork (/lib/modules/4.10.0+/build/vmlinux)&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;perf 记录的堆栈跟踪将显示 bpf_prog_8227addf251b7543() 符号作为调用跟踪的一部分，这意味着带有标签 8227addf251b7543 的 BPF 程序与 kfree_skb 事件相关，并且该程序在入口挂钩上附加到 netdevice em1 为 由 tc 显示。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;内省&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;Linux 内核围绕 BPF 和 XDP 提供了多种 tracepoints，这些 tracepoints 可以用于进一 步查看系统内部行为，例如，跟踪用户空间程序和 bpf 系统调用的交互。&lt;/p&gt;  &lt;p&gt;BPF 相关的 tracepoints：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ perf list | grep bpf:    &lt;br /&gt;bpf:bpf_map_create                                 [Tracepoint event]    &lt;br /&gt;bpf:bpf_map_delete_elem                            [Tracepoint event]    &lt;br /&gt;bpf:bpf_map_lookup_elem                            [Tracepoint event]    &lt;br /&gt;bpf:bpf_map_next_key                               [Tracepoint event]    &lt;br /&gt;bpf:bpf_map_update_elem                            [Tracepoint event]    &lt;br /&gt;bpf:bpf_obj_get_map                                [Tracepoint event]    &lt;br /&gt;bpf:bpf_obj_get_prog                               [Tracepoint event]    &lt;br /&gt;bpf:bpf_obj_pin_map                                [Tracepoint event]    &lt;br /&gt;bpf:bpf_obj_pin_prog                               [Tracepoint event]    &lt;br /&gt;bpf:bpf_prog_get_type                              [Tracepoint event]    &lt;br /&gt;bpf:bpf_prog_load                                  [Tracepoint event]    &lt;br /&gt;bpf:bpf_prog_put_rcu                               [Tracepoint event]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;使用 perf 跟踪 BPF 系统调用（这里用 sleep 只是展示用法，实际场景中应该 执行 tc 等命令）：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ perf record -a -e bpf:* sleep 10    &lt;br /&gt;$ perf script    &lt;br /&gt;sock_example  6197 [005]   283.980322: bpf:bpf_map_create: map type=ARRAY ufd=4 key=4 val=8 max=256 flags=0    &lt;br /&gt;sock_example  6197 [005]   283.980721: bpf:bpf_prog_load: prog=a5ea8fa30ea6849c type=SOCKET_FILTER ufd=5    &lt;br /&gt;sock_example  6197 [005]   283.988423: bpf:bpf_prog_get_type: prog=a5ea8fa30ea6849c type=SOCKET_FILTER    &lt;br /&gt;sock_example  6197 [005]   283.988443: bpf:bpf_map_lookup_elem: map type=ARRAY ufd=4 key=[06 00 00 00] val=[00 00 00 00 00 00 00 00]    &lt;br /&gt;[...]    &lt;br /&gt;sock_example  6197 [005]   288.990868: bpf:bpf_map_lookup_elem: map type=ARRAY ufd=4 key=[01 00 00 00] val=[14 00 00 00 00 00 00 00]    &lt;br /&gt;     swapper     0 [005]   289.338243: bpf:bpf_prog_put_rcu: prog=a5ea8fa30ea6849c type=SOCKET_FILTER&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;对于 BPF 程序，以上命令会打印出每个程序的 tag。&lt;/p&gt;  &lt;p&gt;对于调试，XDP 还有一个 xdp:xdp_exception tracepoint，在抛异常的时候触发：&lt;/p&gt;  &lt;pre&gt;   &lt;p&gt;$ perf list | grep xdp:    &lt;br /&gt;xdp:xdp_exception                                  [Tracepoint event]&lt;/p&gt;&lt;/pre&gt;  &lt;p&gt;异常在下面情况下会触发：&lt;/p&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;BPF 程序返回一个非法/未知的 XDP action code&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;BPF 程序返回 XDP_ABORTED，这表示非优雅的退出（non-graceful exit）&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;BPF 程序返回 XDP_TX，但发送时发生错误，例如，由于端口没有启用、发送缓冲区已 满、分配内存失败等等&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt;这两类 tracepoint 也都可以通过 attach BPF 程序，用这个 BPF 程序本身来收集进一步 信息，将结果放到一个 BPF map 或以事件的方式发送到用户空间收集器，例如利用 bpf_perf_event_output() 辅助函数。&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;其他&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;和 perf 类似，BPF 程序和 map 占用的内存是算在 RLIMIT_MEMLOCK 中的。可以用 ulimit -l 查看当前锁定到内存中的页面大小。setrlimit() 系统调用的 man page 提 供了进一步的细节。&lt;/p&gt;  &lt;p&gt;默认的限制通常导致无法加载复杂的程序或很大的 BPF map，此时 BPF 系统调用会返回 EPERM 错误码。这种情况就需要将限制调大，或者用 ulimit -l unlimited 来临时解 决。RLIMIT_MEMLOCK 主要是针对非特权用户施加限制。根据实际场景不同，为特权 用户设置一个较高的阈值通常是可以接受的。&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;    &lt;em&gt;END&lt;/em&gt;&lt;/strong&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;想要了解更多相关的内容，欢迎扫描下方 关注 公众号，回复关键词 [实战群]  ,就有机会进群和我们进行交流~&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;   &lt;img&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt;   &lt;br /&gt;&lt;/p&gt;  &lt;p&gt;   &lt;strong&gt;分享、在看与点赞，至少我要拥有一个叭~    &lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;  &lt;img&gt;&lt;/img&gt;  &lt;img&gt;&lt;/img&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62262-ebpf-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Wed, 18 May 2022 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>如何优雅地实现"异步"编程？</title>
      <link>https://itindex.net/detail/62102-%E5%BC%82%E6%AD%A5-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;h2&gt;1、 引言&lt;/h2&gt;
 &lt;p&gt;Java异步编程极大的节省了主程序执行时间，提升了计算资源利用效率，是Java高级工程师的必备技能之一。本文围绕什么是异步，异步解决了什么问题，怎么  &lt;a href="https://www.zhihu.com/search?q=%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22article%22%2C%22sourceId%22%3A146336940%7D"&gt;异步编程&lt;/a&gt;来展开。&lt;/p&gt;
 &lt;h3&gt;1.1 什么是异步编程&lt;/h3&gt;
 &lt;p&gt;在解释异步编程之前，我们先来看  &lt;a href="https://www.zhihu.com/search?q=%E5%90%8C%E6%AD%A5%E7%BC%96%E7%A8%8B&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22article%22%2C%22sourceId%22%3A146336940%7D"&gt;同步编程&lt;/a&gt;的定义。同步编程，即是一种典型的请求-响应模型，当请求调用一个函数或方法后，需等待其响应返回，然后执行后续代码。同步的最大特征便是「  &lt;strong&gt;有序&lt;/strong&gt;」，当各个过程都执行完毕，最后返回结果。如图&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image-20211208153717189" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8a21939ee8964b2094e9966caf44ebba~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;异步编程则是只发送了调用的指令，调用者无需等待被调用的方法执行完毕，而是继续执行下面的流程。在一个多处理器或多核的环境中，  &lt;a href="https://www.zhihu.com/search?q=%E5%BC%82%E6%AD%A5%E8%B0%83%E7%94%A8&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22article%22%2C%22sourceId%22%3A146336940%7D"&gt;异步调用&lt;/a&gt;是真正的并行执行。如图&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image-20211208153827238" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/775e0a0d447a4b6eb11b573d4b18abe1~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;1.1 异步编程解决了什么问题&lt;/h3&gt;
 &lt;p&gt;Java异步编程的目的是充分利用计算机CPU资源，不让主程序阻塞在某个长时间运行的任务上，从而优化主程序的执行时间。这类耗时的任务可以是 IO操作、远程调用以及高密度计算任务。如果不使用  &lt;a href="https://www.zhihu.com/search?q=%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22article%22%2C%22sourceId%22%3A146336940%7D"&gt;多线程异步编程&lt;/a&gt;，我们的系统就会阻塞在耗时的子任务上，会导致极大延长完成主函数任务的时间。&lt;/p&gt;
 &lt;p&gt;在实际业务开发中，一般都是同步调用的。但有很多场景非常适合使用异步来处理，如：注册新用户，送100个积分；或下单成功，发送push消息等等。就拿注册新用户这个用例来说，为什么要异步处理？&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;第一个原因：容错性、健壮性，如果送积分出现异常，不能因为送积分而导致用户注册失败； 因为用户注册是主要功能，送积分是次要功能，即使送积分异常也要提示用户注册成功，然后后面在针对积分异常做补偿处理。&lt;/li&gt;
  &lt;li&gt;第二个原因：提升性能，例如注册用户花了20毫秒，送积分花费50毫秒，如果用同步的话，总耗时70毫秒，用异步的话，无需等待积分，故耗时20毫秒。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;故，异步能解决2个问题，性能和容错性。那么想要实现异步，有哪些实现思路呢？&lt;/p&gt;
 &lt;h2&gt;2、基于消息中间件实现异步&lt;/h2&gt;
 &lt;p&gt;最常见的解决方案，是引入消息中间件。同步接口调用导致响应时间长的问题，使用mq之后，将同步调用改成异步，能够显著减少系统响应时间。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image-20211207200827321" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1d4a0aca76940e0bc20dcc5c2b374f2~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;系统A作为消息的生产者，在完成本职工作后，就能直接返回结果了。而无需等待消息消费者的返回，它们最终会独立完成所有的业务功能。&lt;/p&gt;
 &lt;p&gt;这样能避免  &lt;code&gt;总耗时比较长&lt;/code&gt;，从而影响用户的体验的问题。使用消息中间件美中不足之处是：针对相对简单业务，引入第三方消息组件，可能会有些重，会带来一些额外维护工作。&lt;/p&gt;
 &lt;p&gt;如果不引入第三方中间件，我们还可以考虑通过异步非阻塞的观察者模式实现异步，比如google开源包 Guava之EventBus，可以优雅实现异步队列。&lt;/p&gt;
 &lt;img alt="image-20211208152705309" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c54d63b353e049b98e6912dddac650aa~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;
 &lt;h1&gt;3、基于Servlet方式实现异步&lt;/h1&gt;
 &lt;p&gt;Servlet 3.0之前，Servlet采用Thread-Per-Request的方式处理请求，即每一次Http请求都由一个线程从头到尾处理。当涉及到耗时操作时，性能问题便比较明显。Servlet 3.0中提供了异步处理请求。可以先释放容器分配给请求的线程与相关资源，减轻系统负担，从而增加服务的吞吐量。Servlet 3.0的异步是通过AsyncContext对象来完成的，它可以从当前线程传给另一个线程，并归还初始线程。新的线程处理完业务可以直接返回结果给客户端。&lt;/p&gt;
 &lt;img alt="image-20211208151018729" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b51dc177ecc549e98ece6bdc302fee95~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;
 &lt;p&gt;AsyncContext对象可以从HttpServletRequest中获取：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;    @RequestMapping(&amp;quot;/async&amp;quot;)
    public void async(HttpServletRequest request) {
        AsyncContext asyncContext = request.getAsyncContext();
    }
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在AsyncContext中提供了获取ServletRequest、ServletResponse和添加监听（addListener）等功能：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public interface AsyncContext {
    ServletRequest getRequest();
    ServletResponse getResponse();
    void addListener(AsyncListener var1);
    void setTimeout(long var1);
    // 省略其他方法
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;不仅可以通过AsyncContext获取Request和Response等信息，还可以设置异步处理超时时间。通常，超时时间（单位毫秒）是需要设置的，不然无限等下去不就与同步处理一样了。通过AsyncContext的addListener还可以添加监听事件，用来处理异步线程的开始、完成、异常、超时等事件回调。&lt;/p&gt;
 &lt;p&gt;addListener方法的参数AsyncListener的源码如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public interface AsyncListener extends EventListener {
    // 异步执行完毕时调用
    void onComplete(AsyncEvent var1) throws IOException;
    // 异步线程执行超时调用
    void onTimeout(AsyncEvent var1) throws IOException;
    // 异步线程出错时调用
    void onError(AsyncEvent var1) throws IOException;
    // 异步线程开始时调用
    void onStartAsync(AsyncEvent var1) throws IOException;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;通常，异常或超时时返回调用方错误信息，而异常时会处理一些清理和关闭操作或记录异常日志等。&lt;/p&gt;
 &lt;p&gt;下面直接看一个基于Servlet方式的异步请求示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@GetMapping(value = &amp;quot;/email/send&amp;quot;)
public void servletReq(HttpServletRequest request) {
    AsyncContext asyncContext = request.startAsync();
    // 设置监听器:可设置其开始、完成、异常、超时等事件的回调处理
    asyncContext.addListener(new AsyncListener() {
        @Override
        public void onTimeout(AsyncEvent event) {
            System.out.println(&amp;quot;处理超时了...&amp;quot;);
        }

        @Override
        public void onStartAsync(AsyncEvent event) {
            System.out.println(&amp;quot;线程开始执行&amp;quot;);
        }

        @Override
        public void onError(AsyncEvent event) {
            System.out.println(&amp;quot;执行过程中发生错误：&amp;quot; + event.getThrowable().getMessage());
        }
        
        @Override
        public void onComplete(AsyncEvent event) {
            System.out.println(&amp;quot;执行完成，释放资源&amp;quot;);
        }
    });
    //设置超时时间
    asyncContext.setTimeout(6000);
    asyncContext.start(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                System.out.println(&amp;quot;内部线程：&amp;quot; + Thread.currentThread().getName());
                asyncContext.getResponse().getWriter().println(&amp;quot;async processing&amp;quot;);
            } catch (Exception e) {
                System.out.println(&amp;quot;异步处理发生异常：&amp;quot; + e.getMessage());
            }
            // 异步请求完成通知，整个请求完成
            asyncContext.complete();
        }
    });
    //此时request的线程连接已经释放了
    System.out.println(&amp;quot;主线程：&amp;quot; + Thread.currentThread().getName());
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;启动项目，访问对应的URL，打印日志如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;主线程：http-nio-8080-exec-4
内部线程：http-nio-8080-exec-5
执行完成，释放资源
复制代码
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;可以看出，上述代码先执行完了主线程，也就是程序的最后一行代码的日志打印，然后才是内部线程的执行。内部线程执行完成，AsyncContext的onComplete方法被调用。如果通过浏览器访问对应的URL，还可以看到该方法的返回值“async processing”。说明内部线程的结果同样正常的返回到客户端了。&lt;/p&gt;
 &lt;h1&gt;4、基于Spring实现异步&lt;/h1&gt;
 &lt;p&gt;基于Spring可以通过Callable、DeferredResult或者WebAsyncTask等方式实现异步请求。&lt;/p&gt;
 &lt;h4&gt;4.1  基于Callable实现&lt;/h4&gt;
 &lt;p&gt;对于一次请求（/email），基于Callable的处理流程如下：&lt;/p&gt;
 &lt;p&gt;1、Spring MVC开启副线程处理业务(将Callable提交到TaskExecutor)；&lt;/p&gt;
 &lt;p&gt;2、DispatcherServlet和所有的Filter退出Web容器的线程，但是response保持打开状态；&lt;/p&gt;
 &lt;p&gt;3、Callable返回结果，SpringMVC将原始请求重新派发给容器(再重新请求一次/email)，恢复之前的处理；&lt;/p&gt;
 &lt;p&gt;4、DispatcherServlet重新被调用，将结果返回给用户；&lt;/p&gt;
 &lt;p&gt;代码实现示例如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@GetMapping(&amp;quot;/email&amp;quot;)
public Callable&amp;lt;String&amp;gt; order() {
    System.out.println(&amp;quot;主线程开始：&amp;quot; + Thread.currentThread().getName());
    Callable&amp;lt;String&amp;gt; result = () -&amp;gt; {
        System.out.println(&amp;quot;副线程开始：&amp;quot; + Thread.currentThread().getName());
        Thread.sleep(1000);
        System.out.println(&amp;quot;副线程返回：&amp;quot; + Thread.currentThread().getName());
        return &amp;quot;success&amp;quot;;
    };

    System.out.println(&amp;quot;主线程返回：&amp;quot; + Thread.currentThread().getName());
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;访问对应URL，控制台输入日志如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;主线程开始：http-nio-8080-exec-1
主线程返回：http-nio-8080-exec-1
副线程开始：task-1
副线程返回：task-1
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;通过日志可以看出，主线程已经完成了，副线程才进行执行。同时，URL返回结果“success”。这也说明一个问题，服务器端的异步处理对客户端来说是不可见的。&lt;/p&gt;
 &lt;p&gt;Callable默认使用SimpleAsyncTaskExecutor类来执行，该线程池不是真正意义上的线程池。值得特别注意的一点，【此处有坑】使用此线程池无法实现线程重用，每次调用都会新建一条线程。若系统中不断的创建线程，最终会导致系统占用内存过高，引发  &lt;code&gt;OutOfMemoryError&lt;/code&gt;错误，关键代码如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public void execute(Runnable task, long startTimeout) {
  Assert.notNull(task, &amp;quot;Runnable must not be null&amp;quot;);
  Runnable taskToUse = this.taskDecorator != null ? this.taskDecorator.decorate(task) : task;
  //判断是否开启限流，默认为否
  if (this.isThrottleActive() &amp;amp;&amp;amp; startTimeout &amp;gt; 0L) {
    //执行前置操作，进行限流
    this.concurrencyThrottle.beforeAccess();
    this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));
  } else {
    //未限流的情况，执行线程任务
    this.doExecute(taskToUse);
  }

}

protected void doExecute(Runnable task) {
  //不断创建线程
  Thread thread = this.threadFactory != null ? this.threadFactory.newThread(task) : this.createThread(task);
  thread.start();
}

//创建线程
public Thread createThread(Runnable runnable) {
  //指定线程名，task-1，task-2...
  Thread thread = new Thread(this.getThreadGroup(), runnable, this.nextThreadName());
  thread.setPriority(this.getThreadPriority());
  thread.setDaemon(this.isDaemon());
  return thread;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们也可以直接通过上面的控制台日志观察，每次打印的线程名都是[task-1]、[task-2]、[task-3]、[task-4].....递增的。&lt;/p&gt;
 &lt;p&gt;正因如此，所以我们实际使用异步框架时一定要自定义线程池，替代默认的  &lt;code&gt;SimpleAsyncTaskExecutor&lt;/code&gt;。这里通过实现WebMvcConfigurer接口来完成线程池的配置。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;

    /**
     * 配置线程池
     */
    @Bean(name = &amp;quot;asyncPoolTaskExecutor&amp;quot;)
    public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(2);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix(&amp;quot;thread-pool-&amp;quot;);
        // 线程池对拒绝任务（无线程可用）的处理策略，目前只支持AbortPolicy、CallerRunsPolicy；默认为后者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
        // 处理callable超时
        configurer.setDefaultTimeout(60 * 1000);
        configurer.setTaskExecutor(myThreadPoolTaskExecutor);
        configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());
    }

    @Bean
    public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() {
        return new TimeoutCallableProcessingInterceptor();
    }
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;为了验证打印的线程，我们将实例代码中的System.out.println替换成日志输出，会发现在使用线程池之前，打印日志如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;2021-02-21 09:45:37.144  INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程开始：http-nio-8080-exec-1
2021-02-21 09:45:37.144  INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程返回：http-nio-8080-exec-1
2021-02-21 09:45:37.148  INFO 8312 --- [         task-1] c.s.learn.controller.AsynController      : 副线程开始：task-1
2021-02-21 09:45:38.153  INFO 8312 --- [         task-1] c.s.learn.controller.AsynController      : 副线程返回：task-1
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;线程名称为“task-1”。让线程池生效之后，打印日志如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;2021-02-21 09:50:28.950  INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程开始：http-nio-8080-exec-1
2021-02-21 09:50:28.951  INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程返回：http-nio-8080-exec-1
2021-02-21 09:50:28.955  INFO 8339 --- [  thread-pool-1] c.s.learn.controller.AsynController      : 副线程开始：thread-pool-1
2021-02-21 09:50:29.956  INFO 8339 --- [  thread-pool-1] c.s.learn.controller.AsynController      : 副线程返回：thread-pool-1
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;线程名称为“thread-pool-1”，其中前面的“thread-pool”正是我们配置的线程池前缀。&lt;/p&gt;
 &lt;p&gt;除了线程池的配置，还可以配置统一异常处理，这里就不再演示了。&lt;/p&gt;
 &lt;h4&gt;4.2 基于WebAsyncTask实现异步&lt;/h4&gt;
 &lt;p&gt;Spring提供的WebAsyncTask是对Callable的包装，提供了更强大的功能，比如：处理超时回调、错误回调、完成回调等。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@GetMapping(&amp;quot;/webAsyncTask&amp;quot;)
public WebAsyncTask&amp;lt;String&amp;gt; webAsyncTask() {
    log.info(&amp;quot;外部线程：&amp;quot; + Thread.currentThread().getName());
    WebAsyncTask&amp;lt;String&amp;gt; result = new WebAsyncTask&amp;lt;&amp;gt;(60 * 1000L, new Callable&amp;lt;String&amp;gt;() {
        @Override
        public String call() {
            log.info(&amp;quot;内部线程：&amp;quot; + Thread.currentThread().getName());
            return &amp;quot;success&amp;quot;;
        }
    });
    result.onTimeout(new Callable&amp;lt;String&amp;gt;() {
        @Override
        public String call() {
            log.info(&amp;quot;timeout callback&amp;quot;);
            return &amp;quot;timeout callback&amp;quot;;
        }
    });
    result.onCompletion(new Runnable() {
        @Override
        public void run() {
            log.info(&amp;quot;finish callback&amp;quot;);
        }
    });
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;访问对应请求，打印日志：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;2021-02-21 10:22:33.028  INFO 8547 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 外部线程：http-nio-8080-exec-1
2021-02-21 10:22:33.033  INFO 8547 --- [  thread-pool-1] c.s.learn.controller.AsynController      : 内部线程：thread-pool-1
2021-02-21 10:22:33.055  INFO 8547 --- [nio-8080-exec-2] c.s.learn.controller.AsynController      : finish callback

&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;4.3 基于DeferredResult实现异步&lt;/h4&gt;
 &lt;p&gt;DeferredResult使用方式与Callable类似，但在返回结果时不一样，它返回的时实际结果可能没有生成，实际的结果可能会在另外的线程里面设置到DeferredResult中去。DeferredResult的这个特性对实现服务端推技术、订单过期时间处理、长轮询、模拟MQ的功能等高级应用非常重要。常见的使用场景：&lt;/p&gt;
 &lt;p&gt;1、如熟悉Apollo使用原理的同学，可能有了解，Apollo配置实时热更新便是基于DeferredResult实现配置变更通知的&lt;/p&gt;
 &lt;p&gt;2、如前端轮询订单支付状态，为减轻服务器压力，可改为长链接的形式，基于DeferredResult实现异步结果回调&lt;/p&gt;
 &lt;p&gt;关于DeferredResult的使用先来看一下官方的例子和说明：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@RequestMapping(&amp;quot;/quotes&amp;quot;)
@ResponseBody
public DeferredResult&amp;lt;String&amp;gt; quotes() {
  DeferredResult&amp;lt;String&amp;gt; deferredResult = new DeferredResult&amp;lt;String&amp;gt;();
  // Save the deferredResult in in-memory queue ...
  return deferredResult;
}

// In some other thread...
deferredResult.setResult(data);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;上述示例中我们可以发现DeferredResult的调用并不一定在Spring MVC当中，它可以是别的线程。官方的解释也是如此：&lt;/p&gt;
 &lt;p&gt;In this case the return value will also be produced from a separate thread. However, that thread is not known to Spring MVC. For example the result may be produced in response to some external event such as a JMS message, a scheduled task, etc.&lt;/p&gt;
 &lt;p&gt;也就是说，DeferredResult返回的结果也可能是由MQ、定时任务或其他线程触发。来个实例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@Controller
@RequestMapping(&amp;quot;/async/controller&amp;quot;)
public class AsyncHelloController {

    private List&amp;lt;DeferredResult&amp;lt;String&amp;gt;&amp;gt; deferredResultList = new ArrayList&amp;lt;&amp;gt;();

    @ResponseBody
    @GetMapping(&amp;quot;/hello&amp;quot;)
    public DeferredResult&amp;lt;String&amp;gt; helloGet() throws Exception {
        DeferredResult&amp;lt;String&amp;gt; deferredResult = new DeferredResult&amp;lt;&amp;gt;();

        //先存起来，等待触发
        deferredResultList.add(deferredResult);
        return deferredResult;
    }

    @ResponseBody
    @GetMapping(&amp;quot;/setHelloToAll&amp;quot;)
    public void helloSet() throws Exception {
        // 让所有hold住的请求给与响应
        deferredResultList.forEach(d -&amp;gt; d.setResult(&amp;quot;say hello to all&amp;quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;第一个请求/hello，会先将deferredResult存起来，前端页面是一直等待（转圈）状态。直到发第二个请求：setHelloToAll，所有的相关页面才会有响应。&lt;/p&gt;
 &lt;p&gt;整个执行流程如下：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;controller返回一个DeferredResult，把它保存到内存里或者List里面（供后续访问）；&lt;/li&gt;
  &lt;li&gt;Spring MVC调用request.startAsync()，开启异步处理；
与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程，但是response仍然保持打开的状态；&lt;/li&gt;
  &lt;li&gt;应用通过另外一个线程（可能是MQ消息、定时任务等）给DeferredResult#setResult值。然后SpringMVC会把这个请求再次派发给servlet容器；&lt;/li&gt;
  &lt;li&gt;DispatcherServlet再次被调用，然后处理后续的标准流程；&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;通过上述流程可以发现：利用DeferredResult可实现一些长连接的功能，比如当某个操作是异步时，可以先保存对应的DeferredResult对象，当异步通知回来时，再找到这个DeferredResult对象，在setResult处理结果即可。从而提高性能。&lt;/p&gt;
 &lt;h2&gt;5 、基于SpringBoot异步实现&lt;/h2&gt;
 &lt;p&gt;在SpringBoot中将一个方法声明为异步方法非常简单，只需两个注解即可@EnableAsync和@Async。其中@EnableAsync用于开启SpringBoot支持异步的功能，用在SpringBoot的启动类上。@Async用于方法上，标记该方法为异步处理方法。&lt;/p&gt;
 &lt;p&gt;需要注意的是@Async并不支持用于被@Configuration注解的类的方法上。同一个类中，一个方法调用另外一个有@Async的方法，注解也是不会生效的。&lt;/p&gt;
 &lt;p&gt;@EnableAsync的使用示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@SpringBootApplication
@EnableAsync
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;@Async的使用示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@Service
public class SyncService {
    
    @Async
    public void asyncEvent() {
        // 业务处理
    }
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;@Async注解的使用与Callable有类似之处，在默认情况下使用的都是SimpleAsyncTaskExecutor线程池，可参考Callable中的方式来自定义线程池。&lt;/p&gt;
 &lt;p&gt;下面通过一个实例来验证一下，启动类上使用@EnableAsync，然后定义Controller类：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@RestController
public class IndexController {
    
    @Resource
    private UserService userService;
    
    @RequestMapping(&amp;quot;/async&amp;quot;)
    public String async(){
        System.out.println(&amp;quot;--IndexController--1&amp;quot;);
        userService.sendSms();
        System.out.println(&amp;quot;--IndexController--4&amp;quot;);
        return &amp;quot;success&amp;quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;定义Service及异步方法：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;@Service
public class UserService {

    @Async
    public void sendSms(){
        System.out.println(&amp;quot;--sendSms--2&amp;quot;);
        IntStream.range(0, 5).forEach(d -&amp;gt; {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(&amp;quot;--sendSms--3&amp;quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果先注释掉@EnableAsync和@Async注解，即正常情况下的业务请求，打印日志为：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;--IndexController--1
--sendSms--2
--sendSms--3
--IndexController--4
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;使用@EnableAsync和@Async注解时，打印日志如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;--IndexController--1
--IndexController--4
--sendSms--2
--sendSms--3
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;通过日志的对比我们可以看出，使用了@Async的方法，会被当成一个子线程。所以，整个sendSms方法会在主线程执行完了之后执行。&lt;/p&gt;
 &lt;p&gt;今天我们来探讨下 spring 是如何完成这个异步功能的： spring 在扫描bean的时候会扫描方法上是否包含@async的注解，如果包含的，spring会为这个bean动态的生成一个子类，我们称之为代理类(?)， 代理类是继承我们所写的bean的，然后把代理类注入进来，那此时，在执行此方法的时候，会到代理类中，代理类判断了此方法需要异步执行，就不会调用父类 (我们原本写的bean)的对应方法。spring自己维护了一个队列，他会把需要执行的方法，放入队列中，等待线程池去读取这个队列，完成方法的执行， 从而完成了异步的功能。我们可以关注到再配置task的时候，是有参数让我们配置线程池的数量的。因为这种实现方法，所以在同一个类中的方法调用，添加@async注解是失效的！，原因是当你在同一个类中的时候，方法调用是在类体内执行的，spring无法截获这个方法调用。&lt;/p&gt;
 &lt;p&gt;那在深入一步，spring为我们提供了AOP，面向切面的功能。他的原理和异步注解的原理是类似的，spring在启动容器的时候，会扫描切面所定义的 类。在这些类被注入的时候，所注入的也是代理类，当你调用这些方法的时候，本质上是调用的代理类。通过代理类再去执行父类相对应的方法，那spring只 需要在调用之前和之后执行某段代码就完成了AOP的实现了！&lt;/p&gt;
 &lt;h5&gt;@Async、WebAsyncTask、Callable、DeferredResult的区别&lt;/h5&gt;
 &lt;p&gt;所在的包不同：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;@Async：org.springframework.scheduling.annotation;&lt;/li&gt;
  &lt;li&gt;WebAsyncTask：org.springframework.web.context.request.async;&lt;/li&gt;
  &lt;li&gt;Callable：java.util.concurrent；&lt;/li&gt;
  &lt;li&gt;DeferredResult：org.springframework.web.context.request.async;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;通过所在的包，我们应该隐隐约约感到一些区别，比如@Async是位于scheduling包中，而WebAsyncTask和DeferredResult是用于Web（Spring MVC）的，而Callable是用于concurrent（并发）处理的。&lt;/p&gt;
 &lt;p&gt;对于Callable，通常用于Controller方法的异步请求，当然也可以用于替换Runable的方式。在方法的返回上与正常的方法有所区别：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 普通方法
public String aMethod(){
}
// 对照Callable方法
public Callable&amp;lt;String&amp;gt;  aMethod(){
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;而WebAsyncTask是对Callable的封装，提供了一些事件回调的处理，本质上区别不大。&lt;/p&gt;
 &lt;p&gt;DeferredResult使用方式与Callable类似，重点在于跨线程之间的通信。&lt;/p&gt;
 &lt;p&gt;@Async也是替换Runable的一种方式，可以代替我们自己创建线程。而且适用的范围更广，并不局限于Controller层，而可以是任何层的方法上。&lt;/p&gt;
 &lt;p&gt;当然，大家也可以从返回结果，异常处理等角度来分析一下，这里就不再展开了。&lt;/p&gt;
 &lt;h3&gt;6、 自己造轮子&lt;/h3&gt;
 &lt;p&gt;我们还可以自己造轮子，通过自定义线程池的工具类的方式，实现业务异步执行。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public abstract class ExecutorKit {
    private static final Logger logger = LoggerFactory.getLogger(ExecutorKit.class);
    private static ThreadPoolExecutor threadPoolExecutor;

    public static void sleep(int timeout, TimeUnit timeUnit) {
        try {
            timeUnit.sleep((long)timeout);
        } catch (Exception var3) {
        }

    }

    public static void execute(Runnable cmd) {
        execute(&amp;quot;default&amp;quot;, true, cmd);
    }

    public static void execute(String name, Runnable cmd) {
        execute(name, true, cmd);
    }

    public static void execute(String name, boolean ignoreException, Runnable cmd) {
        threadPoolExecutor.execute(() -&amp;gt; {
            Transaction transaction = Cat.newTransaction(&amp;quot;Executor-Tasks&amp;quot;, name);

            try {
                cmd.run();
                transaction.setStatus(&amp;quot;0&amp;quot;);
            } catch (Exception var8) {
                logger.error(&amp;quot;异步运行[{}]任务异常&amp;quot;, name, var8);
                if (ignoreException) {
                    transaction.setStatus(&amp;quot;0&amp;quot;);
                } else {
                    transaction.setStatus(var8);
                }
            } finally {
                transaction.complete();
            }

        });
    }

    private ExecutorKit() {
    }

    static {
        AtomicInteger threadIndex = new AtomicInteger(0);
        LinkedBlockingDeque&amp;lt;Runnable&amp;gt; blockingDeque = new LinkedBlockingDeque(51200);
        ThreadFactory threadFactory = (r) -&amp;gt; {
            Thread thread = new Thread(r);
            thread.setName(ExecutorKit.class.getSimpleName() + &amp;quot;-&amp;quot; + threadIndex.getAndIncrement());
            thread.setDaemon(true);
            return thread;
        };
        threadPoolExecutor = new ThreadPoolExecutor(9, 9, 0L, TimeUnit.SECONDS, blockingDeque, threadFactory, (r, executor) -&amp;gt; {
            logger.error(&amp;quot;线程池任务过多，丢弃处理&amp;quot;);
        });
    }
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;使用示例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;ExecutorKit.execute(&amp;quot;Log-Save-Decide&amp;quot;, () -&amp;gt; logAndSaveDecide(userId, result, userVars));
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;7、小结&lt;/h3&gt;
 &lt;p&gt;经过上述的分析探讨，大家应该对于常见异步处理有所了解，当熟悉这些基础理论，实战实例，使用方法及注意事项之后，在后续实际应用中，还需仔细探究其中实现细节，具体情况具体分析，研发过程需避免“拿来主义”，避免踩坑，敬畏线上的每一行代码。&lt;/p&gt;
 &lt;p&gt;附：参考链接：&lt;/p&gt;
 &lt;p&gt;1、https://zhuanlan.zhihu.com/p/146336940&lt;/p&gt;
 &lt;p&gt;2、https://zhuanlan.zhihu.com/p/99268715&lt;/p&gt;
 &lt;p&gt;3、https://juejin.cn/post/6943129925687705636&lt;/p&gt;
 &lt;p&gt;4、https://blog.csdn.net/wangdong5678999/article/details/80561198&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62102-%E5%BC%82%E6%AD%A5-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Thu, 10 Feb 2022 08:57:33 CST</pubDate>
    </item>
    <item>
      <title>用 AlphaCode 编程</title>
      <link>https://itindex.net/detail/62093-alphacode-%E7%BC%96%E7%A8%8B</link>
      <description>至少在部分问题上 AI 程序员能与真正的程序员竞争了。Alphabet 旗下 AI 子公司 DeepMind  &lt;a href="https://www.deepmind.com/blog/article/Competitive-programming-with-AlphaCode"&gt;宣布&lt;/a&gt;了 AI 代码生成系统  &lt;a href="https://storage.googleapis.com/deepmind-media/AlphaCode/competition_level_code_generation_with_alphacode.pdf"&gt;AlphaCode（PDF）&lt;/a&gt;，声称测试显示其水平在编程竞赛中已经具备了竞争力。计算机科学家 Scott Aaronson 也为 AI 在编程方面的进步 &lt;a href="https://scottaaronson.blog/?p=6288"&gt;惊叹不已&lt;/a&gt;。DeepMind 称，AlphaCode 使用了基于  transformer 的语言模型去生成代码，然后筛选出其中一部分有前景的程序。它使用了数万编程竞赛题和题目的数百万解进行训练，对每一项编程挑战它会生成一百万候选程序，通过检查示例数据丢弃掉大部分候选程序。编程竞赛平台 Codeforces 上的测试显示，AlphaCode 在三分之一的问题中达到了中等人类程序员的水平。标志着 AI 代码生成系统首次在编程竞赛中具有了竞争力。 &lt;div&gt;
  &lt;a href="http://feeds.feedburner.com/~ff/solidot?a=TBXHCCmzFSY:dpkClzPuChw:yIl2AUoC8zA"&gt;   &lt;img border="0" src="http://feeds.feedburner.com/~ff/solidot?d=yIl2AUoC8zA"&gt;&lt;/img&gt;&lt;/a&gt;   &lt;a href="http://feeds.feedburner.com/~ff/solidot?a=TBXHCCmzFSY:dpkClzPuChw:7Q72WNTAKBA"&gt;   &lt;img border="0" src="http://feeds.feedburner.com/~ff/solidot?d=7Q72WNTAKBA"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62093-alphacode-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Mon, 07 Feb 2022 21:09:01 CST</pubDate>
    </item>
    <item>
      <title>dotnet core 编程规范</title>
      <link>https://itindex.net/detail/61833-dotnet-core-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;p&gt;本文实际只是翻译  .NET Core foundational libraries 官方文档的编码风格。&lt;/p&gt;





 &lt;p&gt;在   &lt;a href="https://github.com/dotnet/corefx"&gt;.NET Core foundational libraries&lt;/a&gt;项目使用的编程规范默认就是 VisualStudio 默认样式。&lt;/p&gt;

 &lt;h2&gt;花括号&lt;/h2&gt;

 &lt;p&gt;花括号使用  Allman style 风格，所有的花括号在新的一行开始。&lt;/p&gt;

 &lt;p&gt;关于 Allman style 就是如下，花括号放在下一行，而且花括号不进行缩进，花括号里面的代码缩进。&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;while (x == y)
{
    something();
    somethingelse();
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;但是对于单行的语句，可以不添加花括号，但是请把它放在同一行&lt;/p&gt;

 &lt;div&gt;  &lt;div&gt;   &lt;pre&gt;    &lt;code&gt;          if (xx == null) xx = new Foo();
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

 &lt;p&gt;单行语句同样建议添加花括号避免  &lt;a href="https://www.imperialviolet.org/2014/02/22/applebug.html"&gt;苹果&lt;/a&gt;的bug&lt;/p&gt;

 &lt;p&gt;如果不添加花括号，一定不要在嵌套时使用。&lt;/p&gt;

 &lt;h2&gt;空格&lt;/h2&gt;

 &lt;p&gt;使用 4 个空格作为一个缩进，而不是使用 tab&lt;/p&gt;

 &lt;h2&gt;字段&lt;/h2&gt;

 &lt;p&gt;所有的字段使用  &lt;code&gt;_camelCase&lt;/code&gt;风格，而且尽可能使用  &lt;code&gt;readonly &lt;/code&gt;。在 internal 和 private 访问的字段添加   &lt;code&gt;_&lt;/code&gt; 前缀，对于静态的字段添加  &lt;code&gt;s_&lt;/code&gt;前缀，对于线程静态的字段添加  &lt;code&gt;t_&lt;/code&gt;前缀。如果使用了静态的字段而且可以设置  &lt;code&gt;readonly &lt;/code&gt;，需要把  &lt;code&gt;readonly &lt;/code&gt;放在  &lt;code&gt;static&lt;/code&gt;后面。&lt;/p&gt;

 &lt;p&gt;最好不要公开字段，如果需要公开那么使用PascalCasing风格而且不添加前缀。&lt;/p&gt;

 &lt;p&gt;我不是很赞同对字段的风格，但是因为本文是翻译，我就没有添加自己的理解。&lt;/p&gt;

 &lt;h2&gt;限定&lt;/h2&gt;

 &lt;p&gt;不要添加   &lt;code&gt;this.&lt;/code&gt; ，完全是没必要的。&lt;/p&gt;

 &lt;p&gt;我也不是很赞同这个，因为如果使用扩展方法，需要使用  &lt;code&gt;this.&lt;/code&gt;&lt;/p&gt;

 &lt;h2&gt;访问&lt;/h2&gt;

 &lt;p&gt;显示指定访问，即使访问是默认的，如  &lt;code&gt;private string _foo&lt;/code&gt; ，而不是  &lt;code&gt;string _foo&lt;/code&gt;。访问需要放在最前面，如  &lt;code&gt;public abstract&lt;/code&gt;，不建议  &lt;code&gt;abstract public&lt;/code&gt;&lt;/p&gt;

 &lt;p&gt;默认的访问是可能修改的&lt;/p&gt;

 &lt;h2&gt;命名空间&lt;/h2&gt;

 &lt;p&gt;对于引用需要放在文件的开头。&lt;/p&gt;

 &lt;h2&gt;空行&lt;/h2&gt;

 &lt;p&gt;禁止超过一行的空白，不可以使用两行空白。&lt;/p&gt;

 &lt;p&gt;不要使用两行分割两个类型。&lt;/p&gt;

 &lt;h2&gt;多余空格&lt;/h2&gt;

 &lt;p&gt;禁止在一行结束添加多余空格。通过 VisualStudio 的查看空格(ctrl+E,S)可以看到空格&lt;/p&gt;

 &lt;h2&gt;参照现有&lt;/h2&gt;

 &lt;p&gt;如果一个文件在格式规范定义之前已经有自己的规范，尽可能参照文件现有的规范而不是去修改他。&lt;/p&gt;

 &lt;p&gt;新的修改参照现有的文件的格式。&lt;/p&gt;

 &lt;p&gt;我一点都不赞同。&lt;/p&gt;

 &lt;h2&gt;明显的类型&lt;/h2&gt;

 &lt;p&gt;只能在明显可以知道对象类型可以使用 var ，如  &lt;code&gt;var stream = new FileStream(...)&lt;/code&gt; 。在无法明显知道对象类型不可以使用 var ，如  &lt;code&gt;var stream = OpenStandardInput()&lt;/code&gt; 。&lt;/p&gt;

 &lt;h2&gt;关键字&lt;/h2&gt;

 &lt;p&gt;使用语言的关键字代替 BCL 类型，如使用   &lt;code&gt;int, string, float&lt;/code&gt;代替  &lt;code&gt;Int32&lt;/code&gt;，  &lt;code&gt;String&lt;/code&gt;，  &lt;code&gt;Single&lt;/code&gt;&lt;/p&gt;

 &lt;h2&gt;常量命名&lt;/h2&gt;

 &lt;p&gt;对于所有的常量，包括静态只读，使用   &lt;code&gt;PascalCasing&lt;/code&gt; 风格。如果需要使用 C++ 或其他代码对于命名有要求才可以使用其他的方法。&lt;/p&gt;

 &lt;h2&gt;变量名字符串&lt;/h2&gt;

 &lt;p&gt;如果使用变量名的常量，必须使用   &lt;code&gt;nameof&lt;/code&gt; 关键字。&lt;/p&gt;

 &lt;h2&gt;字段结构&lt;/h2&gt;

 &lt;p&gt;字段必须放在一个类声明的最前面。&lt;/p&gt;

 &lt;p&gt;我一点都不赞成，因为谁会去管一个类的字段，我关心的是构造函数和公开属性、函数。&lt;/p&gt;

 &lt;h2&gt;其他字符&lt;/h2&gt;

 &lt;p&gt;如果在代码使用了 非 ascii 的字符，那么需要把他转换为   &lt;code&gt;\uxx&lt;/code&gt; 方式，这样可以解决一些非 ascii 因为编译器分析的问题。&lt;/p&gt;

 &lt;p&gt;当然上面的要求只是对于编译的代码&lt;/p&gt;

 &lt;h2&gt;Label&lt;/h2&gt;

 &lt;p&gt;使用 Label 缩进一个小于当前缩进的标签&lt;/p&gt;

 &lt;p&gt;原文：  &lt;a href="https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md"&gt;coding-style.md&lt;/a&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="http://image.acmx.xyz/lindexi%2F2018571237288065.jpg"&gt;&lt;/img&gt;&lt;/p&gt;

&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dotnet</category>
      <guid isPermaLink="true">https://itindex.net/detail/61833-dotnet-core-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Mon, 18 Oct 2021 19:07:19 CST</pubDate>
    </item>
    <item>
      <title>腾讯人最喜欢的编程语言是什么？ | 内含完整报告_TAPD敏捷研发-CSDN博客</title>
      <link>https://itindex.net/detail/61729-%E8%85%BE%E8%AE%AF-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%AE%8C%E6%95%B4</link>
      <description>&lt;div&gt;    &lt;p&gt;      &lt;strong&gt;先抛结论：这份报告，含金量很足，请认真研读&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;刚刚，腾讯正式对外发布2020年度《腾讯研发大数据报告》，这份由腾讯技术委员会出品的报告，披露了过去一年腾讯在研发投入、研发效能及开源协同等方面的重要数据。&lt;/p&gt;    &lt;p&gt;大家普遍关注的问题，在这里都可以找到答案，比如，腾讯人最喜欢什么编程语言，还有什么技术leader坚持写代码，腾讯开源协同进展等等，你都能在这份报告中找到答案。&lt;/p&gt;    &lt;p&gt;准备好了吗，一起带你去感受吧。&lt;/p&gt;    &lt;h3&gt;研发人员占比68%，新增代码20亿行&lt;/h3&gt;    &lt;p&gt;腾讯在研发投入上持续加码。报告显示，2020年腾讯研发人员占公司总人数的68%，同比去年增长16%，在科技企业中位居前列。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/bddb8c61cc0e0150409045682f7fc7cf.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;在开源协同、自研上云两大技术战略的推动下，腾讯研发效能进一步提升，2020年腾讯新增研发项目超4000个，同比增长22%；新增代码超过20亿行，同比增长67%。研发人员日均完成5242个需求，有30%的需求能够在1天之内得到响应，平均需求响应时长缩短8.66小时，有46%的需求能够在3天内开发完成，单个Bug的平均解决时长较去年缩短了15%，研发更敏捷。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/016758d2535a878caff18858d843cd8b.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;代码质量也是研发人员关注的重点。腾讯倡导“小批量、多批次”的代码提交策略。2020年，代码评审覆盖率达7成，平均每位评审人参评90次，平均每次评审293行代码。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/95e633d575222b889713046579142200.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;在研发持续交付方面，腾讯平均每周构建次数达170万次，项目年均产物大小1TB，年均交付次数5万次，全年共推动修复代码Bug和安全漏洞131万个，编译加速累计节省编译耗时5.8万个小时。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/41b9857b9953fb3e89c2fa3180ef3a4d.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h3&gt;DevOps工具协同集成，研发效能持续提升&lt;/h3&gt;    &lt;p&gt;在长期的研发实践中，腾讯推动了代码管理平台工蜂、敏捷研发协作平台 TAPD、智能化持续集成平台腾讯 CI（蓝盾）、集成化研效门户智研、企业级研发云平台等多个工具平台协同集成，共同组成了贯穿上下游的研效工具链体系。这一体系的标准化落地，进一步降低了开发成本、增强了研发人员的使用体验。TAPD、腾讯工蜂、蓝盾三大腾讯主流研发工具的日均API请求量达到四千万次。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/badb5bb6024404959aa2405b62e0edc4.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;2020年，腾讯通过信通院《研发运营一体化（ DevOps ）能力成熟度模型》系统和工具部分首批评估，获评为卓越级。这意味着腾讯形成了业内领先的研发体系，研发效能工具得到了国家级的权威认可。&lt;/p&gt;    &lt;p&gt;C++蝉联腾讯最受欢迎的编程语言。随着云计算和微服务相关技术的进一步发展，Go语言使用次数增速第一，并超越JavaScript成为腾讯第二受欢迎的编程语言。同时，TypeScript以其优秀的架构设计和高兼容性，成为了2020年增速第二的语言，也是最具潜力的前端语言。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/6aae7069cc88a9bf91b8ccbd9489c272.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;技术管理人员继续保持在研发方面的高参与度。腾讯70%的技术Leader持续输出代码。2020年全年，平均每人输出3.2万行，并且参与142次代码评审。54%的12级及以上技术专家潜心编码，人均输出代码3万余行，参与98次代码评审。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/7a30ba177f87812360e5ed5d540568e5.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h3&gt;开源协同深入人心，开源贡献度居全球科技企业头部&lt;/h3&gt;    &lt;p&gt;2018年技术委员会成立以来，开源协同已成为腾讯在技术发展层面的一个关键词，开放的技术氛围和开放的代码文化逐渐深入人心。腾讯内部开源代码库新增超过57000个，比2019年增长了29%，有超过17000名研发人员参与贡献内部开源项目。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/a746254e016f55714d11a69eae32cf33.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;上线两年时间的腾讯内部技术交流社区“码客”，成为了腾讯研发人员精进技术、交流心得的“根据地”。2020年，码客上有200+个技术圈子助力研发人员学习成长。其中，55%的技术问题能够在提出后的1小时内得到响应，84%的技术问题可以在1天内得到解决。医疗AI、黑灰产人机对抗、Rust语言等新技术话题的关注度不断提升。&lt;/p&gt;    &lt;p&gt;除社区分享交流之外，内部竞赛比拼也是腾讯研发人员自我提升的重点方向，2020年腾讯内部技术赛事吸引了近万名研发人员参与，赛事代码总提交次数达316万次。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/217fc7a2cabb86a47e5fe841907446ef.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;开源向内提升了公司的研发效率，向外则是连接全球开发者共享知识、共建技术的桥梁。2020年是腾讯开源十周年，十年来，腾讯开源项目在Github上的全球Star数每年都有30%的增长，已经成为全球开源贡献最大的科技公司之一。&lt;/p&gt;    &lt;p&gt;腾讯深度参与了数十个国际知名开源项目的贡献，在OpenJDK、KVM等多个顶级开源社区贡献榜中，腾讯均在国内排行第一，作为主要贡献者主导了7个国际知名开源项目的版本发布。腾讯向多个国际顶级开源基金会捐赠了6个开源项目，两大开源项目TencentOS Tiny、TKEstack入选国内首个开源基金会首批捐献项目。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/b196a941a9aec9160405b22c60c44af2.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;今年抗疫期间，腾讯第一时间参与到Linux基金会全新的公共卫生计划LFPH中，作为中国唯一的创始成员单位，为全球合作抗击疫情做出了贡献。&lt;/p&gt;    &lt;h3&gt;用技术连接公益&lt;/h3&gt;    &lt;p&gt;“技术助力公益”则是腾讯技术文化的温暖一面。2020年，腾讯共有1132名研发人员参与了技术公益志愿者活动，总服务时长超过725个工作日，其中最多的一名同事共参与12个志愿项目。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/aeffd45110dd1c08f020df17f36b432f.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;腾讯即视团队积极探索AI安全技术在智慧养老领域的落地，打造智能视频分析解决方案，推出了“智能跌倒监测系统”，当系统发现老人跌倒时，会自动识别老人姿态，并自动报警，让老人得到及时救治，使养老更加智能、高效和安全。&lt;/p&gt;    &lt;p&gt;在新冠肺炎疫情爆发的初期，在全国各地的腾讯人快速响应战疫需求，远程协作交付需求9万个，需求交付效率提升17%，交付了许多助疫新项目。通过各类疫情服务小程序，帮助民众更便捷地获取疫情信息和服务；通过腾讯会议、企业微信、腾讯文档等产品，帮助企业远程协作；通过在线教育的综合解决方案，服务全国超 1亿的师生授课、学习；为科研机构提供人工智能和算力支持，加速医药研究。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/img_convert/6990b0e9923c7abc19700951a47f0e66.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;扫码观看鹅厂研发大数据揭秘👇&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://img-blog.csdnimg.cn/20210319160928224.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTIwODI2Ng==,size_16,color_FFFFFF,t_70"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt; &lt;/p&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61729-%E8%85%BE%E8%AE%AF-%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80-%E5%AE%8C%E6%95%B4</guid>
      <pubDate>Tue, 24 Aug 2021 07:43:01 CST</pubDate>
    </item>
    <item>
      <title>编程所需要的东西</title>
      <link>https://itindex.net/detail/61709-%E7%BC%96%E7%A8%8B-%E9%9C%80%E8%A6%81-%E4%B8%9C%E8%A5%BF</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt;
 &lt;p&gt;本文试图通过介绍编程工作中的部分技能和知识，帮助新进入程序员职业的朋友熟悉未来的工作。特别的，本文将额外提供以Java编程语言下的Spring框架开发Web应用后端逻辑的工作相关的介绍。&lt;/p&gt;
 &lt;p&gt;因本文实在冗长，建议你结合目录挑选自己感兴趣的部分阅读。如果你想通过文本做跳板寻找优质学习资源，希望附录部分我精心挑选的网站和书籍推荐让你满意。&lt;/p&gt;
 &lt;p&gt;对读者的要求：打算把程序员作为职业，还尚未入行的朋友。&lt;/p&gt;
 &lt;h2&gt;工具&lt;/h2&gt;
 &lt;h3&gt;IDE与文本编辑器&lt;/h3&gt;
 &lt;p&gt;文件格式分两种，人能读懂的文本文件，和机器能读懂的二进制文件。源代码是文本文件。&lt;/p&gt;
 &lt;p&gt;编程其实是在用文本编辑器（Text Editor）修改一个个文本文件（源代码文件）的内容，就像你用Windows记事本打开txt文件敲一些文字一样。理论上你可以用记事本写源码，实际上，有一些电脑达人们喜欢用Linux下的  &lt;a href="https://www.vim.org"&gt;Vim&lt;/a&gt;和  &lt;a href="https://www.gnu.org/software/emacs/"&gt;Emacs&lt;/a&gt;写源码，它们相当原始，但若用好快捷键与定制功能，它们会变得非常强大。&lt;/p&gt;
 &lt;p&gt;虽然可以，但是你往往不想用记事本写代码，因为它们不是“专业写代码”的工具软件，用起来不顺手。你需要的是专业帮手——IDE。IDE全称  &lt;a href="https://baike.baidu.com/item/%E9%9B%86%E6%88%90%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/298524"&gt;集成开发环境（Integrated Development Environment）&lt;/a&gt;，是用于提供程序开发环境的应用程序，包括代码编辑器、编译器、调试器等工具，集成了代码编写、语法分析、编译、调试等额外功能。IDE本质上是一个挂载了许多协助编程的额外功能的文本编辑器。&lt;/p&gt;
 &lt;p&gt;一款，你只需要选择并熟练使用一款文本编辑器，你便有了编写源代码的趁手工具。无所谓它是否属于IDE，自己喜欢就好，因为那些编译调试之类的工具大可以在文本编辑器外单独使用。不同文本编辑器的基本功能（即文本编辑功能）大同小异，我们在意的是附加的功能（或丰富的插件市场）、以及完备的快捷键集合（快捷键对程序员来说很重要，可以让我们手不离开键盘完成代码编写，时不时抓鼠标会减慢速度且打断思路）。&lt;/p&gt;
 &lt;p&gt;我目前用了三款文本编辑器以适应不同需求。下面我给出一些我知晓的文本编辑器供你浏览，你也可以自行搜索&amp;quot;editor for programming&amp;quot;：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://www.vim.org"&gt;Vim&lt;/a&gt;和   &lt;a href="https://www.gnu.org/software/emacs/"&gt;Emacs&lt;/a&gt; 这两个文本编辑器很特殊，你可能会在任意一款热门编辑器上看到这两个编辑器的快捷键替换插件。它们的狂热用户们（也同样是业内的老前辈、扛把子们）不愿意因切换文本编辑器而学习新的快捷键，因为快捷键已经变成了某种肌肉记忆，很难改变。这时你应该注意到了，快捷键是某种门派一样的东西，同时学两套快捷键会形成冲突，你只能选一套并把它变成肌肉记忆。快捷键会伴你整个职业生涯，因此慎重选择你的文本编辑器（或者直接学这两款文本编辑器之一的快捷键）。&lt;/li&gt;
&lt;/ul&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;轻量级：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://notepad-plus-plus.org"&gt;Notepad++&lt;/a&gt; 它有一些用户插件来提供额外功能。因为它的启动速度很快，我用它作为最轻量级的文本编辑器，用来临时记录或粘贴一些文本。&lt;/p&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://www.sublimetext.com/"&gt;Sublime Text&lt;/a&gt; 大概是和notepad++并列的一款轻量级文本编辑器，我没用过，我有一个朋友把它作为轻量编辑器使用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;中量级：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt; 简称vscode，中量级的全能文本编辑器（全能指什么编程语言都支持），有丰富的插件市场，你可以把它改造成编写任何编程语言的IDE。我最近发现自己修改格式化的配置文本文件的频率变高了，因此选择了它作为中量级的编辑器，用它阅读与修改markdown（现在正在用它写这篇文章）、json、toml、yaml等格式的文件。事实上许多前端开发者使用vscode作为编写经典前端代码（HTML+CSS+JavaScript）的IDE。&lt;/p&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://atom.io"&gt;Atom&lt;/a&gt; 大概是与vscode相对的中量级编辑器，还是我的那个朋友会用它，我个人并不了解，姑且列出来。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;重量级（IDE）：&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://visualstudio.microsoft.com/"&gt;Visual Studio&lt;/a&gt; 微软出品，商业产品，被戏称为宇宙第一IDE（但我没搜到原因）。据我所知可以编写.Net、C、C#、C++、Python、Android语言。体积超级大，启动超级慢，用起来…我感觉不到好在哪里，可能我没有深度使用。有插件市场。当你看到安装界面中各种数据分析、机器学习等高大上的可选项时，你会感受到这IDE真的体现了微软一如既往的商业又专业的味道。&lt;/p&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://www.jetbrains.com/"&gt;JetBrains IDEs&lt;/a&gt; JetBrains是一家公司，它旗下有诸多用于编写不同语言程序的IDE：IntelliJ IDEA，GoLand，PyCharm，WebStorm，RubyMine…用来编写Java、Go、Python、JavaScript、Ruby等主流编程语言。这些IDE基本共用一套快捷键。有插件市场。我是JetBrains的忠实用户，因为我感觉JetBrains的IDE开发者们懂得开发者需要什么，产品中的小细节和功能体现着人性化的用心。我用JetBrains IDEs作为重量级编辑器，因为它们启动要好一会。我还选择了JetBrain IDEs快捷键“门派”，还在vscode上安装了&amp;quot;IntelliJ IDEA Keybindings&amp;quot;插件。&lt;/p&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://www.eclipse.org/eclipseide/"&gt;Eclipse IDE&lt;/a&gt; 开源的多编程语言IDE，有插件市场。因为它是开源的，有不少商业第三方定制化IDE都是基于它改造的。我在大学时一直用它编写Java和C++，真的很不错，直到我在实习时试着换了IntelliJ IDEA，之后再也没启动过它。&lt;/p&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://developer.android.com/studio/install"&gt;Android Studio&lt;/a&gt; 谷歌出品，看名字就知道是编写安卓系统应用程序的IDE。我用过一点，只知道它因为时不时要连接谷歌的网站更新索引之类的什么东西，在国内想用它要配置好离线依赖。另外关于它有许多meme损它启动慢吃内存多，比如：我是安卓工程师，我每天早晨来公司先启动Android Studio，然后把咖啡壶放在机箱上煮咖啡，等我喝完整壶咖啡，我就可以开始工作了。&lt;/p&gt;
&lt;/li&gt;
    &lt;li&gt;
     &lt;p&gt;      &lt;a href="https://developer.apple.com/cn/xcode/ide/"&gt;Xcode&lt;/a&gt; 这是苹果公司推出的编写苹果各产品上的应用（现在苹果产品有了共用芯片，只需编译出一种应用）的IDE，我没有用过，也没有用它的苹果开发者朋友，所以我不知道它怎么样。按照苹果一贯的风格，也许这个IDE很顺滑，搜了下也没找到太多说xcode本身不好用的抱怨。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;h4&gt;Java IDE&lt;/h4&gt;
 &lt;p&gt;主流Java IDE有两款，  &lt;a href="https://www.eclipse.org/eclipseide/"&gt;Eclipse IDE&lt;/a&gt;和  &lt;a href="https://www.jetbrains.com/idea/"&gt;JetBrains IntelliJ IDEA&lt;/a&gt;，而且从2018年开始，  &lt;a href="https://visualstudio.microsoft.com/"&gt;Visual Studio&lt;/a&gt;也能够（通过连接vscode）支持Java了。从上文你可以看出，我是属于IDEA派的。&lt;/p&gt;
 &lt;p&gt;在我看来，Eclipse比不过IntelliJ IDEA的关键不在功能上，它快捷键和插件市场都很完备，甚至因为是开源的，所以你可以把它DIY成符合自己习惯的专属IDE。但它败也败在开源上：没有漂亮又符合人机交互理念的界面（这需要专业的UI设计师参与，而开源项目成员中往往只有程序员）；默认配置较不合适（可定制化和开箱即用不冲突，但需要认真琢磨出厂默认配置）…细节，它败在了细节上，“The devil is in the detail”。&lt;/p&gt;
 &lt;p&gt;截至撰文我已经快三年没有用过Eclipse了，因此不了解它的近况，也许你可以给它一个机会，试用一下？还有宇宙第一IDE…也可以试一下，我猜配置会很麻烦。&lt;/p&gt;
 &lt;p&gt;另外，如果你选择使用IDEA，IDEA从2016年开始内置了一个学习快捷键的插件Learn，可以看  &lt;a href="https://blog.jetbrains.com/idea/2016/12/ide-features-trainer/"&gt;这篇官方介绍&lt;/a&gt;学习如何使用。&lt;/p&gt;
 &lt;h3&gt;浏览器、搜索引擎与搜索思维&lt;/h3&gt;
 &lt;h4&gt;浏览器&lt;/h4&gt;
 &lt;p&gt;你在用火狐（FireFox）浏览器吗？建议从Mozilla官网（https://www.mozilla.org/zh-CN/firefox/new/ ）下载，而非北京谋智火狐信息技术有限公司的网站（http://www.firefox.com.cn/ ），这两个的区别可以看  &lt;a href="https://zhuanlan.zhihu.com/p/138034888"&gt;此文章&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;你必然已会使用浏览器和搜索引擎，我在此补充几个浏览器的别样用法，也许可以帮助你更好的使用浏览器：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;给PDF阅读材料加书签。现代浏览器内置了PDF阅读器，配合创建用读书笔记作为标题的书签，你可以为本地硬盘上的某个PDF格式的电子书建立自己的书签目录，方便以后连同互联网书签一同查阅。（遗留问题：那个PDF文件不方便随意更换位置，因为浏览器书签的地址是文件系统的路径）&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;插件市场。主流浏览器都有自己的插件市场，你可以用插件让上网体验更好。值得一提的是    &lt;a href="https://www.tampermonkey.net/"&gt;Tampermonkey&lt;/a&gt;插件，它是JavaScript脚本管理器，有自己的脚本市场。你可以把脚本理解为轻量的插件，毕竟在浏览器中就是Js脚本在操作页面。通过Tampermonkey，你可以向某个网页中添加额外的脚本来实现额外功能，而浏览器插件可以为浏览器本身添加额外功能。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;书签功能。主流浏览器支持同时打开一个书签目录下的所有书签，这样你可以用一个目录来代表一个工作环境，需要时一键开启所有Tab。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;浏览器快捷键。页面内查找，Tab左右切换显示，将光标置入搜索栏、切换搜索所使用的搜索引擎…&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;搜索栏。主流浏览器的搜索栏可以同时搜索书签和历史记录或使用搜索引擎。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;历史记录。历史记录很重要，虽然它会泄露隐私，但我从不清理它。只要有历史记录，我就能搜到曾经浏览过的网页，节省了重新搜索的时间。（如果我想进行隐私上网，我会打开虚拟机中的Tor浏览器…）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;搜索引擎&lt;/h4&gt;
 &lt;p&gt;为什么我不使用百度？有几个原因，仅就编程方面的搜索来说，使用百度会搜索到大量  &lt;a href="https://baike.baidu.com/item/%E5%86%85%E5%AE%B9%E5%86%9C%E5%9C%BA/6651503"&gt;内容农场&lt;/a&gt;网站，浪费时间却找不到有用的信息。&lt;/p&gt;
 &lt;p&gt;我使用微软的Bing（   &lt;a href="https://cn.bing.com/"&gt;https://cn.bing.com/&lt;/a&gt; 这是国内特供版网址，Bing的通常网址是   &lt;a href="https://www.bing.com/"&gt;https://www.bing.com/&lt;/a&gt; ，猜测微软是根据GeoIP来判断该重定向到哪一个网址）。Bing搜索引擎  &lt;a href="https://help.bing.microsoft.com/apex/index/18/en-US/10002"&gt;有一些简单的高级搜索语法&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;另外Bing有两个版面：国内版和国际版，你可以简单理解为国内版搜索结果中的中文结果优先，而国际版是其他语言的结果优先。你可以轻松切换两个版面来实现同时搜索一个关键字的中文和英文搜索结果。就编程方面的搜索来说，你不能肯定一个问题在  &lt;a href="https://www.csdn.net/"&gt;CSDN&lt;/a&gt;或  &lt;a href="https://www.cnblogs.com/"&gt;博客园&lt;/a&gt;上没人写过踩坑文章，也不能肯定  &lt;a href="https://stackoverflow.com/"&gt;StackOverflow&lt;/a&gt;上必然有现成的回答。既然我们会中文和英文两种语言，就应该利用这种优势，同时使用两种语言搜索同一个问题（用中文关键字搜中文结果，vice versa），这样找到答案的概率会更高。&lt;/p&gt;
 &lt;p&gt;用Bing搜索中文编程内容能逃避内容农场吗？不完全能。但是因为百度和Bing是两个公司单独计算网页排名，而刷排名的坏东西的目标一般是百度排名，因此Bing比百度的情况好一些。事实上不止中文搜索结果有内容农场这个坏现象，几乎所有语言的搜索结果都有，这需要搜索引擎公司们自己想办法反制。&lt;/p&gt;
 &lt;h4&gt;搜索思想&lt;/h4&gt;
 &lt;p&gt;记得把使用搜索引擎查找互联网内容的思想迁移到各个使用场景上。想找存在电脑上的文件？Windows系统可以按一下Win键打开搜索栏。想在网页内找关键字？使用浏览器的网页内搜索。想在文本编辑器里搜索本文件中的内容？文件内搜索。在IDE里搜索整个项目目录的内容？项目内搜索。&lt;/p&gt;
 &lt;p&gt;小提示，本文中所有超链接标注的名词都可以作为搜索关键字，我只是每个名词搜了一个质量差不多的网页附上链接…多花点时间应该能搜到质量更好的。&lt;/p&gt;
 &lt;p&gt;搜索的最大目的始终不变：节约寻找答案的时间，因为寻找答案过程本身没有产出。&lt;/p&gt;
 &lt;h2&gt;编程语言&lt;/h2&gt;
 &lt;blockquote&gt;
  &lt;p&gt;文本命令：“老王 吃 苹果” -&amp;gt; 执行动作：老王在客厅桌子上找到一个苹果，吃掉。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;编程分为两个部分，人编写文本格式的  &lt;a href="https://baike.baidu.com/item/%E6%BA%90%E4%BB%A3%E7%A0%81/3969"&gt;源代码&lt;/a&gt;，编译器|解释器（下面统一使用编译器做指代，即使实际上是解释器）负责将源码编译成机器能看懂的  &lt;a href="https://baike.baidu.com/item/%E6%8C%87%E4%BB%A4%E9%9B%86/238130"&gt;指令&lt;/a&gt;。你可以为任何有结构的文本编写一个编译器，文本就是命令的内容，简单说明了要做什么事，编译器按着文本写出详细指令，计算机按着详细指令做事。举个例子：打仗，司令部传达过来的命令很简单，然后战场上的指挥官会以一两行命令为大纲来拟定详细的计划，士兵们可以按照详细计划行动。&lt;/p&gt;
 &lt;p&gt;编译器和战场指挥官一样，帮你补足你在源码中没有提到的细节。注意看上文那句老王吃苹果，源码里没有提到老王在吃苹果前需要找到苹果，也没提到如何找苹果，但最后执行的动作中是存在找苹果的步骤的。这引出了目前主流的两类  &lt;a href="https://baike.baidu.com/item/%E7%BC%96%E7%A8%8B%E8%8C%83%E5%9E%8B/1475451"&gt;编程范式&lt;/a&gt;：命令式（imperative）和声明式（declarative），按命令式编程设计的编程语言需要指明执行的细节，比较唠叨，而声明式只需要指定要做什么事，细节由编译器补足。事实上现代语言都混合了不同的编程范式，更加灵活。上面老王吃苹果的例子是偏向于声明式的。&lt;/p&gt;
 &lt;p&gt;依我看，编译器越“聪明”，越懂得从寥寥数行源代码里猜测程序员的意图并产生出程序（如JavaScript，尽一切努力不报编译错误，即使会输出很离谱的结果）；反之，编译器越“木头脑袋”，越要求源代码“符合语法语义规则”，源代码越冗长（Java，说的就是你）。要么人写源码多费功夫，要么编译器编译源码多费功夫，仅此而已。编译这门手艺的术语叫  &lt;a href="https://baike.baidu.com/item/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86/4194"&gt;编译原理&lt;/a&gt;。如果你有一个无限智能的编译器，就可以用自然语言即中文英文等，去指挥计算机做事。可惜，编译器是有极限的，语言越复杂，编译器所要做的工作越多，人就越难去编写这个编译器（编译器是个特殊的程序，终究还是要人来写），最后我们折衷得到了目前这些编程语言和对应的编译器。&lt;/p&gt;
 &lt;p&gt;不同编程语言适合做不同的事，就像木匠工具箱里不同工具用来做不同的工作，没有高下之分，也没必要搞工具崇拜。因为有些编程语言底层设计特性是相互冲突的，而且现在已有这么多用得好好的不同语言编写的程序支撑着我们的世界，所以从设计理论和改造成本上看永远不会有一个全能的编程语言可以代替所有语言。顺带一提，每个编程语言的命令表达能力都是相等的，因为它们都是  &lt;a href="https://www.zhihu.com/question/20115374"&gt;图灵完备&lt;/a&gt;的，所以“一个语言可以代替另一个语言完成某个具体工作”这种论断是正确的，只是用不同语言间写源码完成相同的某功能花的力气可能差个几倍几十倍都有可能。&lt;/p&gt;
 &lt;p&gt;编程语言本身，和编程语言下的框架、第三方库，只是工具而已。它们一般有个说明书教你如何使用，你可以在网上搜索（官方教程或注释生成的API文档）。有时你能在IDE里看它们源码的注释，注释也会详细地解释这个代码要做什么，就不用搜索了。事实上，在商业编程刚刚起步时，每个程序员手边都有一本关于所用编程语言提供的库API与语法的手册，他们要像查字典一样使用这本工具书，因为那个时候没有互联网，没有在线文档。不要过多花时间在了解工具的使用上（通读文档，试图记住所有库API，类似背诵字典），有需要就用关键字去搜索，越用越熟，就能记住了。&lt;/p&gt;
 &lt;h3&gt;学一门编程语言&lt;/h3&gt;
 &lt;p&gt;自然语言有听说读写四个技能要求，编程语言是书面语言，学习一门编程语言仅要求读写两个方面，另外额外多一个：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;学习它的编程思想、语法和关键字，即读法写法。&lt;/li&gt;
  &lt;li&gt;知道语言的原生库提供了哪些API（帮你省事的现成的功能），知道如何使用或去哪查如何使用这些API（小提示：库代码的注释，网上的在线文档，或教程）。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;推荐一个入门图书系列——  &lt;a href="https://baike.baidu.com/item/Head%20First/13972734"&gt;Head First&lt;/a&gt;。由 O’Relly 出版社发行的一系列教育书籍，它强调以特殊的方式排版，由大量的图片和有趣的内容组合构成，而达到非疲劳的沉浸式学习效果。我读过它的  &lt;a href="https://book.douban.com/subject/2243615/"&gt;设计模式&lt;/a&gt;书，很有趣。&lt;/p&gt;
 &lt;p&gt;还有几个在线中文教程网站：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://www.ruanyifeng.com/blog/archives.html"&gt;阮一峰的网络日志&lt;/a&gt;，    &lt;a href="https://www.liaoxuefeng.com/"&gt;廖雪峰的官方网站&lt;/a&gt; 这两位为中文互联网提供了大量易懂的入门级IT方面的教程，如果你想通过中文教程了解一个新东西，可以先在搜索栏前面输上&amp;quot;阮一峰&amp;quot; “廖雪峰”+新东西关键词，看他俩有没有写过。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://www.runoob.com/"&gt;菜鸟教程&lt;/a&gt; 特别的，如果你想通过中文教程学习某种编程语言|工具，并快速写出能跑的程序|运行环境，建议阅读这个网站上的教程，这上面基本涵盖了所有常用的编程语言和工具的快速入门教程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;Java编程语言&lt;/h4&gt;
 &lt;p&gt;Java中规中矩，有一套严格的语法规则。通过学习复杂的语法规则，新手可以了解编程语言的一些共性，我还是比较推荐新手通过Java了解面向对象编程的。最近几年Java想要变年轻，迭代变快了（半年一个新版本），也  &lt;a href="https://dzone.com/articles/a-guide-to-java-versions-and-features"&gt;引入了不少其他语言中好用的特性&lt;/a&gt;。你可以把它理解为保守谨慎的，只会在一个特性被其他语言实践验证后才考虑引入。但是  &lt;a href="https://www.zhihu.com/question/360985479"&gt;业内不少公司都停留在了Java8&lt;/a&gt;，主要是公司们不想还遗产代码的债。&lt;/p&gt;
 &lt;p&gt;如果你工作项目使用的是Java8，推荐你了解一下Java8引入的  &lt;a href="https://www.w3cschool.cn/java/java-stream-api.html"&gt;Stream API&lt;/a&gt;。如果你需要Java语言的工具书，建议使用  &lt;a href="https://book.douban.com/subject/26880667/"&gt;Java核心技术&lt;/a&gt;，上下两卷两本大厚书，你也可以通过通读它来学习Java。&lt;/p&gt;
 &lt;h3&gt;第三方库&lt;/h3&gt;
 &lt;p&gt;在现代，没有任何编程项目是从零开始的。每个编程语言都有自己的第三方库仓库（repository），比如Python的  &lt;a href="https://pypi.org/"&gt;pypi&lt;/a&gt;，Java的  &lt;a href="https://mvnrepository.com"&gt;Maven Repository&lt;/a&gt;等。有些公司为了保证项目所使用的第三方库的稳定，会建立并维护内部的仓库。库（library）或者叫包（package），就是别人写好的功能。你可以把库从在线的仓库下载到本地，就不需要自己编写了对应的功能了。这算盗窃吗？事实上，每个库（每个开源软件项目，库属于一种特殊的专门给程序员用的项目）都有  &lt;a href="https://zhuanlan.zhihu.com/p/322438947"&gt;许可证&lt;/a&gt;，许可证是一个声明，它规定了该库可在什么情况下使用，比如有些许可证就禁止商业使用。&lt;/p&gt;
 &lt;h4&gt;框架&lt;/h4&gt;
 &lt;p&gt;  &lt;a href="https://baike.baidu.com/item/%E8%BD%AF%E4%BB%B6%E6%A1%86%E6%9E%B6/1471931"&gt;框架（framework）&lt;/a&gt;是一种特殊的项目，顾名思义，它提供了编写某类程序的大致框架。当你自己动手写练手的项目，你会发现写项目最难的在于如何组织各个组件间的关系，我该把这个文件放在哪个目录？我该把这段逻辑放在哪个组件？谁调用谁？选择一款框架，了解它关于组织组件的建议，你便只需专注你的功能。&lt;/p&gt;
 &lt;h4&gt;架构&lt;/h4&gt;
 &lt;p&gt;  &lt;a href="https://zhuanlan.zhihu.com/p/86052051"&gt;架构（architecture）与框架的联系与区别&lt;/a&gt;，  &lt;a href="https://zhuanlan.zhihu.com/p/3455520"&gt;微服务架构&lt;/a&gt;和单体架构。如果想了解更多，请自行搜索。&lt;/p&gt;
 &lt;h5&gt;Java下的Spring框架&lt;/h5&gt;
 &lt;p&gt;“  &lt;a href="https://baike.baidu.com/item/spring/85061"&gt;Spring框架&lt;/a&gt;（  &lt;a href="https://spring.io/"&gt;官方网站&lt;/a&gt;）是最受欢迎的企业级 Java 应用程序开发框架。” 听听，它可没吹牛。目前Spring已经发展成了完整的生态（我不喜欢这个词，它代表封闭，但Spring是很开放的），绝大部分新的Java应用项目选择Spring生态作为基础框架是相当自然的事。&lt;/p&gt;
 &lt;p&gt;使用Spring对程序员编程有很多实际的好处，不仅是企业级的那些什么可伸缩之类的高大上特点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Spring有完备的基础功能，可以让你只写几行代码就配置好一个复杂的功能，如果不用Spring，也许需要一整天去下载配置相同功能的库或其他外部系统。&lt;/li&gt;
  &lt;li&gt;Spring开箱即用，简化了项目配置的过程（配置真是一个消磨精力的事）。&lt;/li&gt;
  &lt;li&gt;Spring统一了Java业界的标准，你会发现各种项目和库都支持对Spring的兼容，不需要你来单独做兼容了。&lt;/li&gt;
  &lt;li&gt;Spring对原本代码的侵入相当少。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;Spring增加了学习成本。我已经见过好几个人，当他们听到除了学Java还要学Spring时，眉头突然皱起来了…但是…总要多学一些工作中会用到的常用库，只是Spring稍微有点大，稍微。而且你这样想，学了Spring，就能享受上面提到的大量好处，很值。最低限度的学习，只需要了解Spring配置文件和Spring MVC的注解就行了。&lt;/p&gt;
 &lt;h4&gt;工具库&lt;/h4&gt;
 &lt;p&gt;按照我的分类，工具库通过API提供对象处理功能，你可以像使用语言本身的原生库API一样使用这些工具库。举个例子，因为Web应用的普及，每个语言都会遇到对  &lt;a href="https://baike.baidu.com/item/JSON/2462549"&gt;Json格式&lt;/a&gt;文本的解析、映射等操作的功能需求。比如Java语言没有在其原生库中提供这些Json格式数据的操作，于是Java社区中应需求出现了好几个Json处理工具库。&lt;/p&gt;
 &lt;h5&gt;Java常用的工具库&lt;/h5&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;Json处理：    &lt;a href="https://github.com/FasterXML/jackson"&gt;Jackson&lt;/a&gt;，    &lt;a href="https://github.com/alibaba/fastjson"&gt;FastJson&lt;/a&gt;，    &lt;a href="https://github.com/google/gson"&gt;Gson&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;数据处理（狭义上的工具类）：    &lt;a href="https://github.com/dromara/hutool"&gt;HuTool&lt;/a&gt;，    &lt;a href="https://mvnrepository.com/artifact/org.apache.commons/commons-lang3"&gt;apache commons-lang3&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;日志记录：    &lt;a href="https://www.tutorialspoint.com/slf4j/slf4j_vs_log4j.htm"&gt;slf4j&lt;/a&gt;（它是接口，不能独立工作），    &lt;a href="https://logging.apache.org/log4j/2.x/"&gt;log4j&lt;/a&gt;（它是一种实现，可以独立工作）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;每一类有其中之一就满足需要了（slf4j除外）。如果你中途加入一个项目组，你需要辨别该项目使用了哪些工具库，并继续使用，不要添加相同功能的其他工具库。&lt;/p&gt;
 &lt;h4&gt;依赖管理工具&lt;/h4&gt;
 &lt;p&gt;当你使用某个第三方库或框架，你便对它产生了“依赖”，有时我们把它名词化，项目依赖的第三方库就起个别名叫“依赖”（dependency）。当然你使用的第三方库本身也可能有依赖，于是会形成一个以你的项目为根的依赖树。&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://cloud.tencent.com/developer/article/1122163"&gt;依赖管理工具&lt;/a&gt; 源自人们想要自动化（程序员爱自动化）依赖管理的需求。一般依赖管理工具和第三方库仓库是相辅相成的，本地的依赖管理工具会将第三方库仓库的网址作为远程仓库，从上面下载第三方库文件。部分依赖管理工具还拥有工程构建的功能，你简单理解为编译、打包与部署项目。&lt;/p&gt;
 &lt;h5&gt;Java依赖管理工具&lt;/h5&gt;
 &lt;p&gt;主流的Java依赖管理工具有两个：  &lt;a href="https://maven.apache.org/"&gt;Apache Maven&lt;/a&gt;和  &lt;a href="https://gradle.org"&gt;Gradle&lt;/a&gt;。区别？看  &lt;a href="https://cloud.tencent.com/developer/article/1122163"&gt;这篇中文文章&lt;/a&gt;或者  &lt;a href="https://gradle.org/maven-vs-gradle/"&gt;这篇Gradle自己写的对比&lt;/a&gt;或者  &lt;a href="https://www.zhihu.com/question/276078446"&gt;这篇知乎回答&lt;/a&gt;。&lt;/p&gt;
 &lt;h4&gt;往项目里添加依赖&lt;/h4&gt;
 &lt;p&gt;以Java语言项目为例，单个现代项目往往被分成多个包（package，对，就是上面提到的第三方库的一种别名），包之间的有一定的隔离性，依赖也是各个包独立管理的。&lt;/p&gt;
 &lt;p&gt;在编程工作中，你会碰到想使用某个第三方库（或者同一个项目的另一个包）的功能，而目前项目里还没有这个依赖的情况。此时你应该斟酌一下你想在哪个包的代码中使用第三方库，对应要把新的依赖放在哪个包的依赖配置文件里。这个问题没有固定答案，原则就是尽可能少增加复杂度。如果你拿不准，咨询一下有经验的程序员的意见。&lt;/p&gt;
 &lt;p&gt;有一个编程原则叫“针对抽象而非具体编程”，我们应该尽可能减少程序中不必要的依赖，因为它会妨碍未来的变更的灵活性。反面例子：依赖是一个树型结构，有些库没有好好遵守这个原则，依赖于某个具体的库，导致使用该库的其他库也  &lt;a href="https://zhuanlan.zhihu.com/p/34427614"&gt;传递依赖&lt;/a&gt;了那个具体库。结果越来越少的库为了避免麻烦选择使用这些库，最终维护者因无人使用放弃了维护，项目死亡了。不可在未来灵活替换的依赖对项目来说是致命的病毒。&lt;/p&gt;
 &lt;h2&gt;单元测试与Debug&lt;/h2&gt;
 &lt;p&gt;关于单元测试的定义与用途，读  &lt;a href="https://baike.baidu.com/item/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/1917084"&gt;百度百科的介绍&lt;/a&gt;。关于单元测试思想方面的内容，可以读  &lt;a href="https://boholder.github.io/blogs/tdd-tips-zh/"&gt;这篇文章&lt;/a&gt;。简单说，单元测试是程序员为了检测自己写的功能是否合乎预期而编写的额外的代码。把这种测试的行为用代码持久化下来有不少好处，比如可以重复运行。单元测试不是必须的，写它可能要费不小的事，所以有时要斟酌是否值得写单元测试，主要看写测试的时间能否弥补未来一遍遍手动测试的时间。&lt;/p&gt;
 &lt;p&gt;一般使用现成的单元测试库来编写单元测试用例。单元测试库属于工具库，也是百花齐放多种多样。现代的框架，比如  &lt;a href="https://docs.spring.io/spring-framework/docs/5.3.2/reference/html/testing.html"&gt;Java的Spring&lt;/a&gt;，  &lt;a href="https://projectreactor.io/docs/core/release/reference/index.html#_testing_a_scenario_with_stepverifier"&gt;Java的Project Reactor&lt;/a&gt;（我相信其他语言的编程框架也有，奈何我没用过无法举例）都会比较贴心地随框架附带专用的测试工具包，帮助你更顺利地使用框架。如果一些热门的库本身没有提供测试工具包，也会有热心程序员提供第三方的辅助测试包…社区就是这个样子，有需求就往往有对应的现成解决方案。&lt;/p&gt;
 &lt;p&gt;单元测试库的API是额外的学习成本，所以部分程序员不喜欢写单元测试，或者写的单元测试无法自动化运行，需要人盯着看日志输出结果。还有一种不写单元测试的情况是，不知道怎么写、或者没有单元测试库的依赖（内网开发时会遇到）或项目使用特殊的商业框架，因此有心无力写不出单元测试。对于前者，斟酌一下学习并写出单元测试的成本；对后两者…默哀，放弃单元测试吧，争取用其他工具把集成测试的测试用例自动化。&lt;/p&gt;
 &lt;p&gt;Debug，或者叫调试，是在程序尚未被打包部署前，把程序在本地的环境里通过调试器等工具的帮助下运行起来，细致观察程序内部的每一步行为，以确定程序行为符合预期。现代IDE中一般会集成调试器和Debug功能，因此通常的实践是在IDE中，以debug模式运行单元测试用例或整个程序。具体怎么用要查IDE的官网文档或自己动手试用，别忘了学习对应的快捷键。&lt;/p&gt;
 &lt;h3&gt;Java的单元测试包和IDEA的Debug功能&lt;/h3&gt;
 &lt;p&gt;我会介绍我现在用的测试库，你也可以选择自己整理一套喜欢的测试库。自从我学会借助测试库写单元测试后只用过两种语言：Java和JavaScript。写JavaScript单元测试我是用FaceBook开源的  &lt;a href="https://jestjs.io/"&gt;Jest&lt;/a&gt;，本身功能就挺全的，有时会安装基于它的插件包来扩展功能。&lt;/p&gt;
 &lt;p&gt;Java就复杂点了，我现在有一套会用的Java测试库，搭配使用它们可以完成基于Spring框架的应用的各种单元测试工作。关键是它们可以用一个  &lt;a href="https://stackoverflow.com/questions/61117933/whats-the-difference-between-spring-boot-test-vs-spring-boot-starter-test"&gt;spring-boot-starter-test&lt;/a&gt;依赖全部导进来，省事（而且公司的内部仓库里大概率会有）。主要有这么几个包：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://junit.org/junit5/"&gt;Junit&lt;/a&gt; 它是一个测试框架，很多其他测试包会基于它编写，角色类似上面的Jest。公司项目中有些用Junit4，但它比较老（10年以上），现在出了Junit5，注意两者间用法的   &lt;a href="https://www.baeldung.com/junit-5-migration"&gt;不同&lt;/a&gt;(   &lt;a href="https://www.baeldung.com/junit-5-runwith"&gt;@Runwith兼容&lt;/a&gt;，   &lt;a href="https://www.baeldung.com/mockito-junit-5-extension"&gt;Mockito in Junit5&lt;/a&gt;)。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://docs.spring.io/spring-framework/docs/5.3.2/reference/html/testing.html"&gt;Spring提供的测试组件&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/features.html"&gt;Spring Boot提供的测试组件&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://site.mockito.org/"&gt;Mockito&lt;/a&gt; Mock，嘲弄，在测试中我们说mock被测试组件的依赖，是指把调用被试和被被试调用的组件，全部用测试库提供的API换成假的空壳子。这样我们可以控制输入被试的数据，和被试调用其他组件时其他组件的反应。举个例子，我们可以mock一个数据库API，然后写明如果被试调用该API，抛出异常，测试被试在数据库异常时的异常处理逻辑。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="http://hamcrest.org/JavaHamcrest/"&gt;Hamcrest&lt;/a&gt; 小巧的断言（assertion）框架，作为Junit断言API的替换品，提供可读性更高的API。（Hamcrest仅是判断一个对象的值怎样怎样，如果想判断其他的比如抛出异常，用Junit的API更方便。）断言是单元测试自动化的核心：自动判断运行结果是否符合预先编写好的结果。例子：assertThat(result,is(“expected-output”))，期望result这个变量是个字符串格式，值是&amp;quot;expected-output&amp;quot;。如果没通过判断，断言API会抛出异常，让这个测试用例失败，并打印出失败的原因。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;写Java的测试用例时要留意一下IDEA Auto Import的哪个库的注解|API，不能无脑回车导入。因为都是用在测试场景，不少库有同名或名称相似注解|API（例：org.junit.Test和org.junit.jupiter.api.Test）。注解|API间需要搭配使用，混用可能使某个注解|API失效导致测试运行失败，报错也总是云里雾里，无法快速发现是注解用错的原因。这个需要多读点教程和文档弄明白常用的注解|API间的关系，又是一笔额外的学习成本。&lt;/p&gt;
 &lt;p&gt;顺带一提，上面的Hamcrest基于的测试理念是“基于用例的测试”，即提前写好预测的结果，一个用例只能预测一种行为的一个输入和对应的结果。现在业内出了一个还没流行起来的新测试理念——“基于特性的测试”，简单说就是测试框架根据给定的范围，自动生成测试数据，一个测试用例可以用大量的随机数据测试被测组件的抽象行为。但是这种测试用例特别难写，主要是难以抽象总结被测组件的行为。感兴趣的话，可以阅读  &lt;a href="https://boholder.github.io/blogs/unit-test-methods/"&gt;这篇文章中的后两节&lt;/a&gt;。&lt;/p&gt;
 &lt;p&gt;IDEA的debug功能挺强大的，尤其是debug中途修改变量值，可以作为写不出单元测试时的替代操作。它还允许现改代码并编译替换运行中的类文件，允许drop堆栈来实现“反悔”回到上一步调用处，使得程序可以在一段逻辑中反复运行，测试各种情况。可以用上文提到的Learn插件学习IDEA的debug功能，或者读  &lt;a href="https://www.jetbrains.com/help/idea/debugging-code.html"&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
 &lt;h2&gt;编程思想&lt;/h2&gt;
 &lt;h3&gt;编程原则&lt;/h3&gt;
 &lt;p&gt;编程原则（principle）是一些抽象化的格言，用来规范提醒程序员的编程行为实践。比如提到面向对象编程实践就是  &lt;a href="https://www.baeldung.com/solid-principles"&gt;SOLID五大原则&lt;/a&gt;。还有什么ETC（easy to change）原则，DRY（do not repeat yourself）原则，FEP（fail early，不够仨单词的原则会把原则的P也算进缩写）等。这些原则不是术语，算是程序员间的共识（或者叫黑话），而且可能同一原则有不同解释方式和缩写变体。&lt;/p&gt;
 &lt;p&gt;编程原则试图从抽象层面对抗未来必然会出现的代码变更，尽可能让代码变更变得容易，少修改已经写好的代码。我借用一下忘了哪本书中提到的观点：把软件开发学科与建筑学科相类比的观点（软件架构architecture一词是从建筑学借来的，软件设计模式也是基于建筑学典著  &lt;a href="https://book.douban.com/subject/1157145/"&gt;建筑模式语言&lt;/a&gt;启发出现）已经过时，因为开发流程不再是严格遵守计划表的  &lt;a href="https://zhuanlan.zhihu.com/p/81383161"&gt;瀑布式开发&lt;/a&gt;。软件开发更像园艺，你把种子种在花园里，不能控制植物的自然生长（未来的需求变更），只能通过园艺技术（  &lt;a href="https://book.douban.com/subject/4262627/"&gt;代码重构&lt;/a&gt;）修剪、梳理花园。&lt;/p&gt;
 &lt;p&gt;编程原则无关编程语言，甚至在编程之外的工作和生活中都能借鉴，算是一种行为哲学。希望你能聆听这些编程原则，写出易于变更的代码。&lt;/p&gt;
 &lt;h3&gt;设计模式&lt;/h3&gt;
 &lt;p&gt;请看这篇文章了解设计模式：  &lt;a href="https://draveness.me/holy-grail-design-pattern/#fnref:5"&gt;圣杯与银弹 · 没用的设计模式&lt;/a&gt;。以我的经验而言，设计模式最大的用途是减少沟通成本，冲这个用途值得花时间学一些常用的设计模式的概念。&lt;/p&gt;
 &lt;h2&gt;完成编程工作所需的外围技能&lt;/h2&gt;
 &lt;p&gt;你可以在网络上搜索什么&amp;quot;Xxx职业技能树|知识图谱|技术栈&amp;quot;，有许多人试图总结出这么一个东西，但是要我说，看看  &lt;a href="https://www.ruanyifeng.com/blog/2021/06/drunk-post-of-a-programmer.html"&gt;这位有10年间有多岗位经验的reddit老哥&lt;/a&gt;是怎么描述这个行业的技术栈的：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;技术栈不重要。技术领域有大约 10-20 条核心原则，重要的是这些原则，技术栈只是落实它们的方法。你如果不熟悉某个技术栈，不需要过度担心。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;英语&lt;/h3&gt;
 &lt;p&gt;四级，英语四级水平的英语读写绝对够用了，只要日常工作没有听说英语的需求。除开每次写方法名都要愣一下单词怎么拼（记得在IDE上安装拼写纠正插件，别闹了笑话）以外，编程工作不是太要求英语水平。编程相关的问答、教程、官网、文档…都不会用很生僻的单词，大家都倾向于用最简单的单词表达意图。唯二的例外是博客和论文。博客是口语化的文章，我碰到过不少次作者想举个例子或者玩笑幽默一下，但把我卡住用字典查里面的生词，继续读下去明白了只是一个例子|玩笑的无奈情况。论文…论文的生词主要是术语，数学术语、机器学习术语、数据分析术语等。&lt;/p&gt;
 &lt;p&gt;有些中文程序员不喜欢英语材料，可能是因为不习惯或者读得慢浪费时间。其实我读得也不快，有时候也因为怕浪费时间选择找中文材料去读。但是如果你去读，你会发现英语材料是可以读懂的，读懂就足够了，多读一些就能克服不习惯的心理障碍，也能慢慢提高阅读速度。&lt;/p&gt;
 &lt;p&gt;你还可以借助科技的辅助：  &lt;a href="https://www.deepl.com/en/translator"&gt;DeepL Translate&lt;/a&gt;等翻译网站、浏览器翻译插件和IDE翻译插件等可以帮你弥补英语能力的不足。&lt;/p&gt;
 &lt;h3&gt;版本控制工具&lt;/h3&gt;
 &lt;p&gt;源代码是文本文件，对吧。  &lt;a href="https://baike.baidu.com/item/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6/3311252"&gt;版本控制工具&lt;/a&gt; 是用来管理文本文件的内容变更的功能，类似Microsoft Office Word里的Review（审计？）功能，就是写论文时导师用来帮你打批注、改论文的那个。我见过有写手用它管理txt格式的小说文件，写代码真的和写作很像啊。版本控制工具不仅会记录哪个人对哪个文件做了什么改动，还允许你独立地管理这些改动，确保多人协作的同时维护源代码文件不会被搞成乱糟糟的“源码-v02的副本(1)(2)(1)”这副模样。&lt;/p&gt;
 &lt;p&gt;有两款主流的版本控制工具工具：  &lt;a href="https://www.runoob.com/git/git-tutorial.html"&gt;Git&lt;/a&gt;和  &lt;a href="https://www.runoob.com/svn/svn-tutorial.html"&gt;SVN&lt;/a&gt;（曾经还有  &lt;a href="https://baike.baidu.com/item/CVS/405463"&gt;CVS&lt;/a&gt;，SVN成功取代了CVS）。两者区别？打开Git的链接阅读介绍。另外我想分享一个写得不错的  &lt;a href="https://www.atlassian.com/git/tutorials/what-is-version-control"&gt;英文的Git教程系列&lt;/a&gt;，它不仅讲命令，还总结命令间的区别，还稍微讲了一些git的实现原理，让你对git命令的执行过程有更深的认识。&lt;/p&gt;
 &lt;h3&gt;Linux命令行操作&lt;/h3&gt;
 &lt;p&gt;现在有三种主流的个人计算机操作系统，微软的Windows，苹果的macOS，和开源的（各种发行版的）Linux。服务器操作系统就很多了，五花八门的商业产品，比如Windows的  &lt;a href="https://www.microsoft.com/en-us/windows-server/"&gt;Windows Server&lt;/a&gt;，还有各种Linux。Linux系列操作系统因为其内存占用小、运行稳定、开源（即，免费，服务器版本的操作系统超贵）等特点，成为了主流的应用服务器的宿主操作系统。就是说，我们编写的应用在编译打包后，最终要部署到运行着Linux操作系统的服务器上。&lt;/p&gt;
 &lt;p&gt;如果你在Windows或MacOS操作系统上编程，一般你会使用Linux服务器做这两种工作：&lt;/p&gt;
 &lt;p&gt;部署（也称发版，发布新版本）程序。有时会有专门的运维职业的人员负责这方面工作，或者有自动化构建流水线，有时现实没那么美好，要程序员自己干。部署的操作流程视编程语言而定，在中途加入的项目组中，一般会有其他程序员写好的部署脚本（  &lt;a href="https://www.runoob.com/linux/linux-shell.html"&gt;Shell脚本&lt;/a&gt;）来简化部署操作。&lt;/p&gt;
 &lt;p&gt;查看日志文件。当程序打包部署后，我们就不能以debug的形式查看程序内部的运行情况了。因此我们会在程序中各处插入打印日志的语句，日志会被打印到一些文本文件中，我们通过查看日志文件来判断程序是否正常运行。看日志文件基本是程序员的职责，比如当自己负责的功能出问题时…就会有电话联系你“看一下某请求的情况”，你就要去对应的服务器上查看日志，来判断到底是你的功能出bug了，还是其他系统或功能形成的异常数据致使你的功能运行不正常（这种情况也要增加异常处理补丁去防止再次发生，编程原则：永远不要信任外部系统）。如果只是看日志文件的话，最低限度需学习的命令有  &lt;a href="https://www.man7.org/linux/man-pages/man1/cd.1p.html"&gt;切换目录的cd&lt;/a&gt;，  &lt;a href="https://www.man7.org/linux/man-pages/man1/ls.1p.html"&gt;查看目录内容的ls&lt;/a&gt;，  &lt;a href="https://www.man7.org/linux/man-pages/man1/grep.1p.html"&gt;使用正则过滤查看日志文件的grep&lt;/a&gt;。当然你还需要咨询同组其他人、或阅读程序中的日志输出配置文件来了解日志文件的位置。&lt;/p&gt;
 &lt;p&gt;如果你不熟悉命令行界面，看  &lt;a href="https://www.digitalocean.com/community/tutorials/an-introduction-to-the-linux-terminal"&gt;这篇英文介绍&lt;/a&gt;或者  &lt;a href="https://www.runoob.com/linux/linux-tutorial.html"&gt;菜鸟教程的中文Linux教程&lt;/a&gt;。另外，在命令行中输入“man”（代表manual）+其他命令，可以打开命令的说明书；或输入命令+  &lt;code&gt;-h&lt;/code&gt;或  &lt;code&gt;--help&lt;/code&gt;参数，打印命令的简单说明。你也可以在网上搜索在Linux中应该使用哪个命令做你想做的事。&lt;/p&gt;
 &lt;p&gt;特别的，从我们自己的电脑连接到服务器来使用服务器的命令行界面，需要使用  &lt;a href="http://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html"&gt;SSH&lt;/a&gt;这么一种命令行工具（同时它也是工具原理的同名协议）。一般不需要直接使用它，有多种图形界面软件供你选择，这里不再列举，请自行搜索。关于服务器的IP、端口、用户名与密码等连接所需的信息，你需要咨询同组的其他人。&lt;/p&gt;
 &lt;h3&gt;对各种外部系统的配置&lt;/h3&gt;
 &lt;p&gt;提问！上文中出现了几次“配置”？（提示：使用搜索快捷键）开个玩笑，其实我是想说，编程离不开对外部系统的配置。编程项目不是从零开始的，对吧，我们要使用许多已有的系统来省事，比如  &lt;a href="https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93/103728"&gt;数据库&lt;/a&gt;、  &lt;a href="https://baike.baidu.com/item/%E4%B8%AD%E9%97%B4%E4%BB%B6/452240"&gt;中间件&lt;/a&gt;、框架、日志系统（打日志是一件很复杂的事）…这些系统都需要我们通过配置来调整它们的行为，以适应我们项目的需要。&lt;/p&gt;
 &lt;p&gt;一般我们说配置一个系统，指的是编写配置文件，配置文件是一种遵守某种  &lt;a href="https://baike.baidu.com/item/%E5%8F%AF%E6%89%A9%E5%B1%95%E6%A0%87%E8%AE%B0%E8%AF%AD%E8%A8%80/2885849"&gt;标记语言&lt;/a&gt;（xml,json,toml,yaml…）的文本文件，它就是系统的设置项，和手机设置一样。系统一般在启动时读取配置文件，基于此调整行为。注意，系统一般只会在启动时读取配置文件，所以如果系统启动后你想修改配置文件并让它生效，往往需要重启系统。当然这有很多例外…比如你可以多开一个  &lt;a href="https://baike.baidu.com/item/%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/966835"&gt;守护进程&lt;/a&gt;专门监控配置文件的变化，若出现变化，就重启系统让系统读新的配置文件。另外在我看来，配置文件的内容是只能被该系统识别并解析的“语言”（  &lt;a href="https://www.infoworld.com/article/2077865/core-java-creating-dsls-in-java-part-1-what-is-a-domain-specific-language.html"&gt;领域特定语言&lt;/a&gt;）。&lt;/p&gt;
 &lt;p&gt;为了写出配置文件，要去读说明书。可以读官方的配置说明文档（往往详细又冗长），也可以省事读简化版的第三方文档，比如中文翻译、博客记录等。这个工作最麻烦的步骤就是要硬着头皮读大篇幅的文档。读哪个都行，能明白怎么写完成工作就好，我推荐配合网页内搜索读官方文档，毕竟最详细。注意配置与软件一样都是有版本的，要读自己项目所用的版本的对应说明书，或者有些文档只有一版，但会在每个配置项下标注从哪个版本开始生效。&lt;/p&gt;
 &lt;h2&gt;总结：太多了，先学哪个？&lt;/h2&gt;
 &lt;p&gt;不好意思，我没收住字数，写太多了。咚地一下全摆在面前是挺吓人的，而且说实话本文仍未涵盖我所认定的所有“必备技能”，如正则表达式等。&lt;/p&gt;
 &lt;p&gt;总之先学编程语言吧，然后是熟悉框架和几个常用的库的用法，此时就能完成工作了。然后如果你对这个职业仍有兴趣，可以学习单元测试、debug和编程思想（看书看博客），还有外围的技能，这些可以慢慢来。额外的知识会提高你的专业性，让你能够有底气地承认自己被付钱来编写程序，你对自己写出的程序有信心且对它负责。&lt;/p&gt;
 &lt;p&gt;如果你对这个职业不感兴趣，学到足够工作使用的知识就可以停止了，你也可能会发现其他兴趣和职业的知识会迁移性地对编程工作产生意想不到的帮助，很多抽象的原则和知识是共通的。&lt;/p&gt;
 &lt;h2&gt;附录&lt;/h2&gt;
 &lt;h3&gt;有用的网站&lt;/h3&gt;
 &lt;p&gt;下面是通用的几个网站：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://stackoverflow.com/"&gt;StackOverflow&lt;/a&gt; 它是    &lt;a href="https://stackexchange.com/"&gt;StackExchange&lt;/a&gt; 问答网站中分离出的编程问答板块。如果想解决程序报错、想知道某个API怎么用、不知道如何完成一个功能、想了解一种编程实践的理念…一切关于编程的东西，基本上你不是第一个碰到这个问题的人，stackoverflow上大概率会有对应的提问和专业亲切的回答。为了不限制搜索引擎的能力，不建议使用网站内部的搜索功能，而是直接在搜索引擎的搜索栏上开头写&amp;quot;stackoverflow&amp;quot; + 你的问题描述（用英文）。关于这个网站有不少meme赞美它的便利，比如：在没有stackoverflow之前程序员怎么可能顺利写完程序呢。PS: stackoverflow需要从    &lt;code&gt;ajax.googleapis.com&lt;/code&gt;域名加载Js脚本，因为GFW封禁了该域名，在国内网络打开该网站的速度会慢得不可忍受。如果你没有网络代理，你可以使用浏览器的广告屏蔽插件规则将该脚本的加载屏蔽，进而恢复正常的网页打开速度。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web"&gt;MDN(Mozilla Developer Network) Web docs&lt;/a&gt; 这是实质上的互联网唯一的Web前端开发参考文档，附带翔实有趣的教程（多语言翻译，有中文）。你可以不走出这个网站而学习完前端开发所需要的大部分基础知识。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://github.com/"&gt;Github&lt;/a&gt; 结合    &lt;a href="https://boholder.github.io/blogs/learn-from-source-code"&gt;这篇文章&lt;/a&gt;从Github里找点感兴趣的项目看一下，感兴趣优先，真的什么千奇百怪的项目都能搜出来。另外有一个小诀窍，如果你拿不准某个库的某个API怎么用，而注释、文档和stackoverflow又没能让你满意，你可以在github中搜索使用了该api的代码片段，学习使用方法。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://old.reddit.com/r/ExperiencedDevs/"&gt;Reddit-ExperiencedDevs&lt;/a&gt; 这版块只允许3年以上从业经验的开发者发帖，版块里聊的都是职业规划、公司中发生的事等等，很少涉及技术。看这里面的帖子就像一群大佬围着聊闲天时你凑进去听，只是听就能学到不少东西。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;下面是Java方面的网站：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://www.baeldung.com/"&gt;Baeldung&lt;/a&gt; 这网站上有内容充实的关于Java与Java生态（如Spring框架）的教程，质量比那种内容农场网站高出一截。不需要刻意在网站中搜索，当你搜索Java相关教程时，这个网站往往排在搜索结果前面…&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://blogs.oracle.com/javamagazine/"&gt;Oracle Java Manazine&lt;/a&gt; 因为Java不太受同行待见（这是真的吗？），所以不怎么能看到关于Java语言的公开讨论。虽然（仅在中国？）工作上用Java的程序员很多，但讨论Java语言俨然是个小圈子。这时我们要借助学术界中小圈子交流的方法：研讨会和期刊。研讨会…至少我不够格参与，因此我选择邮件订阅Oracle公司组织编撰的免费杂志，进而一窥Java行业的动向，学点新东西。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;之前没接触过IT行业的基础知识？&lt;/h3&gt;
 &lt;p&gt;基础知识体系无法突击补习，这是慢活，更别提还会遗忘。目前大学的软件工程|计算机科学专业会开设大约10门左右的IT专业课程，也就是说你和科班出身的IT专业本科毕业生的差距在10本左右的教科书，以及几个练手的小项目。&lt;/p&gt;
 &lt;p&gt;不补基础知识体系的前提下完成公司要求的编程工作是可以的，现在的编程语言和工具已经把底层实现都封装起来了。但它会在工作上限制你的思考：不知道原理，就没法想象你的程序如何运行，也就没法深入地思考一个解决方案的细节，心里很不舒服。或者…小心  &lt;a href="https://droidyue.com/blog/2019/03/24/cargo-cults-programming/"&gt;货物崇拜编程&lt;/a&gt;，它是指新手程序员常犯的那种“网上找解决方案复制粘贴下来”的不良编程实践。越不熟悉基础知识，越可能在工作中出现货物崇拜编程现象，这可能会在程序里埋下炸弹，最终伤害你或身边的程序员。&lt;/p&gt;
 &lt;p&gt;这里有一个好的起步课程：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://www.bilibili.com/video/BV1HJ411q7bm"&gt;【中英字幕|2019 CS50|哈佛大学编程入门】 CS50 Harvard University2019&lt;/a&gt;
用非术语讲解IT行业里的一些知识体系。真正的0基础开始学。可惜课不太全。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;a href="https://www.bilibili.com/video/BV1dx411i7dW"&gt;【哈佛大学-编程入门】 CS50 Harvard University（2016）11集（全）&lt;/a&gt;
课全了，可惜没中文字幕。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;我推荐的阅读材料&lt;/h3&gt;
 &lt;p&gt;API的使用方法可以通过关键字去搜索文档和问答，但职业经验就不方便搜索了，它们恰恰是重要的东西。IT行业很有意思，我没见过除此之外哪个行业在遵守工匠道路的同时，又不吝啬甚至乐于向同行分享自己的经验。有许多人会写经验类的博客，比热门博客更系统化也更著名的是出版成书的著作。下面是我读过的，能打包票的经验方面的阅读材料。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/3032825/"&gt;Clean Code 代码整洁之道&lt;/a&gt; 讲细小层面（语句级别）的编程技巧。当用某种编程语言写了一个练手项目或几个功能时就可以开始看了。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/6114900/"&gt;The Clean Coder 程序员职业素养&lt;/a&gt; 很薄的一本书，对的，和Clean Code是同一系列， 该系列还有两本：架构整洁之道，敏捷开发实践，等级比较高，我还没看¯\_(ツ)_/¯。不涉及技术细节，指导程序员在某件工作乃至职业规划上应该如何做。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/35006892/"&gt;The Pragmatic Programmer: From Journeyman to Master 程序员修炼之道（第2版）&lt;/a&gt; 分了许多小章节，每个章节用一个比喻做标题。两个作者想把他们的职业经验讲给你听，内容涉及方方面面。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/3324516/"&gt;实现模式&lt;/a&gt; 书不厚，类似Clean Code，讲细节的编程技巧，但是对读者的经验要求比Clean Code高一些。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://gist.github.com/zer4tul/44ac7d145a4342d876f3"&gt;如何成为黑客&lt;/a&gt; 不，hacker不是你想象的那个意思，你指的是cracker。这篇文章是一位温和派的开源运动领导者写的关于实践开源精神的一些建议，里面一些原则值得以程序员的角度去理解学习。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/35043034/"&gt;计算机简史（第三版）&lt;/a&gt; 学校里没有开设关于IT行业的历史课，我倒觉得该开一门。了解历史能多明白一些为什么现在的IT行业是这副样子。比如，开源运动如何让行业内出现了可自由使用的第三方库，让软件研发行业对人员专业素质的要求大幅降低，进而让提供了大量岗位。随着科技的发展和商业需求、社会环境的演变，曾经的一些黄金实践原则已经过时或不合时宜，也出现了适应时代的新的原则。举个例子，你不会在50年前要求程序员编写单元测试，因为他们连编译个程序都要等好几个小时，而现在的算力如此廉价，在一台个人笔记本上运行整个项目的整套单元测试只需要几分钟，这也让   &lt;a href="https://www.ruanyifeng.com/blog/2015/09/continuous-integration.html"&gt;持续集成&lt;/a&gt;成为可能。&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://book.douban.com/subject/25956450/"&gt;人件&lt;/a&gt;、   &lt;a href="https://book.douban.com/subject/1102259/"&gt;人月神话&lt;/a&gt;和   &lt;a href="https://book.douban.com/subject/25881855/"&gt;大教堂与集市&lt;/a&gt; 这三本是软件开发组织架构方面的理论书，结合对应的年代和历史事件，它们会让你了解软件开发行业是怎样从瀑布式开发转向敏捷开发的，开发团队的组织规模为何从大部队分散成灵活的小组。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>programming programming blog</category>
      <guid isPermaLink="true">https://itindex.net/detail/61709-%E7%BC%96%E7%A8%8B-%E9%9C%80%E8%A6%81-%E4%B8%9C%E8%A5%BF</guid>
      <pubDate>Mon, 16 Aug 2021 08:00:00 CST</pubDate>
    </item>
    <item>
      <title>学生党最应该知道的资源---技能/软件/编程/英语/二外/计算机二级/其他学习/论文下载/电子书/PPT模板_哔哩哔哩_bilibili</title>
      <link>https://itindex.net/detail/61643-%E5%AD%A6%E7%94%9F-%E7%9F%A5%E9%81%93-%E8%B5%84%E6%BA%90</link>
      <description>1、技能学习平台：哔哩哔哩、中国大学慕课、coursera、edX &lt;br /&gt;2、软件操作：up主：oeasy、doyoudo、星月兮、Genji是真想教会你、旁门左道PPT、Excel自学成才、我是于干，+实战演练 &lt;br /&gt;3、编程：基础：菜鸟教程、进阶：CSDN、Github、stackoverflow、leetcode &lt;br /&gt;4、英语：四六级尽早考+买真题，进阶：扇贝、中国日报网英语点津、Italki、Audible、NPR.org、考满分 &lt;br /&gt;5、二外：最好找老师，入门：多邻国 &lt;br /&gt;6、计算机二级：学校基础课、买题库，up主：小黑课堂计算机二级 &lt;br /&gt;7、其他：学吧导航 &lt;br /&gt;8、论文下载：学校图书馆或省市图书馆，知网、Web of Science、图书馆官网直接检索、SCI-HUB、Library Genesis、Z-Library &lt;br /&gt;9、电子书：某宝、读秀、超星、书格、七彩英语、古腾堡计划、manybooks、鸠摩搜索、Library Genesis &lt;br /&gt;10、PPT模板：OfficePLUS、PPT超级市场、51PPT模板、PPT汇、优品PPT、HiPPTer&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61643-%E5%AD%A6%E7%94%9F-%E7%9F%A5%E9%81%93-%E8%B5%84%E6%BA%90</guid>
      <pubDate>Sun, 25 Jul 2021 23:13:24 CST</pubDate>
    </item>
    <item>
      <title>和AI结对编程！OpenAI与GitHub联手推出AI代码生成工具，比GPT-3更强大</title>
      <link>https://itindex.net/detail/61579-ai-%E7%BB%93%E5%AF%B9%E7%BC%96%E7%A8%8B-openai</link>
      <description>&lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc246e7e2c8.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;作者 | 琰琰、青暮&lt;/p&gt; &lt;p&gt;比GPT-3更强大！&lt;/p&gt; &lt;p&gt;昨日，微软与OpenAI共同推出了一款AI编程工具GitHub Copilot，这款工具基于GitHub及其他网站的源代码，可根据上文提示为程序员自动编写下文代码！&lt;/p&gt; &lt;p&gt;GitHub 的首席执行官 Nat Friedman 介绍说，GitHub Copilot是结对编程的虚拟版本。结对编程是一种常见的敏捷软件开发技术——即两个开发人员在同一个项目上并肩协作，轮流编写代码并检查合作伙伴的输出。&lt;/p&gt; &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc246f1ee65.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;“它可以在编辑器中直接获提供多行或整个函数的代码建议。另外，随着时间的推移和使用量的增加，模型会变得越来越复杂，代码质量也会越来高。”&lt;/p&gt; &lt;p&gt;对于程序员来讲，一款编程工具最重要的当然是支持哪些类型语言。GitHub在预告中说，Copilot 可以支持十几种语言，与 Python、JavaScript、TypeScript、Ruby 和 Go 等主流语言配合效果更佳。&lt;/p&gt; &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc246fdfca5.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;目前GitHub官网已经晒出了 Copilot 的功能介绍，我们来了解一下。&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;strong&gt;1&lt;/strong&gt; &lt;br /&gt; &lt;p&gt;  &lt;strong&gt;给力的结对编程伙伴&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;GitHub表示，相比大多数代码助手，GitHub Copilot可以理解更大范围的上下文。因此，无论是文档字符串、注释、函数名称还是代码本身，GitHub Copilot 都可以基于提供的上下文（已输入的代码）合成新代码以进行匹配。&lt;/p&gt; &lt;p&gt;如果真有如此可靠，那么正如GitHub所说，这项功能可以让程序员丢掉很多的文档阅读工作，从而专注于编辑器。&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc247065703.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;GitHub Copilot还可以将注释转换为代码。只需要写一条评论，描述想要的逻辑，GitHub Copilot 就能自动“理解”并写出相应功能的代码。  &lt;br /&gt;&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc247131b9d.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;GitHub Copilot 也能自动填充重复代码。GitHub表示，GitHub Copilot 非常适合快速生成样板和重复代码模式。对于需要编写大量相似但难以转换为循环语句代码的程序员而言，这项功能非常“香”。  &lt;br /&gt;&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc2471edd71.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;包办测试也是GitHub Copilot值得注意的亮点，GitHub表示，测试是任何强大的软件工程项目的支柱，“导入单元测试包，就可以让 GitHub Copilot 自动编写与你的实现代码匹配的测试。”  &lt;br /&gt;&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc2472af6fd.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;对于相同的上下文，GitHub Copilot可以给出多种解决方案，供程序员自主选择。  &lt;br /&gt;&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc247379732.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;看到这里，小伙伴们是不是跃跃欲试了呢？可惜，GitHub Copilot不是完全开放的，和GPT-3一样，只提供有限用户的试用体验。总之，先排队吧。  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc2473ef559.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;项目地址：https://copilot.github.com/&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;strong&gt;2&lt;/strong&gt; &lt;br /&gt; &lt;p&gt;  &lt;strong&gt;Copilot，专为编程而生&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;Brockman介绍说，GitHub Copilot的核心模型Codex借助了GPT-3强大的生成能力。&lt;/p&gt; &lt;p&gt;我们知道，GPT-3是OpenAI推出的超大规模自然语言生成模型，早期训练GPT-3时，OpenAI无意教它如何帮助编写代码，它更像是一种通用语言模型，主要用于文本生成、语法修改等任务。&lt;/p&gt; &lt;p&gt;后来OpenAI向公众开放API，这个基于文本训练的模型，被网友们玩出了50多种花样，其中包括自动生成代码。OpenAI意识到了GPT-3在编写代码方面的潜力，并在其网站上提供了在线“代码生成”服务。&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc2474c53be.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;几十年来，微软的研究人员一直在尝试如何教计算机编写代码，但未取得显著成果。此次与OpenAI合作，微软将提供了强大 Azure 云计算能力和GitHub平台（2018年被微软收购）的大量语言编程代码。&lt;/p&gt; &lt;p&gt;关于这款产品的代码生成质量，Friedman说，GitHub员工已经做了大量尝试，能够确保GitHub Copilot生成安全、高质量的代码。他说：“我们已经在系统中建立了一些安全机制，这些机制在减少各个领域能够显著减少出错几率。”也就是说，GitHub Copilot并不能保证完全不出错，它仍然只是一个辅助生成代码工具。&lt;/p&gt; &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc2475a2c09.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt; &lt;p&gt;官网报道说，Copilot的底层技术属于微软和OpenAI共有，今年夏天OpenAI将发布Codex模型，供第三方开发者使用。微软也表示会发布该产品的一个版本，让企业通过培训了解自己的编程风格。但目前，微软只提供公共存储库中存储的代码的服务。&lt;/p&gt; &lt;p&gt;这不是微软第一次依靠OpenAI来提供智能软件。上个月，微软展示了powerapps Studio应用程序的更新服务，非技术人员可以使用它来编写应用程序——用户输入他们想要添加的元素单词，GPT-3可提供一些必要代码的选项。&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;strong&gt;3&lt;/strong&gt; &lt;br /&gt; &lt;p&gt;  &lt;strong&gt;网友讨论&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;GitHub Copilot的发布在Reddit上引发了热烈的讨论，不少网友看好编码自动化的产业趋势，网友@markbowick说道：&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;毫无疑问，这将极大地提高大多数程序员的日常编程生产力，并且（我认为）这将是推动全球软件影响呈指数增长的最重要步骤之一。&lt;/p&gt;  &lt;p&gt;值得注意的是，GPT-J（GPT-3 较小模型之一的开源实现）在大量 GitHub 和 StackExchange 查询存储库上进行了训练，并且在特定编程相关任务上的表现明显优于 OpenAI 的其它模型。在接下来的几个月里，我预期会看到性能更好的类似的（更大）模型。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc24766837b.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;也有人提出了质疑，网友@laprika0表示，GitHub Copilot的测试方法令人担忧，“让 GitHub Copilot 建议与你的实现代码匹配的测试，但人们并不应该以这种方式写测试。”不少网友表示同意，并补充道，“如果实现代码有错误，那么GitHub Copilot将把错误引入测试中，从而影响测试效果。”&lt;/p&gt; &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc2476cf032.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;还有人则注意到关于代码所有权的问题，OpenAI发表了一项声明，表示用户使用GitHub Copilot写代码时，代码片段和建议结果将与 GitHub 和 OpenAI 共享，并用于诊断目的和改进建议。同时，为保护隐私，GitHub Copilot 不会使用用户的私有代码来为 GitHub Copilot 的其他用户推荐代码。&lt;/p&gt; &lt;p&gt;网友@touristtam对此声明感到困惑，“他们是否拥有在用户启用GitHub Copilot时编写的代码？”&lt;/p&gt; &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc24775e34c.png?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;参考链接：&lt;/p&gt;https://www.cnbc.com/2021/06/29/microsoft-github-copilot-ai-offers-coding-suggestions.htmlhttps://venturebeat.com/2021/06/29/github-launches-copilot-to-power-pair-programming-with-ai/https://www.reddit.com/r/MachineLearning/comments/oaambv/n_github_and_openai_release_copilot_an_ai_pair/ &lt;p&gt;  &lt;img src="https://static.leiphone.com/uploads/new/sns/blogSpe/article/202106/60dc24781b24d.jpg?imageMogr2/format/jpg/quality/90"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;雷锋网雷锋网雷锋网&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>人工智能学术</category>
      <guid isPermaLink="true">https://itindex.net/detail/61579-ai-%E7%BB%93%E5%AF%B9%E7%BC%96%E7%A8%8B-openai</guid>
      <pubDate>Wed, 30 Jun 2021 16:02:00 CST</pubDate>
    </item>
    <item>
      <title>视力下降问题获重大突破，哈佛团队采用重编程神经元法，恢复小鼠一半视力</title>
      <link>https://itindex.net/detail/61082-%E8%A7%86%E5%8A%9B-%E9%97%AE%E9%A2%98-%E7%AA%81%E7%A0%B4</link>
      <description>&lt;p&gt;衰老是一种机体对环境的生理和心理适应能力进行性降低、逐渐趋向死亡的现象。随着年龄的增长，形态与主要感觉器官功能等都会发生衰退，视觉更是首当其冲。但是，近日有两大海外科研团队相继宣布，其针对衰老和青光眼造成的视力下降问题的研究，均取得了重大突破，或可有效逆转衰老的时钟。&lt;/p&gt; &lt;p&gt;其中，来自哈佛医学院的科学家戴维・辛克莱尔及其同事在国际顶刊 Nature 上在线发表的研究 Reprogramming to recover youthful epigenetic information and restore vision，采用生物学办法来试图纠正眼睛和大脑之间信息传输中的问题。&lt;/p&gt; &lt;p&gt;研究结果发现，可通过把小鼠眼睛的神经元重编程到一个更年轻的状态，让其视力获得再生和恢复。而来自欧洲的科学家则选择将犹他电极阵列植入相应神经元，通过控制注入大脑的电流量形成眼内闪光，让小鼠更快完成识别，从而完全绕开了对眼睛的需求。&lt;/p&gt; &lt;p&gt;众所周知，我们的视觉系统很复杂，不但有接收入射光的感光器，在这些感光器和大脑之间还存在着至少三种类型的神经元。这些视觉输入一旦进入大脑，数个专属区域会将把小块儿的形状与动作构建成场景，从而完成阐释。而这一处理结果可能还会进入处理阅读或面部识别等其他大脑区域，从而进一步解释。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#35270;&amp;#21147;&amp;#19979;&amp;#38477;&amp;#38382;&amp;#39064;&amp;#33719;&amp;#31361;&amp;#30772;&amp;#65292;&amp;#21704;&amp;#20315;&amp;#37319;&amp;#29992;&amp;#37325;&amp;#32534;&amp;#31243;&amp;#31070;&amp;#32463;&amp;#20803;&amp;#27861;&amp;#65292;&amp;#24674;&amp;#22797;&amp;#23567;&amp;#40736;&amp;#19968;&amp;#21322;&amp;#35270;&amp;#21147;" src="http://document.mittrchina.com/img/2020/12/202012171523406862164.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;图 | 放射状切片扫描状态下的眼睛及视神经&lt;/p&gt; &lt;h1&gt;重编程神经元法&lt;/h1&gt; &lt;p&gt;在组织修复中，哈佛团队认识到，仅通过激活四个特定基因就可以将许多细胞类型转化为干细胞，这一进展让他们颇为兴奋。但不幸的是，在广泛激活这些基因的同时会造成小鼠死亡，因为这些基因会促进正常细胞丧失同一性并发生分裂失控。&lt;/p&gt; &lt;p&gt;哈佛大学医学院科学家们认为这些问题大多是 MYC 基因引起的，因此，转而专注于处理其余三个基因。第一组实验结果佐证了这一猜想：激活老年鼠细胞中的其他三个基因可以使细胞恢复年轻特性的同时，不会丧失正常的细胞功能。&lt;/p&gt; &lt;p&gt;视网膜神经节细胞是眼内的一种神经元，它们能将伸长的部分（称为轴突）从眼睛连接到大脑。这些轴突如果在发育早期受损，是可以得到再生的，但到了成年以后，这种能力就会很快消失。&lt;/p&gt; &lt;p&gt;因此，研究人员破坏了视神经，然后激活了视网膜神经节细胞中的三个干细胞基因。这样有了活跃的基因，即使是成年和患青光眼的小鼠，也能很快恢复大脑和眼睛之间的联系。最终的视力测试表明，这种基因治疗方案可以恢复近一半的视力。将三个月大的小鼠与一岁左右的小鼠进行比较发现，基本可以确认，这一办法对衰老带来的视力减退情况十分适用。&lt;/p&gt; &lt;p&gt;并且这一研究结果也支持了之前的观点：衰老的背后是表观遗传变异的累积，要逆转某种复杂组织年龄、并恢复其生物学功能是可能的。&lt;/p&gt; &lt;p&gt;值得一提的是，上述所有基因治疗过程均未发生新细胞增长。相反，几乎是依靠现有细胞来修复或替换视神经的受损部分，也就是轴突。而这种修复效果主要取决于甲基化 DNA 化学修饰的变化，这种变化可以改变许多基因的活性。&lt;/p&gt; &lt;h1&gt;“绕过眼睛” 的硬件植入法&lt;/h1&gt; &lt;p&gt;第二项研究由四名欧洲研究人员完成，主要着眼于眼球下游的部分。&lt;/p&gt; &lt;p&gt;当光信号进入大脑，与视网膜一对一物理映射的区域将首先进行解码。换句话说，大脑中这一率先接收视网膜信号的神经元的几何形状直接反映了视网膜本身的布局。正是利用这种对应关系，研究人员可以通过一些电子设备来尝试激活视觉系统，并做到完全不涉及眼睛。&lt;/p&gt; &lt;p&gt;具体做法是将犹他电极阵列连接到该区域中的神经元。由于此次的实验对象是动物而不是人类，所以研究人员只植入了一组犹他阵列，电极数量也就不多，但这对实验场景来说，也完全满足需求了。&lt;/p&gt; &lt;p&gt;通过这些植入物，研究人员不仅可以连接到视觉信号首先到达并被解释的大脑区域，还可以连接到进行下一步解码处理的大脑区域。这样以来，研究人员就可较为精准地控制注入大脑的电流量，做到只刺激小鼠的小范围视野，而不会使其因刺激过于强烈而不知所措。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#35270;&amp;#21147;&amp;#19979;&amp;#38477;&amp;#38382;&amp;#39064;&amp;#33719;&amp;#31361;&amp;#30772;&amp;#65292;&amp;#21704;&amp;#20315;&amp;#37319;&amp;#29992;&amp;#37325;&amp;#32534;&amp;#31243;&amp;#31070;&amp;#32463;&amp;#20803;&amp;#27861;&amp;#65292;&amp;#24674;&amp;#22797;&amp;#23567;&amp;#40736;&amp;#19968;&amp;#21322;&amp;#35270;&amp;#21147;" src="http://document.mittrchina.com/img/2020/12/202012171523412701891.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;这些电流会让小鼠产生所谓的 “光幻视”，就是通常所说的小闪光。而且，根据大脑这一区域的几何形状，研究人员还可以控制闪光灯在视野中出现的位置。&lt;/p&gt; &lt;p&gt;事实上，这一方法真的有效。我们知道，通常灵长类动物的眼睛会不自觉地转向所感知到的闪光源，尽管那个地方实际上并没有在眼睛中显示出实质性内容。有研究人员曾训练猴子，让其识别两个点是垂直排列还是水平排列，研究结果发现，当两个点是由电极产生的光幻视时，猴子们就能分辨出来。尽管这种方法并没有直观的物理展示效果好，但肯定会比随机选择的效果要好得多。&lt;/p&gt; &lt;p&gt;研究人员又试着训练猴子来识别字母，结果发现，当用注入电流来呈现字母时，猴子可以完成识别。换句话说，猴子们可以识别眼内闪光呈现出的字母，同样这种方式并没有展示真实字母的效果好，但是远高于其他随机呈现的方式。&lt;/p&gt; &lt;h1&gt;离实际应用还有多远？&lt;/h1&gt; &lt;p&gt;美国加利福尼亚州斯坦福大学医学院的 Andrew Huberman 探讨了研究结果是否能推及到人类的问题。他指出，虽然文中描述的转录因子的作用，仍需在人类中进一步验证。但研究结果提示，它们或能重编程不同物种的大脑神经元。&lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#35270;&amp;#21147;&amp;#19979;&amp;#38477;&amp;#38382;&amp;#39064;&amp;#33719;&amp;#31361;&amp;#30772;&amp;#65292;&amp;#21704;&amp;#20315;&amp;#37319;&amp;#29992;&amp;#37325;&amp;#32534;&amp;#31243;&amp;#31070;&amp;#32463;&amp;#20803;&amp;#27861;&amp;#65292;&amp;#24674;&amp;#22797;&amp;#23567;&amp;#40736;&amp;#19968;&amp;#21322;&amp;#35270;&amp;#21147;" src="http://document.mittrchina.com/img/2020/12/202012171523427667655.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;文中提及的两大科学发现虽让人振奋不已，但我们需要明确的一点是，目前为止，这两大实验都还处在早期的动物实验阶段，离人类的治疗还有很远的距离。因为在实验过程中，研究人员无法直接询问那些动物看到了什么，而是只能依靠对它们视觉能力的间接测试。&lt;/p&gt; &lt;p&gt;因此，也就很难解释这两种技术给视觉带来的了多大的变化。并且这类实验还存在很多潜在的安全问题，特别是涉及到改变人类基因的部分。&lt;/p&gt; &lt;p&gt;但这至少意味着，在 “逆生长” 这条道路上，我们再次向前迈出了关键性的 “两步”—— 首先是可以肯定，当对大脑右侧区域的电极进行刺激时会产生人为视觉效果，从而能够实现绕过眼睛恢复视力的目的；&lt;/p&gt; &lt;p&gt;其次是针对衰老或创伤造成的神经功能丧失问题，只需要通过较小的基因干预就能得到恢复。可以想象，这项研究如果得以继续推进，未来必将为除视力问题之外更广阔的层面带来助益。&lt;/p&gt; &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61082-%E8%A7%86%E5%8A%9B-%E9%97%AE%E9%A2%98-%E7%AA%81%E7%A0%B4</guid>
      <pubDate>Thu, 17 Dec 2020 23:13:28 CST</pubDate>
    </item>
    <item>
      <title>Flink SQL 编程实践</title>
      <link>https://itindex.net/detail/59971-flink-sql-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;blockquote&gt;  &lt;p&gt;注： 本教程实践基于 Ververica 开源的    &lt;a href="https://github.com/ververica/sql-training" rel="noopener" target="_blank"&gt;sql-training&lt;/a&gt; 项目。基于 Flink 1.7.2 。&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#36890;&amp;#36807;&amp;#26412;&amp;#35838;&amp;#20320;&amp;#33021;&amp;#23398;&amp;#21040;&amp;#20160;&amp;#20040;&amp;#65311;" title="&amp;#36890;&amp;#36807;&amp;#26412;&amp;#35838;&amp;#20320;&amp;#33021;&amp;#23398;&amp;#21040;&amp;#20160;&amp;#20040;&amp;#65311;"&gt;&lt;/a&gt;通过本课你能学到什么？&lt;/h2&gt; &lt;p&gt;本文将通过五个实例来贯穿 Flink SQL 的编程实践，主要会涵盖以下几个方面的内容。&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;如何使用 SQL CLI 客户端&lt;/li&gt;  &lt;li&gt;如何在流上运行 SQL 查询&lt;/li&gt;  &lt;li&gt;运行 window aggregate 与 non-window aggregate，理解其区别&lt;/li&gt;  &lt;li&gt;如何用 SQL 消费 Kafka 数据&lt;/li&gt;  &lt;li&gt;如何用 SQL 将结果写入 Kafka 和 ElasticSearch&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;本文假定您已具备基础的 SQL 知识。&lt;/p&gt; &lt;a&gt;&lt;/a&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#29615;&amp;#22659;&amp;#20934;&amp;#22791;" title="&amp;#29615;&amp;#22659;&amp;#20934;&amp;#22791;"&gt;&lt;/a&gt;环境准备&lt;/h2&gt; &lt;p&gt;本文教程是基于 Docker 进行的，因此你只需要安装了   &lt;a href="https://www.docker.com/" rel="noopener" target="_blank"&gt;Docker&lt;/a&gt; 即可。不需要依赖 Java、Scala 环境、或是IDE。&lt;/p&gt; &lt;p&gt;注意：Docker 默认配置的资源可能不太够，会导致运行 Flink Job 时卡死。因此推荐配置 Docker 资源到 3-4 GB，3-4 CPUs。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1giprai_1gK0jSZFqXXcpaXXa-1224-1146.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;本次教程的环境使用 Docker Compose 来安装，包含了所需的各种服务的容器，包括：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;Flink SQL Client：用来提交query，以及可视化结果&lt;/li&gt;  &lt;li&gt;Flink JobManager 和 TaskManager：用来运行 Flink SQL 任务。&lt;/li&gt;  &lt;li&gt;Apache Kafka：用来生成输入流和写入结果流。&lt;/li&gt;  &lt;li&gt;Apache Zookeeper：Kafka 的依赖项&lt;/li&gt;  &lt;li&gt;ElasticSearch：用来写入结果&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;我们已经提供好了Docker Compose 配置文件，可以直接下载   &lt;a href="https://raw.githubusercontent.com/ververica/sql-training/master/docker-compose.yml" rel="noopener" target="_blank"&gt;docker-compose.yml&lt;/a&gt; 文件。&lt;/p&gt; &lt;p&gt;然后打开命令行窗口，进入存放   &lt;code&gt;docker-compose.yml&lt;/code&gt; 文件的目录，然后运行以下命令：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;Linux &amp;amp; MacOS&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;docker-compose up -d     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;Windows&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;set COMPOSE_CONVERT_WINDOWS_PATHS=1     &lt;br /&gt;docker-compose up -d     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;  &lt;code&gt;docker-compose&lt;/code&gt; 命令会启动所有所需的容器。第一次运行的时候，Docker 会自动地从 Docker Hub 下载镜像，这可能会需要一段时间（将近 2.3GB）。之后运行的话，几秒钟就能启动起来了。运行成功的话，会在命令行中看到以下输出，并且也可以在   &lt;a href="http://localhost:8081" rel="noopener" target="_blank"&gt;http://localhost:8081&lt;/a&gt; 访问到 Flink Web UI。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB19WduabH1gK0jSZFwXXc7aXXa-1034-292.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#36816;&amp;#34892;-Flink-SQL-CLI-&amp;#23458;&amp;#25143;&amp;#31471;" title="&amp;#36816;&amp;#34892; Flink SQL CLI &amp;#23458;&amp;#25143;&amp;#31471;"&gt;&lt;/a&gt;运行 Flink SQL CLI 客户端&lt;/h2&gt; &lt;p&gt;运行下面命令进入 Flink SQL CLI 。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;docker-compose exec sql-client ./sql-client.sh     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;该命令会在容器中启动 Flink SQL CLI 客户端。然后你会看到如下的欢迎界面。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1.f0wahn1gK0jSZKPXXXvUXXa-3104-1978.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#25968;&amp;#25454;&amp;#20171;&amp;#32461;" title="&amp;#25968;&amp;#25454;&amp;#20171;&amp;#32461;"&gt;&lt;/a&gt;数据介绍&lt;/h2&gt; &lt;p&gt;Docker Compose 中已经预先注册了一些表和数据，可以运行   &lt;code&gt;SHOW TABLES;&lt;/code&gt; 来查看。本文会用到的数据是   &lt;code&gt;Rides&lt;/code&gt; 表，这是一张出租车的行车记录数据流，包含了时间和位置信息，运行   &lt;code&gt;DESCRIBE Rides;&lt;/code&gt; 可以查看表结构。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;Flink SQL&amp;gt; DESCRIBE Rides;     &lt;br /&gt;root     &lt;br /&gt; |-- rideId: Long           // 行为ID (包含两条记录，一条入一条出）     &lt;br /&gt; |-- taxiId: Long           // 出租车ID      &lt;br /&gt; |-- isStart: Boolean       // 开始 or 结束     &lt;br /&gt; |-- lon: Float             // 经度     &lt;br /&gt; |-- lat: Float             // 纬度     &lt;br /&gt; |-- rideTime: TimeIndicatorTypeInfo(rowtime)     // 时间     &lt;br /&gt; |-- psgCnt: Integer        // 乘客数     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;Rides 表的详细定义见   &lt;a href="https://github.com/ververica/sql-training/blob/master/build-image/training-config.yaml" rel="noopener" target="_blank"&gt;training-config.yaml&lt;/a&gt;。&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#23454;&amp;#20363;1&amp;#65306;&amp;#36807;&amp;#28388;" title="&amp;#23454;&amp;#20363;1&amp;#65306;&amp;#36807;&amp;#28388;"&gt;&lt;/a&gt;实例1：过滤&lt;/h2&gt; &lt;p&gt;例如我们现在只想查看  &lt;strong&gt;发生在纽约的行车记录&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;注：Docker 环境中已经预定义了一些内置函数，如   &lt;code&gt;isInNYC(lon, lat)&lt;/code&gt; 可以确定一个经纬度是否在纽约，  &lt;code&gt;toAreaId(lon, lat)&lt;/code&gt; 可以将经纬度转换成区块。&lt;/p&gt; &lt;p&gt;因此，此处我们可以使用   &lt;code&gt;isInNYC&lt;/code&gt; 来快速过滤出纽约的行车记录。在 SQL CLI 中运行如下 Query：&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;SELECT * FROM Rides WHERE isInNYC(lon, lat);     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;SQL CLI 便会提交一个 SQL 任务到 Docker 集群中，从数据源（Rides 流存储在Kafka中）不断拉取数据，并通过   &lt;code&gt;isInNYC&lt;/code&gt; 过滤出所需的数据。SQL CLI 也会进入可视化模式，并不断刷新展示过滤后的结果：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1pSBsaXY7gK0jSZKzXXaikpXa-3104-1978.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;也可以到   &lt;a href="http://localhost:8081" rel="noopener" target="_blank"&gt;http://localhost:8081&lt;/a&gt; 查看 Flink 作业的运行情况。&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#23454;&amp;#20363;2&amp;#65306;Group-Aggregate" title="&amp;#23454;&amp;#20363;2&amp;#65306;Group Aggregate"&gt;&lt;/a&gt;实例2：Group Aggregate&lt;/h2&gt; &lt;p&gt;我们的另一个需求是计算  &lt;strong&gt;搭载每种乘客数量的行车事件数&lt;/strong&gt;。也就是搭载1个乘客的行车数、搭载2个乘客的行车… 当然，我们仍然只关心纽约的行车事件。&lt;/p&gt; &lt;p&gt;因此，我们可以按照乘客数  &lt;code&gt;psgCnt&lt;/code&gt;做分组，使用   &lt;code&gt;COUNT(*)&lt;/code&gt; 计算出每个分组的事件数，注意在分组前需要先过滤出  &lt;code&gt;isInNYC&lt;/code&gt;的数据。在 SQL CLI 中运行如下 Query：&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;SELECT psgCnt, COUNT(*) AS cnt      &lt;br /&gt;FROM Rides      &lt;br /&gt;WHERE isInNYC(lon, lat)     &lt;br /&gt;GROUP BY psgCnt;     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;SQL CLI 的可视化结果如下所示，结果每秒都在发生变化。不过最大的乘客数不会超过 6 人。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1aKxAabr1gK0jSZR0XXbP8XXa-3104-1250.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#23454;&amp;#20363;3&amp;#65306;Window-Aggregate" title="&amp;#23454;&amp;#20363;3&amp;#65306;Window Aggregate"&gt;&lt;/a&gt;实例3：Window Aggregate&lt;/h2&gt; &lt;p&gt;为了持续地监测纽约的交通流量，需要计算出  &lt;strong&gt;每个区块每5分钟的进入的车辆数&lt;/strong&gt;。我们只关心至少有5辆车子进入的区块。&lt;/p&gt; &lt;p&gt;此处需要涉及到窗口计算（每5分钟），所以需要用到 Tumbling Window 的语法。“每个区块” 所以还要按照   &lt;code&gt;toAreaId&lt;/code&gt; 进行分组计算。“进入的车辆数” 所以在分组前需要根据   &lt;code&gt;isStart&lt;/code&gt; 字段过滤出进入的行车记录，并使用   &lt;code&gt;COUNT(*)&lt;/code&gt; 统计车辆数。最后还有一个 “至少有5辆车子的区块” 的条件，这是一个基于统计值的过滤条件，所以可以用 SQL HAVING 子句来完成。&lt;/p&gt; &lt;p&gt;最后的 Query 如下所示：&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;SELECT      &lt;br /&gt;  toAreaId(lon, lat) AS area,      &lt;br /&gt;  TUMBLE_END(rideTime, INTERVAL &amp;apos;5&amp;apos; MINUTE) AS window_end,      &lt;br /&gt;  COUNT(*) AS cnt      &lt;br /&gt;FROM Rides      &lt;br /&gt;WHERE isInNYC(lon, lat) and isStart     &lt;br /&gt;GROUP BY      &lt;br /&gt;  toAreaId(lon, lat),      &lt;br /&gt;  TUMBLE(rideTime, INTERVAL &amp;apos;5&amp;apos; MINUTE)      &lt;br /&gt;HAVING COUNT(*) &amp;gt;= 5;     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;在 SQL CLI 中运行后，其可视化结果如下所示，每个 area + window_end 的结果输出后就不会再发生变化，但是会每隔 5 分钟会输出一批新窗口的结果。因为 Docker 环境中的source我们做了10倍的加速读取（相对于原始速度），所以演示的时候，大概每隔30秒就会输出一批新窗口。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1F0XCaoD1gK0jSZFGXXbd3FXa-3104-1214.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#Window-Aggregate-&amp;#19982;-Group-Aggregate-&amp;#30340;&amp;#21306;&amp;#21035;" title="Window Aggregate &amp;#19982; Group Aggregate &amp;#30340;&amp;#21306;&amp;#21035;"&gt;&lt;/a&gt;Window Aggregate 与 Group Aggregate 的区别&lt;/h2&gt; &lt;p&gt;从实例2和实例3的结果显示上，可以体验出来 Window Aggregate 与 Group Aggregate 是有一些明显的区别的。其主要的区别是，Window Aggregate 是当window结束时才输出，其输出的结果是最终值，不会再进行修改，其输出流是一个   &lt;strong&gt;Append 流&lt;/strong&gt;。而 Group Aggregate 是每处理一条数据，就输出最新的结果，其结果是在不断更新的，就好像数据库中的数据一样，其输出流是一个   &lt;strong&gt;Update 流&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;另外一个区别是，window 由于有 watermark ，可以精确知道哪些窗口已经过期了，所以可以及时清理过期状态，保证状态维持在稳定的大小。而 Group Aggregate 因为不知道哪些数据是过期的，所以状态会无限增长，这对于生产作业来说不是很稳定，所以建议对 Group Aggregate 的作业配上 State TTL 的配置。&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1Ku0zahD1gK0jSZFsXXbldVXa-2566-1266.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;例如统计每个店铺每天的实时PV，那么就可以将 TTL 配置成 24+ 小时，因为一天前的状态一般来说就用不到了。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;SELECT  DATE_FORMAT(ts, &amp;apos;yyyy-MM-dd&amp;apos;), shop_id, COUNT(*) as pv     &lt;br /&gt;FROM T     &lt;br /&gt;GROUP BY DATE_FORMAT(ts, &amp;apos;yyyy-MM-dd&amp;apos;), shop_id     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;当然，如果 TTL 配置地太小，可能会清除掉一些有用的状态和数据，从而导致数据精确性地问题。这也是用户需要权衡地一个参数。&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#23454;&amp;#20363;4&amp;#65306;&amp;#23558;-Append-&amp;#27969;&amp;#20889;&amp;#20837;-Kafka" title="&amp;#23454;&amp;#20363;4&amp;#65306;&amp;#23558; Append &amp;#27969;&amp;#20889;&amp;#20837; Kafka"&gt;&lt;/a&gt;实例4：将 Append 流写入 Kafka&lt;/h2&gt; &lt;p&gt;上一小节介绍了 Window Aggregate 和 Group Aggregate 的区别，以及 Append 流和 Update 流的区别。在 Flink 中，目前 Update 流只能写入支持更新的外部存储，如 MySQL, HBase, ElasticSearch。Append 流可以写入任意地存储，不过一般写入日志类型的系统，如 Kafka。&lt;/p&gt; &lt;p&gt;这里我们希望将  &lt;strong&gt;“每10分钟的搭乘的乘客数”&lt;/strong&gt;写入Kafka。&lt;/p&gt; &lt;p&gt;我们已经预定义了一张 Kafka 的结果表   &lt;code&gt;Sink_TenMinPsgCnts&lt;/code&gt;（  &lt;a href="https://github.com/ververica/sql-training/blob/master/build-image/training-config.yaml" rel="noopener" target="_blank"&gt;training-config.yaml&lt;/a&gt; 中有完整的表定义）。&lt;/p&gt; &lt;p&gt;在执行 Query 前，我们先运行如下命令，来监控写入到   &lt;code&gt;TenMinPsgCnts&lt;/code&gt; topic 中的数据：&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;docker-compose exec sql-client /opt/kafka-client/bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic TenMinPsgCnts --from-beginning     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;每10分钟的搭乘的乘客数可以使用 Tumbling Window 来描述，我们使用   &lt;code&gt;INSERT INTO Sink_TenMinPsgCnts&lt;/code&gt; 来直接将 Query 结果写入到结果表。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;INSERT INTO Sink_TenMinPsgCnts      &lt;br /&gt;SELECT      &lt;br /&gt;  TUMBLE_START(rideTime, INTERVAL &amp;apos;10&amp;apos; MINUTE) AS cntStart,       &lt;br /&gt;  TUMBLE_END(rideTime, INTERVAL &amp;apos;10&amp;apos; MINUTE) AS cntEnd,     &lt;br /&gt;  CAST(SUM(psgCnt) AS BIGINT) AS cnt      &lt;br /&gt;FROM Rides      &lt;br /&gt;GROUP BY TUMBLE(rideTime, INTERVAL &amp;apos;10&amp;apos; MINUTE);     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;我们可以监控到   &lt;code&gt;TenMinPsgCnts&lt;/code&gt;  topic 的数据以 JSON 的形式写入到了 Kafka 中：&lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://img.alicdn.com/tfs/TB1.GRFakY2gK0jSZFgXXc5OFXa-1908-1502.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#23454;&amp;#20363;5&amp;#65306;&amp;#23558;-Update-&amp;#27969;&amp;#20889;&amp;#20837;-ElasticSearch" title="&amp;#23454;&amp;#20363;5&amp;#65306;&amp;#23558; Update &amp;#27969;&amp;#20889;&amp;#20837; ElasticSearch"&gt;&lt;/a&gt;实例5：将 Update 流写入 ElasticSearch&lt;/h2&gt; &lt;p&gt;最后我们实践一下将一个持续更新的 Update 流写入 ElasticSearch 中。我们希望将  &lt;strong&gt;“每个区域出发的行车数”&lt;/strong&gt;，写入到 ES 中。&lt;/p&gt; &lt;p&gt;我们也已经预定义好了一张   &lt;code&gt;Sink_AreaCnts&lt;/code&gt; 的 ElasticSearch 结果表（  &lt;a href="https://github.com/ververica/sql-training/blob/master/build-image/training-config.yaml" rel="noopener" target="_blank"&gt;training-config.yaml&lt;/a&gt; 中有完整的表定义）。该表中只有两个字段   &lt;code&gt;areaId&lt;/code&gt; 和   &lt;code&gt;cnt&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;同样的，我们也使用   &lt;code&gt;INSERT INTO&lt;/code&gt; 将 Query 结果直接写入到   &lt;code&gt;Sink_AreaCnts&lt;/code&gt; 表中。&lt;/p&gt; &lt;table&gt;  &lt;tr&gt;   &lt;td&gt;    &lt;pre&gt;INSERT INTO Sink_AreaCnts      &lt;br /&gt;SELECT toAreaId(lon, lat) AS areaId, COUNT(*) AS cnt      &lt;br /&gt;FROM Rides      &lt;br /&gt;WHERE isStart     &lt;br /&gt;GROUP BY toAreaId(lon, lat);     &lt;br /&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt; &lt;p&gt;在 SQL CLI 中执行上述 Query 后，Elasticsearch 会自动地创建   &lt;code&gt;area-cnts&lt;/code&gt; 索引。Elasticsearch 提供了一个 REST API 。我们可以访问 &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;查看   &lt;code&gt;area-cnts&lt;/code&gt;索引的详细信息：    &lt;a href="http://localhost:9200/area-cnts" rel="noopener" target="_blank"&gt;http://localhost:9200/area-cnts&lt;/a&gt; &lt;/li&gt;  &lt;li&gt;查看   &lt;code&gt;area-cnts&lt;/code&gt;索引的统计信息：    &lt;a href="http://localhost:9200/area-cnts/_stats" rel="noopener" target="_blank"&gt;http://localhost:9200/area-cnts/_stats&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;返回   &lt;code&gt;area-cnts&lt;/code&gt;索引的内容：   &lt;a href="http://localhost:9200/area-cnts/_search" rel="noopener" target="_blank"&gt;http://localhost:9200/area-cnts/_search&lt;/a&gt;&lt;/li&gt;  &lt;li&gt;显示 区块 49791 的行车数：   &lt;a href="http://localhost:9200/area-cnts/_search?q=areaId:49791" rel="noopener" target="_blank"&gt;http://localhost:9200/area-cnts/_search?q=areaId:49791&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;随着 Query 的一直运行，你也可以观察到一些统计值（  &lt;code&gt;_all.primaries.docs.count&lt;/code&gt;,   &lt;code&gt;_all.primaries.docs.deleted&lt;/code&gt;）在不断的增长：  &lt;a href="http://localhost:9200/area-cnts/_stats" rel="noopener" target="_blank"&gt;http://localhost:9200/area-cnts/_stats&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;  &lt;a href="http://wuchong.me/#&amp;#24635;&amp;#32467;" title="&amp;#24635;&amp;#32467;"&gt;&lt;/a&gt;总结&lt;/h2&gt; &lt;p&gt;本文带大家使用 Docker Compose 快速上手 Flink SQL 的编程，并对比 Window Aggregate 和 Group Aggregate 的区别，以及这两种类型的作业如何写入到 外部系统中。感兴趣的同学，可以基于这个 Docker 环境更加深入地去实践，例如运行自己写的 UDF , UDTF, UDAF。查询内置地其他源表等等。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>flink flink</category>
      <guid isPermaLink="true">https://itindex.net/detail/59971-flink-sql-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Tue, 20 Aug 2019 16:16:22 CST</pubDate>
    </item>
    <item>
      <title>iMovie 入门教程：视频编辑的利器</title>
      <link>https://itindex.net/detail/59836-imovie-%E8%A7%86%E9%A2%91%E7%BC%96%E8%BE%91</link>
      <description>&lt;p&gt;我们每天看别人的视频，有时也拿起手机拍一段。你有没有想过，把自己的照片和视频做成一部短片呢？&lt;/p&gt;
 &lt;p&gt;现在的视频编辑软件早就脱离了专业人士，操作日趋傻瓜化，对新人非常友好。哪怕你是一个彻底的外行，也能很轻松地做出一部像样的短片。本文就教你怎么使用免费软件 iMovie 编辑视频。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071523.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;a href="https://support.apple.com/imovie"&gt;iMovie&lt;/a&gt; 是苹果公司推出的视频编辑软件，面向大众，免费使用。虽然不要钱，但是功能一点都不弱，足以满足一般的家庭需求，甚至像  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/mov584384e12"&gt;倒退&lt;/a&gt;、  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/movb8c659f55"&gt;分屏&lt;/a&gt;、  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/mova1aaa682b"&gt;画中画&lt;/a&gt;、  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/mov82da1338a"&gt;绿屏替换&lt;/a&gt;等高级功能，它也支持。如果你不是专业人士，完全不必破费去购买昂贵的专业视频软件，先用 iMovie 试试看，它很可能超出你的预期。&lt;/p&gt;

 &lt;p&gt;注意，iMovie 只用于苹果设备，其他系统无法使用。如果你使用 Windows 系统，可以下载   &lt;a href="https://www.blackmagicdesign.com/products/davinciresolve/"&gt;Davinci Resolve&lt;/a&gt; 的免费版试试看。&lt;/p&gt;

 &lt;p&gt;需要说明的是，我也是 iMovie 的初学者，这篇文章其实是我的学习笔记。学习过程中，我做了一个三分钟的视频（  &lt;a href="http://v.youku.com/v_show/id_XNDI3MzA5ODY5Mg==.html"&gt;优酷&lt;/a&gt;，  &lt;a href="https://www.youtube.com/watch?v=3Kh5-sEE_kk"&gt;Youtube&lt;/a&gt;）。虽然制作水平不高，手机拍照效果也不好，但这就是新人学习一个晚上 iMovie，能够达到的效果。&lt;/p&gt;

 &lt;h2&gt;一、界面&lt;/h2&gt;

 &lt;p&gt;iMovie 有 Mac、iPhone 和 iPad 三个版本，本文针对 Mac 版本，因为它的功能最强。而且学会了桌面版，再用手机版就毫无难度了。&lt;/p&gt;

 &lt;p&gt;如果你的 Mac 没有安装 iMovie，请先到 App store 安装。&lt;/p&gt;

 &lt;p&gt;打开 iMovie，就会进入项目界面。一个&amp;quot;项目&amp;quot;（project）就是你要制作的一部视频。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071501.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;点击那个大号的  &lt;code&gt;+&lt;/code&gt;，选择&amp;quot;影片&amp;quot;，或者使用顶部菜单栏的&amp;quot;文件 / 新建影片&amp;quot;选项。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071502.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;现在进入了视频编辑的主界面。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071503.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;主界面主要分成三个区域。&lt;/p&gt;

 &lt;ul&gt;
  &lt;li&gt;时间轴（timeline，下半部分）：用来编排原始材料的时间顺序。&lt;/li&gt;
  &lt;li&gt;预览区（preview，右上角）：预览视频效果。&lt;/li&gt;
  &lt;li&gt;媒体区（media，左上角）：存放原始的素材文件。&lt;/li&gt;
&lt;/ul&gt;

 &lt;p&gt;新建影片的默认名称是&amp;quot;我的影片&amp;quot;，点击顶部工具栏左侧的&amp;quot;项目&amp;quot;按钮，可以修改影片名称。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071521.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071522.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;二、导入素材&lt;/h2&gt;

 &lt;p&gt;点击媒体区的&amp;quot;导入媒体&amp;quot;按钮，或者点击顶部工具栏的向下箭头  &lt;code&gt;↓&lt;/code&gt;，或者按下  &lt;code&gt;⌘ + I&lt;/code&gt;，就会跳出导入窗口。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071504.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;在这个窗口中，选中用来制作视频的原始素材文件。&lt;/p&gt;

 &lt;p&gt;选中后，点击右下角的&amp;quot;导入所选项&amp;quot;按钮，返回主界面。可以看到，选中的文件已经加入了媒体区。&lt;/p&gt;

 &lt;h2&gt;三、插入时间轴&lt;/h2&gt;

 &lt;p&gt;在媒体区，选择一个素材文件，照片或视频都可以，拖到时间轴。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071505.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;接着，将其他要放进短片的素材，依次拖下来，按照顺序排列好。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071506.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;短片的基本架构就有了。&lt;/p&gt;

 &lt;h2&gt;四、视频编辑&lt;/h2&gt;

 &lt;p&gt;我们可能只需要素材的一小段，这时就需要修剪素材。&lt;/p&gt;

 &lt;p&gt;首先，在时间轴选中目标视频，出现一个黄色方框。然后，将鼠标停在要修剪的地方，按住 R 键不放，拖动鼠标，黄色方框的两侧就会出现控制柄。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071507.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;确定黄色方框选中要保留的范围后，调出该片段的快捷键菜单（control + 点击），选中&amp;quot;修剪所选部分&amp;quot;，视频修剪就完成了。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071508.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;预览区上方还有一排按钮。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071510.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;可以用来完成调节颜色、白平衡、裁切等操作。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071509.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;五、转场&lt;/h2&gt;

 &lt;p&gt;片断与片断之间的过渡，iMovie 称为&amp;quot;转场&amp;quot;。&lt;/p&gt;

 &lt;p&gt;点击&amp;quot;媒体区&amp;quot;上方的&amp;quot;转场&amp;quot;按钮，iMovie 默认提供了很多转场效果。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071511.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;选中一个效果，将它拖到时间轴的两个片断之间即可。&lt;/p&gt;

 &lt;p&gt;如果你觉得，一个个视频添加转场效果太麻烦，可以点击时间轴右上角的&amp;quot;设置&amp;quot;按钮，选中里面的&amp;quot;自动内容&amp;quot;选项即可。&lt;/p&gt;

 &lt;h2&gt;六、字幕&lt;/h2&gt;

 &lt;p&gt;视频往往需要添加说明文字，比如片头的标题。&lt;/p&gt;

 &lt;p&gt;点击&amp;quot;媒体区&amp;quot;上方的&amp;quot;字幕&amp;quot;按钮，会出现各种字幕样式。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071512.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;选中需要的样式，拖到时间轴对应片断的上方。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071513.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;然后，在预览区里面，编辑文字内容。iMovie 只允许改变文本的字体、大小、颜色等，不允许调整字幕样式。必须说，它的字幕功能比较弱。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071514.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;iMovie 还有一个&amp;quot;背景&amp;quot;功能（&amp;quot;媒体区&amp;quot;上方的&amp;quot;背景&amp;quot;按钮），也顺便在这里提一下。它允许在视频插入地图位置演示，或者地图上从 A 点到 B 点的路线动画，比字幕更生动。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071515.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;七、音乐&lt;/h2&gt;

 &lt;p&gt;视频素材编排完成后，往往需要添加背景音乐。&lt;/p&gt;

 &lt;p&gt;点击顶部工具栏的向下箭头  &lt;code&gt;↓&lt;/code&gt;，进入&amp;quot;导入&amp;quot;窗口，将所需的音频文件导入&amp;quot;媒体区&amp;quot;。然后，将音频文件拖到时间轴下方，会出现的绿色的音频轴。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071516.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;接着，拖动音频的时间长度，跟视频的长度匹配就可以了。&lt;/p&gt;

 &lt;p&gt;这里往往还需要在开头和结尾，为音频加上  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/move7a2dcdc6"&gt;淡入淡出效果&lt;/a&gt;。此外，音频的  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/mov3b5ded23e"&gt;音量&lt;/a&gt;和  &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/mov86277afbd"&gt;均衡&lt;/a&gt;，都是可以调的。&lt;/p&gt;

 &lt;p&gt;媒体区还有录音按钮，可以录制画外音，为视频加上旁白。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071517.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h2&gt;八、导出视频&lt;/h2&gt;

 &lt;p&gt;短片制作完成后，就可以导出了。&lt;/p&gt;

 &lt;p&gt;点击顶部工具栏右侧的&amp;quot;导出&amp;quot;按钮。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071518.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;弹出菜单里面，点击&amp;quot;文件&amp;quot;按钮。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071519.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;设置选项框里面，选好各种导出选项，包括分辨率和压缩质量等。最后，点击&amp;quot;下一步&amp;quot;按钮即可。&lt;/p&gt;

 &lt;p&gt;  &lt;img alt="" src="https://www.wangbase.com/blogimg/asset/201907/bg2019071520.jpg" title=""&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;p&gt;iMovie 导出的视频文件体积很大。文章开头那个三分种的视频，原始导出的体积是 500MB。为了减少文件体积，我后来又安装了   &lt;a href="https://www.videolan.org/vlc/index.html"&gt;VLC 播放器&lt;/a&gt;，它菜单里的&amp;quot;文件/转换&amp;quot;功能支持  &lt;code&gt;H.264&lt;/code&gt;协议压缩视频，压缩后的 MP4 文件是 70MB 左右，这个体积就比较能接受了。&lt;/p&gt;

 &lt;h2&gt;九、参考链接&lt;/h2&gt;

 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://help.apple.com/imovie/mac/10.1/?lang=zh-cn#/mov5ec96da08"&gt;iMovie 官方帮助文档&lt;/a&gt;, Apple Inc.&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://blog.storyblocks.com/video-tutorials/getting-started-imovie/"&gt;A Beginner&amp;apos;s Guide to Getting Started in iMovie&lt;/a&gt;, Christopher Petow&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://mediacommons.psu.edu/2017/12/03/introduction-to-basic-editing-in-imovie/"&gt;Introduction to Basic Editing in iMovie&lt;/a&gt;, The Pennsylvania State University&lt;/li&gt;
&lt;/ul&gt;

 &lt;p&gt;（完）&lt;/p&gt;
 &lt;div&gt;  &lt;h3&gt;文档信息&lt;/h3&gt;
  &lt;ul&gt;
   &lt;li&gt;版权声明：自由转载-非商用-非衍生-保持署名（    &lt;a href="http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh"&gt;创意共享3.0许可证&lt;/a&gt;）&lt;/li&gt;
   &lt;li&gt;发表日期： 2019年7月15日&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt; &lt;div&gt;&lt;/div&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>Developer</category>
      <guid isPermaLink="true">https://itindex.net/detail/59836-imovie-%E8%A7%86%E9%A2%91%E7%BC%96%E8%BE%91</guid>
      <pubDate>Mon, 15 Jul 2019 18:05:44 CST</pubDate>
    </item>
    <item>
      <title>Google 推出了一个游戏生成器，让不会编程的你也能自己设计游戏</title>
      <link>https://itindex.net/detail/59700-google-%E6%8E%A8%E5%87%BA-%E6%B8%B8%E6%88%8F</link>
      <description>&lt;p&gt;从像素时代开始，很多游戏就有「自定义模式」，它允许玩家将一些基本元素组合，制作出独特的关卡设计。这种玩法后来被《我的世界》发扬光大，超高的自由度让每个玩家都成了「创世神」，只要你想象力足够丰富，动手能力足够强，就可以在游戏世界里建造出任何东西。&lt;/p&gt; &lt;p&gt;最近 Google 也推出了一款叫做《Game Builder》游戏，你不仅能在里面随意建造物体，更可以通过引导设计出一个完整的游戏。它不需要你有任何编程基础，只要通过简单的卡片组合，就能实现复杂的交互逻辑。&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;h2&gt;是玩家，也是「上帝」&lt;/h2&gt; &lt;p&gt;《Game Builder》目前已经在 Steam 上架，支持 Windows、Mac 平台，它的本质就是一款游戏。你可以新建一个项目，根据引导学习如何设计一款游戏。初始角色是一个机器人，你可以用 WASD 控制移动，空格键跳跃，Shift 加速。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/5b/e4/5be47aa005405104d841a016ce3d59c3.png" width="auto"&gt;&lt;/img&gt;&lt;/div&gt; &lt;p&gt;&lt;/p&gt; &lt;p&gt;遇到更高的台阶怎么办？以一个玩家的角度，当然是想办法去学习二段跳，或者踩在其它物体上进行跳跃。在《Game Builder》里，你除了是一个玩家，也是游戏设计师，可以站在上帝视角对游戏世界进行各种改动。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/75/d1/75d1d5511d5fa0782477f12d667d10d5.png" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;
        &lt;br /&gt;
      &lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;h2&gt;不用代码，拖动卡片就能实现复杂交互&lt;/h2&gt; &lt;p&gt;界面顶部有 Play 和 Build 两个按钮，切换到 Build 可以进入建造模式。点击选中机器人，在 Logic 中找到 Player Control，调整 Walk and Jump 的 JumpHeight 参数，现在你的人物就可以跳得更高了。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/8a/9d/8a9d223e09d52b10a1059c64bda78ae9.png" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;在引导模式中，《Game Builder》会用游戏的方式引导你吃星星，沿途学习基本的游戏设计知识。你可以按 V 切换第一人称视角、第三人称视角、俯视视角，以此体验 FPS、RPG 等类型的游戏。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/d1/d4/d1d403ce6443d86dc49880c2201a9417.png" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;利用 If-Then 逻辑可以让角色与其它元素产生交互，冲撞树木使其变红，攻击敌人使其掉血。这些交互看似简单，实际在设计游戏时需要用代码实现，而《Game Builder》把这些复杂的操作简化成了卡片交互，只要拖动相应的卡片，选择触发动作和反馈方式，就可以创建复杂的交互逻辑。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/4c/62/4c6237bed10bed4aeab9fe999992e396.png" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;
        &lt;br /&gt;
      &lt;/p&gt; &lt;p&gt;&lt;/p&gt; &lt;h2&gt;人人皆是云玩家，云设计师&lt;/h2&gt; &lt;p&gt;《Game Builder》的设想是让没有编程基础的人也能通过拖动卡片，设计出自己想要的游戏。它们将游戏中所需的各种要素都简化成了一个个卡片，移动、生命值、攻击、治疗药水，十分丰富。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/ab/88/ab881c614eabad379bd3a6c1975ed3c8.png" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;为了让初学者更容易上手，《Game Builder》在主菜单页面提供了卡片 Demo，你可以控制机器人与游戏中的元素产生互动，旁边文字标明了这个行为会产生何种后果，以及它是怎么发生的。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/ad/91/ad91c2a3e22a2f85c4c1039c919d8bb3.png" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;Google 之前针对 3D 艺术家推出了 Poly 平台，用户可以上传和下载 3D 模型，这些模型现在可以直接用到《Game Builder》中。游戏建造还支持多人模式，你可以与朋友共同建造一个游戏世界。&lt;/p&gt; &lt;div&gt;
            &lt;img height="auto" src="https://imgslim.geekpark.net/uploads/image/file/fb/c9/fbc94d959742e0bf342e5c355d0c6673.gif" width="auto"&gt;&lt;/img&gt;
        &lt;/div&gt; &lt;p&gt;《Game Builder》的更新文件中出现了 YouTube Textures 的信息，与云游戏平台 Stadia 的实现方式刚好吻合。Google 很可能是在用这种方式，让更多人参与到游戏创作中，而不仅仅是作为「云玩家」。  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;下载地址：  &lt;a href="https://store.steampowered.com/app/929860/Game_Builder/" target="_blank"&gt;Steam&lt;/a&gt;&lt;/p&gt; &lt;p&gt;编辑：Rubberso&lt;/p&gt;
      &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/59700-google-%E6%8E%A8%E5%87%BA-%E6%B8%B8%E6%88%8F</guid>
      <pubDate>Mon, 17 Jun 2019 10:53:53 CST</pubDate>
    </item>
    <item>
      <title>2019高考编程卷：谷歌面试编程题及解题技巧（MIT版）</title>
      <link>https://itindex.net/detail/59667-%E9%AB%98%E8%80%83-%E7%BC%96%E7%A8%8B-%E8%B0%B7%E6%AD%8C</link>
      <description>&lt;ul&gt;&lt;li&gt;&lt;p&gt;课程链接：http://courses.csail.mit.edu/iap/interview/index.php&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;本课程重点介绍科技公司在面试时经常出现的计算机科学问题，其中包括&lt;mark data-type=technologies data-id=324e28ee-fa13-4e36-9968-ae71059f5948&gt;时间复杂度&lt;/mark&gt;、哈希表、二进制树搜索，以及 MIT「算法设计与分析」（MIT 6.046）课程中会出现的内容。但是，大部分时间都会专注于你不会在课堂上学到的内容，例如刁钻的按位&lt;mark data-type=technologies data-id=95a97f4b-79d2-4bbc-91ae-300f074dff9f&gt;逻辑&lt;/mark&gt;和解决问题的技巧。&lt;/p&gt;&lt;p&gt;&lt;img src="https://image.jiqizhixin.com/uploads/editor/45f1aa1c-814d-4142-a7b7-f54066ce93f2/34.png" style="width: 40.4%;" class="fr-fic fr-dib"&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;面试锦囊&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;被问到一个问题时，要和面试官展开对话，让对方知道你在思考。例如，你可能会提供一个较慢或能解决部分问题的方案（让他们知道这个方案并不完美），提到一些关于这个问题的观察结果，或者说一下任何有可能对解决问题有帮助的想法。如果你卡住了，面试官通常会给你点提示。&lt;/p&gt;&lt;p&gt;面试期间，你通常会被要求写一个程序。出于某种原因，面试官通常让人在黑板或纸上写，而不是给你一台电脑。所以，有必要在面试之前练一下在板子上写代码，以备不时之需。&lt;/p&gt;&lt;p&gt;以下是编程面试中的一些注意事项：&lt;/p&gt;&lt;p&gt;这些事要做：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;如果对问题有哪里不理解或有歧义，一定要问清楚；&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;让面试官知道你在想什么；&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;针对问题提出多个解决方案；&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;与面试官交流想法（如关于数据结构和算法的想法）&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;如果你卡住了，不要害怕让他们知道，可以礼貌地寻求提示。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;这些事不要做：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;不要放弃！放弃对你展示自己的问题解决技巧没有任何帮助；&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;思考期间不要只是安静地坐在那里。面试官要在有限的时间内尽可能多地了解你，不和他们交流无法向他们传递任何信息；&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;如果你已经知道问题的答案，不要脱口而出！不然他们会觉得你提前看过这个问题并记下了答案。至少要在给出答案之前假装思考一阵儿。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img src="https://image.jiqizhixin.com/uploads/editor/1fc4658e-367e-4ec6-ad41-5fd2351020a7/4.png" style="width: 38.71%;" class="fr-fic fr-dib"&gt;&lt;/p&gt;&lt;p&gt;好了，如果你对自己的备考情况很有信心，以下是其中的一些经典问题：&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 1：硬币难题&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;假设你有 8 枚大小相同的硬币，但其中 1 枚硬币要比其他 7 枚稍重一点（但你不知道具体是哪一枚）。同时，你还有一个老式天平可以称重，从而得出哪枚硬币稍重（或是否重量相同）。那么，最少要称多少次才能找出那枚稍轻的硬币？&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：从 8 枚硬币中取出 6 枚，天平左右盘各放 3 枚。结果会出现三种情况：天平左盘 3 枚硬币重于右盘，则较重的 1 枚在左盘；天平右盘的 3 枚硬币重于左盘，则较重的 1 枚在右盘；天平左右盘重量相等，则称剩下的 2 枚硬币，得出稍重的这枚硬币。&lt;/p&gt;&lt;p&gt;不太好的答案：分别取 4 枚硬币放置于天平左右盘，找出较轻的一组（4 枚），将该组硬币继续分为两组放入天平左右盘，找出较轻的一组（2 枚），再次重复此步骤找到最轻的一枚。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 2：在数组中进行查找&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;给定一个已排序的整数数组，如何找出特定整数 x 的位置？&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：使用二分搜索法。将数组中间的数字与 x 进行比较。如果相同，则找出了 x。如果数组中的数字较大，则需要查看数组后半部分。如果数字较小，则需要查看数组前半部分。通过比较数组中间元素和 x，我们可以重复搜索该数组的前后部分，从而再次将搜索范围缩小 2 倍。我们重复这一过程直至找出 x。这种算法花费的时间为 O(log n)。&lt;/p&gt;&lt;p&gt;不太好的答案：按顺序查看数组的每个数字，与 x 进行比较。这种算法花费的时间为 O(n)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 3：A to I&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;编写一个函数将字符串转换为整数（这个函数被称为 A to I 或者 atoi()），因为我们要将一个 ASCII 字符串转换为整数。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：从头到尾查看整个字符串。如果首个字符为负号，记下来。从 0 开始进行累计求和。每得到一个新数字，总数乘以 10 并加上这个新数字。当计算结束时，返回当前总数，或者如果出现负号，返回该数字的倒数。&lt;/p&gt;&lt;p&gt;凑合的答案：另一种方法也是从头到尾查看整个字符串，再次进行累计求和。记住表示当前你所在数字的数字 x，x 最开始为 1。针对每个字符，将当前数字乘以 x 并添加到累计总数中，同时将 x 乘以 10。当你到达字符串起点时，返回当前总数，或者如果出现负号，返回该数字的倒数。&lt;/p&gt;&lt;p&gt;注意：面试官可能会询问你自身方法的局限性。你应该回答：只有字符串在每个数字前都包含可选负号时，该方法才能生效。同时，你还应提到：如果数字太大，则结果会因为溢值原因而不正确。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 4：颠倒字符串中的单词顺序&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;编写一个函数将字符串中的单词顺序进行颠倒。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;答案：交换第一个与倒数第一个、第二个与倒数第二个字符的顺序，以此类推，颠倒整个字符串。之后，查看整个字符串，找出空格，这样就可以发现每个单词的位置。再次交换第一个与倒数第一个、第二个与倒数第二个单词的顺序，以此类推，颠倒你所遇到的每个单词的顺序。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 5：最近邻&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;假设你有一个包含 n 个人信息的数组。每个人分别用一个字符串（他们的名字）和一个数字（他们在数轴上的位置）表示。每个人有三个朋友，即数字和他本人最接近的三个人。请写出一个可以找出每个人的三个朋友的算法。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：按每个人数字的升序对数组进行排列。查看每个人前后紧邻的三个人，他们的朋友将出现在这六个人当中。这一算法花费的时间为 O(n log n)，因为将人进行分类也会花费那么多时间。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 6：洗牌问题&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;给定一组不同的整数数组，给出一个算法对这些整数进行随机排序，使每个重排序方法的可能性相等。换句话说，给定一副牌，你要如何洗牌才能确保牌的每种排列方法有相同的可能？&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：按顺序排列这些元素，用数组中不先于某个元素出现的随机元素与该元素进行交换。需要的时间为 O(n)。&lt;/p&gt;&lt;p&gt;注意，这个问题有多个可能的答案，也有几种看似不错但实际上并不正确的答案。例如，对上面的算法做一个小小的修改，即，将每个元素与数组中的任意一个元素交换并不能确保每种重排顺序等概率出现。这里给出的答案（在作者看来）是最佳答案。如果想了解其他答案，可以在维基百科上搜一下「Shuffling」。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 7：单链表中的循环&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;如何确定单链表是否有循环？&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：跟踪链表中的两个指针，并在链表的开始处启动它们。在算法的每轮迭代中，将第一个指针往前移一个节点，把第二个指针往前移两个节点。如果两个指针始终相同（不是在算法起点处），那么就有一个循环。如果指针在两个指针相同之前就达到链表的末端，链表中就没有循环。其实，指针不需要一次移动一到两个节点；指针也不需要以不同的速率移动。这个过程需要的时间为 O(n)。这是一个巧妙的回答，面试官会莫名喜欢。&lt;/p&gt;&lt;p&gt;凑合的回答 1：对于你在逐一浏览链表时遇到的每个节点，将指向该节点的指针放入 O(1) 中&amp;mdash;&amp;mdash;查找时间数据结构，如散列集。接下来，当你遇到一个新的节点时，要看看指向那个节点的指针是否已经存在于你的散列集中。这一过程花费的时间为 O(n)，但占用的空间也是 O(n)。&lt;/p&gt;&lt;p&gt;凑合的回答 2：浏览链表中的元素。「Mark」你到达的每个节点。如果在抵达末端之前你到达了一个 mark 过的节点，列表中就有循环，否则就没有循环。这一过程花费的时间也是 O(n)。&lt;/p&gt;&lt;p&gt;注意，这个问题在技术上是不恰当的。一个普通的链表不会有循环。他们的意思是让你决定能否从一个图中的节点到达循环，该图包含最多有一条输出边的节点。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 8：计算 2^x&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;如何快速计算 2^x？&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;优秀答案：1 &amp;lt;&amp;lt; x (1 left‐shifted by x)&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 9：&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;是一种排序保存项目的数据结构，它由二叉树组成。每个节点都有一个指向两个子节点的指针（可能为 null），一个指向其父节点的可选指针（也可以为 null），以及一个存储在树中的元素（可能是一个字符串或一个整数）。要使&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;有效，每个节点的元素必须大于其左子树中的每个元素，并且小于其右子树中的每个元素。例如，二叉树可能如下所示：&lt;/p&gt;&lt;p&gt;&lt;img src="https://image.jiqizhixin.com/uploads/editor/9857679e-218e-45eb-98a8-e25bfbd6e1b1/bin.jpg" style="width: 42.41%;" class="fr-fic fr-dib"&gt;&lt;/p&gt;&lt;p&gt;要检查元素是否出现在&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;中，只需要遵循父对子之间的相应连接。例如，如果我们想在上面的树中搜索 15，我们从最上方的 17 开始。由于 15&amp;lt;17，我们移动到左边的节点 6。由于 15&amp;gt; 6，我们移动到右边的节点 12；由于 15&amp;gt;12，我们再次移动到正确的节点 15，最终找到了需要的数字。&lt;/p&gt;&lt;p&gt;要将元素加入&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;，我们就要像搜索元素一样，遵循从父节点到子节点的正确连接。当所需的子项为 null 时，我们将该元素添加为新的子节点。例如，如果我们要在上面的树中添加 14，我们就需要不断往下寻找添加的位置。当我们到达 15，就会看到该节点没有左子节点，因此我们将 14 添加为左子节点。&lt;/p&gt;&lt;p&gt;要从&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;中删除一个元素，我们首先要找出包含该元素的节点。如果该节点没有子节点，直接删除即可。如果该节点有一个子节点，则用这个子节点替代它。如果该节点有两个子节点，我们通过一种算法确定树中下一个更小或下一个更大的元素。为简单起见，这里就不赘述所使用的算法了。我们将节点中存储的元素设定为该值。之后，我们从树中拼接包含该值的节点。这个过程相对较容易，因为节点最多有一个子节点。例如，为了从树中删除 6，我们首先将节点值更改为 3。之后，我们删除原本值为 3 的节点，并将原本值为 6 的节点的左子节点值设定为 1。&lt;/p&gt;&lt;p&gt;在&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;上做小小的修改，就可以使用它将键与值关联起来，就像在散列表中一样。我们不需要在每个节点上存储单个值，而是存储一个键值对。该树将根据节点的键进行排序。&lt;/p&gt;&lt;p&gt;面试官有时会问到&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;的问题。此外，&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;往往在回答面试问题时也很有用。需要记住的重要一点是，插入、删除和查找需要的时间为 O(log n)，其中 n 是树中的元素数量，因为一个平衡良好的&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;的高度是 O(log n)。尽管在最糟糕的情况下，一个&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;的高度可能为 O(n)，「自平衡」&lt;mark data-type=technologies data-id=be79d405-c357-4f51-ae05-0295220f3da0&gt;二叉搜索树&lt;/mark&gt;可以周期性地重组一个 BST 来确保其高度为 O(log n)。许多自平衡 BST 保证这些操作花费的时间为 O(log n)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;问题 10：排除 bug&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;描述一种从程序中找出 bug 的方法。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;答案：这个问题有多个可能的答案，也是面试官经常会问的开放性问题。优秀答案可能包括：根据程序的行为判断可能出现 bug 的部分；使用断点和 stepper 逐步执行程序。任何试图找到 bug 源头和缩小 bug 搜索范围的方法都是好答案。&lt;/p&gt;&lt;p&gt;&lt;img src="https://image.jiqizhixin.com/uploads/editor/111ee4db-a39e-4809-a97d-b2ce16f52a2c/6.jpg" style="width: 45.69%;" class="fr-fic fr-dib"&gt;&lt;/p&gt;&lt;p&gt;以上是谷歌程序员面试时可能出现的编程题及解题技巧。当然，只是其中很小的一部分。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;想要了解更多问题和答案请点击：http://courses.csail.mit.edu/iap/interview/materials.php&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;MIT《算法设计与分析》课程资料：https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-046j-design-and-analysis-of-algorithms-spring-2015/&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;最后祝大家取得一个理想的高考成绩！&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/59667-%E9%AB%98%E8%80%83-%E7%BC%96%E7%A8%8B-%E8%B0%B7%E6%AD%8C</guid>
      <pubDate>Fri, 07 Jun 2019 12:33:15 CST</pubDate>
    </item>
    <item>
      <title>程序员笔记——如何编写优雅的Dockerfile</title>
      <link>https://itindex.net/detail/59523-%E7%A8%8B%E5%BA%8F%E5%91%98-%E7%AC%94%E8%AE%B0-dockerfile</link>
      <description>&lt;h2&gt;导读&lt;/h2&gt;
 &lt;p&gt;Kubernetes要从容器化开始，而容器又需要从Dockerfile开始，本文将介绍如何写出一个优雅的Dockerfile文件。&lt;/p&gt;
 &lt;p&gt;文章主要内容包括：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Docker容器&lt;/li&gt;
  &lt;li&gt;Dockerfile&lt;/li&gt;
  &lt;li&gt;使用多阶构建&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;感谢公司提供大量机器资源及时间让我们可以实践，感谢在此专题上不断实践的部分项目及人员的支持。  &lt;br /&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000019015349" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;一、Docker容器&lt;/h2&gt;
 &lt;h3&gt;1.1 容器的特点&lt;/h3&gt;
 &lt;p&gt;我们都知道容器就是一个标准的软件单元，它有以下特点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;随处运行：容器可以将代码与配置文件和相关依赖库进行打包，从而确保在任何环境下的运行都是一致的。&lt;/li&gt;
  &lt;li&gt;高资源利用率：容器提供进程级的隔离，因此可以更加精细地设置CPU和内存的使用率，进而更好地利用服务器的计算资源。&lt;/li&gt;
  &lt;li&gt;快速扩展：每个容器都可作为单独的进程予以运行，并且可以共享底层操作系统的系统资源，这样一来可以加快容器的启动和停止效率。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;1.2 Docker容器&lt;/h3&gt;
 &lt;p&gt;目前市面上的主流容器引擎有Docker、Rocket/rkt、OpenVZ/Odin等等，而独霸一方的容器引擎就是使用最多的Docker容器引擎。&lt;/p&gt;
 &lt;p&gt;Docker容器是与系统其他部分隔离开的一系列进程，运行这些进程所需的所有文件都由另一个镜像提供，从开发到测试再到生产的整个过程中，Linux 容器都具有可移植性和一致性。相对于依赖重复传统测试环境的开发渠道，容器的运行速度要快得多，并且支持在多种主流云平台（PaaS）和本地系统上部署。Docker容器很好地解决了“开发环境能正常跑，一上线就各种崩”的尴尬。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000019015350" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;Docker容器的特点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;轻量：容器是进程级的资源隔离，而虚拟机是操作系统级的资源隔离，所以Docker容器相对于虚拟机来说可以节省更多的资源开销，因为Docker容器不再需要GuestOS这一层操作系统了。&lt;/li&gt;
  &lt;li&gt;快速：容器的启动和创建无需启动GuestOS，可以实现秒级甚至毫秒级的启动。&lt;/li&gt;
  &lt;li&gt;可移植性：Docker容器技术是将应用及所依赖的库和运行时的环境技术改造包成容器镜像，可以在不同的平台运行。&lt;/li&gt;
  &lt;li&gt;自动化：容器生态中的容器编排工作（如：Kubernetes）可帮助我们实现容器的自动化管理。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;二、Dockerfile&lt;/h2&gt;
 &lt;p&gt;Dockerfile是用来描述文件的构成的文本文档，其中包含了用户可以在使用行调用以组合Image的所有命令，用户还可以使用Docker build实现连续执行多个命令指今行的自动构建。&lt;/p&gt;
 &lt;p&gt;通过编写Dockerfile生磁镜像，可以为开发、测试团队提供基本一致的环境，从而提升开发、测试团队的效率，不用再为环境不统一而发愁，同时运维也能更加方便地管理我们的镜像。&lt;/p&gt;
 &lt;p&gt;Dockerfile的语法非常简单，常用的只有11个：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000019015351" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;2.1 编写优雅地Dockerfile&lt;/h3&gt;
 &lt;p&gt;编写优雅的Dockerfile主要需要注意以下几点：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Dockerfile文件不宜过长，层级越多最终制作出来的镜像也就越大。&lt;/li&gt;
  &lt;li&gt;构建出来的镜像不要包含不需要的内容，如日志、安装临时文件等。&lt;/li&gt;
  &lt;li&gt;尽量使用运行时的基础镜像，不需要将构建时的过程也放到运行时的Dockerfile里。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;只要记住以上三点就能写出不错的Dockerfile。&lt;/p&gt;
 &lt;p&gt;为了方便大家了解，我们用两个Dockerfile实例进行简单的对比：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y apt-utils libjpeg-dev \     
python-pip
RUN pip install --upgrade pip
RUN easy_install -U setuptools
RUN apt-get clean&lt;/code&gt;&lt;/pre&gt;
 &lt;pre&gt;  &lt;code&gt;FROM ubuntu:16.04
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y apt-utils \
  libjpeg-dev python-pip \
           &amp;amp;&amp;amp; pip install --upgrade pip \
      &amp;amp;&amp;amp; easy_install -U setuptools \
    &amp;amp;&amp;amp; apt-get clean&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;我们看第一个Dockerfile，乍一看条理清晰，结构合理，似乎还不错。再看第二个Dockerfile，紧凑，不易阅读，为什么要这么写？&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;第一个Dockerfile的好处是：当正在执行的过程某一层出错，对其进行修正后再次Build，前面已经执行完成的层不会再次执行。这样能大大减少下次Build的时间，而它的问题就是会因层级变多了而使镜像占用的空间也变大。&lt;/li&gt;
  &lt;li&gt;第二个Dockerfile把所有的组件全部在一层解决，这样做能一定程度上减少镜像的占用空间，但在制作基础镜像的时候若其中某个组编译出错，修正后再次Build就相当于重头再来了，前面编译好的组件在一个层里，得全部都重新编译一遍，比较消耗时间。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;从下表可以看出两个Dockerfile所编译出来的镜像大小：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ docker images | grep ubuntu      
REPOSITORY      TAG     IMAGE ID    CREATED     SIZE                                                                                                                                   
ubuntu                   16.04       9361ce633ff1  1 days ago 422MB
ubuntu                   16.04-1   3f5b979df1a9  1 days ago  412MB&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;呃…. 好像并没有特别的效果，但若Dockerfile非常长的话可以考虑减少层次，因为Dockerfile最高只能有127层。&lt;/p&gt;
 &lt;h2&gt;三、使用多阶构建&lt;/h2&gt;
 &lt;p&gt;Docker在升级到Docker 17.05之后就能支持多阶构建了，为了使镜像更加小巧，我们采用多阶构建的方式来打包镜像。在多阶构建出现之前我们通常使用一个Dockerfile或多个Dockerfile来构建镜像。&lt;/p&gt;
 &lt;h3&gt;3.1单文件构建&lt;/h3&gt;
 &lt;p&gt;在多阶构建出来之前使用单个文件进行构建，单文件就是将所有的构建过程（包括项目的依赖、编译、测试、打包过程）全部包含在一个Dockerfile中之下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;FROM golang:1.11.4-alpine3.8 AS build-env
ENV GO111MODULE=off
ENV GO15VENDOREXPERIMENT=1
ENV BUILDPATH=github.com/lattecake/hello
RUN mkdir -p /go/src/${BUILDPATH}
COPY ./ /go/src/${BUILDPATH}
RUN cd /go/src/${BUILDPATH} &amp;amp;&amp;amp; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install –v

CMD [/go/bin/hello]&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这种的做法会带来一些问题：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Dockerfile文件会特别长，当需要的东西越来越多的时候可维护性指数级将会下降；&lt;/li&gt;
  &lt;li&gt;镜像层次过多，镜像的体积会逐步增大，部署也会变得越来越慢；&lt;/li&gt;
  &lt;li&gt;代码存在泄漏风险。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;以Golang为例，它运行时不依赖任何环境，只需要有一个编译环境，那这个编译环境在实际运行时是没有任务作用的，编译完成后，那些源码和编译器已经没有任务用处了也就没必要留在镜像里。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000019015352" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;上表可以看到，单文件构建最终占用了312MB的空间。&lt;/p&gt;
 &lt;h3&gt;3.2 多文件构建&lt;/h3&gt;
 &lt;p&gt;在多阶构建出来之前有没有好的解决方案呢？有，比如采用多文件构建或在构建服务器上安装编译器，不过在构建服务器上安装编译器这种方法我们就不推荐了，因为在构建服务器上安装编译器会导致构建服务器变得非常臃肿，需要适配各个语言多个版本、依赖，容易出错，维护成本高。所以我们只介绍多文件构建的方式。&lt;/p&gt;
 &lt;p&gt;多文件构建，其实就是使用多个Dockerfile，然后通过脚本将它们进行组合。假设有三个文件分别是：Dockerfile.run、Dockerfile.build、build.sh。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Dockerfile.run就是运行时程序所必须需要的一些组件的Dockerfile，它包含了最精简的库；&lt;/li&gt;
  &lt;li&gt;Dockerfile.build只是用来构建，构建完就没用了；&lt;/li&gt;
  &lt;li&gt;build.sh的功能就是将Dockerfile.run和Dockerfile.build进行组成，把Dockerfile.build构建好的东西拿出来，然后再执行Dockerfile.run，算是一个调度的角色。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;Dockerfile.build&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;FROM golang:1.11.4-alpine3.8 AS build-env
ENV GO111MODULE=off
ENV GO15VENDOREXPERIMENT=1
ENV BUILDPATH=github.com/lattecake/hello
RUN mkdir -p /go/src/${BUILDPATH}
COPY ./ /go/src/${BUILDPATH}
RUN cd /go/src/${BUILDPATH} &amp;amp;&amp;amp; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install –v&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;Dockerfile.run&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;FROM alpine:latest
RUN apk –no-cache add ca-certificates
WORKDIR /root
ADD hello .
CMD [&amp;quot;./hello&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;Build.sh&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;#!/bin/sh
docker build -t –rm hello:build . -f Dockerfile.build
docker create –name extract hello:build
docker cp extract:/go/bin/hello ./hello
docker rm -f extract
docker build –no-cache -t –rm hello:run . -f Dockerfile.run
rm -rf ./hello&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;执行build.sh完成项目的构建。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000019015353" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从上表可以看到，多文件构建大大减小了镜像的占用空间，但它有三个文件需要管理，维护成本也更高一些。&lt;/p&gt;
 &lt;h3&gt;3.3 多阶构建&lt;/h3&gt;
 &lt;p&gt;最后我们来看看万众期待的多阶构建。&lt;/p&gt;
 &lt;p&gt;完成多阶段构建我们只需要在Dockerfile中多次使用FORM声明，每次FROM指令可以使用不同的基础镜像，并且每次FROM指令都会开始新的构建，我们可以选择将一个阶段的构建结果复制到另一个阶段，在最终的镜像中只会留下最后一次构建的结果，这样就可以很容易地解决前面提到的问题，并且只需要编写一个Dockerfile文件。这里值得注意的是：需要确保Docker的版本在17.05及以上。下面我们来说说具体操作。&lt;/p&gt;
 &lt;p&gt;在Dockerfile里可以使用as来为某一阶段取一个别名”build-env”：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;FROM golang:1.11.2-alpine3.8 AS build-env&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后从上一阶段的镜像中复制文件，也可以复制任意镜像中的文件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;COPY –from=build-env /go/bin/hello /usr/bin/hello &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;看一个简单的例子：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;FROM golang:1.11.4-alpine3.8 AS build-env
 
ENV GO111MODULE=off
ENV GO15VENDOREXPERIMENT=1
ENV GITPATH=github.com/lattecake/hello
RUN mkdir -p /go/src/${GITPATH}
COPY ./ /go/src/${GITPATH}
RUN cd /go/src/${GITPATH} &amp;amp;&amp;amp; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -v
 
FROM alpine:latest
ENV apk –no-cache add ca-certificates
COPY --from=build-env /go/bin/hello /root/hello
WORKDIR /root
CMD [&amp;quot;/root/hello&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;执行docker build -t –rm hello3 .后再执行docker images ，然后我们来看镜像的大小：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://segmentfault.com/img/remote/1460000019015354" title=""&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;多阶构建给我们带来很多便利，最大的优势是在保证运行镜像足够小的情况下还减轻了Dockerfile的维护负担，因此我们极力推荐使用多阶构建来将你的代码打包成Docker 镜像。&lt;/p&gt;
 &lt;p&gt;作者：王聪&lt;/p&gt;
 &lt;p&gt;内容来源：  &lt;a href="https://mp.weixin.qq.com/s?__biz=MzAwNTAyNDU2OA==&amp;mid=2647993392&amp;idx=1&amp;sn=7ab3c8a6e8bd00beceef8b7d8d507718&amp;chksm=83037267b474fb71e43ae57b0a8c736f9ea9594cc187760e71d2c5edb94ad90b0675cb11dd9c&amp;token=774714541&amp;lang=zh_CN#rd" rel="nofollow noreferrer"&gt;宜信技术学院&lt;/a&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>容器 dockerfile docker</category>
      <guid isPermaLink="true">https://itindex.net/detail/59523-%E7%A8%8B%E5%BA%8F%E5%91%98-%E7%AC%94%E8%AE%B0-dockerfile</guid>
      <pubDate>Sun, 28 Apr 2019 15:35:09 CST</pubDate>
    </item>
    <item>
      <title>Gopher 2019 Go并发编程的分享</title>
      <link>https://itindex.net/detail/59521-gopher-go-%E5%B9%B6%E5%8F%91</link>
      <description>&lt;p&gt;昨天参加了 Gopher China 2019 大会，分享了《Go并发编程实践》的主题，在这一篇博客中总结一下。&lt;/p&gt;
 &lt;p&gt;从春节前开始我就有意识的收集和整理Go并发编程的相关知识，以前也有相关的博文进行过分享，本来是想以电子书的方式进行分享，正好Asta Xie邀请在Gopher China 2019上做一个分享，就毫不犹豫的答应了。&lt;/p&gt;
 &lt;p&gt;谈起Go并发编程的分享，每年各地的Go相关的大会都会有，基本上都会针对某一个专题进行介绍，或者针对Go并发编程的哲学进行分享。所以我给自己定了两个方向: 一个是结合业务讲一些Go并发编程在企业实际开发中的实践，另外一个是对Go并发编程做一个全面的总结。&lt;/p&gt;
 &lt;p&gt;我选择了后一个，也是最难的一个。一是内容非常多，而且不结合业务，只讨论编程语言相关的知识，因为涉及到细节，多少会有些枯燥，尤其是针对主要关注于业务同学，但是对于热心于Go编程语言的开发者来说，能有人对Go并发进行一个深入而全面的总结，对于自己回顾和整理Go并发编程的技能，多多少少会有所帮助的，所以我愿意进行一次尝试。&lt;/p&gt;
 &lt;p&gt;最初开始整理了很多的知识点，内容太多了，我忍痛删去了Go并发编程设计模式一节，第一版120多页的ppt发给Asta之后，Asta感觉还是太多，又忍痛删去了分布式并发原语这一节，最后整理了81页的PPT。我是实在不想删减了，再删减，就变成了《Go标准sync库在实践中的几个坑》，就偏离了我最初的分享的目的：全面总结Go并发编程知识，而退化成第一个选择：找一个直接点结合业务进行分享。&lt;/p&gt;
 &lt;p&gt;80页的PPT想在45分钟内顺利的讲完，并且不讲业务只讲Go语言的特性，非常的困难，我预想到会拖堂，实际也完美做到了，所以后面的介绍比较的匆忙了，这些都在预想的流程之内，也是希望大家拿到ppt之后有个回顾，将来遇到并发的问题至少能在ppt找到答案，而不是像听评书一样听过去了就听过去了。当然如果你不想看冗长的ppt和代码的话，这篇文章或许能对你的理解能够更顺畅一些，也希望对未能参加会议的朋友能有所帮助。&lt;/p&gt;
 &lt;p&gt;我也会听取读者和Gopher的意见，来决定是否把删掉了章节补上，以PPT或者电子书的方式提供出来。&lt;/p&gt;
 &lt;a&gt;&lt;/a&gt;
 &lt;h2&gt;内容划分&lt;/h2&gt;
 &lt;p&gt;Go并发编程的知识点非常多，最基本的goroutine因为非常简单，就不介绍了。大致把知识点划分成下面的五部分。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://colobu.com/go-concurrency-in-action.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;基本同步原语&lt;/h2&gt;
 &lt;h3&gt;Mutex&lt;/h3&gt;
 &lt;p&gt;最常用的sync包下的同步原语之一。&lt;/p&gt;
 &lt;p&gt;自2008年开始，经过了几次大的修改，加入了公平性和性能综合的考量、饥饿的处理，今年又进行了内敛的优化，对功能和性能都有了很好的提升。&lt;/p&gt;
 &lt;p&gt;内部结构使用  &lt;code&gt;state&lt;/code&gt;标记是否加锁、唤醒、饥饿等状态，使用高位来记录等待者的数量。&lt;/p&gt;
 &lt;p&gt;虽然  &lt;code&gt;state&lt;/code&gt;是unexported的，但是你可以通过  &lt;code&gt;unsafe&lt;/code&gt;包hack方式读取这些状态。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;Unlock未加锁或者已解锁的Mutex会panic&lt;/li&gt;
  &lt;li&gt;Mutex不会比较当前请求的goroutine是否已经持有这个锁，所以可以一个goorutine Lock,另一个goroutine Unlock,但是慎用，避免死锁&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;非重入锁&lt;/strong&gt;,  Java程序员转Go容易犯的错，会导致死锁。 如果想重入，使用扩展的同步原语。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;RWMutex&lt;/h3&gt;
 &lt;p&gt;读写锁对于读进行了优化，适合写少读多的状态，对并发的读很适合。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;如果有goroutine持有了RWMutex,那么只可能被一组reader持有，或者被一个writer持有。&lt;/li&gt;
  &lt;li&gt;如果已经有一组reader持有了读写锁，这个时候如果writer调用Lock,它会被阻塞。接着如果有reader调用RLock, 等前面那组readerUnlock后， writer优先获取锁&lt;/li&gt;
  &lt;li&gt;不要在递归调用读锁。因为上一条的规定，递归调用锁容易导致死锁&lt;/li&gt;
  &lt;li&gt;可以将读锁返回成一个Locker的接口&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;Cond&lt;/h3&gt;
 &lt;p&gt;类似Monitor，提供了通知的机制，可以  &lt;code&gt;Broadcast&lt;/code&gt;通知所有  &lt;code&gt;Wait&lt;/code&gt;的goroutine,也可以  &lt;code&gt;Signal&lt;/code&gt;通知某一个  &lt;code&gt;Wait&lt;/code&gt;的goroutine。&lt;/p&gt;
 &lt;p&gt;Cond初始化的时候要传入一个Locker接口的实现，这个Locker可以用来保护条件变量。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;Broadcast&lt;/code&gt;和  &lt;code&gt;Signal&lt;/code&gt;不需要加锁调用，但是调用  &lt;code&gt;Wait&lt;/code&gt;的时候需要加锁。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;Wait&lt;/code&gt;执行中有解锁重加锁的过程，在这个期间对临界区是没有保护的。&lt;/p&gt;
 &lt;p&gt;一定要使用  &lt;code&gt;for&lt;/code&gt;循环来检查条件是否满足，因为随时都可以触发通知。&lt;/p&gt;
 &lt;h3&gt;Waitgroup&lt;/h3&gt;
 &lt;p&gt;也是最常用的sync包下的同步原语之一。&lt;/p&gt;
 &lt;p&gt;内部通过一个计数器来记录waiter。&lt;/p&gt;
 &lt;p&gt;在  &lt;code&gt;Wait&lt;/code&gt;之前可以设置这个计数器的数量。等这个计数器为0的时候，所有等待的goroutine都都会解除等待，继续执行。&lt;/p&gt;
 &lt;p&gt;Add方法可以增加计数，也可以传入负值减少技术，但是如果计数器小于0的情况下会panic。&lt;/p&gt;
 &lt;p&gt;Done方法是利用-1实现的，因此Done的次数如果多于计数器，会panic。  &lt;br /&gt;Wait调用多次没问题，只要计数器为0，它就不会阻塞。&lt;/p&gt;
 &lt;p&gt;并发 Add和Wait会panic。&lt;/p&gt;
 &lt;p&gt;前一个Wait还没有完成就Add也会panic。&lt;/p&gt;
 &lt;p&gt;所以Waitgroup是可以   &lt;strong&gt;重用&lt;/strong&gt;的，但是一定等前一个Wait完成后再重用。&lt;/p&gt;
 &lt;h3&gt;Once&lt;/h3&gt;
 &lt;p&gt;用来初始化一次，比如实现单例，单元测试时环境的准备。&lt;/p&gt;
 &lt;p&gt;不要在传给Do的函数中调用这个Once，否则会死锁。&lt;/p&gt;
 &lt;p&gt;即使传入的这个函数会panic，Once也认为它已经初始化了。&lt;/p&gt;
 &lt;p&gt;Go单例的实现：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;常量&lt;/li&gt;
  &lt;li&gt;package 变量 (eager)&lt;/li&gt;
  &lt;li&gt;init函数 (eager)&lt;/li&gt;
  &lt;li&gt;GetInstance() (lazy)&lt;/li&gt;
  &lt;li&gt;通过sync.Once或者类似实现&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;A XXX must not be copied after first use.&lt;/h3&gt;
 &lt;p&gt;看上面的同步原语的 godoc，都有这么一句话。对象使用后就不能被复制了。&lt;/p&gt;
 &lt;p&gt;这是因为使用后这些对象都是有状态的，复制过去也会把状态复制过去，比如已加锁的状态，这不是我们期望的。&lt;/p&gt;
 &lt;p&gt;可以通过  &lt;code&gt;go vet&lt;/code&gt;工具检查。&lt;/p&gt;
 &lt;p&gt;如果你定义的struct也想有这个功能，可以使用  &lt;code&gt;noCopy&lt;/code&gt;这种经济的方式，定义Locker接口，让vet工具也能检查。&lt;/p&gt;
 &lt;p&gt;简单的复制是容易看出来的，很多隐藏的复制检查可以通过工具。&lt;/p&gt;
 &lt;h3&gt;Pool&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;临时对象池&lt;/li&gt;
  &lt;li&gt;可能在任何时候任意的对象都可能被移除&lt;/li&gt;
  &lt;li&gt;可以安全地并发访问&lt;/li&gt;
  &lt;li&gt;装箱/拆箱&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;tcp、数据库连接池的话不要使用它，使用专门的池。&lt;/p&gt;
 &lt;p&gt;标准库中有的池的实现使用它，有的需要永久持有的对象不使用它，而是使用链表，比如rpc。&lt;/p&gt;
 &lt;p&gt;用它做buffer池要注意，避免内存泄漏。Pool的官方例子和标准库fmt、json中都有这个坑。标准库中已经修复了。&lt;/p&gt;
 &lt;h3&gt;Map&lt;/h3&gt;
 &lt;p&gt;使用空间换时间的方式，提供下面两个场景下的性能：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;设置一次，多次读，比如cache&lt;/li&gt;
  &lt;li&gt;多个goroutine并发的读、写、更新不同的key&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;有以下的考量：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;装箱/拆箱&lt;/li&gt;
  &lt;li&gt;Range进行遍历,可能会加锁&lt;/li&gt;
  &lt;li&gt;没有Len方法，并且也不会添加&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;扩展同步原语&lt;/h2&gt;
 &lt;p&gt;对基本同步原语的补充，适用于额外的场景，由Go扩展包(试验包)和第三方提供。&lt;/p&gt;
 &lt;h3&gt;ReentrantLock&lt;/h3&gt;
 &lt;p&gt;标准库sync下的Mutex是不能重入的，如果想实现重入的话，可以利用:&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;goid:用来标记谁持有了当前锁，重入了几次&lt;/li&gt;
  &lt;li&gt;全局id:或者你自己维护一个全局id,但是实现的结构不再满足Locker接口&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;可重入锁也叫做递归锁，但是叫可重入锁更准确些，因为可重入可不只递归这么一种情况。&lt;/p&gt;
 &lt;h3&gt;Semaphore&lt;/h3&gt;
 &lt;p&gt;Dijkstra提出并发访问通用资源的并发原语，使用PV原语提供对临界区的保护。&lt;/p&gt;
 &lt;p&gt;二进制(取值0,1)的semaphore提供了锁的功能。  &lt;br /&gt;计数器semaphore提供了对一组资源的保护。&lt;/p&gt;
 &lt;p&gt;包   &lt;code&gt;golang.org/x/sync/semaphore&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;标准库内部的semaphore提供了休眠/唤醒的功能，用来实现基本同步原语的阻塞。&lt;/p&gt;
 &lt;h3&gt;SingleFlight&lt;/h3&gt;
 &lt;p&gt;并发的访问同一组资源的时候，只允许一个请求进行，这个请求把结果告诉其它等待者，避免雪崩的现象。&lt;/p&gt;
 &lt;p&gt;比如cache 失效的时候，只允许一个goroutine从数据库中捞数据回种，避免雪崩对数据库的影响。&lt;/p&gt;
 &lt;p&gt;扩展库中提供。&lt;/p&gt;
 &lt;h3&gt;ErrGroup&lt;/h3&gt;
 &lt;p&gt;应用于 half sync/half async的场景(这个设计模式以后有机会再介绍)。&lt;/p&gt;
 &lt;p&gt;有一组异步的任务需要处理，利用这个原语可以等待所有的异步任务完成，并获取第一个错误。&lt;/p&gt;
 &lt;p&gt;如果想得到所有的错误，利用额外的map变量进行记录。&lt;/p&gt;
 &lt;p&gt;使用Context可以实现遇到第一个错误就返回。&lt;/p&gt;
 &lt;p&gt;扩展包中提供。&lt;/p&gt;
 &lt;p&gt;bilibili扩展了这个原语，提供了限定并发数量的功能。&lt;/p&gt;
 &lt;h3&gt;SpinLock&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;自旋锁&lt;/li&gt;
  &lt;li&gt;有时候很高效,因为当前CPU中运行的goroutine更有机会获取到锁&lt;/li&gt;
  &lt;li&gt;不公平&lt;/li&gt;
  &lt;li&gt;需要处理器忙等待&lt;/li&gt;
  &lt;li&gt;应用于竞争不是很激烈的状态&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;fslock&lt;/h3&gt;
 &lt;p&gt;文件锁， 可以控制多个进程之间的同步。&lt;/p&gt;
 &lt;h3&gt;concurrent-map&lt;/h3&gt;
 &lt;p&gt;类似Java中的ConcurrentMap的设计思想，将key划分成一定数量的shard,每个shard一个锁，减少锁的竞争。&lt;/p&gt;
 &lt;p&gt;相对于sync.Map，可以应用写/删除/添加更频繁的场景。&lt;/p&gt;
 &lt;h2&gt;原子操作&lt;/h2&gt;
 &lt;p&gt;保证操作是原子的。&lt;/p&gt;
 &lt;h3&gt;操作的数据&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;int32&lt;/li&gt;
  &lt;li&gt;int64&lt;/li&gt;
  &lt;li&gt;uint32&lt;/li&gt;
  &lt;li&gt;uint64&lt;/li&gt;
  &lt;li&gt;uintptr&lt;/li&gt;
  &lt;li&gt;unsafe.Pointer&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;操作方法&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;AddXXX (整数类型)&lt;/li&gt;
  &lt;li&gt;CompareAndSwapXXX：cas&lt;/li&gt;
  &lt;li&gt;LoadXXX：读取&lt;/li&gt;
  &lt;li&gt;StoreXXX：存储&lt;/li&gt;
  &lt;li&gt;SwapXXX：交换&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;Subtract&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;有符号的类型，可以使用Add负数&lt;/li&gt;
  &lt;li&gt;无符号的类型，可以使用AddUint32(&amp;amp;x, ^uint32(c-1)),AddUint64(&amp;amp;x, ^uint64(c-1))&lt;/li&gt;
  &lt;li&gt;无符号类型减一， AddUint32(&amp;amp;x, ^uint32(0))， AddUint64(&amp;amp;x, ^uint64(0))&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;Value&lt;/h3&gt;
 &lt;p&gt;一个通用的对象，可以很方便的对struct等类型进行原子存储和加载。&lt;/p&gt;
 &lt;p&gt;由于不同的架构下对原子操作的支持是不一样的，有些架构师是不支持的。&lt;/p&gt;
 &lt;h2&gt;Channel&lt;/h2&gt;
 &lt;p&gt;channel容易再犯错，甚至于比使用传统sync包下的同步原语的错误率还要高，牢记异常的情况：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://colobu.com/channel-tricks.png"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;close已经close的channel也会panic。&lt;/p&gt;
 &lt;p&gt;利用channel可以实现锁，并且可以实现TryWithTimeout方法，因为利用Go的内存模型可以保障，但是正常情况channel和mutex有不同的应用场景。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;Channel&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;传递数据的owner&lt;/li&gt;
  &lt;li&gt;分发任务&lt;/li&gt;
  &lt;li&gt;交流异步结果&lt;/li&gt;
  &lt;li&gt;任务编排&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;Mutex&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;cache&lt;/li&gt;
  &lt;li&gt;状态&lt;/li&gt;
  &lt;li&gt;临界区&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;Channel的一些应用模式可以参考另一篇文章:   &lt;a href="https://colobu.com/2018/03/26/channel-patterns/" rel="external" target="_blank"&gt;Go Channel 应用模式&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;Go内存模型&lt;/h2&gt;
 &lt;p&gt;内存模型描述了线程(goroutine)通过内存的交互，以及对数据的共享使用。  &lt;br /&gt;Java语言是第一个详细描述其内存模型的流行的编程语言。&lt;/p&gt;
 &lt;p&gt;它并不是描述内存是如何分配的，而是定义了：  &lt;br /&gt;  &lt;strong&gt;对同一个变量，如何保证在一个goroutine对此变量读的时候，能观察到其它goroutine对此变量的写。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;描述这种顺序关系的术语叫做  &lt;strong&gt;happen before&lt;/strong&gt;。&lt;/p&gt;
 &lt;h3&gt;单个goroutine内&lt;/h3&gt;
 &lt;p&gt;执行顺序和代码编写的顺序是一致的（有reorder，也不影响理解，可以按照编写顺序进行分析）&lt;/p&gt;
 &lt;h3&gt;包级别的init函数&lt;/h3&gt;
 &lt;p&gt;在单个goroutine中执行  &lt;br /&gt;最底层引入的包的init先执行。之后再是main函数。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;提供问题： 同一个包下可以定义多个init函数吗？&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;go语句&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;goroutine的创建happens before所有此goroutine中的操作&lt;/li&gt;
  &lt;li&gt;goroutine的销毁happens after所有此goroutine中的操作&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;channel&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;第n个send一定happen before第n个receive完成, 不管是buffered channel还是unbuffered channel&lt;/li&gt;
  &lt;li&gt;对于capacity 为m的channel,第n个receive一定happen before第 (n+m) send完成&lt;/li&gt;
  &lt;li&gt;m=0 unbuffered。第n个receive一定happen before第n个send完成&lt;/li&gt;
  &lt;li&gt;channel的close一定happen before receive端得到通知，得到通知意味着receive收到一个因为channel close而收到的零值&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;注意 send/send completes，receive/receive completes的区别&lt;/p&gt;
 &lt;h3&gt;Mutex/RWMutex&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;对于Mutex/RWMutx m, 第n个成功的 m.Unlock 一定happen before 第 n+1 m.Lock方法调用的返回&lt;/li&gt;
  &lt;li&gt;对于RWMutex rw，如果它的第n个rw.Lock已返回，那么它的第n个成功的rw.Unlock的方法调用一定happen before 任何一个 rw.RLock方法调用的返回（它们 happen after 第n个rw.Lock方法调用返回）&lt;/li&gt;
  &lt;li&gt;对于RWMutex rw,如果它的第n个rw.RLock已返回，接着第m (m &amp;lt; n)个rm.RUnlock方法调用一定happen before 任意的 rw.Lock(它们happen after 第n个rw.RLock方法调用返回之后) &lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;Waitgroup&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;对于 Waitgroup b, 对于其计数器不是0的时候，假如此时刻之后有一组wg.Add(n),并且我们确信只有最后一组方法调用使其计数器最后复原为0，那么这组wg.Add 方法调用一定happen before 这一时刻之后发生的wg.Wait&lt;/li&gt;
  &lt;li&gt;wg.Done()也是wg.Add(-1)&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;Once&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;once.Do方法的执行一定happen before 任何一个once.Do方法的返回&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;Atomic&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;没有官方的保证&lt;/li&gt;
  &lt;li&gt;建议是不要依赖atomic保证内存的顺序&lt;/li&gt;
  &lt;li&gt;#5045 历史悠久的讨论，还没close&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>Go</category>
      <guid isPermaLink="true">https://itindex.net/detail/59521-gopher-go-%E5%B9%B6%E5%8F%91</guid>
      <pubDate>Sun, 28 Apr 2019 06:06:19 CST</pubDate>
    </item>
    <item>
      <title>Python 编程速成</title>
      <link>https://itindex.net/detail/59454-python-%E7%BC%96%E7%A8%8B-%E9%80%9F%E6%88%90</link>
      <description>&lt;blockquote&gt;翻译：疯狂的技术宅  &lt;br /&gt;  &lt;a href="https://likegeeks.com/python-programming-basics/" rel="nofollow noreferrer"&gt;https://likegeeks.com/python-...&lt;/a&gt;
&lt;/blockquote&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;本文首发微信公众号：前端先锋  &lt;br /&gt;欢迎关注，每天都给你推送新鲜的前端技术文章&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;Python是一种非常流行的脚本语言，而且功能非常强大，几乎可以做任何事情，比如爬虫、网络工具、科学计算、树莓派、Web开发、游戏等各方面都可以派上用场。同时无论在哪种平台上，都可以用 Python 进行系统编程。&lt;/p&gt;
 &lt;p&gt;机器学习可以用一些 Python 库来实现，比如人工智能常用的  &lt;a href="https://likegeeks.com/tensorflow-examples/" rel="nofollow noreferrer"&gt;TensorFlow&lt;/a&gt;。也可以用像 NLTK 这样的 Python 库进行自然语言处理（NLP）。&lt;/p&gt;
 &lt;p&gt;本文讨论基本的 Python 编程，后续会写一些 Python 编程的实际案例。&lt;/p&gt;
 &lt;h2&gt;操作字符串&lt;/h2&gt;
 &lt;p&gt;Python 中的字符串是不可变的，所以不能直接修改。对字符串内容的任何更改都需要产生新的副本。  &lt;br /&gt;在 Python 中处理字符串非常简单。&lt;/p&gt;
 &lt;h3&gt;拼接字符串&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;welcome &amp;quot; + &amp;quot;to Python&amp;quot; 
print (str) &lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这是字符串连接，你也可以对字符串做乘法操作：&lt;/p&gt;
 &lt;h3&gt;重复字符串&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;Python&amp;quot; * 2 
print (str)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;与非字符串拼接&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;str()&lt;/code&gt; 函数将非字符串的值转换为字符串，然后再连接，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;This is test number &amp;quot; + str(15) 
print (str)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;搜索子字符串&lt;/h3&gt;
 &lt;p&gt;可以使用   &lt;code&gt;find()&lt;/code&gt; 方法搜索子字符串，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;welcome to Python&amp;quot; 
print(str.find(&amp;quot;Python&amp;quot;))&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果找到了字符串&amp;quot;Python&amp;quot;，则   &lt;code&gt;find&lt;/code&gt; 方法会返回第一次出现这个字符串的位置。&lt;/p&gt;
 &lt;p&gt;如果没有找到，则返回 -1。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;find&lt;/code&gt; 函数默认从第一个字符开始搜索，也可以从第n个字符开始，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;welcome to Python&amp;quot; 
print(str.find(&amp;quot;Python&amp;quot;,12))&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;因为我们从第12个字符开始，所以找不到 Python 这个单词，所以它会返回 -1。&lt;/p&gt;
 &lt;h3&gt;获取子字符串&lt;/h3&gt;
 &lt;p&gt;所以我们得到了我们要搜索的字符串的索引，现在我们要打印匹配的字符串。&lt;/p&gt;
 &lt;p&gt;你可以按索输出印字符串，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;first second third&amp;quot; 
print(str[:2]) 
print(str[2:]) 
print(str[3:5]) 
print(str[-1])&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在第 2 行的代码会打印第一个和第二个字符，而第 3 行会从第二个字符开始打印到结束。要注意代码中结冒号的位置。字符串从 0 开始计数。  &lt;br /&gt;如果使用负数，则会从最后开始计数。第 5 行代码会打印最后一个字符。&lt;/p&gt;
 &lt;h3&gt;替换字符串&lt;/h3&gt;
 &lt;p&gt;你可以用如下方法替换字符串：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;This website is about programming&amp;quot; 
str2 = str.replace(&amp;quot;This&amp;quot;, &amp;quot;That&amp;quot;) 
print(str2)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果你想替换的字符串多次出现，但是只想替换第一次出现的，可以指定位置：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;This website is about programming I like this website&amp;quot; 
str2 = str.replace(&amp;quot;website&amp;quot;, &amp;quot;page&amp;quot;,1) 
print(str2)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;第一个词只被替换了。&lt;/p&gt;
 &lt;h3&gt;去除字符串两端的空格&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;strip&lt;/code&gt; 方法去掉字符串两端的空格，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str = &amp;quot;   This website is about programming    &amp;quot;
print(str.strip())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;你可以用  &lt;code&gt;rstrip&lt;/code&gt;仅去除最右的空格，或者用   &lt;code&gt;lstrip&lt;/code&gt; 去除最左边的空格。&lt;/p&gt;
 &lt;h3&gt;改变字符大小写&lt;/h3&gt;
 &lt;p&gt;在某些情况下你可能需要改变字符的大小写。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str=&amp;quot;Welcome to likegeeks&amp;quot; 
print(str.upper()) 
print(str.lower())&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;将字符串转换为数字&lt;/h3&gt;
 &lt;p&gt;前面学到了用   &lt;code&gt;str()&lt;/code&gt;函数将数字转为字符串，但这不是 Python 中唯一的转换函数，另外还有   &lt;code&gt;int()&lt;/code&gt; 、  &lt;code&gt;float()&lt;/code&gt;、  &lt;code&gt;long()&lt;/code&gt;和其他强制转换函数。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;int()&lt;/code&gt; 可以把输入的字符串转为整数，  &lt;code&gt;float()&lt;/code&gt; 函数将字符串转为float。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str=&amp;quot;10&amp;quot; 
str2=&amp;quot;20&amp;quot; 
print(str+str2) 
print(int(str)+int(str2))&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;第 3 行只是连接两个字符串，而第 4 行把两个值相加并输出结果。&lt;/p&gt;
 &lt;h3&gt;字符串计数&lt;/h3&gt;
 &lt;p&gt;可以用  &lt;code&gt;min()&lt;/code&gt;找到字符串中 ASCII 值最小的字符，  &lt;code&gt;max()&lt;/code&gt;找到最大的字符，用  &lt;code&gt;len()&lt;/code&gt;函数得到字符的总长度。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str=&amp;quot;welcome to Python&amp;quot; 
print(min(str)) 
print(max(str)) 
print(len(str))&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;迭代字符串&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;for&lt;/code&gt; 迭代字符串并单独操作每个字符，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str=&amp;quot;welcome to likegeeks website&amp;quot; 
for i in range(len(str)):
    print(str[i])&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;其中   &lt;code&gt;len()&lt;/code&gt; 函数用来得到字符串的长度。&lt;/p&gt;
 &lt;h3&gt;字符串编码&lt;/h3&gt;
 &lt;p&gt;如果你使用的是Python 3，默认情况下所有字符都是 Unicode 字符集编码，但是如果用的是Python 2，可能需要对字符串进行编码，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;str=&amp;quot;welcome to Python&amp;quot; 
str.encode(&amp;apos;utf-8&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;操作数字&lt;/h2&gt;
 &lt;p&gt;在 Python 中定义数字变量的方式如下：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;a=15&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;可以定义整数，也可以定义浮点数。  &lt;br /&gt;浮点数可以用   &lt;code&gt;int()&lt;/code&gt;函数进行舍入，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;a=15.5 
print(int(a))&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;舍入数字&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;round()&lt;/code&gt; 函数对数字进行舍入：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;a=15.5652645
print(round(a,2))&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;只需指定需要舍入到小数点后几位。&lt;/p&gt;
 &lt;h3&gt;自定义精度的数字&lt;/h3&gt;
 &lt;p&gt;很多情况下会用到自定义精度的浮点数。&lt;/p&gt;
 &lt;p&gt;可以处理用户自定义精度数的 decimal 模块。&lt;/p&gt;
 &lt;p&gt;这样导入模块并使用：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;from decimal import *

a=Decimal(5.5)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;生成随机数&lt;/h3&gt;
 &lt;p&gt;Python 中的 random 模块提供了生成随机数的函数。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import random 

print(random.random())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;生成的随机数介于 0.0 和 1.0 之间。&lt;/p&gt;
 &lt;p&gt;可以从自己定义的范围中生成一个随机数，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import random

numbers=[1,2,3,4,5,6,7]
print(random.choices(numbers))&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;操作日期和时间&lt;/h2&gt;
 &lt;p&gt;你可以从日期中提取所需的值，如下所示。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import datetime 

cur_date = datetime.datetime.now() 
print(cur_date) 
print(cur_date.year) 
print(cur_date.day) 
print(cur_date.weekday()) 
print(cur_date.month) 
print(cur_date.time())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;可以得到两个时间或日期之间的差，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import datetime 

time1 = datetime.datetime.now() 
time2 = datetime.datetime.now() 
timediff = time2 - time1 
print(timediff.microseconds())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;上例中的   &lt;code&gt;timediff&lt;/code&gt; 变量是   &lt;code&gt;timedelta&lt;/code&gt; 类型的对象，你也可以自己创建这种对象：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;time1 = datetime.datetime.now() 
time2 = datetime.timedelta(days=3) 
time3=time1+time2 
print(time3.date())&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;格式化日期和时间&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;strftime()&lt;/code&gt; 格式化日期或时间。&lt;/p&gt;
 &lt;p&gt;下表指定了一些常用的格式选项：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;%y 两位数的年份表示（00-99）&lt;/li&gt;
  &lt;li&gt;%Y 四位数的年份表示（000-9999）&lt;/li&gt;
  &lt;li&gt;%m 月份（01-12）&lt;/li&gt;
  &lt;li&gt;%d 月内中的一天（0-31）&lt;/li&gt;
  &lt;li&gt;%H 24小时制小时数（0-23）&lt;/li&gt;
  &lt;li&gt;%I 12小时制小时数（01-12）&lt;/li&gt;
  &lt;li&gt;%M 分钟数（00=59）&lt;/li&gt;
  &lt;li&gt;%S 秒（00-59）&lt;/li&gt;
  &lt;li&gt;%a 本地简化星期名称&lt;/li&gt;
  &lt;li&gt;%A 本地完整星期名称&lt;/li&gt;
  &lt;li&gt;%b 本地简化的月份名称&lt;/li&gt;
  &lt;li&gt;%B 本地完整的月份名称&lt;/li&gt;
  &lt;li&gt;%c 本地相应的日期表示和时间表示&lt;/li&gt;
  &lt;li&gt;%j 年内的一天（001-366）&lt;/li&gt;
  &lt;li&gt;%p 本地A.M.或P.M.的等价符&lt;/li&gt;
  &lt;li&gt;%U 一年中的星期数（00-53）星期天为星期的开始&lt;/li&gt;
  &lt;li&gt;%w 星期（0-6），星期天为星期的开始&lt;/li&gt;
  &lt;li&gt;%W 一年中的星期数（00-53）星期一为星期的开始&lt;/li&gt;
  &lt;li&gt;%x 本地相应的日期表示&lt;/li&gt;
  &lt;li&gt;%X 本地相应的时间表示&lt;/li&gt;
  &lt;li&gt;%Z 当前时区的名称&lt;/li&gt;
  &lt;li&gt;%% %号本身&lt;/li&gt;
&lt;/ul&gt;
 &lt;pre&gt;  &lt;code&gt;import datetime 

date1 = datetime.datetime.now() 
print(date1.strftime(&amp;apos;%d. %B %Y %I:%M%p&amp;apos;))&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;从字符串创建日期&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;strptime()&lt;/code&gt;函数从字符串创建日期，如下所示：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;date1=datetime.datetime.strptime(“2015-11-21”, “%Y-%m-%d”)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;也可以像这样创建：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;date1= datetime.datetime(year=2015, month=11, day=21)&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;处理文件系统&lt;/h2&gt;
 &lt;p&gt;在 Python 中处理文件非常容易，不管你信不信，在所欲语言中是是最简单的。当然你也可以说 Python 是一种做什么事情都最简单的语言。&lt;/p&gt;
 &lt;h3&gt;复制文件&lt;/h3&gt;
 &lt;p&gt;  &lt;code&gt;shutil&lt;/code&gt; 模块中包含用于复制文件的功能。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import shutil 
copied_path = shutil.copy(&amp;apos;my_file.txt&amp;apos;, &amp;apos;copied_file.txt&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果 my_file.txt 是一个软连接的话，那么上面的代码将会把 copied_file.txt 创建为独立的文件。&lt;/p&gt;
 &lt;p&gt;你也可以创建一个软链接的副本，如下所示：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;copied_path = shutil.copy(&amp;apos;my_file.txt&amp;apos;, &amp;apos;copied_file.txt&amp;apos;,follow_symlinks=False)&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;移动文件&lt;/h3&gt;
 &lt;p&gt;你可以像这样移动一个文件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import shutil 
shutil.move(&amp;apos;file1.txt&amp;apos;, &amp;apos;file3.txt&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;也可以使用   &lt;code&gt;os&lt;/code&gt; 模块中的   &lt;code&gt;rename&lt;/code&gt; 函数重命名文件，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import os 
os.rename(&amp;apos;file1.txt&amp;apos;, &amp;apos;file3.txt&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;读写文本文件&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;open&lt;/code&gt; 函数打开文件，然后再用   &lt;code&gt;read&lt;/code&gt; 或  &lt;code&gt;write&lt;/code&gt; 方法进行读写。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;fd = open(&amp;apos;file1.txt&amp;apos;) 
content = fd.read() 
print(content)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;首先，使用   &lt;code&gt;open&lt;/code&gt; 函数打开文件并读取，然后我再用   &lt;code&gt;read&lt;/code&gt; 函数读取文件内容，最后，将得到的内容放入变量   &lt;code&gt;content&lt;/code&gt; 中。&lt;/p&gt;
 &lt;p&gt;你可以指定    &lt;code&gt;read()&lt;/code&gt;（）函数读取的字节数：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;fd.read(20)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;如果文件不是太大的话，你可以将整个内容读入一个列表，然后再遍历列表打印输出。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;content = fd.readlines() 
print(content[0])&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;可以通过指定打开的模式来写入文件。有两种写入模式，即写入模式和追加模式。&lt;/p&gt;
 &lt;p&gt;下面是写入模式，会覆盖掉文件中的旧内容。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;fd = open(&amp;apos;file1.txt&amp;apos;,&amp;apos;w&amp;apos;) 
content = fd.write(&amp;apos;YOUR CONTENT GOES HERE&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;下面是附加模式：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;fd = open(&amp;apos;file1.txt&amp;apos;,&amp;apos;a&amp;apos;) 
content = fd.write(&amp;apos;YOUR CONTENT GOES HERE&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;创建目录&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;os&lt;/code&gt;模块中的   &lt;code&gt;mkdir&lt;/code&gt; 函数创建一个新目录，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import os 

os.mkdir(&amp;apos;./NewFolder)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果目录已经存在将会引发错误。不过不用担心，在以后的文章中会讨论异常处理，可以帮你避免此类错误。&lt;/p&gt;
 &lt;h3&gt;获取访问和修改以及创建时间&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;getmtime()&lt;/code&gt;、  &lt;code&gt; getatime()&lt;/code&gt; 和   &lt;code&gt;getctime()&lt;/code&gt; 分别获取修改时间、访问时间和创建时间。&lt;/p&gt;
 &lt;p&gt;返回的时间格式为 Unix 时间戳，我们可以把它转换为人类可读的格式，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import os 
import datetime 

tim=os.path.getctime(&amp;apos;./file1.txt&amp;apos;) 
print(datetime.datetime.fromtimestamp(tim))&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;迭代文件&lt;/h3&gt;
 &lt;p&gt;你可以用   &lt;code&gt;os&lt;/code&gt; 模块中的   &lt;code&gt;listdir()&lt;/code&gt;  函数来获取文件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import os

files = os.listdir(&amp;apos;.&amp;apos;)
print(files)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;此外，你可以用   &lt;code&gt;glob&lt;/code&gt; 模块执行相同的操作：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import glob 
files = glob.glob(&amp;apos;*&amp;apos;) 
print(files)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;你可以为   &lt;code&gt;glob()&lt;/code&gt;设定任何一种扩展名，例如设定为   &lt;code&gt;* .doc&lt;/code&gt; 获取所有word文档。&lt;/p&gt;
 &lt;h3&gt;序列化Python对象&lt;/h3&gt;
 &lt;p&gt;此过程用于将 Python 对象序列化为字节流，以便以后重用。&lt;/p&gt;
 &lt;p&gt;你可以用   &lt;code&gt;pickle&lt;/code&gt; 模块做到这一点：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import pickle 

fd = open(&amp;apos;myfile.pk &amp;apos;, &amp;apos;wb&amp;apos;) 
pickle.dump(mydata,fd)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;可以用   &lt;code&gt;load()&lt;/code&gt; 函数反序列化此数据，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import pickle 

fd = open(&amp;apos;myfile.pk &amp;apos;, &amp;apos;rb&amp;apos;) 
mydata = pickle.load(fd)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;压缩文件&lt;/h3&gt;
 &lt;p&gt;Python 标准库使你可以处理不同格式的压缩文件，如tar，zip，gzip，bzip2。&lt;/p&gt;
 &lt;p&gt;要处理   &lt;code&gt;zip&lt;/code&gt;文件，你可以使用   &lt;code&gt;zipfile&lt;/code&gt;模块：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import zipfile 

my_zip = zipfile.ZipFile(&amp;apos;zipped_file.zip&amp;apos;, mode=&amp;apos;r&amp;apos;) 
print(file.namelist())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;你可以把一个文件压缩成 zip 格式，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import zipfile 

file=zipfile.ZipFile(&amp;apos;files.zip&amp;apos;,&amp;apos;w&amp;apos;) 
file.write(&amp;apos;file1.txt&amp;apos;) 
file.close()&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;可以用   &lt;code&gt;extractall()&lt;/code&gt; 方法提取 zip 压缩包中的文件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import zipfile 

file=zipfile.ZipFile(&amp;apos;files.zip&amp;apos;,&amp;apos;r&amp;apos;) 
file.extractall() 
file.close()&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;另外还可以用附加模式将文件附加到现有的zip文件，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import zipfile 

file=zipfile.ZipFile(&amp;apos;files.zip&amp;apos;,&amp;apos;a&amp;apos;) 
file.write(&amp;apos;file2.txt&amp;apos;) 
file.close()&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在处理 gz 或 bz 文件时，可以用和上面相同的套路。不过要导入   &lt;code&gt;gzip&lt;/code&gt; 模块或   &lt;code&gt;bz2&lt;/code&gt; 模块。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import gzip 
import bz2 

gz_file=gzip.GzipFile(&amp;apos;files.gz&amp;apos;,&amp;apos;r&amp;apos;) 
bz_file=bz2.BZ2File(&amp;apos;fiels.bz2&amp;apos;,&amp;apos;r&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后用同样的方式读写。&lt;/p&gt;
 &lt;p&gt;你可以使用   &lt;strong&gt;unrar&lt;/strong&gt; 包处理rar文件。首先，安装包：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;pip install unrar&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后用同样的方式使用。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import unrar.rarfile 

m=unrar.rarfile.RarFile(&amp;apos;file.rar&amp;apos;) 
m.namelist() 
m.extractall()&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;解析CSV文件&lt;/h3&gt;
 &lt;p&gt;一个非常有用的包叫做 pandas。它可以解析 CSV 和 Excel 文件，并轻松地从中提取数据。&lt;/p&gt;
 &lt;p&gt;首先，安装包&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;pip install pandas&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后你可以在自己的代码中使用它，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import pandas 

data=pandas.read_csv(&amp;apos;file.csv)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;默认情况下，Pandas 将第一列视为每行的标签。如果列索引不是第一列，则可以通过传递   &lt;code&gt;index_col&lt;/code&gt; 参数来指定列索引。&lt;/p&gt;
 &lt;p&gt;如果文档中没有行标签，则应使用参数   &lt;code&gt;index_col = False&lt;/code&gt;。&lt;/p&gt;
 &lt;p&gt;要写入CSV文件，可以使用   &lt;code&gt;to_csv()&lt;/code&gt; 方法。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;data.to_csv(&amp;apos;file.csv)&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;解析Excel文件&lt;/h3&gt;
 &lt;p&gt;可以用 pandas 模块中的   &lt;code&gt;read_excel()&lt;/code&gt; 方法来解析excel文件。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;data = pd.read_excel(&amp;apos;file.xls&amp;apos;, sheetname=&amp;apos;Sheet1&amp;apos;)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;如果有多个工作表，可以这样加载：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;ta = pd.ExcelFile(&amp;apos;file.xls&amp;apos;)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;这样写入excel文件：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;ta.to_excel(&amp;apos;file.xls&amp;apos;, sheet=&amp;apos;Sheet1&amp;apos;)&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;网络和连接&lt;/h2&gt;
 &lt;p&gt;Python 有一个   &lt;code&gt;socket&lt;/code&gt; 类，它提供了一种通过低级 API 访问网络的方法，它支持许多网络协议。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import socket 
host = &amp;apos;192.168.1.5&amp;apos; 
port = 5050 
m_sock = socket.create_connection ((host, port))&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这段代码与 IP 地址为 192.168.1.5 的主机通过 5050 端口建立连接。&lt;/p&gt;
 &lt;p&gt;打开 socket 后，就可以发送和接收数据了。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;m_sock.sendall(b&amp;apos;Hello World&amp;apos;)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;注意，我在字符串之前使用了 b 字符，因为数据需要是字节字符串。&lt;/p&gt;
 &lt;p&gt;如果发送的数据太大，你应该通过循环分割并发送，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;msg = b&amp;apos;Longer Message Goes Here&amp;apos; 
mesglen = len(msg)
total = 0
while total &amp;lt; msglen: 
    sent = m_sock.send(msg[total:])
    total = total + sent&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;要接收数据，你需要告诉   &lt;code&gt;recv()&lt;/code&gt; 方法一次读入多少个字节。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;data_in = m_sock.recv(2000)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;这是有效的，因为你能够确定发送的消息长度小于2000个字节。&lt;/p&gt;
 &lt;p&gt;如果消息很大，则必须反复循环，直到收到所有的数据块。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;buffer = bytearray(b&amp;apos; &amp;apos; * 2000) 
m_sock.recv_into(buffer)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;在这里定义了一个空缓冲区，然后将消息写入缓冲区。&lt;/p&gt;
 &lt;h3&gt;从POP邮件服务器接收电子邮件&lt;/h3&gt;
 &lt;p&gt;  &lt;code&gt;poplib&lt;/code&gt; 模块使你可以与 POP 服务器进行通信。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import getpass,poplib 

pop_serv = poplib.POP3(&amp;apos;192.168.1.5&amp;apos;) 
pop_serv.user(&amp;quot;myuser&amp;quot;) 
pop_serv.pass_(getpass.getpass())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;code&gt;getpass&lt;/code&gt; 模块可以安全地处理密码。&lt;/p&gt;
 &lt;p&gt;如果需要安全连接，可以用 POP3_SSL 类。&lt;/p&gt;
 &lt;p&gt;要获取邮件列表和邮件计数，可以这样做：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;msg_list = pop_serv.list()        # to list the messages 
msg_count = pop_serv.msg_count()  # to get message count&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;处理完毕后，一定要记得关闭所有打开的连接。&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;pop_serv.quit()&lt;/code&gt;&lt;/p&gt;
 &lt;h3&gt;从IMAP邮件服务器接收电子邮件&lt;/h3&gt;
 &lt;p&gt;可以用   &lt;code&gt;imaplib&lt;/code&gt; 模块与 IMAP 邮件服务器通信。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import imaplib, getpass 

my_imap = imaplib.IMAP4(&amp;apos;imap.server.com&amp;apos;) 
my_imap.login(&amp;quot;myuser&amp;quot;, getpass.getpass())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果你的 IMAP 服务器上使用 SSL，则应用 IMAP4_SSL 类。&lt;/p&gt;
 &lt;p&gt;要获取电子邮件列表，需要先执行查询操作：&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;data = my_imap.search(None, &amp;apos;ALL&amp;apos;)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;然后，通过迭代   &lt;code&gt;data&lt;/code&gt; 变量中的邮件索引获取邮件内容&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;msg = my_imap.fetch(email_id, &amp;apos;(RFC822)&amp;apos;)&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;最后，不要忘记关闭连接：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;my_imap.close() 
my_imap.logout()&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;发送电子邮件&lt;/h3&gt;
 &lt;p&gt;想要通过 SMTP 协议发送电子邮件。可以用   &lt;code&gt;smtplib&lt;/code&gt; 。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import smtplib, getpass 

my_smtp = smtplib.SMTP(smtp.server.com&amp;apos;) 
my_smtp.login(&amp;quot;myuser&amp;quot;, getpass.getpass())&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果在 SMTP服务器上使用SSL，则应用 SMTP_SSL 类。&lt;/p&gt;
 &lt;p&gt;打开连接后，你可以这样发送邮件：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;from_addr = &amp;apos;me@example.com&amp;apos; 
to_addr = &amp;apos;you@example.com&amp;apos; 
msg = &amp;apos;From: me@example.com\r\nTo: you@example.com\r\n\r\nHello, this is a test message&amp;apos; 
my_smtp.sendmail(from_addr, to_addr, msg)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;抓取网页&lt;/h3&gt;
 &lt;p&gt;要与Web服务器进行通信，需要用到   &lt;code&gt;urllib.request&lt;/code&gt; 子模块。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import urllib.request 

my_web = urllib.request.urlopen(&amp;apos;https://www.google.com&amp;apos;) 
print(my_web.read())&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;提交 Web 表单&lt;/h3&gt;
 &lt;p&gt;如果你想要提交 Web 表单，应该向网页发送POST请求。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import urllib.request 
my_data = b&amp;apos;Your Data Goes Here&amp;apos; 
my_req = urllib.request.Request(&amp;apos;http://localhost&amp;apos;, data=my_data,method=&amp;apos;POST&amp;apos;) 
my_frm = urllib.request.urlopen(my_req) 
print(my_frm.status)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;另外还可以用 mechanize 或 urllib2 模块，还有很多方法可以实现这个功能。&lt;/p&gt;
 &lt;h3&gt;创建一个微型服务器&lt;/h3&gt;
 &lt;p&gt;  &lt;code&gt;socket&lt;/code&gt; 类支持侦听连接请求。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import socket 

host = &amp;apos;&amp;apos;
port = 3535 
my_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
my_server.bind((host, port)) 
my_server.listen(1)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;现在你可以接受这样的连接请求：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;addr = my_server.accept() 
print(&amp;apos;Connected ... &amp;apos;, addr) 
data = conn.recv(1024)&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;最后不要忘记在操作完成后关闭连接&lt;/p&gt;
 &lt;p&gt;  &lt;code&gt;conn.close()&lt;/code&gt;&lt;/p&gt;
 &lt;h2&gt;Python 中的线程&lt;/h2&gt;
 &lt;p&gt;并发运行任务是非常有用的，Python 有一个名为   &lt;code&gt;threading&lt;/code&gt; 的模块，它包含一个   &lt;code&gt;Thread&lt;/code&gt; 类。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import threading 

def print_message():     
    print(&amp;apos;The message got printed from a different thread&amp;apos;) 

my_thread = threading.Thread(target=print_message) 
my_thread.start()&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果函数需要很长时间才能完成，可以用   &lt;code&gt;is_alive()&lt;/code&gt;方法检查它是否仍在运行。&lt;/p&gt;
 &lt;p&gt;有时你的线程需要安全地访问全局资源。这时候就需要用到锁。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import threading

num = 1 
my_lock = threading.Lock()

def my_func():
    global num, my_lock
    my_lock.acquire()
    sum = num + 1
    print(sum)
    my_lock.release() 

my_thread = threading.Thread(target=my_func)
my_thread.start()&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;使用树莓派&lt;/h2&gt;
 &lt;p&gt;使用树莓派是一种价格便宜的单板电脑，可以在上面开发各种有趣的应用。&lt;/p&gt;
 &lt;p&gt;可以用 Python 的   &lt;strong&gt;RPi.GPIO&lt;/strong&gt; 模块使树莓派与外接的传感器通信。&lt;/p&gt;
 &lt;p&gt;首先，在你的树莓派中安装包，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;$ sudo apt-get install python-dev python-rpi.gpio&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后你就能在Python脚本中使用它了。可以在树莓派的 GPIO 总线上写输出：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import RPi.GPIO as GPIO 

GPIO.setmode(GPIO.BOARD) 
GPIO.setup(1, GPIO.OUT, initial=GPIO.LOW) 
GPIO.output(1, GPIO.HIGH)&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;通过树莓派的GPIO中读取数据&lt;/h3&gt;
 &lt;p&gt;你可以用   &lt;code&gt;RPi.GPIO&lt;/code&gt; 模块从 GPIO 接口读取数据，如下所示：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import RPi.GPIO 

RPi.GPIO.setup(1, GPIO.IN)
if RPi.GPIO.input(1):
    print(&amp;apos;Input was HIGH&amp;apos;) 
else:
    print(&amp;apos;Input was LOW&amp;apos;)&lt;/code&gt;&lt;/pre&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;以上只是 Python 的一小部分基础知识，还有很长的路需要走。&lt;/p&gt;
 &lt;p&gt;实际上，我们已经介绍了一小部分Python，还有很多内容需要介绍。将来会有更多的文章来帮助大家学习 Python 这种充满魅力的语言，请关注我。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h4&gt;本文首发微信公众号：前端先锋&lt;/h4&gt;
 &lt;h4&gt;欢迎扫描二维码关注公众号，每天都给你推送新鲜的前端技术文章&lt;/h4&gt;
 &lt;p&gt;  &lt;img alt="&amp;#27426;&amp;#36814;&amp;#25195;&amp;#25551;&amp;#20108;&amp;#32500;&amp;#30721;&amp;#20851;&amp;#27880;&amp;#20844;&amp;#20247;&amp;#21495;&amp;#65292;&amp;#27599;&amp;#22825;&amp;#37117;&amp;#32473;&amp;#20320;&amp;#25512;&amp;#36865;&amp;#26032;&amp;#40092;&amp;#30340;&amp;#21069;&amp;#31471;&amp;#25216;&amp;#26415;&amp;#25991;&amp;#31456;" src="https://segmentfault.com/img/bVRyYe?w=430&amp;h=430" title="&amp;#27426;&amp;#36814;&amp;#25195;&amp;#25551;&amp;#20108;&amp;#32500;&amp;#30721;&amp;#20851;&amp;#27880;&amp;#20844;&amp;#20247;&amp;#21495;&amp;#65292;&amp;#27599;&amp;#22825;&amp;#37117;&amp;#32473;&amp;#20320;&amp;#25512;&amp;#36865;&amp;#26032;&amp;#40092;&amp;#30340;&amp;#21069;&amp;#31471;&amp;#25216;&amp;#26415;&amp;#25991;&amp;#31456;"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;h3&gt;欢迎继续阅读本专栏其它高赞文章：&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018036227"&gt;12个令人惊叹的CSS实验项目&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018604138"&gt;必须要会的 50 个React 面试题&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000017949080"&gt;世界顶级公司的前端面试都问些什么&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018577143"&gt;11 个最好的 JavaScript 动态效果库&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018056228"&gt;CSS Flexbox 可视化手册&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018141328"&gt;从设计者的角度看 React&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018088062"&gt;过节很无聊？还是用 JavaScript 写一个脑力小游戏吧！&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018113832"&gt;CSS粘性定位是怎样工作的&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000017977613"&gt;一步步教你用HTML5 SVG实现动画效果&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018224157"&gt;程序员30岁前月薪达不到30K，该何去何从&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018646425"&gt;14个最好的 JavaScript 数据可视化库&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018439250"&gt;8 个给前端的顶级 VS Code 扩展插件&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018660861"&gt;Node.js 多线程完全指南&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://segmentfault.com/a/1190000018701596"&gt;把HTML转成PDF的4个方案及实现&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>python 前端</category>
      <guid isPermaLink="true">https://itindex.net/detail/59454-python-%E7%BC%96%E7%A8%8B-%E9%80%9F%E6%88%90</guid>
      <pubDate>Fri, 12 Apr 2019 18:37:57 CST</pubDate>
    </item>
    <item>
      <title>异步编程 In .NET - 腾飞（Jesse) - 博客园</title>
      <link>https://itindex.net/detail/59432-%E5%BC%82%E6%AD%A5-%E7%BC%96%E7%A8%8B-in</link>
      <description>&lt;div&gt;    &lt;h2&gt;概述&lt;/h2&gt;    &lt;p&gt;　　在之前写的一篇关于      &lt;a href="http://www.cnblogs.com/jesse2013/p/async-and-await.html"&gt;async和await的前世今生&lt;/a&gt;的文章之后，大家似乎在async和await提高网站处理能力方面还有一些疑问，博客园本身也做了不少的尝试。今天我们再来回答一下这个问题，同时我们会做一个async和await在WinForm中的尝试，并且对比在4.5之前的异步编程模式APM/EAP和async/await的区别，最后我们还会探讨在不同线程之间交互的问题。&lt;/p&gt;    &lt;p&gt;　　IIS存在着处理能力的问题，但是WinForm却是UI响应的问题，并且WinForm的UI线程至始至终都是同一个，所以两者之间有一定的区别。有人会问，现在还有人写WinForm吗？好吧，它确是一个比较老的东西呢，不如WPF炫，技术也不如WPF先进，但是从架构层面来讲，不管是Web，还是WinForm，又或是WPF，Mobile，这些都只是表现层，不是么？现在的大型系统一般桌面客户端，Web端，手机，平板端都会涉及，这也是为什么会有应用层，服务层的存在。我们在这谈论的ASP.NET MVC，WinForm，WFP，Android/IOS/WP 都是表现层，在表现层我们应该只处理与“表现”相关的逻辑，任何与业务相关的逻辑应该都是放在下层处理的。关于架构的问题，我们后面再慢慢深入，另外别说我没有提示您，我们今天还会看到.NET中另一个已经老去的技术Web Service。&lt;/p&gt;    &lt;p&gt;　　还得提示您，文章内容有点长，涉及的知识点比较多，所以，我推荐：”先顶后看“ ，先顶后看是21世纪看长篇的首选之道，是良好沟通的开端，想知道是什么会让你与众不同吗？想知道为什么上海今天会下这么大的雨么？请记住先顶后看，你顶的不是我的文章，而是我们冒着大雨还要去上班的可贵精神！先顶后看，你值得拥有！&lt;/p&gt;    &lt;h2&gt;目录&lt;/h2&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#asyncwithiis"&gt;async/await如何提升IIS响应能力&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#aspnet-async"&gt;并行处理的步骤&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#facts-controlling-capacity"&gt;哪些因素影响了我们的响应能力&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#what-asyncandawait-have-done"&gt;async 和 await做了什么？&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Best-Practises"&gt;几点建议&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#apm"&gt;早期Web Service的异步模式APM&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#winform-and-webservice"&gt;当WinForm遇到Web Service&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#async-and-webservice"&gt;WinForm异步调用Web Service&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#APM-In-Detail"&gt;APM异步编程详解&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#thread-in-APM"&gt;线程问题&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Start-By-Delegate"&gt;从Delegate开始&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Inside-APM"&gt;再次认识APM&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#EAP"&gt;EAP(Event-Based Asynchroronous Pattern)&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Threading-in-EAP"&gt;线程问题&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Asyncandawait-In-WinForm"&gt;async/await 给WinForm编程带来了什么？&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Synchronization"&gt;不同线程之间通讯的问题&lt;/a&gt;        &lt;ul&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Invoke"&gt;万能的InvokeI&lt;/a&gt;&lt;/li&gt;          &lt;li&gt;            &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#SynchronizationContext"&gt;SynchronizationContex上下文同步对象&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Summary"&gt;小结&lt;/a&gt;&lt;/li&gt;      &lt;li&gt;        &lt;a href="https://www.cnblogs.com/jesse2013/p/Asynchronous-Programming-In-DotNet.html#Reference"&gt;引用 &amp;amp; 扩展阅读&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h1&gt;      &lt;a name="asyncwithiis"&gt;&lt;/a&gt;async/await如何提升IIS处理能力&lt;/h1&gt;    &lt;p&gt;　　首先响应能力并不完全是说我们程序性能的问题，有时候可能你的程序没有任何问题，而且精心经过优化，可是响应能力还是没有上去，网站性能分析是一个复杂的活，有时候只能靠经验和不断的尝试才能达到比较好的效果。当然我们今天讨论的主要是IIS的处理能力，或者也可能说是IIS的性能，但绝非代码本身的性能。即使async/await能够提高IIS的处理能力，但是对于用户来说整个页面从发起请求到页面渲染完成的这些时间，是不会因为我们加了async/await之后产生多大变化的。&lt;/p&gt;    &lt;p&gt;　　另外异步的ASP.NET并非只有async/await才可以做的，ASP.NET在Web Form时代就已经有异步Page了，包括ASP.NET MVC不是也有异步的Controller么？async/await 很新，很酷，但是它也只是在原有一技术基础上做了一些改进，让程序员们写起异步代码来更容易了。大家常说微软喜欢新瓶装旧酒，至少我们要看到这个新瓶给我们带来了什么，不管是任何产品，都不可能一开始就很完美，所以不断的迭代更新，也可以说是一种正确做事的方式。&lt;/p&gt;    &lt;h2&gt;      &lt;a name="aspnet-async"&gt;&lt;/a&gt;ASP.NET并行处理的步骤&lt;/h2&gt;    &lt;p&gt; 　　      &lt;a href="http://www.cnblogs.com/fengzheng/p/3668283.html" target="_blank"&gt;ASP.NET是如何在IIS中工作的&lt;/a&gt;一文已经很详细的介绍了一个请求是如何从客户端到服务器的HTTP.SYS最后进入CLR进行处理的（强烈建议不了解这一块的同学先看这篇文章，有助于你理解本小节），但是所有的步骤都是基于一个线程的假设下进行的。IIS本身就是一个多线程的工作环境，如果我们从多线程的视角来看会发生什么变化呢？我们首先来看一下下面这张图。注意：我们下面的步骤是建立在IIS7.0以后的集成模式基础之上的。（注：下面这张图在dudu的提醒之后，重新做了一些搜索工作，做了一些改动，w3dt这一步来自于博客园团队对问题的不断探索，详情可以点      &lt;a href="http://www.cnblogs.com/cmt/p/3774994.html" target="_blank"&gt;这里&lt;/a&gt;）&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/152137500061694.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;　　我们再来梳理一下上面的步骤：&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;所有的请求最开始是由HTTP.SYS接收的，HTTP.SYS内部有一个队列维护着这些请求，这个队列的request的数量大于一定数量（默认是1000）的时候，HTTP.SYS就会直接返回503状态（服务器忙），这是我们的第一个阀门。性能计数指标：“        &lt;strong&gt;Http Service Request Queues\CurrentQueueSize&lt;/strong&gt;”&lt;/li&gt;      &lt;li&gt;由w3dt负责把请求从HTTP.SYS 的队列中放到一个对应端口的队列中，据非官方资料显示该队列长度为能为20（该队列是非公开的，没有文档，所以也没有性能计数器）。&lt;/li&gt;      &lt;li&gt;IIS 的IO线程从上一步的队列中获取请求，如果是需要ASP.NET处理的，就会转交给CLR 线程池的Worker 线程，IIS的IO线程继续返回重复做该步骤。CLR 线程池的Worker线程数量是第二个阀门。&lt;/li&gt;      &lt;li&gt;当CLR中正在被处理的请求数据大于一定值（最大并行处理请求数量，.NET4以后默认是5000）的时候，从IO线程过来的请求就不会直接交给Worker线程，而是放到一个进程池级别的一个队列了，等到这个数量小于临界值的时候，才会把它再次交给Worker线程去处理。这是我们的第三个阀门。&lt;/li&gt;      &lt;li&gt;上一步中说到的那个进程池级别的队列有一个长度的限制，可以通过web.config里面的processModel/requestQueueLimit来设置。这可以说也是一个阀门。当正在处理的数量大于所允许的最大并行处理请求数量的时候，我们就会得到503了。可以通过性能计数指标：“        &lt;strong&gt;ASP.NET v4.0.30319\Requests Queued&lt;/strong&gt;” 来查看该队列的长度。&lt;/li&gt;&lt;/ol&gt;    &lt;h2&gt;       &lt;a name="facts-controlling-capacity"&gt;&lt;/a&gt;哪些因素会控制我们的响应能力&lt;/h2&gt;    &lt;p&gt;　　从上面我们提到了几大阀门中，我们可以得出下面的几个数字控制或者说影响着我们的响应能力。&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;HTTP.SYS队列的长度&lt;/li&gt;      &lt;li&gt;CLR线程池最大Worker线程数量&lt;/li&gt;      &lt;li&gt;最大并行处理请求数量&lt;/li&gt;      &lt;li&gt;进程池级别队列所允许的长度&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;      &lt;strong&gt;HTTP.SYS队列的长度&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;　　&lt;/strong&gt;这个我觉得不需要额外解释，默认值是1000。这个值取决于我们我们后面IIS IO线程和Worker线程的处理速度，如果它们两个都处理不了，这个数字再大也没有用。因为最后他们会被存储到进程池级别的队列中，所以只会造成内存的浪费。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;最大Worker线程数量&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;　　这个值是可以在web.config中进行配置的。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/152211154748645.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;　　maxWorkerThreads: CLR中真实处理请求的最大Worker线程数量      &lt;br /&gt;　　minWorkerThreads:CLR中真实处理请求的最小Worker线程数量&lt;/p&gt;    &lt;p&gt;　　minWorkerThreads的默认值是1，合理的加大他们可以避免不必要的线程创建和销毁工作。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;最大并行处理请求数量&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;　&lt;/strong&gt;　进程池级别的队列给我们的CLR一定的缓冲，这里面要注意的是，这个队列还没有进入到CLR，所以它不会占用我们托管环境的任何资源，也就是把请求卡在了CLR的外面。我们需要在aspnet.config级别进行配置，我们可以在.net fraemwork的安装目录下找到它。一般是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 如果你安装的是4.0的话。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/061820183095970.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;　　maxConcurrentRequestPerCPU: 每个CPU所允许的最大并行处理请求数量，当CLR中worker线程正在处理的请求之和大于这个数时，从IO线程过来的请求就会被放到我们进程池级别的队列中。      &lt;br /&gt;　　maxConcurrentThreadsPerCPU: 设置为0即禁用。      &lt;br /&gt;　　requestQueue:      &lt;strong&gt;进程池级别队列所允许的长度&lt;/strong&gt;。      &lt;strong&gt;　　&lt;/strong&gt;&lt;/p&gt;    &lt;h2&gt;      &lt;a name="what-asyncandawait-have-done"&gt;&lt;/a&gt;async和await 做了什么？&lt;/h2&gt;    &lt;p&gt;　　我们终于要切入正题了，拿ASP.NET MVC举例，如果不采用async的Action，那么毫无疑问，它是在一个Woker线程中执行的。当我们访问一些web service，或者读文件的时候，这个Worker线程就会被阻塞。假设我们这个Action执行时间一共是100ms，其它访问web service花了80ms，理想情况下一个Worker线程一秒可以响应10个请求，假设我们的maxWorkerThreads是10，那我们一秒内总是可响应请求就是100。如果说我们想把这个可响应请求数升到200怎么做呢？&lt;/p&gt;    &lt;p&gt;　　有人会说，这还不简单，把maxWorkerThreads调20不就行了么？ 其实我们做也没有什么 问题，确实是可以的，而且也确实能起到作用。那我们为什么还要大费周章的搞什么 async/await呢？搞得脑子都晕了？async/await给我们解决了什么问题？它可以在我们访问web service的时候把当前的worker线程放走，将它放回线程池，这样它就可以去处理其它的请求了。等到web service给我们返回结果了，会再到线程池中随机拿一个新的woker线程继续往下执行。也就是说我们减少了那一部分等待的时间，充份利用了线程。&lt;/p&gt;    &lt;p&gt;　   我们来对比一下使用async/awit和不使用的情况，&lt;/p&gt;    &lt;p&gt;　　不使用async/await： 20个woker线程1s可以处理200个请求。&lt;/p&gt;    &lt;p&gt;　　那转换成总的时间的就是 20 * 1000ms =  20000ms，      &lt;br /&gt;　　其中等待的时间为 200 * 80ms = 16000ms。      &lt;br /&gt;　　也就是说使用async/await我们至少节约了16000ms的时间，这20个worker线程又会再去处理请求，即使按照每个请求100ms的处理时间我们还可以再增加160个请求。而且别忘了100ms是基于同步情况下，包括等待时间在内的基础上得到的，所以实际情况可能还要多，当然我们这里没有算上线程切换的时间，所以实际情况中是有一点差异的，但是应该不会很大，因为我们的线程都是基于线程池的操作。      &lt;br /&gt;　　所有结果是20个Worker线程不使用异步的情况下，1s能自理200个请求，而使用异步的情况下可以处理360个请求，立马提升80%呀！采用异步之后，对于同样的请求数量，需要的Worker线程数据会大大减少50%左右，一个线程至少会在堆上分配1M的内存，如果是1000个线程那就是1G的容量，虽然内存现在便宜，但是省着总归是好的嘛，而且更少的线程是可以减少线程池在维护线程时产生的CPU消耗的。另：dudu分享CLR1秒之内只能创建2个线程。&lt;/p&gt;    &lt;p&gt;　　注意：以上数据并非真实测试数据，真实情况一个request的时间也并非100ms，花费在web service上的时间也并非80ms，仅仅是给大家一个思路:)，所以这里面用了async和await之后对响应能力有多大的提升和我们原来堵塞在这些IO和网络上的时间是有很大的关系的。&lt;/p&gt;    &lt;h2&gt;      &lt;a name="Best-Practises"&gt;&lt;/a&gt;几点建议&lt;/h2&gt;    &lt;p&gt;　　看到这里，不知道大家有没有得到点什么。首先第一点我们要知道的是async/await不是万能药，不们不能指望光写两个光键字就希望性能的提升。要记住，      &lt;strong&gt;一个CPU在同一时间段内是只能执行一个线程的。&lt;/strong&gt;所以这也是为什么async和await建议在IO或者是网络操作的时候使用。我们的MVC站点访问WCF或者Web Service这种场景就非常的适合使用异步来操作。在上面的例子中80ms读取web service的时间，大部份时间都是不需要cpu操作的，这样cpu才可以被其它的线程利用，如果不是一个读取web service的操作，而是一个复杂计算的操作，那你就等着cpu爆表吧。&lt;/p&gt;    &lt;p&gt;　　第二点是，除了程序中利用异步，我们上面讲到的关于IIS的配置是很重要的，如果使用了异步，请记得把maxWorkerThreads和maxConcurrentRequestPerCPU的值调高试试。&lt;/p&gt;    &lt;h1&gt;       &lt;a name="apm"&gt;&lt;/a&gt;早期对Web service的异步编程模式APM&lt;/h1&gt;    &lt;p&gt;　　讲完我们高大上的async/await之后，我们来看看这个技术很老，但是概念确依旧延续至今的Web Service。 我们这里所说的针对web service的异步编程模式不是指在服务器端的web service本身，而是指调用web service的客户端。大家知道对于web service，我们通过添加web service引用或者.net提供的生成工具就可以生成相应的代理类，可以让我们像调用本地代码一样访问web service，而所生成的代码类中对针对每一个web service方法生成3个对应的方法，比如说我们的方法名叫DownloadContent，除了这个方法之外还有BeginDownloadContent和EndDownloadContent方法，而这两个就是我们今天要说的早期的异步编程模式APM(Asynchronous Programming Model)。下面就来看看我们web service中的代码，注意我们现在的项目都是在.NET Framework3.5下实现的。&lt;/p&gt;    &lt;p&gt; PageContent.asmx的代码&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;public class PageContent : System.Web.Services.WebService
{
    [WebMethod]
    public string DownloadContent(string url)
    {
        var client = new System.Net.WebClient();
        return client.DownloadString(url);
    }
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　注意我们web service中的DownloadContent方法调用的是WebClient的同步方法，WebClient也有异步方法即：DownloadStringAsync。但是大家要明白，不管服务器是同步还是异步，对于客户端来说调用了你这个web service都是一样的，就是得等你返回结果。&lt;/p&gt;    &lt;p&gt;　　当然，我们也可以像MVC里面的代码一样，把我们的服务器端也写成异步的。那得到好处的是那个托管web service的服务器，它的处理能力得到提高，就像ASP.NET一样。如果我们用JavaScript去调用这个Web Service，那么Ajax(Asynchronous Javascript + XML)就是我们客户端用到的异步编程技术。如果是其它的客户端呢？比如说一个CS的桌面程序？我们需要异步编程么？&lt;/p&gt;    &lt;h2&gt;      &lt;a name="winform-and-webservice"&gt;&lt;/a&gt;当WinForm遇上Web Service&lt;/h2&gt;    &lt;p&gt;　　WinForm不像托管在IIS的ASP.NET网站，会有一个线程池管理着多个线程来处理用户的请求，换个说法ASP.NET网站生来就是基于多线程的。但是，在WinForm中，如果我们不刻意使用多线程，那至始至终，都只有一个线程，称之为UI线程。也许在一些小型的系统中WinForm很少涉及到多线程，因为WinForm本身的优势就在它是独立运行在客户端的，在性能上和可操作性上都会有很大的优势。所以很多中小型的WinForm系统都是直接就访问数据库了，并且基本上也只有数据的传输，什么图片资源那是很少的，所以等待的时间是很短的，基本不用费什么脑力去考虑什么3秒之内必须将页面显示到用户面前这种问题。&lt;/p&gt;    &lt;p&gt;　　既然WinForm在性能上有这么大的优势，那它还需要异步吗？&lt;/p&gt;    &lt;p&gt;　　我们上面说的是中小型的WinForm，如果是大型的系统呢？如果WinForm只是其它的很小一部分，就像我们文章开始说的还有很多其它成千上万个手机客户端，Web客户端，平板客户端呢？如果客户端很多导致数据库撑不住怎么办？ 想在中间加一层缓存怎么办？&lt;/p&gt;    &lt;p&gt;　　拿一个b2b的网站功能举例，用户可以通过网站下单，手机也可以下单，还可以通过电脑的桌面客户端下单。在下完单之后要完成交易，库存扣减，发送订单确认通知等等功能，而不管你的订单是通过哪个端完成的，这些功能我们都要去做，对吗？那我们就不能单独放在WinForm里面了，不然这些代码在其它的端里面又得全部全新再一一实现，同样的代码放在不同的地方那可是相当危险的，所以就有了我们后来的SOA架构，把这些功能都抽成服务，每种类型的端都是调用服务就可以了。一是可以统一维护这些功能，二是可以很方便的做扩展，去更好的适应功能和架构上的扩展。比如说像下面这样的一个系统。&lt;/p&gt;    &lt;p&gt;       &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/082056308644926.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;　　在上图中，Web端虽然也是属于我们平常说的服务端（甚至是由多台服务器组成的web群集），但是对我们整个系统来说，它也只是一个端而已。对于一个端来说，它本身只处理和用户交互的问题，其余所有的功能，业务都会交给后来台处理。在我们上面的架构中，应用层都不会直接参加真正业务逻辑相关的处理，而是放到我们更下层数据层去做处理。那么应用层主要协助做一些与用户交互的一些功能，如果手机短信发送，邮件发送等等，并且可以根据优先级选择是放入队列中稍候处理还是直接调用功能服务立即处理。&lt;/p&gt;    &lt;p&gt;　　在这样的一个系统中，我们的Web服务器也好，Winform端也好都将只是整个系统中的一个终端，它们主要的任何是用户和后面服务之间的一个桥梁。涉及到Service的调用之后，为了给用户良好的用户体验，在WinForm端，我们自然就要考虑异步的问题。 &lt;/p&gt;    &lt;h2&gt;      &lt;a name="async-and-webservice"&gt;&lt;/a&gt;WinForm异步调用Web Service&lt;/h2&gt;    &lt;p&gt;　　有了像VS这样强大的工具为我们生成代理类，我们在写调用Web service的代码时就可以像调用本地类库一样调用Web Service了，我们只需要添加一个Web Reference就可以了。&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/121431403173957.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;// Form1.cs的代码&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private void button1_Click(object sender, EventArgs e)
{
    var pageContentService = new localhost.PageContent();
    pageContentService.BeginDownloadContent(
        &amp;quot;http://jesse2013.cnblogs.com&amp;quot;,
        new AsyncCallback(DownloadContentCallback),
        pageContentService);
}

private void DownloadContentCallback(IAsyncResult result)
{
    var pageContentService = (localhost.PageContent)result.AsyncState;
    var msg = pageContentService.EndDownloadContent(result);
    MessageBox.Show(msg);
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　代码非常的简单，在执行完pageContentService.BeginDownloadContent之后，我们的主线程就返回了。在调用Web service这段时间内我们的UI不会被阻塞，也不会出现“无法响应这种情况”，我们依然可以拖动窗体甚至做其它的事情。这就是APM的魔力，但是我们的callback究竟是在哪个线程中执行的呢？是线程池中的线程么？咋们接着往下看。&lt;/p&gt;    &lt;h1&gt;      &lt;a name="APM-In-Detail"&gt;&lt;/a&gt;APM异步编程模式详解&lt;/h1&gt;    &lt;h2&gt;      &lt;a name="thread-in-APM"&gt;&lt;/a&gt;线程问题&lt;/h2&gt;    &lt;p&gt;　　接下来我们就是更进一步的了解APM这种模式是如何工作的，但是首先我们要回答上面留下来的问题，这种异步的编程方式有没有为我们开启新的线程？让代码说话：&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private void button1_Click(object sender, EventArgs e)
{
    Trace.TraceInformation(&amp;quot;Is current thread from thread pool? {0}&amp;quot;, Thread.CurrentThread.IsThreadPoolThread ? &amp;quot;Yes&amp;quot; : &amp;quot;No&amp;quot;);
    Trace.TraceInformation(&amp;quot;Start calling web service on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
    var pageContentService = new localhost.PageContent();
    pageContentService.BeginDownloadContent(
        &amp;quot;http://jesse2013.cnblogs.com&amp;quot;,
        new AsyncCallback(DownloadContentCallback),
        pageContentService);
}

private void DownloadContentCallback(IAsyncResult result)
{
    var pageContentService = (localhost.PageContent)result.AsyncState;
    var msg = pageContentService.EndDownloadContent(result);

    Trace.TraceInformation(&amp;quot;Is current thread from thread pool? {0}&amp;quot; , Thread.CurrentThread.IsThreadPoolThread ? &amp;quot;Yes&amp;quot; : &amp;quot;No&amp;quot;);
    Trace.TraceInformation(&amp;quot;End calling web service on thread: {0}, the result of the web service is: {1}&amp;quot;,
        Thread.CurrentThread.ManagedThreadId,
        msg);
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　我们在按钮点击的方法和callback方法中分别输出当前线程的ID，以及他们是否属于线程池的线程，得到的结果如下：&lt;/p&gt;    &lt;p&gt;　　Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? NO      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? NO      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : Start calling web service on thread: 9      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? YES      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? YES      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : End calling web service on thread: 14, the result of the web service is: &amp;lt;!DOCTYPE html&amp;gt;...&lt;/p&gt;    &lt;p&gt;　　按钮点击的方法是由UI直接控制，很明显它不是一个线程池线程，也不是后台线程。而我们的callback却是在一个来自于线程池的后台线程执行的，答案揭晓了，可是这会给我们带来一个问题，我们上面讲了只有UI线程也可以去更新我们的UI控件，也就是说在callback中我们是不能更新UI控件的，那我们如何让更新UI让用户知道反馈呢？答案在后面接晓 :)，让我们先专注于把APM弄清楚。&lt;/p&gt;    &lt;h2&gt;      &lt;a name="Start-By-Delegate"&gt;&lt;/a&gt;从Delegate开始&lt;/h2&gt;    &lt;p&gt;　　其实，APM在.NET3.5以前都被广泛使用，在WinForm窗体控制中，在一个IO操作的类库中等等！大家可以很容易的找到搭配了Begin和End的方法，更重要的是只要是有代理的地方，我们都可以使用APM这种模式。我们来看一个很简单的例子：&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;delegate void EatAsync(string food);
private void button2_Click(object sender, EventArgs e)
{
    var myAsync = new EatAsync(eat);
    Trace.TraceInformation(&amp;quot;Activate eating on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
    myAsync.BeginInvoke(&amp;quot;icecream&amp;quot;, new AsyncCallback(clean), myAsync);
}

private void eat(string food)
{
    Trace.TraceInformation(&amp;quot;I am eating.... on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
}

private void clean(IAsyncResult asyncResult)
{
    Trace.TraceInformation(&amp;quot;I am done eating.... on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　上面的代码中，我们通过把eat封装成一个委托，然后再调用该委托的BeginInvoke方法实现了异步的执行。也就是实际的eat方法不是在主线程中执行的，我们可以看输出的结果：&lt;/p&gt;    &lt;p&gt;　　Desktop4.0.vshost.exe Information: 0 : Activate eating on thread: 10      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : I am eating.... on thread: 6      &lt;br /&gt;　　Desktop4.0.vshost.exe Information: 0 : I am done eating.... on thread: 6&lt;/p&gt;    &lt;p&gt;　　clean是我们传进去的callback，该方法会在我们的eat方法执行完之后被调用，所以它会和我们eat方法在同一个线程中被调用。大家如果熟悉代理的话就会知道，代码实际上会被编译成一个类，而BeginInvoke和EndInvoke方法正是编译器为我们自动加进去的方法，我们不用额外做任何事情，这在早期没有TPL和async/await之前（APM从.NET1.0时代就有了），的确是一个不错的选择。&lt;/p&gt;    &lt;h2&gt;      &lt;a name="Inside-APM"&gt;&lt;/a&gt;再次认识APM&lt;/h2&gt;    &lt;p&gt;了解了Delegate实现的BeginInvoke和EndInvoke之后，我们再来分析一下APM用到的那些对象。 拿我们Web service的代理类来举例，它为我们生成了以下3个方法：&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;string DownloadContent(string url)： 同步方法&lt;/li&gt;      &lt;li&gt;IAsyncResult BeginDownloadContent(string url, AsyncCallback callback, object asyncState)： 异步开始方法&lt;/li&gt;      &lt;li&gt;EndDownloadContent(IAsyncResult asyncResult)：异步结束方法&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;　　在我们调用EndDownloadContent方法的时候，如果我们的web service调用还没有返回，那这个时候就会用阻塞的方式去拿结果。但是在我们传到BeginDownloadContent中的callback被调用的时候，那操作一定是已经完成了，也就是说IAsyncResult.IsCompleted = true。而在APM异步编程模式中Begin方法总是返回IAsyncResult这个接口的实现。IAsyncReuslt仅仅包含以下4个属性：&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/121633468174536.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;　　WaitHanlde通常作为同步对象的基类，并且可以利用它来阻塞线程，更多信息可以参考      &lt;a href="http://msdn.microsoft.com/zh-cn/library/system.threading.waithandle(v=vs.110).aspx" target="_blank"&gt;MSDN&lt;/a&gt; 。 借助于IAsyncResult的帮助，我们就可以通过以下几种方式去获取当前所执行操作的结果。&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;轮询&lt;/li&gt;      &lt;li&gt;强制等待&lt;/li&gt;      &lt;li&gt;完成通知&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;　　完成通知就是们在&amp;quot;WinForm异步调用WebService&amp;quot;那结中用到的方法，调完Begin方法之后，主线程就算完成任务了。我们也不用监控该操作的执行情况，当该操作执行完之后，我们在Begin方法中传进去的callback就会被调用了，我们可以在那个方法中调用End方法去获取结果。下面我们再简单说一下前面两种方式。&lt;/p&gt;    &lt;p&gt;      &lt;em&gt;//轮询获取结果代码&lt;/em&gt;&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;var pageContentService = new localhost.PageContent();
IAsyncResult asyncResult = pageContentService.BeginDownloadContent(
    &amp;quot;http://jesse2013.cnblogs.com&amp;quot;,
    null,
    pageContentService);

while (!asyncResult.IsCompleted)
{
    Thread.Sleep(100);
}
var content = pageContentService.EndDownloadContent(asyncResult);&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;       &lt;em&gt;// 强制等待结果代码&lt;/em&gt;&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;var pageContentService = new localhost.PageContent();
IAsyncResult asyncResult = pageContentService.BeginDownloadContent(
    &amp;quot;http://jesse2013.cnblogs.com&amp;quot;,
    null,
    pageContentService);

// 也可以调用WaitOne()的无参版本，不限制强制等待时间
if (asyncResult.AsyncWaitHandle.WaitOne(2000))
{
    var content = pageContentService.EndDownloadContent(asyncResult);
}
else
{ 
    // 2s时间已经过了，但是还没有执行完   
}&lt;/pre&gt;&lt;/div&gt;    &lt;h1&gt;      &lt;a name="EAP"&gt;&lt;/a&gt;EAP(Event-Based Asynchronous Pattern)&lt;/h1&gt;    &lt;p&gt;　　EAP是在.NET2.0推出的另一种过渡的异步编程模型，也是在.NET3.5以后Microsoft支持的一种做法，为什么呢？ 如果大家建一个.NET4.0或者更高版本的WinForm项目，再去添加Web Reference就会发现生成的代理类中已经没有Begin和End方法了，记住在3.5的时候是两者共存的，你可以选择任意一种来使用。但是到了.NET4.0以后，EAP成为了你唯一的选择。（我没有尝试过手动生成代理类，有兴趣的同学可以尝试一下）让我们来看一下在.NET4下，我们是如何异步调用Web Service的。&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private void button1_Click(object sender, EventArgs e)
{
    var pageContent = new localhost.PageContent();
    pageContent.DownloadContentAsync(&amp;quot;http://jesse2013.cnblogs.com&amp;quot;);
    pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted;
}

private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e)
{
    if (e.Error == null)
    {
        textBox1.Text = e.Result;
    }
    else
    { 
        // 出错了
    }
}&lt;/pre&gt;&lt;/div&gt;    &lt;h2&gt;      &lt;a name="Threading-in-EAP"&gt;&lt;/a&gt;线程问题&lt;/h2&gt;    &lt;p&gt;　　不知道大家还是否记得，在APM模式中，callback是执行在另一个线程中，不能随易的去更新UI。但是如果你仔细看一下上面的代码，我们的DownloadContentCompleted事件绑定的方法中直接就更新了UI，把返回的内容写到了一个文本框里面。通过同样的方法可以发现，在EAP这种异步编程模式下，事件绑定的方法也是在调用的那个线程中执行的。也就是说解决了异步编程的时候UI交互的问题，而且是在同一个线程中执行。 看看下面的代码：&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private void button1_Click(object sender, EventArgs e)
{
    Trace.TraceInformation(&amp;quot;Call DownloadContentAsync on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
    Trace.TraceInformation(&amp;quot;Is current from thread pool? : {0}&amp;quot;, Thread.CurrentThread.IsThreadPoolThread ? &amp;quot;YES&amp;quot; : &amp;quot;NO&amp;quot;);

    var pageContent = new localhost.PageContent();
    pageContent.DownloadContentAsync(&amp;quot;http://jesse2013.cnblogs.com&amp;quot;);
    pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted;
}

private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e)
{
    Trace.TraceInformation(&amp;quot;Completed DownloadContentAsync on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
    Trace.TraceInformation(&amp;quot;Is current from thread pool? : {0}&amp;quot;, Thread.CurrentThread.IsThreadPoolThread ? &amp;quot;YES&amp;quot; : &amp;quot;NO&amp;quot;);
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　      &lt;em&gt;　Desktop4.vshost.exe Information: 0 : Call DownloadContentAsync on thread: 10&lt;/em&gt;      &lt;br /&gt;      &lt;em&gt;　　Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO&lt;/em&gt;      &lt;br /&gt;      &lt;em&gt;　　Desktop4.vshost.exe Information: 0 : Completed DownloadContentAsync on thread: 10&lt;/em&gt;      &lt;br /&gt;      &lt;em&gt;　　Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO&lt;/em&gt;&lt;/p&gt;    &lt;h1&gt;      &lt;a name="Asyncandawait-In-WinForm"&gt;&lt;/a&gt;async/await 给WinFrom带来了什么&lt;/h1&gt;    &lt;p&gt;　　如果说async给ASP.NET带来的是处理能力的提高，那么在WinForm中给程序员带来的好处则是最大的。我们再也不用因为要实现异步写回调或者绑定事件了，省事了，可读性也提高了。不信你看下面我们将调用我们那个web service的代码在.NET4.5下实现一下：&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private async void button2_Click(object sender, EventArgs e)
{
    var pageContent = new localhost.PageContentSoapClient();
    var content = await pageContent.DownloadContentAsync(&amp;quot;http://jesse2013.cnblogs.com&amp;quot;);

    textBox1.Text = content.Body.DownloadContentResult;
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　简单的三行代码，像写同步代码一样写异步代码，我想也许这就是async/await的魔力吧。在await之后，UI线程就可以回去响应UI了，在上面的代码中我们是没有新线程产生的，和EAP一样拿到结果直接就可以对UI操作了。&lt;/p&gt;    &lt;p&gt;　　async/await似乎真的很好，但是如果我们await后面的代码执行在另外一个线程中会发生什么事情呢？&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private async void button1_Click(object sender, EventArgs e)
{
    label1.Text = &amp;quot;Calculating Sqrt of 5000000&amp;quot;;
    button1.Enabled = false;
    progressBar1.Visible = true;

    double sqrt = await Task&amp;lt;double&amp;gt;.Run(() =&amp;gt;
    {
        double result = 0;
        for (int i = 0; i &amp;lt; 50000000; i++)
        {
            result += Math.Sqrt(i);

            progressBar1.Maximum = 50000000;
            progressBar1.Value = i;
        }
        return result;
    });

    progressBar1.Visible = false;
    button1.Enabled = true;
    label1.Text = &amp;quot;The sqrt of 50000000 is &amp;quot; + sqrt;
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　我们在界面中放了一个ProgressBar，同时开一个线程去把从1到5000000的平方全部加起来，看起来是一个非常耗时的操作，于是我们用Task.Run开了一个新的线程去执行。（注：如果是纯运算的操作，多线程操作对性能没有多大帮助，我们这里主要是想给UI一个进度显示当前进行到哪一步了。）看起来没有什么问题，我们按F5运行吧！      &lt;br /&gt;　　Bomb~&lt;/p&gt;    &lt;p&gt;      &lt;img alt="" src="https://images0.cnblogs.com/i/554526/201407/122046099423060.png"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;　　当执行到这里的时候，程序就崩溃了，告诉我们”无效操作，只能从创建porgressBar的线程访问它。“  这也是我们一开始提到的，在WinForm程序中，只有UI主线程才能对UI进行操作，其它的线程是没有权限的。接下来我们就来看看，如果在WinForm中实现非UI线程对UI控制的更新操作。 &lt;/p&gt;    &lt;h1&gt;      &lt;a name="Synchronization"&gt;&lt;/a&gt;不同线程之间通讯的问题&lt;/h1&gt;    &lt;h2&gt;      &lt;a name="Invoke"&gt;&lt;/a&gt;万能的Invoke&lt;/h2&gt;    &lt;p&gt;　　WinForm中绝大多数的控件包括窗体在内都实现了      &lt;a href="http://msdn.microsoft.com/en-us/library/0b1bf3y3(v=vs.110).aspx" target="_blank"&gt;Invoke&lt;/a&gt;方法，可以传入一个Delegate，这个Delegate将会被拥有那个控制的线程所调用，从而避免了跨线程访问的问题。&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;Trace.TraceInformation(&amp;quot;UI Thread : {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
double sqrt = await Task&amp;lt;double&amp;gt;.Run(() =&amp;gt;
{
    Trace.TraceInformation(&amp;quot;Run calculation on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
    double result = 0;
    for (int i = 0; i &amp;lt; 50000000; i++)
    {
        result += Math.Sqrt(i);
        progressBar1.Invoke(new Action(() =&amp;gt; {
            Trace.TraceInformation(&amp;quot;Update UI on thread: {0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);
            progressBar1.Maximum = 50000000;
            progressBar1.Value = i;
        }));
    }
    return result;
});&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　Desktop.vshost.exe Information: 0 : UI Thread : 9      &lt;br /&gt;　　Desktop.vshost.exe Information: 0 : Run calculation on thread: 10      &lt;br /&gt;　　Desktop.vshost.exe Information: 0 : Update UI on thread: 9&lt;/p&gt;    &lt;p&gt;　　Invoke方法比较简单，我们就不做过多的研究了，但是我们要考虑到一点，Invoke是WinForm实现的UI跨线程沟通方式，WPF用的却是Dispatcher，如果是在ASP.NET下跨线程之间的同步又怎么办呢。为了兼容各种技术平台下，跨线程同步的问题，Microsoft在.NET2.0的时候就引入了我们下面的这个对象。      &lt;br /&gt;&lt;/p&gt;    &lt;h2&gt;      &lt;a name="SynchronizationContext"&gt;&lt;/a&gt;SynchronizationContext上下文同步对象&lt;/h2&gt;    &lt;p&gt;      &lt;strong&gt;为什么需要SynchronizationContext&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;　　就像我们在WinForm中遇到的问题一样，有时候我们需要在一个线程中传递一些数据或者做一些操作到另一个线程。但是在绝大多数情况下这是不允许的，出于安全因素的考虑，每一个线程都有它独立的内存空间和上下文。因此在.NET2.0，微软推出了SynchronizationContext。&lt;/p&gt;    &lt;p&gt;　　它主要的功能之一是为我们提供了一种将一些工作任务（Delegate)以队列的方式存储在一个上下文对象中，然后把这些上下文对象关联到具体的线程上，当然有时候多个线程也可以关联到同一个SynchronizationContext对象。获取当前线程的同步上下文对象可以使用SynchronizationContext.Current。同时它还为我们提供以下两个方法Post和Send，分别是以异步和同步的方法将我们上面说的工作任务放到我们SynchronizationContext的队列中。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;SynchronizationContext示例&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;　&lt;/strong&gt;　还是拿我们上面Invoke中用到的例子举例，只是这次我们不直接调用控件的Invoke方法去更新它，而是写了一个Report的方法专门去更新UI。&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;double sqrt = await Task&amp;lt;double&amp;gt;.Run(() =&amp;gt;
{
    Trace.TraceInformation(&amp;quot;Current thread id is:{0}&amp;quot;, Thread.CurrentThread.ManagedThreadId);

    double result = 0;
    for (int i = 0; i &amp;lt; 50000000; i++)
    {
        result += Math.Sqrt(i);
        Report(new Tuple&amp;lt;int, int&amp;gt;(50000000, i));
    }
    return result;
});&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　每一次操作完之后我们调用一下Report方法，把我们总共要算的数字，以及当前正在计算的数字传给它就可以了。接下来就看我们的Report方法了。&lt;/p&gt;    &lt;div&gt;      &lt;pre&gt;private SynchronizationContext m_SynchronizationContext;
private DateTime m_PreviousTime = DateTime.Now;

public Form1()
{
    InitializeComponent();
    // 在全局保存当前UI线程的SynchronizationContext对象
    m_SynchronizationContext = SynchronizationContext.Current;
}

public void Report(Tuple&amp;lt;int, int&amp;gt; value)
{
    DateTime now = DateTime.Now;
    if ((now - m_PreviousTime).Milliseconds &amp;gt; 100)
    {
        m_SynchronizationContext.Post((obj) =&amp;gt;
        {
            Tuple&amp;lt;int, int&amp;gt; minMax = (Tuple&amp;lt;int, int&amp;gt;)obj;
            progressBar1.Maximum = minMax.Item1;
            progressBar1.Value = minMax.Item2;
        }, value);

        m_PreviousTime = now;
    }
}&lt;/pre&gt;&lt;/div&gt;    &lt;p&gt;　　整个操作看起来要比Inovke复杂一点，与Invoke不同的是SynchronizationContext不需要对Control的引用，而Invoke必须先得有那个控件才能调用它的Invoke方法对它进行操作。&lt;/p&gt;    &lt;h1&gt;      &lt;a name="Summary"&gt;&lt;/a&gt;小结&lt;/h1&gt;    &lt;p&gt;　　这篇博客内容有点长，不知道有多少人可以看到这里:)。最开始我只是想写写WinFrom下异步调用Web Service的一些东西，在一开始这篇文件的题目是”异步编程在WinForm下的实践“，但是写着写着发现越来越多的迷团没有解开，其实都是一些老的技术以前没有接触和掌握好，所以所幸就一次性把他们都重新学习了一遍，与大家分享。&lt;/p&gt;    &lt;p&gt;　　我们再来回顾一下文章所涉及到的一些重要的概念：&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;async/await 在ASP.NET做的最大贡献（早期ASP.NET的异步开发模式同样也有这样的贡献），是在访问数据库的时候、访问远程IO的时候及时释放了当前的处理性程，可以让这些线程回到线程池中，从而实现可以去处理其它请求的功能。&lt;/li&gt;      &lt;li&gt;异步的ASP.NET开发能够在处理能力上带来多大的提高，取决于我们的程序有多少时间是被阻塞的，也就是那些访问数据库和远程Service的时间。&lt;/li&gt;      &lt;li&gt;除了将代码改成异步，我们还需要在IIS上做一些相对的配置来实现最优化。&lt;/li&gt;      &lt;li&gt;不管是ASP.NET、WinForm还是Mobile、还是平板，在大型系统中都只是一个与用户交互的端而已，所以不管你现在是做所谓的前端（JavaScript + CSS等)，还是所谓的后端(ASP.NET MVC、WCF、Web API 等 )，又或者是比较时髦的移动端（IOS也好，Andrioid也罢，哪怕是不争气的WP），都只是整个大型系统中的零星一角而已。当然我并不是贬低这些端的价值，正是因为我们专注于不同，努力提高每一个端的用户体验，才能让这些大型系统有露脸的机会。我想说的是，在你对现在技术取得一定的成就之后，不要停止学习，因为整个软件架构体系中还有很多很多美妙的东西值得我们去发现。&lt;/li&gt;      &lt;li&gt;APM和EAP是在async/await之前的两种不同的异步编程模式。&lt;/li&gt;      &lt;li&gt;APM如果不阻塞主线程，那么完成通知（回调）就会执行在另外一个线程中，从而给我们更新UI带来一定的问题。&lt;/li&gt;      &lt;li&gt;EAP的通知事件是在主线程中执行的，不会存在UI交互的问题。&lt;/li&gt;      &lt;li&gt;最后，我们还学习了在Winform下不同线程之间交互的问题，以及SynchronizationContext。&lt;/li&gt;      &lt;li&gt;APM是.NET下最早的异步编程方法，从.NET1.0以来就有了。在.NET2.0的时候，微软意识到了APM的回调函数中与UI交互的问题，于是带来了新的EAP。APM与EAP一直共存到.NET3.5，在.NET4.0的时候微软带来了TPL，也就是我们所熟知的Task编程，而.NET4.5就是我们大家知道的async/await了，可以看到.NET一直在不停的进步，加上最近不断的和开源社区的合作，跨平台等特性的引入，我们有理由相信.NET会越走越好。&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;　　最后，这篇文章从找资料学习到写出来，差不多花了我两个周未的时间，希望能够给需要的人或者感兴趣想要不断学习的人一点帮助（不管是往前学习，还是往后学习）最后还要感谢@田园里面的蟋蟀，在阅读的时候给我找了一些错别字！&lt;/p&gt;    &lt;h1&gt;      &lt;a name="Reference"&gt;&lt;/a&gt;引用 &amp;amp; 扩展阅读&lt;/h1&gt;    &lt;p&gt;http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx      &lt;br /&gt;http://blog.stevensanderson.com/2008/04/05/improve-scalability-in-aspnet-mvc-using-asynchronous-requests      &lt;br /&gt;http://blogs.msdn.com/b/tmarq/archive/2007/07/21/asp-net-thread-usage-on-iis-7-0-and-6-0.aspx       &lt;br /&gt;http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx      &lt;br /&gt;http://mohamadhalabi.com/2014/05/08/thread-throttling-in-iis-hosted-wcf-sync-vs-async/      &lt;br /&gt;Pro Asynchronous Programs with .NET by Richard Blewett and Andrew Clymer&lt;/p&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/59432-%E5%BC%82%E6%AD%A5-%E7%BC%96%E7%A8%8B-in</guid>
      <pubDate>Tue, 09 Apr 2019 11:12:21 CST</pubDate>
    </item>
    <item>
      <title>【OpenCV入门教程之十二】OpenCV边缘检测：Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑 - 【浅墨的游戏编程Blog】毛星云（浅墨）的专栏 - CSDN博客</title>
      <link>https://itindex.net/detail/59310-opencv-%E7%A8%8B%E4%B9%8B-%E5%8D%81%E4%BA%8C</link>
      <description>&lt;div&gt;    &lt;blockquote&gt;      &lt;div&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;     &lt;br /&gt;&lt;/p&gt;        &lt;p&gt;  &lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;          &lt;strong&gt;文章链接：            &lt;a href="http://blog.csdn.net/poem_qianmo/article/details/22745559" rel="nofollow"&gt; &lt;/a&gt;            &lt;a href="http://blog.csdn.net/poem_qianmo/article/details/25560901" rel="nofollow"&gt;http://blog.csdn.net/poem_qianmo/article/details/25560901&lt;/a&gt;            &lt;a href="http://blog.csdn.net/poem_qianmo/article/details/21479533" rel="nofollow"&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;        &lt;p&gt;&lt;/p&gt;        &lt;p&gt;     &lt;br /&gt;&lt;/p&gt;&lt;/div&gt;      &lt;p&gt;本篇文章中，我们将一起学习OpenCV中边缘检测的各种算子和滤波器——Canny算子,Sobel算子,Laplace算子以及Scharr滤波器。文章中包含了五个浅墨为大家准备的详细注释的博文配套源代码。在介绍四块知识点的时候分别一个，以及最后的综合示例中的一个。文章末尾提供配套源代码的下载。    &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;依然是是放出一些程序运行截图吧：&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511205916531?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511210026921?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; 效果图看完，我们来唠唠嗑。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;首先，需要说明的是，浅墨这篇文章最后的示例代码是采用两周前刚刚发布的2.4.9来书写的。里面的lib都已经改成了2.4.9版本的。如果大家需要运行的话，要么配置好2.4.9.要么把浅墨在工程中包含的末尾数字为249的各种lib改成之前的248或者你对应的OpenCV版本。&lt;/p&gt;      &lt;p&gt;不然会提示： LINK : fatal error LNK1181: 无法打开输入文件“opencv_calib3d248.lib”之类的错误。&lt;/p&gt;      &lt;p&gt;OpenCV 2.4.9的配置和之前的2.4.8差不多，如果还是不太清楚，具体可以参考浅墨修改过的对应2.4.9版的配置文章：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;a href="http://blog.csdn.net/poem_qianmo/article/details/19809337" rel="nofollow"&gt;          &lt;strong&gt;【OpenCV入门教程之一】 安装OpenCV：OpenCV 2.4.8或2.4.9 +VS 开发环境配置&lt;/strong&gt;&lt;/a&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;第二，给大家分享一个OpenCV中写代码时节约时间的小常识。其实OpenCV中，不用namedWindow，直接imshow就可以显示出窗口。大家看下文的示例代码就可以发现，浅墨在写代码的时候并没有用namedWindow，遇到想显示出来的Mat变量直接imshow。我们一般是为了规范，才先用namedWindow创建窗口，再imshow出它来，因为我们还有需要用到指定窗口名称的地方，比如用到trackbar的时候。而一般情况想显示一个Mat变量的图片的话，直接imshow就可以啦。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;OK，开始正文吧~&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h1&gt;一、关于边缘检测&lt;/h1&gt;      &lt;p&gt;       &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;在具体介绍之前，先来一起看看边缘检测的一般步骤吧。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;1）滤波：边缘检测的算法主要是基于图像强度的一阶和二阶导数，但导数通常对噪声很敏感，因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波，即采用离散化的高斯函数产生一组归一化的高斯核（具体见“高斯滤波原理及其编程离散化实现方法”一文），然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和（具体程序实现见下文）。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;       2）增强：增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时，可通过计算梯度幅值来确定。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;       3）检测：经过增强的图像，往往邻域中有很多点的梯度值比较大，而在特定的应用中，这些点并不是我们要找的边缘点，所以应该采用某种方法来对这些点进行取舍。实际工程中，常用的方法是通过阈值化方法来检测。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;另外，需要注意，下文中讲到的Laplace算子，sobel算子和Scharr算子都是带方向的，所以，示例中我们分别写了X方向,Y方向和最终合成的的效果图。        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;OK，正餐开始，召唤canny算子。：）&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h1&gt;二、canny算子篇&lt;/h1&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;2.1canny算子相关理论与概念讲解&lt;/h2&gt;      &lt;h3&gt;        &lt;br /&gt;&lt;/h3&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;h3&gt;2.1.1canny算子简介&lt;/h3&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Canny边缘检测算子是John F.Canny于 1986 年开发出来的一个多级边缘检测算法。更为重要的是 Canny 创立了边缘检测计算理论（Computational theory ofedge detection），解释了这项技术是如何工作的。Canny边缘检测算法以Canny的名字命名，被很多人推崇为当今最优的边缘检测的算法。&lt;/p&gt;      &lt;p&gt;其中，Canny 的目标是找到一个最优的边缘检测算法，让我们看一下最优边缘检测的三个主要评价标准:&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;1.低错误率:标识出尽可能多的实际边缘，同时尽可能的减少噪声产生的误报。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;2.高定位性:标识出的边缘要与图像中的实际边缘尽可能接近。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;3.最小响应:图像中的边缘只能标识一次，并且可能存在的图像噪声不应标识为边缘。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;为了满足这些要求 Canny 使用了变分法，这是一种寻找满足特定功能的函数的方法。最优检测使用四个指数函数项的和表示，但是它非常近似于高斯函数的一阶导数。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;h3&gt;2.1.2Canny 边缘检测的步骤&lt;/h3&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;1.消除噪声。一般情况下，使用高斯平滑滤波器卷积降噪。 如下显示了一个 size = 5 的高斯内核示例:&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" height="180" src="https://img-blog.csdn.net/20140511211519875?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="350"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;2.计算梯度幅值和方向。此处，按照Sobel滤波器的步骤。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;strong&gt;Ⅰ.&lt;/strong&gt;运用一对卷积阵列 (分别作用于 x 和 y 方向):&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" height="98" src="https://img-blog.csdn.net/20140511211922328?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="433"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;strong&gt;Ⅱ.&lt;/strong&gt;使用下列公式计算梯度幅值和方向:&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140707092033453?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;梯度方向近似到四个可能角度之一(一般为0, 45, 90, 135)&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;3.非极大值抑制。这一步排除非边缘像素， 仅仅保留了一些细线条(候选边缘)。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;4.滞后阈值。最后一步，Canny 使用了滞后阈值，滞后阈值需要两个阈值(高阈值和低阈值):&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;          &lt;strong&gt;Ⅰ.&lt;/strong&gt;如果某一像素位置的幅值超过 高 阈值, 该像素被保留为边缘像素。&lt;/p&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;          &lt;strong&gt;Ⅱ.&lt;/strong&gt;如果某一像素位置的幅值小于 低 阈值, 该像素被排除。&lt;/p&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;          &lt;strong&gt;Ⅲ&lt;/strong&gt;.如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于 高 阈值的像素时被保留。&lt;/p&gt;        &lt;p&gt;          &lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;tips：对于Canny函数的使用，推荐的高低阈值比在2:1到3:1之间。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;p&gt;更多的细节，可以参考canny算子的wikipedia：&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;p&gt;            &lt;a href="http://en.wikipedia.org/wiki/Canny_edge_detector" rel="nofollow"&gt;http://en.wikipedia.org/wiki/Canny_edge_detector&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;p&gt;canny边缘检测的原理讲述，课参看这篇博文：&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;p&gt;            &lt;a href="http://blog.csdn.net/likezhaobin/article/details/6892176" rel="nofollow"&gt;http://blog.csdn.net/likezhaobin/article/details/6892176&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;p&gt;canny算子的中文wikipedia：&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;p&gt;            &lt;a href="http://zh.wikipedia.org/wiki/Canny%E7%AE%97%E5%AD%90" rel="nofollow"&gt;http://zh.wikipedia.org/wiki/Canny%E7%AE%97%E5%AD%90&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;2.2OpenCV中Canny函数详解&lt;/h2&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;Canny函数利用Canny算法来进行图像的边缘检测。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;C++: void Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false )&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;br /&gt;      &lt;p&gt;&lt;/p&gt;      &lt;ul&gt;        &lt;li&gt;第一个参数，InputArray类型的image，输入图像，即源图像，填Mat类的对象即可，且需为单通道8位图像。&lt;/li&gt;        &lt;li&gt;第二个参数，OutputArray类型的edges，输出的边缘图，需要和源图片有一样的尺寸和类型。&lt;/li&gt;        &lt;li&gt;第三个参数，double类型的threshold1，第一个滞后性阈值。&lt;/li&gt;        &lt;li&gt;第四个参数，double类型的threshold2，第二个滞后性阈值。&lt;/li&gt;        &lt;li&gt;第五个参数，int类型的apertureSize，表示应用Sobel算子的孔径大小，其有默认值3。&lt;/li&gt;        &lt;li&gt;第六个参数，bool类型的L2gradient，一个计算图像梯度幅值的标识，有默认值false。&lt;/li&gt;&lt;/ul&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;需要注意的是，这个函数阈值1和阈值2两者的小者用于边缘连接，而大者用来控制强边缘的初始段，推荐的高低阈值比在2:1到3:1之间。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;调用示例：&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;//载入原始图 
       Mat src = imread(&amp;quot;1.jpg&amp;quot;);  //工程目录下应该有一张名为1.jpg的素材图
       Canny(src, src, 3, 9,3 );
       imshow(&amp;quot;【效果图】Canny边缘检测&amp;quot;, src);&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;如上三句，就有结果出来，非常好用。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;2.3调用Canny函数的实例代码&lt;/h2&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;OpenCV中调用Canny函数的实例代码如下：&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;//-----------------------------------【头文件包含部分】---------------------------------------
//            描述：包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include &amp;lt;opencv2/opencv.hpp&amp;gt;
#include&amp;lt;opencv2/highgui/highgui.hpp&amp;gt;
#include&amp;lt;opencv2/imgproc/imgproc.hpp&amp;gt;

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述：包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//            描述：控制台应用程序的入口函数，我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图  
	Mat src = imread(&amp;quot;1.jpg&amp;quot;);  //工程目录下应该有一张名为1.jpg的素材图
	Mat src1=src.clone();

	//显示原始图 
	imshow(&amp;quot;【原始图】Canny边缘检测&amp;quot;, src); 

	//----------------------------------------------------------------------------------
	//	一、最简单的canny用法，拿到原图后直接用。
	//----------------------------------------------------------------------------------
	Canny( src, src, 150, 100,3 );
	imshow(&amp;quot;【效果图】Canny边缘检测&amp;quot;, src); 

	
	//----------------------------------------------------------------------------------
	//	二、高阶的canny用法，转成灰度图，降噪，用canny，最后将得到的边缘作为掩码，拷贝原图到效果图上，得到彩色的边缘图
	//----------------------------------------------------------------------------------
	Mat dst,edge,gray;

	// 【1】创建与src同类型和大小的矩阵(dst)
	dst.create( src1.size(), src1.type() );

	// 【2】将原图像转换为灰度图像
	cvtColor( src1, gray, CV_BGR2GRAY );

	// 【3】先用使用 3x3内核来降噪
	blur( gray, edge, Size(3,3) );

	// 【4】运行Canny算子
	Canny( edge, edge, 3, 9,3 );

	//【5】将g_dstImage内的所有元素设置为0 
	dst = Scalar::all(0);

	//【6】使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码，来将原图g_srcImage拷到目标图g_dstImage中
	src1.copyTo( dst, edge);

	//【7】显示效果图 
	imshow(&amp;quot;【效果图】Canny边缘检测2&amp;quot;, dst); 


	waitKey(0); 

	return 0; 
}&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;br /&gt;      &lt;p&gt;运行效果图：&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511212932562?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511213016093?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511213053078?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h1&gt;三、sobel算子篇&lt;/h1&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;h2&gt;3.1sobel算子相关理论与概念讲解&lt;/h2&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;div&gt;        &lt;br /&gt;&lt;/div&gt;      &lt;h3&gt;3.1.1基本概念&lt;/h3&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Sobel 算子是一个主要用作边缘检测的离散微分算子 (discrete differentiation operator)。 它Sobel算子结合了高斯平滑和微分求导，用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子，将会产生对应的梯度矢量或是其法矢量。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;sobel算子的wikipedia：&lt;/p&gt;      &lt;p&gt;        &lt;a href="http://zh.wikipedia.org/wiki/%E7%B4%A2%E8%B2%9D%E7%88%BE%E7%AE%97%E5%AD%90" rel="nofollow"&gt;http://zh.wikipedia.org/wiki/%E7%B4%A2%E8%B2%9D%E7%88%BE%E7%AE%97%E5%AD%90&lt;/a&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;sobel算子相关概念，还可以参看这篇博文：&lt;/p&gt;      &lt;p&gt;        &lt;a href="http://www.cnblogs.com/lancidie/archive/2011/07/17/2108885.html" rel="nofollow"&gt;http://www.cnblogs.com/lancidie/archive/2011/07/17/2108885.html&lt;/a&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;strong&gt;3.1.2&lt;/strong&gt;        &lt;strong&gt;sobel算子的计算过程&lt;/strong&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;我们假设被作用图像为 I.然后进行如下的操作：&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;1.分别在x和y两个方向求导。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Ⅰ.水平变化:将 I 与一个奇数大小的内核        &lt;img alt="" height="18" src="https://img-blog.csdn.net/20140511213402078?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="25"&gt;&lt;/img&gt;进行卷积。比如，当内核大小为3时,        &lt;img alt="" height="18" src="https://img-blog.csdn.net/20140511213402078?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="25"&gt;&lt;/img&gt;的计算结果为:&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" height="99" src="https://img-blog.csdn.net/20140511213501265?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="246"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;Ⅱ.垂直变化:将: I 与一个奇数大小的内核        &lt;img alt="" height="18" src="https://img-blog.csdn.net/20140511213636203?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="25"&gt;&lt;/img&gt;进行卷积。比如，当内核大小为3时,        &lt;img alt="" height="16" src="https://img-blog.csdn.net/20140511213402078?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="22"&gt;&lt;/img&gt;的计算结果为:&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" height="90" src="https://img-blog.csdn.net/20140511213743031?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="267"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;2.在图像的每一点，结合以上两个结果求出近似梯度:&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;img alt="" height="45" src="https://img-blog.csdn.net/20140511214009500?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="166"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;另外有时，也可用下面更简单公式代替:&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;blockquote&gt;            &lt;blockquote&gt;              &lt;blockquote&gt;                &lt;blockquote&gt;                  &lt;blockquote&gt;                    &lt;blockquote&gt;                      &lt;blockquote&gt;                        &lt;p&gt;                           &lt;img alt="" height="26" src="https://img-blog.csdn.net/20140511214037500?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="165"&gt;&lt;/img&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;3.2OpenCV中Sobel函数详解&lt;/h2&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Sobel函数使用扩展的 Sobel 算子，来计算一阶、二阶、三阶或混合图像差分。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;C++: void Sobel (
InputArray src,//输入图
 OutputArray dst,//输出图
 int ddepth,//输出图像的深度
 int dx,
 int dy,
 int ksize=3,
 double scale=1,
 double delta=0,
 int borderType=BORDER_DEFAULT );&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;br /&gt;      &lt;p&gt;&lt;/p&gt;      &lt;ul&gt;        &lt;li&gt;第一个参数，InputArray 类型的src，为输入图像，填Mat类型即可。&lt;/li&gt;        &lt;li&gt;第二个参数，OutputArray类型的dst，即目标图像，函数的输出参数，需要和源图片有一样的尺寸和类型。&lt;/li&gt;        &lt;li&gt;第三个参数，int类型的ddepth，输出图像的深度，支持如下src.depth()和ddepth的组合：&lt;/li&gt;&lt;/ul&gt;      &lt;ul&gt;        &lt;li&gt;          &lt;ul&gt;            &lt;li&gt;若src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F&lt;/li&gt;            &lt;li&gt;若src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F&lt;/li&gt;            &lt;li&gt;若src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F&lt;/li&gt;            &lt;li&gt;若src.depth() = CV_64F, 取ddepth = -1/CV_64F&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;      &lt;ul&gt;        &lt;li&gt;第四个参数，int类型dx，x 方向上的差分阶数。&lt;/li&gt;        &lt;li&gt;第五个参数，int类型dy，y方向上的差分阶数。&lt;/li&gt;        &lt;li&gt;第六个参数，int类型ksize，有默认值3，表示Sobel核的大小;必须取1，3，5或7。&lt;/li&gt;        &lt;li&gt;第七个参数，double类型的scale，计算导数值时可选的缩放因子，默认值是1，表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍，来得到这个参数的更多信息。&lt;/li&gt;        &lt;li&gt;第八个参数，double类型的delta，表示在结果存入目标图（第二个参数dst）之前可选的delta值，有默认值0。&lt;/li&gt;        &lt;li&gt;第九个参数， int类型的borderType，我们的老朋友了（万年是最后一个参数），边界模式，默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。&lt;/li&gt;&lt;/ul&gt;      &lt;p&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;一般情况下，都是用ksize x ksize内核来计算导数的。然而，有一种特殊情况——当ksize为1时，往往会使用3 x 1或者1 x 3的内核。且这种情况下，并没有进行高斯平滑操作。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;一些补充说明：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;1.当内核大小为 3 时, 我们的Sobel内核可能产生比较明显的误差(毕竟，Sobel算子只是求取了导数的近似值而已)。 为解决这一问题，OpenCV提供了Scharr 函数，但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快，但结果却更加精确，其内核是这样的:&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" height="82" src="https://img-blog.csdn.net/20140511214422468?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="433"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;2.因为Sobel算子结合了高斯平滑和分化（differentiation），因此结果会具有更多的抗噪性。大多数情况下，我们使用sobel函数时，取【xorder = 1，yorder = 0，ksize = 3】来计算图像X方向的导数，【xorder = 0，yorder = 1，ksize = 3】来计算图像y方向的导数。&lt;/p&gt;      &lt;p&gt;计算图像X方向的导数，取【xorder= 1，yorder = 0，ksize = 3】情况对应的内核：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;           &lt;img alt="" height="90" src="https://img-blog.csdn.net/20140511214539484?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="114"&gt;&lt;/img&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;而计算图像Y方向的导数，取【xorder= 0，yorder = 1，ksize = 3】对应的内核：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;blockquote&gt;            &lt;blockquote&gt;              &lt;blockquote&gt;                &lt;blockquote&gt;                  &lt;p&gt;                    &lt;img alt="" height="90" src="https://img-blog.csdn.net/20140511214630593?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="140"&gt;&lt;/img&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;3.3调用Sobel函数的实例代码&lt;/h2&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;调用Sobel函数的实例代码如下。这里只是教大家如何使用Sobel函数，就没有先用一句cvtColor将原图;转化为灰度图，而是直接用彩色图操作。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;//-----------------------------------【头文件包含部分】---------------------------------------
//            描述：包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include &amp;lt;opencv2/opencv.hpp&amp;gt;
#include&amp;lt;opencv2/highgui/highgui.hpp&amp;gt;
#include&amp;lt;opencv2/imgproc/imgproc.hpp&amp;gt;

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述：包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//            描述：控制台应用程序的入口函数，我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//【0】创建 grad_x 和 grad_y 矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y,dst;

	//【1】载入原始图  
	Mat src = imread(&amp;quot;1.jpg&amp;quot;);  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow(&amp;quot;【原始图】sobel边缘检测&amp;quot;, src); 

	//【3】求 X方向梯度
	Sobel( src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT );
	convertScaleAbs( grad_x, abs_grad_x );
	imshow(&amp;quot;【效果图】 X方向Sobel&amp;quot;, abs_grad_x); 

	//【4】求Y方向梯度
	Sobel( src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT );
	convertScaleAbs( grad_y, abs_grad_y );
	imshow(&amp;quot;【效果图】Y方向Sobel&amp;quot;, abs_grad_y); 

	//【5】合并梯度(近似)
	addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst );
	imshow(&amp;quot;【效果图】整体方向Sobel&amp;quot;, dst); 

	waitKey(0); 
	return 0; 
}&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;运行截图如下：&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511215117796?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511215153093?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h1&gt;四、Laplace算子篇&lt;/h1&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;4.1Laplace算子相关理论与概念讲解&lt;/h2&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;Laplacian 算子是n维欧几里德空间中的一个二阶微分算子，定义为梯度grad（）的散度div（）。因此如果f是二阶可微的实函数，则f的拉普拉斯算子定义为：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;strong&gt;(1)&lt;/strong&gt;f的拉普拉斯算子也是笛卡儿坐标系xi中的所有非混合二阶偏导数求和：&lt;/p&gt;      &lt;p&gt;        &lt;strong&gt;(2)&lt;/strong&gt;作为一个二阶微分算子，拉普拉斯算子把C函数映射到C函数，对于k ≥ 2。表达式(1)（或(2)）定义了一个算子Δ :C(R) → C(R)，或更一般地，定义了一个算子Δ : C(Ω) → C(Ω)，对于任何开集Ω。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;根据图像处理的原理我们知道，二阶导数可以用来进行检测边缘 。 因为图像是 “二维”, 我们需要在两个方向进行求导。使用Laplacian算子将会使求导过程变得简单。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;Laplacian 算子的定义:&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;blockquote&gt;            &lt;blockquote&gt;              &lt;blockquote&gt;                &lt;blockquote&gt;                  &lt;blockquote&gt;                    &lt;p&gt;                       &lt;img alt="" height="51" src="https://img-blog.csdn.net/20140511215358843?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="222"&gt;&lt;/img&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;需要点破的是，由于 Laplacian使用了图像梯度，它内部的代码其实是调用了 Sobel 算子的。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;另附一个小tips：让一幅图像减去它的Laplacian可以增强对比度。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;关于Laplace算子的相关概念阐述，可以参看这篇博文：&lt;/p&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;          &lt;a href="http://www.cnblogs.com/xfzhang/archive/2011/01/19/1939020.html" rel="nofollow"&gt;http://www.cnblogs.com/xfzhang/archive/2011/01/19/1939020.html&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;Laplace算子的wikipedia：&lt;/p&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt;          &lt;a href="http://zh.wikipedia.org/wiki/%E6%8B%89%E6%99%AE%E6%8B%89%E6%96%AF%E7%AE%97%E5%AD%90" rel="nofollow"&gt;http://zh.wikipedia.org/wiki/%E6%8B%89%E6%99%AE%E6%8B%89%E6%96%AF%E7%AE%97%E5%AD%90&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;      &lt;blockquote&gt;        &lt;p&gt; &lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h2&gt;4.2OpenCV中Laplacian函数详解&lt;/h2&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;C++: void Laplacian(InputArray src,OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0, intborderType=BORDER_DEFAULT );&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;ul&gt;        &lt;li&gt;第一个参数，InputArray类型的image，输入图像，即源图像，填Mat类的对象即可，且需为单通道8位图像。&lt;/li&gt;        &lt;li&gt;第二个参数，OutputArray类型的edges，输出的边缘图，需要和源图片有一样的尺寸和通道数。&lt;/li&gt;        &lt;li&gt;第三个参数，int类型的ddept，目标图像的深度。&lt;/li&gt;        &lt;li&gt;第四个参数，int类型的ksize，用于计算二阶导数的滤波器的孔径尺寸，大小必须为正奇数，且有默认值1。&lt;/li&gt;        &lt;li&gt;第五个参数，double类型的scale，计算拉普拉斯值的时候可选的比例因子，有默认值1。&lt;/li&gt;        &lt;li&gt;第六个参数，double类型的delta，表示在结果存入目标图（第二个参数dst）之前可选的delta值，有默认值0。&lt;/li&gt;        &lt;li&gt;第七个参数， int类型的borderType，边界模式，默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate()处得到更详细的信息。&lt;/li&gt;&lt;/ul&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Laplacian( )函数其实主要是利用sobel算子的运算。它通过加上sobel算子运算出的图像x方向和y方向上的导数，来得到我们载入图像的拉普拉斯变换结果。&lt;/p&gt;      &lt;p&gt;其中，sobel算子（ksize&amp;gt;1）如下：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;blockquote&gt;            &lt;blockquote&gt;              &lt;blockquote&gt;                &lt;blockquote&gt;                  &lt;p&gt;                    &lt;img alt="" height="59" src="https://img-blog.csdn.net/20140511215812515?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="325"&gt;&lt;/img&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;而当ksize=1时，Laplacian()函数采用以下3x3的孔径：&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;blockquote&gt;        &lt;blockquote&gt;          &lt;blockquote&gt;            &lt;blockquote&gt;              &lt;blockquote&gt;                &lt;blockquote&gt;                  &lt;blockquote&gt;                    &lt;blockquote&gt;                      &lt;p&gt;                         &lt;img alt="" height="108" src="https://img-blog.csdn.net/20140511215703203?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="130"&gt;&lt;/img&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;h2&gt;4.3调用Laplacian函数的实例代码&lt;/h2&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;让我们看一看调用实例：&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;//-----------------------------------【头文件包含部分】---------------------------------------
//            描述：包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include &amp;lt;opencv2/opencv.hpp&amp;gt;
#include&amp;lt;opencv2/highgui/highgui.hpp&amp;gt;
#include&amp;lt;opencv2/imgproc/imgproc.hpp&amp;gt;

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述：包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;


//-----------------------------------【main( )函数】--------------------------------------------
//            描述：控制台应用程序的入口函数，我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//【0】变量的定义
	Mat src,src_gray,dst, abs_dst;

	//【1】载入原始图  
	src = imread(&amp;quot;1.jpg&amp;quot;);  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow(&amp;quot;【原始图】图像Laplace变换&amp;quot;, src); 

	//【3】使用高斯滤波消除噪声
	GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );

	//【4】转换为灰度图
	cvtColor( src, src_gray, CV_RGB2GRAY );

	//【5】使用Laplace函数
	Laplacian( src_gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT );

	//【6】计算绝对值，并将结果转换成8位
	convertScaleAbs( dst, abs_dst );

	//【7】显示效果图
	imshow( &amp;quot;【效果图】图像Laplace变换&amp;quot;, abs_dst );

	waitKey(0); 

	return 0; 
}&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;示例效果图：&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511220044843?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h1&gt;五、scharr滤波器篇&lt;/h1&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;scharr一般我就直接称它为滤波器，而不是算子。上文我们已经讲到，它在OpenCV中主要是配合Sobel算子的运算而存在的,一个万年备胎。让我们直接来看看函数讲解吧。&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;h2&gt;5.1OpenCV中Scharr函数详解&lt;/h2&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;使用Scharr滤波器运算符计算x或y方向的图像差分。其实它的参数变量和Sobel基本上是一样的，除了没有ksize核的大小。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;C++: void Scharr(
InputArray src, //源图
 OutputArray dst, //目标图
 int ddepth,//图像深度
 int dx,// x方向上的差分阶数
 int dy,//y方向上的差分阶数
 double scale=1,//缩放因子
 double delta=0,// delta值
 intborderType=BORDER_DEFAULT )// 边界模式&lt;/code&gt;&lt;/pre&gt;      &lt;br /&gt;      &lt;br /&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;ul&gt;        &lt;li&gt;第一个参数，InputArray 类型的src，为输入图像，填Mat类型即可。&lt;/li&gt;        &lt;li&gt;第二个参数，OutputArray类型的dst，即目标图像，函数的输出参数，需要和源图片有一样的尺寸和类型。&lt;/li&gt;        &lt;li&gt;第三个参数，int类型的ddepth，输出图像的深度，支持如下src.depth()和ddepth的组合：&lt;/li&gt;&lt;/ul&gt;      &lt;ul&gt;        &lt;li&gt;          &lt;ul&gt;            &lt;li&gt;若src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F&lt;/li&gt;            &lt;li&gt;若src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F&lt;/li&gt;            &lt;li&gt;若src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F&lt;/li&gt;            &lt;li&gt;若src.depth() = CV_64F, 取ddepth = -1/CV_64F&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;      &lt;ul&gt;        &lt;li&gt;第四个参数，int类型dx，x方向上的差分阶数。&lt;/li&gt;        &lt;li&gt;第五个参数，int类型dy，y方向上的差分阶数。&lt;/li&gt;        &lt;li&gt;第六个参数，double类型的scale，计算导数值时可选的缩放因子，默认值是1，表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍，来得到这个参数的更多信息。&lt;/li&gt;        &lt;li&gt;第七个参数，double类型的delta，表示在结果存入目标图（第二个参数dst）之前可选的delta值，有默认值0。&lt;/li&gt;        &lt;li&gt;第八个参数， int类型的borderType，我们的老朋友了（万年是最后一个参数），边界模式，默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。&lt;/li&gt;&lt;/ul&gt;      &lt;p&gt;&lt;/p&gt;&lt;/blockquote&gt;    &lt;blockquote&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;不难理解，如下两者是等价的：&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;Scharr(src, dst, ddepth, dx, dy, scale,delta, borderType);&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;与&lt;/p&gt;      &lt;p&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;Sobel(src, dst, ddepth, dx, dy, CV_SCHARR,scale, delta, borderType);&lt;/code&gt;&lt;/pre&gt;      &lt;br /&gt;      &lt;br /&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;h2&gt;5.2调用Scharr函数的实例代码&lt;/h2&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;//-----------------------------------【头文件包含部分】---------------------------------------
//            描述：包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include &amp;lt;opencv2/opencv.hpp&amp;gt;
#include&amp;lt;opencv2/highgui/highgui.hpp&amp;gt;
#include&amp;lt;opencv2/imgproc/imgproc.hpp&amp;gt;

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述：包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//            描述：控制台应用程序的入口函数，我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//【0】创建 grad_x 和 grad_y 矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y,dst;

	//【1】载入原始图  
	Mat src = imread(&amp;quot;1.jpg&amp;quot;);  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow(&amp;quot;【原始图】Scharr滤波器&amp;quot;, src); 

	//【3】求 X方向梯度
	Scharr( src, grad_x, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( grad_x, abs_grad_x );
	imshow(&amp;quot;【效果图】 X方向Scharr&amp;quot;, abs_grad_x); 

	//【4】求Y方向梯度
	Scharr( src, grad_y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( grad_y, abs_grad_y );
	imshow(&amp;quot;【效果图】Y方向Scharr&amp;quot;, abs_grad_y); 

	//【5】合并梯度(近似)
	addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst );

	//【6】显示效果图
	imshow(&amp;quot;【效果图】合并梯度后Scharr&amp;quot;, dst); 

	waitKey(0); 
	return 0; 
}&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;运行效果图：&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511220429046?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511220444343?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511220459812?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511220541718?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;h1&gt;六、综合示例篇——在实战中熟稔&lt;/h1&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;依然是每篇文章都会配给大家的一个详细注释的博文配套示例程序，把这篇文章中介绍的知识点以代码为载体，展现给大家。&lt;/p&gt;      &lt;p&gt;这个示例程序中，分别演示了canny边缘检测，sobel边缘检测，scharr滤波器的使用，那么，上详细注释的代码吧：&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;pre&gt;        &lt;code&gt;//-----------------------------------【程序说明】----------------------------------------------
//		程序名称:：《【OpenCV入门教程之十二】OpenCV边缘检测：Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑合辑》 博文配套源码 
//		开发所用IDE版本：Visual Studio 2010
//开发所用OpenCV版本：	2.4.9
//		2014年5月11日 Create by 浅墨
//		浅墨的微博：@浅墨_毛星云 http://weibo.com/1723155442/profile?topnav=1&amp;amp;wvr=5&amp;amp;user=1
//		浅墨的知乎：http://www.zhihu.com/people/mao-xing-yun
//		浅墨的豆瓣：http://www.douban.com/people/53426472/
//----------------------------------------------------------------------------------------------



//-----------------------------------【头文件包含部分】---------------------------------------
//		描述：包含程序所依赖的头文件
//---------------------------------------------------------------------------------------------- 
#include &amp;lt;opencv2/highgui/highgui.hpp&amp;gt;
#include &amp;lt;opencv2/imgproc/imgproc.hpp&amp;gt;

//-----------------------------------【命名空间声明部分】--------------------------------------
//		描述：包含程序所使用的命名空间
//----------------------------------------------------------------------------------------------- 
using namespace cv;


//-----------------------------------【全局变量声明部分】--------------------------------------
//		描述：全局变量声明
//-----------------------------------------------------------------------------------------------
//原图，原图的灰度版，目标图
Mat g_srcImage, g_srcGrayImage,g_dstImage;

//Canny边缘检测相关变量
Mat g_cannyDetectedEdges;
int g_cannyLowThreshold=1;//TrackBar位置参数  

//Sobel边缘检测相关变量
Mat g_sobelGradient_X, g_sobelGradient_Y;
Mat g_sobelAbsGradient_X, g_sobelAbsGradient_Y;
int g_sobelKernelSize=1;//TrackBar位置参数  

//Scharr滤波器相关变量
Mat g_scharrGradient_X, g_scharrGradient_Y;
Mat g_scharrAbsGradient_X, g_scharrAbsGradient_Y;


//-----------------------------------【全局函数声明部分】--------------------------------------
//		描述：全局函数声明
//-----------------------------------------------------------------------------------------------
static void ShowHelpText( );
static void on_Canny(int, void*);//Canny边缘检测窗口滚动条的回调函数
static void on_Sobel(int, void*);//Sobel边缘检测窗口滚动条的回调函数
void Scharr( );//封装了Scharr边缘检测相关代码的函数


//-----------------------------------【main( )函数】--------------------------------------------
//		描述：控制台应用程序的入口函数，我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( int argc, char** argv )
{
	//改变console字体颜色
	system(&amp;quot;color 2F&amp;quot;);  

	//显示欢迎语
	ShowHelpText();

	//载入原图
	g_srcImage = imread(&amp;quot;1.jpg&amp;quot;);
	if( !g_srcImage.data ) { printf(&amp;quot;Oh，no，读取srcImage错误~！ \n&amp;quot;); return false; }

	//显示原始图
	namedWindow(&amp;quot;【原始图】&amp;quot;);
	imshow(&amp;quot;【原始图】&amp;quot;, g_srcImage);

	// 创建与src同类型和大小的矩阵(dst)
	g_dstImage.create( g_srcImage.size(), g_srcImage.type() );

	// 将原图像转换为灰度图像
	cvtColor( g_srcImage, g_srcGrayImage, CV_BGR2GRAY );

	// 创建显示窗口
	namedWindow( &amp;quot;【效果图】Canny边缘检测&amp;quot;, CV_WINDOW_AUTOSIZE );
	namedWindow( &amp;quot;【效果图】Sobel边缘检测&amp;quot;, CV_WINDOW_AUTOSIZE );

	// 创建trackbar
	createTrackbar( &amp;quot;参数值：&amp;quot;, &amp;quot;【效果图】Canny边缘检测&amp;quot;, &amp;amp;g_cannyLowThreshold, 120, on_Canny );
	createTrackbar( &amp;quot;参数值：&amp;quot;, &amp;quot;【效果图】Sobel边缘检测&amp;quot;, &amp;amp;g_sobelKernelSize, 3, on_Sobel );

	// 调用回调函数
	on_Canny(0, 0);
	on_Sobel(0, 0);

	//调用封装了Scharr边缘检测代码的函数
	Scharr( );

	//轮询获取按键信息，若按下Q，程序退出
	while((char(waitKey(1)) != &amp;apos;q&amp;apos;)) {}

	return 0;
}


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		描述：输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
	//输出一些帮助信息
	printf( &amp;quot;\n\n\t嗯。运行成功，请调整滚动条观察图像效果~\n\n&amp;quot;
		&amp;quot;\t按下“q”键时，程序退出~!\n&amp;quot;
		&amp;quot;\n\n\t\t\t\t by浅墨&amp;quot;	);
}


//-----------------------------------【on_Canny( )函数】----------------------------------
//		描述：Canny边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------------
void on_Canny(int, void*)
{
	// 先使用 3x3内核来降噪
	blur( g_srcGrayImage, g_cannyDetectedEdges, Size(3,3) );

	// 运行我们的Canny算子
	Canny( g_cannyDetectedEdges, g_cannyDetectedEdges, g_cannyLowThreshold, g_cannyLowThreshold*3, 3 );

	//先将g_dstImage内的所有元素设置为0 
	g_dstImage = Scalar::all(0);

	//使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码，来将原图g_srcImage拷到目标图g_dstImage中
	g_srcImage.copyTo( g_dstImage, g_cannyDetectedEdges);

	//显示效果图
	imshow( &amp;quot;【效果图】Canny边缘检测&amp;quot;, g_dstImage );
}



//-----------------------------------【on_Sobel( )函数】----------------------------------
//		描述：Sobel边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------
void on_Sobel(int, void*)
{
	// 求 X方向梯度
	Sobel( g_srcImage, g_sobelGradient_X, CV_16S, 1, 0, (2*g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT );
	convertScaleAbs( g_sobelGradient_X, g_sobelAbsGradient_X );//计算绝对值，并将结果转换成8位

	// 求Y方向梯度
	Sobel( g_srcImage, g_sobelGradient_Y, CV_16S, 0, 1, (2*g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT );
	convertScaleAbs( g_sobelGradient_Y, g_sobelAbsGradient_Y );//计算绝对值，并将结果转换成8位

	// 合并梯度
	addWeighted( g_sobelAbsGradient_X, 0.5, g_sobelAbsGradient_Y, 0.5, 0, g_dstImage );

	//显示效果图
	imshow(&amp;quot;【效果图】Sobel边缘检测&amp;quot;, g_dstImage); 

}


//-----------------------------------【Scharr( )函数】----------------------------------
//		描述：封装了Scharr边缘检测相关代码的函数
//-----------------------------------------------------------------------------------------
void Scharr( )
{
	// 求 X方向梯度
	Scharr( g_srcImage, g_scharrGradient_X, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( g_scharrGradient_X, g_scharrAbsGradient_X );//计算绝对值，并将结果转换成8位

	// 求Y方向梯度
	Scharr( g_srcImage, g_scharrGradient_Y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( g_scharrGradient_Y, g_scharrAbsGradient_Y );//计算绝对值，并将结果转换成8位

	// 合并梯度
	addWeighted( g_scharrAbsGradient_X, 0.5, g_scharrAbsGradient_Y, 0.5, 0, g_dstImage );

	//显示效果图
	imshow(&amp;quot;【效果图】Scharr滤波器&amp;quot;, g_dstImage); 
}&lt;/code&gt;&lt;/pre&gt;      &lt;p&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;放出一些运行效果图：&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511220858375?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt;canny边缘检测效果图：&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511220918234?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511220954781?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Sobel边缘检测：&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511221046312?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;Scharr滤波器：&lt;/p&gt;      &lt;p&gt;        &lt;img alt="" src="https://img-blog.csdn.net/20140511221130640?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;        &lt;br /&gt;&lt;/p&gt;      &lt;p&gt;好的，就放出这些效果图吧，具体更多的运行效果大家就自己下载示例程序回去玩~&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;本篇文章的配套源代码请点击这里下载：&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;        &lt;a href="http://download.csdn.net/detail/zhmxy555/7332581" rel="nofollow"&gt;【浅墨OpenCV入门教程之十二】配套源代码下载&lt;/a&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt;OK，今天的内容大概就是这些，我们下篇文章见：）&lt;/p&gt;      &lt;p&gt;         &lt;img alt="" src="https://img-blog.csdn.net/20140511221331687?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcG9lbV9xaWFubW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"&gt;&lt;/img&gt;&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/59310-opencv-%E7%A8%8B%E4%B9%8B-%E5%8D%81%E4%BA%8C</guid>
      <pubDate>Mon, 25 Feb 2019 11:18:58 CST</pubDate>
    </item>
    <item>
      <title>个人对全栈工程师和JVM编程语言的一些理解</title>
      <link>https://itindex.net/detail/59306-%E4%B8%AA%E4%BA%BA-%E5%B7%A5%E7%A8%8B%E5%B8%88-jvm</link>
      <description>&lt;p&gt;全栈工程师、JVM生态。&lt;/p&gt;
 &lt;p&gt;我个人对全栈工程师和JVM编程语言的一些理解，大家有不同看法的，我们一起交流梳理下。&lt;/p&gt;
 &lt;p&gt;全栈工程师从我的视角来看，是熟练使用多种编程语言，满足前后端各种业务、技术需求的服务和应用开发。因为语言都有自己的特点、独特性和应用领域，没有能解决所有问题的最佳语言，所以未来混合编程会很普遍，这对开发者也提出了更高要求，需要了解、掌握和使用多种语言。&lt;/p&gt;
 &lt;p&gt;但目前主流观点还是以JavaScript/TypeScript/Node.js为主流的全栈工程师。&lt;/p&gt;
 &lt;p&gt;Java也可简单理解为Android + Server的全栈语言，一个优秀Java工程师可同时完成Android和Server的开发。&lt;/p&gt;
 &lt;p&gt;当然，其它语言也有类似的全栈功能，如：C++。&lt;/p&gt;
 &lt;p&gt;JVM是个巨大生态系统，服务端语言除了Java，还有：Scala、Clojure和Kotlin。&lt;/p&gt;
 &lt;p&gt;Kotlin目前主要还是用于Android开发。&lt;/p&gt;
 &lt;p&gt;Scala是面向对象和函数式语言的混合，因为Apache Spark和Kafka也日益流行，但学习曲线很高。&lt;/p&gt;
 &lt;p&gt;Clojure是JVM上的Lisp，这三门语言的定位可简单描述为：&lt;/p&gt;
 &lt;p&gt;Clojure is about Data, Scala is about Types, Java is about Objects.&lt;/p&gt;
 &lt;p&gt;这三门语言的应用领域是服务端开发，Apache基金会大部分项目是Java项目，&lt;/p&gt;
 &lt;p&gt;若自己定位大数据工程师，是需要在Hadoop和Spark上有深度历练的。&lt;/p&gt;
 &lt;p&gt;一些思考：&lt;/p&gt;
 &lt;p&gt;1、若专注服务端开发，可选择JVM阵营，因为JVM的强项就是服务端；  &lt;br /&gt;
2、除JVM外，Python、PHP、Go也都是Web服务端语言的选择；  &lt;br /&gt;
3、此外，Python是数据科学和人工智能的首选语言；  &lt;br /&gt;
4、全栈开发基本是JavaScript系的天下，从Web、App到Server；  &lt;br /&gt;
5、Java仍是目前市场主流，不是因为它很好，主要是市场决定。更好用的Scala和Clojure是小众语言，生产力更高，若坚持使用并努力提升，机会也很大，毕竟函数式语言是未来，现在主流语言都或多或少在融合函数式编程；  &lt;br /&gt;
6、以生态系统的方式去思考一种编程语言，GitHub Awesome 是个很好的开始。&lt;/p&gt;
 &lt;p&gt;以下语言链接，可拓展思路和关注点：  &lt;br /&gt;
  &lt;a href="https://github.com/sindresorhus/awesome-nodejs" rel="noopener" target="_blank"&gt;Awesome Node.js&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/vinta/awesome-python" rel="noopener" target="_blank"&gt;Awesome Python&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/ziadoz/awesome-php" rel="noopener" target="_blank"&gt;Awesome PHP&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/avelino/awesome-go" rel="noopener" target="_blank"&gt;Awesome Go&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/akullpp/awesome-java" rel="noopener" target="_blank"&gt;Awesome Java&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/KotlinBy/awesome-kotlin" rel="noopener" target="_blank"&gt;Awesome Kotlin&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/lauris/awesome-scala" rel="noopener" target="_blank"&gt;Awesome Scala&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/razum2um/awesome-clojure" rel="noopener" target="_blank"&gt;Awesome Clojure&lt;/a&gt;  &lt;br /&gt;
  &lt;a href="https://github.com/fffaraz/awesome-cpp" rel="noopener" target="_blank"&gt;Awesome C++&lt;/a&gt;
&lt;/p&gt; &lt;p&gt;  &lt;a href="http://www.bshare.cn/share?url=http%3A%2F%2Fblog.huihoo.com%2F%3Fp%3D1416&amp;title=%E4%B8%AA%E4%BA%BA%E5%AF%B9%E5%85%A8%E6%A0%88%E5%B7%A5%E7%A8%8B%E5%B8%88%E5%92%8CJVM%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%9A%84%E4%B8%80%E4%BA%9B%E7%90%86%E8%A7%A3" title="&amp;#29992;bShare&amp;#20998;&amp;#20139;&amp;#25110;&amp;#25910;&amp;#34255;&amp;#26412;&amp;#25991;"&gt;   &lt;img alt="&amp;#29992;bShare&amp;#20998;&amp;#20139;&amp;#25110;&amp;#25910;&amp;#34255;&amp;#26412;&amp;#25991;" src="http://static.bshare.cn/frame/images/button_custom1-zh.gif"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;br /&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>Java</category>
      <guid isPermaLink="true">https://itindex.net/detail/59306-%E4%B8%AA%E4%BA%BA-%E5%B7%A5%E7%A8%8B%E5%B8%88-jvm</guid>
      <pubDate>Sat, 23 Feb 2019 22:02:56 CST</pubDate>
    </item>
    <item>
      <title>防雪崩利器：熔断器 Hystrix 的原理与使用 - 编程随笔 - SegmentFault 思否</title>
      <link>https://itindex.net/detail/58605-%E9%9B%AA%E5%B4%A9-%E7%86%94%E6%96%AD%E5%99%A8-hystrix</link>
      <description>&lt;div&gt;    &lt;h2&gt;前言&lt;/h2&gt;    &lt;p&gt;分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况, 这种现象被称为服务雪崩效应. 为了应对服务雪崩, 一种常见的做法是手动服务降级. 而Hystrix的出现,给我们提供了另一种选择.&lt;/p&gt;    &lt;h2&gt;服务雪崩效应的定义&lt;/h2&gt;    &lt;p&gt;服务雪崩效应是一种因      &lt;strong&gt;服务提供者&lt;/strong&gt;的不可用导致      &lt;strong&gt;服务调用者&lt;/strong&gt;的不可用,并将不可用      &lt;strong&gt;逐渐放大&lt;/strong&gt;的过程.如果所示:&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;上图中, A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了.&lt;/p&gt;    &lt;h2&gt;服务雪崩效应形成的原因&lt;/h2&gt;    &lt;p&gt;我把服务雪崩的参与者简化为      &lt;strong&gt;服务提供者&lt;/strong&gt;和      &lt;strong&gt;服务调用者&lt;/strong&gt;, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;        &lt;p&gt;服务提供者不可用&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;重试加大流量&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;服务调用者不可用&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;服务雪崩的每个阶段都可能由不同的原因造成, 比如造成      &lt;strong&gt;服务不可用&lt;/strong&gt;的原因有:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;硬件故障&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;程序Bug&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;缓存击穿&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;用户大量请求&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问.      &lt;br /&gt;缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.      &lt;br /&gt;在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.&lt;/p&gt;    &lt;p&gt;而形成      &lt;strong&gt;重试加大流量&lt;/strong&gt;的原因有:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;用户重试&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;代码逻辑重试&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.      &lt;br /&gt;服务调用端的会存在大量服务异常后的重试逻辑.      &lt;br /&gt;这些重试都会进一步加大请求流量.&lt;/p&gt;    &lt;p&gt;最后,      &lt;strong&gt;服务调用者不可用&lt;/strong&gt;产生的主要原因是:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;同步等待造成的资源耗尽&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;当服务调用者使用      &lt;strong&gt;同步调用&lt;/strong&gt;时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.&lt;/p&gt;    &lt;h2&gt;服务雪崩的应对策略&lt;/h2&gt;    &lt;p&gt;针对造成服务雪崩的不同原因, 可以使用不同的应对策略:&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;        &lt;p&gt;流量控制&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;改进缓存模式&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;服务自动扩容&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;服务调用者降级服务&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;      &lt;strong&gt;流量控制&lt;/strong&gt;的具体措施包括:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;网关限流&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;用户交互限流&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;关闭重试&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也越来越热门.&lt;/p&gt;    &lt;p&gt;用户交互限流的具体措施有: 1. 采用加载动画,提高用户的忍耐等待时间.  2. 提交按钮添加强制等待时间机制.&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;改进缓存模式&lt;/strong&gt;的措施包括:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;缓存预加载&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;同步改为异步刷新&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;      &lt;strong&gt;服务自动扩容&lt;/strong&gt;的措施主要有:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;AWS的auto scaling&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;      &lt;strong&gt;服务调用者降级服务&lt;/strong&gt;的措施包括:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;资源隔离&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;对依赖服务进行分类&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;不可用服务的调用快速失败&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;p&gt;资源隔离主要是对调用服务的线程池进行隔离.&lt;/p&gt;    &lt;p&gt;我们根据具体业务,将依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止.&lt;/p&gt;    &lt;p&gt;不可用服务的调用快速失败一般通过      &lt;strong&gt;超时机制&lt;/strong&gt;,      &lt;strong&gt;熔断器&lt;/strong&gt;和熔断后的      &lt;strong&gt;降级方法&lt;/strong&gt;来实现.&lt;/p&gt;    &lt;h2&gt;使用Hystrix预防服务雪崩&lt;/h2&gt;    &lt;p&gt;      &lt;strong&gt;Hystrix&lt;/strong&gt;[hɪst&amp;apos;rɪks]的中文含义是豪猪, 因其背上长满了刺,而拥有自我保护能力. Netflix的      &lt;strong&gt;Hystrix&lt;/strong&gt;是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力.&lt;/p&gt;    &lt;p&gt;Hystrix的设计原则包括:&lt;/p&gt;    &lt;ul&gt;      &lt;li&gt;        &lt;p&gt;资源隔离&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;熔断器&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;命令模式&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;    &lt;h4&gt;资源隔离&lt;/h4&gt;    &lt;p&gt;货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个, 如下图所示:&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;这种资源隔离减少风险的方式被称为:Bulkheads(舱壁隔离模式).      &lt;br /&gt;Hystrix将同样的模式运用到了服务调用者上.&lt;/p&gt;    &lt;p&gt;在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:      &lt;br /&gt;商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示:&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩. 如图所示:&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩.      &lt;br /&gt;如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h4&gt;熔断器模式&lt;/h4&gt;    &lt;p&gt;熔断器模式定义了熔断器开关相互转换的逻辑:&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;p&gt;服务的健康状况 =  请求失败数 / 请求总数.      &lt;br /&gt;熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;        &lt;p&gt;当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;当熔断器开关打开时, 请求被禁止通过.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.&lt;/p&gt;    &lt;h4&gt;命令模式&lt;/h4&gt;    &lt;p&gt;Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).      &lt;br /&gt;同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. 如下代码所示:&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;public class Service1HystrixCommand extends HystrixCommand&amp;lt;Response&amp;gt; {
  private Service1 service;
  private Request request;

  public Service1HystrixCommand(Service1 service, Request request){
    supper(
      Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(&amp;quot;ServiceGroup&amp;quot;))
          .andCommandKey(HystrixCommandKey.Factory.asKey(&amp;quot;servcie1query&amp;quot;))
          .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(&amp;quot;service1ThreadPool&amp;quot;))
          .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
            .withCoreSize(20))//服务线程池数量
          .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
            .withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
            .withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
      ))
      this.service = service;
      this.request = request;
    );
  }

  @Override
  protected Response run(){
    return service1.call(request);
  }

  @Override
  protected Response getFallback(){
    return Response.dummy();
  }
}&lt;/code&gt;&lt;/pre&gt;    &lt;p&gt;在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能.      &lt;br /&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h4&gt;Hystrix的内部处理逻辑&lt;/h4&gt;    &lt;p&gt;下图为Hystrix服务调用的内部逻辑:      &lt;br /&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;        &lt;p&gt;构建Hystrix的Command对象, 调用执行方法.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若服务执行成功, 返回正常结果.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若服务降级方法getFallback执行成功, 则返回降级结果.&lt;/p&gt;&lt;/li&gt;      &lt;li&gt;        &lt;p&gt;若服务降级方法getFallback执行失败, 则抛出异常.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;    &lt;h2&gt;Hystrix Metrics的实现&lt;/h2&gt;    &lt;p&gt;Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑. 因此Metrics的实现非常重要.&lt;/p&gt;    &lt;h4&gt;1.4之前的滑动窗口实现&lt;/h4&gt;    &lt;p&gt;Hystrix在这些版本中的使用自己定义的滑动窗口数据结构来记录当前时间窗的各种事件(成功,失败,超时,线程池拒绝等)的计数.      &lt;br /&gt;事件产生时, 数据结构根据当前时间确定使用旧桶还是创建新桶来计数, 并在桶中对计数器经行修改.      &lt;br /&gt;这些修改是多线程并发执行的, 代码中有不少加锁操作,逻辑较为复杂.&lt;/p&gt;    &lt;p&gt;      &lt;img alt="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;" src="https://static.segmentfault.com/v-5b6d47f5/global/img/squares.svg" title="&amp;#22270;&amp;#29255;&amp;#25551;&amp;#36848;"&gt;&lt;/img&gt;&lt;/p&gt;    &lt;h4&gt;1.5之后的滑动窗口实现&lt;/h4&gt;    &lt;p&gt;Hystrix在这些版本中开始使用RxJava的Observable.window()实现滑动窗口.      &lt;br /&gt;RxJava的window使用后台线程创建新桶, 避免了并发创建桶的问题.      &lt;br /&gt;同时RxJava的单线程无锁特性也保证了计数变更时的线程安全. 从而使代码更加简洁.      &lt;br /&gt;以下为我使用RxJava的window方法实现的一个简易滑动窗口Metrics, 短短几行代码便能完成统计功能,足以证明RxJava的强大:&lt;/p&gt;    &lt;pre&gt;      &lt;code&gt;@Test
public void timeWindowTest() throws Exception{
  Observable&amp;lt;Integer&amp;gt; source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -&amp;gt; RandomUtils.nextInt(2));
  source.window(1, TimeUnit.SECONDS).subscribe(window -&amp;gt; {
    int[] metrics = new int[2];
    window.subscribe(i -&amp;gt; metrics[i]++,
      InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
      () -&amp;gt; System.out.println(&amp;quot;窗口Metrics:&amp;quot; + JSON.toJSONString(metrics)));
  });
  TimeUnit.SECONDS.sleep(3);
}&lt;/code&gt;&lt;/pre&gt;    &lt;h2&gt;总结&lt;/h2&gt;    &lt;p&gt;通过使用Hystrix,我们能方便的防止雪崩效应, 同时使系统具有自动降级和自动恢复服务的效果.&lt;/p&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/58605-%E9%9B%AA%E5%B4%A9-%E7%86%94%E6%96%AD%E5%99%A8-hystrix</guid>
      <pubDate>Fri, 10 Aug 2018 20:02:22 CST</pubDate>
    </item>
    <item>
      <title>这可能是我见过的最好的编程指南</title>
      <link>https://itindex.net/detail/58496-%E6%88%91%E8%A7%81-%E6%9C%80%E5%A5%BD-%E7%BC%96%E7%A8%8B</link>
      <description>&lt;div&gt;    &lt;p&gt;从大一入学被调剂到计算机专业，到喜欢上这个专业，再到毕业拿到 10 多个 offer，最终进入理想的大厂工作。回想起来这些年确确实实踩了很多坑。      &lt;strong&gt;我刚开始学习编程的时候也想一口吃成一个胖子，想速成，但是有时候却是不尽人意。&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;回忆了下这几年学习编程的过程，整理了一些我自己认为很需要注意的几个方面，分享给大家。希望能让初学编程的你，少走一些弯路，      &lt;strong&gt;可能文章比较长，但我真心希望初学编程的你能够认真看完，至少，我认为如果我刚学编程的时候看到这篇文章，对我或多或少是有一些帮助的。&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;我个人是一名计算机专业的学生，很多人可能会认为我是在课堂上学到的编程，其实不是这样。&lt;/p&gt;    &lt;p&gt;我认为科班出身和非科班出身的学生最大的区别在于      &lt;strong&gt;科班出身的学生知道去学什么&lt;/strong&gt;，知道每一门课程是干什么的；还有一些必须完成的作业、小项目，促使他们去做一些实际的编码练习，除此之外，真的全靠自学。&lt;/p&gt;    &lt;p&gt;对于自学编程，我认为首先应该谈的是如何去避免一些坑，这样就可能节约大把的时间。下面我就以问题的形式来分享一些我认为重要的方面。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;        &lt;em&gt;1. 我应该选择什么编程语言&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;可能困扰编程新手最多的一个问题是【我应该学什么编程语言】或者【我需要学习哪些课程才能做出一个 web、一个 app】，很多人一直纠结这个问题，陷入了东学一点、西看一点的死循环，到头来啥也没学好，这会很浪费时间。&lt;/p&gt;    &lt;p&gt;刚上大一的时候，我也很想知道应该选择什么编程语言。我问了很多人，网上各种查资料，但所能得到的答案都很片面，多数对这个问题答非所问，总是回答说“某某编程语言难”，“某某编程语言性能好”。      &lt;strong&gt;其实作为初学者，我们对计算机体系都不了解，就不要过多地去纠结性能，或者难易等因素&lt;/strong&gt;，原因我等下再说。&lt;/p&gt;    &lt;p&gt;如果你有明确的方向，那么很好选择。      &lt;strong&gt;如果你想做算法、机器学习方向，那么 python 是最好的选择。如果你想做 web 开发，java、php 等都可以。如果想做一些更底层的工作，那么就可以选 c。&lt;/strong&gt;当然这是建立在你有明确方向的基础上。可是，很多人都没怎么接触过计算机行业，特别是和我一样刚入学就被调剂到计算机专业的人。对这些同学来说，各个编程语言就只是个名字，除了叫法不一样，你根本不知道它们有什么差别。所以索性不要纠结了，我替你选一个吧。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;如果你是在校大学生，那么你有大把连续的时间，就先学习 c，然后再学 c++。&lt;/strong&gt;我个人是学 c 入门的，也许很多人不理解我为什么推荐学 c，因为 c 和 c++ 都很难、很复杂，看起来并不适合入门。然而正是它们的难和复杂才能让你更好地理解计算机系统【      &lt;strong&gt;计算机系统不是指操作系统】&lt;/strong&gt;。      &lt;strong&gt;学习编程不是学习编程语言，而是学习一个计算机生态，即一个庞大的知识体系。&lt;/strong&gt;只会编程语言而不理解整个计算机的体系，就像只会写字而写不出好文章。了解 c/c++ 和了解计算机系统是极为贴合的，      &lt;strong&gt;        &lt;em&gt;向下&lt;/em&gt;&lt;/strong&gt;可以帮助你更容易地理解操作系统、编译原理、计算机网络、计算机组成原理，为什么呢？因为较为底层的东西很多都是用 c 实现的，和系统的贴合度极高，很多教材源码甚至教程，在讲述这些知识的时候都是用 c 或 c++ 作为媒介。而      &lt;strong&gt;        &lt;em&gt;向上&lt;/em&gt;&lt;/strong&gt;，c++ 面向对象的机制，也可以做出一些应用，譬如五子棋游戏等，也不会显得那么枯燥。花个小半年时间了解 c 和 c++，之后你就会觉得看书、看资料可以轻松很多。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;如果你是一个上班族，&lt;/strong&gt;但是刚刚学习编程，可能学 c 和 c++ 对你来说有些复杂和困难，因为学习它们确实是很需要时间。你们不像在校生那样有大把的连续时间，而零碎的时间去学习一个比较复杂的东西效果不见得有那么好，所以可以先学一些【更容易见效】的编程语言，      &lt;strong&gt;从 python 入手吧&lt;/strong&gt;，至少能快速做出一些小应用，不至于丢失了兴趣，但是真的要入门编程又还得看看与计算机系统相关的书籍，这样才能更深层次地去编程，譬如【深入理解计算机系统】这一本书可以读很多遍，这本书把整个计算机系统给串起来了。&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;        &lt;em&gt;2.学习编程，我需要学习哪些课程？&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;我要学哪些课程？我为什么要学习如高数、离散数学、线性代数、概率论等课程？&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;这个问题也是之前困扰了我很久的问题。不过我现在想通了，对于【高数、离散、线性代数、概率论】等课程，很好解释，做算法的同学肯定知道为啥要学习这些课程。      &lt;strong&gt;机器学习&lt;/strong&gt;中会大量用到上述提到的课程，所以会比较好理解。对在校生而言，学校开设的很多课程我们不知道为什么要学，我们很疑惑，不知道学它有什么用，这个时候我们就会很纠结，还会产生抵触情绪。这很正常，因为我们学习得不够深入，自然不能理解它们的用处。&lt;/p&gt;    &lt;p&gt;在我看来，      &lt;strong&gt;大学本科课程更多的是面向“面”的教学&lt;/strong&gt;，即什么课程都教给你一些，但是又讲得不那么深入；而      &lt;strong&gt;工作或者读研，更多的则是面向“点”的学习&lt;/strong&gt;，用到的知识更专。本科时，学校也不知道你以后是去搞算法、还是搞架构、还是搞服务器开发，甚至去搞硬件，所以学校需要你学很多课程，至少有个了解。对学生来说，一方面可以从中选择自己感兴趣的点；一方面也可以对未来的就业方向有些启发。所以即使像数电、模电等课程，虽然之后可能用不着，但是你也要学，并且会花费大量的时间。虽然你最后不一定去搞硬件，但是这些课程也会让你更容易去理解一些知识，比如 cpu 中的逻辑器件。&lt;/p&gt;    &lt;p&gt;如果你在大一的时候就有一个明确的定位，知道自己今后想从事哪方面的工作，课程与课程之间是可以调一下      &lt;strong&gt;优先级&lt;/strong&gt;的。不过像大学物理，这种课程确实是对编程没有帮助，但是像我前面所说的，大学教育更注重广度，大物等课程可能就是为了给你普及生活常识吧。&lt;/p&gt;    &lt;p&gt;其实，大学教育的问题是普遍存在的，我认为我们学习一项技能的时候，应该采取的是      &lt;strong&gt;项目驱动式学习&lt;/strong&gt;，即需要用到什么东西时不会了再去学，而不是先填鸭式的都填进脑子，并且在学习的过程中我们还不知道它这是干嘛用的，等之后用到了，甚至不记得自己学过，反而查资料才会想起：哦，原来我之前学的 xx 科目是这个用处啊，可是我当时并没有好好学。很多时候学生时间的浪费可能还是要怪老师、怪学校，他们一开始没给我们做好充分的课程介绍。所以，在经过比较多的编程和项目实践后，我认为一个比较好的学习方式是，      &lt;strong&gt;改良版的项目驱动学习法&lt;/strong&gt;。即：&lt;/p&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;学习一段时间，做个小项目，将做项目遇到的问题记下来，针对性地学习相关知识，然后再实践，再学一段时间理论，让知识成网状发射状地变大。当然，项目驱动式学习有一个弊端，就是每次学习的知识都是项目所需要的，很零碎、不成体系，所以需要改良，即在采取项目驱动学习法的时候每天抽一段时间去完整地读一本书，或者一个相关问题的完整介绍，这样就很容易把一些知识成体系地串起来。这样一段时间下来，慢慢的，你就知道我们为什么要学那么多科目，学这些科目能干什么。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;为了表达地更加形象，我就举一个小例子，是我最近遇到的。我本身的工作是做 Linux C++ 的，但不仅限于此。我个人对 python、数据分析，以及机器学习等内容比较感兴趣，大家可以看到我最近也在我的专栏发布了很多文章。就从      &lt;strong&gt;数据获取&lt;/strong&gt;开始，我讲讲我这两个月做了什么东西。&lt;/p&gt;    &lt;p&gt;谈到数据获取，可能最容易想到的是      &lt;strong&gt;爬虫&lt;/strong&gt;，爬虫是一个在知乎上被说烂了的话题，所以我不想多说它是什么。很多时候有人觉得爬虫简单，为什么呢，因为有      &lt;strong&gt;现成的框架&lt;/strong&gt;，所以获取少量的数据就比较容易。但是当你需要爬取的数据很大的时候（比如我之前抓取了知乎 500 万用户的数据，在下班的时间、用自己家里普通的 pc，计算机性能并不是那么好，比不上服务器，又要在不被封 IP 的情况下抓到这么大量的数据，然后对数据进行清洗，最后还要可视化展示），使用现成的爬虫框架就并不是那么容易实现了。况且，我需要抓很多数据源，并不是一锤子买卖。所以我选择去      &lt;strong&gt;开发一个系统&lt;/strong&gt;，即在现有的框架下进行二次开发，搭建一个属于自己的爬虫系统，并植入一些算法。我在系统中添加了很多中间件，直到现在，它还可以在 10 分钟内就部署一个能抓取大量数据的爬虫应用。当然，这个过程也遇到了不少麻烦，我就简单讲讲，怎么去攻克一个个问题。&lt;/p&gt;    &lt;p&gt;下面先给出一个      &lt;strong&gt;树形图&lt;/strong&gt;，从上往下每一个圈都代表了学习过程中遇到的难点，如果你现在看不懂，没关系，我想告诉你的是一种梳理知识的方法：&lt;/p&gt;    &lt;img alt="" src="https://pic2.zhimg.com/v2-be7424213282cc1581f2fa79cee03724_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;如上图所示，就是一个项目驱动式学习的例子，我们的目的是为了获取数据，所以选择了爬虫：&lt;/p&gt;    &lt;ol&gt;      &lt;li&gt;爬虫可以理解为一个简单的过程：发送 request，获取 response，然后提取数据。这个过程会涉及到网络，是发送 http 还是 https 请求；目标网站是否需要登录，是 post 请求还是 get 请求，从这条线，衍生出了一条对网络进行学习的路径。&lt;/li&gt;      &lt;li&gt;获取到网页之后，如果不是结构化的数据，可能返回的是一个 html 源代码，那么可能就需要了解 dom，或者 html 页面解析的知识，甚至需要了解一下前端开发。&lt;/li&gt;      &lt;li&gt;在抓取的过程中，经常会遇到数据中途不能被爬取的情况，一般是 IP 被封禁了，那么可能又要用上代理，代理是什么呢？http，https 代理能不能混用呢？如何构建一个代理池呢？这里又有很多要学习的东西。还有可能遇到的情况是，抓下来的数据是加密的，需要通过 js 解密，这时候就要了解一下 js，如何用爬虫模拟浏览器进行抓取。除此之外，如果抓取的频率不对，很多数据源会给你假数据，这就是一些经验问题了，本文不是技术文，所以就不多讨论。&lt;/li&gt;      &lt;li&gt;当解决了上述问题后，我们好像可以拿到一些数据了，但是当数据大起来，问题又复杂了，你可能需要使用分布式抓取了，这时候你可能需要了解一下 redis，当 request 产生的速度大于其消费的速度之后，你的任务队列可能爆炸，所以这里又涉及到算法和数据结构的应用了。&lt;/li&gt;      &lt;li&gt;数据量上去之后，把数据写在文件里面是不靠谱的，这时候又涉及到存储了，到底是使用关系型数据库还是非关系型数据库呢，有什么区别呢？存进去的数据怎么去重呢？为什么 insert 操作越来越卡了呢？电脑怎么越来越热了呢？索引是什么，什么时候该建立索引呢？这里又牵扯到数据库原理相关的知识。&lt;/li&gt;      &lt;li&gt;遇到一些比较难处理的网站，比如有验证码识别该怎么办呢？其实对于很多纯数字和字母的验证码都很好解决，自己用深度学习训练即可。在 TensorFlow 的 Demo 中就要生成验证码自己训练的教程，然后制定个中间件放在爬虫系统中，这个问题就解决了。可是什么是深度学习呢？这里又引出一条对深度学习进行探索的例子，而我自己也是之前在学校的时候自学了小半年机器学习，有了一定的基础后，才能比较容易地上手 TensorFlow 框架。再往下就比较深了。&lt;/li&gt;&lt;/ol&gt;    &lt;p&gt;上述六点简单讲了讲项目驱动式学习的介绍，其实，你看到的每一个小圆圈，深挖下去都大有文章。我们现在看到的只是冰山一角，任何一条学习路径学习下去都深无止境，我们不可能完全学会，可是      &lt;strong&gt;项目驱动式学习最大的好处是让你知道你应该去学习什么，而不是先学一大堆知识，再去做一个项目&lt;/strong&gt;。严格来说，项目驱动式学习的可视化路径是一张网，而不是一棵树，这里画成树状只是为了便于大家理解。&lt;/p&gt;    &lt;p&gt;除了获得数据，还有清洗数据、分析数据，甚至挖掘数据，最后可视化数据并且展示数据，这里我就不一一介绍了。可以参见下面这张图，      &lt;strong&gt;如果大家想看我做的一些成品，可以看看我的其他文章&lt;/strong&gt;。&lt;/p&gt;    &lt;img alt="" src="https://pic3.zhimg.com/v2-4e82f497fda1c55dc7dec2024d963303_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;      &lt;strong&gt;        &lt;em&gt;3.学习编程是否需要制定计划？&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;      &lt;strong&gt;学习编程是否需要制定计划，该制定什么样的计划呢?&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;我认为不只是编程需要制定计划，其他任何的学习和工作都需要制定计划。我从 13 年上大学就开始定期给自己制定计划，这个习惯也一直坚持到了现在，受益匪浅。当然也不只是制定学习计划，还可以列一些自己需要做的其他的事情。我最近在整理笔记的时候也发现了一些之前记录的计划和清单，可以给大家看看。      &lt;strong&gt;比如下图就是我 14 年写的笔记，笔记上都留下了最后一次打开的时间&lt;/strong&gt;。列举了一些自己需要看的文章，因为当时不太懂得规划，所以比较乱。&lt;/p&gt;    &lt;img alt="" src="https://pic2.zhimg.com/v2-4f616f9035e6db7cac9fdcfb0ab1551b_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;到了 16 年的时候，我做计划做得更加有条理了。下图是 16 年 10 月 30 日的计划，那时候我已经大四了，并且已经找到了工作、签了满意的 offer，并且没有什么课，按理说可以放松放松了，不过我还是制定了一些学习计划，并且选择在 11 月去百度实习。      &lt;strong&gt;从内容上看，主要是学习英语和计算机专业课，因为大一大二的时候我确实不明白为什么要学习专业课，到了大三下想清楚原因以后，我也就一直在重新学习，因为计算机专业课真的很重要！学好了这些课，能让你在日后的学习工作中轻松不少：&lt;/strong&gt;&lt;/p&gt;    &lt;img alt="" src="https://pic4.zhimg.com/v2-4047dc9fb19c2db9c4345cc6caab4f96_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;      &lt;strong&gt;除了大四制定的计划外，大二的时候我也制定过较为详细的学习计划（如下图），把需要学习的内容进行了编号，存入表格，这样才能让你过得有条不紊。当然，很难完全按照计划去执行，不过制定相应的计划能让你清楚地知道自己应该干什么。&lt;/strong&gt;&lt;/p&gt;    &lt;img alt="" src="https://pic2.zhimg.com/v2-10705fa6f1ba136d2ccd82b153fedfaa_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;所以，如果你是在校生，那么好好制定一个计划吧，因为你有大把的时间。当然，如果你已经毕业了，没关系，我现在也在上班，同样也列举了自己最近要学习的内容，如下图（2 月 27 日更新过），包括了短期和长期需要学习的内容：&lt;/p&gt;    &lt;img alt="" src="https://pic4.zhimg.com/v2-abe41f2379b7136315c9daee9db3ad44_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;      &lt;strong&gt;        &lt;em&gt;4.编程是否需要做笔记和写博客？&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;我觉得，写不写博客无所谓，因为博客是要写出来给大家看的，可能要保证格式美观、语法也要尽量准确，最好比较有文采，我觉得太麻烦也就一直没写。      &lt;strong&gt;而笔记是必须要做的，并且记笔记是一个长期的过程。在学习的过程中，我们一直都在追求一种最高效的学习方法，比如，同一个班的同学，他用他的学习方法考上了清华，而你用同样的方法就不行，为什么？因为他的方法对他自己而言是定制化的，可能且大概率不适合你，比如他的笔记你不一定能看懂，因为他可能设计了一套属于自己的符号。而就编程而言，很多同学说善用搜索引擎，是对的，可是搜索引擎搜出来的是别人的答案。你照搬过来，也许可以用，但是你没有记住，这些知识并不属于你，之后你可能还会遇到同样的问题，又要再搜索一遍，可能很难找到之前的那个答案了。但是记笔记就不一样，记笔记是定制化的，对你自己定制，你可以用自己最爽的表达方式来描述一个问题，是自己写给自己看的东西，看了几遍之后就能非常迅速和容易地理解。之后遇到相同的问题可以快速地通过找笔记解决。&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;举个例子，下图是我记录的一些关于 gdb【linux 下调试 c++ 的工具】的使用的一些笔记。我只记录了我自己最常用的一些内容，也许你看着很乱，但是我就能很容易看懂，这就是我的定制化。&lt;/p&gt;    &lt;img alt="" src="https://pic3.zhimg.com/v2-62666ae0466b9bdba6bb42c72f4d8cc9_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;记笔记的习惯一定要坚持，等过个一年或者两年，这就是你巨大的财富，因为那是只有你才能看懂的东西。我已经记录了 4 年多、1G 多的内容，现在的笔记基本已经形成了体系，可以给大家展示其中的一部分。&lt;/p&gt;    &lt;p&gt;专业知识相关笔记：&lt;/p&gt;    &lt;img alt="" src="https://pic3.zhimg.com/v2-1a92b35caf6c7daa7fce92d8b37ba5b4_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;开发相关的笔记：&lt;/p&gt;    &lt;img alt="" src="https://pic3.zhimg.com/v2-e47df51a4a67071f0e0d6c7f7cb239a1_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;一些类目：&lt;/p&gt;    &lt;img alt="" src="https://pic1.zhimg.com/v2-ac61d7e115b0f3bdbae27c390d923141_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;5.有什么比较好的编程方法？&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;除了上述分享的一些方法，我认为在同一时间段不要学习太多类别的课程，比如你可以同时学习 python 和 html/css，但是你不要同时学 python、操作系统、编译原理、计算机组成、数据结构、网络，我曾经试过，一门课没学一会儿就学下一门，其实上一门根本学不到什么实际的知识。因为记忆知识是符合      &lt;strong&gt;艾宾浩斯记忆曲线&lt;/strong&gt;的。对于一门课，特别是很难的专业课，譬如操作系统，你每天看半小时，效果是比较差的，可能你热身就得半小时。所以宁可每天学两门，然后每一门学长一点的时间，比如两小时。【毕竟学校上课，一次课也得两小时】，要避免贪多，一口吃不成个胖子。&lt;/p&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;6.我需要刷 oj 么？&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;我认为刚开始编程的时候还是应该刷的，但是一定要注意，不要被你周围的“X 神”给误导了。因为我上大学的时候，身边总是有很多搞计算机竞赛的人，他们之间都互相称对方为“X 神”，某某神又使用一个牛逼的算法，将程序时间从 1 秒降低到了 0.999 秒。我要劝大家的是，刷题不是为了达到这个目的，不是说非要在竞赛中拿奖，除非你是特别喜欢，否则，没必要去      &lt;strong&gt;背代码&lt;/strong&gt;。我们刷题的目的是适应写代码的感觉，在这个过程中你会遇到编译错误，你会慢慢去记住一些语法、关键字，并理解一些概念，还可以自己去使用它，比如实现数据结构。慢慢的你就会变得有经验，知道一些错误产生的原因。我也是慢慢这样过来的，我现在在工作和下班以后写代码时，基本都不用 IDE 了，比如写 c++，要么 vim，要么就是 sublime，而调试用的是我前面提到的工具 gdb。即，有一个文本编辑器就能写代码，脱离了 IDE 的束缚。在写 oj 之后一段时间，在比较熟练了之后，就可以不去刷题了，可以去譬如 github 这样的网站上找点项目来看，然后自己跟着写一下，编程能力慢慢就提升了。就计算机专业来说，很多同学在大一上完编程课之后，就很少写代码了，这样是很不好的。刷题除了可以锻炼编程能力，对于找工作前突击也很有作用。比如，我之前投递过华为公司的研发岗位，校招的时候有笔试题。我就在 16 年国庆的时候刷了一下华为的 oj，我记得笔试是 600 分的总分，过 100 就给面试机会，而我很轻松的就拿了 500 分，而当时也就刷了 20 多道华为的题。&lt;/p&gt;    &lt;img alt="" src="https://pic3.zhimg.com/v2-3d5b9b8461b68fa85cf85aeb841a9537_b.jpg"&gt;&lt;/img&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;7.看书还是看视频？&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;网上有不少人鄙视看视频学习的同学，我不知道为什么，因为我认为看视频是一个很好的学习方式。不过我们得明白看书和看视频分别有什么优缺点。&lt;/p&gt;    &lt;p&gt;其实我是很建议看视频入门的，因为目前网上的应用型【非学术型：比如清华大学的操作系统，非常难】的视频都是很简单的，很多是面向初学者的，视频能用较短的时间告诉你你现在所学的技术可以干什么，可能需要先修哪些知识，可以帮助我们搭建一个项目驱动式学习的网络。可是视频也有个缺点：就是知识非常的杂，很不系统。虽然现在很多教学网站都提供了学习路径，但是这些路径中的视频很多时候都不是同一个老师录制的，只是按照知识的依赖关系排的顺序，所以，如果想通过视频去系统地学习一门知识，是比较困难的。【当然，一些学术型的视频还是很推荐的，比如斯坦福的机器学习，清华的操作系统、数据结构等课程，能坚持看完，绝对受益匪浅】。而应用型的，比如 web 开发等知识，还是得看书。书籍等特点就是系统化，由浅入深，你可以定制化地看自己薄弱的章节。所以一个比较好的学习方式是：&lt;/p&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;看视频入门，看书进阶。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;8.多久能学会编程？&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;其实这个问题是没有答案的，如果只是想做出一个小应用，2 个月足矣，而就我个人而言，我认为学习编程不是学习一种编程语言，而是学习一个生态，一个计算机系统，所以无止境。&lt;/p&gt;    &lt;p&gt;      &lt;em&gt;        &lt;strong&gt;9.我应该选择什么资料，看什么书？&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;    &lt;p&gt;其实这个问题也是很多编程新手容易困惑的问题。网络上拥有我们一辈子都看不完的教程和资料，所以现在应该不会存在找不着视频教程、找不着书看的问题。而问题就是我们不知道看什么视频、看什么书。从开始学编程到现在，我也买了上百本书，而真正适合自己的好书并不多。而视频教程的问题就更严重了，东看一点、西看一点，知识很难组织成网络。所以      &lt;strong&gt;学习编程的过程中，我们遇到的最大的问题是：当我们遇到问题的时候，在大量资料面前，我们不知道选择什么资料去学习。&lt;/strong&gt;即使我们使用项目驱动式学习的方法找到了我们的方向，但是同一个路径下，也有很多资料。前文列举的项目驱动式学习的图中，我们是自上而下的去发现问题，然后再解决问题。如果能有人帮我们组织好学习路径，然后自下而上地去学习，那么效率可能会提高很多。&lt;/p&gt;    &lt;div&gt;      &lt;a href="http://zhuanlan.zhihu.com/p/34418394" target="_self"&gt;查看知乎原文&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/58496-%E6%88%91%E8%A7%81-%E6%9C%80%E5%A5%BD-%E7%BC%96%E7%A8%8B</guid>
      <pubDate>Wed, 04 Jul 2018 14:37:49 CST</pubDate>
    </item>
  </channel>
</rss>

