<?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>killscan.eth</title>
        <link>https://paragraph.com/@killscan</link>
        <description>undefined</description>
        <lastBuildDate>Sun, 03 May 2026 11:44:52 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/@killscan/designing-token-economies</link>
            <guid>R612xPrjhivOpVZN13jb</guid>
            <pubDate>Tue, 24 Jan 2023 07:40:36 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>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[干货：Dune Analytics 初学者完全指南（工具）]]></title>
            <link>https://paragraph.com/@killscan/dune-analytics</link>
            <guid>1WETfiOXDoqLOXBf5d48</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>一般来说，抽象可以分为两个主要类别：</p><ul><li><p>领域（Sector）抽象：特定领域的数据</p></li><li><p>项目（Project）抽象：特定项目的具体数据</p></li></ul><p>从抽象子菜单中，我们可以看到一个抽象列表，其中的标签说明了该抽象是针对领域还是针对项目的。拆开，贴上标签，并放入表格，以便用户对某些数据有一个简单和标准化的参考。</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></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>首先，让我们决定在仪表板上想要什么图表。让我们重建 Pooly 在他们的主页上建立的视图! 仔细看看下面两个截图，我们可以看到一些基于链上数据的指标。</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[分析Go1.18 GMP调度器底层原理（入门）]]></title>
            <link>https://paragraph.com/@killscan/go1-18-gmp</link>
            <guid>JoZay8zL9fe2ZShCnE7T</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>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[如何建立自己的宏观经济分析框架？投资]]></title>
            <link>https://paragraph.com/@killscan/6tnH7YHmq5dvxTqzTtAx</link>
            <guid>6tnH7YHmq5dvxTqzTtAx</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>以此为基础，只要读懂央行、银行体系（信贷）、实体经济（以GDP为框架的指标体系）、财政体系，就可以判断宏观经济的情况了(金融市场是基于这些而变化的，所以先不考虑)。</p><p>呃，好累啊，而且知乎这个编辑器太差了，经常跳出来乱码，直接把我梳理的笔记放这吧：</p><p>基础部分1.1：央行资产负债表</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>第二部分：周期</strong></p><p>说一下周期。依然根据我上面的划分，分为金融层面和实体层面。</p><p><strong>1、社融周期(也可以说是信用周期，因为我主要通过社融数据划分，所以习惯叫社融周期)</strong></p><p>该周期属于金融层面，极为重要。</p><p>大致三个阶段：</p><p><strong>第一阶段</strong>：经济下行，信用收缩，票据融资为代表的融资项上升。</p><p>票据融资是很好的反向指标。当经济不好，企业不愿意贷款，银行又有贷款任务的时候，就会用票据冲量。所以这个科目一旦持续上升，经济一般都在信用收缩期。</p><p><strong>第二阶段</strong>：政府主导的逆周期信用融资上行，信用收缩持续，但放缓。</p><p>主要代表为政府债（对应基建）、居民中长期消费贷款（对应地产）的增速触底回升。但此时顺周期信用融资还在下行，主要代表为企业中长期贷款（企业中长期贷款主要用来扩产，这样就和前面制造业那张图的逻辑对应上了）。今年经济的困难，有部分原因就是居民中长期消费贷上不来（具体分析看地产那张图，另外还要分析居民收入与杠杆率），政府债的增长需要先对冲居民中长贷的掉速，然后才能稳增长，难度可想而知了。</p><p><strong>第三阶段</strong>：政府稳经济的以工代赈见效，需求增加，顺周期信用扩张，社融上行。</p><p>除了刚才说的企业中长贷，还有居民短期消费贷、居民经营贷款，这两个贷款都说明实体经济自身的恢复情况。</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[60天入门区块链 带你进币圈暴富]]></title>
            <link>https://paragraph.com/@killscan/60</link>
            <guid>I3MJtjinQYEU0U6QtGaf</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>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[Web3 从炒币入门到精通]]></title>
            <link>https://paragraph.com/@killscan/web3</link>
            <guid>k38O8dMq8RpS7IzWwIfh</guid>
            <pubDate>Tue, 24 Jan 2023 06:54:01 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>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[撸毛加密货币研究工具]]></title>
            <link>https://paragraph.com/@killscan/VKqBLmGeoQctfgt2Xts7</link>
            <guid>VKqBLmGeoQctfgt2Xts7</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>这是我的职业——我每天超过12个小时都在做这些事情。</p><p>大多数时候你并不需要太多工具。</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[有人撸空投赚十万，有人撸空投赔百万]]></title>
            <link>https://paragraph.com/@killscan/QsRICb9bnayLjNpfvTRx</link>
            <guid>QsRICb9bnayLjNpfvTRx</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>虽然目前Defi的热度已经不及去年，但是这条赛道上的机会还有很多。尤其是目前各大公链纷纷开始发力的时候，公链上的项目必然也会井喷式的出现。所以在关注以太坊生态的同时，我们还可以关注SOL、FTM、BNB、Near、KSM、HT、OK等公链的机会。</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[zkSync生态]]></title>
            <link>https://paragraph.com/@killscan/zksync</link>
            <guid>Xy27diGoS21TyoWHMhkF</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 桥。 为了奖励早期的社区参与，该项目在各种社交平台上维护社交媒体奖励计划，包括 Twitter、YouTube 和 Reddit。活跃的社区成员最有可能收到空投。]]></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>为了奖励早期的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.zhihu.com/search?q=%E7%A4%BE%E5%8C%BA%E5%8F%82%E4%B8%8E&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>，该项目在各种社交平台上维护社交媒体奖励计划，包括 Twitter、YouTube 和 Reddit。活跃的社区成员最有可能收到空投。</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[怎么才能快速撸空投？]]></title>
            <link>https://paragraph.com/@killscan/XkWbe5IMhAGaF9D4K5uk</link>
            <guid>XkWbe5IMhAGaF9D4K5uk</guid>
            <pubDate>Tue, 24 Jan 2023 06:30:29 GMT</pubDate>
            <description><![CDATA[撸空投之前，你至少需要准备好以下工具： 1、一个区块链钱包，并存入一点主网代币（比如以太坊主网为ETH，币安智能链为BNB，等等）留作备用gas费。 2、一套海外常用社交账号，至少包括Twitter、Telegram、Discord账号。 3、在钱包里提前收录好多个常用主网。 关于钱包，我们建议您准备一个专门撸空投的钱包。因为空投项目质量不一，万一您的钱包进入了一些带有木马的链接，可能威胁到资产安全。所以，还是要和自己的其他数字资产分开管理。 存入主网代币，主要是空投项目一般会邀请您使用它的主要功能来促进链上日活，试用过程中，需要消耗少量的gas费用。 ZetaLabs 上的第一个实时 dApp 是一个功能性的全链交换，用于在任何连接的区块链之间交易资产。测试网用户可以通过交换资产、邀请新成员和报告问题来获得 ZETA 积分。下表显示了用户可以通过每项任务获得多少积分。获得 ZETA 积分的结构如下：]]></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><p>ZetaLabs 上的第一个实时 dApp 是一个功能性的全链交换，用于在任何连接的区块链之间交易资产。测试网用户可以通过交换资产、邀请新成员和报告问题来获得 ZETA 积分。下表显示了用户可以通过每项任务获得多少积分。获得 ZETA 积分的结构如下：</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
        <item>
            <title><![CDATA[Scroll测试网交互教程]]></title>
            <link>https://paragraph.com/@killscan/scroll</link>
            <guid>p0IZ94GaLfEMeE1QO0y4</guid>
            <pubDate>Tue, 24 Jan 2023 06:20:20 GMT</pubDate>
            <description><![CDATA[Scroll 是一个基于 zk-rollup 技术的以太坊扩容网络，被称为最兼容 EVM 的 ZK-Rollup，旨在为用户提供近乎即时且具有成本效益的交易，同时保持以太坊网络提供的高安全性。 登录测试网：4556456 添加完成后去领取测试币 测试币领取成功之后是在L1网络上 然后去跨链桥进行交互这里要先把钱包网络切换成L1网络钱包完成确认，等待几分钟，就可以在L2网络显示 完成之后进行代币兑换交互操作 兑换完成之后到POOL提供流动性，选择可用的余额进行流动性提供即可。这样就结束了。 关注我的mirro，不定期更新交互教程]]></description>
            <content:encoded><![CDATA[<p>Scroll 是一个基于 zk-rollup 技术的以太坊扩容网络，被称为最兼容 EVM 的 ZK-Rollup，<strong>旨在为用户提供近乎即时且具有成本效益的交易，同时保持以太坊网络提供的高安全性。</strong></p><p><strong>登录测试网：4556456</strong></p><p>添加完成后去领取测试币 测试币领取成功之后是在L1网络上 然后去跨链桥进行交互这里要先把钱包网络切换成L1网络钱包完成确认，等待几分钟，就可以在L2网络显示 完成之后进行代币兑换交互操作 兑换完成之后到POOL提供流动性，选择可用的余额进行流动性提供即可。这样就结束了。 关注我的mirro，不定期更新交互教程</p>]]></content:encoded>
            <author>killscan@newsletter.paragraph.com (killscan.eth)</author>
        </item>
    </channel>
</rss>