<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>dehop.eth</title>
        <link>https://paragraph.com/@dehop</link>
        <description>undefined</description>
        <lastBuildDate>Tue, 18 Aug 2026 11:23:20 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Designing Token Economies]]></title>
            <link>https://paragraph.com/@dehop/designing-token-economies</link>
            <guid>TsDzgzmC17HHqHs86a7W</guid>
            <pubDate>Tue, 24 Jan 2023 07:40:40 GMT</pubDate>
            <description><![CDATA[This piece is a collaboration between Tina He and Packy McCormick. Tina is the founder of Station, which I was lucky enough to invest in via Not Boring Capital, and the author of one of my favorite Substacks, Fakepixels. All the smart parts are hers. When trying to understand tokens, it’s tempting to draw from what we already know. Sometimes, tokens function like equity in a company, and owning a token is akin to holding a stake in the project’s potential upside. Other times, tokens function ...]]></description>
            <content:encoded><![CDATA[<p><em>This</em> <em>piece is a collaboration between </em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/fkpxls"><em>Tina He</em></a> <em>and</em> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/packyM"><em>Packy McCormick</em></a>. <em>Tina is the founder of </em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.station.express/"><em>Station</em></a><em>, which I was lucky enough to invest in via Not Boring Capital, and the author of one of my favorite Substacks, </em><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://fakepixels.substack.com/"><em>Fakepixels</em></a><em>.  All the smart parts are hers.</em></p><p>When trying to understand tokens, it’s tempting to draw from what we already know. </p><p>Sometimes, tokens function like equity in a company, and owning a token is akin to holding a stake in the project’s potential upside. Other times, tokens function like a “token-of-gratitude” and symbolize goodwill among close friends in the purest sense. The wide-ranging role isn’t a bug but a feature representing value in the most abstract sense, whose meaning is given by the system&apos;s very design.  In other words, a token doesn’t necessarily have any intrinsic value but relative value. It’s the encapsulation of a unit of value universally recognizable and enforceable by a system. </p><p>Tokens are barely a new concept. Shells and beads were the earliest types of tokens as a medium of exchange. Others that we’re familiar with today — casino chips, credit card points, stock certificates, concert tickets, and club memberships – are all forms of tokens as they represent a unit of value universally recognized and enforced by the system that issues that token. When the respective systems fail to enforce and recognize the value of these tokens, the jurisdiction can step in to protect the token holders. </p><p>Think about the last token you interacted with — <em>What does it allow you to do that you otherwise cannot? Why are you holding it and want to own more of it? What happens if you discard or transfer ownership of your tokens?</em> To many, the answer to these questions would be “getting even more tokens.” To others, holding tokens allows for participation rights in projects and communities that they care deeply about. The former speaks to the economics of holding a token, the latter to access rights. </p><p>A <strong>token design</strong> is poor when there’s value misalignment between value accrual in the system and value accrual to the token. <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/lex_node/status/1505724699472547843?s=20&amp;t=R_FgMb3lp_aiWiST9mvfaQ">Gabriel Shapiro</a> aptly describes tokens like UNI, COMP, and the recently launched APE, as “value by association,” as he acutely identifies the fragmentation in value streams for the protocols aforementioned — the prime slice has been preserved for the insiders, while the “illusion of power” gets distributed to the rest. </p><p>One of the reasons that there is so much confusion around token design, and value accrual specifically, is that tokens, and the DAOs and protocols that issue them, are so multifaceted. Sometimes, the issuers want them to behave like shares in a <strong>corporation</strong>. Others issue “governance” rights to skirt regulations while insiders pump the tokens in the hope of getting out before the price tumbles. Others still want to build and unify digital <strong>nations.</strong> Often, even the issuers aren’t clear exactly what they want to do with the token, but they know that tokens are a great way to capture value.</p><p>While token design isn’t the only important aspect of creating a new protocol or digital economy – delivering value to users should always be priority #1 or else the token’s price will inevitably crumble – it’s a critical one. Just like a messy cap table can inflict mortal wounds on a startup or poor monetary policy can derail a nation’s economy, bad token design can doom a protocol before it even gets off the ground. The crypto graveyard is littered with examples of good projects whose token designs cemented their eventual demise from day one – maybe the tokenomics encouraged too much growth, too fast – and we’ll cover some of them here. There are others whose token designs allow them to do things that non-web3 companies can’t by properly aligning incentives in the system, and connecting the system to the larger ecosystem. We’ll cover those, too. </p><p>Why does it matter? Everything is falling apart. Terra collapsed largely thanks to its token design. Projects that attracted millions or billions of dollars with the promise of absurd APYs are learning the truth of the old adage, “Easy come, easy go.” The regulators are coming.  Tokens that were worth a lot of fiat a couple weeks ago are worth a lot less today.</p><p>All of those reasons and more are <em>exactly</em> why it’s critical to understand good token design today. Not only because good token design can help avoid catastrophic outcomes, but because, assuming we’re entering a sustained bear market, now is the <em>perfect</em> time to experiment with novel token designs without the pressure of the expectation of high prices and “up only.” </p><p>Tokens are naturally economic; they have a price attached from inception, and are instantly tradeable on liquid, global, 24/7 markets. But tokens can be much more than that. They’re programmable primitives that allow DAOs and protocols to signal what matters in their ecosystem, to reward good participation, to trade with each other and build interconnected webs of support, and to support new forms of digital organizations and nations. </p><p>So what are you building? Are you building a club or a co-op, a corporation or a country? </p><p>Protocols can be all of the above, so we’ll start by walking through how they <strong>compare to companies and countries</strong>, before laying out a <strong>framework for token analysis</strong>, and imagining <strong>what the world will look like</strong> when the dust settles. We hope that it’s useful to the builders, contributors, and investors alike. </p><p>We know you wanted a break, anon, but it’s time to jump back down the rabbit hole. </p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[干货：Dune Analytics 初学者完全指南（炒币）]]></title>
            <link>https://paragraph.com/@dehop/dune-analytics</link>
            <guid>dCtAjHoImw4LB5R97hEP</guid>
            <pubDate>Tue, 24 Jan 2023 07:33:01 GMT</pubDate>
            <description><![CDATA[Dune 可能是目前向公众提供的最强大的区块链数据分析工具，而最棒的是：它还是免费的！ 通过 Dune，你可以通过一个公共数据库近乎实时地访问区块链数据，你可以通过 Dune 的网站使用 SQL 查询。 这是一个很大的能力。 Dune 在将区块链数据添加到他们的数据库之前对其进行解码，这意味着你不需要自己弄清字节码通信。相反，你可以使用 Dune 的数据集浏览器来浏览数据集、特定的智能合约、事件或调用 随着 Dune最近宣布他们的 V2 引擎，性能提高了 10 倍，现在是时候让你学习如何使用 Dune 了。 在本指南中，你将学习：第一部分：Dune 的界面第二部分：用 SQL 建立你自己的查询和图表 -- 从最基本的功能开始第三部分：将所有内容组织成一个仪表板在这个手把手教程中，我们将为 Pooly NFT 系列建立以下仪表盘的查询，本文较长，做好准备，学习完收获很大。 仪表盘是一个查询的集合，它被安排成一系列的图表、展示面板和其他信息，给用户提供关于特定兴趣领域的数据。下面，我打开了一个由传奇人物@hildobby制作的以太坊仪表盘。在这里，我们可以看到从 Dune 的数据库中...]]></description>
            <content:encoded><![CDATA[<p>Dune 可能是目前向公众提供的最强大的区块链数据分析工具，而最棒的是：<strong>它还是免费的！ 通过 Dune，你可以通过一个公共数据库近乎实时地访问区块链数据，你可以通过 Dune 的网站使用 SQL 查询。</strong></p><p><strong>这是一个很大的能力。</strong></p><p>Dune 在将区块链数据添加到他们的数据库之前对其进行解码，这意味着你不需要自己弄清字节码通信。相反，你可以使用 Dune 的数据集浏览器来浏览数据集、特定的智能合约、事件或调用</p><p>随着 Dune<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//dune.com/blog/Dune-Engine-V2">最近宣布</a>他们的 V2 引擎，性能提高了 10 倍，现在是时候让你学习如何使用 Dune 了。</p><p>在本指南中，你将学习：</p><ul><li><p>第一部分：Dune 的界面</p></li><li><p>第二部分：用 SQL 建立你自己的查询和图表 -- 从最基本的功能开始</p></li><li><p>第三部分：将所有内容组织成一个仪表板</p></li></ul><p>在这个手把手教程中，我们将为 Pooly NFT 系列建立以下仪表盘的查询，本文较长，做好准备，学习完收获很大。</p><p>仪表盘是一个查询的集合，它被安排成一系列的图表、展示面板和其他信息，给用户提供关于特定兴趣领域的数据。下面，我打开了一个由传奇人物<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//dune.com/hildobby/">@hildobby</a>制作的以太坊仪表盘。在这里，我们可以看到从 Dune 的数据库中提取的各种数据，以总和和时间序列图的形式显示。</p><p>在 Dune 中，每个仪表板都是公开的。这意味着你或其他人建立的所有东西都可以被任何人查看和分叉（即复制）！这大大减少了仪表盘的数量。这大大减少了仪表板的创建时间，并让你从其Ethereum 价格查询，截图来自 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//dune.com/queries/663019/1231425">https://dune.com/queries/663019/1231425</a></p><p>让我们分叉以太坊价格图表! 一旦你在一个查询上按下 &quot;Fork&quot;，你就会进入到查询编辑器，前面的代码已经被复制进去了!</p><p>让我在这里向你介绍一下屏幕上的各种元素：</p><ol><li><p>查询的位置和名称 -- 一旦你点击保存，名称就可以改变。</p></li><li><p>数据集浏览器 -- 搜索一个特定的数据集</p></li><li><p>查询窗口--在这里输入你的 SQL 查询</p></li><li><p>让我们仔细看看数据集浏览器。在数据集浏览器中，有六个功能区：</p><ol><li><p>链的选择</p></li><li><p>在数据集选择中，你可以选择你要解析的链。选择 <code>Dune Engine V2 (Beta)</code>可以让你使用 Dune 的最新改进，包括多链查询和 10 倍的性能提升。</p><p>如果你选择了另一条链，你的类别选择（上图中的第 3-6 项）就会消失，取而代之的是你可以交互的合约调用和事件的列表。</p></li><li><p>在搜索栏中，你可以输入你的搜索参数，Dune 会在所有包含该关键词的表格中进行搜索。</p><p>_注意：Dune Engine V2 和旧的搜索功能以不同的方式返回结果。旧的搜索返回一个所有结果的列表，而 Dune Engine V2 返回一个嵌套的结果列表。我们将使用 V2 引擎！_。</p></li><li><p>如果你点击进入原始区块链数据，你可以很容易地找到 Dune 支持的各种区块链的查询，在一个嵌套的数据结构中，你可以首先挑选你的原始表，然后从那里挑选你想进一步调查的特定表列。在每个层次的嵌套中，你还可以选择过滤你要寻找的特定搜索结果。</p></li><li><p>在这里，你会发现已经被 Dune 解码的项目。解码的项目是指 Dune 团队将项目抽象可以被认为是定制的表格，它连接和组合各种查询和数据片段，形成一个独特的表格。抽象帮助用户更容易地查询他们正在寻找的特定数据，而不需要手动组合各种数据片段的麻烦。</p><p>一般来说，抽象可社区部分可以被认为是抽象部分的延伸，但数据的汇总是由 Dune 社区成员提供的。</p><p>你可能想知道为什么社区部分只有一个条目（&quot;flashbots&quot;）-- 那是因为 Dune Engine V2 刚刚发布，随着时间的推移，我们可以期待看到越来越多的由值得信赖的社区成员建立的社区数据集。</p></li></ol></li></ol><p>在下面的插图中，你可以看到截至 Dune Engine V2 发布时，Dune 内部的数据汇总：四个主要的数据类别是原始区块链数据、解码项目、抽象和社区，它们以表格的形式保存了各种区块链的数据，可以保存各种数据类型。</p><p>我们继续，先保存这个查询。在你点击保存后，有几件事会发生。首先，你会被一旦你选择了一个名字，你会注意到（1）查询的位置和名字已经更新为你选择的名字；（2）你的查询正在运行。这意味着 Dune 正在从他们的数据库中获取最新的数据，该数据库会定期更新各种区块链的最新数据。</p><pre data-type="codeBlock" text="   一旦查询运行完毕，你会看到你的查询结果（3）。

6. 在这里，如果你点击(1) &quot;查询结果（Query result）&quot;、&quot;折线图（Line Chart）&quot; 或 &quot;&quot;新的可视化&quot; 中的任何一个，(2)对你的选择的设置，结果/可视化框就会更新查询列表包括你在账户中保存的所有查询。在下面的屏幕截图中，我们可以看到最新创建的查询。

7. 恭喜你，你已经分叉并保存了你的第一个具有可视化的查询！。

   分叉(fork)是一种 Dune 的超级能力，它可以帮助你基于之前建立查询向导（是的，你现在也是一个向导！）建立查询，轻松而快速地创建新的查询。你可以结合多个分叉的查询来建立你自己的仪表板

   让我们亲自动手，从头开始建立一个仪表盘--查询和可视化的集合，而不通过分叉。这将教会我们为特定项目寻找正确的区块链细节，以及教你 SQL 基础知识。，同时出现在它下面的(3)。这里你还有一个 “添加到仪表盘（Add to dashboard） ”的按钮，可以快速将你的查询结果或可视化添加到新的或现有的仪表盘中--就像@hildobby 之前的以太坊仪表盘一样!

8. 如果你点击(1)右上方的圆圈，然后点击(2) &quot;我的查询&quot;，你将打开你的账户的查询列表。

9. 要求给你的查询一个名字。
"><code>   一旦查询运行完毕，你会看到你的查询结果（<span class="hljs-number">3</span>）。

<span class="hljs-number">6</span>. 在这里，如果你点击(<span class="hljs-number">1</span>) "查询结果（Query result）"、"折线图（Line Chart）" 或 ""新的可视化" 中的任何一个，(<span class="hljs-number">2</span>)对你的选择的设置，结果/可视化框就会更新查询列表包括你在账户中保存的所有查询。在下面的屏幕截图中，我们可以看到最新创建的查询。

<span class="hljs-number">7</span>. 恭喜你，你已经分叉并保存了你的第一个具有可视化的查询！。

   分叉(fork)是一种 Dune 的超级能力，它可以帮助你基于之前建立查询向导（是的，你现在也是一个向导！）建立查询，轻松而快速地创建新的查询。你可以结合多个分叉的查询来建立你自己的仪表板

   让我们亲自动手，从头开始建立一个仪表盘--查询和可视化的集合，而不通过分叉。这将教会我们为特定项目寻找正确的区块链细节，以及教你 SQL 基础知识。，同时出现在它下面的(<span class="hljs-number">3</span>)。这里你还有一个 “添加到仪表盘（Add to dashboard） ”的按钮，可以快速将你的查询结果或可视化添加到新的或现有的仪表盘中--就像<span class="hljs-keyword">@hildobby</span> 之前的以太坊仪表盘一样!

<span class="hljs-number">8</span>. 如果你点击(<span class="hljs-number">1</span>)右上方的圆圈，然后点击(<span class="hljs-number">2</span>) <span class="hljs-string">"我的查询"</span>，你将打开你的账户的查询列表。

<span class="hljs-number">9</span>. 要求给你的查询一个名字。
</code></pre><ol start="10"><li><p>以分为两个主要类别：</p><ul><li><p>领域（Sector）抽象：特定领域的数据</p></li><li><p>项目（Project）抽象：特定项目的具体数据</p></li></ul><p>从抽象子菜单中，我们可以看到一个抽象列表，其中的标签说明了该抽象是针对领域还是针对项目的。拆开，贴上标签，并放入表格，以便用户对某些数据有一个简单和标准化的参考。</p><p>你会注意到，同样，搜索结果是嵌套的。在最高层，有你可以搜索的项目，在较低层，你可以过滤该项目中的特定智能合约，最后我们得到了由该智能合约生成的各种表格。如果你点击任何一个表，你会看到一个列表，就像在原始区块链数据中一样。</p></li><li><br></li><li><p>数据</p></li><li><p>Dune 数据集浏览器的概述集搜索</p></li><li><p>浏览原始区块链数据</p></li><li><p>浏览解码后的合约数据</p></li><li><p>浏览抽象的数据</p></li><li><p>浏览社区提供的数据可视化选择器--选择是否查看查询结果、折线图或创建一个新的可视化视图。</p></li><li><p>运行 - 运行查询窗口中的查询</p></li><li><p>结果/可视化--看到查询结果或你用查询结果创建的可视化效果</p></li><li><p>保存--保存你的（分叉的）查询!</p></li><li><br></li></ol><p>他用户的查询中学习。</p><p>在这里，我们可以看到屏幕上的两个主要元素：查询（顶部；黑框）和输出图表（底部）。这就对如果你想把整个仪表盘或者只把图表的查询保存到你自己的账户中，你可以点击右上方的 &quot;分叉（Fork）&quot;，屏幕上的所有内容都会被复制到一个新的窗口中，在把视图保存到你的账户之前，你可以在其中进行编辑。</p><p>了：无论你点击哪个区块或图表，你都可以看到<strong>用户是如何创建该图表的</strong>!</p><p>如果你还记得，我提到过仪表盘是<em>查询</em>的集合。如果你点击任何一个仪表盘元素的标题，你就会进入到该图表的 SQL 查询。</p><p>在我们开始研究 Dune 之前，我们需要找到正确的信息。从网站上我们可以知道 PoolTogether 正在销售三套 NFT。</p><ul><li><p>支持者 NFT -- 9 个随机收藏品中的一个，售价 0.1ETH</p></li><li><p>律师 NFT -- 只有一件，售价 1ETH</p></li><li><p>法官 NFT -- 只有一件，售价 75ETH</p></li></ul><p>Pooly 是通过一份合约，还是通过三份不同的合约出售这三套 NFT？</p><p>让我们前往 Etherscan，看看是否能找到与 Pooly 有关的智能合约。打开<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//etherscan.io/">Etherscan.io</a>后，输入 &quot;Pooly&quot;，看看这些智能合约的所有者是否在 Etherscan 注册了它们。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入分析Go1.18 GMP调度器底层原理（编码）]]></title>
            <link>https://paragraph.com/@dehop/go1-18-gmp</link>
            <guid>RoHeulJpB02vwvmG1cxy</guid>
            <pubDate>Tue, 24 Jan 2023 07:20:33 GMT</pubDate>
            <description><![CDATA[Go 语言有强大的并发能力，能够简单的通过 go 关键字创建大量的轻量级协程 Goroutine，帮助程序快速执行各种任务，比Java等其他支持多线程的语言在并发方面更为强大，除了会用它，我们还需要掌握其底层原理，自己花时间把 GMP 调度器的底层源码学习一遍，才能对它有较为深刻的理解和掌握，本文是自己个人对于 Go语言 GMP 调度器（Go Scheduler）底层原理的学习笔记。 在学习 Go 语言的 GMP 调度器之前，原以为 GMP 底层原理较为复杂，要花很多时间和精力才能掌握，亲自下场学习之后，才发现其实并不复杂，只需三个多小时就足够：先花半个多小时，学习下刘丹冰Aceld 的 B 站讲解视频《Golang深入理解GPM模型》，然后花两个小时，结合《Go语言设计和实现》6.5节调度器的内容阅读下相关源码，最后，可以快速看看 GoLang-Scheduling In Go 前两篇文章的中译版，这样可以较快掌握 GMP 调度器的设计思想。 当然，如果希望理解的更加透彻，还需要仔细钻研几遍源码，并学习其他各种资料，尤其是 Go 开发者的文章，最好能够输出一篇文章，以加深头脑中...]]></description>
            <content:encoded><![CDATA[<p>Go 语言有强大的并发能力，能够简单的通过 go 关键字创建大量的轻量级协程 Goroutine，帮助程序快速执行各种任务，比Java等其他支持多线程的语言在并发方面更为强大，除了会用它，我们还需要掌握其底层原理，自己花时间把 GMP 调度器的底层源码学习一遍，才能对它有较为深刻的理解和掌握，本文是自己个人对于 Go语言 GMP 调度器（Go Scheduler）底层原理的学习笔记。</p><p>在学习 Go 语言的 GMP 调度器之前，原以为 GMP 底层原理较为复杂，要花很多时间和精力才能掌握，亲自下场学习之后，才发现其实并不复杂，只需三个多小时就足够：先花半个多小时，学习下刘丹冰Aceld 的 B 站讲解视频《Golang深入理解GPM模型》，然后花两个小时，结合《Go语言设计和实现》6.5节调度器的内容阅读下相关源码，最后，可以快速看看 GoLang-Scheduling In Go 前两篇文章的中译版，这样可以较快掌握 GMP 调度器的设计思想。</p><p>当然，如果希望理解的更加透彻，还需要仔细钻研几遍源码，并学习其他各种资料，尤其是 Go 开发者的文章，最好能够输出一篇文章，以加深头脑中神经元的连接和对事情本质的理解，本文就是这一学习思路的结果，希望能帮助到感兴趣的同学。</p><p>本文的代码基于 Go1.18.1 版本，跟 Go1.14 版本的调度器的主要逻辑相比，依然没有大的变化，目前看到的改动是调度循环的 runtime.findrunnable() 函数，抽取了多个逻辑封装成了新的方法，比如 M 从 其他 P 上偷取 G 的 runtime.stealWork()。</p><h3 id="h-0" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">0. 结论</h3><p>先给出整篇文章的结论和大纲，便于大家获取重点：</p><p>\1. 为了解决 Go 早期多线程 M 对应多协程 G 调度器的全局锁、中心化状态带来的锁竞争导致的性能下降等问题，Go 开发者引入了处理器 P 结构，形成了当前经典的 GMP 调度模型；</p><p>\2. Go 调度器是指：运行时在用户态提供的多个函数组成的一种机制，目的是高效地调度 G 到 M上去执行；</p><p>\3. Go 调度器的核心思想是：尽可能复用线程 M，避免频繁的线程创建和销毁；利用多核并行能力，限制同时运行（不包含阻塞）的 M 线程数 等于 CPU 的核心数目； Work Stealing 任务窃取机制，M 可以从其他 M 绑定的 P 的运行队列偷取 G 执行；Hand Off 交接机制，为了提高效率，M 阻塞时，会将 M 上 P 的运行队列交给其他 M 执行；基于协作的抢占机制，为了保证公平性和防止 Goroutine 饥饿问题，Go 程序会保证每个 G 运行 10ms 就让出 M，交给其他 G 去执行，这个 G 运行 10ms 就让出 M 的机制，是由单独的系统监控线程通过 retake() 函数给当前的 G 发送抢占信号实现的，如果所在的 P 没有陷入系统调用且没有满，让出的 G 优先进入本地 P 队列，否则进入全局队列；；基于信号的真抢占机制，Go1.14 引入了基于信号的抢占式调度机制，解决了 GC 垃圾回收和栈扫描时无法被抢占的问题；</p><p>\4. 由于数据局部性，新创建的 G 优先放入本地队列，在本地队列满了时，会将本地队列的一半 G 和新创建的 G 打乱顺序，一起放入全局队列；本地队列如果一直没有满，也不用担心，全局队列的 G 永远会有 1/61 的机会被获取到，调度循环中，优先从本地队列获取 G 执行，不过每隔61次，就会直接从全局队列获取，至于为啥是 61 次，Dmitry 的视频讲解了，就是要一个既不大又不小的数，而且不能跟其他的常见的2的幂次方的数如 64 或 48 重合；</p><p>\5. M 优先执行其所绑定的 P 的本地运行队列中的 G，如果本地队列没有 G，则会从全局队列获取，为了提高效率和负载均衡，会从全局队列获取多个 G，而不是只取一个，个数是自己应该从全局队列中承担的，globrunqsize / nprocs + 1；同样，当全局队列没有时，会从其他 M 的 P 上偷取 G 来运行，偷取的个数通常是其他 P 运行队列的一半；</p><p>\6. G 在运行时中的状态可以简化成三种：等待中_Gwaiting、可运行_Grunnable、运行中_Grunning，运行期间大部分情况是在这三种状态间来回切换；</p><p>\7. M 的状态可以简化为只有两种：自旋和非自旋；自旋状态，表示 M 绑定了 P 又没有获取 G；非自旋状态，表示正在执行 Go 代码中，或正在进入系统调用，或空闲；</p><p>\8. P 结构体中最重要的，是持有一个可运行 G 的长度为 256 的本地环形队列，可以通过 CAS 的方式无锁访问，跟需要加锁访问的全局队列 schedt.runq 相对应；</p><p>\9. 调度器的启动逻辑是：初始化 g0 和 m0，并将二者互相绑定， m0 是程序启动后的初始线程，g0 是 m0 线程的系统栈代表的 G 结构体，负责普通 G 在 M 上的调度切换 --&gt; runtime.schedinit()：负责M、P 的初始化过程，分别调用runtime.mcommoninit() 初始化 M 的全局队列allm 、调用 runtime.procresize() 初始化全局 P 队列 allp --&gt; runtime.newproc()：负责获取空闲的 G 或创建新的 G --&gt; runtime.mstart() 启动调度循环；；</p><p>\10. 调度器的循环逻辑是：运行函数 schedule() --&gt; 通过 runtime.globrunqget() 从全局队列、通过 runtime.runqget() 从 P 本地队列、 runtime.findrunnable 从各个地方，获取一个可执行的 G --&gt; 调用 runtime.execute() 执行 G --&gt; 调用 runtime.gogo() 在汇编代码层面上真正执行G --&gt; 调用 runtime.goexit0() 执行 G 的清理工作，重新将 G 加入 P 的空闲队列 --&gt; 调用 runtime.schedule() 进入下一次调度循环。</p><h3 id="h-1-gmp" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1. GMP调度模型的设计思想</h3><h3 id="h-11" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1.1 传统多线程的问题</h3><p>在现代的操作系统中，为了提高并发处理任务的能力，一个 CPU 核上通常会运行多个线程，多个线程的创建、切换使用、销毁开销通常较大：</p><p>1）一个内核线程的大小通常达到1M，因为需要分配内存来存放用户栈和内核栈的数据；</p><p>2）在一个线程执行系统调用（发生 IO 事件如网络请求或读写文件）不占用 CPU 时，需要及时让出 CPU，交给其他线程执行，这时会发生线程之间的切换；</p><p>3）线程在 CPU 上进行切换时，需要保持当前线程的上下文，将待执行的线程的上下文恢复到寄存器中，还需要向操作系统内核申请资源；</p><p>在高并发的情况下，大量线程的创建、使用、切换、销毁会占用大量的内存，并浪费较多的 CPU 时间在非工作任务的执行上，导致程序并发处理事务的能力降低。</p><p><em>图1.1 传统多线程之间的切换开销较大</em></p><h3 id="h-12-go-gm" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1.2 Go语言早期引入的 GM 模型</h3><p>为了解决传统内核级的线程的创建、切换、销毁开销较大的问题，Go 语言将线程分为了两种类型：内核级线程 M （Machine），轻量级的用户态的协程 Goroutine，至此，Go 语言调度器的三个核心概念出现了两个：</p><p>M： Machine的缩写，代表了内核线程 OS Thread，CPU调度的基本单元；</p><p>G： Goroutine的缩写，用户态、轻量级的协程，一个 G 代表了对一段需要被执行的 Go 语言程序的封装；每个 Goroutine 都有自己独立的栈存放自己程序的运行状态；分配的栈大小 2KB，可以按需扩缩容；</p><p><em>图1.2 Go将线程拆分为内核线程 M 和用户线程 G</em></p><p>在早期，Go 将传统线程拆分为了 M 和 G 之后，为了充分利用轻量级的 G 的低内存占用、低切换开销的优点，会在当前一个M上绑定多个 G，某个正在运行中的 G 执行完成后，Go 调度器会将该 G 切换走，将其他可以运行的 G 放入 M 上执行，这时一个 Go 程序中只有一个 M 线程：</p><p><em>图1.3 多个 G 对应一个 M</em></p><p>这个方案的优点是用户态的 G 可以快速切换，不会陷入内核态，缺点是每个 Go 程序都用不了硬件的多核加速能力，并且 G 阻塞会导致跟 G 绑定的 M 阻塞，其他 G 也用不了 M 去执行自己的程序了。</p><p>为了解决这些不足，Go 后来快速上线了多线程调度器：</p><p><em>图1.4 多个 M 对应多个 G</em></p><p>每个Go程序，都有多个 M 线程对应多个 G 协程，该方案有以下缺点：</p><p>1）全局锁、中心化状态带来的锁竞争导致的性能下降； 2）M 会频繁交接 G，导致额外开销、性能下降；每个 M 都得能执行任意的 runnable 状态的 G； 3）每个 M 都需要处理内存缓存，导致大量的内存占用并影响数据局部性； 4）系统调用频繁阻塞和解除阻塞正在运行的线程，增加了额外开销；</p><h3 id="h-13-gmp" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1.3 当前高效的 GMP 模型</h3><p>为了解决多线程调度器的问题，Go 开发者 Dmitry Vyokov 在已有 G、M 的基础上，引入了 P 处理器，由此产生了当前 Go 中经典的 GMP 调度模型。</p><p>P：Processor的缩写，代表一个虚拟的处理器，它维护一个局部的可运行的 G 队列，可以通过 CAS 的方式无锁访问，工作线程 M 优先使用自己的局部运行队列中的 G，只有必要时才会去访问全局运行队列，这大大减少了锁冲突，提高了大量 G 的并发性。每个 G 要想真正运行起来，首先需要被分配一个 P。</p><p>如图 1.5 所示，是当前 Go 采用的 GMP 调度模型。可运行的 G 是通过处理器 P 和线程 M 绑定起来的，M 的执行是由操作系统调度器将 M 分配到 CPU 上实现的，Go 运行时调度器负责调度 G 到 M 上执行，主要在用户态运行，跟操作系统调度器在内核态运行相对应。</p><p><em>图1.5 当前高效的GMP调度模型</em></p><p>需要说明的是，Go 调度器也叫 Go 运行时调度器，或 Goroutine 调度器，指的是由运行时在用户态提供的多个函数组成的一种机制，目的是为了高效地调度 G 到 M上去执行。可以跟操作系统的调度器 OS Scheduler 对比来看，后者负责将 M 调度到 CPU 上运行。从操作系统层面来看，运行在用户态的 Go 程序只是一个请求和运行多个线程 M 的普通进程，操作系统不会直接跟上层的 G 打交道。</p><p>至于为什么不直接将本地队列放在 M 上、而是要放在 P 上呢？ 这是因为当一个线程 M 阻塞（可能执行系统调用或 IO请求）的时候，可以将和它绑定的 P 上的 G 转移到其他线程 M 去执行，如果直接把可运行 G 组成的本地队列绑定到 M，则万一当前 M 阻塞，它拥有的 G 就不能给到其他 M 去执行了。</p><p>基于 GMP 模型的 Go 调度器的核心思想是：</p><p>\1. 尽可能复用线程 M：避免频繁的线程创建和销毁；</p><p>\2. 利用多核并行能力：限制同时运行（不包含阻塞）的 M 线程数为 N，N 等于 CPU 的核心数目，这里通过设置 P 处理器的个数为 GOMAXPROCS 来保证，GOMAXPROCS 一般为 CPU 核数，因为 M 和 P 是一一绑定的，没有找到 P 的 M 会放入空闲 M 列表，没有找到 M 的 P 也会放入空闲 P 列表；</p><p>\3. Work Stealing 任务窃取机制：M 优先执行其所绑定的 P 的本地队列的 G，如果本地队列为空，可以从全局队列获取 G 运行，也可以从其他 M 偷取 G 来运行；为了提高并发执行的效率，M 可以从其他 M 绑定的 P 的运行队列偷取 G 执行，这种 GMP 调度模型也叫任务窃取调度模型，这里，任务就是指 G；</p><p>\4. Hand Off 交接机制：M 阻塞，会将 M 上 P 的运行队列交给其他 M 执行，交接效率要高，才能提高 Go 程序整体的并发度；</p><p>\5. 基于协作的抢占机制：每个真正运行的G，如果不被打断，将会一直运行下去，为了保证公平，防止新创建的 G 一直获取不到 M 执行造成饥饿问题，Go 程序会保证每个 G 运行10ms 就要让出 M，交给其他 G 去执行；</p><p>\6. 基于信号的真抢占机制：尽管基于协作的抢占机制能够缓解长时间 GC 导致整个程序无法工作和大多数 Goroutine 饥饿问题，但是还是有部分情况下，Go调度器有无法被抢占的情况，例如，for 循环或者垃圾回收长时间占用线程，为了解决这些问题， Go1.14 引入了基于信号的抢占式调度机制，能够解决 GC 垃圾回收和栈扫描时存在的问题。</p><h3 id="h-2" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2. 多图详解几种常见的调度场景</h3><p>在进入 GMP 调度模型的数据结构和源码之前，可以先用几张图形象的描述下 GMP 调度机制的一些场景，帮助理解 GMP 调度器为了保证公平性、可扩展性、及提高并发效率，所设计的一些机制和策略。</p><p>1）创建 G： 正在 M1 上运行的P，有一个G1，通过go func() 创建 G2 后，由于局部性，G2优先放入P的本地队列；</p><p><em>图2.1 正在M1上运行的G1通过go func() 创建 G2 后，由于局部性，G2优先放入P的本地队列</em></p><p>2）G 运行完成后：M1 上的 G1 运行完成后（调用goexit()函数），M1 上运行的 Goroutine 会切换为 G0，G0 负责调度协程的切换（运行schedule() 函数），从 M1 上 P 的本地运行队列获取 G2 去执行（函数execute()）；注意：这里 G0 是程序启动时的线程 M（也叫M0）的系统栈表示的 G 结构体，负责 M 上 G 的调度；</p><p><em>图2.2 M1 上的 G1 运行完会切换到 P 本地队列的 G2 运行</em></p><p>3）M 上创建的 G 个数大于本地队列长度时：如果 P 本地队列最多能存 4 个G（实际上是256个），正在 M1 上运行的 G2 要通过go func()创建 6 个G，那么，前 4 个G 放在 P 本地队列中，G2 创建了第 5 个 G（G7）时，P 本地队列中前一半和 G7 一起打乱顺序放入全局队列，P 本地队列剩下的 G 往前移动，G2 创建的第 6 个 G（G8）时，放入 P 本地队列中，因为还有空间；</p><p><em>图2.3 M1上的G2要创建的G个数多于P本地队列能够存放的G个数时</em></p><p>4）M 的自旋状态：创建新的 G 时，运行的 G 会尝试唤醒其他空闲的 M 绑定 P 去执行，如果 G2 唤醒了M2，M2 绑定了一个 P2，会先运行 M2 的 G0，这时 M2 没有从 P2 的本地队列中找到 G，会进入自旋状态（spinning），自旋状态的 M2 会尝试从全局空闲线程队列里面获取 G，放到 P2 本地队列去执行，获取的数量满足公式：n = min(len(globrunqsize)/GOMAXPROCS + 1, len(localrunsize/2))，含义是每个P应该从全局队列承担的 G 数量，为了提高效率，不能太多，要给其他 P 留点；</p><p><em>图2.4 创建新的 G 时，运行的G会尝试唤醒其他空闲的M绑定P去执行</em></p><p>5）任务窃取机制：自旋状态的 M 会寻找可运行的 G，如果全局队列为空，则会从其他 P 偷取 G 来执行，个数是其他 P 运行队列的一半；</p><p><em>图2.5 自旋状态的 M 会寻找可运行的 G，如果全局队列为空，则会从其他 P 偷取 G 来执行</em></p><p>6）G 发生系统调用时：如果 G 发生系统调度进入阻塞，其所在的 M 也会阻塞，因为会进入内核状态等待系统资源，和 M 绑定的 P 会寻找空闲的 M 执行，这是为了提高效率，不能让 P 本地队列的 G 因所在 M 进入阻塞状态而无法执行；需要说明的是，如果是 M 上的 G 进入 Channel 阻塞，则该 M 不会一起进入阻塞，因为 Channel 数据传输涉及内存拷贝，不涉及系统资源等待；</p><p><em>图2.6 如果 G 发生阻塞，其所在的 M 也会阻塞，和 M 绑定的 P 会寻找空闲的 M 执行</em></p><p>7）G 退出系统调用时：如果刚才进入系统调用的 G2 解除了阻塞，其所在的 M1 会寻找 P 去执行，优先找原来的 P，发现没有找到，则其上的 G2 会进入全局队列，等其他 M 获取执行，M1 进入空闲队列；</p><p><em>图 2.7 当 G 解除阻塞时，所在的 M会寻找 P 去执行，如果没有找到，则 G 进入全局队列，M 进入空闲队列</em></p><h3 id="h-3-gmp" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3. GMP的数据结构和各种状态</h3><h3 id="h-31-g" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.1 G 的数据结构和状态</h3><p>G 的数据结构是：</p><pre data-type="codeBlock" text="// src/runtime/runtime2.go
type g struct {
 stack       stack       // 描述了当前 Goroutine 的栈内存范围 [stack.lo, stack.hi)
 stackguard0 uintptr     // 用于调度器抢占式调度
 _panic      *_panic     // 最内侧的 panic 结构体
 _defer      *_defer     // 最内侧的 defer 延迟函数结构体
 m           *m          // 当前 G 占用的线程，可能为空
 sched       gobuf       //  存储 G 的调度相关的数据
 atomicstatus uint32     // G 的状态
 goid         int64      //  G 的 ID
 waitreason   waitReason //当状态status==Gwaiting时等待的原因
 preempt       bool      // 抢占信号
 preemptStop   bool      // 抢占时将状态修改成 `_Gpreempted`
 preemptShrink bool      // 在同步安全点收缩栈
 lockedm        muintptr   //G 被锁定只能在这个 m 上运行
 waiting        *sudog     // 这个 g 当前正在阻塞的 sudog 结构体
 ......
}
"><code><span class="hljs-comment">// src/runtime/runtime2.go</span>
<span class="hljs-keyword">type</span> g <span class="hljs-keyword">struct</span> {
 stack       stack       <span class="hljs-comment">// 描述了当前 Goroutine 的栈内存范围 [stack.lo, stack.hi)</span>
 stackguard0 uintptr     <span class="hljs-comment">// 用于调度器抢占式调度</span>
 _panic      <span class="hljs-operator">*</span>_panic     <span class="hljs-comment">// 最内侧的 panic 结构体</span>
 _defer      <span class="hljs-operator">*</span>_defer     <span class="hljs-comment">// 最内侧的 defer 延迟函数结构体</span>
 m           <span class="hljs-operator">*</span>m          <span class="hljs-comment">// 当前 G 占用的线程，可能为空</span>
 sched       gobuf       <span class="hljs-comment">//  存储 G 的调度相关的数据</span>
 atomicstatus <span class="hljs-keyword">uint32</span>     <span class="hljs-comment">// G 的状态</span>
 goid         <span class="hljs-keyword">int64</span>      <span class="hljs-comment">//  G 的 ID</span>
 waitreason   waitReason <span class="hljs-comment">//当状态status==Gwaiting时等待的原因</span>
 preempt       <span class="hljs-keyword">bool</span>      <span class="hljs-comment">// 抢占信号</span>
 preemptStop   <span class="hljs-keyword">bool</span>      <span class="hljs-comment">// 抢占时将状态修改成 `_Gpreempted`</span>
 preemptShrink <span class="hljs-keyword">bool</span>      <span class="hljs-comment">// 在同步安全点收缩栈</span>
 lockedm        muintptr   <span class="hljs-comment">//G 被锁定只能在这个 m 上运行</span>
 waiting        <span class="hljs-operator">*</span>sudog     <span class="hljs-comment">// 这个 g 当前正在阻塞的 sudog 结构体</span>
 ......
}
</code></pre><p>G 的主要字段有：</p><p>​ stack：描述了当前 Goroutine 的栈内存范围 [stack.lo, stack.hi)；</p><p>​ stackguard0： 可以用于调度器抢占式调度；preempt，preemptStop，preemptShrink跟抢占相关；</p><p>​ defer 和 panic：分别记录这个 G 最内侧的panic和 _defer结构体；</p><p>​ m：记录当前 G 占用的线程 M，可能为空；</p><p>​ atomicstatus：表示G 的状态；</p><p>​ sched：存储 G 的调度相关的数据；</p><p>​ goid：表示 G 的 ID，对开发者不可见；</p><p>需要展开描述的是sched 字段的 runtime.gobuf 结构体：</p><pre data-type="codeBlock" text="type gobuf struct {
 sp   uintptr      // 栈指针
 pc   uintptr      // 程序计数器，记录G要执行的下一条指令位置
 g    guintptr     // 持有 runtime.gobuf 的 G
 ret  uintptr      // 系统调用的返回值
 ......
}
"><code><span class="hljs-keyword">type</span> gobuf <span class="hljs-keyword">struct</span> {
 sp   <span class="hljs-type">uintptr</span>      <span class="hljs-comment">// 栈指针</span>
 pc   <span class="hljs-type">uintptr</span>      <span class="hljs-comment">// 程序计数器，记录G要执行的下一条指令位置</span>
 g    guintptr     <span class="hljs-comment">// 持有 runtime.gobuf 的 G</span>
 ret  <span class="hljs-type">uintptr</span>      <span class="hljs-comment">// 系统调用的返回值</span>
 ......
}
</code></pre><p>这些字段会在调度器将当前 G 切换离开 M 和调度进入 M 执行程序时用到，栈指针 sp 和程序计数器 pc 用来存放或恢复寄存器中的值，改变程序执行的指令。</p><p>结构体 runtime.g 的 atomicstatus 字段存储了当前 G 的状态，G 可能处于以下状态：</p><pre data-type="codeBlock" text="const (
 // _Gidle 表示 G 刚刚被分配并且还没有被初始化
 _Gidle = iota // 0
 // _Grunnable 表示 G  没有执行代码，没有栈的所有权，存储在运行队列中
 _Grunnable // 1
 // _Grunning 可以执行代码，拥有栈的所有权，被赋予了内核线程 M 和处理器 P
 _Grunning // 2
 // _Gsyscall 正在执行系统调用，拥有栈的所有权，没有执行用户代码，被赋予了内核线程 M 但是不在运行队列上
 _Gsyscall // 3
 // _Gwaiting 由于运行时而被阻塞，没有执行用户代码并且不在运行队列上，但是可能存在于 Channel 的等待队列上
 _Gwaiting // 4
 // _Gdead 没有被使用，没有执行代码，可能有分配的栈
 _Gdead // 6
 // _Gcopystack 栈正在被拷贝，没有执行代码，不在运行队列上
 _Gcopystack // 8
 // _Gpreempted 由于抢占而被阻塞，没有执行用户代码并且不在运行队列上，等待唤醒
 _Gpreempted // 9
 // _Gscan GC 正在扫描栈空间，没有执行代码，可以与其他状态同时存在
 _Gscan          = 0x1000
 ......
)
"><code>const (
 <span class="hljs-comment">// _Gidle 表示 G 刚刚被分配并且还没有被初始化</span>
 _Gidle <span class="hljs-operator">=</span> iota <span class="hljs-comment">// 0</span>
 <span class="hljs-comment">// _Grunnable 表示 G  没有执行代码，没有栈的所有权，存储在运行队列中</span>
 _Grunnable <span class="hljs-comment">// 1</span>
 <span class="hljs-comment">// _Grunning 可以执行代码，拥有栈的所有权，被赋予了内核线程 M 和处理器 P</span>
 _Grunning <span class="hljs-comment">// 2</span>
 <span class="hljs-comment">// _Gsyscall 正在执行系统调用，拥有栈的所有权，没有执行用户代码，被赋予了内核线程 M 但是不在运行队列上</span>
 _Gsyscall <span class="hljs-comment">// 3</span>
 <span class="hljs-comment">// _Gwaiting 由于运行时而被阻塞，没有执行用户代码并且不在运行队列上，但是可能存在于 Channel 的等待队列上</span>
 _Gwaiting <span class="hljs-comment">// 4</span>
 <span class="hljs-comment">// _Gdead 没有被使用，没有执行代码，可能有分配的栈</span>
 _Gdead <span class="hljs-comment">// 6</span>
 <span class="hljs-comment">// _Gcopystack 栈正在被拷贝，没有执行代码，不在运行队列上</span>
 _Gcopystack <span class="hljs-comment">// 8</span>
 <span class="hljs-comment">// _Gpreempted 由于抢占而被阻塞，没有执行用户代码并且不在运行队列上，等待唤醒</span>
 _Gpreempted <span class="hljs-comment">// 9</span>
 <span class="hljs-comment">// _Gscan GC 正在扫描栈空间，没有执行代码，可以与其他状态同时存在</span>
 _Gscan          <span class="hljs-operator">=</span> <span class="hljs-number">0x1000</span>
 ......
)
</code></pre><p>其中主要的六种状态是：</p><p>​ Gidle：G 被创建但还未完全被初始化；</p><p>​ Grunnable：当前 G 为可运行的，正在等待被运行；</p><p>​ Grunning：当前 G 正在被运行；</p><p>​ Gsyscall：当前 G 正在被系统调用；</p><p>​ Gwaiting：当前 G 正在因某个原因而等待；</p><p>​ Gdead：当前 G 完成了运行；</p><p>图3.1描述了G从创建到结束的生命周期中经历的各种状态变化过程：</p><p><em>图3.1 G的状态变化</em></p><p>虽然 G 在运行时中定义的状态较多且复杂，但是我们可以将这些不同的状态聚合成三种：等待中、可运行、运行中，分别由_Gwaiting、_Grunnable、_Grunning 三种状态表示，运行期间大部分情况是在这三种状态来回切换：</p><p>​ 等待中：G 正在等待某些条件满足，例如：系统调用结束等，包括 _Gwaiting、_Gsyscall 几个状态； ​ 可运行：G 已经准备就绪，可以在线程 M 上运行，如果当前程序中有非常多的 G，每个 G 就可能会等待更多的时间，即 _Grunnable； ​ 运行中：G 正在某个线程 M 上运行，即 _Grunning。</p><h3 id="h-32-m" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.2 M 的数据结构</h3><p>M 的数据结构是：</p><pre data-type="codeBlock" text="// src/runtime/runtime2.go
type m struct {
 g0            *g          // 持有调度栈的 G
 gsignal       *g                // 处理 signal 的 g
 tls           [tlsSlots]uintptr // 线程本地存储
        mstartfn      func()      // M的起始函数，go语句携带的那个函数
 curg          *g          // 在当前线程上运行的 G
 p             puintptr    // 执行 go 代码时持有的 p (如果没有执行则为 nil)
 nextp         puintptr    // 用于暂存与当前 M 有潜在关联的 P
 oldp          puintptr    // 执行系统调用前绑定的 P
 spinning      bool        // 表示当前 M 是否正在寻找 G，在寻找过程中 M 处于自旋状态
 lockedg       guintptr    // 表示与当前 M 锁定的那个 G
 .....
}
"><code><span class="hljs-comment">// src/runtime/runtime2.go</span>
<span class="hljs-keyword">type</span> m <span class="hljs-keyword">struct</span> {
 g0            <span class="hljs-operator">*</span>g          <span class="hljs-comment">// 持有调度栈的 G</span>
 gsignal       <span class="hljs-operator">*</span>g                <span class="hljs-comment">// 处理 signal 的 g</span>
 tls           [tlsSlots]uintptr <span class="hljs-comment">// 线程本地存储</span>
        mstartfn      func()      <span class="hljs-comment">// M的起始函数，go语句携带的那个函数</span>
 curg          <span class="hljs-operator">*</span>g          <span class="hljs-comment">// 在当前线程上运行的 G</span>
 p             puintptr    <span class="hljs-comment">// 执行 go 代码时持有的 p (如果没有执行则为 nil)</span>
 nextp         puintptr    <span class="hljs-comment">// 用于暂存与当前 M 有潜在关联的 P</span>
 oldp          puintptr    <span class="hljs-comment">// 执行系统调用前绑定的 P</span>
 spinning      <span class="hljs-keyword">bool</span>        <span class="hljs-comment">// 表示当前 M 是否正在寻找 G，在寻找过程中 M 处于自旋状态</span>
 lockedg       guintptr    <span class="hljs-comment">// 表示与当前 M 锁定的那个 G</span>
 .....
}
</code></pre><p>M 的字段众多，其中最重要的为下面几个：</p><p>​ g0: Go 运行时系统在启动之初创建的，用来调度其他 G 到 M 上；</p><p>​ mstartfn：表示M的起始函数，其实就是go 语句携带的那个函数；</p><p>​ curg：存放当前正在运行的 G 的指针；</p><p>​ p：指向当前与 M 关联的那个 P；</p><p>​ nextp：用于暂存于当前 M 有潜在关联的 P；</p><p>​ spinning：表示当前 M 是否正在寻找 G，在寻找过程中 M 处于自旋状态；</p><p>​ lockedg：表示与当前M锁定的那个 G，运行时系统会把 一个 M 和一个 G 锁定，一旦锁定就只能双方相互作用，不接受第三者；</p><p>M 并没有像 G 和 P 一样的状态标记, 但可以认为一个 M 有以下的状态:</p><p>​ 自旋中(spinning): M 正在从运行队列获取 G, 这时候 M 会拥有一个 P；</p><p>​ 执行go代码中: M 正在执行go代码, 这时候 M 会拥有一个 P；</p><p>​ 执行原生代码中: M 正在执行原生代码或者阻塞的syscall, 这时M并不拥有P；</p><p>​ 休眠中: M 发现无待运行的 G 时会进入休眠, 并添加到空闲 M 链表中, 这时 M 并不拥有 P。</p><h3 id="h-33-p" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.3 P 的数据结构</h3><p>P 的数据结构是：</p><pre data-type="codeBlock" text="// src/runtime/runtime2.go
type p struct {
 status      uint32      // p 的状态 pidle/prunning/...
 schedtick   uint32      // 每次执行调度器调度 +1
 syscalltick uint32      // 每次执行系统调用 +1
 m           muintptr    // 关联的 m 
 mcache      *mcache     // 用于 P 所在的线程 M 的内存分配的 mcache
 deferpool    []*_defer  // 本地 P 队列的 defer 结构体池
 // 可运行的 Goroutine 队列，可无锁访问
 runqhead uint32
 runqtail uint32
 runq     [256]guintptr
 // 线程下一个需要执行的 G
 runnext guintptr
 // 空闲的 G 队列，G 状态 status 为 _Gdead，可重新初始化使用
 gFree struct {
  gList
  n int32
 }
        ......
}
"><code><span class="hljs-comment">// src/runtime/runtime2.go</span>
<span class="hljs-keyword">type</span> p <span class="hljs-keyword">struct</span> {
 status      <span class="hljs-keyword">uint32</span>      <span class="hljs-comment">// p 的状态 pidle/prunning/...</span>
 schedtick   <span class="hljs-keyword">uint32</span>      <span class="hljs-comment">// 每次执行调度器调度 +1</span>
 syscalltick <span class="hljs-keyword">uint32</span>      <span class="hljs-comment">// 每次执行系统调用 +1</span>
 m           muintptr    <span class="hljs-comment">// 关联的 m </span>
 mcache      <span class="hljs-operator">*</span>mcache     <span class="hljs-comment">// 用于 P 所在的线程 M 的内存分配的 mcache</span>
 deferpool    []<span class="hljs-operator">*</span>_defer  <span class="hljs-comment">// 本地 P 队列的 defer 结构体池</span>
 <span class="hljs-comment">// 可运行的 Goroutine 队列，可无锁访问</span>
 runqhead <span class="hljs-keyword">uint32</span>
 runqtail <span class="hljs-keyword">uint32</span>
 runq     [<span class="hljs-number">256</span>]guintptr
 <span class="hljs-comment">// 线程下一个需要执行的 G</span>
 runnext guintptr
 <span class="hljs-comment">// 空闲的 G 队列，G 状态 status 为 _Gdead，可重新初始化使用</span>
 gFree <span class="hljs-keyword">struct</span> {
  gList
  n <span class="hljs-keyword">int32</span>
 }
        ......
}
</code></pre><p>最主要的数据结构是 status 表示 P 的不同的状态，而 runqhead、runqtail 和 runq 三个字段表示处理器持有的运行队列，是一个长度为256的环形队列，其中存储着待执行的 G 列表，runnext 中是线程下一个需要执行的 G；gFree 存储 P 本地的状态为_Gdead 的空闲的 G，可重新初始化使用。</p><p>P 结构体中的状态 status 字段会是以下五种中的一种：</p><p>​ _Pidle：P 没有运行用户代码或者调度器，被空闲队列或者改变其状态的结构持有，运行队列为空；</p><p>​ _Prunning：被线程 M 持有，并且正在执行用户代码或者调度器；</p><p>​ _Psyscall：没有执行用户代码，当前线程陷入系统调用；</p><p>​ _Pgcstop：被线程 M 持有，当前处理器由于垃圾回收被停止；</p><p>​ _Pdead：当前 P 已经不被使用；</p><p>P 的五种状态之间的转化关系如图 3.2 所示：</p><p><em>图3.2 P的状态变化</em></p><h3 id="h-34-schedt" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.4 schedt 的数据结构</h3><p>调度器的schedt结构体存储了全局的 G 队列，空闲的 M 列表和 P 列表：</p><pre data-type="codeBlock" text="// src/runtime/runtime2.go
type schedt struct {
 lock mutex            // schedt的锁
 midle        muintptr // 空闲的M列表
 nmidle       int32    // 空闲的M列表的数量
 nmidlelocked int32    // 被锁定正在工作的M数
 mnext        int64    // 下一个被创建的 M 的 ID
 maxmcount    int32    // 能拥有的最大数量的 M
 pidle      puintptr   // 空闲的 P 链表
 npidle     uint32     // 空闲 P 数量
 nmspinning uint32     // 处于自旋状态的 M 的数量
 // 全局可执行的 G 列表
 runq     gQueue
 runqsize int32        // 全局可执行 G 列表的大小
 // 全局 _Gdead 状态的空闲 G 列表
 gFree struct {
  lock    mutex
  stack   gList // Gs with stacks
  noStack gList // Gs without stacks
  n       int32
 }
 // sudog结构的集中存储
 sudoglock  mutex
 sudogcache *sudog
 // 有效的 defer 结构池
 deferlock mutex
 deferpool *_defer
        ......
}
"><code><span class="hljs-comment">// src/runtime/runtime2.go</span>
<span class="hljs-keyword">type</span> schedt <span class="hljs-keyword">struct</span> {
 lock mutex            <span class="hljs-comment">// schedt的锁</span>
 midle        muintptr <span class="hljs-comment">// 空闲的M列表</span>
 nmidle       <span class="hljs-keyword">int32</span>    <span class="hljs-comment">// 空闲的M列表的数量</span>
 nmidlelocked <span class="hljs-keyword">int32</span>    <span class="hljs-comment">// 被锁定正在工作的M数</span>
 mnext        <span class="hljs-keyword">int64</span>    <span class="hljs-comment">// 下一个被创建的 M 的 ID</span>
 maxmcount    <span class="hljs-keyword">int32</span>    <span class="hljs-comment">// 能拥有的最大数量的 M</span>
 pidle      puintptr   <span class="hljs-comment">// 空闲的 P 链表</span>
 npidle     <span class="hljs-keyword">uint32</span>     <span class="hljs-comment">// 空闲 P 数量</span>
 nmspinning <span class="hljs-keyword">uint32</span>     <span class="hljs-comment">// 处于自旋状态的 M 的数量</span>
 <span class="hljs-comment">// 全局可执行的 G 列表</span>
 runq     gQueue
 runqsize <span class="hljs-keyword">int32</span>        <span class="hljs-comment">// 全局可执行 G 列表的大小</span>
 <span class="hljs-comment">// 全局 _Gdead 状态的空闲 G 列表</span>
 gFree <span class="hljs-keyword">struct</span> {
  lock    mutex
  stack   gList <span class="hljs-comment">// Gs with stacks</span>
  noStack gList <span class="hljs-comment">// Gs without stacks</span>
  n       <span class="hljs-keyword">int32</span>
 }
 <span class="hljs-comment">// sudog结构的集中存储</span>
 sudoglock  mutex
 sudogcache <span class="hljs-operator">*</span>sudog
 <span class="hljs-comment">// 有效的 defer 结构池</span>
 deferlock mutex
 deferpool <span class="hljs-operator">*</span>_defer
        ......
}
</code></pre><p>除了上面的四个结构体，还有一些全局变量：</p><pre data-type="codeBlock" text="// src/runtime/runtime2.go
var (
 allm       *m         // 所有的 M
 gomaxprocs int32      // P 的个数，默认为 ncpu 核数
 ncpu       int32
 ......
 sched      schedt     // schedt 全局结构体
 newprocs   int32

 allpLock mutex       // 全局 P 队列的锁
 allp []*p            // 全局 P 队列，个数为 gomaxprocs
        ......
}
"><code><span class="hljs-comment">// src/runtime/runtime2.go</span>
<span class="hljs-keyword">var</span> (
 allm       <span class="hljs-operator">*</span>m         <span class="hljs-comment">// 所有的 M</span>
 gomaxprocs <span class="hljs-keyword">int32</span>      <span class="hljs-comment">// P 的个数，默认为 ncpu 核数</span>
 ncpu       <span class="hljs-keyword">int32</span>
 ......
 sched      schedt     <span class="hljs-comment">// schedt 全局结构体</span>
 newprocs   <span class="hljs-keyword">int32</span>

 allpLock mutex       <span class="hljs-comment">// 全局 P 队列的锁</span>
 allp []<span class="hljs-operator">*</span>p            <span class="hljs-comment">// 全局 P 队列，个数为 gomaxprocs</span>
        ......
}
</code></pre><p>此外，src/runtime/proc.go 文件有两个全局变量：</p><pre data-type="codeBlock" text="// src/runtime/proc.go 
var (
 m0           m       //  进程启动后的初始线程
 g0            g      //  代表着初始线程的stack
 ......
)
"><code><span class="hljs-comment">// src/runtime/proc.go </span>
<span class="hljs-keyword">var</span> (
 m0           m       <span class="hljs-comment">//  进程启动后的初始线程</span>
 g0            g      <span class="hljs-comment">//  代表着初始线程的stack</span>
 ......
)
</code></pre><p>到这里，G、M、P、schedt结构体和全局变量都描述完毕，GMP 的全部队列如下表3-1所示：</p><p><em>表3-1 GMP的队列</em></p><p>中文名源码的名称作用域简要说明全局M列表runtime.allm运行时系统存放所有M全局P列表runtime.allp运行时系统存放所有P调度器中的空闲M列表runtime.schedt.midle调度器存放空闲M调度器中的空闲P列表runtime.schedt.pidle调度器存放空闲P调度器中的可运行G队列runtime.schedt.runq调度器存放可运行GP的本地可运行G队列runtime.p.runq本地P存放当前P中的可运行G调度器中的空闲G列表runtime.schedt.gfree调度器存放空闲的GP中的空闲G列表runtime.p.gfree本地P存放当前P中的空闲G</p><h3 id="h-4" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">4. 调度器的启动</h3><h3 id="h-41" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">4.1 程序启动流程</h3><p>Go 程序一启动，Go 的运行时 runtime 自带的调度器 scheduler 就开始启动了。</p><p>对于一个最简单的Go程序：</p><pre data-type="codeBlock" text="package main

import &quot;fmt&quot;

func main() {
 fmt.Println(&quot;hello world&quot;)
}
"><code><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
 fmt.Println(<span class="hljs-string">"hello world"</span>)
}
</code></pre><p>通过 gdb或dlv的方式调试，会发现程序的真正入口不是在 runtime.main，对 AMD64 架构上的 Linux 和 macOS 服务器来说，分别在runtime包的 src/runtime/rt0_linux_amd64.s 和 src/runtime/rt0_darwin_amd64.s：</p><pre data-type="codeBlock" text="TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
 JMP _rt0_amd64(SB)
TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
 JMP _rt0_amd64(SB)
"><code>TEXT <span class="hljs-built_in">_rt0_amd64_linux</span>(SB),NOSPLIT,<span class="hljs-variable">$-8</span>
 JMP <span class="hljs-built_in">_rt0_amd64</span>(SB)
TEXT <span class="hljs-built_in">_rt0_amd64_darwin</span>(SB),NOSPLIT,<span class="hljs-variable">$-8</span>
 JMP <span class="hljs-built_in">_rt0_amd64</span>(SB)
</code></pre><p>两者均跳转到了 src/runtime/asm_amd64.s 包的 _rt0_amd64 函数：</p><pre data-type="codeBlock" text="TEXT _rt0_amd64(SB),NOSPLIT,$-8
 MOVQ 0(SP), DI // argc
 LEAQ 8(SP), SI // argv
 JMP runtime·rt0_go(SB)
"><code>TEXT <span class="hljs-built_in">_rt0_amd64</span>(SB),NOSPLIT,<span class="hljs-variable">$-8</span>
 MOVQ <span class="hljs-number">0</span>(SP), DI <span class="hljs-comment">// argc</span>
 LEAQ <span class="hljs-number">8</span>(SP), SI <span class="hljs-comment">// argv</span>
 JMP runtime·<span class="hljs-built_in">rt0_go</span>(SB)
</code></pre><p>_rt0_amd64 函数调用了 src/runtime/asm_arm64.s 包的 runtime·rt0_go 函数：</p><pre data-type="codeBlock" text="TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
 ......
 // 初始化g0
 MOVD $runtime·g0(SB), g
        ......
 // 初始化 m0
 MOVD $runtime·m0(SB), R0
// 绑定 g0 和 m0
 MOVD g, m_g0(R0)
 MOVD R0, g_m(g)
        ......
 BL runtime·schedinit(SB)      // 调度器初始化

 // 创建一个新的 goroutine 来启动程序
 MOVD $runtime·mainPC(SB), R0    // main函数入口 
 .......
        BL runtime·newproc(SB)        // 负责根据主函数即 main 的入口地址创建可被运行时调度的执行单元goroutine
 .......

 // 开始启动调度器的调度循环
 BL runtime·mstart(SB)
 ......

DATA runtime·mainPC+0(SB)/8,$runtime·main&lt;ABIInternal&gt;(SB)    // main函数入口地址
GLOBL runtime·mainPC(SB),RODATA,$8
"><code>TEXT runtime·<span class="hljs-built_in">rt0_go</span>(SB),NOSPLIT|TOPFRAME,$<span class="hljs-number">0</span>
 ......
 <span class="hljs-comment">// 初始化g0</span>
 MOVD <span class="hljs-variable">$runtime</span>·<span class="hljs-built_in">g0</span>(SB), g
        ......
 <span class="hljs-comment">// 初始化 m0</span>
 MOVD <span class="hljs-variable">$runtime</span>·<span class="hljs-built_in">m0</span>(SB), R0
<span class="hljs-comment">// 绑定 g0 和 m0</span>
 MOVD g, <span class="hljs-built_in">m_g0</span>(R0)
 MOVD R0, <span class="hljs-built_in">g_m</span>(g)
        ......
 BL runtime·<span class="hljs-built_in">schedinit</span>(SB)      <span class="hljs-comment">// 调度器初始化</span>

 <span class="hljs-comment">// 创建一个新的 goroutine 来启动程序</span>
 MOVD <span class="hljs-variable">$runtime</span>·<span class="hljs-built_in">mainPC</span>(SB), R0    <span class="hljs-comment">// main函数入口 </span>
 .......
        BL runtime·<span class="hljs-built_in">newproc</span>(SB)        <span class="hljs-comment">// 负责根据主函数即 main 的入口地址创建可被运行时调度的执行单元goroutine</span>
 .......

 <span class="hljs-comment">// 开始启动调度器的调度循环</span>
 BL runtime·<span class="hljs-built_in">mstart</span>(SB)
 ......

DATA runtime·mainPC+<span class="hljs-number">0</span>(SB)/<span class="hljs-number">8</span>,<span class="hljs-variable">$runtime</span>·<span class="hljs-selector-tag">main</span>&#x3C;ABIInternal>(SB)    <span class="hljs-comment">// main函数入口地址</span>
GLOBL runtime·<span class="hljs-built_in">mainPC</span>(SB),RODATA,$<span class="hljs-number">8</span>
</code></pre><p>Go程序的真正启动函数 runtime·rt0_go 主要做了几件事：</p><p>1）初始化 g0 和 m0，并将二者互相绑定， m0 是程序启动后的初始线程，g0 是 m0 的系统栈代表的 G 结构体，负责普通 G 在M 上的调度切换；</p><p>2）schedinit：进行各种运行时组件初始化工作，这包括我们的调度器与内存分配器、回收器的初始化；</p><p>3）newproc：负责根据主函数即 main 的入口地址创建可被运行时调度的执行单元；</p><p>4）mstart：开始启动调度器的调度循环；</p><p>阅读 Go 调度器的源码，需要先从整体结构上对其有个把握，Go 程序启动后的调度器主逻辑如图 4.1 所示：</p><p><em>图4.1 调度器主逻辑</em></p><p>下面分为两部分来分析调度器的原理：调度器的启动和调度循环。</p><h3 id="h-42" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">4.2 调度器的启动</h3><p>调度器启动函数在 src/runtime/proc.go 包的 schedinit() 函数：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
// 调度器初始化
func schedinit() {
 ......
 _g_ := getg()   
 ......
        // 设置机器线程数M最大为10000
 sched.maxmcount = 10000
        ......
 // 栈、内存分配器相关初始化
 stackinit()          // 初始化栈
 mallocinit()         // 初始化内存分配器
 ......
 // 初始化当前系统线程 M0
 mcommoninit(_g_.m, -1)
 ......
        // GC初始化
 gcinit()
        ......
        // 设置P的值为GOMAXPROCS个数
 procs := ncpu
 if n, ok := atoi32(gogetenv(&quot;GOMAXPROCS&quot;)); ok &amp;&amp; n &gt; 0 {
  procs = n
 }
        // 调用procresize调整 P 列表
 if procresize(procs) != nil {
  throw(&quot;unknown runnable goroutine during bootstrap&quot;)
 }
 ......
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
<span class="hljs-comment">// 调度器初始化</span>
func schedinit() {
 ......
 _g_ :<span class="hljs-operator">=</span> getg()   
 ......
        <span class="hljs-comment">// 设置机器线程数M最大为10000</span>
 sched.maxmcount <span class="hljs-operator">=</span> <span class="hljs-number">10000</span>
        ......
 <span class="hljs-comment">// 栈、内存分配器相关初始化</span>
 stackinit()          <span class="hljs-comment">// 初始化栈</span>
 mallocinit()         <span class="hljs-comment">// 初始化内存分配器</span>
 ......
 <span class="hljs-comment">// 初始化当前系统线程 M0</span>
 mcommoninit(_g_.m, <span class="hljs-number">-1</span>)
 ......
        <span class="hljs-comment">// GC初始化</span>
 gcinit()
        ......
        <span class="hljs-comment">// 设置P的值为GOMAXPROCS个数</span>
 procs :<span class="hljs-operator">=</span> ncpu
 <span class="hljs-keyword">if</span> n, ok :<span class="hljs-operator">=</span> atoi32(gogetenv(<span class="hljs-string">"GOMAXPROCS"</span>)); ok <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> n <span class="hljs-operator">></span> <span class="hljs-number">0</span> {
  procs <span class="hljs-operator">=</span> n
 }
        <span class="hljs-comment">// 调用procresize调整 P 列表</span>
 <span class="hljs-keyword">if</span> procresize(procs) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"unknown runnable goroutine during bootstrap"</span>)
 }
 ......
}
</code></pre><p>schedinit() 函数会设置 M 最大数量为10000，实际中不会达到；会分别调用stackinit() 、mallocinit() 、mcommoninit() 、gcinit() 等执行 goroutine栈初始化、进行内存分配器初始化、进行系统线程M0的初始化、进行GC垃圾回收器的初始化；接着，将 P 个数设置为 GOMAXPROCS 的值，即程序能够同时运行的最大处理器数，最后会调用 runtime.procresize()函数初始化 P 列表。</p><p><em>图4.2 runtime.schedinit() 函数逻辑</em></p><p>schedinit() 函数负责M、P、G 的初始化过程。M/P/G 彼此的初始化顺序遵循：mcommoninit、procresize、newproc，他们分别负责初始化 M 资源池（allm）、P 资源池（allp）、G 的运行现场（g.sched）以及调度队列（p.runq）。</p><p>mcommoninit() 函数主要负责对 M0 进行一个初步的初始化，并将其添加到 schedt 全局结构体中，这里访问 schedt 会加锁：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
func mcommoninit(mp *m, id int64) {
        ......
 lock(&amp;sched.lock)

 if id &gt;= 0 {
  mp.id = id
 } else { // mReserveID() 会返回 sched.mnext 给当前 m，并对 sched.mnext++，记录新增加的这个 M 到 schedt 全局结构体
  mp.id = mReserveID()
 }

 ......

 // 添加到 allm 中
 mp.alllink = allm

 // 等价于 allm = mp
 atomicstorep(unsafe.Pointer(&amp;allm), unsafe.Pointer(mp))
 unlock(&amp;sched.lock)
        ......
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
func mcommoninit(mp <span class="hljs-operator">*</span>m, id <span class="hljs-keyword">int64</span>) {
        ......
 lock(<span class="hljs-operator">&#x26;</span>sched.lock)

 <span class="hljs-keyword">if</span> id <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  mp.id <span class="hljs-operator">=</span> id
 } <span class="hljs-keyword">else</span> { <span class="hljs-comment">// mReserveID() 会返回 sched.mnext 给当前 m，并对 sched.mnext++，记录新增加的这个 M 到 schedt 全局结构体</span>
  mp.id <span class="hljs-operator">=</span> mReserveID()
 }

 ......

 <span class="hljs-comment">// 添加到 allm 中</span>
 mp.alllink <span class="hljs-operator">=</span> allm

 <span class="hljs-comment">// 等价于 allm = mp</span>
 atomicstorep(unsafe.Pointer(<span class="hljs-operator">&#x26;</span>allm), unsafe.Pointer(mp))
 unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
        ......
}
</code></pre><p>runtime.procresize()函数的逻辑是：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
func procresize(nprocs int32) *p {
 ......
        // 获取先前的 P 个数
 old := gomaxprocs
 ......
 // 如果全局变量 allp 切片中的处理器数量少于期望数量，对 allp 扩容
 if nprocs &gt; int32(len(allp)) {
  // 加锁
  lock(&amp;allpLock)
  if nprocs &lt;= int32(cap(allp)) { // 如果要达到的 P 个数 nprocs 小于当前全局 P 切片到容量
   allp = allp[:nprocs]    // 在当前全局 P 切片上截取前 nprocs 个 P
  } else {
                        // 否则，调大了，超出全局 P 切片的容量，创建容量为 nprocs 的新的 P 切片
   nallp := make([]*p, nprocs)
   // 将原有的 p 复制到新创建的 nallp 中
   copy(nallp, allp[:cap(allp)])
   allp = nallp  // 新的 nallp 切片赋值给旧的 allp
  }
                ......
  unlock(&amp;allpLock)
 }

 // 使用 new 创建新的 P 结构体并调用 runtime.p.init 初始化刚刚扩容的allp列表里的 P
 for i := old; i &lt; nprocs; i++ {
  pp := allp[i]
                // 如果 p 是新创建的(新创建的 p 在数组中为 nil)，则申请新的 P 对象
  if pp == nil {
   pp = new(p)
  }
  pp.init(i)
  atomicstorep(unsafe.Pointer(&amp;allp[i]), unsafe.Pointer(pp))
 }

 _g_ := getg()
        // 当前 G 的 M 上的 P 不为空，并且其 id 小于 nprocs，说明 ID 有效，则可以继续使用当前 G 的 P
 if _g_.m.p != 0 &amp;&amp; _g_.m.p.ptr().id &lt; nprocs {
  // 继续使用当前 P， 其状态设置为 _Prunning
  _g_.m.p.ptr().status = _Prunning
  _g_.m.p.ptr().mcache.prepareForSweep()
 } else {
  // 否则，释放当前 P 并获取 allp[0]
  if _g_.m.p != 0 {
   ......
   _g_.m.p.ptr().m = 0
  }
  _g_.m.p = 0
                // 将处理器 allp[0] 绑定到当前 M
  p := allp[0]
  p.m = 0
  p.status = _Pidle  // P 状态设置为 _Pidle 
  acquirep(p)        // 将allp[0]绑定到当前的 M
  if trace.enabled {
   traceGoStart()
  }
 }

 
 mcache0 = nil

 // 调用 runtime.p.destroy 释放从未使用的 P
 for i := nprocs; i &lt; old; i++ {
  p := allp[i]
  p.destroy()
  // 不能释放 p 本身，因为他可能在 m 进入系统调用时被引用
 }

 // 裁剪 allp，保证allp长度与期望处理器数量相等
 if int32(len(allp)) != nprocs {
  lock(&amp;allpLock)
  allp = allp[:nprocs]
  idlepMask = idlepMask[:maskWords]
  timerpMask = timerpMask[:maskWords]
  unlock(&amp;allpLock)
 }

 var runnablePs *p
        // 将除 allp[0] 之外的处理器 P 全部设置成 _Pidle 并加入到全局的空闲队列中
 for i := nprocs - 1; i &gt;= 0; i-- {
  p := allp[i]
  if _g_.m.p.ptr() == p {    // 跳过当前 P
   continue
  }
  p.status = _Pidle          // 设置 P 的状态为空闲状态
  if runqempty(p) {
   pidleput(p)        // 放入到全局结构体 schedt 的空闲 P 列表中
  } else {
   p.m.set(mget())    // 如果有本地任务，则为其绑定一个 M
   p.link.set(runnablePs)
   runnablePs = p
  }
 }
 stealOrder.reset(uint32(nprocs))
 var int32p *int32 = &amp;gomaxprocs 
 atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
 return runnablePs      // 返回所有包含本地任务的 P 链表
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
func procresize(nprocs <span class="hljs-keyword">int32</span>) <span class="hljs-operator">*</span>p {
 ......
        <span class="hljs-comment">// 获取先前的 P 个数</span>
 old :<span class="hljs-operator">=</span> gomaxprocs
 ......
 <span class="hljs-comment">// 如果全局变量 allp 切片中的处理器数量少于期望数量，对 allp 扩容</span>
 <span class="hljs-keyword">if</span> nprocs <span class="hljs-operator">></span> <span class="hljs-keyword">int32</span>(len(allp)) {
  <span class="hljs-comment">// 加锁</span>
  lock(<span class="hljs-operator">&#x26;</span>allpLock)
  <span class="hljs-keyword">if</span> nprocs <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> <span class="hljs-keyword">int32</span>(cap(allp)) { <span class="hljs-comment">// 如果要达到的 P 个数 nprocs 小于当前全局 P 切片到容量</span>
   allp <span class="hljs-operator">=</span> allp[:nprocs]    <span class="hljs-comment">// 在当前全局 P 切片上截取前 nprocs 个 P</span>
  } <span class="hljs-keyword">else</span> {
                        <span class="hljs-comment">// 否则，调大了，超出全局 P 切片的容量，创建容量为 nprocs 的新的 P 切片</span>
   nallp :<span class="hljs-operator">=</span> make([]<span class="hljs-operator">*</span>p, nprocs)
   <span class="hljs-comment">// 将原有的 p 复制到新创建的 nallp 中</span>
   copy(nallp, allp[:cap(allp)])
   allp <span class="hljs-operator">=</span> nallp  <span class="hljs-comment">// 新的 nallp 切片赋值给旧的 allp</span>
  }
                ......
  unlock(<span class="hljs-operator">&#x26;</span>allpLock)
 }

 <span class="hljs-comment">// 使用 new 创建新的 P 结构体并调用 runtime.p.init 初始化刚刚扩容的allp列表里的 P</span>
 <span class="hljs-keyword">for</span> i :<span class="hljs-operator">=</span> old; i <span class="hljs-operator">&#x3C;</span> nprocs; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span> {
  pp :<span class="hljs-operator">=</span> allp[i]
                <span class="hljs-comment">// 如果 p 是新创建的(新创建的 p 在数组中为 nil)，则申请新的 P 对象</span>
  <span class="hljs-keyword">if</span> pp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
   pp <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span>(p)
  }
  pp.init(i)
  atomicstorep(unsafe.Pointer(<span class="hljs-operator">&#x26;</span>allp[i]), unsafe.Pointer(pp))
 }

 _g_ :<span class="hljs-operator">=</span> getg()
        <span class="hljs-comment">// 当前 G 的 M 上的 P 不为空，并且其 id 小于 nprocs，说明 ID 有效，则可以继续使用当前 G 的 P</span>
 <span class="hljs-keyword">if</span> _g_.m.p <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> _g_.m.p.ptr().id <span class="hljs-operator">&#x3C;</span> nprocs {
  <span class="hljs-comment">// 继续使用当前 P， 其状态设置为 _Prunning</span>
  _g_.m.p.ptr().status <span class="hljs-operator">=</span> _Prunning
  _g_.m.p.ptr().mcache.prepareForSweep()
 } <span class="hljs-keyword">else</span> {
  <span class="hljs-comment">// 否则，释放当前 P 并获取 allp[0]</span>
  <span class="hljs-keyword">if</span> _g_.m.p <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
   ......
   _g_.m.p.ptr().m <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
  }
  _g_.m.p <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
                <span class="hljs-comment">// 将处理器 allp[0] 绑定到当前 M</span>
  p :<span class="hljs-operator">=</span> allp[<span class="hljs-number">0</span>]
  p.m <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
  p.status <span class="hljs-operator">=</span> _Pidle  <span class="hljs-comment">// P 状态设置为 _Pidle </span>
  acquirep(p)        <span class="hljs-comment">// 将allp[0]绑定到当前的 M</span>
  <span class="hljs-keyword">if</span> trace.enabled {
   traceGoStart()
  }
 }

 
 mcache0 <span class="hljs-operator">=</span> nil

 <span class="hljs-comment">// 调用 runtime.p.destroy 释放从未使用的 P</span>
 <span class="hljs-keyword">for</span> i :<span class="hljs-operator">=</span> nprocs; i <span class="hljs-operator">&#x3C;</span> old; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span> {
  p :<span class="hljs-operator">=</span> allp[i]
  p.destroy()
  <span class="hljs-comment">// 不能释放 p 本身，因为他可能在 m 进入系统调用时被引用</span>
 }

 <span class="hljs-comment">// 裁剪 allp，保证allp长度与期望处理器数量相等</span>
 <span class="hljs-keyword">if</span> <span class="hljs-keyword">int32</span>(len(allp)) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nprocs {
  lock(<span class="hljs-operator">&#x26;</span>allpLock)
  allp <span class="hljs-operator">=</span> allp[:nprocs]
  idlepMask <span class="hljs-operator">=</span> idlepMask[:maskWords]
  timerpMask <span class="hljs-operator">=</span> timerpMask[:maskWords]
  unlock(<span class="hljs-operator">&#x26;</span>allpLock)
 }

 <span class="hljs-keyword">var</span> runnablePs <span class="hljs-operator">*</span>p
        <span class="hljs-comment">// 将除 allp[0] 之外的处理器 P 全部设置成 _Pidle 并加入到全局的空闲队列中</span>
 <span class="hljs-keyword">for</span> i :<span class="hljs-operator">=</span> nprocs <span class="hljs-operator">-</span> <span class="hljs-number">1</span>; i <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i<span class="hljs-operator">-</span><span class="hljs-operator">-</span> {
  p :<span class="hljs-operator">=</span> allp[i]
  <span class="hljs-keyword">if</span> _g_.m.p.ptr() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> p {    <span class="hljs-comment">// 跳过当前 P</span>
   <span class="hljs-keyword">continue</span>
  }
  p.status <span class="hljs-operator">=</span> _Pidle          <span class="hljs-comment">// 设置 P 的状态为空闲状态</span>
  <span class="hljs-keyword">if</span> runqempty(p) {
   pidleput(p)        <span class="hljs-comment">// 放入到全局结构体 schedt 的空闲 P 列表中</span>
  } <span class="hljs-keyword">else</span> {
   p.m.set(mget())    <span class="hljs-comment">// 如果有本地任务，则为其绑定一个 M</span>
   p.link.set(runnablePs)
   runnablePs <span class="hljs-operator">=</span> p
  }
 }
 stealOrder.reset(<span class="hljs-keyword">uint32</span>(nprocs))
 <span class="hljs-keyword">var</span> int32p <span class="hljs-operator">*</span><span class="hljs-keyword">int32</span> <span class="hljs-operator">=</span> <span class="hljs-operator">&#x26;</span>gomaxprocs 
 atomic.Store((<span class="hljs-operator">*</span><span class="hljs-keyword">uint32</span>)(unsafe.Pointer(int32p)), <span class="hljs-keyword">uint32</span>(nprocs))
 <span class="hljs-keyword">return</span> runnablePs      <span class="hljs-comment">// 返回所有包含本地任务的 P 链表</span>
}
</code></pre><p>runtime.procresize() 函数 的执行过程如下：</p><p>1）如果全局变量 allp 切片中的 P 数量少于期望数量，会对切片进行扩容；</p><p>2）使用 new 创建新的 P 结构体并调用 runtime.p.init 初始化刚刚扩容的 P；</p><p>3）通过指针将线程 m0 和处理器 allp[0] 绑定到一起；</p><p>4）调用 runtime.p.destroy 释放不再使用的 P 结构；</p><p>5）通过切片截断改变全局变量 allp 的长度，保证它与期望 P 数量相等；</p><p>6）将除 allp[0] 之外的处理器 P 全部设置成 _Pidle 并加入到全局 schedt 的空闲 P 队列中；</p><p>runtime.procresize() 函数的逻辑如图 4.3 所示：</p><p><em>图4.3 runtime.procresize() 函数逻辑</em></p><p>runtime.procresize() 函数调用 runtime.p.init 初始化新创建的 P：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
// 初始化 P
func (pp *p) init(id int32) {
 pp.id = id             // p 的 id 就是它在 allp 中的索引
 pp.status = _Pgcstop   // 新创建的 p 处于 _Pgcstop 状态
 ......
        // 为 P 分配 cache 对象，涉及对象分配
 if pp.mcache == nil {
  if id == 0 {
   if mcache0 == nil {
    throw(&quot;missing mcache?&quot;)
   }
   pp.mcache = mcache0
  } else {
   pp.mcache = allocmcache()
  }
 }
 ......
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
<span class="hljs-comment">// 初始化 P</span>
func (pp <span class="hljs-operator">*</span>p) init(id <span class="hljs-keyword">int32</span>) {
 pp.id <span class="hljs-operator">=</span> id             <span class="hljs-comment">// p 的 id 就是它在 allp 中的索引</span>
 pp.status <span class="hljs-operator">=</span> _Pgcstop   <span class="hljs-comment">// 新创建的 p 处于 _Pgcstop 状态</span>
 ......
        <span class="hljs-comment">// 为 P 分配 cache 对象，涉及对象分配</span>
 <span class="hljs-keyword">if</span> pp.mcache <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
  <span class="hljs-keyword">if</span> id <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
   <span class="hljs-keyword">if</span> mcache0 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
    <span class="hljs-keyword">throw</span>(<span class="hljs-string">"missing mcache?"</span>)
   }
   pp.mcache <span class="hljs-operator">=</span> mcache0
  } <span class="hljs-keyword">else</span> {
   pp.mcache <span class="hljs-operator">=</span> allocmcache()
  }
 }
 ......
}
</code></pre><p>需要说明的是，mcache内存结构原来是在 M 上的，自从引入了 P 之后，就将该结构体移到了P上，这样，就不用每个 M 维护自己的内存分配 mcache，由于 P 在有 M 可以执行时才会移动到其他 M 上去，空闲的 M 无须分配内存，这种灵活性使整体线程的内存分配大大减少。</p><h3 id="h-43-g" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">4.3 怎样创建 G ？</h3><p>我们再回到 4.1 节对程序启动函数 runtime·rt0_go，有个动作是通过 runtime.newproc 函数创建 G，runtime.newproc 入参是 funcval 结构体函数，代表 go 关键字后面调用的函数：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
// 创建G，并放入 P 的运行队列
func newproc(fn *funcval) {
 gp := getg()
 pc := getcallerpc()    // 获取调用方 PC 寄存器值，即调用方程序要执行的下一个指令地址
        // 用 g0 系统栈创建 Goroutine 对象
        // 传递的参数包括 fn 函数入口地址, gp（g0），调用方 pc
 systemstack(func() {
  newg := newproc1(fn, gp, pc)  // 调用 newproc1 获取 Goroutine 结构

  _p_ := getg().m.p.ptr()       // 获取当前 G 的 P 
  runqput(_p_, newg, true)      // 将新的 G 放入 P 的本地运行队列

  if mainStarted {              // M 启动时唤醒新的 P 执行 G
   wakep()
  }
 })
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
<span class="hljs-comment">// 创建G，并放入 P 的运行队列</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">newproc</span><span class="hljs-params">(fn *funcval)</span></span> {
 gp := getg()
 pc := getcallerpc()    <span class="hljs-comment">// 获取调用方 PC 寄存器值，即调用方程序要执行的下一个指令地址</span>
        <span class="hljs-comment">// 用 g0 系统栈创建 Goroutine 对象</span>
        <span class="hljs-comment">// 传递的参数包括 fn 函数入口地址, gp（g0），调用方 pc</span>
 systemstack(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
  newg := newproc1(fn, gp, pc)  <span class="hljs-comment">// 调用 newproc1 获取 Goroutine 结构</span>

  _p_ := getg().m.p.ptr()       <span class="hljs-comment">// 获取当前 G 的 P </span>
  runqput(_p_, newg, <span class="hljs-literal">true</span>)      <span class="hljs-comment">// 将新的 G 放入 P 的本地运行队列</span>

  <span class="hljs-keyword">if</span> mainStarted {              <span class="hljs-comment">// M 启动时唤醒新的 P 执行 G</span>
   wakep()
  }
 })
}
</code></pre><p>runtime.newproce函数主要是调用 runtime.newproc1 获取新的 Goroutine 结构，将新的 G 放入P的运行队列，M 启动时唤醒新的 P 执行 G。</p><p>runtime.newproce函数的逻辑如图4.4所示：</p><p><em>图4.4 runtime.newproce() 函数逻辑</em></p><p>runtime.newproce1() 函数的逻辑是：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
// 创建一个运行fn函数的goroutine
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
 _g_ := getg()       // 因为是在系统栈运行所以此时的 g 为 g0

 if fn == nil {
  _g_.m.throwing = -1 // do not dump full stacks
  throw(&quot;go of nil func value&quot;)
 }
 acquirem() // 加锁，禁止这时 G 的 M 被抢占，因为它可以在一个局部变量中保存 P

 _p_ := _g_.m.p.ptr()         // 获取 P
 newg := gfget(_p_)           // 从 P 的空闲列表获取一个空闲的 G
 if newg == nil {             // 找不到则创建
  newg = malg(_StackMin)     // 创建一个栈大小为 2K 的 G
  casgstatus(newg, _Gidle, _Gdead)     // CAS 改变 G 的状态为_Gdead
  allgadd(newg) // 将 _Gdead 状态的 g 添加到 allg，这样 GC 不会扫描未初始化的栈
 }
 ......
        // 计算运行空间大小，对齐
 totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) 
 totalSize = alignUp(totalSize, sys.StackAlign)
 sp := newg.stack.hi - totalSize       // 确定 SP 和参数入栈位置
 spArg := sp
 ......
        // 清理、创建并初始化 G 的运行现场
 memclrNoHeapPointers(unsafe.Pointer(&amp;newg.sched), unsafe.Sizeof(newg.sched))
 newg.sched.sp = sp
 newg.stktopsp = sp
// 保存goexit的地址到sched.pc
 newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
 newg.sched.g = guintptr(unsafe.Pointer(newg))
 gostartcallfn(&amp;newg.sched, fn)
        // 初始化 G 的基本状态
 newg.gopc = callerpc
 newg.ancestors = saveAncestors(callergp)
 newg.startpc = fn.fn
 ......
        // 将 G 的状态设置为_Grunnable
 casgstatus(newg, _Gdead, _Grunnable)
 ......
        // 生成唯一的goid
 newg.goid = int64(_p_.goidcache)
 _p_.goidcache++
 ......
        // 释放对 M 加的锁
 releasem(_g_.m)

 return newg
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
<span class="hljs-comment">// 创建一个运行fn函数的goroutine</span>
func newproc1(fn <span class="hljs-operator">*</span>funcval, callergp <span class="hljs-operator">*</span>g, callerpc uintptr) <span class="hljs-operator">*</span>g {
 _g_ :<span class="hljs-operator">=</span> getg()       <span class="hljs-comment">// 因为是在系统栈运行所以此时的 g 为 g0</span>

 <span class="hljs-keyword">if</span> fn <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
  _g_.m.throwing <span class="hljs-operator">=</span> <span class="hljs-number">-1</span> <span class="hljs-comment">// do not dump full stacks</span>
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"go of nil func value"</span>)
 }
 acquirem() <span class="hljs-comment">// 加锁，禁止这时 G 的 M 被抢占，因为它可以在一个局部变量中保存 P</span>

 _p_ :<span class="hljs-operator">=</span> _g_.m.p.ptr()         <span class="hljs-comment">// 获取 P</span>
 newg :<span class="hljs-operator">=</span> gfget(_p_)           <span class="hljs-comment">// 从 P 的空闲列表获取一个空闲的 G</span>
 <span class="hljs-keyword">if</span> newg <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {             <span class="hljs-comment">// 找不到则创建</span>
  newg <span class="hljs-operator">=</span> malg(_StackMin)     <span class="hljs-comment">// 创建一个栈大小为 2K 的 G</span>
  casgstatus(newg, _Gidle, _Gdead)     <span class="hljs-comment">// CAS 改变 G 的状态为_Gdead</span>
  allgadd(newg) <span class="hljs-comment">// 将 _Gdead 状态的 g 添加到 allg，这样 GC 不会扫描未初始化的栈</span>
 }
 ......
        <span class="hljs-comment">// 计算运行空间大小，对齐</span>
 totalSize :<span class="hljs-operator">=</span> uintptr(<span class="hljs-number">4</span><span class="hljs-operator">*</span>goarch.PtrSize <span class="hljs-operator">+</span> sys.MinFrameSize) 
 totalSize <span class="hljs-operator">=</span> alignUp(totalSize, sys.StackAlign)
 sp :<span class="hljs-operator">=</span> newg.stack.hi <span class="hljs-operator">-</span> totalSize       <span class="hljs-comment">// 确定 SP 和参数入栈位置</span>
 spArg :<span class="hljs-operator">=</span> sp
 ......
        <span class="hljs-comment">// 清理、创建并初始化 G 的运行现场</span>
 memclrNoHeapPointers(unsafe.Pointer(<span class="hljs-operator">&#x26;</span>newg.sched), unsafe.Sizeof(newg.sched))
 newg.sched.sp <span class="hljs-operator">=</span> sp
 newg.stktopsp <span class="hljs-operator">=</span> sp
<span class="hljs-comment">// 保存goexit的地址到sched.pc</span>
 newg.sched.pc <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.FuncPCABI0(goexit) <span class="hljs-operator">+</span> sys.PCQuantum <span class="hljs-comment">// +PCQuantum so that previous instruction is in same function</span>
 newg.sched.g <span class="hljs-operator">=</span> guintptr(unsafe.Pointer(newg))
 gostartcallfn(<span class="hljs-operator">&#x26;</span>newg.sched, fn)
        <span class="hljs-comment">// 初始化 G 的基本状态</span>
 newg.gopc <span class="hljs-operator">=</span> callerpc
 newg.ancestors <span class="hljs-operator">=</span> saveAncestors(callergp)
 newg.startpc <span class="hljs-operator">=</span> fn.fn
 ......
        <span class="hljs-comment">// 将 G 的状态设置为_Grunnable</span>
 casgstatus(newg, _Gdead, _Grunnable)
 ......
        <span class="hljs-comment">// 生成唯一的goid</span>
 newg.goid <span class="hljs-operator">=</span> <span class="hljs-keyword">int64</span>(_p_.goidcache)
 _p_.goidcache+<span class="hljs-operator">+</span>
 ......
        <span class="hljs-comment">// 释放对 M 加的锁</span>
 releasem(_g_.m)

 <span class="hljs-keyword">return</span> newg
}
</code></pre><p>runtime.newproc1() 函数主要执行三个动作： 1）获取或者创建新的 Goroutine 结构体，会先从处理器的 gFree 列表中查找空闲的 Goroutine，如果不存在空闲的 Goroutine，会通过 runtime.malg 创建一个栈大小足够的新结构体，新创建的 G 的状态为_Gdead； 2）将传入的参数 callergp，callerpc，fn更新到 G 的栈上，初始化 G 的相关参数； 3）将 G 状态设置为 _Grunnable 状态，返回；</p><p>runtime.newproc1() 函数的逻辑如图 4.5 所示：</p><p><em>图4.5 runtime.newproc1() 函数逻辑</em></p><p>runtime.newproc1() 函数主要调用 runtime.gfget() 函数 获取 G：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
func gfget(_p_ *p) *g {
retry:
        // 如果 P 的空闲列表 gFree 为空，sched 的的空闲列表 gFree 不为空
 if _p_.gFree.empty() &amp;&amp; (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {
  lock(&amp;sched.gFree.lock)
  // 从 sched 的 gFree 列表中移动 32 个 G 到 P 的 gFree 中
  for _p_.gFree.n &lt; 32 {
   gp := sched.gFree.stack.pop()
   if gp == nil {
    gp = sched.gFree.noStack.pop()
    if gp == nil {
     break
    }
   }
   sched.gFree.n--
   _p_.gFree.push(gp)
   _p_.gFree.n++
  }
  unlock(&amp;sched.gFree.lock)
  goto retry
 }
        // 如果此时 P 的空闲列表还是为空，返回nil，说明无空闲的G
 gp := _p_.gFree.pop()
 if gp == nil {
  return nil
 }
 _p_.gFree.n--
        // 设置 G 的栈空间
 if gp.stack.lo == 0 {
  systemstack(func() {
   gp.stack = stackalloc(_FixedStack)
  })
  gp.stackguard0 = gp.stack.lo + _StackGuard
 } else {
  .....
 }
 return gp   // 从 P 的空闲列表获取 G 返回
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
func gfget(_p_ <span class="hljs-operator">*</span>p) <span class="hljs-operator">*</span>g {
retry:
        <span class="hljs-comment">// 如果 P 的空闲列表 gFree 为空，sched 的的空闲列表 gFree 不为空</span>
 <span class="hljs-keyword">if</span> _p_.gFree.empty() <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> (<span class="hljs-operator">!</span>sched.gFree.stack.empty() <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-operator">!</span>sched.gFree.noStack.empty()) {
  lock(<span class="hljs-operator">&#x26;</span>sched.gFree.lock)
  <span class="hljs-comment">// 从 sched 的 gFree 列表中移动 32 个 G 到 P 的 gFree 中</span>
  <span class="hljs-keyword">for</span> _p_.gFree.n <span class="hljs-operator">&#x3C;</span> <span class="hljs-number">32</span> {
   gp :<span class="hljs-operator">=</span> sched.gFree.stack.<span class="hljs-built_in">pop</span>()
   <span class="hljs-keyword">if</span> gp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
    gp <span class="hljs-operator">=</span> sched.gFree.noStack.<span class="hljs-built_in">pop</span>()
    <span class="hljs-keyword">if</span> gp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
     <span class="hljs-keyword">break</span>
    }
   }
   sched.gFree.n-<span class="hljs-operator">-</span>
   _p_.gFree.<span class="hljs-built_in">push</span>(gp)
   _p_.gFree.n+<span class="hljs-operator">+</span>
  }
  unlock(<span class="hljs-operator">&#x26;</span>sched.gFree.lock)
  goto retry
 }
        <span class="hljs-comment">// 如果此时 P 的空闲列表还是为空，返回nil，说明无空闲的G</span>
 gp :<span class="hljs-operator">=</span> _p_.gFree.<span class="hljs-built_in">pop</span>()
 <span class="hljs-keyword">if</span> gp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
  <span class="hljs-keyword">return</span> nil
 }
 _p_.gFree.n-<span class="hljs-operator">-</span>
        <span class="hljs-comment">// 设置 G 的栈空间</span>
 <span class="hljs-keyword">if</span> gp.stack.lo <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  systemstack(func() {
   gp.stack <span class="hljs-operator">=</span> stackalloc(_FixedStack)
  })
  gp.stackguard0 <span class="hljs-operator">=</span> gp.stack.lo <span class="hljs-operator">+</span> _StackGuard
 } <span class="hljs-keyword">else</span> {
  .....
 }
 <span class="hljs-keyword">return</span> gp   <span class="hljs-comment">// 从 P 的空闲列表获取 G 返回</span>
}
</code></pre><p>runtime.gfget() 函数的主要逻辑是：当 P 的空闲列表 gFree 为空时，从 sched 持有的全局空闲列表 gFree 中移动最多 32个 G 到当前的 P 的空闲列表上；然后从 P 的 gFree 列表头返回一个 G；如果还是没有，则返回空，说明获取不到空闲的 G。</p><p>在 runtime.newproc1() 函数中，如果不存在空闲的 G，会通过 runtime.malg() 创建一个栈大小足够的新结构体：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
// 创建一个新的 g 结构体 
func malg(stacksize int32) *g {
 newg := new(g)
 if stacksize &gt;= 0 {     // 如果申请的堆栈大小大于 0，会通过 runtime.stackalloc 分配 2KB 的栈空间
  stacksize = round2(_StackSystem + stacksize)
  systemstack(func() {
   newg.stack = stackalloc(uint32(stacksize))
  })
  newg.stackguard0 = newg.stack.lo + _StackGuard
  newg.stackguard1 = ^uintptr(0)
  *(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
 }
 return newg
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
<span class="hljs-comment">// 创建一个新的 g 结构体 </span>
func malg(stacksize <span class="hljs-keyword">int32</span>) <span class="hljs-operator">*</span>g {
 newg :<span class="hljs-operator">=</span> <span class="hljs-keyword">new</span>(g)
 <span class="hljs-keyword">if</span> stacksize <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {     <span class="hljs-comment">// 如果申请的堆栈大小大于 0，会通过 runtime.stackalloc 分配 2KB 的栈空间</span>
  stacksize <span class="hljs-operator">=</span> round2(_StackSystem <span class="hljs-operator">+</span> stacksize)
  systemstack(func() {
   newg.stack <span class="hljs-operator">=</span> stackalloc(<span class="hljs-keyword">uint32</span>(stacksize))
  })
  newg.stackguard0 <span class="hljs-operator">=</span> newg.stack.lo <span class="hljs-operator">+</span> _StackGuard
  newg.stackguard1 <span class="hljs-operator">=</span> <span class="hljs-operator">^</span>uintptr(<span class="hljs-number">0</span>)
  <span class="hljs-operator">*</span>(<span class="hljs-operator">*</span>uintptr)(unsafe.Pointer(newg.stack.lo)) <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
 }
 <span class="hljs-keyword">return</span> newg
}
</code></pre><p>回到 runtime.newproce函数，在获取到 G 后，会调用 runtime.runqput() 函数将 G 放入 P 本地队列，或全局队列：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
// 将 G 放入 P 的运行队列中
func runqput(_p_ *p, gp *g, next bool) {
        // 保持一定的随机性，不将当前 G 设置为 P 的下一个执行的任务
 if randomizeScheduler &amp;&amp; next &amp;&amp; fastrandn(2) == 0 {
  next = false
 }

 if next {
 retryNext:
               // 将 G 放入到 P 的 runnext 变量中，作为下一个 P 执行的任务
  oldnext := _p_.runnext
  if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
   goto retryNext
  }
  if oldnext == 0 {
   return
  }
  // 获取原来的 runnext 存储的 G，放入 P 本地运行队列，或全局队列
  gp = oldnext.ptr()
 }

retry:
 h := atomic.LoadAcq(&amp;_p_.runqhead) // 获取 P 环形队列的头和尾部指针
 t := _p_.runqtail
        // P 本地环形队列没有满，将 G 放入本地环形队列
 if t-h &lt; uint32(len(_p_.runq)) {
  _p_.runq[t%uint32(len(_p_.runq))].set(gp)
  atomic.StoreRel(&amp;_p_.runqtail, t+1) 
  return
 }
        // P 本地环形队列已满，将 G 放入全局队列
 if runqputslow(_p_, gp, h, t) {
  return
 }
 // 本地队列和全局队列没有满，则不会走到这里，否则循环尝试放入
 goto retry
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
<span class="hljs-comment">// 将 G 放入 P 的运行队列中</span>
func runqput(_p_ <span class="hljs-operator">*</span>p, gp <span class="hljs-operator">*</span>g, next <span class="hljs-keyword">bool</span>) {
        <span class="hljs-comment">// 保持一定的随机性，不将当前 G 设置为 P 的下一个执行的任务</span>
 <span class="hljs-keyword">if</span> randomizeScheduler <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> next <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> fastrandn(<span class="hljs-number">2</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  next <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>
 }

 <span class="hljs-keyword">if</span> next {
 retryNext:
               <span class="hljs-comment">// 将 G 放入到 P 的 runnext 变量中，作为下一个 P 执行的任务</span>
  oldnext :<span class="hljs-operator">=</span> _p_.runnext
  <span class="hljs-keyword">if</span> <span class="hljs-operator">!</span>_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
   goto retryNext
  }
  <span class="hljs-keyword">if</span> oldnext <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
   <span class="hljs-keyword">return</span>
  }
  <span class="hljs-comment">// 获取原来的 runnext 存储的 G，放入 P 本地运行队列，或全局队列</span>
  gp <span class="hljs-operator">=</span> oldnext.ptr()
 }

retry:
 h :<span class="hljs-operator">=</span> atomic.LoadAcq(<span class="hljs-operator">&#x26;</span>_p_.runqhead) <span class="hljs-comment">// 获取 P 环形队列的头和尾部指针</span>
 t :<span class="hljs-operator">=</span> _p_.runqtail
        <span class="hljs-comment">// P 本地环形队列没有满，将 G 放入本地环形队列</span>
 <span class="hljs-keyword">if</span> t<span class="hljs-operator">-</span>h <span class="hljs-operator">&#x3C;</span> <span class="hljs-keyword">uint32</span>(len(_p_.runq)) {
  _p_.runq[t<span class="hljs-operator">%</span><span class="hljs-keyword">uint32</span>(len(_p_.runq))].set(gp)
  atomic.StoreRel(<span class="hljs-operator">&#x26;</span>_p_.runqtail, t<span class="hljs-operator">+</span><span class="hljs-number">1</span>) 
  <span class="hljs-keyword">return</span>
 }
        <span class="hljs-comment">// P 本地环形队列已满，将 G 放入全局队列</span>
 <span class="hljs-keyword">if</span> runqputslow(_p_, gp, h, t) {
  <span class="hljs-keyword">return</span>
 }
 <span class="hljs-comment">// 本地队列和全局队列没有满，则不会走到这里，否则循环尝试放入</span>
 goto retry
}
</code></pre><p>runtime.runqput() 函数的主要处理逻辑是：</p><p>1）保留一定的随机性，设置 next 为 false，即不将当前 G 设置为 P 的下一个执行的 G；</p><p>2）当 next 为 true 时，将 G 设置到 P 的 runnext 作为 P 下一个执行的任务；</p><p>3）当 next 为 false 并且本地运行队列还有剩余空间时，将 Goroutine 加入处理器持有的本地运行队列；</p><p>4）当处理器的本地运行队列已经没有剩余空间时，就会把本地队列中的一部分 G 和待加入的 G 通过 runtime.runqputslow 添加到调度器持有的全局运行队列上；</p><p>runtime.runqput() 函数的逻辑如图 4.6 所示：</p><p><em>图4.6 runtime.runqput() 函数的逻辑</em></p><p>runtime.runqputslow() 函数的逻辑如下：</p><pre data-type="codeBlock" text="// 将 G 和 P 本地队列的一部分放入全局队列
func runqputslow(_p_ *p, gp *g, h, t uint32) bool {
 var batch [len(_p_.runq)/2 + 1]*g   // 初始化一个本地队列长度一半 + 1 的 G 列表 batch

 // 首先，从 P 本地队列中获取一部分 G 放入初始化的列表 batch
 n := t - h
 n = n / 2
 if n != uint32(len(_p_.runq)/2) {
  throw(&quot;runqputslow: queue is not full&quot;)
 }
 for i := uint32(0); i &lt; n; i++ { // 将 P 本地环形队列的前一半 G 放入batch
  batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr()
 }
 if !atomic.CasRel(&amp;_p_.runqhead, h, h+n) { // cas-release, commits consume
  return false
 }
 batch[n] = gp    // 将传入的 G 放入列表 batch 的尾部

 if randomizeScheduler {     // 打乱 batch 列表中G的顺序
  for i := uint32(1); i &lt;= n; i++ {
   j := fastrandn(i + 1)
   batch[i], batch[j] = batch[j], batch[i]
  }
 }

 // 将 batch列表的 G 串成一个链表.
 for i := uint32(0); i &lt; n; i++ {
  batch[i].schedlink.set(batch[i+1])
 }
 var q gQueue     // 将 batch 列表设置成 gQueue 队列
 q.head.set(batch[0])
 q.tail.set(batch[n])

 // 现在把 gQueue 队列放入全局队列
 lock(&amp;sched.lock)
 globrunqputbatch(&amp;q, int32(n+1))
 unlock(&amp;sched.lock)
 return true
}
"><code><span class="hljs-comment">// 将 G 和 P 本地队列的一部分放入全局队列</span>
func runqputslow(_p_ <span class="hljs-operator">*</span>p, gp <span class="hljs-operator">*</span>g, h, t <span class="hljs-keyword">uint32</span>) <span class="hljs-keyword">bool</span> {
 <span class="hljs-keyword">var</span> batch [len(_p_.runq)<span class="hljs-operator">/</span><span class="hljs-number">2</span> <span class="hljs-operator">+</span> <span class="hljs-number">1</span>]<span class="hljs-operator">*</span>g   <span class="hljs-comment">// 初始化一个本地队列长度一半 + 1 的 G 列表 batch</span>

 <span class="hljs-comment">// 首先，从 P 本地队列中获取一部分 G 放入初始化的列表 batch</span>
 n :<span class="hljs-operator">=</span> t <span class="hljs-operator">-</span> h
 n <span class="hljs-operator">=</span> n <span class="hljs-operator">/</span> <span class="hljs-number">2</span>
 <span class="hljs-keyword">if</span> n <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(len(_p_.runq)<span class="hljs-operator">/</span><span class="hljs-number">2</span>) {
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"runqputslow: queue is not full"</span>)
 }
 <span class="hljs-keyword">for</span> i :<span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(<span class="hljs-number">0</span>); i <span class="hljs-operator">&#x3C;</span> n; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span> { <span class="hljs-comment">// 将 P 本地环形队列的前一半 G 放入batch</span>
  batch[i] <span class="hljs-operator">=</span> _p_.runq[(h<span class="hljs-operator">+</span>i)<span class="hljs-operator">%</span><span class="hljs-keyword">uint32</span>(len(_p_.runq))].ptr()
 }
 <span class="hljs-keyword">if</span> <span class="hljs-operator">!</span>atomic.CasRel(<span class="hljs-operator">&#x26;</span>_p_.runqhead, h, h<span class="hljs-operator">+</span>n) { <span class="hljs-comment">// cas-release, commits consume</span>
  <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
 }
 batch[n] <span class="hljs-operator">=</span> gp    <span class="hljs-comment">// 将传入的 G 放入列表 batch 的尾部</span>

 <span class="hljs-keyword">if</span> randomizeScheduler {     <span class="hljs-comment">// 打乱 batch 列表中G的顺序</span>
  <span class="hljs-keyword">for</span> i :<span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(<span class="hljs-number">1</span>); i <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> n; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span> {
   j :<span class="hljs-operator">=</span> fastrandn(i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>)
   batch[i], batch[j] <span class="hljs-operator">=</span> batch[j], batch[i]
  }
 }

 <span class="hljs-comment">// 将 batch列表的 G 串成一个链表.</span>
 <span class="hljs-keyword">for</span> i :<span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(<span class="hljs-number">0</span>); i <span class="hljs-operator">&#x3C;</span> n; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span> {
  batch[i].schedlink.set(batch[i<span class="hljs-operator">+</span><span class="hljs-number">1</span>])
 }
 <span class="hljs-keyword">var</span> q gQueue     <span class="hljs-comment">// 将 batch 列表设置成 gQueue 队列</span>
 q.head.set(batch[<span class="hljs-number">0</span>])
 q.tail.set(batch[n])

 <span class="hljs-comment">// 现在把 gQueue 队列放入全局队列</span>
 lock(<span class="hljs-operator">&#x26;</span>sched.lock)
 globrunqputbatch(<span class="hljs-operator">&#x26;</span>q, <span class="hljs-keyword">int32</span>(n<span class="hljs-operator">+</span><span class="hljs-number">1</span>))
 unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
 <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
}
</code></pre><p>runtime.runqputslow() 函数会把 P 本地环形队列的前一半 G 获取出来，跟传入的 G 组成一个列表，打乱顺序，再放入全局队列。</p><p>综上所属，用下图表示调度器启动流程：</p><p><em>图4.7 调度器启动流程</em></p><h3 id="h-5" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5. 调度循环</h3><p>我们再回到5.1节的程序启动流程，runtime·rt0_go 函数在调用 runtime.schedinit() 初始化好了调度器、调用 runtime.newproc()创建了main函数的 G 后，会调用runtime.mstart() 函数启动 M 去执行G。</p><pre data-type="codeBlock" text="TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0
 CALL runtime·mstart0(SB)
 RET // not reached
"><code>TEXT runtime·<span class="hljs-built_in">mstart</span>(SB),NOSPLIT|TOPFRAME,$<span class="hljs-number">0</span>
 CALL runtime·<span class="hljs-built_in">mstart0</span>(SB)
 RET <span class="hljs-comment">// not reached</span>
</code></pre><p>runtime.mstart() 是用汇编写的，会直接调用 runtime.mstart0() 函数：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
func mstart0() {
 _g_ := getg()
 ......
        // 初始化 g0 的参数
 _g_.stackguard0 = _g_.stack.lo + _StackGuard
 _g_.stackguard1 = _g_.stackguard0
 mstart1()

 ......
 mexit(osStack)
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
func mstart0() {
 _g_ :<span class="hljs-operator">=</span> getg()
 ......
        <span class="hljs-comment">// 初始化 g0 的参数</span>
 _g_.stackguard0 <span class="hljs-operator">=</span> _g_.stack.lo <span class="hljs-operator">+</span> _StackGuard
 _g_.stackguard1 <span class="hljs-operator">=</span> _g_.stackguard0
 mstart1()

 ......
 mexit(osStack)
}
</code></pre><p>runtime.mstart0() 函数主要调用 runtime.mstart1()：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
func mstart1() {
 _g_ := getg()

 if _g_ != _g_.m.g0 {
  throw(&quot;bad runtime·mstart&quot;)
 }

 // 记录当前栈帧，便于其他调用复用，当进入 schedule 之后，再也不会回到 mstart1
 _g_.sched.g = guintptr(unsafe.Pointer(_g_))
 _g_.sched.pc = getcallerpc()
 _g_.sched.sp = getcallersp()

 asminit()
 minit()

 // 设置信号 handler；在 minit 之后，因为 minit 可以准备处理信号的的线程
 if _g_.m == &amp;m0 {
  mstartm0()
 }
        // 执行启动函数
 if fn := _g_.m.mstartfn; fn != nil {
  fn()
 }
        // 如果当前 m 并非 m0，则要求绑定 p
 if _g_.m != &amp;m0 {
  acquirep(_g_.m.nextp.ptr())
  _g_.m.nextp = 0
 }
        // 准备好后，开始调度循环，永不返回
 schedule()
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
func mstart1() {
 _g_ :<span class="hljs-operator">=</span> getg()

 <span class="hljs-keyword">if</span> _g_ <span class="hljs-operator">!</span><span class="hljs-operator">=</span> _g_.m.g0 {
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"bad runtime·mstart"</span>)
 }

 <span class="hljs-comment">// 记录当前栈帧，便于其他调用复用，当进入 schedule 之后，再也不会回到 mstart1</span>
 _g_.sched.g <span class="hljs-operator">=</span> guintptr(unsafe.Pointer(_g_))
 _g_.sched.pc <span class="hljs-operator">=</span> getcallerpc()
 _g_.sched.sp <span class="hljs-operator">=</span> getcallersp()

 asminit()
 minit()

 <span class="hljs-comment">// 设置信号 handler；在 minit 之后，因为 minit 可以准备处理信号的的线程</span>
 <span class="hljs-keyword">if</span> _g_.m <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-operator">&#x26;</span>m0 {
  mstartm0()
 }
        <span class="hljs-comment">// 执行启动函数</span>
 <span class="hljs-keyword">if</span> fn :<span class="hljs-operator">=</span> _g_.m.mstartfn; fn <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
  fn()
 }
        <span class="hljs-comment">// 如果当前 m 并非 m0，则要求绑定 p</span>
 <span class="hljs-keyword">if</span> _g_.m <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-operator">&#x26;</span>m0 {
  acquirep(_g_.m.nextp.ptr())
  _g_.m.nextp <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
 }
        <span class="hljs-comment">// 准备好后，开始调度循环，永不返回</span>
 schedule()
}
</code></pre><p>runtime.mstart1() 保存调度信息后，会调用 runtime.schedule() 进入调度循环，寻找一个可执行的 G 并执行。</p><p>循环调度主逻辑如图5.1所示：</p><p><em>图5.1 循环调度主逻辑</em></p><p>runtime.schedule() 函数的逻辑是：</p><pre data-type="codeBlock" text="// src/runtime/proc.go
func schedule() {
 _g_ := getg()

 if _g_.m.locks != 0 {
  throw(&quot;schedule: holding locks&quot;)
 }
 ......
top:
 pp := _g_.m.p.ptr()
 pp.preempt = false

 if sched.gcwaiting != 0 {    // 如果需要 GC，不再进行调度
  gcstopm()
  goto top
 }
 if pp.runSafePointFn != 0 {  // 不等于0，说明在安全点
  runSafePointFn()
 }

 // 如果 G 所在的 M 在自旋，说明其P运行队列为空，如果不为空，则应该甩出错误
 if _g_.m.spinning &amp;&amp; (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
  throw(&quot;schedule: spinning with local work&quot;)
 }
        // 运行 P 上准备就绪的 Timer
 checkTimers(pp, 0)

 var gp *g
 var inheritTime bool
        ......
 if gp == nil {    // 说明不在 GC
  // 每调度 61 次，就检查一次全局队列，保证公平性；否则两个 Goroutine 可以通过互换，一直占领本地的 runqueue
  if _g_.m.p.ptr().schedtick%61 == 0 &amp;&amp; sched.runqsize &gt; 0 {
   lock(&amp;sched.lock)
   gp = globrunqget(_g_.m.p.ptr(), 1)     // 从全局队列中偷 g
   unlock(&amp;sched.lock)
  }
 }
 if gp == nil {
                // 从 P 的本地队列获取 G
  gp, inheritTime = runqget(_g_.m.p.ptr())
 }
 if gp == nil {
  gp, inheritTime = findrunnable() // 阻塞式查找可用 G
 }

 // M 这时候一定是获取到了G
 // 如果 M 是自旋状态，重置其状态到非自旋
 if _g_.m.spinning {
  resetspinning()
 }
        .......
 // 执行 G
 execute(gp, inheritTime)
}
"><code><span class="hljs-comment">// src/runtime/proc.go</span>
func schedule() {
 _g_ :<span class="hljs-operator">=</span> getg()

 <span class="hljs-keyword">if</span> _g_.m.locks <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"schedule: holding locks"</span>)
 }
 ......
