MoreRSS

site iconsmallyu修改

区块链行业的开发者。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

smallyu的 RSS 预览

我的加密货币定投策略(二)

2025-08-28 18:46:21

前情提要:《我的加密货币定投策略(一)

新的定投策略

现在把新的定投策略调整为这样:

  1. 定投频率:每周一次
  2. 定投金额:100 美元
  3. 定投标旳:比特币(BTC)

就这么简单,不再整那些花里胡哨的东西,只投比特币。至于以太坊或者其他平台币,想用的时候买就行了,手续费用不了多少,价格也高不了多少,没必要因为 “可能要用” 而提前布局。

你也许会犹豫,比特币现在的价格这么高了,还可以买吗?定投策略本身就是在消除对入场时机的顾虑,毕竟这是定投,不会一下子把钱砸进去,所以不用那么在意此时此刻的价格。关于定投策略的优点,李笑来的《定投改变命运》里已经有非常详细的解释,这里不多复述。

你也许会纠结,以太坊最近涨了那么多,接近历史新高,ETH/BTC 汇率持续走高,BTC 市占率持续下跌,为什么只选择定投 BTC 这一个币种?我的建议是,不要 FOMO。在接下来半年到一年的时间里,像以太坊一样动人心弦的例子还会有很多,不是可能有,是一定会有,也许每一次都会让你后悔,当初怎么没选这个币?要是早点买早点定投就好了。所以还是那句老话,不要 FOMO。

之前的错误

我之前犯了很严重的错误,也许是出于想体现出自己 “知道的多” 的心理,在定投组合中,选择了非常多小币种。现在差不多一年过去了,根据计算,实际上的收益效果比较差,大概在 22% 左右,这个收益率还不如全买 BTC 的涨幅。

当时有几个月的时间,我确实是按照定投组合在操作,但是后来由于这些原因,我自己都没能坚持下来定投这件事情:

  1. 某些币种比例太小,导致金额也小,假如一天定投 10 美元,某个代币占比 5%,一天就是 0.5 美元,一个月下来一共 15 美元。这样的金额即使涨 50% 或者跌 50% 都不会有感觉。而且这种小金额的代币一直放在账户里,也会让人不舒服。所以主观感受上,天天看到这些币种很难受,会想清理掉他们。

  2. 由于我时常关注加密行情的变化,有时明明知道价格下跌了,但是由于定投计划的存在,我无法手动加仓;有时明明知道价格上涨了,但是由于定投计划的存在,无法暂停定投。这样的感觉也很难受,像是被定投计划操控一样。

  3. 对于山寨币的风险预估不足,后来逐渐认识到山寨币的价值有限,而定投计划一开始给了山寨币比较高的比例,造成我头脑中的计划调整频率很快,快到定投策略跟不上。另外由于我的投资预算的不稳定,也就是收入和支出的不稳定,经常需要动一些钱,导致定投计划不得不暂停、重新开始。

所以不管以前怎么回事,以前的定投策略始终是有问题的,连我自己都无法好好实施。

黄金

注意这里只是 “加密货币” 的定投策略,不包含其他资产类型。比如价格锚定黄金的 PAXG 代币,不属于 “加密货币定投策略”,但是我会买、会定投,策略也是一样的:

  1. 定投频率:每周一次
  2. 定投金额:100 美元
  3. 定投标旳:黄金(PAXG)

美股

要这么说,把 BTC 和 PAXG 两个代币列出来不就行了吗?为什么还要强调黄金是不同于加密货币的资产类型?

因为 Kraken (海妖交易所)上支持买美股了。代币是发行在 Solana 链上的。我不懂美股,现在不能制定美股的定投计划。假如认为价格锚定黄金的代币算加密货币的话,那价格锚定美股的代币,算不算加密货币呢?事情就变复杂了。

美股上链这事还挺有意思,给未来带来了很多可能性。时代在发展,社会在进步,越来越有意思了。

关于卖出策略的疑问

人们常说,你无法同时拥有青春和对青春的体验。同样的道理,你无法同时享受拥有比特币,和比特币价格上涨带来的财富。

假如你现在还年轻,你需要钱,你要买车,你要旅游,你要体验生活。然后你把比特币卖了。等到老年之后,你会不会后悔?也许到时候会想,假如当时没卖掉,现在价值可就翻了很多倍。

要么放弃当下的享受,要么换来日后的后悔。怎么选呢?

王垠的计算机课程贵吗?

2025-08-25 01:05:32

课程的稀缺性

设想一下,假如你不想学王垠的课,但是又想掌握课程中的知识,有哪些途径?

你花同样的钱,是没有其他地方可买的。世界上还有其他华人,能有王垠的学识背景,并且在经过几年的教学试验后,整理出如此精品的课程吗?这种水平的课程,有市无价!

你也可以自己去求学,先掌握流利的英语,然后考个美国顶尖大学的硕士,经过几年的留学生涯,不但花很多钱,还要付出许多时间、精力和努力,还需要一些天赋和运气,才能学到与王垠课程同等水平的知识。你付出的代价,远远不是只花钱就够的。

你还觉得王垠的课程贵吗?

课程的内容

我刚才对王垠课程的描述是 “这种水平的课程”,那到底是什么水平呢?我可以举个具体的例子。

课程的第 1 课讲函数,对吧,所有程序员都知道函数是什么,即使不是程序员,初中上数学课也知道函数。

学习课程以前,函数是什么?函数是编程语言的语法之一,作用是把很多行代码包裹起来,方便以后重复调用。面向对象里面叫 “封装”。函数就是个特别基础的概念。

学习课程以后,函数是什么?函数可以是 “计算” 的基本元素,函数可以作为计算的输入,也可以作为计算的输出,一个计算的输出可以作为另一个计算的输入,输出的函数可以被另外的函数调用……

我这么说你肯定没看明白。

换个角度解释,王垠的导师 Daniel P. Friedman,有一本出版的书《The Little Learner》,Guy L. Steele Jr. 在给这本书的序言中写到,Friedman 在这本书里用高阶函数(higher-order functions)的 4 种不同用途,表达了机器学习(machine learning)的核心原理。

你是不是不相信,函数可以表达深度学习的原理?那就去了解一下 lambda calculus,一种和图灵机同等地位的形式化系统,可以表达的是整个计算机体系,而不只是深度学习。

这才是 “函数”。第 1 课学的函数,是这个函数。

DeFi 基础: 理解 AMM 定价机制

2025-08-20 22:00:00

这是一个 DeFi 系列教程,在动手实践的过程中,学习和理解 DeFi 相关的概念与原理。因为篇幅问题,博客只放第 1 篇:

  1. DeFi 基础: 理解 AMM 定价机制
  2. DeFi 基础: 预言机与报价
  3. DeFi 基础: 借贷与清算
  4. DeFi 进阶: 闪电贷与套利

AMM 的全称是 Automated Market Maker,自动做市商,作用是不需要订单簿撮合交易,就可以自动完成定价与交易。

这篇文章解释了 Uniswap V2 的核心定价逻辑,并且提供了完整的合约代码示例、命令行操作步骤、实际的链上交易现场等,作为理解 AMM 的配套参考。

AMM 计算公式

基本逻辑

Uniswap V2 用的定价逻辑是恒定乘积做市商(Constant Product Market Maker, CPMM),也是我们的示例 AMM 合约在用的方法。这里有一个恒等公式:

x * y = k

意味着池子里有两种资产 xy,当 x 增多的时候,y 就应该减少,y 增多的时候,x 应该减少,k 总是保持不变。

在添加初始流动性的时候,我们第一次确定下来这个 k 的值,比如我们按照 2000 USDC / 1 WETH 的价格注入初始流动性,会得到(不考虑精度):

k = 2000

当我们想要用 USDC 换出 WETH 的时候,池子里的 USDC 增多,为了保持 k 不变,合约会计算应该保留多少 WETH,然后把相应数量的 WETH 转给我们。

第一次兑换

当我们想要用 USDC 换出 WETH 的时候,池子里的 USDC 增多,为了保持 k 不变,合约就会把相应数量的 WETH 转给我们了。

例如,我们试图用 500 USDC 换出 WETH,此时加上初始流动性的 2000 USDC,池子里一共 2500 USDC,那么:

x = 2500y = k/x = 2000/2500 = 0.8

这个 0.8 意味着,为了保证 AMM 池子里的 k 值恒定为 2000,池子需要转出 0.2 WETH。也就是说,我们会得到 0.2 个WETH。

第二次兑换

我们再来用 500 USDC 买一次,此时池子里一共有 2500+500=3000 USDC,则:

x = 3000y = k/x = 2000/3000 = 0.667

这个恒定乘积公式计算得出池子里应该保留 0.667 个 WETH,上一轮交换后还剩 0.8 WETH,所以这一轮我们实际得到 0.8-0.667 = 0.133 WETH。

对比来看,第一次用 500 USDC 可以换出 0.2 WETH,第二次用 500 USDC 就只能换出 0.133 WETH 了。随着池子里流动性的减少,WETH 的价格涨了。

价格曲线

这就是自动做市商的核心逻辑,价格不是写死的,而是根据池子中剩余的流动性算出来的。要注意 x 和 y 的乘积是一条曲线,因为 y=k/x,画成图是这样:

接下来会用实际的操作步骤与链上交互,来体验 AMM 的运作。

示例合约

合约代码源文件在仓库:smallyunet/[email protected]

首先准备两个合约,一个是 TestERC20.sol,比起标准的 ERC-20 合约,支持自定义代币精度,以及随意 mint 一些代币。

第二个要准备的合约是 SimpleAMM.sol,提供了对代币增加流动性、代币兑换等功能。合约代码不算很简单,我们会在接下来实际的操作用,逐步体会和理解这个合约的功能,以及解读源代码。

以下所有操作都在以太坊的测试网 Sepolia 上进行。

环境准备

准备好命令行工具,以及设置两个环境变量:

foundryupexport RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"export PK_HEX="<YOUR_PRIVATE_KEY_HEX>"

下载合约仓库、进入到仓库根目录:

git clone https://github.com/smallyunet/defi-invariant-lab/git switch v0.0.1cd defi-invariant-lab

部署代币合约

部署合约

部署两个测试版本的 ERC-20 代币,一个叫 USDC,一个叫 WETH:

forge create \  --rpc-url $RPC_URL \  --private-key $PK_HEX \  --broadcast \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args "USD Coin" "USDC6" 6

部署的合约地址是:0x84637EaB3d14d481E7242D124e5567B72213D7F2

forge create \  --rpc-url $RPC_URL \  --private-key $PK_HEX \  --broadcast \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args "Wrapped Ether" "WETH18" 18

部署的合约地址是:0xD1d071cBfce9532C1D3c372f3962001A8aa332b7

验证合约

如果愿意,可以这样验证下合约:

export ETHERSCAN_API_KEY=你的keycast abi-encode "constructor(string,string,uint8)" "USD Coin" "USDC6" 6forge verify-contract \  --chain-id 11155111 \  0x84637EaB3d14d481E7242D124e5567B72213D7F2 \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000855534420436f696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055553444336000000000000000000000000000000000000000000000000000000" \  --etherscan-api-key $ETHERSCAN_API_KEYforge verify-contract \  --chain-id 11155111 \  0xD1d071cBfce9532C1D3c372f3962001A8aa332b7 \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args $(cast abi-encode "constructor(string,string,uint8)" "Wrapped Ether" "WETH18" 18) \  --etherscan-api-key $ETHERSCAN_API_KEY