top:
 pp :<span class="hljs-operator">=</span> _g_.m.p.ptr()
 pp.preempt <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>

 <span class="hljs-keyword">if</span> sched.gcwaiting <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {    <span class="hljs-comment">// 如果需要 GC，不再进行调度</span>
  gcstopm()
  goto top
 }
 <span class="hljs-keyword">if</span> pp.runSafePointFn <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {  <span class="hljs-comment">// 不等于0，说明在安全点</span>
  runSafePointFn()
 }

 <span class="hljs-comment">// 如果 G 所在的 M 在自旋，说明其P运行队列为空，如果不为空，则应该甩出错误</span>
 <span class="hljs-keyword">if</span> _g_.m.spinning <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> (pp.runnext <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> pp.runqhead <span class="hljs-operator">!</span><span class="hljs-operator">=</span> pp.runqtail) {
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"schedule: spinning with local work"</span>)
 }
        <span class="hljs-comment">// 运行 P 上准备就绪的 Timer</span>
 checkTimers(pp, <span class="hljs-number">0</span>)

 <span class="hljs-keyword">var</span> gp <span class="hljs-operator">*</span>g
 <span class="hljs-keyword">var</span> inheritTime <span class="hljs-keyword">bool</span>
        ......
 <span class="hljs-keyword">if</span> gp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {    <span class="hljs-comment">// 说明不在 GC</span>
  <span class="hljs-comment">// 每调度 61 次，就检查一次全局队列，保证公平性；否则两个 Goroutine 可以通过互换，一直占领本地的 runqueue</span>
  <span class="hljs-keyword">if</span> _g_.m.p.ptr().schedtick%<span class="hljs-number">61</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> sched.runqsize <span class="hljs-operator">></span> <span class="hljs-number">0</span> {
   lock(<span class="hljs-operator">&#x26;</span>sched.lock)
   gp <span class="hljs-operator">=</span> globrunqget(_g_.m.p.ptr(), <span class="hljs-number">1</span>)     <span class="hljs-comment">// 从全局队列中偷 g</span>
   unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
  }
 }
 <span class="hljs-keyword">if</span> gp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
                <span class="hljs-comment">// 从 P 的本地队列获取 G</span>
  gp, inheritTime <span class="hljs-operator">=</span> runqget(_g_.m.p.ptr())
 }
 <span class="hljs-keyword">if</span> gp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
  gp, inheritTime <span class="hljs-operator">=</span> findrunnable() <span class="hljs-comment">// 阻塞式查找可用 G</span>
 }

 <span class="hljs-comment">// M 这时候一定是获取到了G</span>
 <span class="hljs-comment">// 如果 M 是自旋状态，重置其状态到非自旋</span>
 <span class="hljs-keyword">if</span> _g_.m.spinning {
  resetspinning()
 }
        .......
 <span class="hljs-comment">// 执行 G</span>
 execute(gp, inheritTime)
}
</code></pre><p>runtime.schedule() 函数会从下面几个地方查找待执行的 Goroutine：</p><p>1）为了保证公平，当全局运行队列中有待执行的 G 时，通过 schedtick 对 61 取模，表示每 61 次会有一次从全局的运行队列中查找对应的 G，这样可以避免两个 G 在 P 本地队列互换一直占有本地队列； 2）调用 runtime.runqget() 函数从 P 本地的运行队列中获取待执行的 G； 3）如果前两种方法都没有找到 G，会通过 runtime.findrunnable() 进行阻塞地查找 G；</p><p>runtime.schedule 函数从全局队列获取 G 的函数是 runtime.globrunqget() 函数：</p><pre data-type="codeBlock" text="// 从全局队列获取 G
func globrunqget(_p_ *p, max int32) *g {
 assertLockHeld(&amp;sched.lock)
        // 如果全局队列没有 G，则直接返回
 if sched.runqsize == 0 {
  return nil
 }
        // 计算n，表示从全局队列放入本地队列的 G 的个数
 n := sched.runqsize/gomaxprocs + 1
 if n &gt; sched.runqsize {
  n = sched.runqsize
 }
        // n 不能超过取的要获取的max个数 
 if max &gt; 0 &amp;&amp; n &gt; max {
  n = max
 }
        // 计算能不能用本地队列的一般放下 n 个 G，如果放不下，则 n 设为本地队列的一半
 if n &gt; int32(len(_p_.runq))/2 {
  n = int32(len(_p_.runq)) / 2
 }

 sched.runqsize -= n
        // 拿到全局队列的队头作为返回的 G
 gp := sched.runq.pop()   
 n--   // n计数减 1
        // 继续取剩下的 n-1个全局队列 G 放入本地队列
 for ; n &gt; 0; n-- {
  gp1 := sched.runq.pop()
  runqput(_p_, gp1, false)
 }
 return gp
}
"><code><span class="hljs-comment">// 从全局队列获取 G</span>
func globrunqget(_p_ <span class="hljs-operator">*</span>p, max <span class="hljs-keyword">int32</span>) <span class="hljs-operator">*</span>g {
 assertLockHeld(<span class="hljs-operator">&#x26;</span>sched.lock)
        <span class="hljs-comment">// 如果全局队列没有 G，则直接返回</span>
 <span class="hljs-keyword">if</span> sched.runqsize <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  <span class="hljs-keyword">return</span> nil
 }
        <span class="hljs-comment">// 计算n，表示从全局队列放入本地队列的 G 的个数</span>
 n :<span class="hljs-operator">=</span> sched.runqsize/gomaxprocs <span class="hljs-operator">+</span> <span class="hljs-number">1</span>
 <span class="hljs-keyword">if</span> n <span class="hljs-operator">></span> sched.runqsize {
  n <span class="hljs-operator">=</span> sched.runqsize
 }
        <span class="hljs-comment">// n 不能超过取的要获取的max个数 </span>
 <span class="hljs-keyword">if</span> max <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> n <span class="hljs-operator">></span> max {
  n <span class="hljs-operator">=</span> max
 }
        <span class="hljs-comment">// 计算能不能用本地队列的一般放下 n 个 G，如果放不下，则 n 设为本地队列的一半</span>
 <span class="hljs-keyword">if</span> n <span class="hljs-operator">></span> <span class="hljs-keyword">int32</span>(len(_p_.runq))<span class="hljs-operator">/</span><span class="hljs-number">2</span> {
  n <span class="hljs-operator">=</span> <span class="hljs-keyword">int32</span>(len(_p_.runq)) <span class="hljs-operator">/</span> <span class="hljs-number">2</span>
 }

 sched.runqsize <span class="hljs-operator">-</span><span class="hljs-operator">=</span> n
        <span class="hljs-comment">// 拿到全局队列的队头作为返回的 G</span>
 gp :<span class="hljs-operator">=</span> sched.runq.<span class="hljs-built_in">pop</span>()   
 n<span class="hljs-operator">-</span><span class="hljs-operator">-</span>   <span class="hljs-comment">// n计数减 1</span>
        <span class="hljs-comment">// 继续取剩下的 n-1个全局队列 G 放入本地队列</span>
 <span class="hljs-keyword">for</span> ; n <span class="hljs-operator">></span> <span class="hljs-number">0</span>; n<span class="hljs-operator">-</span><span class="hljs-operator">-</span> {
  gp1 :<span class="hljs-operator">=</span> sched.runq.<span class="hljs-built_in">pop</span>()
  runqput(_p_, gp1, <span class="hljs-literal">false</span>)
 }
 <span class="hljs-keyword">return</span> gp
}
</code></pre><p>runtime.globrunqget() 函数会从全局队列获取 n 个 G，第一个 G 返回给调度器去执行，剩下的 n-1 个 G 放入本地队列，其中，n一般为全局队列长度 / P处理器个数 + 1，含义是平均每个 P 应该从全局队列中承担的 G 数量，且不能超过 P 本地长度的一半。</p><p>runtime.schedule() 函数调用 runtime.runqget() 函数从 P 本地的运行队列中获取待执行的 G：</p><pre data-type="codeBlock" text="// 从 P 本地队列中获取 G
func runqget(_p_ *p) (gp *g, inheritTime bool) {
 // 如果 P 有一个 runnext，则它就是下一个要执行的 G.
 next := _p_.runnext
 // 如果 runnext 不为空，而 CAS 失败, 则它又可能被其他 P 偷取了,
 // 因为其他 P 可以竞争机会到设置 runnext 为 0, 当前 P 只能设置该字段为非0
 if next != 0 &amp;&amp; _p_.runnext.cas(next, 0) {
  return next.ptr(), true
 }

 for {
  h := atomic.LoadAcq(&amp;_p_.runqhead) //从本地环形队列头遍历
  t := _p_.runqtail
  if t == h {    // 头尾指针相等，表示本地队列为空
   return nil, false
  }
                // 获取头部指针指向的 G
  gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()
  if atomic.CasRel(&amp;_p_.runqhead, h, h+1) { 
   return gp, false
  }
 }
}
"><code><span class="hljs-comment">// 从 P 本地队列中获取 G</span>
func runqget(_p_ <span class="hljs-operator">*</span>p) (gp <span class="hljs-operator">*</span>g, inheritTime <span class="hljs-keyword">bool</span>) {
 <span class="hljs-comment">// 如果 P 有一个 runnext，则它就是下一个要执行的 G.</span>
 next :<span class="hljs-operator">=</span> _p_.runnext
 <span class="hljs-comment">// 如果 runnext 不为空，而 CAS 失败, 则它又可能被其他 P 偷取了,</span>
 <span class="hljs-comment">// 因为其他 P 可以竞争机会到设置 runnext 为 0, 当前 P 只能设置该字段为非0</span>
 <span class="hljs-keyword">if</span> next <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> _p_.runnext.cas(next, <span class="hljs-number">0</span>) {
  <span class="hljs-keyword">return</span> next.ptr(), <span class="hljs-literal">true</span>
 }

 <span class="hljs-keyword">for</span> {
  h :<span class="hljs-operator">=</span> atomic.LoadAcq(<span class="hljs-operator">&#x26;</span>_p_.runqhead) <span class="hljs-comment">//从本地环形队列头遍历</span>
  t :<span class="hljs-operator">=</span> _p_.runqtail
  <span class="hljs-keyword">if</span> t <span class="hljs-operator">=</span><span class="hljs-operator">=</span> h {    <span class="hljs-comment">// 头尾指针相等，表示本地队列为空</span>
   <span class="hljs-keyword">return</span> nil, <span class="hljs-literal">false</span>
  }
                <span class="hljs-comment">// 获取头部指针指向的 G</span>
  gp :<span class="hljs-operator">=</span> _p_.runq[h<span class="hljs-operator">%</span><span class="hljs-keyword">uint32</span>(len(_p_.runq))].ptr()
  <span class="hljs-keyword">if</span> atomic.CasRel(<span class="hljs-operator">&#x26;</span>_p_.runqhead, h, h<span class="hljs-operator">+</span><span class="hljs-number">1</span>) { 
   <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
  }
 }
}
</code></pre><p>本地队列的获取会先从 P 的 runnext 字段中获取，如果不为空则直接返回。如果 runnext 为空，那么从本地环形队列头指针遍历本地队列，取到了则返回。</p><p>阻塞式获取 G 的 runtime.findrunnable() 函数的整个逻辑看起来比较繁琐，其实无非是按这个顺序获取 G: local -&gt; global -&gt; netpoll -&gt; steal -&gt; local -&gt; global -&gt; netpoll：</p><pre data-type="codeBlock" text="// 找到一个可运行的 G 去执行
// 会从其他 P 的运行队列偷取，从本地会全局队列获取，或从网络轮询器获取
func findrunnable() (gp *g, inheritTime bool) {
 _g_ := getg()

top:
 _p_ := _g_.m.p.ptr()
 if sched.gcwaiting != 0 {     // 如果在 gc，则休眠当前 m，直到复始后回到 top
  gcstopm()
  goto top
 }
 if _p_.runSafePointFn != 0 {  // 不等于0，说明在安全点
  runSafePointFn()
 }

 now, pollUntil, _ := checkTimers(_p_, 0)
 

 // 取本地队列 local runq，如果已经拿到，立刻返回
 if gp, inheritTime := runqget(_p_); gp != nil {
  return gp, inheritTime
 }

 // 全局队列 global runq，如果已经拿到，立刻返回
 if sched.runqsize != 0 {
  lock(&amp;sched.lock)
  gp := globrunqget(_p_, 0)
  unlock(&amp;sched.lock)
  if gp != nil {
   return gp, false
  }
 }

 // 从 netpoll 网络轮询器中尝试获取 G，优先级比从其他 P 偷取 G 要高
 if netpollinited() &amp;&amp; atomic.Load(&amp;netpollWaiters) &gt; 0 &amp;&amp; atomic.Load64(&amp;sched.lastpoll) != 0 {
  if list := netpoll(0); !list.empty() { // non-blocking
   gp := list.pop()
   injectglist(&amp;list)
   casgstatus(gp, _Gwaiting, _Grunnable)
   if trace.enabled {
    traceGoUnpark(gp, 0)
   }
   return gp, false
  }
 }

 // 自旋 M: 从其他 P 中窃取任务 G
 
 // 限制自旋 M 数量到忙碌P数量的一半. 避免一半P数量、并行机制很慢时的CPU消耗
 procs := uint32(gomaxprocs)
 if _g_.m.spinning || 2*atomic.Load(&amp;sched.nmspinning) &lt; procs-atomic.Load(&amp;sched.npidle) {
  if !_g_.m.spinning {
   _g_.m.spinning = true
   atomic.Xadd(&amp;sched.nmspinning, 1)
  }
                // 从其他 P 或 timer 中偷取G
  gp, inheritTime, tnow, w, newWork := stealWork(now)
  now = tnow
  if gp != nil {
   // Successfully stole.
   return gp, inheritTime
  }
  if newWork {
   // 可能有新的 timer 或 GC，重新开始
   goto top
  }
  if w != 0 &amp;&amp; (pollUntil == 0 || w &lt; pollUntil) {
   // Earlier timer to wait for.
   pollUntil = w
  }
 }

 // 没有任何 work 可做。
        // 如果我们在 GC mark 阶段，则可以安全的扫描并标记对象为黑色
        // 然后便有 work 可做，运行 idle-time 标记而非直接放弃当前的 P。
 if gcBlackenEnabled != 0 &amp;&amp; gcMarkWorkAvailable(_p_) {
  node := (*gcBgMarkWorkerNode)(gcBgMarkWorkerPool.pop())
  if node != nil {
   _p_.gcMarkWorkerMode = gcMarkWorkerIdleMode
   gp := node.gp.ptr()
   casgstatus(gp, _Gwaiting, _Grunnable)
   if trace.enabled {
    traceGoUnpark(gp, 0)
   }
   return gp, false
  }
 }
        .....
 // 放弃当前的 P 之前，对 allp 做一个快照
 allpSnapshot := allp
 idlepMaskSnapshot := idlepMask
 timerpMaskSnapshot := timerpMask

 // 准备归还 p，对调度器加锁
 lock(&amp;sched.lock)
        // 进入了 gc，回到顶部并停止 m
 if sched.gcwaiting != 0 || _p_.runSafePointFn != 0 {
  unlock(&amp;sched.lock)
  goto top
 }
        // 全局队列中又发现了任务
 if sched.runqsize != 0 {
  gp := globrunqget(_p_, 0)     // 赶紧偷掉返回
  unlock(&amp;sched.lock)
  return gp, false
 }
 if releasep() != _p_ {         // 归还当前的 p
  throw(&quot;findrunnable: wrong p&quot;)
 }
 pidleput(_p_)       // 将 p 放入 idle 链表
 unlock(&amp;sched.lock)      // 完成归还，解锁

 // 这里要非常小心: 线程从自旋到非自旋状态的转换，可能与新 Goroutine 的提交同时发生
 wasSpinning := _g_.m.spinning
 if _g_.m.spinning {
  _g_.m.spinning = false    // M 即将睡眠，状态不再是 spinning
  if int32(atomic.Xadd(&amp;sched.nmspinning, -1)) &lt; 0 {
   throw(&quot;findrunnable: negative nmspinning&quot;)
  }

  // 再次检查所有的 runqueue
  _p_ = checkRunqsNoP(allpSnapshot, idlepMaskSnapshot)
  if _p_ != nil {
   acquirep(_p_)
   _g_.m.spinning = true
   atomic.Xadd(&amp;sched.nmspinning, 1)
   goto top
  }

  // 再次检查 idle-priority GC work，和上面重新找 runqueue 的逻辑类似
  _p_, gp = checkIdleGCNoP()
  if _p_ != nil {
   acquirep(_p_)
   _g_.m.spinning = true
   atomic.Xadd(&amp;sched.nmspinning, 1)

   // Run the idle worker.
   _p_.gcMarkWorkerMode = gcMarkWorkerIdleMode
   casgstatus(gp, _Gwaiting, _Grunnable)
   if trace.enabled {
    traceGoUnpark(gp, 0)
   }
   return gp, false
  }

  // 最后, 检查 timer creation
  pollUntil = checkTimersNoP(allpSnapshot, timerpMaskSnapshot, pollUntil)
 }

 // 再次检查 netpoll 网络轮询器，和上面重新找 runqueue 的逻辑类似
 if netpollinited() &amp;&amp; (atomic.Load(&amp;netpollWaiters) &gt; 0 || pollUntil != 0) &amp;&amp; atomic.Xchg64(&amp;sched.lastpoll, 0) != 0 {
  ......
  lock(&amp;sched.lock)
  _p_ = pidleget()
  unlock(&amp;sched.lock)
  if _p_ == nil {
   injectglist(&amp;list)
  } else {
   acquirep(_p_)
   if !list.empty() {
    gp := list.pop()
    injectglist(&amp;list)
    casgstatus(gp, _Gwaiting, _Grunnable)
    if trace.enabled {
     traceGoUnpark(gp, 0)
    }
    return gp, false
   }
   if wasSpinning {
    _g_.m.spinning = true
    atomic.Xadd(&amp;sched.nmspinning, 1)
   }
   goto top
  }
 } else if pollUntil != 0 &amp;&amp; netpollinited() {
  pollerPollUntil := int64(atomic.Load64(&amp;sched.pollUntil))
  if pollerPollUntil == 0 || pollerPollUntil &gt; pollUntil {
   netpollBreak()
  }
 }
 stopm()     // 真的什么都没找到，暂止当前的 m
 goto top
}
"><code><span class="hljs-comment">// 找到一个可运行的 G 去执行</span>
<span class="hljs-comment">// 会从其他 P 的运行队列偷取，从本地会全局队列获取，或从网络轮询器获取</span>
func findrunnable() (gp <span class="hljs-operator">*</span>g, inheritTime <span class="hljs-keyword">bool</span>) {
 _g_ :<span class="hljs-operator">=</span> getg()

top:
 _p_ :<span class="hljs-operator">=</span> _g_.m.p.ptr()
 <span class="hljs-keyword">if</span> sched.gcwaiting <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {     <span class="hljs-comment">// 如果在 gc，则休眠当前 m，直到复始后回到 top</span>
  gcstopm()
  goto top
 }
 <span class="hljs-keyword">if</span> _p_.runSafePointFn <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {  <span class="hljs-comment">// 不等于0，说明在安全点</span>
  runSafePointFn()
 }

 <span class="hljs-built_in">now</span>, pollUntil, <span class="hljs-keyword">_</span> :<span class="hljs-operator">=</span> checkTimers(_p_, <span class="hljs-number">0</span>)
 

 <span class="hljs-comment">// 取本地队列 local runq，如果已经拿到，立刻返回</span>
 <span class="hljs-keyword">if</span> gp, inheritTime :<span class="hljs-operator">=</span> runqget(_p_); gp <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
  <span class="hljs-keyword">return</span> gp, inheritTime
 }

 <span class="hljs-comment">// 全局队列 global runq，如果已经拿到，立刻返回</span>
 <span class="hljs-keyword">if</span> sched.runqsize <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  lock(<span class="hljs-operator">&#x26;</span>sched.lock)
  gp :<span class="hljs-operator">=</span> globrunqget(_p_, <span class="hljs-number">0</span>)
  unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
  <span class="hljs-keyword">if</span> gp <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
   <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
  }
 }

 <span class="hljs-comment">// 从 netpoll 网络轮询器中尝试获取 G，优先级比从其他 P 偷取 G 要高</span>
 <span class="hljs-keyword">if</span> netpollinited() <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> atomic.Load(<span class="hljs-operator">&#x26;</span>netpollWaiters) <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> atomic.Load64(<span class="hljs-operator">&#x26;</span>sched.lastpoll) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  <span class="hljs-keyword">if</span> list :<span class="hljs-operator">=</span> netpoll(<span class="hljs-number">0</span>); <span class="hljs-operator">!</span>list.empty() { <span class="hljs-comment">// non-blocking</span>
   gp :<span class="hljs-operator">=</span> list.<span class="hljs-built_in">pop</span>()
   injectglist(<span class="hljs-operator">&#x26;</span>list)
   casgstatus(gp, _Gwaiting, _Grunnable)
   <span class="hljs-keyword">if</span> trace.enabled {
    traceGoUnpark(gp, <span class="hljs-number">0</span>)
   }
   <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
  }
 }

 <span class="hljs-comment">// 自旋 M: 从其他 P 中窃取任务 G</span>
 
 <span class="hljs-comment">// 限制自旋 M 数量到忙碌P数量的一半. 避免一半P数量、并行机制很慢时的CPU消耗</span>
 procs :<span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(gomaxprocs)
 <span class="hljs-keyword">if</span> _g_.m.spinning <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-number">2</span><span class="hljs-operator">*</span>atomic.Load(<span class="hljs-operator">&#x26;</span>sched.nmspinning) <span class="hljs-operator">&#x3C;</span> procs<span class="hljs-operator">-</span>atomic.Load(<span class="hljs-operator">&#x26;</span>sched.npidle) {
  <span class="hljs-keyword">if</span> <span class="hljs-operator">!</span>_g_.m.spinning {
   _g_.m.spinning <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
   atomic.Xadd(<span class="hljs-operator">&#x26;</span>sched.nmspinning, <span class="hljs-number">1</span>)
  }
                <span class="hljs-comment">// 从其他 P 或 timer 中偷取G</span>
  gp, inheritTime, tnow, w, newWork :<span class="hljs-operator">=</span> stealWork(<span class="hljs-built_in">now</span>)
  <span class="hljs-built_in">now</span> <span class="hljs-operator">=</span> tnow
  <span class="hljs-keyword">if</span> gp <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
   <span class="hljs-comment">// Successfully stole.</span>
   <span class="hljs-keyword">return</span> gp, inheritTime
  }
  <span class="hljs-keyword">if</span> newWork {
   <span class="hljs-comment">// 可能有新的 timer 或 GC，重新开始</span>
   goto top
  }
  <span class="hljs-keyword">if</span> w <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> (pollUntil <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> w <span class="hljs-operator">&#x3C;</span> pollUntil) {
   <span class="hljs-comment">// Earlier timer to wait for.</span>
   pollUntil <span class="hljs-operator">=</span> w
  }
 }

 <span class="hljs-comment">// 没有任何 work 可做。</span>
        <span class="hljs-comment">// 如果我们在 GC mark 阶段，则可以安全的扫描并标记对象为黑色</span>
        <span class="hljs-comment">// 然后便有 work 可做，运行 idle-time 标记而非直接放弃当前的 P。</span>
 <span class="hljs-keyword">if</span> gcBlackenEnabled <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> gcMarkWorkAvailable(_p_) {
  node :<span class="hljs-operator">=</span> (<span class="hljs-operator">*</span>gcBgMarkWorkerNode)(gcBgMarkWorkerPool.<span class="hljs-built_in">pop</span>())
  <span class="hljs-keyword">if</span> node <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
   _p_.gcMarkWorkerMode <span class="hljs-operator">=</span> gcMarkWorkerIdleMode
   gp :<span class="hljs-operator">=</span> node.gp.ptr()
   casgstatus(gp, _Gwaiting, _Grunnable)
   <span class="hljs-keyword">if</span> trace.enabled {
    traceGoUnpark(gp, <span class="hljs-number">0</span>)
   }
   <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
  }
 }
        .....
 <span class="hljs-comment">// 放弃当前的 P 之前，对 allp 做一个快照</span>
 allpSnapshot :<span class="hljs-operator">=</span> allp
 idlepMaskSnapshot :<span class="hljs-operator">=</span> idlepMask
 timerpMaskSnapshot :<span class="hljs-operator">=</span> timerpMask

 <span class="hljs-comment">// 准备归还 p，对调度器加锁</span>
 lock(<span class="hljs-operator">&#x26;</span>sched.lock)
        <span class="hljs-comment">// 进入了 gc，回到顶部并停止 m</span>
 <span class="hljs-keyword">if</span> sched.gcwaiting <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> _p_.runSafePointFn <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
  goto top
 }
        <span class="hljs-comment">// 全局队列中又发现了任务</span>
 <span class="hljs-keyword">if</span> sched.runqsize <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  gp :<span class="hljs-operator">=</span> globrunqget(_p_, <span class="hljs-number">0</span>)     <span class="hljs-comment">// 赶紧偷掉返回</span>
  unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
  <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
 }
 <span class="hljs-keyword">if</span> releasep() <span class="hljs-operator">!</span><span class="hljs-operator">=</span> _p_ {         <span class="hljs-comment">// 归还当前的 p</span>
  <span class="hljs-keyword">throw</span>(<span class="hljs-string">"findrunnable: wrong p"</span>)
 }
 pidleput(_p_)       <span class="hljs-comment">// 将 p 放入 idle 链表</span>
 unlock(<span class="hljs-operator">&#x26;</span>sched.lock)      <span class="hljs-comment">// 完成归还，解锁</span>

 <span class="hljs-comment">// 这里要非常小心: 线程从自旋到非自旋状态的转换，可能与新 Goroutine 的提交同时发生</span>
 wasSpinning :<span class="hljs-operator">=</span> _g_.m.spinning
 <span class="hljs-keyword">if</span> _g_.m.spinning {
  _g_.m.spinning <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>    <span class="hljs-comment">// M 即将睡眠，状态不再是 spinning</span>
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">int32</span>(atomic.Xadd(<span class="hljs-operator">&#x26;</span>sched.nmspinning, <span class="hljs-number">-1</span>)) <span class="hljs-operator">&#x3C;</span> <span class="hljs-number">0</span> {
   <span class="hljs-keyword">throw</span>(<span class="hljs-string">"findrunnable: negative nmspinning"</span>)
  }

  <span class="hljs-comment">// 再次检查所有的 runqueue</span>
  _p_ <span class="hljs-operator">=</span> checkRunqsNoP(allpSnapshot, idlepMaskSnapshot)
  <span class="hljs-keyword">if</span> _p_ <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
   acquirep(_p_)
   _g_.m.spinning <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
   atomic.Xadd(<span class="hljs-operator">&#x26;</span>sched.nmspinning, <span class="hljs-number">1</span>)
   goto top
  }

  <span class="hljs-comment">// 再次检查 idle-priority GC work，和上面重新找 runqueue 的逻辑类似</span>
  _p_, gp <span class="hljs-operator">=</span> checkIdleGCNoP()
  <span class="hljs-keyword">if</span> _p_ <span class="hljs-operator">!</span><span class="hljs-operator">=</span> nil {
   acquirep(_p_)
   _g_.m.spinning <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
   atomic.Xadd(<span class="hljs-operator">&#x26;</span>sched.nmspinning, <span class="hljs-number">1</span>)

   <span class="hljs-comment">// Run the idle worker.</span>
   _p_.gcMarkWorkerMode <span class="hljs-operator">=</span> gcMarkWorkerIdleMode
   casgstatus(gp, _Gwaiting, _Grunnable)
   <span class="hljs-keyword">if</span> trace.enabled {
    traceGoUnpark(gp, <span class="hljs-number">0</span>)
   }
   <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
  }

  <span class="hljs-comment">// 最后, 检查 timer creation</span>
  pollUntil <span class="hljs-operator">=</span> checkTimersNoP(allpSnapshot, timerpMaskSnapshot, pollUntil)
 }

 <span class="hljs-comment">// 再次检查 netpoll 网络轮询器，和上面重新找 runqueue 的逻辑类似</span>
 <span class="hljs-keyword">if</span> netpollinited() <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> (atomic.Load(<span class="hljs-operator">&#x26;</span>netpollWaiters) <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> pollUntil <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> atomic.Xchg64(<span class="hljs-operator">&#x26;</span>sched.lastpoll, <span class="hljs-number">0</span>) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> {
  ......
  lock(<span class="hljs-operator">&#x26;</span>sched.lock)
  _p_ <span class="hljs-operator">=</span> pidleget()
  unlock(<span class="hljs-operator">&#x26;</span>sched.lock)
  <span class="hljs-keyword">if</span> _p_ <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nil {
   injectglist(<span class="hljs-operator">&#x26;</span>list)
  } <span class="hljs-keyword">else</span> {
   acquirep(_p_)
   <span class="hljs-keyword">if</span> <span class="hljs-operator">!</span>list.empty() {
    gp :<span class="hljs-operator">=</span> list.<span class="hljs-built_in">pop</span>()
    injectglist(<span class="hljs-operator">&#x26;</span>list)
    casgstatus(gp, _Gwaiting, _Grunnable)
    <span class="hljs-keyword">if</span> trace.enabled {
     traceGoUnpark(gp, <span class="hljs-number">0</span>)
    }
    <span class="hljs-keyword">return</span> gp, <span class="hljs-literal">false</span>
   }
   <span class="hljs-keyword">if</span> wasSpinning {
    _g_.m.spinning <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
    atomic.Xadd(<span class="hljs-operator">&#x26;</span>sched.nmspinning, <span class="hljs-number">1</span>)
   }
   goto top
  }
 } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> pollUntil <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> netpollinited() {
  pollerPollUntil :<span class="hljs-operator">=</span> <span class="hljs-keyword">int64</span>(atomic.Load64(<span class="hljs-operator">&#x26;</span>sched.pollUntil))
  <span class="hljs-keyword">if</span> pollerPollUntil <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> pollerPollUntil <span class="hljs-operator">></span> pollUntil {
   netpollBreak()
  }
 }
 stopm()     <span class="hljs-comment">// 真的什么都没找到，暂止当前的 m</span>
 goto top
}
</code></pre><p>runtime.findrunnable 函数的主要工作是：</p><p>1）首先检查是是否正在进行 GC，如果是则休眠当前的 M ； 2）尝试从本地队列中取 G，如果取到，则直接返回，否则继续从全局队列中找 G，如果找到则直接返回； 3）检查 netpoll 网络轮询器中是否有 G，如果有，则直接返回； 4）如果此时仍然无法找到 G，则从其他 P 的本地队列中偷取；从其他 P 本地队列偷取的工作会执行四轮，如果找到 G，则直接返回； 5）所有的可能性都尝试过了，在准备休眠 M 之前，还要进行额外的检查； 6）首先检查此时是否是 GC mark 阶段，如果是，则直接返回 mark 阶段的 G； 7）如果仍然没有，则对当前的 P 进行快照，准备对调度器进行加锁； 8）当调度器被锁住后，仍然还需再次检查这段时间里是否有进入 GC，如果已经进入了 GC，则回到第一步，阻塞 M 并休眠； 9）如果又在全局队列中发现了 G，则直接返回； 10）当调度器被锁住后，我们彻底找不到任务了，则归还释放当前的 P，将其放入 idle 链表中，并解锁调度器； 11）当 M、P 已经解绑后，我们需要将 M 的状态切换出自旋状态，并减少 nmspinning； 12）此时仍然需要重新检查所有的队列，如果我们又在全局队列中发现了 g，则直接返回； 13）还需要再检查是否存在 poll 网络的 G，如果有，则直接返回； 14）什么也没找到，休眠当前的 M。</p><p>runtime.findrunnable 函数的逻辑如图 5.2 所示：</p><p><em>图5.2 runtime.findrunnable()函数逻辑</em></p><p>如果调度循环函数 runtime.schedule() 从通过 runtime.globrunqget() 从全局队列，通过 runtime.runqget() 从 P 本地队列，以及 runtime.findrunnable 从各个地方，获取到了一个可执行的 G， 则会调用 runtime.execute() 函数去执行它，它会通过 runtime.gogo() 将 G 调度到当前线程上开始真正执行，之后 runtime.gogo() 会调用 runtime.goexit()，并依次进入runtime.goexit1()，和 runtime.goexit0()，最后在 runtime.goexit0() 函数的结尾会再次调用 runtime.schedule() ，进入下一次调度循环。</p><h3 id="h-6" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">6. 总结</h3><p>总结的内容已经放在了开头的结论中了。</p><p>最近听到一句话：任何领域的顶尖高手，都是在花费大量时间和身心投入后，达到了用灵魂触碰到更高维度的真实存在的境界，而不是在用头脑在思考和工作，因此作出来的产品都极具美感、实用性和创造性，就好像偷取了上帝的创意一样。</p><p>在 Go 调度器的底层原理的学习中，不仅需要亲自花时间去分析源码的细节，更加要大量阅读 Go 开发者的文章，需要用心体会机制设计背后的原因。</p><p>Reference</p><p>Go语言设计与实现6.5节调度器 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/%23g">https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/#g</a></p><p>Go语言原本6.3节MPG模型与并发调度单元 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/mpg/">https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/mpg/</a></p><p>Go调度器系列（3）图解调度原理 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//lessisbetter.site/2019/04/04/golang-scheduler-3-principle-with-graph/">https://lessisbetter.site/2019/04/04/golang-scheduler-3-principle-with-graph/</a></p><p>Golang的协程调度器原理及GMP设计思想 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//www.yuque.com/aceld/golang/srxd6d">https://www.yuque.com/aceld/golang/srxd6d</a></p><p>详解Go语言调度循环源码实现 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//www.luozhiyun.com/archives/448">https://www.luozhiyun.com/archives/448</a></p><p>golang源码分析之协程调度器底层实现( G、M、P) <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_25870633/article/details/83445946">https://blog.csdn.net/qq_25870633/article/details/83445946</a></p><p>「译」Scheduling In Go Part I - OS Scheduler <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//blog.lever.wang/golang/os_schedule/">https://blog.lever.wang/golang/os_schedule/</a></p><p>「译」Scheduling In Go Part II - Go Scheduler <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//blog.lever.wang/golang/go_schedule/">https://blog.lever.wang/golang/go_schedule/</a></p><p>深入 golang -- GMP调度 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://zhuanlan.zhihu.com/p/502740833">https://zhuanlan.zhihu.com/p/502740833</a></p><p>深度解密 Go 语言之 scheduler <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//qcrao.com/post/dive-into-go-scheduler/">https://qcrao.com/post/dive-int</a></p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[如何建立自己的宏观经济分析框架？币圈]]></title>
            <link>https://paragraph.com/@dehop/6IPOz1vmhO4vKsunfO9J</link>
            <guid>6IPOz1vmhO4vKsunfO9J</guid>
            <pubDate>Tue, 24 Jan 2023 07:10:52 GMT</pubDate>
            <description><![CDATA[本回答分为三部分： 1.宏观经济框架； 2.周期； 3.宏观经济学习的方法论。第一部分：宏观经济框架 宏观经济分析框架其实有很多，关键看你要分析什么问题，比如你是侧重经济增长和周期，还是侧重生产与分配。 大学经济学专业的教材，其实和日常的宏观分析实践挺脱节的。我给题主分享一个我自己梳理的体系，梳理的基础目的是串联所有宏观指标，将其构成一个体系，以便对整体形成判断。 先给个总体框架，下面慢慢讲： 首先，把现在的经济划分为两个层面：实体层面和金融层面。 实体层面主要包括生产活动、非金融部分的流通活动（物流就算做实体层面，而信贷则不计入）。 金融层面主要是对各种资产做定价，而这个定价的本质就是对资源的分配。 实体层面的活动是人类任何阶段都要进行的，只要人类存在，就必须从事生产。但金融层面不然，金融层面是随着生产力发展而变化的，因为金融本身要做的是分配，如果有更好的分配方式，金融本身就会剧变，甚至消失。 我们关心宏观经济，主要是关心生产活动，即实体层面，因为蛋糕变大可以解决很多问题。而统计实体经济，就可以从这两个层面入手。 **从实体层面统计，**这个很容易理解，比如我们看一看工业增加值...]]></description>
            <content:encoded><![CDATA[<p>本回答分为三部分：</p><p>1.宏观经济框架；</p><p>2.周期；</p><p>3.宏观经济学习的方法论。</p><hr><p><strong>第一部分：宏观经济框架</strong></p><p>宏观经济分析框架其实有很多，关键看你要分析什么问题，比如你是侧重经济增长和周期，还是侧重生产与分配。</p><p>大学经济学专业的教材，其实和日常的宏观分析实践挺脱节的。<strong>我给题主分享一个我自己梳理的体系，梳理的基础目的是串联所有宏观指标，将其构成一个体系，以便对整体形成判断</strong>。</p><p>先给个总体框架，下面慢慢讲：</p><p><strong>首先，把现在的经济划分为两个层面：实体层面和金融层面。</strong></p><p><strong>实体层面主要包括生产活动、非金融部分的流通活动（物流就算做实体层面，而信贷则不计入）。</strong></p><p><strong>金融层面主要是对各种资产做定价，而这个定价的本质就是对资源的分配。</strong></p><p>实体层面的活动是人类任何阶段都要进行的，只要人类存在，就必须从事生产。但金融层面不然，金融层面是随着生产力发展而变化的，因为金融本身要做的是分配，如果有更好的分配方式，金融本身就会剧变，甚至消失。</p><p>我们关心宏观经济，主要是关心生产活动，即实体层面，因为蛋糕变大可以解决很多问题。而<strong>统计实体经济，就可以从这两个层面入手。</strong></p><p>**从实体层面统计，**这个很容易理解，比如我们看一看工业增加值，看一看固定资产投资，查一下用电量和物流数据，就可以比较好的认识当下的经济情况。</p><p>**从金融层面统计，**其背后的原理是因为在市场经济下，一切生产活动最终都要以市场定价的方式进行流通，而这一切都是以货币计量的。消费，购房，企业扩产，政府基建，这一个个实体经济行为，背后都对应着具体的金融统计：消费贷款，居民中长期消费贷款，企业中长期贷款，政府债券。正如前面说的，金融给资产做定价的过程，本质上是在进行资源分配，而每一个实体经济行为都是需要资源支撑的，因此可以通过金融数据对实体情况进行统计。</p><p>随着历史演化，专门从事金融层面核心资源，即<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=%E4%BF%A1%E7%94%A8%E5%88%9B%E9%80%A0&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2367963781%7D">信用创造</a>的行业：银行业出现了，并逐步脱离其他行业。<strong>至此，“银行体系-金融市场-实体经济”的三角关系形成。之后，随着发币权收归央行所有，再加入财政体系，就形成了这个基本框架</strong>：</p><p>以此为基础，只要读懂<strong>央行</strong>、<strong>银行体系（信贷）</strong>、<strong>实体经济（以GDP为框架的指标体系）</strong>、<strong>财政体系</strong>，<strong>就可以判断宏观经济的情况了</strong>(金融市场是基于这些而变化的，所以先不考虑)。</p><p>呃，好累啊，而且知乎这个编辑器太差了，经常跳出来乱码，直接把我梳理的笔记放这吧：</p><p><strong>基础部分1.1：央行资产负债表</strong></p><p><strong>基础部分1.2：超储率体系（主要是分析央行货币政策工具）</strong></p><p>央行资产负债表和超储率体系是分析流动性框架的基础。现实分析中最常涉及到的就是货币政策工具mlf/<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=slf&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2367963781%7D">slf</a>之类。注意把握其在央行表内的位置比例和工具间的差异，可以对央妈的放水与回笼有基础的认识。</p><p><strong>基础部分2：社融与M2（分析经济中的资金需求与供给）</strong></p><p>我梳理这张表是为了分析社会中的资金供求情况，该表极为重要，包含央行每月发布的几乎所有数据：<strong>货币当局资产负债表+其他存款</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=%E7%B1%BB%E9%87%91%E8%9E%8D&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2367963781%7D"><strong>类金融</strong></a><strong>机构资产负债表(商业银行为主)+信贷收支表+社融+M2</strong>。阅读类似财务报表的资料时，注意两点：1、来源与去向；2、与哪个主体相关。央行的这几张表，主要主体有居民部门、实体部门(企业+机关团体)、政府部门和金融部门。</p><p><strong>基础部分3：GDP体系（支出法和生产法是很重要的分析框架）</strong></p><p>几乎所有的非金融宏观经济指标都可以在这里找到对应的经济活动。分析时，一定<strong>要建立支出法和生产法之间的联系</strong>。我国是第一制造国，所以需要用生产法观察工业以判断产业结构的变化与整体周期阶段；而在分析经济增长的来源时，更多需要从支出法，即三家马车的角度思考。</p><p>同时也要通过对GDP代表的统计方法的了解，增强对统计数据的认识。比如之前，全国各省GDP之和往往是大于全国值的，这里除了有部分地区高报外，还有跨省经济活动被重复度量的原因，而这就是个统计问题。随着国家统计局协调各省统计局建立统一的统计活动，这种现象会越来越少。</p><p><strong>基础部分4：财政体系（有些复杂，记住一般公共预算和</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=%E6%94%BF%E5%BA%9C%E6%80%A7%E5%9F%BA%E9%87%91%E9%A2%84%E7%AE%97&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2367963781%7D"><strong>政府性基金预算</strong></a><strong>即可）</strong></p><p>如果没有理清财政收支的基本框架，建议看自媒体财政分析时不要评论，因为你尚不具备判断的能力。财政体系是十分庞杂的，但先了解<strong>一般公共预算的收入(税收结构与金额)与支出结构及分类</strong>，<strong>政府性基金预算(土地出让金和专项债为大头)收支，和各种赤字的计算口径</strong>，可以快速理解当下财政状况。</p><p>这些都是宏观分析的每一个局部，把这些局部统筹起来才能进行整体分析，放两张我分析地产和制造业投资的笔记。</p><p><strong>3、</strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=%E6%9C%B1%E6%A0%BC%E6%8B%89%E5%91%A8%E6%9C%9F&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2367963781%7D"><strong>朱格拉周期</strong></a><strong>，也有叫产能周期的，不重要。</strong></p><p>为啥不重要？因为我国基本看不出来这个周期。原因在于过去20年，我国产业结构变化极大，三大产业的比例也有分阶段的变化，因此，产能方面体现为趋势项强于周期项(你可以在前面制造业投资的导图里看到，我国固定资产投资的周期性弱于趋势性)。当然，如果你用一些数学方法剔除趋势项后，也能观察出周期。但我日常跟踪数据并不会做特别复杂的处理，因为每次处理都会引入新的假设。通常的，学者认为一个朱格拉周期包含2-3个库存周期，也就是大约10年。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[80天入门区块链 带你进币圈炒币]]></title>
            <link>https://paragraph.com/@dehop/80</link>
            <guid>bhTDY5HEcjkXibfiGbkG</guid>
            <pubDate>Tue, 24 Jan 2023 07:02:46 GMT</pubDate>
            <description><![CDATA[从 1 到 N 此时你已经选定了赛道，接下来面临的问题就是如何在这个赛道深耕下去，我给你准备了详细的践行方案，整个流程分七步，深唿吸，我们继续： 第 1 步：研究这个赛道最优秀的20个项目，熟悉它们的发展历史与解决方案 如果你喜欢 NFT，那你就去 OpenSea，将排行榜上市值前 20 的项目从前期宣传、发行方式到后期运营再到生态拓展每一步都了解清楚，思考他们的内在联系，总结这些项目为什么可以成为蓝筹；再去了解那些在某个时刻红极一时的项目，他们是如何触及到了社区的情绪，又是因为什么没有将热度延续下去；最后总结出自己对这些项目的思考逻辑，形成自己的研究框架。 如果你喜欢 DeFi，那就去 DeFi Llama，将 TVL 排行榜前 20 的项目从白皮书、要解决的问题、核心方案、经济模型、发展历史等每个方面都了解清楚，了解整个 DeFi 历史的进化历程，明白每个项目最显着的特点，思考不同项目之间的联系，建立起自己对整个 DeFi 生态的认知。 其他领域以此类推。谨记：懂得行业的前提是你看过足够多的项目，量变引起质变，不要以为自己是天才，看几个解读文章和视频就懂得了全部，一定要对未知...]]></description>
            <content:encoded><![CDATA[<p><strong>从 1 到 N</strong></p><p>此时你已经选定了赛道，接下来面临的问题就是如何在这个赛道深耕下去，我给你准备了详细的践行方案，整个流程分七步，深唿吸，我们继续：</p><p><strong>第 1 步：研究这个赛道最优秀的20个项目，熟悉它们的发展历史与解决方案</strong></p><p>如果你喜欢 NFT，那你就去 OpenSea，将排行榜上市值前 20 的项目从前期宣传、发行方式到后期运营再到生态拓展每一步都了解清楚，思考他们的内在联系，总结这些项目为什么可以成为蓝筹；再去了解那些在某个时刻红极一时的项目，他们是如何触及到了社区的情绪，又是因为什么没有将热度延续下去；最后总结出自己对这些项目的思考逻辑，形成自己的研究框架。</p><p>如果你喜欢 DeFi，那就去 DeFi Llama，将 TVL 排行榜前 20 的项目从白皮书、要解决的问题、核心方案、经济模型、发展历史等每个方面都了解清楚，了解整个 DeFi 历史的进化历程，明白每个项目最显着的特点，思考不同项目之间的联系，建立起自己对整个 DeFi 生态的认知。</p><p>其他领域以此类推。谨记：懂得行业的前提是你看过足够多的项目，量变引起质变，不要以为自己是天才，看几个解读文章和视频就懂得了全部，一定要对未知的行业抱以敬意，谦虚是最佳的学习态度。</p><p><strong>第 2 步：善用工具，使用数据分析工具对项目深入研究</strong></p><p>工欲善其事必先利其器。虽然大部分项目的数据在链上可以拿到，但是并不是所有人都有技术背景，不懂技术的玩家可以使用一些工具进行辅助研究。我将在 Web3 项目研究中可能会用到的网站都放在了上面整理的生态图谱中。这里我以 NFT 为例，简单介绍几个最常用的网站。</p><p>综合数据分析网站：<strong>NFTGO</strong>。市场整体数据、细分赛道数据、单个项目数据等等都有收录，网站十分符合国人的使用习惯，也是国人的产品，查询数据必备。</p><p>作用：分析市场、项目数据，关注鲸鱼动向</p><p>社区数据查询网站：**NFT Inspect。**收录了大部分NFT项目的 Twitter 社区数据，包括话题数量、趋势、头像使用率等等，全方位体现一个项目的社区热度，对于提前埋伏一个项目有很大的帮助。</p><p>作用：根据社区数据判断项目热度，做出对应的行动。</p><p>项目鲸鱼关注度查询网站：**TwitterScan。**用于新项目的研究，项目鲸鱼关注量越高，项目后期发展潜力越大。</p><p>作用：一般用于评判项目前期值不值得肝白，以及根据鲸鱼增长趋势判断是否会拉升。</p><p>项目历史数据查询网站：**NFT Flips。**查看项目本身数据，二级市场购买趋势，监控链上动向。</p><p>作用：前期评判一个项目值不值得 Mint，后期评判值不值得冲二级。</p><p>除了以上网站外，在你研究项目的过程中还会有很多不同的需求，根据需求寻找合适的工具是一项必备技能。</p><p>第 3 步：拓宽自己的消息渠道，最快获得项目的最新消息</p><p>经过以上两个步骤后，你已经有了对行业的整体认识，也具备了查询项目数据的能力，下面你就需要得到一个机会，一个发现优秀项目的机会。</p><p>你要明白一个道理，当你在解读文章、视频中看到一个项目的“最新”消息，那么它基本上已经不是最新的了，机会转瞬即逝，那么多人都在寻找，所以你一定要抢在前头。获得项目最新消息最好的途径就是各种社群，Discord、微信群是最直接的渠道，但是前提一定是这些社区足够专业也敏锐，你需要找到几个高质量的社区，他们在你成长的路上真的会给予你很多帮助。在社区中看到大多人都在冲的项目，然后立刻去研究！另外，Twitter 上专注信息发布的博主和项目方动态也是相当重要的，如果你觉得翻阅查找太麻烦也可以使用 TweetDeck 对信息流做整合处理。</p><p>第 4 步：学会分析新项目</p><p>此时你已经从社区小伙伴口中得知了一个可能很有潜力的项目，现在你需要考虑的是面对一个全新的项目，如何去分析，因为他没有链上数据，所以分析过程与上面会有所不同，参考步骤如下：</p><p>查看 NFT 图片，判断质量、是否符合玩家审美与潮流，特别需要注意分辨项目是否为盗图 查看项目方 Twitter，主要参考因素为：关注人数、互动活跃度、是否有你关注的大佬关注了此项目，初步观察项目方 Twitter互动的质量，分辨是否为机器人刷活跃 使用 TwitterScan 查看项目鲸鱼关注数量，与第一步配合分析项目方是否使用了机器人刷活跃 加入项目方 Discord，观察成员的的活跃程度与聊天话题，项目方 mod 的回复速度、准确率，Discord 的运营等等。 查看项目方官网，从官网的整体设计与细节可以看出项目方是否在认真做事，那些直接用模板改的网站基本上可以 Pass，另外项目的设计理念、团队、常见问题等在网站上也可以找到，这些都可以帮助你分辨项目的好坏。根据项目背后的团队判断这个项目的发展潜力、RUG 概率等等。 观察项目方运营手段，与其他项目、KOL 的联合宣发情况 在 Mint 时追踪“聪明钱包”的操作，使用 NFT Flips 查看 Mint 与二级市场情况，伺机而动 Mint 后观察交易频率、拉盘力度，选择短线操作 后续根据社区情绪选择合适的操作路线 第 5 步：尝试评析项目解决方案的优劣，给出改进建议</p><p>在你见过足够多的项目之后，你就会对这个行业有自己独特的认识，进而可以对某些项目作出评判，当然我希望这一切都有依据，而不是单纯的“我觉得”。</p><p>去总结某个项目成功或者失败的原因，几个项目之间的解决方案有什么不同，为什么这样设计，分别是为了解决什么问题，它们存在的缺陷可以怎么改进......当你习惯了去探究事情的本质，你会发现这个行业其实很纯粹、很简单。</p><p>第 6 步：建立自己的个人 IP</p><p>无论你选择了哪个赛道，我都推荐你将学习内容记录下来，如果你喜欢写短文就用 Twitter，喜欢写长文就用 mirror，喜欢录制视频就选择 YouTube，总之要让更多的人看到你，这对你的帮助是很大的。</p><p>你或许会因为这些内容遇到志同道合的同伴，或许会被项目方找到邀请你加入他们的团队，又或者它会成为你在求职时非常重要的凭借。Web3 是一片混沌之地，这里真正有价值的东西太少了，相信我，只要你足够努力，你一定会被发现，收获你应得的财富和荣誉。</p><p>第 7 步：加入组织/创建组织</p><p>一个人前进太孤独了，你需要伙伴，需要组织，或许它能给你情感上的支持，或许它能给你物质上的支撑，或许它可以带给你更多的思考。无论如何，一个优秀的团队对你个人的成长是大有裨益的。</p><p>这里有一点请注意，你的所有劳动都应该被尊重，现在很多 DAO 打着“为爱发电”的旗号让你无偿付出，但是这实际上却是在白嫖你。你要明白自己想要的是什么，金钱？人脉？学习？...... 你可以多多尝试，但是不要一直做傻事。</p><p>有些朋友不会仅仅满足于参与其他项目，而是会亲自下场做一个产品，这是非常值得鼓励的，Web3 需要优秀的 Builder。此时你面临的问题将是组织一个团队，作为发起人，你需要花费很长时间招募合适的伙伴，对每个人的特质了如指掌，充分调动团队的积极性去做成这件事。</p><p>如果你特别喜欢 Web3，真正想在这做些事情，你也可以联系我，我会给予你力所能及的帮助，谈不上指导，但是可以一起成长。</p><p>三、经验篇</p><p>这个篇章会以问答的形式呈现，尽量为大家解答那些困扰你的疑难问题。</p><ol><li><p>我可以以什么形式投身Web3？</p></li></ol><p>在 Web2 工作过一段时间或者大学即将毕业的的小伙伴经常会问这个问题，例如，我在某某大厂做产品、做开发，现在想进入 Web3，我可以做些什么呢，现在应怎么做呢？</p><p>首先，Web3 的公司组织架构与 Web2 差别并不大，产品、开发、运营等等职位都是相同的， 如果你的目标是在 Web3 的公司中寻一份工作，那你可以参考上文“生态图谱”学习并掌握这个行业最核心的知识架构。技术职位需要深入研究图谱上的“技术栈”模块，产品需要对项目的架构和经济模型深入研究，运营则把重心放在社区和项目方的各类活动设计上。将这些研究透彻，找到一份薪资可观的工作还是比较容易的。</p><p>但是我认为， 在 Web3 工作仅仅是第一层，接下来的第二层是参与优秀项目（这里的参与不是指为之工作）。很少有 Web3 的深度参与者只工作不参与其他项目的，用我一个朋友的话来说， 工资只是你的生活低保而已，Web3 真正的价值在社区中那些优秀项目之中。你不会因为超过行业平均水平的工资而财富自由，但是却可能因为深度参与一个靠谱的项目而功成名就，这个叙述毫不夸张，因为这样的事情在 Web3 经常发生。</p><p>一般来说，项目方会将 80% 左右的权益给到项目支持者，如果你通过一系列的研究找到了一个优秀的项目并持续的参与，相信我，你的回报是很可观的。但是，这一切都建立在你能发现优秀的项目！Web3 中 90% 的项目都会死掉，找到鱼目中混杂的珍珠是你必须修炼的技能。</p><p>第三层是建立个人 IP，并为行业的发展做出贡献。相比于有形的财富，个人价值的塑造反而更加珍贵。个人 IP 是在个人发展过程中逐步建立的，你输出的文章、视频、参与的项目、会议，都会成为 IP 的底层基础，当你有了足够的阅历与人脉，接下来便是回馈行业是时候了。</p><p>你可以站在行业布局的角度思考未来的走势与发展方向，尽自己所能为推动 Web3 的发展贡献一份力量，推出一个优秀的产品，追求更高的人生价值。链上信息是不可篡改的，你的所有贡献都会被记录，你对行业的贡献永远不会被磨灭。</p><ol><li><p>初入Web3，有哪些事情越早知道越好？</p></li></ol><p>这是一个混乱的行业，骗子远比好人多得多得多 不要相信任何人，除了你自己，我能给你的建议就是不要相信任何人的投资建议 不深入市场永远不知道市场是什么样的，没有实践就做出的评判都是口嗨 重视社区情绪，没有什么是一场 fomo 解决不了的 少在 Twitter 上看人吹牛撕逼，多关注一些优质博主 每次大涨大跌都预测准确的“神级交易员”一般都是靠返佣活着 优质中文 Web3 博主不超过 50 个 项目盈利最直接的方式还是 Token，你也是 不要瞧不起撸毛，该撸就撸，不要白不要，有时候利润很高 在深入研究之后勇敢发表自己的意见，不要怕嘲笑，每个人都是新人，这里没有专家 注重钱包安全，切勿随意授权，助记词不要截屏，特别是自己的主钱包 必要的合约知识在某些时候会发挥极大的优势 会卖比会买更重要 不要尝试说服别人相信你的观点，穷则独善其身，不要做无意义的事 虽然不想承认，但是目前国人的项目跑路风险最大，市场普遍不看好国人项目 3. 怎么才能最大程度上防止被骗呢？</p><p>这里可直接参考漫雾的《黑暗森林自救手册》，另外感谢@BTCOld8小伙伴的图片总结：</p><p><strong>写在最后</strong></p><p>我用了很长时间来写这个教程，逐字逐句校验，以免误人子弟，希望它能对你有所启发，Web3 非常需要更多建设者的加入！</p><p>另外，由于我的个人水平十分有限，不可能只用约一万字就将 Web3 阐述清晰，文章内容难免会有些矫枉过当，还请大家辩证的看待。如果您希望就某些方面与我深入沟通，请私信我并说明来意，Web3 的道路并不平坦，感谢有大家的建设和陪伴！</p><p>最后，以鱼池（F2Pool）在以太坊 PoW 时代最后一个区块上写的寄语作为结尾：只要勇于做出承诺，世界自会助你铲除不可逾越之阻碍；去完成未竟之梦想，宇宙绝不会抑制你前进的步伐，这即是奥义所在。</p><p>今年很艰难。</p><p>硕士生做实验被封控打断了好几次，年末更是直接被BA.5.4.2一波全带走，新冠暴击下，体力下降一半，精神力下降若干，实验室停摆了十多天了。</p><p>整个深圳所有体制内的岗位，2022年降薪，包括之前大火的初高中老师，深圳基层公务员；当然，也包括我自己。</p><p>青年人就业率进一步降低，考研大军进一步壮大。</p><p>絮絮叨叨乱七八糟扯半天，总之，2022年，国际国内“雷雨交加”。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[Web3 从新手入门到精通撸毛]]></title>
            <link>https://paragraph.com/@dehop/web3</link>
            <guid>1k3l0Aa6Mn5TE6LxpmFl</guid>
            <pubDate>Tue, 24 Jan 2023 06:54:07 GMT</pubDate>
            <description><![CDATA[其实在很早之前，我就计划写一篇《Web3 新手教程》，送给 想要进入Web3 或者 刚刚进入Web3 的小伙伴，但是考虑到两点原因，我还是打算把这件事推迟一些时日。 一是在各大媒体都鼓吹 All In Web3 的时候，难免会有很多朋友头脑一热，辞去大厂的工作，怀揣着暴富的梦想，冲刺进入 Web3 行业。但是 这无论是对于行业还是你个人来说，都不是一件正确的事，我也不想在那个时候火上浇油。希望大家做任何事情之前都要经过理性的思考，不要被各种声音影响了自己的判断。现在，随着市场的逐渐冷却和越来越多质疑声音的出现，人们开始回归理性，大量的投机者也带着谩骂和空空的钱包离开了，这个时候你如果还深深热爱着这个行业，那么我认为你才是应该投身 Web3 的人，我也愿意把我的一些经验和思考分享出来，供大家参考。 二是我实在认为自己对行业的认知深度还不够，需要更多的时间研究项目、观察行业动向与社区情绪，不想早早地写篇所谓的教程误人子弟。当然有的朋友可能觉得现在进入 Web3 是不是太晚了？我想说， 你什么时候进入 Web3 都不晚，**区块链诞生才短短十几年，而同为前沿科技的人工智能诞生已经六十多...]]></description>
            <content:encoded><![CDATA[<p>其实在很早之前，我就计划写一篇《Web3 新手教程》，送给 想要进入Web3 或者 刚刚进入Web3 的小伙伴，但是考虑到两点原因，我还是打算把这件事推迟一些时日。</p><p>一是在各大媒体都鼓吹 <strong>All In Web3</strong> 的时候，难免会有很多朋友头脑一热，辞去大厂的工作，怀揣着暴富的梦想，冲刺进入 Web3 行业。但是 这无论是对于行业还是你个人来说，都不是一件正确的事，我也不想在那个时候火上浇油。希望大家做任何事情之前都要经过理性的思考，不要被各种声音影响了自己的判断。现在，随着市场的逐渐冷却和越来越多质疑声音的出现，人们开始回归理性，大量的投机者也带着谩骂和空空的钱包离开了，这个时候你如果还深深热爱着这个行业，那么我认为你才是应该投身 Web3 的人，我也愿意把我的一些经验和思考分享出来，供大家参考。</p><p>二是我实在认为自己对行业的认知深度还不够，需要更多的时间研究项目、观察行业动向与社区情绪，不想早早地写篇所谓的教程误人子弟。当然有的朋友可能觉得现在进入 Web3 是不是太晚了？我想说， 你<strong>什么时候进入 Web3 都不晚</strong>，**区块链诞生才短短十几年，而同为前沿科技的人工智能诞生已经六十多年了，所以 Web3 真的是处在一个特别早期的阶段。**这里没有权威大佬和专家，每个人都在不停的学习、思考，几天不关注市场就会被别人落下，大家的机会都是均等的，拼的就是努力和认知。</p><p>如果你也喜欢 Web3，希望在这做些有趣的、有意义的事情，那么我希望这篇文章可以帮助到你。</p><p>下面我将以**《认知篇》 、《实践篇》 、《经验篇》**三部分为大家描绘一幅 Web3 的宏伟蓝图，通过阅读本文，你会得到以下问题的答案：</p><p>Web3 的核心思想与正确认识 你是真的否适合 Web3 从0到1：教你走出迈向 Web3 的第一步 生态图谱：带你审视 Web3 全貌，公链 + DeFi + NFT + GameFi + DAO + SocialFi + 基础设施 + 技术栈 + 顶级投资机构 + 常用工具，为你的 Web3 之路指引方向 从1到N：手把手教你如何在一个赛道深耕（建立认知、分析项目、使用工具） 投身 Web3 的最佳姿态（工作、投资、创业、私域、个人IP） 几点必须谨记的行业原则 Web3 黑暗森林防骗手册 一、认知篇</p><p>首先我们需要思考一个问题：你为什么想进入 Web3 行业？是听朋友或者营销号说参与 Web3 可以暴富？是想投机性的以小博大赚一笔就走？是听到这个概念想来学习了解？还是真的认同加密理念对这个行业充满热爱？</p><p>这个问题的答案不需要写下来向公众宣布，你只需要将其记在心中，所以对自己请不要有任何隐瞒。</p><p>好了，现在你已经有了答案，我们继续。</p><p>财富？</p><p>我猜大家很多人都是被 Web3 的财富效应吸引来的，这并没什么错，毕竟谁能不爱钱呢？但是 Web3 真的能让你暴富吗？任何领域都遵循二八原则，Web3 更甚， 真正赚钱的人往往不足一成。如果你是以通过 Web3 让自己的暴富的心态参与进来，那结局很可能会是血本无归。Web3 的骗子远比你想象中的多得多得多，有些做法甚至能刷新你的认知，在这里你不能相信任何人，除了你自己！所以我们常说“DYOR”即 Do Your Own Research。保护好自己的本金，遇到任何说可以带你稳定盈利的人，请直接拉黑。</p><p>但是 Web3 确实也给了很多人跨越阶层的机会，这还是一个“原始竞技场”，没有绝对的规则、也没有一成不变的领袖，每个人各凭本事，公平的对决，以获得自己的权益。这里隔绝了大部分权力、关系，让很多底层人民有机会与华尔街精英站在同一条起跑线。</p><p>有位从北京离职定居大理做 Web3 开发的朋友说：之所以选择 Web3，是因为在这里大家更关注你这个人本身，而不是你的职场身份；在这里，你不是个工具，不需要扮演一个商业螺丝钉，你可以有点生命力，不用内卷、不被裁员，可以获得体面的报酬，可以享受阳光，可以不被无意义的工作透支健康，可以尽情释放创造力，这让人舒服多了。</p><p>所以，这样的行业又怎么能不让人着迷呢？</p><p>虽然潜在回报丰厚，但是这也注定了路途的艰辛， 请不要相信 Twitter 上那些每次涨跌都准确预测的交易分析师，他有这个本事就不会在那发 Twitter 了。一个人如果真的有赚钱的诀窍，他绝对不会炫耀更不会想教给在座的朋友们。当然 Web3 也有一些优秀的 KOL 在坚持传道解惑，只是这样的人真的很少，你需要仔细分辨，找到与自己风格相符的前辈。所以在 Web3，你可以参考一些优秀 KOL 的观点，但是请不要单纯的模仿复制，你需要慢慢培养自己的分析框架、投资逻辑。经历的多了、懂得的多了，你就是专家，你也就更有机会接触那璀璨的梦想。</p><p>理念？</p><p>选择一个领域，你至少要认同它的理念。</p><p>回顾比特币诞生的阶段，2008-2009，那时正值全球金融危机，人们对中心化的金融机构产生了不信任，事实也正是由于它们的错误决策才造成了一次次的经济危机。所以此时化名“中本聪”的神秘人（组织）提出了一种点对点的电子现金系统，也就是比特币。从此，货币可以脱离中心化银行系统运行，每个人的资产都在自己的钱包中，这笔资产抗审查、可追溯。慢慢的人们的信仰铸就了如今的比特币，加密文化的基石建立。</p><p>Web3 是传统加密文化的升华，它将数据所有权、去中心化身份、加密资产、新商业模式等作为关键要素，着眼于构建下一代价值互联网，将 Crypto 扩展到更广阔的领域，人们也有了更大的想象空间。比特币的创建是“神迹”，Web3 的理想是“殿堂”，但是在追求梦想的同时我们也要直视一些事实：去中心化的效率远远比不上中心化、去中心化滋生了很多黑色产业、项目鱼龙混杂骗子横行，也要明白很多行业、很多人并不需要去中心化服务。</p><p>Web3 极客们愿意牺牲效率换取真正的数据所有权；愿意自己承担风险而不献出数据让中心化机构为自己提供“安全服务”，拒绝“棱镜门”事件再次发生；愿意接受 Web3 的各种乱象并披荆斩棘勇敢前行；愿意相信 Web3 的未来并为其不断奋斗。</p><p>如果你也愿意相信 Web3，那么我推荐你在行业内深耕，这里有太多蓝海，终有一日你可以有所建树。但是如果你觉得比特币是空气，NFT 是骗局，GameFi 是无意义的旁氏，去中心化就是一个伪命题，那么我觉得 Web2 更适合你，那么多行业，那么多机会，不要逼自己融入一个自己不喜欢的圈子。所以我们要明白一个现状：不是所有人都适合 Web3 的，尤其是在现在这个阶段！</p><p>敬畏之心</p><p>选择进入 Web3，首先你要对这个行业充满敬畏，无论你之前是大厂高管还是硕博教授，在Web3 中你要接受自己是一个新人。Web3 的行业壁垒很高，除去技术不谈，单是思维就需要有很大的改变，你需要知道 Web3 的核心思想、行业的生态布局以及其中最优秀的解决方案。如果单纯用 Web2 思维审视 Web3，你可能不会理解诸多应用为什么会存在，如此“不严谨”的产品为什么有那么多资金和玩家涌入，然后得出 Web3 是骗局的结论。</p><p>所以大家要以空杯的心态投入 Web3 的学习与研究，承认自己对这个行业的无知，从 0 开始建立对行业的认识，这会是一个比较健康的成长路径。Web3 中没有谁是一定对的，更没有一成不变的权威大佬，行业每天都在进步都在改变，真正的学习者应该每天都关注市场动态、社区情绪，资金动向，再优秀的人一段时间不学习也会被市场抛弃。这一点与很多传统行业不同，有些行业的知识结构基本定型，技术在短时间内也不会有很大的创新，所以很多“专家”可以“一劳永逸”，用前 30 年的学识支撑后 40 年的工作，但是这种情况在 Web3不会发生。</p><p>如果你相信 Web3 的未来、接受 Web3 的混沌无序、享受这种不确定性，并且已经决定要在这大展拳脚了，那么下一篇章我会告诉你如何以正确的姿态踏上 Web3 的舞台！</p><p>二、实践篇T 型人才</p><p>Web3 的深度参与者都是“T型人才”。首先你要建立起对整个行业的认知，然后找到自己感兴趣的领域深入研究，成为这个领域的专家，一横一竖搭建起牢固的知识体系。但是现实却不尽如人意，很多人只懂某个领域的皮毛，比如玩 NFT 的人完全不懂什么是 DeFi，甚至我遇到一些 NFT 从业者都不知道什么是 EVM。Web3 的各个行业之间是有联系的，就像 Sudoswap 创立了 NFT AMM 机制，其实它和 Uniswap V3 就很像，但是能说清 AMM 是什么的人又有几个呢？虽然不懂这些妨碍不住你买 NFT，但是可能也就仅仅止步于买 NFT，如果你是想长期在Web3 深耕，那么对于整个行业的结构性认知是很有必要的。</p><p>对于入局 Web3 的步骤，我推荐尝试“公链-DeFi-NFT-GameFi-DAO、SocialFi-基础设施”这个学习路径。</p><p>公链是一切的基础；DeFi 是 Web3 的发动机；NFT 相对独立又特殊，是新的应用范式；GameFi 需要“游戏+NFT+DeFi”的融会贯通；DAO 和 SocialFi 更多的是聚焦在人的角度去设计的产品；隐私、存储等基础设施赛道涉及到的知识相对比较难懂，可以作为扩展类的学习。当然，钱包、交易所之类的应用就属于必需品了，这里不过多赘述。</p><p>从 0 到 1</p><p>如果此时你已经确定要在 Web3 这条路上走下去，那就让我们扣响 Web3 的大门吧，继续阅读下去，找到自己的兴趣所在：</p><ol><li><p>公链</p></li></ol><p>公链着眼于建设 Web3 的底层基础，围绕“去中心化、可扩展性、安全性”的“不可能三角”做技术的改进与探索。</p><p>以最近的新公链项目 Aptos 和 Sui 为例，其都采用了 Move 语言与事务处理并行化概念，又在侧重点上有所不同：Aptos 的动作较快，各种生态的布局发展都相对完整，社区热度高，短时间内聚集了很多开发者；而 Sui 的脚步明显更慢一点，但是核心技术却是比 Apots 要强，无论是更低且可预测的 Gas、验证者激励、分开支付的存储与交易费用或者更强的安全性，这些对于 L1 来说都是非常重要的。</p><ol><li><p>DeFi</p></li></ol><p>DeFi 致力于提升 Web3 的资产利用效率，构建链上的去中心化金融体系，它是各类项目的发动机和连接器，极大的丰富了链上应用场景，给用户提供了新的利益来源，提升了 Token 流动性。</p><p>借贷与去中心化交易所是 DeFi 的核心。在借贷平台，用户存放、借出代币，同时支付或赚取利息（与传统金融内的存本获息模式相同）；在去中心化交易所，玩家把资产对存入流动性池（Liquidity Pool）提供流动性，赚取手续费，更为关键的是，DEX 实现了各类代币的市场价格标定和资产之间的转换，这也让各种资产可以更高效的流转。</p><p>流动性挖矿（LiquidityMining）使 DeFi 的动力进一步增强。它创新性地改变了公司利润分配、治理的模式，同时创造了项目发展的正循环。用户通过流动性挖矿获得利息以及项目权益收益，而项目方也通过公平发行，正向激励项目的发展。</p><ol><li><p>NFT</p></li></ol><p>NFT 扩展了 Web3 的应用领域，使其从单一的金融属性开始向品牌、实物等方向发展，这一进步是具有跨时代意义的。如果仅有链上的 FT，Web3 再怎么发展也只是一个金融市场， 它与现实世界是割裂的， 而如果想让更多的人参与进来，Web3 就需要考虑如何与其他领域互通，而 NFT 有可能就是其中一个正确答案。</p><p>现在 NFT 的发展也是处于绝对的早期，虽然经过了一轮牛市，但是其中跑出来的项目基本上都是 PFP，成功的的商业模式也只有 Yuga Labs 的 “PFP-土地-Token-游戏-品牌”的“猿”宇宙帝国。不过最令人兴奋的是 NFT 所凝聚起的社区文化，它将最重要的因素——人，聚在了一起，即使在现在的慢慢长熊之中，很多社区也是激情满满，所以我们有理由相信，下一个叙事周期必会出现令人眼前一亮的 NFT 项目。</p><ol><li><p>GameFi</p></li></ol><p>用户过少是阻碍 Web3 发展的重要原因，而 GameFi 很有可能打开这扇流量之门。Play2Earn 的模式吸引了大量用户参与，Axie 在巅峰时期的日均收益比某些东南亚国家的人均工资还要高，但是目前链游的经济模型依旧不完善，几乎没有项目能够逃脱死亡螺旋。</p><p>其实 GameFi 的本质还是 DeFi，游戏只是它的外壳。项目的持续发展需要足够的流动性，而这里的流动性很大程度上需要新用户的加入。玩家购买 NFT 进入游戏给游戏增加流动性，产出的 Token 一部分自我消耗一部分出售给新用户，这便是最简单的内循环，在新用户数量快速增长时，NFT 和 Token 需求增加、价格上升，玩家和和气气，市场欣欣向荣，但是当新用户增长放缓时，游戏生态内的 Token 没有足够的消耗途径，供大于求、价格下跌，而价格下跌导致回本周期变长，这会使得新用户数量进一步下降，进入死亡螺旋的节奏。</p><p>虽然面临困境，但是 GameFi 绝对是最潜力的赛道之一，近期 GameFi 也是受到了资本的青睐，融资金额远超其他领域。在下一个周期，我们有理由相信类似 StepN 的现象级链游会越来越多，更多的人也会通过链游进入 Web3。</p><ol><li><p>DAO、SocialFi</p></li></ol><p>SocialFi 是在这轮牛市中唯一没有项目跑出来的赛道，大家基本都认同社交是必要的需求，也具有极高的上限，CZ 在年初也是公开表示看好 SocialFi。但不少机构都认为现有的 SocialFi 项目尚处于发展早期，模式尚不清晰且没有形成可持续范式。若仅依靠代币经济和对 Web2 社交进行包装改造，则难以从传统巨头手中获取用户。不过近期有一些社交类项目开始被资本注意到，希望下个周期可以看到 SocialFi 有好的产品出现。</p><p>DAO 是全新的团队组织架构，极具活力与效率，自从这个概念被广泛传播之后，各种各样的DAO 如雨后春笋一般迅速发展壮大。我相信它会是一个开创性的应用，但是现在来看却并不成熟，DAO 不仅仅是拉一个微信群，更不是资本白嫖劳动力的理由。随着组织与治理工具的完善与人们认知的提高，DAO 肯定会朝一个更好的方向发展。</p><p>生态图谱</p><p>Web3的每个赛道都有自己独特的魅力，你需要对各个赛道的优秀项目以及生态有一个全面的认识，然后找到自己最喜欢的方向深入研究。为了方便大家的学习，我将 Web3 各领域的龙头的项目、常用工具以及顶级投资机构、开发技术栈等都归入了下图之中，并且对每个项目都用一句话做了简单总结。如果你将这张图中的项目、工具全部都研究透彻，那你绝对已经超越了 Web3 90% 以上的人。在学习的同时，你会发现自己的兴趣所在，找到那个属于你的赛道，并开始深入研究：</p><p>项目鲸鱼关注度查询网站：**TwitterScan。**用于新项目的研究，项目鲸鱼关注量越高，项目后期发展潜力越大。</p><p>作用：一般用于评判项目前期值不值得肝白，以及根据鲸鱼增长趋势判断是否会拉升。</p><p>项目历史数据查询网站：**NFT Flips。**查看项目本身数据，二级市场购买趋势，监控链上动向。</p><p>作用：前期评判一个项目值不值得 Mint，后期评判值不值得冲二级。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[最好的加密货币研究工具]]></title>
            <link>https://paragraph.com/@dehop/njJ5TC1mosTttmSyORLI</link>
            <guid>njJ5TC1mosTttmSyORLI</guid>
            <pubDate>Tue, 24 Jan 2023 06:47:57 GMT</pubDate>
            <description><![CDATA[DeFi网站TheDeFiEdge创始人Edgy近日分享了他每天都在用的10个必备的加密货币研究工具。 1、Uniwhale（分析仪表盘） 我使用这个工具跟踪不同L1间的跨链桥活动。 我可以使用UniWhale观察储存和提取情况，掌握L1整体动向。 2、Dune Analytics （分析仪表盘） Dune可以让你将不同指标可视化，生成明了易懂的图表。 从DeFi的应用到具体某个dapp仪表盘，一切都变得可视化。 3、Crypto Panic （新闻聚合器） 这是我获知多数加密货币新闻的途径（除Twitter之外）。 可将它看做加密货币新闻界的Reddit。 新闻量大得你看不过来。 我按“重要”标签分类，从海量消息里筛选出有价值的内容。 4、Santiment （加密情报平台） 多数人用来查看价格图表。 Sanbase可以让你将价格和其他行为结合在一起。 你可以查看价格+发展活动+社会趋势（以及其他指标） 5、Nansen（链上分析平台） 其最佳功能是“Smart Money”（聪明的钱）——可跟踪那些经验证表现最佳的巨鲸账号。（例如基金和先行者） 使用它查看聪明的钱都在买入什么...]]></description>
            <content:encoded><![CDATA[<p>DeFi网站TheDeFiEdge创始人Edgy近日分享了他每天都在用的10个必备的加密货币研究工具。</p><p>1、Uniwhale（分析仪表盘）</p><p>我使用这个工具跟踪不同L1间的跨链桥活动。</p><p>我可以使用UniWhale观察储存和提取情况，掌握L1整体动向。</p><p>2、Dune Analytics （分析仪表盘）</p><p>Dune可以让你将不同指标可视化，生成明了易懂的图表。</p><p>从DeFi的应用到具体某个dapp仪表盘，一切都变得可视化。</p><p>3、Crypto Panic （新闻聚合器）</p><p>这是我获知多数加密货币新闻的途径（除Twitter之外）。</p><p>可将它看做加密货币新闻界的Reddit。</p><p>新闻量大得你看不过来。</p><p>我按“重要”标签分类，从海量消息里筛选出有价值的内容。</p><p>4、Santiment （加密情报平台）</p><p>多数人用来查看价格图表。</p><p>Sanbase可以让你将价格和其他行为结合在一起。</p><p>你可以查看价格+发展活动+社会趋势（以及其他指标）</p><p>5、Nansen（链上分析平台）</p><p>其最佳功能是“Smart Money”（聪明的钱）——可跟踪那些经验证表现最佳的巨鲸账号。（例如基金和先行者）</p><p>使用它查看聪明的钱都在买入什么，你便可以在YouTubers（油管主播）开始制作视频之前早一步买入。</p><p>6、Messari（市场情报平台）</p><p>我更愿意把Messari比作瑞士军刀。</p><p>分析加密货币最具价值的指标，这些功能每月付费25美元即可获取。</p><p>7、Token Terminal（加密货币金融数据）</p><p>人们受够了通货膨胀的代币和纸币。</p><p>他们想要看到真正的收益情况。</p><p>使用Token Terminal查看哪些区块链和dapp正在赚钱。</p><p>8、Fear &amp; Greed Index（情绪指标工具）</p><p>你阅读Twitter内容是比较难探究真相的。</p><p>万一你关注的是一群永远乐观的牛人，该怎么办？</p><p>我查看fear &amp; greed index（恐惧与贪婪指数）更清楚掌握市场情绪。</p><p>9、DeFiLlama（DeFi分析平台）</p><p>我是Alt L1的重度用户——DeFiLlama帮助我通过总锁定价值这一指标进行项目比较分析。</p><p>DeFillama对我来说最大用处就是它能帮我找到每个生态系统内隐藏的宝石。</p><p>可以这样操作：</p><p>我是一个AVAX用户，最近没有关注这个生态系统的发展趋势。</p><p>我可以按以下条件查询：</p><p>• 总锁定价值至少100万美元</p><p>• 每周价值高达七位整数（显示一些线索）</p><p>EverRise也在其中。我会把它加入我的研究列表。（声明：这并非是让你购买此工具的背书）</p><p>10、DexScreener（实时价格图表工具）</p><p>我使用此工具观察我的账号列表，生成价格预警。</p><p>设想一个大事件。例如一个有影响力的巨鲸在推广某个项目，或者发生了攻击。</p><p>使用此工具你可以查看实时的买入和卖出动作。</p><p>每天都在用的10个必备的加密货币研究工具</p><p>￼ 启元宇宙 15小时前 栏目：DeFi 992</p><p>10个必备的加密货币研究工具</p><p>DeFi网站TheDeFiEdge创始人Edgy近日分享了他每天都在用的10个必备的加密货币研究工具。区块链网打假记者刘芳华独家报道：</p><p>1、Uniwhale（分析仪表盘）</p><p>我使用这个工具跟踪不同L1间的跨链桥活动。</p><p>我可以使用UniWhale观察储存和提取情况，掌握L1整体动向。</p><p>2、Dune Analytics （分析仪表盘）</p><p>Dune可以让你将不同指标可视化，生成明了易懂的图表。</p><p>从DeFi的应用到具体某个dapp仪表盘，一切都变得可视化。</p><p>ENS数据可视化</p><p>3、Crypto Panic （新闻聚合器）</p><p>这是我获知多数加密货币新闻的途径（除Twitter之外）。</p><p>可将它看做加密货币新闻界的Reddit。</p><p>新闻量大得你看不过来。</p><p>我按“重要”标签分类，从海量消息里筛选出有价值的内容。</p><p>4、Santiment （加密情报平台）</p><p>多数人用来查看价格图表。</p><p>Sanbase可以让你将价格和其他行为结合在一起。</p><p>你可以查看价格+发展活动+社会趋势（以及其他指标）</p><p>5、Nansen（链上分析平台）</p><p>其最佳功能是“Smart Money”（聪明的钱）——可跟踪那些经验证表现最佳的巨鲸账号。（例如基金和先行者）</p><p>使用它查看聪明的钱都在买入什么，你便可以在YouTubers（油管主播）开始制作视频之前早一步买入。</p><p>6、Messari（市场情报平台）</p><p>我更愿意把Messari比作瑞士军刀。</p><p>• 图表</p><p>• 价格数据</p><p>• 研究报告</p><p>• 社区筛分</p><p>分析加密货币最具价值的指标，这些功能每月付费25美元即可获取。</p><p>7、Token Terminal（加密货币金融数据）</p><p>人们受够了通货膨胀的代币和纸币。</p><p>他们想要看到真正的收益情况。</p><p>使用Token Terminal查看哪些区块链和dapp正在赚钱。</p><p>8、Fear &amp; Greed Index（情绪指标工具）</p><p>你阅读Twitter内容是比较难探究真相的。</p><p>万一你关注的是一群永远乐观的牛人，该怎么办？</p><p>我查看fear &amp; greed index（恐惧与贪婪指数）更清楚掌握市场情绪。</p><p>9、DeFiLlama（DeFi分析平台）</p><p>我是Alt L1的重度用户——DeFiLlama帮助我通过总锁定价值这一指标进行项目比较分析。</p><p>DeFillama对我来说最大用处就是它能帮我找到每个生态系统内隐藏的宝石。</p><p>可以这样操作：</p><p>我是一个AVAX用户，最近没有关注这个生态系统的发展趋势。</p><p>我可以按以下条件查询：</p><p>• 总锁定价值至少100万美元</p><p>• 每周价值高达七位整数（显示一些线索）</p><p>EverRise也在其中。我会把它加入我的研究列表。（声明：这并非是让你购买此工具的背书）</p><p>10、DexScreener（实时价格图表工具）</p><p>我使用此工具观察我的账号列表，生成价格预警。</p><p>设想一个大事件。例如一个有影响力的巨鲸在推广某个项目，或者发生了攻击。</p><p>使用此工具你可以查看实时的买入和卖出动作。</p><p>这还不是完整的列表。</p><p>现在我能想到的还有Glassnode、Trading View、Coingecko、IntotheBlock，等等，这些我也经常使用。</p><p>我列举的这些工具是我日常使用的“必备工具”，是我研究过程不可缺少的一部分。</p><p>我不想让太多的选项迷了你的眼。想想你的作业流程。我首先设计我的研究过程，然后借助工具的力量。</p><p>你可以说：</p><p>1）我是Alt L1用户</p><p>2）我是个视觉型学习者</p><p>3）我想寻找规模大于100万美元的热门代币项目。</p><p>这些工具辅助我的投资和交易风格。</p><p>我现在不是在和你做交易。</p><p>我仍然是ETH/稳定币用户，还参与了一些不同的小额项目。</p><p>我每天都在进行研究，为了知晓最新趋势，预判将来发展态势。</p><p>不要迫于压力注册使用所有工具。</p><p>本文的写作完全透明。没人给我钱让我写这些。也没人给我免费的账号。你如果因本文注册使用任何一个工具，也并不会对我的养娃经费有所助益。所以再次声明，不要“社交控”跟风购买工具，不要把工具当做灵丹妙药。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[有人撸空投几年赚千万，有人撸空投赔百万]]></title>
            <link>https://paragraph.com/@dehop/BKRPPfExxKJvr7HmpyPK</link>
            <guid>BKRPPfExxKJvr7HmpyPK</guid>
            <pubDate>Tue, 24 Jan 2023 06:43:00 GMT</pubDate>
            <description><![CDATA[千呼万唤的DYDX终于在昨天上所了。随着DYDX的价格明朗，大家也能够计算出这次DYDX项目方的空投力度了。总体来说还是相当给力的。 昨天DYDX刚上线的时候，价格一度达到了30U。后面因为抛盘压力较大，价格不断下滑，如今DYDX基本稳定在了10多U的位置。 这次因为DYDX空投暴富的人不少，据说有人光靠空投就赚了上千万。微博大V矿工老马，之前在DYDX上建了800个账号，每个账号赚了1163.51个DYDX。粗略计算，光这次的空投老马就领到了93万多DYDX。按照今天DYDX的价格计算，这相当于是900多万美金了。 根据之前的空投规则，一个账号只要在DYDX上面进行过一次交易，不管交易金额多少，都可以领到310.75枚代币。如果你的交易量在500美元以上，那么就可以获得1163.51枚空投。交易量在5千美金以上的可以获得4349.63枚代币。交易量达到5万美金以上的则可以获得6413.91枚代币。如果你在DYDX上曾经有超过10万美金的交易量，那么就可以获得最高的9529.86枚代币。 DYDX的这次空投其实还是相当亲民的，官方并没有因为你的交易量太小就不给空投的机会，只要你象...]]></description>
            <content:encoded><![CDATA[<p>千呼万唤的DYDX终于在昨天上所了。随着DYDX的价格明朗，大家也能够计算出这次DYDX项目方的空投力度了。总体来说还是相当给力的。</p><p>昨天DYDX刚上线的时候，价格一度达到了30U。后面因为抛盘压力较大，价格不断下滑，如今DYDX基本稳定在了10多U的位置。</p><p>这次因为DYDX空投暴富的人不少，据说有人光靠空投就赚了上千万。微博大V矿工老马，之前在DYDX上建了800个账号，每个账号赚了1163.51个DYDX。粗略计算，光这次的空投老马就领到了93万多DYDX。按照今天DYDX的价格计算，这相当于是900多万美金了。</p><p>根据之前的空投规则，一个账号只要在DYDX上面进行过一次交易，不管交易金额多少，都可以领到310.75枚代币。如果你的交易量在500美元以上，那么就可以获得1163.51枚空投。交易量在5千美金以上的可以获得4349.63枚代币。交易量达到5万美金以上的则可以获得6413.91枚代币。如果你在DYDX上曾经有超过10万美金的交易量，那么就可以获得最高的9529.86枚代币。</p><p>DYDX的这次空投其实还是相当亲民的，官方并没有因为你的交易量太小就不给空投的机会，只要你象征性的完成一次交易，那么就可以领到3000多刀美金。</p><p>多学多看多做，加上运气，说不定就能暴富了</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[zkSync空投教程]]></title>
            <link>https://paragraph.com/@dehop/zksync</link>
            <guid>36J5g1aBYK3O2JdXgcvQ</guid>
            <pubDate>Tue, 24 Jan 2023 06:36:12 GMT</pubDate>
            <description><![CDATA[第三种选择可能是zkSync。空投的最佳选择可能是与测试网上的 zkSync dapp 进行交互。但是，在用户执行此操作之前，他们需要添加 RPC 配置。最简单的方法是在https://chainlist.org上配置与测试网的钱包连接。 与 zkSync 测试网及其 dApp 交互的大量示例已由匿名 Twitter 用户在以下线程中编译。更多选择 …第四个选项是零层。该项目是一个全链互操作性协议，可以实现具有低级通信原语的跨链应用程序。 一方面，有机会通过使用免费的测试网获得可能的空投。其中，可以测试 USDC 零层桥和 Liquid Swap 桥。 希望这篇文章对大家有一定的帮助。]]></description>
            <content:encoded><![CDATA[<p>第三种选择可能是zkSync。空投的最佳选择可能是与测试网上的 zkSync dapp 进行交互。但是，在用户执行此操作之前，他们需要添加 RPC 配置。最简单的方法是在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://link.zhihu.com/?target=https%3A//chainlist.org">https://chainlist.org</a>上配置与测试网的钱包连接。</p><p>与 zkSync 测试网及其 dApp 交互的大量示例已由匿名 Twitter 用户在以下线程中编译。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">更多选择 …</h3><p>第四个选项是零层。该项目是一个全链互操作性协议，可以实现具有低级通信原语的跨链应用程序。</p><p>一方面，有机会通过使用免费的测试网获得可能的空投。其中，可以测试 USDC 零层桥和 Liquid Swap 桥。</p><p>希望这篇文章对大家有一定的帮助。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[怎么才能快速成撸空投高手？]]></title>
            <link>https://paragraph.com/@dehop/587033m88jH1OAGEeFrC</link>
            <guid>587033m88jH1OAGEeFrC</guid>
            <pubDate>Tue, 24 Jan 2023 06:30:29 GMT</pubDate>
            <description><![CDATA[撸空投之前，你至少需要准备好以下工具： 1、一个区块链钱包，并存入一点主网代币（比如以太坊主网为ETH，币安智能链为BNB，等等）留作备用gas费。 2、一套海外常用社交账号，至少包括Twitter、Telegram、Discord账号。 3、在钱包里提前收录好多个常用主网。 关于钱包，我们建议您准备一个专门撸空投的钱包。因为空投项目质量不一，万一您的钱包进入了一些带有木马的链接，可能威胁到资产安全。所以，还是要和自己的其他数字资产分开管理。 存入主网代币，主要是空投项目一般会邀请您使用它的主要功能来促进链上日活，试用过程中，需要消耗少量的gas费用。SUI 可能胜过 Aptos与 Aptos 一起，SUI区块链是目前加密领域最受炒作的项目之一。就像 Aptos 一样，SUI 是由前 Meta 员工创立的，他们正在开发基于 Diem 的 MOVE 编程语言和权益证明的高度可扩展的第 1 层区块链。 虽然 SUI 已经在 8 月底关闭了对激励性测试网中新验证者的接受，但仍有可能进行空投，因为 SUI 应该有兴趣为网络带来流动性。]]></description>
            <content:encoded><![CDATA[<p>撸空投之前，你至少需要准备好以下工具：</p><p>1、一个区块链钱包，并存入一点主网代币（比如以太坊主网为ETH，币安智能链为BNB，等等）留作备用gas费。</p><p>2、一套海外常用社交账号，至少包括Twitter、Telegram、Discord账号。</p><p>3、在钱包里提前收录好多个常用主网。</p><p>关于钱包，我们建议您准备一个专门撸空投的钱包。因为空投项目质量不一，万一您的钱包进入了一些带有木马的链接，可能威胁到资产安全。所以，还是要和自己的其他数字资产分开管理。</p><p>存入主网代币，主要是空投项目一般会邀请您使用它的主要功能来促进链上日活，试用过程中，需要消耗少量的gas费用。</p><h2 id="h-sui-aptos" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">SUI 可能胜过 Aptos</h2><p>与 Aptos 一起，SUI区块链是目前加密领域最受炒作的项目之一。就像 Aptos 一样，SUI 是由前 Meta 员工创立的，他们正在开发基于 Diem 的 MOVE <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80&amp;search_source=Entity&amp;hybrid_search_source=Entity&amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2723170775%7D">编程语言</a>和权益证明的高度可扩展的第 1 层区块链。</p><p>虽然 SUI 已经在 8 月底关闭了对激励性测试网中新验证者的接受，但仍有可能进行空投，因为 SUI 应该有兴趣为网络带来流动性。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[Scroll测试网交互教程]]></title>
            <link>https://paragraph.com/@dehop/scroll</link>
            <guid>y3awun18V3uoAtTnCYwo</guid>
            <pubDate>Tue, 24 Jan 2023 06:20:27 GMT</pubDate>
            <description><![CDATA[Scroll 是一个基于 zk-rollup 技术的以太坊扩容网络，被称为最兼容 EVM 的 ZK-Rollup，旨在为用户提供近乎即时且具有成本效益的交易，同时保持以太坊网络提供的高安全性。 登录测试网：5567 添加完成后去领取测试币 测试币领取成功之后是在L1网络上 然后去跨链桥进行交互这里要先把钱包网络切换成L1网络钱包完成确认，等待几分钟，就可以在L2网络显示 完成之后进行代币兑换交互操作 兑换完成之后到POOL提供流动性，选择可用的余额进行流动性提供即可。这样就结束了。 关注我的mirro，不定期更新交互教程]]></description>
            <content:encoded><![CDATA[<p>Scroll 是一个基于 zk-rollup 技术的以太坊扩容网络，被称为最兼容 EVM 的 ZK-Rollup，<strong>旨在为用户提供近乎即时且具有成本效益的交易，同时保持以太坊网络提供的高安全性。</strong></p><p><strong>登录测试网：5567</strong></p><p>添加完成后去领取测试币 测试币领取成功之后是在L1网络上 然后去跨链桥进行交互这里要先把钱包网络切换成L1网络钱包完成确认，等待几分钟，就可以在L2网络显示 完成之后进行代币兑换交互操作 兑换完成之后到POOL提供流动性，选择可用的余额进行流动性提供即可。这样就结束了。 关注我的mirro，不定期更新交互教程</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[加密游民指南：如何在格鲁吉亚开设个人银行账户？]]></title>
            <link>https://paragraph.com/@dehop/ojySRMi9WIKiNq0GcLlr</link>
            <guid>ojySRMi9WIKiNq0GcLlr</guid>
            <pubDate>Sun, 15 Jan 2023 04:20:51 GMT</pubDate>
            <description><![CDATA[本篇是关于如何在格鲁吉亚开设个人银行账户的介绍（被小伙伴们催更最多的主题），属于 Guide for crypto nomads （写给加密游民的指南）系列，将在 k.mirror.xyz 不定期更新（偶尔空闲会根据反馈更新一下单篇文章本身），最终可能会为了阅读体验，汇总成电子书。 以下仅根据研究和实际经验写重点和结论，关于银行的介绍网上可以找到很多资料，此处不赘述。 格鲁吉亚介绍 台译：喬治亞；地跨欧亚两洲，文化深受欧洲影响，故也被认为是欧洲国家；曾经是苏联加盟共和国，1991年4月9日正式独立，1995年8月24日通过新宪法，国名定为 Georgia - 格鲁吉亚，曾为独联体成员。 友情提示： 格鲁吉亚首都第比利斯是数字游民比较喜爱的旅居地，但请不要完全参考网上一些旧的资料，误以为这里的物价很低，没钱也可以过的很爽；由于通胀，格鲁吉亚物价飞涨，本地货币拉里也一直在涨，对于游客来说是双重涨幅，比如很多短途旅行费用大约上升了40% -50%，博物馆票价也有涨 2.5 倍。 如果你和我一样不打算穷游，而是尽可能过的精致，深度体验每一个目的地，吃参考上海市中心物价，住参考上海中环物价...]]></description>
            <content:encoded><![CDATA[<p>本篇是关于如何在格鲁吉亚开设个人银行账户的介绍（被小伙伴们催更最多的主题），属于 Guide for crypto nomads （写给加密游民的指南）系列，将在 k.mirror.xyz 不定期更新（偶尔空闲会根据反馈更新一下单篇文章本身），最终可能会为了阅读体验，汇总成电子书。</p><p>以下仅根据研究和实际经验写重点和结论，关于银行的介绍网上可以找到很多资料，此处不赘述。</p><p>格鲁吉亚介绍 台译：喬治亞；地跨欧亚两洲，文化深受欧洲影响，故也被认为是欧洲国家；曾经是苏联加盟共和国，1991年4月9日正式独立，1995年8月24日通过新宪法，国名定为 Georgia - 格鲁吉亚，曾为独联体成员。</p><p>友情提示：</p><p>格鲁吉亚首都第比利斯是数字游民比较喜爱的旅居地，但请不要完全参考网上一些旧的资料，误以为这里的物价很低，没钱也可以过的很爽；由于通胀，格鲁吉亚物价飞涨，本地货币拉里也一直在涨，对于游客来说是双重涨幅，比如很多短途旅行费用大约上升了40% -50%，博物馆票价也有涨 2.5 倍。</p><p>如果你和我一样不打算穷游，而是尽可能过的精致，深度体验每一个目的地，吃参考上海市中心物价，住参考上海中环物价，按摩、美甲、旅游一类的消费请参考国内四五线旅游城市的均价，而且它还在涨……</p><p>关于第比利斯旅居，我写了一个 to do list，有需要的朋友可以配合之后更新的旅居指南使用：</p><p>加密游牧指南：住在第比利斯自由广场附近可以做什么？</p><p>点击右上角 duplicate 复制到你的 Notion 使用 点击右上角 duplicate 复制到你的 Notion 使用 我应该申请哪一家银行？ 如果你是一位需要用英文沟通的外国游客，那么我推荐你首选 Bank of Georgia 办理个人银行账户。</p><p>尽管网上有不少人推荐 TBC，但根据我在旅行中的实际体验，不论是服务质量，还是账户的权限，都是 Bank of Georgia 更胜一筹。</p><p>为什么不推荐 TBC ？ 非格鲁吉亚居民的账号限制 TBC 的借记卡分为居民和非居民两种，非居民能够申请的借记卡仅能在格鲁吉亚境内使用，并且不包括电子服务。 语言沟通问题 我们选择了自由广场附近的 TBC，相对而言是非常中心的位置，但柜员均不能讲英文，也没有让会英文的大堂经理协助，最终只是用谷歌翻译表示无法服务。 为什么推荐 Bank of Georgia ？ 靠谱和安全</p><p>成立于 1903 年，市占率第二，资产量第一，英国富时250 指数，JP Morgan 持股 3.11%。</p><p>网点多，服务质量好</p><p>根据目前数据（2022年8月），一共有272 家分行，933 台 ATM，几乎在格鲁吉亚大部分城市都能见到分行，办事比较方便。</p><p>语言沟通顺畅</p><p>为了写好体验，我和同伴分别在自由广场附近和比较远（但是比较大）的郊区分行卡申请开通账户，自由广场分行的柜员英文沟通无阻碍，郊区大的分行有部分柜员几乎不会英语，但是非常积极求助英语好的大堂经理辅助，办事很顺利，基本没有沟通问题。</p><p>申请流程 前期准备： 护照 格鲁吉亚电话号码 空出至少两个半天时间（网点排队时间长，每次需要耗费大约为1-2小时） 具体流程 第一天流程：</p><p>选择一家距离你近的 Bank of Georgia 分行，于工作时间前往，告知大堂经理你需要开设一个账户，取号等待； 等待期间，注意大屏幕上的号码，没有语音叫号，每次叮的一声响就需要看一眼是不是到自己了； 排到服务之后，告知柜员你需要办理一个新的银行账号，并需要借记卡，柜员会要求你提供护照； 填写柜员提供的各种表格，包括以下信息：公司，职位，收入，存款来源，预计存款数量，开通账号的目的，确认自己不是政治敏感人物或者被制裁等等； 填写完毕后，需要继续在柜员打印出来的条款上确认签字，并在此过程中向柜员出示手机收到的验证码，开通电子账户； 申请过程中，你的手机会收到电子银行的注册信息和密码，你可以下载 APP，登录并修改密码； 完成这些事项后，柜员会问你要不要先拿一张美国运通的 PLUS card，好处是消费有返利，如果你有在格鲁吉亚刷卡消费的需求，可以选择拿一张，它和你的主卡共享同一个账户，相关优惠可以在此查询（我觉得可以忽略不计）：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://bankofgeorgia.ge/en/offers-hub">https://bankofgeorgia.ge/en/offers-hub</a> 至此，第一天的部分全部完成，记得询问柜员下一次需要在什么时间来拿借记卡。</p><p>PLUS 卡长这样，不记名 PLUS 卡长这样，不记名 第二天流程：</p><p>来到前一天办理的分行，取号等待； 排到之后，递护照给柜员，告知你昨天已经开通了账户，来领取卡片； 等待柜员确认和取卡，需要直接存钱可以让柜员帮忙，大约5分钟就能办完； 目前能获取的实体卡（你可以在第一天问清楚可以选什么）只有 MasterCard，没有 VISA，有效期如下，为4年，VISA 是两年有效期：</p><p>开通账户之后我可以干什么？ 记得改掉你卡片的密码（登录 APP）； 日常的消费、转账、取现、换汇换现，在第比利斯也可以当公交卡使用； 如果你是一个正在四处旅居的 crypto trader，那么这个账户可以作为你平时日常消费和基本理财使用，比如使用 FTX 转账（银行在第一天会给你一张纸，写清楚了各种转账的信息）；格鲁吉亚对于 crypto 的政策宽容友好，资金来源填 crypto 完全没问题，暂时也不会有其他国家追税的问题，但转账数额大的话，银行会问你，可能需要证明和解释； 线上绑定付费用，比如用于节点运维、开发需要的服务付费，Web3 的花费用 Web3 赚的钱付，愉快内循环，没有冻卡风险。 维护费用 和新加坡和香港的银行比，便宜到忽略不计，普通账户每年不到100 RMB，具体情况参考以下链接： <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://bankofgeorgia.ge/en/retail/cards/visa-and-mc">https://bankofgeorgia.ge/en/retail/cards/visa-and-mc</a></p><p>其他 如果不怕麻烦，而且空闲时间多，可以尝试去申请 Basis Bank 的 discovery 卡，比较方便在境外取现。</p><p>信息如下： <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://bb.ge/en/personal/cards/debit-cards/travel-card-discovery-card/discovery">https://bb.ge/en/personal/cards/debit-cards/travel-card-discovery-card/discovery</a></p><p>具体流程：前往一家 Basis Bank 分行填表登记信息，等待电话联系你，如果办理成功，需要3个工作日制卡，最后你前往办理的分行取卡。</p><p>然而，根据一些欧美旅行者的反馈，这家银行工作人员的英语普遍也不好，所以有很大概率会办理失败和浪费时间。</p><p>题外话：</p><p>Basis Bank 已被来自中国新疆的华凌集团收购，在第比利斯用谷歌地图搜索 hualing，可以搜到他们正在第比利斯郊区建设的商场、酒店和公寓，还有一家名为 新疆味道 的餐厅，这是第比利斯唯一可以吃到新疆拉面的地方，这家店的对面还有一家叫 中国味道 的餐厅，地图上没有标记。</p><p>旅居第比利斯期间，我对这家集团产生了好奇，所以跑去遥远的郊区看了一眼，顺便吃了一碗久违的牛肉面（上一次还是6月初，上海刚刚解封的时候）。</p><p>15 GEL （折合人民币 37 块）的牛肉面，面条是从冷藏拿出来的 15 GEL （折合人民币 37 块）的牛肉面，面条是从冷藏拿出来的 如果非得总结，这里可以说是还没建起来的第比利斯国际城（对标迪拜国际城），和第比利斯市中心完全是两个世界，郊区风大，空气还行，公交人也不多。</p><p>周围还有一些做中格贸易的区域，但看上去都没人，安静得像是在逛 Tsukaltubo 的疗养院废墟（接下来也会在 mirror 写格鲁吉亚的废墟探险~）。</p><p>疫情改变了很多东西。在第比利斯到其他城市的路上，还有一些中国建筑公司正在建设；在格鲁吉亚，吃的比不上国内好，建筑工地上的人自然更辛苦，祝愿这些身在异国他乡的人们能过得好。</p><p>读者反馈区（不定期更新） @Butterbeerbeer 补充：注意格鲁吉亚的银行卡汇至美加澳港坡无法通过 FATF 审核，东欧 南美和高加索都没法通过 FATF；但是 hk 之类对资金来源审查严格，不如这些方便。（所以作为消费用卡为佳。）（2022年8月12日留言）</p><p>写在最后 写这个系列的文章纯属兴趣使然和方便大家，没有相关的付费群，有兴趣支持作者可以订阅和收藏 NFT。</p><p>如果你是我任何一篇 Mirror 文章 NFT 的藏家，你也可以通过推特私信问一个相关问题 —— 是的，包括免费的 NFT，但仅限文章发布一周内 mint 的部分 —— 避免问题堆积如山，债还不完；因私信长期处于堆积如山状态，无法保证回复速度，敬请见谅。</p><p>如何获得秒回？ 钞能力在此作用不大；人活着就是在认识世界和探索世界，如果你认为自己正在做非常特别的事，或是有很棒的东西想和我分享，欢迎来交个朋友，求同存异，互相触达更多的世界边角~</p><p>给长期支持者和订阅读者的小礼物： 一直都在 Mirror 单向输出，感到有点无聊了，从这篇起，临时尝试一下读者互动。</p><p>如果你提的问题和回答被选到未来文章的 Q&amp;A 部分，你会和本文的第一位藏家一起，获得一个相关主题的 NFT 作为纪念，比如和格鲁吉亚相关的文章和问题就对应格鲁吉亚主题的 NFT。</p><p>旅行期间，特地设计了一款 POAP，发给了和自己面基、远程开会的朋友做纪念 旅行期间，特地设计了一款 POAP，发给了和自己面基、远程开会的朋友做纪念 这些纯属作者本人的业余爱好，没事自己整点有纪念意义的东西玩儿，顺便标记一下可爱读者的地址，毕竟我也不知道 Web3 + 创作者经济未来还可以玩出什么新花样，但有意思的方式我应该都会试试看~ ^_^</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[加密游牧指南：塞尔维亚（贝尔格莱德）]]></title>
            <link>https://paragraph.com/@dehop/vkh1xGqajIik4rKWfo3p</link>
            <guid>vkh1xGqajIik4rKWfo3p</guid>
            <pubDate>Sun, 15 Jan 2023 04:19:39 GMT</pubDate>
            <description><![CDATA[概览 塞尔维亚共和国，简称塞尔维亚，位于欧洲东南部、巴尔干半岛中部内陆国，接壤国为黑山、波斯尼亚和黑塞哥维那、克罗地亚、匈牙利、罗马尼亚、保加利亚、北马其顿、科索沃。 地理上，山河交错，大部分河流通过多瑙河流入黑海，可以在首都贝尔格莱德看到多瑙河景观；气候上，冬天较为寒冷，建议选择夏末秋初前往。 当地语言: 塞尔维亚语 推荐点：博物馆（多元文化），中餐厅（数量很多），历史文化游，没有滤镜也好看的女性（如果你也能够看山还是山地欣赏美的存在）。 如果问这座城市能和 crypto 有什么关系 —— 它的气质，让我想起很久以前 Steemit 社区经常办的聚会，很喜欢选择这种有历史感、生活又平稳的城市，人们在这种城市相聚，能够更放松地互相认识。只可惜，在当下的 Web3 世界，即使是一些 DAO 的活动，也没再能重现当年 Steemit 社区聚会的那种轻松、多元和创新并存的氛围。 （我想自己大概是年纪大了，竟然开始怀念一些一去不复返的事物 :p 重要提示 如果你和我一样只是在此短暂停留，建议住在贝尔格莱德市区，地方不算大，但是生活机能好，也可以很快抵达博物馆和历史文化景点。 另外，如果你...]]></description>
            <content:encoded><![CDATA[<p>概览 塞尔维亚共和国，简称塞尔维亚，位于欧洲东南部、巴尔干半岛中部内陆国，接壤国为黑山、波斯尼亚和黑塞哥维那、克罗地亚、匈牙利、罗马尼亚、保加利亚、北马其顿、科索沃。</p><p>地理上，山河交错，大部分河流通过多瑙河流入黑海，可以在首都贝尔格莱德看到多瑙河景观；气候上，冬天较为寒冷，建议选择夏末秋初前往。</p><p>当地语言: 塞尔维亚语</p><p>推荐点：博物馆（多元文化），中餐厅（数量很多），历史文化游，没有滤镜也好看的女性（如果你也能够看山还是山地欣赏美的存在）。</p><p>如果问这座城市能和 crypto 有什么关系 —— 它的气质，让我想起很久以前 Steemit 社区经常办的聚会，很喜欢选择这种有历史感、生活又平稳的城市，人们在这种城市相聚，能够更放松地互相认识。只可惜，在当下的 Web3 世界，即使是一些 DAO 的活动，也没再能重现当年 Steemit 社区聚会的那种轻松、多元和创新并存的氛围。</p><p>（我想自己大概是年纪大了，竟然开始怀念一些一去不复返的事物 :p</p><p>重要提示 如果你和我一样只是在此短暂停留，建议住在贝尔格莱德市区，地方不算大，但是生活机能好，也可以很快抵达博物馆和历史文化景点。</p><p>另外，如果你希望找到中国人多的地方，那么可以参考推友 @skrnmy_ 告知我的情况：</p><p>「中国人主要都集中在新贝尔格莱德（萨瓦河西岸）的70号中国城，以及机场那边一个叫雷蒂娜的郊区里。」</p><p>交通 飞机： 最方便的选择，机场到市中心打车约25分钟。</p><p>大巴：从附近国家到塞尔维亚，可以选择大巴，比如从波黑萨拉热窝的东站可以买到8:00、9:45 到晚上的班次到贝尔格莱德，车程大概为7小时，不建议选过夜班次，特别是秋冬天，半夜出入境会非常冷，而且睡不好。</p><p>此外，注意：萨拉热窝东站的工作人员几乎不会英文，请再三和司机、工作人员以及其他乘客确认你前往的目的地，并不是准点停在票上显示停车位的车就是对的！（我的悲惨遭遇请见「随记」部分。）</p><p>火车：从波黑到塞尔维亚的火车目前已经没了，不清楚何时会再有。</p><p>市内交通：公共交通卡可以在报刊亭买到，在贝尔格莱德期间我没有搭乘公共交通，几乎都是打车，但是大部分出租车打表有问题，建议下载 Pink Taxi Beograd 这个 APP，然后用 APP 打车，绑卡或者现金支付都行。</p><p>住宿 贝尔格莱德的住宿比萨拉热窝贵一些，但基础设施也更好，可以找到大部分常见的品牌酒店，正常民宿价格大约在每晚 200-600 CNY （适合1-2人的方案）；如果希望住比较有历史感的地方，推荐莫斯科酒店（Hotel Moskva），有百年历史，比如爱因斯坦、希区柯克都下榻过这家酒店，价格大约在每晚 1000 CNY 起，位置和设施都相对好，只是空间略小，从这里步行到大部分博物馆和景点都不远，自助早餐和下午茶的品质都还不错。</p><p>消费 当地货币为塞尔维亚第纳尔（RSD），可以通过 ATM 直接取现，或者兑换点用美元或者欧元换，但大部分兑换点打烊很早，特别是周末；大部分店铺支持刷卡支付，但不排除遇到刷卡机故障（我就遇到了）；迫不得已情况下，可以找附近的中餐厅尝试微信或者支付宝打钱换现。</p><p>手机卡 报刊亭直接买（和波黑差不多），大概几十块钱可以用一周，部分卡需要激活比较烦，建议找个店员英语好的报刊亭买，让他们帮你激活。</p><p>吃 因持有中国护照可免签在塞尔维亚待30天，外加长签很容易拿，你可以在这里看到非常多的中国人和正宗的中餐厅。</p><p>玩 老实说，我在贝尔格莱德几乎没有娱乐，一是因为只在此待3天，二是因为第二天早上我发现自己无法直起身（在边境发生意外，拉伤了腰），好不容易才挣扎着拿笔记本上床，趴着办公，第三天晚上11点，我就要离开这里，于是我选择早上6点起床，痛并快乐着，一个一个逛完了以下几乎所有景点。</p><p>贝尔格莱德国家博物馆：力荐！我本次旅行中，唯一一个第一天看的地方，我在这里待了一下午，因此打乱了原来的计划。博物馆藏品非常丰富，特别是雕刻和画作，但是匆匆看容易发现不了大作，因为实在是太多了！展览的内容也很好，比如货币馆的大部分展示橱窗都配了文字解说，不会让人看得眼花缭乱；周日是开放日，不收门票，值得再来。</p><p>应该算是我在贝尔格莱德国家博物馆最喜欢的一个展品 应该算是我在贝尔格莱德国家博物馆最喜欢的一个展品 特斯拉博物馆：强制讲解，无法选择自由游览，英文讲解票价是800第纳尔，需要提前至少15分钟到场排队，英文只有下午场次（参考：最早是15点，请以实际为准），场地很小，主要是看了一场电影和示范，对小朋友比较友好，推荐亲子游。</p><p>首先，看一部特斯拉的纪录片 首先，看一部特斯拉的纪录片 贝尔格莱德要塞：东西不是很多，多瑙河交汇处，风景还行，也有小朋友的娱乐设施，纪念品店不是很行。</p><p>圣萨瓦教堂：最大的东正教教堂，内部非常崭新和现代，但因其空间大且壁画精致，身处其中还是很震撼。</p><p>中国驻前南斯拉夫大使馆遗址：准确而言，已经没有遗址了，现在是中国文化中心，但是主体是一家酒店，距离市中心有一点距离（市中心和机场之间，打车 30 - 40 CNY），对当年的事件有立一个纪念碑。</p><p>「缅怀烈士，珍爱和平」 「缅怀烈士，珍爱和平」 没来得及去的地方：</p><p>铁托陵墓</p><p>南斯拉夫历史博物馆</p><p>随记 原计划中 15 天的巴尔干独行之旅，塞尔维亚占了一周，除了首都贝尔格莱德，还有西北部边境号称是吸血鬼发源地的小镇。但由于行程变更和身体原因，我最终放弃了吸血鬼小镇，在贝尔格莱德待了3天，其中一天我因为腰伤趴着，动弹不得。</p><p>「抱歉，这里没有交通查询网站」 10月7日，因为无法再网络购票，我不得不空出半天，前往距离市区非常遥远的萨拉热窝东站现场购票。在波黑，你很难找到一个网站或是 APP 能查询所有交通信息，包括最常用的谷歌地图也是连一丁点公共交通信息都没。</p><p>原本我是一个相当死宅的人，大部分时候靠网络搜索和整合信息，提前做好准备，但在波黑期间，这个习惯被彻底打破，我不得不积极主动和人交流，通过他们习以为常的口耳相传模式获取准确的信息。</p><p>这一天，我约莫问了20多个人（包括民宿的房东），才搞明白从哪搭乘前往萨拉热窝东站，比较走运，车站有一个英文特别好的萨拉热窝女孩，恰好也要坐同一趟车。</p><p>好不容易找到的车站 好不容易找到的车站 「你们有任何网站或者 APP 查询交通信息么？」</p><p>「没有，全靠自己记。我哥哥以前创业开发过一款给游客使用的 APP，但是失败了，根本没人用。」</p><p>女孩和我聊了很多，我将这些情况发给国内 Web2 互联网创业的朋友：你看，人多还是有优势。</p><p>萨拉热窝东站 萨拉热窝东站 等抵达萨拉热窝东站，天色已晚，车站几乎没人，只有一个售票员老阿姨接过钱，面无表情地扔给我找零的钱和车票，一句话也没说。不过也可以理解，我要去的是塞尔维亚，而波黑的塞族曾在上个世纪90年代发动过内战和种族大屠杀，至今仍有许多人没能找到自己失散的亲人，每一年他们都试图前往新发现的集体墓穴中寻找，历史留下的心理创伤让人无法礼貌对待对前往世仇国家的游客。</p><p>不论如何，我即将离开萨拉热窝，去一个新的地方，第二天的晚上，我就可以躺在莫斯科酒店的房间里，好好休息了。那一刻，我这么想着，全然没想过，10月9日的晚上，我并没有抵达塞尔维亚。</p><p>第二天早上，我搭乘房东帮忙喊的出租车，早早抵达了萨拉热窝东站。</p><p>为了确保行程顺利，我拿着票向工作人员和其他乘客确认：6号停车位，发车时间：8点，目的地：贝尔格莱德。 7:55，一辆车停在了6号停车位，没有标记目的地，我又拿着票，用谷歌地图标记 Belgrade，和司机确认了一遍，司机一直点头，检票，然后将我的行李放上了车 —— 如果有行李，需要多收 1KM。</p><p>于是，我想，这样应该就没什么问题了，我上了车，开始睡觉。</p><p>在雾蒙蒙的早晨出发 在雾蒙蒙的早晨出发 我是谁？我在哪？我又要去哪？ 一路上，我迷迷糊糊醒过几次，眼前都是很美的景色，车在环山绕，雾在山间飘，让我想起去年爬山时见到的风景，我拿起手机拍照，然后继续睡觉。</p><p>中途停靠的小车站 中途停靠的小车站 车在几个小站稍作停留，放下去一些人，又上了一些人，直到司机最终赶我下车，我才发现自己快要到波黑和黑山的边境，以及他只是因为完全听不懂英语，所以上车时并没有仔细看我展示的地图。</p><p>我哭笑不得，只好将沉重无比的背包放在箱子上，思考接下来该怎么办。抵达新加坡之前，我像是一只背着家奔波的蜗牛，双肩包里有一台带 GPU 的游戏本、一台 MAC、一台 ThinkPad、两个机械硬盘、一个蓝牙键盘、三只手机、砖头一样大的充电宝，箱子里装着舍不得扔的一些绝版书，我犹豫到底要不要带着它们去找车。</p><p>「要不你拼个旅行团去黑山算了！」一直在好心回复我问题的塞尔维亚推友建议说。</p><p>如果继续去黑山，我应该无法再赶上11号晚上从贝尔格莱德去新加坡的航班，往后的行程也会陷入不可控的范围。如果我拖着箱子和背包去找车，我的身体可能会在这段路上遭到不可避免的损伤。我决定还是选择后者，因为曾经我用斩钉截铁的语气对 CP 说，不论如何，12号我一定会到。</p><p>我环顾四周，找到一个面相不错的老爷爷，试图问他附近有没有去贝尔格莱德的车。虽然他不会英语，但还是很好心地打了一个朋友的电话，我们透过他朋友交流，最终他帮我拎了一小段路的箱子——真的感谢！我终于在附近的车站买到了去贝尔格莱德的车，尽管是晚上10点左右才出发。</p><p>车站的WiFi 神速，我在那里办公了四五个小时，期间有两位刚到迪拜和波哥大的builder 朋友和我分享他们的经历，获知我的定位后，他们都表示：那是哪？</p><p>既然来都来了。我心想，还是出去转转吧！ 虽然车站位置偏远，但设施很完善，行李寄存处都是带锁的柜子，看上去比较安全。我花了3 KM 寄存行李，决定先去附近的博物馆看看。</p><p>波黑的全称是波斯尼亚和黑塞哥维那，由两个历史上的地名组成，这里的博物馆叫黑塞哥维那博物馆，票价 5KM，共4层，但给我的印象不太好：一，展示的物品质量不算好，而且非常潦草地摆在底上，没有任何解释说明；二，民族文化部分，虽然展品不少，但一直有十几只大苍蝇你追我赶地撞上展览玻璃，对于参观者来说宛如噩梦；三，最好的顶楼，虽然文字内容详尽，但没有明确的参观顺序，看的很是头疼。 随意摆放的展品 随意摆放的展品 离开博物馆，我在附近的古城和公园转了转，又在回车站的路上发现了两家挂着红色招牌的「中国商店」，里面卖的东西大同小异，都是五花八门的义乌小商品，地方虽小，客人很多，老板一直在结账收钱。</p><p>我和其中一家店的老板娘聊天，发现他们都是从浙江丽水过来做生意，小孩放老家读书，父母和兄弟姐妹帮忙带，每年合适的时候回去一次，通常是从迪拜转机回国，如此过了十多年。</p><p>「这里有什么餐厅比较推荐吗？」我问。</p><p>老板娘一愣，「我们都是自己做饭。」</p><p>我看了一圈附近的餐厅，确实没什么好吃的东西。</p><p>最终，我从附近的超市买了炒饭和几道菜，回到车站吃完，这大概也是出来之后吃过最便宜的晚餐了，只是肉还是和波黑的其他餐厅一样偏咸。</p><p>22点上车前，我找了十几个乘客一一确认：这趟车确实是去贝尔格莱德的，这才放心找到座位，躺下睡觉。</p><p>第二天半夜三点，我终于离开了波黑，进入了塞尔维亚。站在冷风中排队敲入境章，我发现只有我一个人穿着轻薄的风衣，其他人都裹着羽绒服，但他们抖得比我更狠。 早上8点，四肢僵硬的我在贝尔格莱德的车站下车，搭乘出租车前往莫斯科酒店，向几位询问的朋友报平安，匆匆吃过早餐，回房一觉睡到了中午。</p><p>这天下午，我去了附近的贝尔格莱德博物馆，如你在前面的部分所读到的，这是一个值得一来再来的地方，尽管因为它的存在，我下午的其他计划全都泡汤了。</p><p>博物馆货币区，南斯拉夫曾经最严重的通货膨胀 博物馆货币区，南斯拉夫曾经最严重的通货膨胀</p><p>找遍市中心的古董店，买到了博物馆同款 5000 亿面值的纸币 找遍市中心的古董店，买到了博物馆同款 5000 亿面值的纸币 计划「腰」折：我发现我直不起身了！ 这一天晚上，我睡得很香，没有梦见在这家酒店下榻过的任何一个人。</p><p>前一天结束前，我对第二天做了详尽的规划：首先，我要去看看中国驻前南斯拉夫大使馆遗址，然后在回来的路上，去贝尔格莱德要塞看看，接着是特斯拉博物馆，最后，下午继续开会……这一切，在我睁开眼，企图起身的那一刻，全都化作了梦幻泡影，因为我发现——我根本无法直起身！</p><p>只要我稍微一用力，就会有一阵剧痛从左边的腰部辐射开来，遍及整张背。</p><p>于是，从早上 5 点开始，我像一台突然坏掉的机器一样瘫在床上，只能用手机看新闻和电子书；8 点，我挣扎着将笔记本电脑移到床上，开始趴着打字；临近 10 点，我终于成功站了起来，慢吞吞下楼吃饭。</p><p>「我终于理解了你们腰痛是怎样的感受。」我发信息给某位腰痛多年的朋友，对方很好心地回了我一堆如何护理的方式，接下来整个下午，除了开会，我都是将记忆枕垫在腰下，趴在床上休息。</p><p>原本在推特私信推荐贝尔格莱德景点和美食的朋友，收到了我的求助信息：请教一下，哪里可以买到靠谱的膏药？（午餐时间我挣扎着去了附近的药店，结果一家都没。）</p><p>总之，这一天，我已经完全放弃了出门的念头，决定就此躺到去机场前。</p><p>「我以前就住在你对面，超近的，去看看吧！」 从萨拉热窝开始，我这趟旅途就完全没按照计划走。想在贝尔格莱德躺平的计划，自然也注定失败。</p><p>最后一天早上 6 点，我醒过来，腰还在隐隐作痛，只是已经没有昨天那么夸张。 我直起身，洗漱，下楼吃早餐，餐厅只有我一个客人。</p><p>算了，还是去「那里」看看吧！</p><p>「那里」指的是中国驻前南斯拉夫领事馆遗址，位于市中心和机场之间，打车过去20分钟不到，只是到了目的地，发现除了两座纪念碑，已看不出历史的痕迹：原来的位置被一座崭新的中国文化中心取代，建筑物内部还有一家看上去还可以的酒店；只是纪念碑前，还有看上去刚刚送达不久的鲜花，人们并没有忘记历史。</p><p>我回到酒店拿行李退房，手机屏幕亮起，显示收到曾经斯拉夫文学老师的信息，我们已有多年没有联系，她看到我的定位，兴冲冲发给我贝尔格莱德大学的位置，说她年轻的时候在这里访学过，就住在我对面。</p><p>「超近的，去看看吧！」她说。</p><p>我想了想，寄存了行李，没几分钟就走到了贝尔格莱德大学。</p><p>午间的校园里，一群学生捧着饭在树下狼吞虎咽，看着他们朝气满满的样子，我摸了摸自己的腰：分明还是2开头的年纪，为什么此时此刻的我却像个五六十岁的人，生怕出什么意外？算了，就当腰疼不存在，走完剩下的点吧！</p><p>我翻出 Notion 的列表，发现只有铁托陵墓要到第二天才开放。这一天剩下的时间里，我走完了原计划中的大部分景点，包括需要卡点排队等入场的特斯拉博物馆。</p><p>至于腰疼，实际上无法当它不存在。几乎每走一步，我左侧的腰就疼一下，就像是指尖落到琴键上，平地移动是轻轻抚过，斜坡上下是曲至高潮的猛烈敲击。结合景点间路线的地势，我差不多体验了一回腰疼版的《冬风》。</p><p>「对不起，刷卡机故障」 逛完要去的景点，距离登机时间还有5小时不到，考虑接下来又有近20小时的转机航程，我打算找一家店推拿，至少躺下1-2小时。然而，很不幸，贝尔格莱德市区的推拿店生意兴隆，竟没有一家没满客。</p><p>我找到了一家位于居民楼里、店员完全不会讲英语的美容店。好歹能躺一个多小时，也算是休息，省得到新加坡，整个人都废了。</p><p>现金差不多花完了，我用谷歌翻译解决了沟通问题，确认这家店可以刷卡，安心躺下。</p><p>一个半小时过去，店员告知我：「对不起，刷卡机故障，只能付现金。」</p><p>「我需要把我的包放在这吗？」我问。</p><p>店员摇摇头：「没这个必要。」</p><p>此时已是接近晚上8点，我跑出门，去了1公里以内的3家兑换点，发现它们全都打烊了，而银行的 ATM 也无法用随身带的格鲁吉亚银行卡取现（至今我也没明白这是怎么一回事）。再胡乱跑下去，我就白躺了，但也不想一走了之，留下中国人消费不给钱的坏印象，我想起附近有一家中餐厅，决定去那试试。</p><p>最终，我用微信转账，和中餐厅的老板娘换了足够的钱，回去店里付了钱。</p><p>本次旅途中见到最出戏的东西，可能只有 DAppChaser 的老读者才会懂…… 本次旅途中见到最出戏的东西，可能只有 DAppChaser 的老读者才会懂…… 我不记得我是怎么回莫斯科酒店拿行李，又是怎么打车去贝尔格莱德特斯拉机场，飞往迪拜，再转机到新加坡，一路上我都在和睡意做斗争，回头看，我在路上迷迷糊糊看完了特斯拉的自传，还回复了几位在 DevCon 的朋友的信息，我甚至不记得我是怎么回复了那些文字。</p><p>贝尔格莱德国际机场，以特斯拉命名。 贝尔格莱德国际机场，以特斯拉命名。 直到抵达新加坡的第二天晚上，我才稍微放松了一些，疲倦感吞没我，我躺在床上，感觉自己在不断往下沉。</p><p>「我终于放松了下来，好累啊……」</p><p>「你怎么现在才放松？」</p><p>「我也不知道……」</p><p>（关灯。）</p><p>（腰上换新的膏药。凉的。）</p><p>梦见回到了巴黎，不过是波德莱尔笔下的巴黎，一具具浮尸被人从塞纳河捞起，摆在路边，阴天，我们在巷间游荡，一直往荒野去，没有道路，没有草坪，没有一株蒺藜菜，也没有一棵荨麻草，每张开口吐出一个音，便有一阵狂风呼啸着将它卷到更深的黑暗中去。一团积雨云。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[随记 20230115]]></title>
            <link>https://paragraph.com/@dehop/20230115</link>
            <guid>B51TlpTc4JAHuP3ZaIHg</guid>
            <pubDate>Sun, 15 Jan 2023 04:17:27 GMT</pubDate>
            <description><![CDATA[心情复杂，仅记录个人想法，主要写给自己看，难以准确表述想表达的意思，只能不求对错。当下要做的事太多，时间不等人。 1- 历史上基于意识形态冲突的战争，无一不伴随着惨烈的屠杀，若非如此，难以从根本上改变什么；根本上的改变很难，改变之后有喘息和重建，对见证的两三代人也有钝刀子割肉的精神代价和反噬，具体请参考前苏联解体后的俄裔文学。 2- 革命共识是对人类依恋机制的挑战，代价高昂，仅出于维护依恋机制的短期爆发，难以直接转化为长期的革命，但温和的反抗有它本身的意义；农耕文明是人类依恋机制最稳固的土壤，而游牧文明是其一个反面。 3- 所有违反人类依恋机制的行为，都难以长久，包括一些看似充满希望的新兴社会制度理念，「人类向来都会在热情冷却后一脚踢开束缚自己的意识形态和崇高理想，重回爱人和孩子身边」，这也是为什么清算第一个勇士的事件总在反复发生，即使有幸创造了改变，也很容易在不远的将来重返原点。我们在循环往复中期待破局。 4- 致敬第一个勇士和冲锋陷阵的「焰火青年」，不论他们是谁，不论显得多么不理性，不论换一个场景，同样的勇气会不会又变作另一种截然相反的模样。哪怕是一些理中客口中所谓的成不了大...]]></description>
            <content:encoded><![CDATA[<p>心情复杂，仅记录个人想法，主要写给自己看，难以准确表述想表达的意思，只能不求对错。当下要做的事太多，时间不等人。</p><p>1- 历史上基于意识形态冲突的战争，无一不伴随着惨烈的屠杀，若非如此，难以从根本上改变什么；根本上的改变很难，改变之后有喘息和重建，对见证的两三代人也有钝刀子割肉的精神代价和反噬，具体请参考前苏联解体后的俄裔文学。</p><p>2- 革命共识是对人类依恋机制的挑战，代价高昂，仅出于维护依恋机制的短期爆发，难以直接转化为长期的革命，但温和的反抗有它本身的意义；农耕文明是人类依恋机制最稳固的土壤，而游牧文明是其一个反面。</p><p>3- 所有违反人类依恋机制的行为，都难以长久，包括一些看似充满希望的新兴社会制度理念，「人类向来都会在热情冷却后一脚踢开束缚自己的意识形态和崇高理想，重回爱人和孩子身边」，这也是为什么清算第一个勇士的事件总在反复发生，即使有幸创造了改变，也很容易在不远的将来重返原点。我们在循环往复中期待破局。</p><p>4- 致敬第一个勇士和冲锋陷阵的「焰火青年」，不论他们是谁，不论显得多么不理性，不论换一个场景，同样的勇气会不会又变作另一种截然相反的模样。哪怕是一些理中客口中所谓的成不了大事的弱者，在一个长期失语的环境中，不再沉默就是勇气，他们比我们勇敢，值得尊重。为这些朋友流泪，希望大家保护好自己。</p><p>人活着需要自我叙事作为支撑，而人类社会本身就是一个无数谎言堆砌而成的 scam，制定规则、维持群体相对平衡地生存下去是第一要务，打破旧叙事，则需要另一个更美好的新叙事继续维系平衡。</p><p>关于这一点，PKD 已经在《倒数第二个真相》中写尽，因为世上永远存在倒数第一个真相 —— 那个蚁群永远无法知道的真相。尽管有人明白，他们还是愿意勇敢涉险，追寻一个概率极低的破局。</p><p>我不知这些喊口号和跟着喊口号的人未来会遭遇什么，也不知他们是否清楚有些代价很可能要几代人背负，历史大势之前，理性讲道理或是螳臂当车。换位思考，除了手里的笔，我一无是处，只剩揪心和无力。</p><p>扪心自问，因上上辈背负过代价，从小到大，感受着代价压在几代人身上的沉重，如今已活在责任纵横交错的年纪里，做任何决定都要瞻前顾后，很难做到他们那般果断和勇敢。</p><p>感谢勇敢者。</p><p>5- 「资本」是调用资源的符号和工具，而长期的革命需要持续不断地调用大量资源，其基底是足够稳固、长久、广泛、有力和合适的利益绑定，这也是 BTC 的伟大之处，它的设计从未违背人性，站在依恋机制的肩膀上更易长久，润物细无声，而误判人性的设计终将走向败局，比如大部分 Web3 项目的治理代币 —— 当然，或许其中的大多数也没想追求这种长远的成功，阶段性胜利已不易。</p><p>6- 以太坊生态中有太多空想社会主义的复现，是在试图「构建人类社会应该有的样子」，但希望可贵 —— 我们是否可以跳开过往的束缚，不再纠结于守护某一片土地，在加密游牧状态之中，重新建立一个新世界？</p><p>7- Crypto 是这个时代调用资源的必要辅助，脑机接口是改变根本的关键，尽管目前还存在很多黑箱，但已在抑制癫痫和听觉脑干植入方面达成的成功不容忽视；我们或将在此生的末端，抹去自己生而为人的弱点，迎来一个《和谐》中的「美好」世界 —— 虽然这种「美好」很残酷和可悲。</p><p>8- 人类将在近未来迎来更加醉生梦死的阶段，练就强大的身心胜过一切，否则无法逃过近未来的双重奴役 —— 现实奴役和数字奴役。</p><p>即使不是革命，只是独自一人扛过一份责任到无人之地，个中滋味，也唯有自知。</p><p>如无强大的意志力，一个试图坚持到底的人，身心只会在看似没有尽头的折磨中逐步被摧毁，只因世间多的是锦上添花，却鲜有雪中送炭，更多是火上浇油。</p><p>革命的核心是准备充分过后的纵身一跃，外围是情绪，催化剂是时运，大部分人没思考清楚代价，只是来「帮帮忙」，珍视有勇气「帮帮忙」的人，但避免对此类行为投射过多期待。</p><p>最难忘的案例，还是以前因工作意外接触到的从言论自由勇士到狱中为己背弃所有队友的人；人非圣贤，从勇士到小人，一线之间，避免对自己和队友期待过多；但变成小人，不会抹去他曾经勇敢过的事实。</p><p>9- 永远给人性留足摇摆空间是人生必备的风控；勿要求人人都是完人，勿低估信仰坍塌的后遗症，勿一叶障目。</p><p>还是那句话：</p><p>世上没有绝对的善与恶，在极端情况下，善与恶会以你意想不到的高频率互相转换。「当人性、道德、价值观，这些似乎有着『普世』标准的词汇进入黑暗的宇宙，一切或许就将崩塌，然后重组」。</p><p>10- 基于近未来的趋势，最终解决问题的革命者未必是一个过早背弃依恋机制、事事都能摒弃情绪去对待的人，但一定是能忍辱负重的人；身在乱纪元，多珍惜身边稀缺的美好 —— 没有庄颜和小家，就没有最合适的执剑人罗辑。</p><p>11- 万事皆有代价，全面放开也有全面放开的代价，有余力时，多关心家中老幼，做好风控吧。</p><p>写到这，Dark Forest 乌克兰公会 Orden GG 的组织者 Yanik 跑来和我聊天，值得尊敬的对手，曾经相隔万里，以为永远会停留于线上的对战，今年上半年，他奔赴了战争，我被关在上海，现实的困局让人无暇再顾及喜爱的事物，但现在我们却被迫流浪到了同一个国家， 同时发现，热闹散场，好像也只剩我们了。</p><p>「你刚刚描述了整个世界，乌克兰也正变得越来越像叙利亚。」</p><p>去年的这个时候，他还在乌克兰晒新买的车，我抬头就可以看见自己最在意的人坐在电脑桌前 。如果穿越回去，告诉一年前的我们，眼前发生的这一切，他们不论如何也不会相信吧！</p><p>我惭愧于自己软弱，没去守护好原本的家，却发现曾扛着武器去守护家园的战士，也还是一样走向流浪。</p><p>我们都是历史长河中的蚂蚁。要怎样改变蚁群的方向？改变之后，又要往何处去？</p><p>记得《精通比特币》里科普原理的类比，也是用的切叶蚁。</p><p>12- KEEP BUIDL. MAKE LOVE.</p><p>13- 珍重。</p><p>「完」</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[以太坊白皮书中文翻译加详解]]></title>
            <link>https://paragraph.com/@dehop/GjyVWuCx8dbBKIpHaLqP</link>
            <guid>GjyVWuCx8dbBKIpHaLqP</guid>
            <pubDate>Sun, 15 Jan 2023 04:16:32 GMT</pubDate>
            <description><![CDATA[i. 如果被引用的UTXO不在状态S内，返回错误； ii. 如果签名与UTXO所有者不匹配，返回错误； 2、如果所有输入的UTXO面值总额比所有输出的面值总额小，返回错误； 3、返回状态S当所有UTXO输入被移除及所有UTXO输出被增加时，即当不存在以上错误时，返回 到新状态S&apos;，S&apos;里面就已经移除了所有输入的UTXO，增加了所有输出的UTXO。 第一步的前一半防止交易发送者花不存在的币，后一半防止交易发送者花其他人的币，第二步来确 保交易前后价值守恒。 为了将此应用与支付，协议如下： 假设Alice想发送11.7个币给Bob。 首先，Alice将寻找一套她已有的UTXO，总数至少要等于11.7个比特币。 事实上，Alice不可能刚好有11.7个比特币，如果假设她能得到的最小数量是6+4+2=12个币的 话。 然后她用6+4+2这三笔输入来创建一笔交易，这笔交易有两个输出。 第一笔输出是11.7个币，这11.7个币在Bob的地址上，因为Bob成了这11.7个币的所有者。第二笔 输出是剩下的0.3个币，“找零”，所有者还是Alice自己。 3.2【挖矿】 如果我...]]></description>
            <content:encoded><![CDATA[<p>i. 如果被引用的UTXO不在状态S内，返回错误；</p><p>ii. 如果签名与UTXO所有者不匹配，返回错误；</p><p>2、如果所有输入的UTXO面值总额比所有输出的面值总额小，返回错误；</p><p>3、返回状态S当所有UTXO输入被移除及所有UTXO输出被增加时，即当不存在以上错误时，返回</p><p>到新状态S&apos;，S&apos;里面就已经移除了所有输入的UTXO，增加了所有输出的UTXO。</p><p>第一步的前一半防止交易发送者花不存在的币，后一半防止交易发送者花其他人的币，第二步来确</p><p>保交易前后价值守恒。</p><p>为了将此应用与支付，协议如下：</p><p>假设Alice想发送11.7个币给Bob。</p><p>首先，Alice将寻找一套她已有的UTXO，总数至少要等于11.7个比特币。</p><p>事实上，Alice不可能刚好有11.7个比特币，如果假设她能得到的最小数量是6+4+2=12个币的</p><p>话。</p><p>然后她用6+4+2这三笔输入来创建一笔交易，这笔交易有两个输出。</p><p>第一笔输出是11.7个币，这11.7个币在Bob的地址上，因为Bob成了这11.7个币的所有者。第二笔</p><p>输出是剩下的0.3个币，“找零”，所有者还是Alice自己。</p><p>3.2【挖矿】</p><p>如果我们已经接入了一个可信的中心服务，这个系统可能部署起来就普普通通的，它可以轻松通过</p><p>编码来实现。</p><p>但是，比特币呢我们想把它建成一个去中心化的系统，因此我们需要结合状态转换系统和共识系统</p><p>这两者，以此确保每个人都认可里面的交易顺序。</p><p>比特币的去中心化共识处理，需要网络中的节点持续地尝试产生交易包，称之为“区块”，就是持</p><p>续地把交易打包到区块中。</p><p>网络大约每10分钟产生一个区块，每个区块包含一个时间戳、一个随机数、一个对上一区块的引</p><p>用 (例如hash值)，和一个自上一个区块开始已经发生的全部交易的清单。</p><p>如前面的图所示。</p><p>这就随时间创造了一个持续的不断延长的“区块链”，它不断的更新，以此代表最新的比特币账本</p><p>状态。</p><p>范例中所表达的，检查一个区块是否有效的算法，如下：</p><p>1、检查本区块引用的前一个区块是否已经存在且有效；</p><p>2、检查本区块的时间戳晚于前一个区块的时间戳，且早于未来2小时；</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[以太坊白皮书中文翻译加详解]]></title>
            <link>https://paragraph.com/@dehop/OjKDG8VrLovevmZU4Xme</link>
            <guid>OjKDG8VrLovevmZU4Xme</guid>
            <pubDate>Sun, 15 Jan 2023 04:15:31 GMT</pubDate>
            <description><![CDATA[APPLY(S,TX) > S&apos; or ERROR 这个函数的意思是，检查交易的格式是否正确（即有正确数值）、签名是否有效、随机数是否与发 送者账户的随机数匹配。如是，通过；如否，返回错误。 而在银行系统内定义如下： APPLY({ Alice: $50, Bob: $50 },"send $20 from Alice to Bob") = { Alice: $30, Bob: $70 } But: APPLY({ Alice: $50, Bob: $50 },"send $70 from Alice to Bob") = ERROR 当Alice和Bob的账上各有50美元时，如果Alice转给Bob20美元，返回结果为Alice账上有30美 元，Bob账上有70美元。 但当Alice和Bob的账上各有50美元时，如果Alice转给Bob70美元，返回结果为错误。 “状态”在比特币系统中是所有已经挖出来但是没有被花费的币的集合(技术上讲，是没有花费的 交易输出，unspent transaction outputs，简称UTXO) ，每一个币都有一个面值和一个所有者 (...]]></description>
            <content:encoded><![CDATA[<p>APPLY(S,TX) &gt; S&apos; or ERROR</p><p>这个函数的意思是，检查交易的格式是否正确（即有正确数值）、签名是否有效、随机数是否与发</p><p>送者账户的随机数匹配。如是，通过；如否，返回错误。</p><p>而在银行系统内定义如下：</p><p>APPLY({ Alice: $50, Bob: $50 },&quot;send $20 from Alice to Bob&quot;) = { Alice: $30, Bob: $70 }</p><p>But:</p><p>APPLY({ Alice: $50, Bob: $50 },&quot;send $70 from Alice to Bob&quot;) = ERROR</p><p>当Alice和Bob的账上各有50美元时，如果Alice转给Bob20美元，返回结果为Alice账上有30美</p><p>元，Bob账上有70美元。</p><p>但当Alice和Bob的账上各有50美元时，如果Alice转给Bob70美元，返回结果为错误。</p><p>“状态”在比特币系统中是所有已经挖出来但是没有被花费的币的集合(技术上讲，是没有花费的</p><p>交易输出，unspent transaction outputs，简称UTXO) ，每一个币都有一个面值和一个所有者</p><p>(由一个20字节的地址，实际上是一个密码学公钥所定义)[1]</p><p>注释[1]：一个聪明的读者可能会注意到事实上比特币地址是椭圆曲线公钥的哈希，而非公钥本</p><p>身。然而事实上从密码学术语角度把公钥哈希本身称为公钥是完全合理的。这是因为比特币密</p><p>码学可以被认为是一种定制化的数字签名算法，公钥由椭圆曲线公钥的哈希组成，签名由椭圆</p><p>曲线签名连接的椭圆曲线公钥组成，而验证算法包括用作为公钥提供的椭圆曲线公钥哈希来检</p><p>查椭圆曲线公钥，以及之后的用椭圆曲线公钥来验证椭圆曲线签名。</p><p>TX : Transaction（交易）</p><p>TXO : TX output（交易输出）：包含一个value值和一段脚本，该脚本规定了谁有权使用这笔交</p><p>易（比如需要私钥签名）。</p><p>UTXO： Unspent TXO（未花费的交易输出）：只有对“尚未使用过”的交易签名才能是有效签</p><p>名。</p><p>数字货币无法像金属货币（比如黄金）那样，仅靠物理转移即可转移所有权，即当A将一份黄金交</p><p>给B后，A必然不再拥有这份黄金。</p><p>而A将一份数字货币转给（签名）B后，A仍可以把同一笔交易转给C，因为A掌握私钥，这两份签</p><p>名均为有效签名，这就构成了“双花”。因此必须有一种机制来确保每笔交易只能使用一次，即只</p><p>有对“尚未使用过”的交易签名才能是有效签名。</p><p>UTXO，未花费的交易输出，是比特币交易生成及验证的一个核心概念。因为所有比特币交易构成</p><p>了一组链式结构，每笔交易都可以回溯到一个或多个交易的输出，而这些链条的源头实际上都是挖</p><p>矿奖励，末尾则是当前未花费的交易输出。所有的未花费的输出即整个比特币网络的UTXO。</p><p>因为比特币每一笔新的交易的输入必须是某笔交易未花费的输出，每一笔输入同时也需要上一笔输</p><p>出所对应的私钥进行签名，并且每个比特币的节点都会存储当前整个区块链上的UTXO，整个网络</p><p>上的节点通过UTXO及签名算法来验证新交易的合法性。这样，节点不需要追溯历史就可以验证新</p><p>交易的合法性。</p><p>一笔交易包含一个或多个输入，每笔输入包含一个对现有UTXO的引用和一个由关联所有者地址的</p><p>私钥产生的密码学签名。而一个或多个输出，每个输出都包含一个被加入到状态里面的新的</p><p>UTXO。</p><p>状态转换函数APPLY(S,TX) &gt; S&apos;可以就被大致定义为：</p><p>1、对每个交易输入：</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[以太坊白皮书中文翻译加详解]]></title>
            <link>https://paragraph.com/@dehop/ATeBpxfNBcYgI1ZklvZd</link>
            <guid>ATeBpxfNBcYgI1ZklvZd</guid>
            <pubDate>Sun, 15 Jan 2023 04:14:15 GMT</pubDate>
            <description><![CDATA[点来单方面控制大多数份额。 sybil attack 女巫攻击 在P2P网络中，因为节点可以随时加入或退出，那么为了维持网络稳定，同一份数据通常需要备 份到多个分布式节点上，这叫做数据冗余机制。 如果网络中存在一个恶意节点，它可以通过控制多个虚假身份，然后利用这些身份控制或影响 网络的其他正常节点。比如，原来需要备份到多个节点的数据被欺骗地备份到了同一个恶意节 点（该恶意节点伪装成多重身份），这就是女巫攻击。 女巫攻击出自Flora Rhea Schreiberie在1973年的小说《女巫》（ Sybil）改编的同名电影，讲 的是一个化名Sybil Dorsett的女人被诊断为分离性身份认同障碍，兼具16种人格，分裂出16种 身份。 botnet 僵尸网络 互联网上受到黑客集中控制的一群计算机，被黑客用来发起大规模的网络攻击，比如分布式拒 绝服务攻击（DDoS）、发送海量垃圾邮件等，同时黑客控制的这些计算机所保存的信息也都可 被黑客随意“取用”。 前面那句话就是说，在匿名环境下，比特币出现之前的那些电子现金协议，都是不可靠的，太 脆弱。 中本聪的创新之处在于，他的这个想法结合了两...]]></description>
            <content:encoded><![CDATA[<p>点来单方面控制大多数份额。</p><p>sybil attack 女巫攻击</p><p>在P2P网络中，因为节点可以随时加入或退出，那么为了维持网络稳定，同一份数据通常需要备</p><p>份到多个分布式节点上，这叫做数据冗余机制。</p><p>如果网络中存在一个恶意节点，它可以通过控制多个虚假身份，然后利用这些身份控制或影响</p><p>网络的其他正常节点。比如，原来需要备份到多个节点的数据被欺骗地备份到了同一个恶意节</p><p>点（该恶意节点伪装成多重身份），这就是女巫攻击。</p><p>女巫攻击出自Flora Rhea Schreiberie在1973年的小说《女巫》（</p><p>Sybil）改编的同名电影，讲</p><p>的是一个化名Sybil Dorsett的女人被诊断为分离性身份认同障碍，兼具16种人格，分裂出16种</p><p>身份。</p><p>botnet 僵尸网络</p><p>互联网上受到黑客集中控制的一群计算机，被黑客用来发起大规模的网络攻击，比如分布式拒</p><p>绝服务攻击（DDoS）、发送海量垃圾邮件等，同时黑客控制的这些计算机所保存的信息也都可</p><p>被黑客随意“取用”。</p><p>前面那句话就是说，在匿名环境下，比特币出现之前的那些电子现金协议，都是不可靠的，太</p><p>脆弱。</p><p>中本聪的创新之处在于，他的这个想法结合了两样东西，哪两样呢？1、一个非常简单的去中心化</p><p>共识协议，基于节点每10分钟打包交易进入区块来创造一条持续延长的区块链；2、节点通过获取</p><p>记账权来才能参与系统的工作量证明机制。</p><p>当拥有大量计算能力的节点确实有了相应的更大影响力，那么你要是想搞出比全网算力加起来还大</p><p>的计算能力，就比仿照100万个节点还难。</p><p>也就是说，当诚实节点控制了大系统多数，那这时你想搞出更大的算力来超所有诚实节点的算</p><p>力，或者说超越全网算力，这个太难了，比你搞100万个假节点还难。</p><p>也就是，中本聪的创新之处在于，当诚实节点控制了大多数，系统就会安全。因为系统基于两</p><p>点来运行，1、认可最长链；2、通过竞争工作量来获得记账权。（不明白仔细看比特币白皮书</p><p>翻译加详解）</p><p>尽管比特币区块链模型很简单粗糙，但是它已经被证明足够好，并且可能在未来五年成为全世界超</p><p>过200种数字货币和协议的基石。</p><p>3.1【比特币作为一种状态转换系统】</p><p>从技术角度讲，比特币账本可以被看作一个状态转换系统，这个系统里面有一种“状态”，包含</p><p>了：1、所有现存比特币的所有权状态，2、一个“状态转换函数”，这里面就包含了一种状态和</p><p>一笔交易，然后可以输出为一种新的状态作为结果。</p><p>上图，State是前一种状态，里面包含若干地址，中间是一个状态转换系统，也就是交易过程，</p><p>在原地址上进行增减和签名， State&apos;是交易后得到的原地址新状态，因为交易后账户余额有变</p><p>化了。</p><p>例如，在标准的银行系统内，状态就是一本资产负债表，一笔交易就意味着一个请求，即把X元钱</p><p>从A转给B，同时状态转换系统在A的账户减掉X元，在B的账户增加X元。</p><p>如果A的账户余额不足X元，状态转换函数就会返回错误提示。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
        <item>
            <title><![CDATA[以太坊白皮书中文翻译加详解]]></title>
            <link>https://paragraph.com/@dehop/fK1Wo1weaKZpv22ZCppq</link>
            <guid>fK1Wo1weaKZpv22ZCppq</guid>
            <pubDate>Sun, 15 Jan 2023 04:13:06 GMT</pubDate>
            <description><![CDATA[人不知道他所签署消息的具体内容，而签名消息也不可追踪，即当签名消息被公布后，签名人 无法知道这是他哪次签署的。 打个简单的比方，A要寄一封信，封在信封里，但是要让B在信上签名，又不要B看见内容。怎么 办？在信封里同时加入一张复写纸，于是B在信封上签名，信封里面的信上就同时印下了B的签 名，这时B既没有看见信的内容，同时又在信上签了名。 另一点，A寄了很多信，都有B的盲签名，当公布出其中一封信时，B也不知道这是他哪次签的 名。 [翻译注意：1、provided指的是e-cash protocols provided，不是指cryptographic primitive provided或者Chaumian blinding provided，2、mostly reliant on a cryptographic primitive known as Chaumian blinding这句话只是插入语，修饰一下anonymous e-cash protocols，真正的谓语是provided。这里网上有的翻译有点小错误] 1998年，Wei Dai的b-money第一次提议了通过解决...]]></description>
            <content:encoded><![CDATA[<p>人不知道他所签署消息的具体内容，而签名消息也不可追踪，即当签名消息被公布后，签名人</p><p>无法知道这是他哪次签署的。</p><p>打个简单的比方，A要寄一封信，封在信封里，但是要让B在信上签名，又不要B看见内容。怎么</p><p>办？在信封里同时加入一张复写纸，于是B在信封上签名，信封里面的信上就同时印下了B的签</p><p>名，这时B既没有看见信的内容，同时又在信上签了名。</p><p>另一点，A寄了很多信，都有B的盲签名，当公布出其中一封信时，B也不知道这是他哪次签的</p><p>名。</p><p>[翻译注意：1、provided指的是e-cash protocols provided，不是指cryptographic primitive</p><p>provided或者Chaumian blinding provided，2、mostly reliant on a cryptographic</p><p>primitive known as Chaumian blinding这句话只是插入语，修饰一下anonymous e-cash</p><p>protocols，真正的谓语是provided。这里网上有的翻译有点小错误]</p><p>1998年，Wei Dai的b-money第一次提议了通过解决计算难题和去中心化共识来创造货币的思</p><p>想，但是这个提议在细节上还有缺陷，即去中心化共识如何去实施。</p><p>2005年，芬尼（Hal Finney）介绍了“可重复使用的工作量证明”这一概念，即一个采用了两种</p><p>方案的系统，哪两种方案？1、b-money的理念；2、亚当贝克（Adam Back）的通过有计算难度</p><p>的哈希现金难题来创造加密货币的概念。但是[芬尼的这个概念]还是不够理想，因为它依赖可信的</p><p>计算来支持。</p><p>Wei Dai的b-money和亚当贝克（</p><p>Adam Back）的Hashcash在比特币白皮书翻译与详解里面</p><p>都有，不累述。</p><p>因为货币是一种先申请应用，[上一篇说了，first-to-file先申请是指，假如如果一个人有50BTC，</p><p>并且同时向A和B发送这50BTC，只有被首先被确认的那笔交易才会有效]，那么这种先申请应用，</p><p>交易顺序通常就至关重要，去中心化货币就需要解决达成去中心共识的问题。</p><p>比特币之前的货币协议遇到的主要障碍是，尽管多年以来已经有大量的关于创造安全的拜占庭容错</p><p>多方共识系统的研究，但是所有的协议都只解决了部分问题。</p><p>Byzantine-fault-tolerant拜占庭容错</p><p>拜占庭帝国进攻敌国，敌国能抵御5支拜占庭军队，拜占庭帝国派出10支军队，这10支军队任何</p><p>一支单独去进攻都毫无胜算，除非有至少6支军队（一半以上）同时进攻，才能攻下敌国。</p><p>10支军队分散在敌国的四周，依靠通信兵骑马相互通信来协商进攻意向及进攻时间。问题来</p><p>了！他们不确定他们中是否有叛徒。叛徒可能擅自变更进攻意向或者进攻时间。在这种状态</p><p>下，拜占庭将军们怎么才能保证有6支以上军队在同一时间发起进攻，从而攻下敌国？假定军队</p><p>之间的通信毫无问题。</p><p>没有叛徒情况下：</p><p>假如一个将军A提出一个进攻提议（比如：明天上午9点进攻，你愿意加入吗？）由通信兵分别</p><p>告诉其他的将军。</p><p>如果幸运，A将军收到了其他5位将军以上的同意，发起进攻。</p><p>如果不幸，其他将军在此时发出不同的进攻提议（比如：明天上午10点、11点进攻，你愿意加</p><p>入吗？）。由于时间上的差异，不同的将军收到并认可的进攻提议可能是不一样的，这是可能</p><p>出现A提议有3个支持者，B提议有4个支持者，C提议有2个支持者等等。</p><p>有叛徒情况下：</p><p>一个叛徒通信兵会向不同的将军发出不同的进攻提议（比如，通知A明天上午9点进攻， 通知B</p><p>明天下午1点进攻）。</p><p>而一个叛徒将军也可能同意多个进攻提议（即同意明天上午9点进攻又同意明天下午1点进</p><p>攻）。</p><p>这种发送前后不一致的进攻提议，被称为“拜占庭错误”。而能够处理拜占庭错误的这种容错</p><p>性，就称为拜占庭容错，Byzantine fault tolerance，简称为BFT。</p><p>这些协议假设系统里所有的参与者是已知的，并且产生安全边界，什么安全边界呢，即“如果N</p><p>方参与，那么系统可以容忍N/4的恶意参与者。”</p><p>也就是说，比特币出现之前的那些电子现金协议，都设置了一个多方参与多方共识的容错机</p><p>制，即假设N个人参与进来，他们不是匿名的，而是已知的，那么系统最多可以容忍四分之一的</p><p>人搞破坏。只要搞破坏的人不超过四分之一，那么这个系统还会运行良好。</p>]]></content:encoded>
            <author>dehop@newsletter.paragraph.com (dehop.eth)</author>
        </item>
    </channel>
</rss>