部署 AMM 合约

部署合约

这里的参数 30 指收取 0.3% 的手续费:

forge create \  --rpc-url $RPC_URL \  --private-key $PK_HEX \  --broadcast \  contracts/amm/SimpleAMM.sol:SimpleAMM \  --constructor-args $USDC_ADDR $WETH_ADDR 30

部署的合约地址是:0x339278aA7A09657A4674093Ab6A1A3df346EcFCF`

验证合约

forge verify-contract \  --chain-id 11155111 \  0x339278aA7A09657A4674093Ab6A1A3df346EcFCF \  contracts/amm/SimpleAMM.sol:SimpleAMM \  --constructor-args $(cast abi-encode "constructor(address,address,uint16)" $USDC_ADDR $WETH_ADDR 30) \  --etherscan-api-key $ETHERSCAN_API_KEY

mint 代币

声明钱包地址与合约地址:

export MY_ADDR=0x44D7A0F44e6340E666ddaE70dF6eEa9b5b17a657export AMM_ADDR=0x339278aA7A09657A4674093Ab6A1A3df346EcFCFexport USDC_ADDR=0x84637EaB3d14d481E7242D124e5567B72213D7F2export WETH_ADDR=0xD1d071cBfce9532C1D3c372f3962001A8aa332b7

挖 100 万个 USDC,精度是 6 位数:

cast send $USDC_ADDR "mint(address,uint256)" $MY_ADDR 1000000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX

挖 1000 个 WETH,精度是 18 位数:

cast send $WETH_ADDR "mint(address,uint256)" $MY_ADDR 1000000000000000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX

铸币的交易与结果可以直接在浏览器上看到,这个 是挖 USDC 的交易,这个 是挖 WETH 的交易。

给 AMM 合约授权

给 AMM 授权是因为接下来想要给 AMM 添加流动性,添加流动性会调用 addLiquidity 函数,其中用到了 transferFrom,所以需要先给合约授权,让合约可以动用我的 USDC 和 WETH 代币:

cast send $USDC_ADDR "approve(address,uint256)" $AMM_ADDR "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" \  --rpc-url $RPC_URL --private-key $PK_HEXcast send $WETH_ADDR "approve(address,uint256)" $AMM_ADDR "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" \  --rpc-url $RPC_URL --private-key $PK_HEX

交易哈希分别是 USDCWETH

添加初始流动性

添加流动性的 函数 比较简单,大概是合约里有两个变量 reserve0reserve1,调用 addLiquidity 函数的时候,会向 AMM 合约转账参数数量个代币。

先以 2000 USDC / 1 WETH 的价格,添加初始流动性:

cast send $AMM_ADDR "addLiquidity(uint256,uint256)" 200000000000 100000000000000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX

交易 完成后,可以查询到 AMM 合约剩余的代币数量:

cast call $AMM_ADDR "getReserves()(uint112,uint112)" --rpc-url $RPC_URL# 200000000000 [2e11]# 100000000000000000000 [1e20]

用 USDC 换 WETH

合约代码解读

我们的合约代码 swap0For1 是这样:

function swap0For1(uint256 amtIn) external returns (uint256 out) {    require(token0.transferFrom(msg.sender, address(this), amtIn), "t0in"); // 把用户的 x 转进合约    uint256 r0 = token0.balanceOf(address(this)); // 查询当前 x    uint256 r1 = token1.balanceOf(address(this)); // 查询当前 y    uint256 amtInEff = (amtIn * (10_000 - feeBps)) / 10_000; //计算扣除手续费后,用户转入了多少 x    // x*y=k, solve out = r1 - k/(r0)    uint256 k = (r0 - amtInEff) * r1;   // 计算 k    out = r1 - Math.ceilDiv(k, r0);     // 计算给用户多少 y    require(token1.transfer(msg.sender, out), "t1out");}

函数代码体现了刚才描述的关于 x*y=k 的恒定公式。因为 AMM 合约考虑到收手续费的问题,所以有一个 amtInEff 用来表示用户实际转入了多少 x。

测试第 1 次交换

我们来实际发起交易,看看合约运行后的效果,先试着用 1000 USDC,看能换多少个 WETH 出来:

cast send $AMM_ADDR "swap0For1(uint256)" 1000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX

交易 完成后,查看一下代币余额:

cast call $USDC_ADDR "balanceOf(address)(uint256)" $MY_ADDR --rpc-url $RPC_URLcast call $WETH_ADDR "balanceOf(address)(uint256)" $MY_ADDR --rpc-url $RPC_URLcast call $AMM_ADDR "getReserves()(uint112,uint112)" --rpc-url $RPC_URL

其实区块链浏览器上能很直接的看到交换的数量,交易哈希是:0xf13bd1d1602d7c106c2acdf4cb3b1ec37fa42d8871a682e32cce3f2049fff5a2

我们转出了 1000 USDC,收到了 0.496019900497512437 个 WETH。这里因为有 0.3% 的手续费,所以收到的 WETH 不是 0.5。

除了手续费,还存在一个价格的问题,按理来说,随着剩余 WETH 数量的减少,WETH 的价格会越来越高。

测试第 2 次交换

再来用 1000 USDC 兑换一次,看能换出多少 WETH:

cast send $AMM_ADDR "swap0For1(uint256)" 1000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX

这次兑换的交易哈希是:0x1ee9ceb0707d77d78669bfb6cc1179bf9d6b31c57b868f5f52ed2f01a4127481

这一次,花费了 1000 USDC,收到了 0.491116179005960297 个 WETH。与上一次兑换的结果相比,收到的 WETH 真的减少了。

用 WETH 换 USDC

可以自己测试玩一下。

Go 语言 GMP 调度器的原理是什么

2025-08-18 21:50:50

声明:我看不起 “Go 语言 GMP 调度器的原理是什么” 这种技术话题。

我平时没兴趣研究这种问题。因为在面试中被问到的频率太高了,现在想花 2 个小时的时间来了解下。一方面研究下这个问题背后到底有多大的技术含量,另一方面把这个问题的答案写下来。但是我不会让这种内容停留在我的头脑里,所以下次面试被问到,我肯定还说不会 😏

基本概念

GMP 是一个缩写:

  • G(goruntine):就是协程,代码里每 go 一个,G 的数量就多一个
  • M(Machine):就是系统级别的线程,在其他语言里的 thread
  • P(Processor):数量为 GOMAXPROCS,通常默认是 CPU 核心数。

GMP 的意思是,启动多少个 M(线程) 来执行 G(协程),最多允许 P(核心数)个 M 并行执行。

三个不变量

无聊的(简化后的)定义来了:

  1. 只有拿到 P 的 M 才能执行任务
  2. 可运行的 G 只会在某个 P 的本地 runq 或者全局队列
  3. 当 M 进入阻塞状态(syscall/cgo)时,会及时把 P 让出

这几句话看着很费劲,不需要现在理解,接下来会用一些代码例子来说明他们的含义。

GMP 的调试日志

这是一个最简单的代码文件,用来演示启动一个协程:

package mainimport (    "fmt"    "sync")func main() {    var wg sync.WaitGroup    wg.Add(1)    go func() {        defer wg.Done()        fmt.Println("Hello from goroutine")    }()    wg.Wait()}

然后带上调试参数运行一下:

go build demo0.goGODEBUG='schedtrace=200,scheddetail=1' ./demo0

注意不要用 go run,因为会引入一些 Go 语言运行时的日志。这个二进制版本的日志比较干净,内容是:

SCHED 0ms: gomaxprocs=10 idleprocs=7 threads=5 spinningthreads=1 needspinning=0 idlethreads=0 runqueue=0 gcwaiting=false nmidlelocked=1 stopwait=0 sysmonwait=false  P0: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P1: status=1 schedtick=0 syscalltick=0 m=2 runqsize=0 gfreecnt=0 timerslen=0  P2: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P3: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P4: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P5: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P6: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P7: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P8: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P9: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  M3: p=0 curg=nil mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=true blocked=false lockedg=nil  M2: p=1 curg=nil mallocing=0 throwing=0 preemptoff= locks=6 dying=0 spinning=false blocked=false lockedg=nil  M1: p=nil curg=nil mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=nil  M0: p=nil curg=nil mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=1  G1: status=1() m=nil lockedm=0  G2: status=4(force gc (idle)) m=nil lockedm=nilHello from goroutine

这些日志显示了这些信息:

  • 第一行 SCHED 开头的是汇总信息,告诉我们程序启动了 10 个 P(gomaxprocs=10)。
  • 只有 P1M2 拿着运行
  • P0M3 拿着处于 spinning 状态,也就是等待任务的状态。

没看到 print 相关的 G,是因为任务运行时间太短了,没被 trace 捕获就结束了,这里主要展示 GMP 的详细信息可以用 debug 命令来看。

抢占式调度

package mainimport (    "fmt"    "runtime"    "time")func busy(tag string, d time.Duration) {    end := time.Now().Add(d)    x := 0    for time.Now().Before(end) {        x++    }    fmt.Println(tag, "done", x)}func main() {    runtime.GOMAXPROCS(1)    go busy("A", 1500*time.Millisecond)    busy("B", 1500*time.Millisecond)}

这个代码的运行结果是,有时候 AB 前面,有时候 BA 前面。

我们已经用 runtime.GOMAXPROCS(1) 设定只有一个 P,但是 Go 语言的 GMP 调度器,仍然会 10ms 释放一次时间片,也就意味着,即使 go busy("A") 处于阻塞状态,时间片之后也会让出执行权,交给主线程去运行 B

可以用这个 busy 的函数定义来让抢占式调度更加肉眼可见:

func busy(tag string, d time.Duration) {    end := time.Now().Add(d)    next := time.Now()    for time.Now().Before(end) {        if time.Now().After(next) {            fmt.Print(tag, " ") // 每 ~100ms 打印一次            next = time.Now().Add(100 * time.Millisecond)        }    }    fmt.Println(tag, "done")}

程序的打印结果会是 B A B A B A A B A B A B A B A B A B A B A B A B B A B A B A B done。这意味着不是 tag 为 A 的 P 一路执行到底,也不是 tag 为 B 的 P 一路执行到底,他们在 GMP 调度器中交替执行。

P 偷活干(work-stealing)

来看这个代码示例:

package mainimport (    "runtime"    "sync"    "time")func spin(d time.Duration) {    deadline := time.Now().Add(d)    for time.Now().Before(deadline) {    } // 纯CPU忙等}func main() {    runtime.GOMAXPROCS(1) // 先让所有 G 挤到同一个 P 的本地队列    const N = 120    var wg sync.WaitGroup    wg.Add(N)    for i := 0; i < N; i++ {        go func() { defer wg.Done(); spin(500 * time.Millisecond) }()    }    time.Sleep(30 * time.Millisecond) // 给点时间把队列堆满到 P0    runtime.GOMAXPROCS(4) // 突然放大并行度:P1~P3 会去“偷” P0 的一半    wg.Wait()}

这个代码干了什么呢,首先设定之后一个 P,然后启动 120 个 G 给这个 P 去执行。30 毫秒后,突然增大 P 的数量。

用 debug 日志能看到,运行后半段有这样的日志:

P0: status=1 schedtick=46 syscalltick=2 m=0 runqsize=17 gfreecnt=0 timerslen=0P1: status=1 schedtick=58 syscalltick=0 m=4 runqsize=5 gfreecnt=15 timerslen=0P2: status=1 schedtick=60 syscalltick=0 m=2 runqsize=5 gfreecnt=18 timerslen=0P3: status=1 schedtick=42 syscalltick=0 m=3 runqsize=17 gfreecnt=0 timerslen=0

也就是说,本应该 G 全在 P0 上运行,等到 P1、P2、P3 出来后,它们发现 P0 很忙,就去 P0 的队列里拿了几个任务过来执行。

P 的 runq 队列和全局队列

一个 P 想找活干的时候,上面的代码是偷其他 P 的示例。更严谨的流程是,P 先从本地 runq 队列找,再到全局队列找,找不到再去偷其他 P 的。

什么是 runq 队列,什么是全局队列?可以看这个代码:

package mainimport (    "runtime"    "sync"    "time")func spin(d time.Duration) {    end := time.Now().Add(d)    for time.Now().Before(end) {    } // 纯CPU忙等:保持 runnable}func main() {    runtime.GOMAXPROCS(1) // 只有 P0:所有新 G 先进入 P0 的本地 runq    const N = 600 // 让它明显超过本地 runq 容量(当前实现通常是 256)    var wg sync.WaitGroup    wg.Add(N)    for i := 0; i < N; i++ {        go func() { defer wg.Done(); spin(800 * time.Millisecond) }()    }    time.Sleep(500 * time.Millisecond) // 给运行时时间把“溢出的一半”推到全局队列    runtime.GOMAXPROCS(4) // 其它 P 进场,会先从“全局队列”拿活(不是偷)    wg.Wait()}

debug 状态运行:

go build demo4.go   GODEBUG='schedtrace=200,scheddetail=1' ./demo4 &> demo4.log

日志会比较多,日志前面几行像这样:

SCHED 0ms: gomaxprocs=10 idleprocs=9 threads=2 spinningthreads=0 needspinning=0 idlethreads=0 runqueue=0 gcwaiting=false nmidlelocked=0 stopwait=0 sysmonwait=false  P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0 timerslen=0  P1: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0

其中首行的 runqueue=0 就是全局队列,P0 后面的 runqsize=0 是 P0 的本地队列,P1 后面的 runqsize=0 是 P1 的本地队列。可以看到此时的 P1 状态是 0,也就是不可运行。

随着程序的运行,P0 会启动非常多个 G,日志状态是这样:

SCHED 200ms: gomaxprocs=1 idleprocs=0 threads=5 spinningthreads=0 needspinning=1 idlethreads=3 runqueue=395 gcwaiting=false nmidlelocked=0 stopwait=0 sysmonwait=false  P0: status=1 schedtick=10 syscalltick=2 m=0 runqsize=204 gfreecnt=0 timerslen=1

一般 P 的本地队列默认是上限是 256,达到这个峰值后,就会把任务溢出到全局队列。

再然后,P1、P2、P3 启动,开始从全局队列拿任务(全局队列有任务则不需要偷其他 P 的):

SCHED 826ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 needspinning=1 idlethreads=0 runqueue=217 gcwaiting=false nmidlelocked=0 stopwait=0 sysmonwait=false  P0: status=1 schedtick=35 syscalltick=2 m=0 runqsize=179 gfreecnt=0 timerslen=0  P1: status=1 schedtick=14 syscalltick=0 m=3 runqsize=90 gfreecnt=0 timerslen=0  P2: status=1 schedtick=14 syscalltick=0 m=4 runqsize=64 gfreecnt=0 timerslen=0  P3: status=1 schedtick=13 syscalltick=0 m=2 runqsize=46 gfreecnt=0 timerslen=0

另外,当 P 依次从本地 runq、全局队列、其他 P 都找不到任务时,会再去问一下 netpoll(问一下 OS)有没有新的 G,要是有就执行,没有就自旋(待命)。这就是 P 执行任务的逻辑。

阻塞 syscall 会及时让出

看这个代码例子:

package mainimport (    "fmt"    "runtime"    "time")func main() {    runtime.GOMAXPROCS(2)    go func() {        time.Sleep(2 * time.Second) // 类比阻塞 syscall/cgo        fmt.Println("blocking done")    }()    go func() {        for i := 0; i < 6; i++ {            time.Sleep(300 * time.Millisecond)            fmt.Println("still running", i)        }    }()    time.Sleep(3 * time.Second)}

运行结果会是:

still running 0still running 1still running 2still running 3still running 4still running 5blocking done

这个代码示例的含义是,第一个 G 明明会阻塞任务队列,一直占着 P 执行,但实际上第二个 G 仍然在运行。

说明 GMP 调度器不会因为某个 G 的阻塞,影响到其他 G 的执行。(其实这是协程调度器很基本的要求)

关闭异步抢占

对于这个代码示例:

package mainimport (    "fmt"    "runtime"    "time")func spin() {    for { /* 紧密循环 */    }}func main() {    runtime.GOMAXPROCS(1)    go spin()    time.Sleep(100 * time.Millisecond)    fmt.Println("I should still print unless preemption is off")}

可以分别用两个命令来运行,一个是

go build demo7.goGODEBUG='schedtrace=1000,scheddetail=1' ./demo7

另一种是:

go build demo7.goGODEBUG='schedtrace=1000,scheddetail=1,asyncpreemptoff=1' ./demo7

asyncpreemptoff=1 可以关闭异步抢占。也就是说,如果没有关闭,没有带这个参数,程序会正常运行,打印出:

I should still print unless preemption is off

如果关闭了异步抢占,则程序会被死循环卡住。这个例子主要可以体现 GMP 主动让出 CPU 的特点,当关闭了主动让出的能力后,GMP 就会被阻塞住了。

Go 语言源码

我没有深入看源码,比如 G、M、P 的常量定义在 src/runtime/runtime2.go 文件:

再比如 src/runtime/proc.go 文件中的 runqputslow 函数,功能就是判断本地队列有没有满,如果满了就放到全局队列:

进一步深入

这篇文章肯定有不全面和不到位的地方,我不想进一步深入了,也许有人喜欢折腾这些吧。

Go 语言的 GMP,就是协程调度器的一种具体的工程化的实现,估计很多人在意的,是这种工程化实现背后的细节,比如怎么用栈结构来管理任务队列、怎么实现抢占、让出逻辑等。协程调度器的具体实现方式可以有各种各样的变化,但它们的基本原理都是 continuation。只是 Go 语言把协程作为卖点了。只要其他语言愿意,也是可以开发出自己版本的协程调度器的。

那么问题来了,那些喜欢研究 GMP 原理的人,你们有没有了解过其他语言的协程(coroutine)、虚拟线程、异步函数、Process 是怎么实现的,它们都是比线程更轻量的类似于协程的东西,和 Go 语言的 gorountine 有什么区别?横向对比一下?

如果什么时候,我的工作需要,只有我了解这些内容,才能把工作做好,那么我肯定去把这些东西搞明白。

疑问

我之前写过一个观点:

Go 语言 “千辛万苦” 做出了自动的垃圾回收,减轻程序员对于内存管理的头脑负担。而有些面试官 “千辛万苦” 去搞明白 Go 语言 GC 的原理是什么,怎么标记怎么释放之类,不但引以为豪,而且拿来考察候选人。作为 Go 语言的教徒,你知不知道你的行为在否定 Go 语言设计者的努力?如果真的相信用头脑来管理内存的力量,为什么不去搞 Rust?好比我是一个汽车驾驶员,我要去考驾照,难道需要我搞清楚发动机的工作原理、是怎么把汽油燃烧转变为机械动力的、能量转化公式是什么?我又不是在制造汽车,也不是在开发编程语言。

同样的道理:

Go 语言为了让广大程序员能便捷简单地、用上轻量级的协程,“千辛万苦” 搞出来一个 go 关键字,然而有些人却费尽 “千辛万苦” 研究这个调度器是怎么实现的,懂原理则说明会 Go 语言,不懂则说明 Go 语言水平不行,这是什么道理?作为 Go 语言的教徒,你在否定 Go 语言设计者的努力,明白吗?如果这个语言需要你搞清楚协程调度的原理,才能写出好的代码,那就说明这个语言实现的不到位,偏离了设计者的初衷,没有达到设计者本来的意图。

如果你是编程语言的开发者,需要在另一种语言中借鉴、实现、优化 Go 语言的调度器,那么你就尽情研究吧,这样的工作确实需要懂 GMP 调度器的原理。如果不是那样的工作呢?

Web3 项目分析计划

2025-08-14 23:00:00

Web3 项目分析系列文章专用的 Paragraph 频道地址是:

计划内容

经过几天时间的尝试,我觉得 Web3 项目分析计划是一件很有意义的事情。不清楚看到分析文章的人有没有收获,但是从我自己理解项目、学习技术的角度,是有收获的,所以我需要把这个计划继续下去,变为一件常态化的事情。

具体计划内容是,每周分析一个 Web3 行业的项目,从看白皮书开始,到理解项目的运作模式、当前商业状态等,尤其关注技术理念和技术创新方面,然后写成分析报告,不需要很专业的那种报告,大概相当于学习笔记就可以了。具体分析哪个项目是经过主观挑选的。最终的分析报告也许会有质量,有看点,但也许会比较短,没有质量。因为我并不能在一开始选定项目的时候,就知道这个项目有没有含量,尤其是技术含量。

工程代码没有价值

这个计划有点像是区块链研究员干的事情,而不是区块链程序员应该干的事情。为什么我的计划不是每天写 100 行代码,开发一个区块链小工具,或者每天积累一点,开发一个大的区块链工程?

因为工程代码如果脱离项目背景,就没有价值。我在几年的工作中写过很多代码,但是如果现在把那些代码拿出来,会发现毫无意义。工程化的代码,往往是为了完善项目的功能,而项目需要某些方面功能,是为了迎合运营和宣发的需求,一定是有商业目的的。如果需求背景不存在,代码就毫无价值。

尤其是随着 AI 的日益强大,写工程代码这件事情更是越来越廉价。AI 可以几分钟写出上万行代码,堆砌代码的能力绝对超过人类。如果我想靠每天写几百行工程代码来训练和提升自己,那我一定会失败的很惨。所以不能干这样的事情。

什么样的工程代码是有意义的呢?就是已经找准了产品需求和定位的情况下,想把功能落实跑通,然后让 AI 来干活,把代码写出来。AI 写的代码有时候会跑偏,需要手动修复一下 bug,这种情况下,手动写出的工程代码才有意义。现在的开发节奏已经应该是这样了。

以前的时代,人们喜欢说 “Talk is cheap. Show me the code”,但是现在时代变了, prompt 比 code 更有价值,也许这句话会变为 “Code is cheap. Show me the prompt”。

文章更能表达思想

不去计划每天写一些工程代码,另一个原因在于,我已经做过了一些尝试,去试图开发小的区块链工具,或者大的区块链工程。目前来看,我之前的想法,无论是做小工具的思路,还是做大工程的思路,都是没有结果的,因为需求本身也许不存在。没有任何正反馈,根本做不下去。

与代码相比,写文字、写文章、写观点更有意义一点。一个产品创意背后,可能有 100 行代码,也可以有 10000 行代码,需要付出的时间成本完全不同,但如果最终的关注量都是 0,那么结果就是一样的,9900 行代码白写了。而文字是能够体现思想的。

你也许想反驳,怎么能说工程代码没有价值呢?以太坊的客户端同一份 Spec,有五六种工程化的实现,用了不同语言、做了不同优化,市场占有率有高有低,难道不是工程化代码价值的体现吗?当然是,他们拿着以太坊基金会的赞助开着公司写着代码,而且已经有了明确的项目背景,工程代码自然是有价值的。我指的是没有项目背景的工程代码。

虽然工程代码没有价值,但教学性质的代码是很有意义的,我仍然会复习计算机课的练习题,以保证自己的代码水平。我已经是第三轮做那些练习题了,这次我严格限制自己的做题速度,一天最多做一道题。一方面是保证有足够的时间消化练习题包含的知识,相信潜意识的力量。另一方面,得分配时间到其他事情上,不能整天只反复做同样的题。而且由于做题比较慢,可以逐渐培养自己每天做题的习惯,不至于遗忘计算机课的知识。

提高宏观理解能力

为什么我觉得对项目做分析是有意义的?因为其实我对区块链技术的理解,很大程度上,来自于几年前读了很多白皮书。我当时按照币种市值的排名,逐一下载了排名前几百的币种白皮书,还用 A4 纸都打印出来看。

记得几年前有人发邮件问我,如何学习区块链技术。我当时认真写了个回复,说我是从哪个网站下载的白皮书,以及看了哪些书之类。后来对方回复我说,这不是他想要学习的区块链技术,他想要学习的是如何写代码。那个时候我才意识到,不同的人,对技术的定义是不同的。

以前没有 AI,我没能认识到代码的价值,现在有了 AI,我还是认识不到代码的价值。

研究能力的重要性

在币圈,人们常说 DYOR(Do Your Own Research),这个词经常出现在 KOL 推广和夸赞某个代币的时候,用来声明不做投资建议,你要自己对自己负责。“研究能力” 一直都是非常重要的能力,如果不具备好的研究能力,你连自己的钱都管理不好。事实上什么事情都需要研究,研究如何学英语、研究如何找工作、研究假期去哪儿玩、研究写代码、研究科学技术、研究如何哄女朋友开心,等等,都是研究。Web3 项目分析计划的目标正是研究项目、锻炼研究能力。

具备好的研究能力的人,不管学习什么都会变得轻松。试想,你觉得去研究明白怎么把代码写好,尤其是工作中用的普通代码,需要多长时间?很多时候连 “研究” 都用不着!那么,你觉得能把某种技术研究明白的人,会没有能力研究清楚怎么写代码吗?

那么为什么我觉得自己可以写出分析报告?我以前没专门写过,但是有时会根据技术来对项目做横向对比,所以专注于对某个项目做技术分析,应该不是难事。我工作过的项目,假如让我写分析,肯定能写出其中的细节,只是因为项目还在,不能写。写项目分析对我来说也是一个学习和积累的过程。

实际上分析区块链项目的方法论,我早在《看懂任意区块链项目的技术架构》就写过了,到现在都不觉得那篇文章内容有什么问题,无非就是链上链下交互,不同项目往里面填充不同的业务逻辑而已。

写作平台的选择

我对于博客上应该放哪些文章,是比较纠结的,我不希望一打开博客,满屏幕都是 “对 XX 项目的分析”。为了保持文章列表的简洁,这些项目分析系列的内容应该换一个平台放。最近看到 Paragraph 不错。Paragraph 是一个 Web3 领域的 Newsletter 平台,功能类似于 Web2 的 Substack,每篇文章的全部内容都会提交到 Arweave 区块链上,包括作者的名字、头像、文章正文、配图等。(这也就意味着文章一旦发布,就不可能被删除。)

为什么不选择其他平台呢?比如发到知乎、掘金,甚至是头条、百度、登链等平台,再加上 Meidum、X、Mirror 一类,文章访问量肯定可以高很多,关注量也会高很多。

因为那些充斥着低质量内容的平台,不值得去发布高质量内容的文章。那些人是看不懂的,看不懂我在写什么。看看掘金首页上有什么?10 篇文章 8 篇讲 Cursor,很难想象用户素质得多低。知乎就更不用说了,内容杂乱、商业化,关键是网页访问弹窗,不是让登陆就是让下载 APP,正经人谁去那种平台啊。我在脉脉的职言区,匿名账号下,发布过几千条帖子,总阅读量超过几千万,发的都是观点偏激、引战一类的内容。那种阅读量有意义吗?没有意义。

所以继续努力吧,等自己成为 somebody,再考虑访问量的问题。没有人会关心 nobody 写的东西。

为什么要做出计划

其实要按照我自己喜好,我觉得自己真正有价值的文章,是吐槽同事、吐槽公司、吐槽面试经历等情绪宣泄类内容。那些是包含了亲身经历、切实体会、真情实感在里面的,耗费了时间和心情才得到的、宝贵的人生体验,比技术文章有意思多了。对行业的见解、对公司的不满、对同事的吐槽,是我的文章永远超越 AI 的地方,因为 AI 没有情绪,不会生气、不会沮丧。单纯讲技术知识点,AI 一下子就能生成很多,但是 AI 永远无法体会到作为人的情感。

反正人总要做选择,要么忙着活,要么忙着死。

对 0G 项目的分析

2025-08-06 13:08:00

首先我不是很看好 0G 的技术含量,因为 0G 是中国团队开发的项目。0G 是一个 AI 赛道的项目,3 月份在 TinTinLand 上发布过招聘信息,大概 9 月份要发币的样子,猜测在 AI 方面的噱头大于技术积累。我因为最近加了一个 TinTinLand 的学习群,和 0G 合作推出社区课程那种,所以稍微有点兴趣来分析下这个项目。

0G 的官网地址是 0g.ai,在官网上就极尽所能的把各种名词摆上了,”the next generation”、“decentralized AI”、”DeAIOS”、”RWA”,用词口径越大通常不是一个好兆头。

项目背景

0G 在 2024年8月 发布了 白皮书,单从白皮书目录和篇幅来看不是很乐观,目录结构比较简单,一共只有 20 页的内容。篇幅长度是肤浅的判断方式,比特币的白皮书也才 9 页。主要是目录结构,作为一个 AI 技术导向的项目,如此简洁的章节会给人草台的感觉。

首先来看看摘要里怎么说,0G 在解决的是 AI 模型训练过程中透明度的问题:

话说,看到 modular 这个词我有点不好的预感,尤其是看到 DA 这个词后,心想该不会用的 Celestia 吧,结合官网首页上宣称的 2500/s 的 TPS,有哪条链能做到呢?Cosmos 有点像。不过到这里还不理解首页上说的 8K 个 validator 是什么含义,Cosmos 可做不到这个。

好在不是 Celestia,白皮书里没详细说技术选型的事,但明显和 Celestia 是并列关系,自己搞了个叫 0G DA 的链。

白皮书里详细解释了 PoRA(Proof of Random Access)的挖矿机制,这个是有技术含量的部分,与 Filecoin 冷储存的模式不同,0G Storage 强调链上可以即时访问数据,所以设定了 8TB 的挖矿窗口,要求矿工可以快速在范围内验证数据完整性。

PoRA 的局限性在于,通过随机抽样验证的方式,可以验证矿工是否拥有完整数据,但是不能证明矿工拥有的数据是唯一的,也就是缺少 Filecoin 的 PoRep 提供的能力。这与网络面对的场景以及经济模型设计有关,0G Storage 只希望保证数据的可用,从矿工的奖励方式上限定了作恶是不能得到更多奖励的,所以整体机制上奏效。而 Filecoin 是根据算力高低给奖励,要面对的问题不一样。

从官网的第一篇 博客文章 中能更直观看到一些信息,0G 包含两个关键组成部分:0G Storage 和 0G DA,本质上在解决的就是 DA 的问题,主要是试图把这种 DA 能力用到 AI 场景中,所以分类到 AI 赛道了。项目背景上是一个分布式存储类的区块链项目。

0G 去年得到了 3 千万美元的种子轮融资,还是挺有资本的。

具体到工程实现上,可以看到 0G Storage 的 代码 基于 Conflux 的节点代码,在其之上做了一些功能开发:

PoRA 的工程实现部分就不深究了。

项目架构

刚才从项目背景的角度,只提到了 0G Storage 和 0G DA 两部分,除此之外,0G 这个项目还有两个角色,0G Chain 和 0G Compute Network。估计一开始的项目规划里没有,所以白皮书里没提。

0G Chain 是一个用 Cosmos SDK 开发的链节点(终于看到 Cosmos 的身影了),而且是直接用了 evmos 来兼容以太坊智能合约的做法:

0G Chain 的仓库最后一次提交代码是在 5 个月前,也许已经放弃了用 Cosmos SDK 的路线。因为有一个近期比较活跃的仓库 0g-geth,看起来是在做 Geth 的二次开发,通过集成预编译合约的方式,加入对 0G DA 的支持。

0G Compute Network 是真正和 AI 模型训练相关的部分,现在已经支持一些 预训练模型 的使用。用户层面的使用比较简单,类似于 OpenAI 的 SDK 一样,发起请求,得到响应,就是一个 Client 层的 SDK。

给 0G Compute Network 的模型提供算力的节点叫 Provider,代码仓库是 0g-serving-broker,代码仓库里有体现模型训练的代码,比如 finetune.py 这个脚本是基于 Transformer 做文本模型的微调,Docker 容器是直接基于 pytorch 2.5.1-cuda12.4-cudnn9-devel 的容器打包。

所以从 LLM 模型训练的角度看,0G 有一些工程方面的技术内容。只不过 0G 在干的事情是微调(Fine-tuning),也就是基于预训练(Pre-training)好的模型,进一步用较小的算力训练,达到执行某种特定任务的效果。而我们平时看到的 OpenAI 和 Grok 等大公司,动辄 1 TB tokens 的训练量,干的事情才是预训练。

比如 OpenAI 训练并开源出一个 GPT-3 模型(实际上没开源),那么 0G Compute Network 就是基于这个 GPT-3 模型,结合自己的语料进行一些微调,训练出一个自己版本的 GPT-3 模型。大概就是这个意思。

更准确一点说,0G Compute Network 是提供了一个训练的场地,结合了区块链相关的经济模型、奖励机制等交互,让用户可以给微调这件事情提供算力并获得收益,另一些用户可以使用微调之后的模型。

至于 Provider 与链上合约交互的部分,应该就好理解了。0G 是用 Solidity 写的合约 0g-serving-contract ,对合约的调用自然也是以太坊生态的那一套组件。而 0G 需要做的,就是把模型微调(训练)的结果,以及关于训练任务的分发、奖励记录、惩罚机制等,用合约来实现,然后在链下的算力节点上集成对合约的交互。

总结

综合来看,我需要改正一开始的态度,0G 是有一些技术含量在的,只不过更加侧重于工程方面的技术,无论是区块链方面的 DA,还是 AI 方面的模型微调,其实做的都不错,业务逻辑上已经能形成闭环。

但是说实话,写 0G 项目的分析,比之前写其他项目的分析,思路稍微不清晰一点,因为白皮书和文档都不是很完善,项目的技术路线又不是特别统一,所以没有非常好的资料自上而下的贯彻整个项目结构。不过经过以上内容的分析,我想应该已经刨析清楚了 0G 这个项目的技术情况。