Compare commits
No commits in common. "main" and "v20241122+3" have entirely different histories.
main
...
v20241122+
@ -26,7 +26,7 @@ jobs:
|
||||
subject: 博客部署启动
|
||||
to: 3145078758@qq.com
|
||||
from: Gitea Actions
|
||||
body: 博客自动化部署开始执行, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_number }} 查看执行日志
|
||||
body: 博客自动化部署开始执行, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_id }} 查看执行日志
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
commit_options: "--no-verify"
|
||||
token: "${{ secrets.TOKEN }}"
|
||||
- name: Call API to Update Caddy
|
||||
run: curl https://upd.linloir.cn/update/blog.linloir.cn
|
||||
run: curl https://upd.linloir.cn/update/blog.linloir.gitea.linloir.cn
|
||||
- name: Send mail On Success
|
||||
if: success()
|
||||
uses: https://github.com/dawidd6/action-send-mail@v3
|
||||
@ -86,4 +86,4 @@ jobs:
|
||||
subject: 博客部署失败
|
||||
to: 3145078758@qq.com
|
||||
from: Gitea Actions
|
||||
body: 博客自动化部署执行失败, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_number }} 查看执行日志
|
||||
body: 博客自动化部署执行失败, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_id }} 查看执行日志
|
||||
|
||||
@ -1,773 +0,0 @@
|
||||
---
|
||||
title: Coroutine 相关疑惑大模型问答记录
|
||||
date: 2025-03-17 21:09:21
|
||||
tags:
|
||||
- 后端
|
||||
- 编程语言
|
||||
categories:
|
||||
- 技术
|
||||
---
|
||||
|
||||
近日在看 python 的 asyncio 时突然想起了曾经对 C++ coroutine 以及 go 的 goroutine 的诸多未解疑惑。还记得当时有这些疑惑的时候,还没有 ChatGPT 或是 DeepSeek 这样方便的大模型解惑,反复查找各种资料最后因为时间原因还是没有深究,而如今 DeepSeek 已然如此发达,遂借此机会解答遗留在心中多年的疑惑,并以此贴记录。
|
||||
|
||||
## Round 1
|
||||
|
||||
**Q:**
|
||||
AsyncIO 或者说,Coroutine 是怎么实现,例如在 await 一个网络请求的时候,进程不会被内核挂起进入等待状态,而是可以继续正常被调度并且执行余下内容的?同时,当网络请求完成的时候,内核又是怎么让进程知道请求完成从而继续 await 后续未执行的内容的
|
||||
|
||||
**A:**
|
||||
|
||||
> 嗯,用户问的是AsyncIO或者说协程是怎么实现的,特别是在await一个网络请求的时候,进程不会被内核挂起,而是可以继续执行其他任务。然后当网络请求完成后,内核又是如何通知进程继续执行的。我需要详细解释AsyncIO和协程的工作原理,特别是事件循环、非阻塞I/O以及操作系统层面的机制,比如epoll或者kqueue。
|
||||
>
|
||||
> 首先,用户可能已经了解一些基本的异步编程概念,但想深入理解底层机制。需要分步骤解释,从协程、事件循环到非阻塞I/O和操作系统接口。可能需要提到用户态和内核态的区别,以及如何通过事件驱动的方式处理I/O操作,避免阻塞。
|
||||
>
|
||||
> 然后,用户可能对内核如何通知事件完成感兴趣,这里需要解释I/O多路复用机制,比如select、poll、epoll,特别是epoll在Linux中的工作方式。当有I/O事件发生时,内核如何通知应用程序,从而触发回调函数,恢复协程执行。
|
||||
>
|
||||
> 另外,需要区分进程、线程和协程的不同。协程是用户态的轻量级线程,由事件循环调度,而不是由操作系统内核调度。这样在遇到I/O操作时,可以挂起当前协程,执行其他任务,提高并发性能。
|
||||
>
|
||||
> 可能还需要提到Future和Task对象,在AsyncIO中如何管理异步操作的状态,以及await关键字如何挂起协程,让出控制权给事件循环,直到Future完成。
|
||||
>
|
||||
> 最后,总结整个流程,从发起网络请求到事件循环注册,到操作系统处理I/O,完成后的回调,恢复协程执行。确保解释清晰,技术细节准确,同时避免过于晦涩。
|
||||
|
||||
AsyncIO/Coroutine 的实现核心在于**用户态协作式调度**和**操作系统非阻塞I/O机制**的结合。以下是技术实现的关键点:
|
||||
|
||||
---
|
||||
|
||||
### 一、核心机制
|
||||
1. **事件循环 (Event Loop)**
|
||||
通过单线程轮询I/O事件,使用`epoll`(Linux)/`kqueue`(BSD)/`IOCP`(Windows)等系统调用实现高效事件监听:
|
||||
```python
|
||||
# 伪代码示例
|
||||
while True:
|
||||
events = epoll_wait(epoll_fd, timeout)
|
||||
for fd, event in events:
|
||||
callback = fd_to_callback[fd]
|
||||
callback()
|
||||
```
|
||||
|
||||
2. **非阻塞I/O**
|
||||
通过`fcntl(fd, F_SETFL, O_NONBLOCK)`设置socket为非阻塞模式,当I/O未就绪时立即返回`EWOULDBLOCK`错误,避免进程被挂起。
|
||||
|
||||
3. **协程调度**
|
||||
使用生成器(Python 3.5+的`async/await`)实现协程切换:
|
||||
```python
|
||||
async def fetch():
|
||||
await socket.read() # 让出控制权
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 二、内核交互细节
|
||||
1. **I/O准备阶段**
|
||||
```mermaid
|
||||
graph LR
|
||||
A[发起socket.read()] --> B[注册到epoll]
|
||||
B --> C[挂起当前协程]
|
||||
C --> D[事件循环继续运行其他协程]
|
||||
```
|
||||
|
||||
2. **内核通知机制**
|
||||
当数据到达时,网卡通过**中断**通知内核,内核将socket标记为就绪,epoll会检测到该事件并通知用户态。
|
||||
|
||||
3. **回调触发**
|
||||
事件循环收到内核通知后,通过`callback()`恢复对应协程的上下文继续执行。
|
||||
|
||||
---
|
||||
|
||||
### 三、性能关键指标
|
||||
1. **上下文切换成本**
|
||||
| 类型 | 切换成本 |
|
||||
|------------|---------------|
|
||||
| 进程切换 | ~1-10μs |
|
||||
| 线程切换 | ~0.5-2μs |
|
||||
| 协程切换 | ~0.1-0.3μs |
|
||||
|
||||
2. **I/O吞吐量对比**
|
||||
```math
|
||||
\text{Throughput} = \frac{\text{Requests}}{\text{Context Switch Time} + \text{I/O Latency}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 四、操作系统支持
|
||||
1. **Linux epoll**
|
||||
使用红黑树管理文件描述符,时间复杂度为$O(1)$的事件通知。
|
||||
|
||||
2. **BSD kqueue**
|
||||
支持更多事件类型过滤,但API更复杂。
|
||||
|
||||
3. **Windows IOCP**
|
||||
真正的异步I/O模型,与Overlapped I/O配合使用。
|
||||
|
||||
---
|
||||
|
||||
### 五、编程模型对比
|
||||
| 特性 | 多进程 | 多线程 | 协程 |
|
||||
|---------------|--------|--------|----------|
|
||||
| 内存占用 | 高 | 中 | 低 |
|
||||
| 切换成本 | 高 | 中 | 极低 |
|
||||
| 并发能力 | 低 | 中 | 高 |
|
||||
| 数据共享 | 困难 | 需同步 | 天然共享 |
|
||||
|
||||
---
|
||||
|
||||
### 六、典型工作流程
|
||||
1. 协程调用`await socket.read()`
|
||||
2. 事件循环将socket注册到epoll
|
||||
3. 当前协程挂起,保存上下文(PC/寄存器/栈)
|
||||
4. 事件循环继续执行其他协程
|
||||
5. 内核通知数据到达,epoll返回就绪事件
|
||||
6. 事件循环调用注册的回调函数
|
||||
7. 恢复挂起协程的上下文,继续执行
|
||||
|
||||
这种设计使得单线程即可实现数万并发连接(如Nginx的C10K解决方案),同时避免了内核态/用户态频繁切换的开销。
|
||||
|
||||
## Round 2
|
||||
|
||||
**Q:**
|
||||
协程是否需要保存当前栈帧
|
||||
|
||||
**A:**
|
||||
> 好的,用户问协程是否需要保存当前栈帧。我需要先回顾之前关于协程和AsyncIO的讨论,确保回答的一致性。用户可能已经了解协程的基本原理,现在想深入技术细节。
|
||||
>
|
||||
> 首先,协程的挂起和恢复确实需要保存栈帧。因为协程在用户态切换,没有内核的调度,所以必须手动保存和恢复上下文。栈帧保存了局部变量、函数调用链等信息,这对恢复执行至关重要。
|
||||
>
|
||||
> 接下来,我需要解释栈帧保存的具体内容,比如寄存器状态、局部变量、返回地址等。可能需要用例子说明,比如Python的生成器如何保存状态。同时,要区分不同语言的实现,比如Go的goroutine使用分段栈,而Python的协程可能依赖生成器。
|
||||
>
|
||||
> 用户可能还想知道为什么需要保存这些信息,以及不保存会有什么后果。比如,如果栈帧不保存,恢复时无法继续执行,导致数据错误或崩溃。此外,栈的动态增长如何处理,比如是否使用协程私有栈,避免栈溢出影响其他协程。
|
||||
>
|
||||
> 还要考虑性能方面,栈帧保存的开销如何,比如对比线程的上下文切换。可能提到协程切换的高效性,因为只在用户态操作,无需陷入内核。
|
||||
>
|
||||
> 另外,用户可能对具体实现机制感兴趣,比如setjmp/longjmp、ucontext库或者汇编手动切换上下文。不同语言和库的实现方式不同,但核心都是保存和恢复栈帧。
|
||||
>
|
||||
> 最后,需要总结栈帧保存的必要性,强调这是协程能够正确挂起和恢复的关键,确保用户理解其重要性。
|
||||
|
||||
|
||||
|
||||
协程的实现**必须完整保存当前栈帧**,这是实现正确挂起/恢复的核心机制。栈帧保存的完整性和实现方式直接决定了协程的性能和可靠性,具体技术细节如下:
|
||||
|
||||
---
|
||||
|
||||
### 一、栈帧保存的必要性
|
||||
| 组件 | 必须保存的原因 | 丢失后果 |
|
||||
|-----------------|---------------------------------|----------------------------|
|
||||
| 返回地址 | 确定恢复执行的位置 | 程序计数器错乱导致崩溃 |
|
||||
| 局部变量 | 维护函数调用上下文 | 数据不一致或计算错误 |
|
||||
| 寄存器状态 | 保证指令连续执行 | 寄存器污染导致未定义行为 |
|
||||
| 调用链信息 | 维护协程嵌套调用关系 | 栈回溯失败,调试信息丢失 |
|
||||
|
||||
---
|
||||
|
||||
### 二、栈帧保存的实现方式
|
||||
#### 1. 轻量级方案(Python等解释型语言)
|
||||
```python
|
||||
# 生成器协程的栈帧保存
|
||||
def coro():
|
||||
x = 42
|
||||
yield # 此处保存栈帧:
|
||||
# - 局部变量x
|
||||
# - 指令指针(IP)
|
||||
# - 当前栈指针(SP)
|
||||
print(x)
|
||||
|
||||
gen = coro()
|
||||
next(gen) # 触发栈帧保存
|
||||
```
|
||||
**特点**:
|
||||
- 通过代码对象(__code__)和帧对象(f_lasti)记录状态
|
||||
- 每个yield消耗约200字节内存
|
||||
|
||||
#### 2. 高性能方案(C/C++/Rust)
|
||||
```cpp
|
||||
// 使用ucontext库手动切换
|
||||
void coroutine_entry(void* arg) {
|
||||
while (true) {
|
||||
// 业务逻辑
|
||||
swapcontext(&ctx_current, &ctx_scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
// 上下文切换时自动保存:
|
||||
// - 所有通用寄存器 (rax, rbx...)
|
||||
// - 浮点寄存器 (xmm0-15)
|
||||
// - 栈指针 (rsp)
|
||||
// - 指令指针 (rip)
|
||||
// - 标志寄存器 (rflags)
|
||||
```
|
||||
**性能指标**:
|
||||
- 单次切换约23ns (x86) / 35ns (ARM)
|
||||
- 内存开销约2KB/协程
|
||||
|
||||
#### 3. 零拷贝方案(Go goroutine)
|
||||
```go
|
||||
// Go使用分段栈实现协程
|
||||
func coro() {
|
||||
var big [8192]byte // 触发栈扩容
|
||||
// ...
|
||||
}
|
||||
|
||||
// 运行时自动管理:
|
||||
// - 栈扩容时复制到新内存区域
|
||||
// - 使用连续栈时维护拷贝指针
|
||||
```
|
||||
**优势**:
|
||||
- 栈空间自动增长,无最大限制
|
||||
- 栈切换仅需修改寄存器,无需内存拷贝
|
||||
|
||||
---
|
||||
|
||||
### 三、栈帧保存的底层原理
|
||||
#### 1. 寄存器级保存
|
||||
```asm
|
||||
; x86-64上下文保存示例
|
||||
swapcontext:
|
||||
push rbp
|
||||
mov rbp, rsp
|
||||
push rbx
|
||||
push r12
|
||||
push r13
|
||||
push r14
|
||||
push r15
|
||||
mov [rdi + UC_MCONTEXT_GREGS + REG_RSP], rsp
|
||||
mov [rdi + UC_MCONTEXT_GREGS + REG_RIP], rcx
|
||||
; 共保存23个寄存器和浮点状态
|
||||
```
|
||||
|
||||
#### 2. 栈内存管理
|
||||
```c
|
||||
// 协程私有栈实现
|
||||
struct coroutine {
|
||||
void* stack; // 独立栈空间
|
||||
size_t stack_size;
|
||||
jmp_buf env; // 保存的寄存器集合
|
||||
};
|
||||
|
||||
// 切换时重置栈指针
|
||||
void switch_to(struct coroutine* from, struct coroutine* to) {
|
||||
_switch(&from->env, &to->env);
|
||||
// 汇编实现栈指针切换:
|
||||
// mov rsp, [to->env + REG_RSP]
|
||||
// ret
|
||||
```
|
||||
|
||||
#### 3. 栈溢出防护
|
||||
```rust
|
||||
// Rust async-std的栈保护机制
|
||||
async fn deep_recursion() {
|
||||
let guard = PageGuard::new(); // 分配保护页
|
||||
recursive_call().await;
|
||||
}
|
||||
|
||||
// 内存布局:
|
||||
// [ 可用栈空间 | RED ZONE (不可访问) | 保护页 ]
|
||||
// 访问保护页触发SIGSEGV,自动扩容栈
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 四、不同语言的实现对比
|
||||
| 语言 | 栈帧保存方式 | 切换开销 | 最大协程数 (8GB内存) |
|
||||
|------------|----------------------------|----------|----------------------|
|
||||
| Python | 生成器帧对象 | 120ns | ~500,000 |
|
||||
| Go | 分段栈/连续栈 | 35ns | ~10,000,000 |
|
||||
| C++ | ucontext/手动汇编 | 23ns | ~5,000,000 |
|
||||
| Java(Loom) | 虚拟线程栈镜像 | 45ns | ~2,000,000 |
|
||||
| Rust | async/await状态机编译时展开 | 18ns | ~8,000,000 |
|
||||
|
||||
---
|
||||
|
||||
### 五、特殊场景处理
|
||||
1. **动态栈扩展**
|
||||
当协程调用深度递归时:
|
||||
```c
|
||||
void coro() {
|
||||
char buf[1024];
|
||||
await read(); // 栈空间不足时:
|
||||
// 1. 分配新栈
|
||||
// 2. 拷贝旧栈数据
|
||||
// 3. 修改栈指针
|
||||
}
|
||||
```
|
||||
|
||||
2. **跨栈引用保护**
|
||||
C++20协程要求:
|
||||
```cpp
|
||||
task<void> unsafe_coro() {
|
||||
int local = 42;
|
||||
co_await something(); // 挂起点之后
|
||||
print(local); // 可能引用已释放栈!
|
||||
}
|
||||
// 正确做法:使用shared_ptr持有跨栈数据
|
||||
```
|
||||
|
||||
3. **垃圾回收协同**
|
||||
Go的逃逸分析确保协程栈安全:
|
||||
```go
|
||||
func coro() {
|
||||
x := 42
|
||||
go func() {
|
||||
println(x) // x逃逸到堆
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 结论
|
||||
协程必须完整保存当前栈帧,这是实现正确执行流控制的基石。现代运行时通过寄存器快照、独立栈空间、智能内存管理等技术,在保证安全性的同时将切换开销控制在纳秒级。不同语言根据自身特性选择最优实现策略,如Go的分段栈、Rust的零成本抽象等,最终目标都是在用户态实现接近线程的编程模型,同时获得更高的并发性能。
|
||||
|
||||
## Round 3
|
||||
|
||||
**Q:**
|
||||
我好奇的一点是:如果函数 `A` 中调用了 `b = B()`,而 `B` 中调用了 `await post_http_request()`,我理解在 `post_http_request` 中会在发送完 http 请求后对 `post_http_request` 函数的栈帧内所有内容进行保存,然后返回到 `B` 中,由于 `B` 中调用了 `await`,则会保存 B 的栈帧,最后返回到 `A` 中继续执行,对吗?
|
||||
|
||||
我的疑问在于,在你前面给出的例子中,只保存了寄存器相关的值而没有保存栈上存储的内容,如果在这种情况下,`A` 函数继续执行,可能会覆盖原先 `B` 和 `post_http_request` 栈上的内容,这是否会导致切换失败?还是说,栈帧保存也会保存栈上的数据?这样不会消耗额外的时间来拷贝内存吗?
|
||||
|
||||
**A:**
|
||||
> 嗯,用户的问题是关于协程在挂起时如何保存栈帧的。他举了一个例子,函数A调用B,B中调用了await post_http_request()。用户想知道,当post_http_request发送完HTTP请求后,是否会保存它的栈帧,然后返回到B,再保存B的栈帧,最后回到A继续执行。用户担心如果只保存寄存器而没有保存栈上的数据,A继续执行可能会覆盖B和post_http_request的栈内容,导致切换失败。或者是否需要保存栈上的数据,这样会不会有内存拷贝的开销。
|
||||
>
|
||||
> 首先,我需要回顾之前关于协程保存栈帧的讨论。之前提到协程必须保存当前栈帧,包括寄存器、局部变量等。用户可能误解了栈帧保存的方式,认为只保存寄存器而没有保存栈上的数据,但实际上,正确的实现需要保存整个栈帧的内容,或者使用独立的栈空间来避免被覆盖。
|
||||
>
|
||||
> 接下来,需要解释协程的不同实现方式。比如,在Python这样的生成器协程中,每个协程可能共享同一个栈,通过生成器对象保存局部变量和指令指针,但可能不保存整个栈帧。而在其他语言如Go或Rust中,每个协程可能有自己的独立栈,切换时切换栈指针,这样就不会被其他协程覆盖。
|
||||
>
|
||||
> 用户提到的例子中,当post_http_request被await挂起时,协程需要保存当前的执行状态,包括B函数和post_http_request的栈数据。如果所有协程共享同一个栈,那么当A继续执行时,确实可能覆盖之前的栈内容,导致数据损坏。因此,正确的实现需要每个协程有自己的栈空间,或者在挂起时将栈数据拷贝到堆中保存。
|
||||
>
|
||||
> 需要解释不同语言的实现策略。例如,Go使用分段栈或连续栈,每个goroutine有自己的栈,切换时只需切换栈指针,无需拷贝数据。而Python的协程可能使用生成器,保存必要的状态而非整个栈,适用于浅层调用,但深层嵌套可能有问题。
|
||||
>
|
||||
> 用户担心内存拷贝的开销,需要说明高性能实现(如Go、Rust)如何通过独立栈来避免拷贝,只在必要时增长栈或分配新栈,从而减少开销。同时,解释栈切换的机制,比如修改栈指针寄存器,使得每个协程使用不同的内存区域,互不干扰。
|
||||
>
|
||||
> 可能还需要提到,当协程挂起时,其栈帧的数据必须保留在内存中,直到恢复执行。如果使用独立栈,这些数据不会被其他协程覆盖,因此无需拷贝。而如果共享栈,则需要将栈数据保存到堆中,这会带来拷贝开销,但现代实现通常避免这种方式,选择为每个协程分配独立栈空间。
|
||||
>
|
||||
> 总结用户的疑问点:是否保存栈数据,如何避免覆盖,以及性能影响。需要明确回答,协程实现中栈帧的保存方式,不同策略的优缺点,以及如何解决用户提到的覆盖问题。
|
||||
|
||||
|
||||
|
||||
协程的实现中,**栈帧保存机制**是保证正确性的核心,但具体实现方式在不同语言/框架中有显著差异。针对你的场景,我们分层次解析:
|
||||
|
||||
---
|
||||
|
||||
### 一、场景还原与问题本质
|
||||
```python
|
||||
async def A():
|
||||
b = await B() # <- 挂起点1
|
||||
|
||||
async def B():
|
||||
await post_http_request() # <- 挂起点2
|
||||
```
|
||||
|
||||
当执行到`post_http_request()`中的`await`时:
|
||||
1. **调用链**:`A → B → post_http_request`
|
||||
2. **挂起顺序**:从`post_http_request`开始逐层向上传递`await`
|
||||
3. **关键问题**:如何保存整个调用链的上下文,避免栈数据被覆盖?
|
||||
|
||||
---
|
||||
|
||||
### 二、两种核心实现策略
|
||||
|
||||
#### 1. **共享栈模式 (Stackful Coroutines)**
|
||||
- **代表实现**:Go goroutine、C++ Boost.Coroutine
|
||||
- **核心机制**:
|
||||
- 每个协程拥有**独立栈空间**(初始大小通常为2KB-8KB)
|
||||
- 切换时**直接切换栈指针寄存器**(如x86的`rsp`)
|
||||
- **无需拷贝栈数据**,通过内存隔离避免覆盖
|
||||
|
||||
```c
|
||||
// Go调度器伪代码
|
||||
func schedule() {
|
||||
// 保存当前协程的rsp
|
||||
old_gobuf.rsp = get_rsp()
|
||||
// 切换到目标协程的栈
|
||||
set_rsp(new_gobuf.rsp)
|
||||
ret
|
||||
}
|
||||
```
|
||||
|
||||
- **内存布局**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 协程X的栈 │ ← rsp_x
|
||||
├─────────────┤
|
||||
│ 协程Y的栈 │ ← rsp_y
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
- **优势**:零拷贝,切换极快(约35ns)
|
||||
- **代价**:每个协程需预分配独立栈空间
|
||||
|
||||
#### 2. **状态机模式 (Stackless Coroutines)**
|
||||
- **代表实现**:Python AsyncIO、Rust async/await
|
||||
- **核心机制**:
|
||||
- 通过编译器将协程转换为**状态机**
|
||||
- **所有局部变量提升到堆分配的结构体**
|
||||
- 栈帧数据**显式保存到堆内存**
|
||||
|
||||
```rust
|
||||
// Rust async编译后伪代码
|
||||
struct B_Future {
|
||||
state: u32,
|
||||
local_vars: B_Locals, // 包含所有局部变量
|
||||
}
|
||||
|
||||
struct A_Future {
|
||||
b_future: B_Future,
|
||||
local_b: Option<B_Result>,
|
||||
}
|
||||
```
|
||||
|
||||
- **内存布局**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 主线程栈 │ ← 仅存状态机指针
|
||||
├─────────────┤
|
||||
│ 堆内存 │ ← 存储所有协程状态
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
- **优势**:内存占用低(无独立栈)
|
||||
- **代价**:局部变量访问需间接寻址(约多1-3 CPU周期)
|
||||
|
||||
---
|
||||
|
||||
### 三、关键问题解答
|
||||
|
||||
#### Q1:为何不会发生栈数据覆盖?
|
||||
- **共享栈模式**:每个协程有独立栈空间,物理内存不重叠
|
||||
- **状态机模式**:所有协程共享主线程栈,但局部变量已提升到堆,栈上仅存指针
|
||||
|
||||
#### Q2:栈数据是否需要显式拷贝?
|
||||
- **共享栈模式**:无需拷贝,切换时直接跳转栈指针
|
||||
- **状态机模式**:编译器自动将局部变量"移动"到堆结构体,相当于隐式拷贝
|
||||
|
||||
#### Q3:性能影响对比
|
||||
| 指标 | 共享栈模式 | 状态机模式 |
|
||||
|---------------------|----------------|----------------|
|
||||
| 单次切换延迟 | ~35ns | ~18ns |
|
||||
| 内存开销/协程 | 2KB-8KB | 200B-2KB |
|
||||
| 局部变量访问速度 | 直接栈访问 | 间接堆访问 |
|
||||
| 最大协程数 (8GB) | ~1,000,000 | ~10,000,000 |
|
||||
|
||||
---
|
||||
|
||||
### 四、具体语言实现剖析
|
||||
|
||||
#### 1. **Python AsyncIO**
|
||||
- **实现方式**:生成器+状态机
|
||||
- **栈保存机制**:
|
||||
- 每个`async def`函数转换为生成器
|
||||
- 局部变量保存在生成器对象的`gi_frame`中
|
||||
- **不保存完整栈**,依赖事件循环调度
|
||||
|
||||
```python
|
||||
async def B():
|
||||
x = 42 # 存储在生成器对象的x属性
|
||||
await post_http_request()
|
||||
print(x) # 从生成器对象读取x
|
||||
|
||||
# 编译后伪代码
|
||||
class B_Generator:
|
||||
def __init__(self):
|
||||
self.x = None
|
||||
self.state = 0
|
||||
|
||||
def __next__(self):
|
||||
if self.state == 0:
|
||||
self.x = 42
|
||||
self.state = 1
|
||||
return post_http_request()
|
||||
elif self.state == 1:
|
||||
print(self.x)
|
||||
```
|
||||
|
||||
- **缺陷**:无法处理深层递归调用(栈深度受限)
|
||||
|
||||
#### 2. **Rust async/await**
|
||||
- **实现方式**:状态机 + 零成本抽象
|
||||
- **栈保存机制**:
|
||||
- 编译器将每个`async fn`转换为结构体
|
||||
- 所有局部变量成为结构体字段
|
||||
- 通过`Pin`保证内存位置固定
|
||||
|
||||
```rust
|
||||
async fn B() {
|
||||
let x = 42; // 存储在B_Future结构体
|
||||
post_http_request().await;
|
||||
println!("{}", x);
|
||||
}
|
||||
|
||||
// 编译后伪代码
|
||||
struct B_Future {
|
||||
state: u32,
|
||||
x: i32,
|
||||
http_request: PostHttpRequestFuture,
|
||||
}
|
||||
|
||||
impl Future for B_Future {
|
||||
fn poll(...) {
|
||||
match self.state {
|
||||
0 => {
|
||||
self.x = 42;
|
||||
self.state = 1;
|
||||
self.http_request.poll()
|
||||
}
|
||||
1 => {
|
||||
println!("{}", self.x);
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **优势**:无堆分配(可通过`no_std`运行)
|
||||
|
||||
---
|
||||
|
||||
### 五、实际内存布局示例
|
||||
|
||||
#### 场景:`A → B → post_http_request`挂起
|
||||
- **共享栈模式 (Go)**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 协程A栈 │ ← 保存B的返回地址和局部变量
|
||||
├─────────────┤
|
||||
│ 协程B栈 │ ← 保存post_http_request的上下文
|
||||
└─────────────┘
|
||||
```
|
||||
- 每个协程栈独立,切换时硬件自动隔离
|
||||
|
||||
- **状态机模式 (Rust)**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 主栈 │ → 仅存储当前活跃Future指针
|
||||
├─────────────┤
|
||||
│ 堆内存 │ ← A_Future { B_Future { PostHttpRequest_Future } }
|
||||
└─────────────┘
|
||||
```
|
||||
- 所有状态嵌套存储在堆上
|
||||
|
||||
---
|
||||
|
||||
### 六、总结
|
||||
- **共享栈协程**通过独立栈空间物理隔离数据,**无需拷贝但预分配内存**
|
||||
- **状态机协程**通过编译期代码变换将栈数据提升到堆,**牺牲局部变量访问速度换取高密度**
|
||||
- 现代语言(如Go/Rust)通过精细设计,使得协程切换开销可控制在**20-50ns**,完全满足高并发需求
|
||||
|
||||
## Round 4
|
||||
|
||||
**Q:**
|
||||
所以你的意思是协程函数会有自己独立的栈空间是吗?这样的话,调用协程函数是否是和调用不同函数时不同,例如需要额外切换 rsp 到对应协程函数所在的栈上?
|
||||
|
||||
**A:**
|
||||
> 好的,我现在需要回答用户关于协程函数是否拥有独立栈空间以及调用协程函数是否需要切换栈指针的问题。用户之前的对话已经涉及协程的实现机制,包括共享栈和状态机模式的区别,以及栈帧保存的必要性。用户现在想确认协程是否使用独立栈空间,以及调用时是否需要切换栈指针。
|
||||
>
|
||||
> 首先,我需要回顾之前的讨论。用户之前的问题集中在协程如何保存栈帧,避免数据被覆盖。我的回答提到了共享栈和状态机两种模式,其中共享栈模式每个协程有独立栈空间,而状态机模式则通过编译器将局部变量提升到堆中。用户现在的问题直接询问协程是否有独立栈空间,以及是否需要切换rsp寄存器。
|
||||
>
|
||||
> 接下来,我需要明确回答用户的问题:是的,某些协程实现(如共享栈模式)确实使用独立栈空间,并在调用时切换rsp。而状态机模式(如Rust的async/await)则不依赖独立栈,而是将状态保存在堆上,因此不需要切换栈指针。
|
||||
>
|
||||
> 需要分点详细解释两种模式的区别,特别是栈的使用和切换机制。同时,要举例说明不同语言中的实现,如Go的goroutine使用共享栈,而Rust使用状态机模式。此外,需要比较两者的优缺点,如性能、内存占用和适用场景。
|
||||
>
|
||||
> 可能还需要涉及技术细节,如栈指针切换的具体操作,如何通过修改rsp寄存器实现协程切换,以及状态机模式如何避免栈切换。同时,要解答用户可能的疑问,比如为什么有的实现选择共享栈,有的选择状态机,各自的权衡是什么。
|
||||
>
|
||||
> 需要确保回答准确,避免错误。例如,Go的协程确实使用分段栈或连续栈,而Rust的async/await在编译时转换为状态机,不依赖独立栈空间。同时,要指出切换栈指针的性能影响,以及状态机模式在内存使用上的优势。
|
||||
>
|
||||
> 最后,总结两种模式的适用场景,帮助用户理解不同协程实现的优缺点,以便根据具体需求选择合适的模型。
|
||||
|
||||
|
||||
|
||||
协程是否拥有独立栈空间取决于具体的实现模型,不同编程语言和框架采用了不同的策略。以下是两种主流实现方式的详细对比:
|
||||
|
||||
---
|
||||
|
||||
### **一、共享栈协程 (Stackful Coroutines)**
|
||||
**代表实现**:Go goroutine、Lua coroutine、C++ Boost.Coroutine
|
||||
**核心特性**:每个协程拥有独立的栈空间,切换时需要修改栈指针寄存器(如x86的`rsp`)。
|
||||
|
||||
#### **1. 栈空间管理**
|
||||
- **独立栈分配**:
|
||||
```go
|
||||
// Go运行时为每个goroutine预分配栈(初始2KB)
|
||||
go func() { // 创建新协程
|
||||
myFunc() // 在独立栈中运行
|
||||
}()
|
||||
```
|
||||
- **栈增长机制**:
|
||||
- **分段栈**(旧版Go):栈不足时分配新段,通过`morestack`检测
|
||||
- **连续栈**(现代Go):栈不足时整体复制到更大的内存区域
|
||||
|
||||
#### **2. 栈指针切换**
|
||||
- **切换原理**:
|
||||
```asm
|
||||
; x86-64协程切换伪代码
|
||||
swapcontext:
|
||||
push rbp
|
||||
mov rbp, rsp
|
||||
; 保存当前寄存器到旧协程上下文
|
||||
mov [old_ctx + RSP_OFFSET], rsp
|
||||
; 加载新协程的栈指针
|
||||
mov rsp, [new_ctx + RSP_OFFSET]
|
||||
; 恢复新协程的寄存器
|
||||
pop rbp
|
||||
ret
|
||||
```
|
||||
- **性能指标**:
|
||||
- 切换耗时:约35-50ns(包括保存/恢复16个通用寄存器+浮点寄存器)
|
||||
- 内存开销:每个协程至少2KB(Go) 到 8KB (C++)
|
||||
|
||||
#### **3. 调用链示例**
|
||||
```go
|
||||
func A() {
|
||||
B() // 切换到B的栈
|
||||
}
|
||||
|
||||
func B() {
|
||||
C() // 切换到C的栈
|
||||
}
|
||||
```
|
||||
**内存布局**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 主线程栈 │ ← 0x7ffeefbff000
|
||||
├─────────────┤
|
||||
│ Goroutine A栈 │ ← 0xc000032000
|
||||
├─────────────┤
|
||||
│ Goroutine B栈 │ ← 0xc0000ac000
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **二、无栈协程 (Stackless Coroutines)**
|
||||
**代表实现**:Rust async/await、Python AsyncIO、JavaScript async
|
||||
**核心特性**:协程共享主线程栈,通过状态机管理上下文,无需切换栈指针。
|
||||
|
||||
#### **1. 实现原理**
|
||||
- **状态机转换**:
|
||||
```rust
|
||||
async fn example() {
|
||||
let a = 1; // 状态0
|
||||
some_io().await; // 状态1
|
||||
println!("{}", a);
|
||||
}
|
||||
```
|
||||
编译后展开为:
|
||||
```rust
|
||||
struct ExampleFuture {
|
||||
state: u32,
|
||||
a: i32,
|
||||
io_future: SomeIOFuture,
|
||||
}
|
||||
```
|
||||
- **栈共享机制**:
|
||||
- 所有局部变量存储在堆上的Future对象中
|
||||
- 主线程栈仅保留当前活跃Future的指针
|
||||
|
||||
#### **2. 内存布局**
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 主线程栈 │
|
||||
│-------------│ ← 仅存储临时变量和返回地址
|
||||
│ Future对象指针 →
|
||||
├─────────────┤
|
||||
│ 堆内存 │
|
||||
│-------------│ ← Future对象包含所有协程状态
|
||||
│ a=1 │
|
||||
│ io_future │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
#### **3. 性能对比**
|
||||
| 指标 | 共享栈协程 | 无栈协程 |
|
||||
|---------------------|----------------|----------------|
|
||||
| 单次切换开销 | ~35ns | ~18ns |
|
||||
| 内存开销/协程 | 2KB-8KB | 200B-2KB |
|
||||
| 最大协程数 (8GB) | ~1,000,000 | ~10,000,000 |
|
||||
| 局部变量访问 | 直接栈访问 | 间接堆访问 |
|
||||
|
||||
---
|
||||
|
||||
### **三、关键问题解答**
|
||||
#### **Q1:协程调用是否需要切换栈指针?**
|
||||
- **共享栈协程**:**需要**显式切换`rsp`寄存器
|
||||
```asm
|
||||
; Go调度器切换goroutine时的关键操作
|
||||
MOVQ gobuf_sp(BX), SP // 加载新协程的栈指针
|
||||
```
|
||||
- **无栈协程**:**无需切换**,所有状态在堆上管理
|
||||
|
||||
#### **Q2:如何避免栈数据覆盖?**
|
||||
- **共享栈**:物理内存隔离,每个协程栈独立分配
|
||||
```c
|
||||
// Go连续栈扩容时的复制操作
|
||||
old_stack = current_stack;
|
||||
new_stack = malloc(new_size);
|
||||
memcpy(new_stack, old_stack, old_size);
|
||||
free(old_stack);
|
||||
```
|
||||
- **无栈协程**:局部变量存储在堆对象中,与调用链解耦
|
||||
|
||||
#### **Q3:为何无栈协程性能更高?**
|
||||
- **寄存器压力小**:无需保存/恢复完整寄存器上下文
|
||||
- **缓存友好**:状态机结构通常更紧凑,提高缓存命中率
|
||||
- **零内存拷贝**:状态切换通过指针跳转完成
|
||||
|
||||
---
|
||||
|
||||
### **四、语言实现对比**
|
||||
#### **1. Go (共享栈)**
|
||||
- **栈增长策略**:
|
||||
- 初始2KB,每次翻倍直至32KB,之后按25%增长
|
||||
- 栈收缩机制防止内存泄漏
|
||||
- **调度优势**:
|
||||
```go
|
||||
// Go调度器在系统线程间均衡goroutine
|
||||
runtime.GOMAXPROCS(4) // 使用4个OS线程
|
||||
```
|
||||
|
||||
#### **2. Rust (无栈)**
|
||||
- **零成本抽象**:
|
||||
```rust
|
||||
// async函数编译为状态机,无额外运行时开销
|
||||
let fut = async { 1 };
|
||||
// 等效于:
|
||||
struct MyFuture { /* 状态字段 */ }
|
||||
impl Future for MyFuture {
|
||||
type Output = i32;
|
||||
fn poll(...) { ... }
|
||||
}
|
||||
```
|
||||
- **无堆分配优化**:
|
||||
```rust
|
||||
// 通过`#[async_trait]`实现动态分发
|
||||
#[async_trait]
|
||||
trait MyTrait {
|
||||
async fn method(&self);
|
||||
}
|
||||
```
|
||||
|
||||
#### **3. Python (生成器式无栈)**
|
||||
- **生成器协程**:
|
||||
```python
|
||||
async def coro():
|
||||
await asyncio.sleep(1)
|
||||
# 转换为:
|
||||
class Coro:
|
||||
def __init__(self):
|
||||
self.state = 0
|
||||
def __next__(self):
|
||||
if self.state == 0:
|
||||
self.state = 1
|
||||
return sleep(1)
|
||||
```
|
||||
- **缺陷**:无法深度递归(受解释器栈限制)
|
||||
|
||||
---
|
||||
|
||||
### **五、总结**
|
||||
- **共享栈协程**:通过独立栈空间和`rsp`切换实现真并发,适合需要深度递归或复杂调用链的场景,以Go为代表。
|
||||
- **无栈协程**:通过状态机和堆存储实现高密度协程,以Rust/Python为代表,适合I/O密集型和高并发场景。
|
||||
- **选择依据**:
|
||||
- 需要**高性能和可控内存** → Rust无栈协程
|
||||
- 需要**简单并发模型和深度调用** → Go共享栈协程
|
||||
- **快速开发和高层抽象** → Python无栈协程
|
||||
|
||||
两种模型各有优劣,现代运行时(如.NET)甚至支持混合模式,开发者应根据具体需求选择最合适的协程实现。
|
||||
@ -1,32 +0,0 @@
|
||||
---
|
||||
title: 年终总结 - 2019
|
||||
date: 2019-12-30 23:47:36
|
||||
tags:
|
||||
- 年终总结
|
||||
- 自言自语
|
||||
categories:
|
||||
- 杂思
|
||||
---
|
||||
|
||||
还有24h,2019就结束了
|
||||
本来想说,今年是过去17年中最糟糕的一年
|
||||
但仔细想想,大概之后的每一年都会是这样,没什么意思,所以也不想写很多情绪化的东西,就随便说说好了。
|
||||
|
||||
我想,这一年我学到的唯一的道理,就是
|
||||
“在学校之外的很多事情,自有他们发展的规律。有很多事情,努力除了能让自己不后悔以外,什么也改变不了。”
|
||||
|
||||
今年第一次真正认识到人的渺小,在意外、灾难、疾病面前,人的力量是薄弱的,甚至,连一点挣扎的机会都没有。在无锡侧翻的高架桥下的人也许永远也料不到自己会被180吨的轧钢要去了性命,重庆广场305楼下的两名学生也绝不会想到自己会因为一具没了灵魂的躯壳而永远停留在17岁。就像任何人都无法躲避死亡,我们所有的观念,行为无非是在改变着我们走向终点的路途罢了。
|
||||
|
||||
我自己的经历也不例外。我以为自己可以做到,我以为只要我倾注足够的精力,我祈祷,我等待,事情就可以有所改变,甚至还凭着侥幸心理还在知乎上写了一篇回答,给别人讲方法,提帮助。
|
||||
就像在用沙子搭积木,一粒一粒地把沙子垒起来,注水,看它们黏在一起,即便我知道当水全部蒸发的时候,所有的努力都会化为泡影,但我不信这个邪,我骗自己,只要我不断地修补、维护,水就永远不会蒸干,于是一眨眼便是三年。
|
||||
但突然有一天,一阵风吹过,带走了不起眼角落里最后一滴水,什么也没有了。
|
||||
|
||||
曾经看过一个理论,在高维的空间里,时间像长、宽、深那样是既定的,也就是说我们现在经历的,过去发生的,和即将发生的一切,都是存在的。
|
||||
我想了很久,难道这不就意味着混吃等死吗?
|
||||
也还是有一些差别的。对于处于三维的我们,未来之所以是未来,正是因为它的不可预见性,就像看不到的立方体的背面那样。即使我能猜到它背面的样子,即使翻转它不会对它有一分一毫的改变,但我仍然对它抱有好奇,好奇和我想象的有什么差别。
|
||||
努力也许改变不了结果,但努力的过程会创造回忆。
|
||||
站在2019的终点看之前的365个日夜,有开心,有难过,有绝望。我看到自己因为一句话而高兴,又因为一些事而失落;我看到自己疯癫的样子,也看到自己沉默的样子;我看到一些人因为我而高兴,也看到一些人被我波动的情绪所影响...所有的这一切构筑了我来到这世界第17年的回忆,我的2019年。
|
||||
“如果我什么也改变不了,那我就在这个世界里创造一些回忆吧。”
|
||||
(正文完)
|
||||
|
||||
P.S.过去的一年因为自己的原因情绪波动很大,很多时候都因为失控没有去注意正常的情绪表达,多多少少都对身边的人产生了影响。借此机会,我希望向很多人道歉,真的对不起。
|
||||
@ -1,19 +0,0 @@
|
||||
---
|
||||
title: 年终总结 - 2020
|
||||
date: 2021-01-11 23:59:02
|
||||
tags:
|
||||
- 年终总结
|
||||
- 自言自语
|
||||
categories:
|
||||
- 杂思
|
||||
---
|
||||
|
||||
准切地说,这篇年终总结来得有些晚了,晚了一个月十一天二十三小时五十九分钟。但过去的这一年是那么的不可思议,那么的跌宕起伏、刻骨铭心,倘若能用一篇年终总结给这样的一年画上一个句号,便是再好不过的了。
|
||||
|
||||
公元贰零贰零年,我来到这个世界的第十八年。年初,武汉疫情爆发,所有的一切都仿佛按下了暂停键,电影取消了,商场关闭了,学校放假了,就连那么多年来从未改变的高考时间也史无前例地推迟了。在这些与自己独处的漫长日夜里,很难得地,我慢慢开始和自己交流,和过去交流,执念被慢慢放下,伤痕开始逐渐愈合,记忆的回声终于不再萦绕,一切都在一个梦醒的早晨恢复正轨。年中,高考是生活赠与我的第一个成人礼,高考结束后的第二天,我结束了一个人面对生活的日子,我有了陪伴我、鼓励我、支持我、给我惊喜的女朋友,我收获了让我感到温暖而幸福的爱情,这是生活赠与我的第二个成人礼。第三个成人礼,是中大录取的通知讯息。当我看到短信的时候,我想到了过去三年我做的所有的事情,我想到了高考后的不甘,想到了她,想到了未来,我之后所有的生活都在那一刻,因为那一个短信而走向了一个崭新的方向。年末,我来到了大学,遇到了和蔼的老师,友好的舍友,我犹记得每一次和所爱之人见面时那种溢于言表的喜悦与激动,犹记得期末周复习的紧张与担心。而这个学期所有的故事,也都以虽不差但仍不尽如意的期末成绩画上了完整的句号。
|
||||
|
||||
过去这一年,我经历了悲伤,感受了快乐,收获了爱情,也开始了我成年后的第一年人生。2019快结束的时候,我认为生活已经不能更糟了,然而,2020开年时便驳倒了我这样的想法。但过完整整一年,虽然生活在很多时候变得更糟了,但在更多的时候,变得更加地与以往不一样了——我们认识到生命的宝贵,看到信仰的光芒与责任的力量,我们开始与身边的人联系起来,我们和家人们一起说话,和朋友散心,这一切,在2020都显得那么不一样。
|
||||
|
||||
可以说,从去年开始,我才真正开始生活。
|
||||
|
||||
转眼就要离开鼠年,希望我们所有人都能在新的一年,再出发。
|
||||
@ -1,51 +0,0 @@
|
||||
---
|
||||
title: 年终总结 - 2021
|
||||
date: 2022-01-01 00:03:12
|
||||
tags:
|
||||
- 年终总结
|
||||
- 自言自语
|
||||
categories:
|
||||
- 杂思
|
||||
---
|
||||
|
||||
很快啊,又是一年过完了。
|
||||
不知道什么时候开始,感觉一年的长度开始越来越短,短到年初念叨的想见的人到了年末也还是没有见,短到年初开始背的六级单词到了年末也没背下几个,短到年初那些信誓旦旦在今天看来不过又是明年一定...
|
||||
|
||||
如果要用一个词来描述2021,我认为“改变”是最贴切不过的。
|
||||
|
||||
年初的时候,确诊了反流性食管炎。
|
||||
本来以为会像以往的胃疼那样不过是短时间生活习惯的影响,过一阵子就会自己康复,但没想到即便是做了胃镜开了药也仍然没有阻止它一直折磨我到今天。为了缓解这个问题对生活的影响,很多习惯都不得不改变,其中就包括戒掉我喝了六七年的奶茶和几乎所有的甜食。
|
||||
到了年中,就是转专业。
|
||||
本是父母随心的一问,却没想让本来准备努力保研遥感的我重新思考了转专业的可能性,并在截止日期当天提交了申请。
|
||||
在转专业的过程中发生了很多其他的事情,并且还要同时准备着转专业失败可能面临的绩点断崖下滑以及忍受每晚胃病的折磨,使得那一个月的每一天都无比难熬。
|
||||
也正是在这个过程中我改变了对曾经很亲密的人的看法,并最终选择了去和平结束上一段关系。
|
||||
|
||||
除了这些明显的生活上的改变以外,今年更多的是一些想法和看法上的改变。
|
||||
|
||||
我曾经以为,我身边的好友尽管可能会对一些有争议的事件持有不同的看法——我始终认为这一点十分正常并可以理解,但至少大家在发表自己观点的时候会是理性的、客观的或最起码是有理且有据的。
|
||||
但当我看到曾经标榜自己理智的人在社交媒体上发出匪夷所思的话语,当我还没开口聊就被曾经亲近的好友一句“别说了,再说我就要打拳了”这一原话给堵住的时候,当我提到我希望能付出更多精力在学业上以能够有能力会报父母却得到“我不能理解”的答复时
|
||||
我感到的不仅仅是惊愕、诧异,更多的是失望和害怕,对他们而言,事件不再是一个被客观评价的现象和引发人思考的主体,而是变成被抒发自己偏激想法的载体。
|
||||
于是我选择了改变,选择了去远离和保持沉默,远离那些匪夷所思的看法,并避免自己成为这样的家伙。
|
||||
|
||||
今年改变的同样也有着对事的看法。
|
||||
无论是对未来的规划,还是对专业的理解,甚至是平日里的目标,都随着转专业的完成被全部刷新。
|
||||
在2021,我看到了更多我所没有学习的知识,看到了更多厉害的人,看到了未来更多的可能性。
|
||||
|
||||
当然,过去的一年远远不止这些
|
||||
第一次拿到奖学金(说不定是最后一次)
|
||||
第一次在b站达到单日上万的播放量
|
||||
第一次收到实习邀请......
|
||||
所有的这些,都发生在这过去的365天里,都成为了构筑我2021的片段。
|
||||
|
||||
今天,过去的一年已成定局,新的一年就在我写下这些文字的时候就已然悄无声息地开始。如果要说对2022有什么展望的话...
|
||||
|
||||
我希望,
|
||||
新的一年,
|
||||
我和我所爱的所有人,
|
||||
平安,健康,快乐。
|
||||
就这样,就够了。
|
||||
|
||||
最后,
|
||||
感谢过去的一年有你们陪伴。
|
||||
真的,
|
||||
谢谢你们。
|
||||
@ -1,39 +0,0 @@
|
||||
---
|
||||
title: 年终总结 - 2022
|
||||
date: 2023-01-01 00:00:00
|
||||
tags:
|
||||
- 年终总结
|
||||
- 自言自语
|
||||
categories:
|
||||
- 杂思
|
||||
---
|
||||
|
||||
2022.12.31 21:00
|
||||
今年只剩下一场电影的时间了。
|
||||
|
||||
过去一年,如果要用一个词语概括的话,那我想应当是“遇见”吧。
|
||||
|
||||
其实,关于这个词语,在过去的一年里的无数个瞬间,我都有过构想。
|
||||
之所以选择“遇见”,因为遇见有着往前走的意思,当我们没有再停留在原地的时候,当我们不在回头看的时候,才会看见那些原本看不见的风景,遇见那些原本碰不到的人们。
|
||||
|
||||
过去的一年里,我确实有在向前走,如果更加贴切一些地说,应当是在向前狂奔。
|
||||
年初的时候,我终于决定选择试着加入Matrix的大家庭,这个决定成为了我今年做得最正确的决定之一。在Matrix,我遇见了真正优秀,有梦想和追求的人们,我看到了与宿舍舍友待白昼如黑夜迥然不同的生活态度,我开始寻着前辈的步子,发展自己热爱的方向,向着自己曾经最仰慕的样子进步。
|
||||
接着,我遇见了一个温柔,耐心而又体贴的女生。最初的遇见,本以为只会是寥寥几语就会最终沉没在微信记录中的一面之缘,但完全出乎意料地,这一面之缘让我们互相都成为了对方生命中最重要的人之一。这一次遇见,不但让我两点一线的生活再一次有了色彩,更是给过去的一年里留下了无数令我难忘的回忆。
|
||||
在今年的五月份,我还在知乎收到了一份很长的留言,留言那头是我的初恋,留言是关于我曾经的一篇有关她的知乎回答。在我们分手后的很长一段时间里,那篇回答都是被我删除的状态,直到今年三月份的时候,我决定正视和放下我的过去,于是恢复了那一篇回答,她看到了,特地来感谢我。留言里讲述了一些分手以后的事情和她的近况,她也有了新的生活,新的恋人,希望我不要再因为过去而责怪自己。读完留言是泪流满面,三年过去了,大家都没选择憎恨对方,都没有留在原地,而是都踏入了新的环境,新的生活。
|
||||
再后来,我搬到了D502,在那里遇见了后半年玩的最多的一只吉祥物,开启了接下来几乎大半年的实验室生活。D502可以说见证了我过去大半年的脚步,见证了我许多个难忘的项目,我的操作系统、计网小作业和我的图形学大作业等等等等。
|
||||
|
||||
过去的一年,有无数次遇见,每一次遇见,都在提醒我,我在向前走。
|
||||
|
||||
2022.12.31 23:00
|
||||
我用这最后一场电影的时间,看了一场关于我自己的,2022年的回忆录。
|
||||
|
||||
我很感谢我自己,没有沉沦,没有停留,也许有时迷茫,但一直在向着远处的微光前行。
|
||||
我很感谢我的朋友们,在我低落的时候支持我,生病的时候关心我,你们做了作为朋友最应该做的事情,遇见你们是我的荣幸。
|
||||
我很感谢我的女朋友,在我失落的时候愿意倾听我的苦恼,在我需要陪伴的时候愿意陪我出去散心,很多时候你是我精神上最有力的支持,是于我而言最重要的存在。
|
||||
我很感谢实验室的吉祥物和实验室的伙伴们,让我过去一年学到了很多,也让我在D502的生活充满了有趣的回忆,鞠躬。
|
||||
|
||||
感谢2022带给我的成长,回忆。
|
||||
|
||||
……
|
||||
|
||||
接下来,把时间交给2023,我们拭目以待。
|
||||
@ -1,72 +0,0 @@
|
||||
---
|
||||
title: 年终总结 - 2023 至 2024
|
||||
date: 2025-01-01 01:15:49
|
||||
tags:
|
||||
- 年终总结
|
||||
- 自言自语
|
||||
categories:
|
||||
- 杂思
|
||||
---
|
||||
|
||||
是因为太久没有写过叙事风格的文字,还是因为脑子早已经被快节奏的短视频侵蚀,又到了年末回顾的时候,我竟与面前的电脑周旋了一晚,最后愣是一句能读的话都没有写出来。末了,把写不出东西这个毛病归结于家里的环境缺乏情调,次日专门找了个海边的图书馆来写,结果到了太阳都下山了,才想起来自己此行的正事不是来这儿看书,而是写点什么。倒是歪打正着,好不容易来一趟,总得在关门之前写下点什么,遂匆匆留下此篇,作为我过去两年存在过的痕迹。
|
||||
|
||||
2024 年的终曲轻轻落幕,与我二十一和二十二岁的许多事情一样,没有轰轰烈烈的收场,只是在那某一瞬间突然意识到,过去的时光就这么结束了。
|
||||
|
||||
是什么时候开始觉得时间过得很快的呢?
|
||||
是那一天想喝家门口的喜茶,却发现它几个月前就已经搬走的时候吗?
|
||||
是那个实习结束后的早晨伸着懒腰起床,打开手机看见日历上赫然写着 10 月的时候?
|
||||
是考完研的那天午后,抱着沉沉的资料最后一次走出图书馆的瞬间?
|
||||
是意识到自考研后又已经过去了两个 75 天的那天?
|
||||
还是一转眼发现就连 2024 都已经过完的今天?
|
||||
|
||||
已经记不太清了。
|
||||
只觉得像是一个阳光灿烂的下午,躺在阳台上被太阳晒得暖暖的椅子里眯上了眼睛。在梦里,时间很慢,慢到周一到周五的时间像有一个学期那么长,床头的耳塞堆成了小山,书页翻得泛黄。醒来的时候,冬日的阳光还照在脸上,窗外的鸟儿还在叽喳,楼下的商铺喇叭还播着广告,对面小区的女人也还在遛她的狗,一切都好像没变。只是家里客厅的台历不再写着 2023,楼下不再有开了两年的排骨饭,对面小区的萨摩也再没看到了。
|
||||
|
||||
马尔克斯说,“生活不是我们活过的日子,而是我们记住的日子,我们为了讲述在记忆中重现的日子”。
|
||||
|
||||
2023 与 2024 ,是 “寻找” 的两年。
|
||||
|
||||
我去实习,去工作,去寻找那个从小就向往的大人的世界,去争取那个看起来还算不错且稳定的生活。而我同时害怕,害怕未知带来的不确定性,便又去逃避,去考研,去努力开拓那条曾经未曾想过的道路。为了毕业,我还去试着做了一些研究,去学习,去尝试在未曾涉足的学术领域中寻找自己的兴趣与潜能。
|
||||
|
||||
寻找是创造的过程,但也因此注定是选择与放弃的过程。过去的这两年里,我时常会想到一首诗:
|
||||
|
||||
黄色的树林里分出两条路,
|
||||
可惜我不能同时去涉足,
|
||||
我在那路口久久伫立,
|
||||
我向着一条路极目望去,
|
||||
直到它消失在丛林深处。
|
||||
但我却选了另外一条路,
|
||||
它荒草萋萋,十分幽寂,
|
||||
显得更诱人、更美丽,
|
||||
虽然在这条小路上,
|
||||
都很少留下旅人的足迹,
|
||||
虽然那天清晨落叶满地,
|
||||
两条路都未经脚印污染。
|
||||
啊,留下一条路等改日再见!
|
||||
但我知道路径延绵无尽头,
|
||||
恐怕我难以再回返。
|
||||
也许多少年后在某个地方,
|
||||
我将轻声叹息把往事回顾,
|
||||
一片树林里分出两条路,
|
||||
而我选了人迹更少的一条,
|
||||
因此走出了这迥异的旅途。
|
||||
|
||||
还记得在发出放弃考研录取邮件的那个下午,我仿佛就站在这样的岔路口前,极目远眺。我很明白,这不同于我过去二十年人生中的许多选择,这一次没有明确的答案,也没有周旋的余地。
|
||||
|
||||
第一次,我意识到,放弃并非选择的反面,而是彼此纠缠,同时发生的一对结果。选择了一条路,便意味着永远失去了踏足那另一条的权利。
|
||||
|
||||
最终按下发送键的那一刻,我恍惚间感到,那些考研的日子似乎就像写满了字的旧报纸,在刹那间被撕得稀碎。
|
||||
|
||||
过去的日子,就一文不值了吗?我问自己。
|
||||
|
||||
想起学马原时徐涛老师对认识的反复性和无限性的讲解:人的认识能力是不断提高的,有的我们认知的局限性决定了我们没有办法看到事物的本质。
|
||||
|
||||
可以说,是考研的过程改变了我,是这些日夜饱满了我的人生和我的经历,从而让我能够更清醒地认识我所想要的人生到底是什么样的。也就是说,宣告考研死亡的不是我,当我下定决心开始的时候,它就注定是会灭亡的。是那些日夜带给我的,最终否定了它们自己。
|
||||
|
||||
虽然,此生再无机会走上那未曾走过的路,但那些为之准备的日夜,我想,将会化作我的行囊,带着我走向路的更深处。
|
||||
|
||||
那,选择另一条路,就会更好吗?
|
||||
|
||||
我永远也不会知道,在平行世界里,选择另一条路的自己,到底会不会过得更好。但可以肯定的是,在往后的日子里,在现在的道路上,我会好好过。
|
||||
|
||||
2025,向前走,别回头。
|
||||
@ -1,73 +0,0 @@
|
||||
---
|
||||
title: Swift 学习笔记 - 配置 VS Code 开发 Flutter iOS Native 代码
|
||||
date: 2025-05-15 09:53:53
|
||||
tags:
|
||||
- Swift
|
||||
- iOS
|
||||
- Flutter
|
||||
- 编程语言
|
||||
categories:
|
||||
- 技术
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
这段时间学 Swift 就冲着一个 iOS 开发来的,这学完了必须给环境整上,虽然说 VS Code 和 XCode 搭配着用也没什么不好的,毕竟开发过程中再怎么也没法避免打开 XCode 的过程 (比如配置开发者签名之类的),但是能够写码时全程不离开当前 IDE 终归还是让人感觉会舒服很多,遂研究如何在 VS Code 中配置 Swift 开发环境以及正常开发 Flutter/ios 项目
|
||||
|
||||
## 配置 Swift 插件
|
||||
|
||||
根据官方提供的文档 [Configuring VS Code for Swift Development](https://www.swift.org/documentation/articles/getting-started-with-vscode-swift.html),可以在 VS Code 插件商店中下载 Swift 插件,即可开始编写一般的 Swift 项目了
|
||||
|
||||
{% note warning flat %}
|
||||
在 Windows 上,Swift 插件可能会因为编码原因拒绝工作,例如出现错误 `Unable to parse output from 'swift package init --help'`,目前在本地未能解决,建议直接使用 ssh 远程 Mac 机器进行开发
|
||||
{% endnote %}
|
||||
|
||||
安装完插件后,对于一般的普通 Swift 项目,插件看起来能够正常工作,包括 F5 运行等都可以正常使用。似乎编译器的依赖关系是通过 `Package.swfit` 解析的,正确配置 `Package.swift` 即可在一般项目里正常开发
|
||||
|
||||
## No such module 'Flutter'
|
||||
|
||||
创建完 Flutter 工程后,进入 `ios` 文件夹下 Flutter 创建的 swift 文件,会发现报错 `no such module 'Flutter'`,此时如果使用 XCode 打开同样会有此报错
|
||||
|
||||
由于是初始工程,没有添加任何使用了 native code 的插件,因此在 `ios` 文件夹下是**不会有 Podfile 文件的**,网上关于这个问题的解决方法大多是 `pod install`,对当前的问题没有任何效果,可以不用尝试了
|
||||
|
||||
误打误撞发现在 XCode 下选择 Product-Analyze 后,XCode 内报错消失,猜测是 XCode 成功找到了 Flutter 相关的库文件,然而 Swift 插件由于缺少 `Package.swift` 文件,依然无法找到 Flutter 模块
|
||||
|
||||
遂就此思路询问 Gemini,借助大模型强大的 DeepResearch 能力,得到了下述解决方案
|
||||
|
||||
### 安装 SweetPad
|
||||
|
||||
首先,在 VSCode 插件商店中搜索 SweetPad 插件并安装,安装完成后左侧工具栏会出现糖果图标
|
||||
|
||||
点击糖果图标进入 SweetPad 配置页面,大概率 SweetPad 已经自动识别了工程。如果没有,可能需要在 `.vscode/settings.json` 中手动添加配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"sweetpad.build.xcodeWorkspacePath": "ios/Runner.xcworkspace"
|
||||
}
|
||||
```
|
||||
|
||||
之后重新加载 VS Code
|
||||
|
||||
### 安装 xcode-build-server
|
||||
|
||||
使用 brew 安装 xcode-build-server:
|
||||
|
||||
```bash
|
||||
brew install xcode-build-server
|
||||
```
|
||||
|
||||
如果想的话,可以额外安装 xcbeautify (SweetPad 推荐):
|
||||
|
||||
```bash
|
||||
brew install xcbeautify
|
||||
```
|
||||
|
||||
### 生成 buildServer.json
|
||||
|
||||
打开命令面板 (或使用 `Ctrl` + `Shift` + `P` / `Command` + `Shift` + `P`),搜索 `SweetPad`
|
||||
|
||||
选中 **SweetPad: Generate Build Server Config**
|
||||
|
||||
SweetPad 应该会提示选择对应的 scheme,例如 `Runner` 以及目标设备等
|
||||
|
||||
完成后,SweetPad 应该会生成例如 `buildServer.json` 或是 `compile_commands.json` 这样的文件,之后重新启动 VS Code 即可
|
||||
@ -1,606 +0,0 @@
|
||||
---
|
||||
title: Swift 学习笔记 - 从 Property Wrapper 视角探索 State 与 Binding 如何工作
|
||||
date: 2025-05-12 23:30:52
|
||||
tags:
|
||||
- Swift
|
||||
- iOS
|
||||
- 编程语言
|
||||
categories:
|
||||
- 技术
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
基本学完 Swift 语法后,就开始跟随 [Apple 关于 Swift UI 的官方教程](https://developer.apple.com/tutorials/swiftui-concepts) 来学习 Swift 如何应用在 iOS App 开发中。
|
||||
|
||||
在 [Driving Changes in your UI with State and Bindings](https://developer.apple.com/tutorials/swiftui-concepts/driving-changes-in-your-ui-with-state-and-bindings) 这一小节中,教程首次引入了 Swift UI 中关于状态的概念。
|
||||
|
||||
教程中使用了简单的代码来介绍 `@State` 和 `@Binding` 属性,以及如何在工程中使用这两个属性:
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
// 省略部分代码 ...
|
||||
struct RecipeEditorConfig {
|
||||
var recipe = Recipe.emptyRecipe()
|
||||
var shouldSaveChanges = false
|
||||
var isPresented = false
|
||||
// 省略部分代码 ...
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct ContentListView: View {
|
||||
// 省略部分代码 ...
|
||||
@State private var recipeEditorConfig = RecipeEditorConfig()
|
||||
|
||||
var body: some View {
|
||||
// 省略部分代码 ...
|
||||
RecipeEditor(config: $recipeEditorConfig)
|
||||
// 省略部分代码 ...
|
||||
}
|
||||
|
||||
// 省略部分代码 ...
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
|
||||
struct RecipeEditor: View {
|
||||
@Binding var config: RecipeEditorConfig
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
RecipeEditorForm(config: $config)
|
||||
// 省略部分代码 ...
|
||||
}
|
||||
}
|
||||
// 省略部分代码 ...
|
||||
}
|
||||
```
|
||||
|
||||
由于之前也接触过其他的前端语言,例如 Dart 或是 JavaScript,自然地就会好奇:Swift 这样的语法设计下,是怎么实现状态的刷新的呢?
|
||||
|
||||
## 一些猜测
|
||||
|
||||
比较直接的猜测就是,传入的 `$config` 类似闭包语法,实则是传递了一个 `{ config in return config }` 闭包给 `RecipeEditor`
|
||||
|
||||
这看起来好像解答了为什么 `RecipeEditor` 可以直接修改 `ContentListView` 中 `recipeEditorConfig` 的值,但实则有几个关键的问题没有解答:
|
||||
|
||||
1. `@State` 存在的意义是在状态变更的时候通知渲染层进行重绘,类似 `setState` 方法,如果直接传入变量闭包,虽然可以修改,但是如何通知 UI 层重绘?
|
||||
2. 如果传入的是闭包,闭包是如何赋值给 `RecipeEditorConfig` 这样的变量的?为什么没有 `init` 函数依然可以完成这样的赋值?
|
||||
3. 在 `RecipeEditor` 中,是可以直接修改 `config` 的值的,例如直接给 `config` 赋新值,如果原本的引用通过闭包传入后解析出来赋值给 `config`,那这样修改后不就丢失了对原先的对象的引用吗?
|
||||
|
||||
几番提问下来,基本可以肯定应该不是直接通过闭包传递的,而是 `@State` 和 `@Binding` 做了更复杂的处理
|
||||
|
||||
而具体做了哪些处理,则要从 Property Wrapper 开始讲起
|
||||
|
||||
## Swift 中的 Property Wrapper
|
||||
|
||||
在一篇关于 `@State` 和 `@Binding` 的问答中,我了解到,这两个属性并非宏 (至少不是库中按照 Macro 描述编写的那种宏),而是受语言支持的两个 Property Wrapper
|
||||
|
||||
关于 Property Wrapper 的内容,在 [Swift Evolution - Property Wrapper](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0258-property-wrappers.md) 中有提到整个 Property Wrapper 设计的背景、提案和应用场景
|
||||
|
||||
简单说来,Property Wrapper 可以分为 3 步:
|
||||
|
||||
1. 编写由 `@propertyWrapper` 标识的结构体,这个结构体用于存储包装属性所需要的所有额外信息以及可以提供的方法(例如一个 lazy 包装属性可能会想要存储当前变量是否已经被初始化过、初始化后的值等),这个结构体会暴露 `wrappedValue` 和 `projectValue` 两个属性供外部访问
|
||||
2. (猜想): 编译器识别到 `@propertyWrapper`,将后续结构体对应的 AST 转化为包含该结构体定义的,更复杂的宏定义
|
||||
3. 编译器识别到 `@MyPropertyWrapper`,将后续对应的声明 AST 转化为更复杂的表达式,包括一个私有 `MyPropertyWrapper<Value>` 结构体对象和对应的访问器
|
||||
|
||||
### PropertyWrapper 结构体
|
||||
|
||||
Property Wrapper 这一方案的提出意图解决的问题就是,当访问一个变量时,能够在不增加越来越多且复杂的编译器属性的同时,支持将通用的、可复用的额外逻辑作用在这个访问过程中
|
||||
|
||||
例如,关键词 `lazy` 本质上就是在试图解决这类问题中的一个: 当访问一个变量时,先判断是否初始化,如果初始化则返回初始化后的值,否则进行初始化
|
||||
|
||||
最初的想法可能就是,写一些重复的代码 (boilerplate code) 来实现这个功能:
|
||||
|
||||
```swift
|
||||
// Irrelevant code omitted
|
||||
struct MyClass {
|
||||
var _myDefaultValue: Int = 123
|
||||
var _myValue: Int? = nil
|
||||
var myValue: Int {
|
||||
get {
|
||||
if _myValue == nil {
|
||||
_myValue = _myDefaultValue
|
||||
}
|
||||
guard let v = _myvalue else {
|
||||
raise fatalError()
|
||||
}
|
||||
return v
|
||||
}
|
||||
set {
|
||||
_myValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
但是如果每一个需要用到这个逻辑的地方都要这么些,未免有些复杂
|
||||
|
||||
考虑到宏可以扩展代码,也许可以定义一个宏 `@Lazy` 来展开这些内容,从而避免每次都重复编写
|
||||
|
||||
```swift
|
||||
// Irrelevant code omitted
|
||||
struct MyClass {
|
||||
// 也就是,将
|
||||
@Lazy var myValue: Int = 123
|
||||
// 转换为
|
||||
var _myValue: Int? = nil
|
||||
var myValue: Int {
|
||||
get {
|
||||
if _myValue == nil {
|
||||
_myValue = 123
|
||||
}
|
||||
guard let v = _myvalue else {
|
||||
raise fatalError()
|
||||
}
|
||||
return v
|
||||
}
|
||||
set {
|
||||
_myValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然而,具体的宏想想就已经非常复杂了
|
||||
|
||||
也就是说,如果想要直接用宏来实现给属性访问添加额外逻辑这一能力,虽然可以做到,但那样的话似乎更多的关注会在如何实现宏本身而不是实际的逻辑上了 (Make 的困境幻视)
|
||||
|
||||
那么,从语言设计的角度考虑,是不是可以让开发者更关注于具体逻辑的实现而隐藏宏转换的细节呢?类似 CMake 那样,用一套规范来约束开发者对逻辑的定义,再在这一规范之上构建编译器属性,使得能够将具体逻辑转换成某种编译器能识别的宏,从而开发者只需要遵循规范来定义逻辑,而不再需要编写具体的宏定义了
|
||||
|
||||
虽然不知道 Swift 具体是怎么实现的,但我想 Property Wrapper 的出现大概率是相似的思路
|
||||
|
||||
在 Property Wrapper 的描述中,Swift 约定了一个编译器属性 `@propertyWrapper`,其可以用于修饰自定义的结构体,结构体的一般结构约定如下:
|
||||
|
||||
```swift
|
||||
@propertyWrapper
|
||||
struct MyPropertyWrapper {
|
||||
private var myPropertyUnderlyValue: MyValueType
|
||||
|
||||
init(myPropertyUnderlyValue: MyValueType) {
|
||||
self.myPropertyUnderlyValue = myPropertyUnderlyValue
|
||||
}
|
||||
|
||||
var wrappedValue: MyValueType {
|
||||
get {
|
||||
// Add logic when get
|
||||
myPropertyUnderlyValue
|
||||
}
|
||||
set {
|
||||
// Add logic when set
|
||||
myPropertyUnderlyValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var projectedValue: SomeProjectedType {
|
||||
get {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我想,之所以这么设计,是因为通过 `struct` 可以提供一个统一的抽象语法树节点来描述整个额外逻辑所需要的所有内容。想象如果不使用 `struct` 结构来包装,那为了实现 `@Lazy` 的能力,需要找一个地方写 `_myValue` 变量,另找一个地方写 `myValue` 需要的 `getter` 和 `setter` 逻辑,并且编译器要能够直到这些分散的表达式节点都是 `@Lazy` 的一部分,这想想就不好实现
|
||||
|
||||
通过 `struct` 的包装,虽然使得实际值的存储多了一层结构体的封装,但简化了编译器的实现,并且也统一了可能的表现形式
|
||||
|
||||
现在,编译器只需要在将这个结构体转换为一个宏,其在标记了对应属性的声明处进行如下操作即可:
|
||||
|
||||
1. 将原先的声明替换为一个 `_originalVarName: MyPropertyWrapper` 的值
|
||||
2. 声明 `var originalVarName: MyPropertyWrapper { get, set }` 访问器,通过 `MyPropertyWrapper.wrappedValue` 进行访问
|
||||
3. 声明 `var $originalVarName: SomeProjectedType { get, set }` 访问器,通过 `MyPropertyWrapper.projectedValue` 进行访问
|
||||
4. 生成 synthesized initializer (如果必须)
|
||||
|
||||
看起来就很好实现了,不是吗?
|
||||
|
||||
最后生成出来的代码大致可以理解成下面这样:
|
||||
|
||||
```swift
|
||||
// Original class declaration
|
||||
// struct MyClass {
|
||||
// @MyPropertyWrapper var myPropertyValue: MyValueType
|
||||
// }
|
||||
|
||||
// Converted class declaration
|
||||
struct MyClass {
|
||||
private var _myPropertyValue: MyPropertyWrapper
|
||||
|
||||
var myPropertyValue: MyValueType {
|
||||
get {
|
||||
_myPropertyValue.wrappedValue
|
||||
}
|
||||
set {
|
||||
_myPropertyValue.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var $myPropertyValue: SomeProjectedType {
|
||||
get {
|
||||
_myPropertyValue.projectedValue
|
||||
}
|
||||
set {
|
||||
_myPropertyValue.projectedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
init(myPropertyValue: MyValueType) {
|
||||
_myPropertyValue = MyPropertyWrapper(myPropertyUnderlyValue: myPropertyValue)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### projectedValue 和 wrappedValue
|
||||
|
||||
在 `MyPropertyWrapper` 的实现中,`wrappedValue` 的作用比较显而易见: 提供了一个对实际存储内容的访问器,在访问器中实现了需要对目标属性添加的额外访问逻辑。而通过将 `myPropertyValue` 改为对 `_myPropertyValue.wrappedValue` 的访问器,可以满足在当前类内对目标属性的访问经过所需要的额外逻辑
|
||||
|
||||
然而,只暴露 `wrappedValue` 往往不能满足需求,因为在类内访问 `self.myPropertyValue` 会直接经过 `_myPropertyValue.wrappedValue.get` 解析到 `_myPropertyValue.myPropertyUnderlyValue` 这一实际的底层存储值,这对变量传递就不太友好了: 如果我试图将这个属性传递到其他的函数中,它就会作为实际存储值进行传递,而不会再传递对应的访问器了!
|
||||
|
||||
虽然 `@autoclosure` 看似可以解决这个问题,但是这需要被调函数进行主动适配,显然不能够满足所有的情况。我猜想,Swift 语言的开发团队就是为此类情况而额外支持了一种访问器: `projectedValue`
|
||||
|
||||
`projectedValue` 可以由开发者自由指定需要返回的对象,并且在类内以 `$myPropertyValue` 的形式暴露,一个简单的设计就是:
|
||||
|
||||
```swift
|
||||
var projectedValue: MyPropertyWrapper {
|
||||
self
|
||||
}
|
||||
```
|
||||
|
||||
提供一个对 `MyPropertyWrapper` 实例对象的 `get` 访问器,从而当使用 `$myPropertyValue` 进行传参时,依然可以通过传入参数的 `wrappedValue` 来访问并且修改底层存储值
|
||||
|
||||
## State 的魔法
|
||||
|
||||
在 SwiftUI 中,`@State` 就是依赖了 Property Wrapper 这一语言特性
|
||||
|
||||
很显然,对于显示内容状态的存储,完全符合了 Property Wrapper 的适用场景:
|
||||
|
||||
- View 的数据需要以成员变量形式存储并且加以访问和修改 (对象为类的成员变量)
|
||||
- 对 View 所引用数据的修改需要通知 UI 框架进行重绘 (变量需要在被赋值时添加额外的通知逻辑)
|
||||
- 数据需要能够沿着控件树向下传递,并且子树也能够对存储数据进行修改 (包含额外逻辑的变量访问器需要能够作为参数传递)
|
||||
- 对于所有的 View 需要的数据,虽然类型不同,但依赖的逻辑相同 (需要多处复用的额外逻辑)
|
||||
|
||||
### 可能的 State 结构体
|
||||
|
||||
要实现对状态的存储和访问,`@State` 包装主要需要做的就是在变量被赋值时,触发 UI 更新逻辑
|
||||
|
||||
因此,一个可能的简化版的 `@State` 大概会是如下的样子:
|
||||
|
||||
```swift
|
||||
@propertyWrapper
|
||||
struct State<T> {
|
||||
private state: T
|
||||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
state
|
||||
}
|
||||
set {
|
||||
state = newValue
|
||||
// call UI update logic
|
||||
}
|
||||
}
|
||||
|
||||
var projectedValue: State<T> {
|
||||
get {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
init(state: T) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 展开后的 @State 代码
|
||||
|
||||
参考 Property Wrapper 展开的逻辑,对于使用了 `@State` 的成员变量,其简化后的展开代码大概会是下面的样子:
|
||||
|
||||
```swift
|
||||
class MyView: View {
|
||||
// Original declaration
|
||||
// @State var counter: Int = 5
|
||||
|
||||
// Expanded declaration
|
||||
var _counter: State<Int> = State(5)
|
||||
|
||||
var counter: Int {
|
||||
get {
|
||||
_counter.wrappedValue
|
||||
}
|
||||
set {
|
||||
_counter.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var $counter: State<Int> {
|
||||
get {
|
||||
_counter.projectedValue
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// View hierarchy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对于当前的 View 来说,直接使用例如 `counter = counter + 1` 可以更新状态值并且触发 UI 更新逻辑;而通过传递 `$counter` 则可以允许其他 View 通过 `State<Int>` 提供的接口来更新状态值并且触发 UI 更新逻辑
|
||||
|
||||
### 使用 Binding 作为 projectedValue
|
||||
|
||||
这看起来已经解决了大半的问题,但还有一些情况需要考虑: 例如有时可能会需要将已有的属性外再添加一些逻辑后再传递给子控件
|
||||
|
||||
对于这种情况,如果直接传递 `$state` 所对应的 `State<Int>` 对象,显然是不满足要求的,而如果只是传递一个 `@autoclosure`,又没办法实现 `set` 能力
|
||||
|
||||
因此,可以想到的就是创建一个额外的类,来包含需要新增的逻辑,并提供访问接口 (就像另一个 Property Wrapper 那样):
|
||||
|
||||
```swift
|
||||
class WierdCounter {
|
||||
let get: () -> Int
|
||||
let set: (Int) -> Void
|
||||
|
||||
init(get: () -> Int, set: (Int) -> Void) {
|
||||
self.get = get
|
||||
self.set = set
|
||||
}
|
||||
|
||||
var wrappedValue: Int {
|
||||
get {
|
||||
self.get()
|
||||
}
|
||||
nonmutating set {
|
||||
self.set(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyView: View {
|
||||
@State var counter: Int = 5
|
||||
|
||||
var wrappedCounter: WierdCounter = WierdCounter {
|
||||
counter
|
||||
}, set: { newValue in
|
||||
counter = newValue * 2
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
MyAnotherView(counter: wrappedCounter)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
此时,更进一步地思考,目前子控件想要访问父控件传入的状态的话,多少还是有些复杂的: 不仅有时候传入 `State<T>` 有时候传入额外的类,访问实际状态的时候还要经过额外的一层访问器去访问
|
||||
|
||||
相比 Swift 设计团队也是这么想的,于是他们首先解决了第一个问题: 先让传入的状态参数类型统一
|
||||
|
||||
这其实还比较好实现,不难发现,`WierdCounter` 和 `State<T>` 其实有着相似的结构,而且他们的核心目标都是需要访问 `counter.wrappedValue`,因此不妨在库中就提供一个类似的类,例如 `MyBinding<T>`:
|
||||
|
||||
```swift
|
||||
struct MyBinding<T> {
|
||||
let get: () -> T
|
||||
let set: (T) -> Void
|
||||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
self.get()
|
||||
}
|
||||
nonmutating set {
|
||||
self.set(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
init(@escaping get: () -> T, @escaping set: (T) -> Void) {
|
||||
self.get = get
|
||||
self.set = set
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对应的,`State<T>` 的设计也进行相应的调整:
|
||||
|
||||
```swift
|
||||
@propertyWrapper
|
||||
struct State<T> {
|
||||
private state: T
|
||||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
state
|
||||
}
|
||||
set {
|
||||
state = newValue
|
||||
// call UI update logic
|
||||
}
|
||||
}
|
||||
|
||||
var projectedValue: MyBinding<T> {
|
||||
MyBinding {
|
||||
self.wrappedValue
|
||||
}, set { newValue in
|
||||
self.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
init(state: T) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样,展开后的 `MyView` 类也会有相应的变化:
|
||||
|
||||
```swift
|
||||
class MyView: View {
|
||||
// Original declaration
|
||||
// @State var counter: Int = 5
|
||||
|
||||
// Expanded declaration
|
||||
var _counter: State<Int> = State(5)
|
||||
|
||||
var counter: Int {
|
||||
get {
|
||||
_counter.wrappedValue
|
||||
}
|
||||
set {
|
||||
_counter.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var $counter: MyBinding<Int> {
|
||||
_counter.projectedValue
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// View hierarchy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
统一了状态传参的类型,接下来,对于第二个问题,就是 `@Binding` 展现魔法的时刻了
|
||||
|
||||
## Binding 的魔法
|
||||
|
||||
其实读到这里,难免会有一种不由自主的冲动:
|
||||
|
||||
既然状态参数的类型都统一了,那要简化子控件访问状态的逻辑,不就是要打包 `incomingState.wrappedValue` 这个逻辑嘛!让每个对 `incomingState` 的访问都以 `incomingState.wrappedValue` 的方式进行,不就可以了嘛?
|
||||
|
||||
没错!还记得 Property Wrapper 就是干这事的吧!
|
||||
|
||||
想必 Swift 开发团队也是这么想的,于是就有了 `@Binding`
|
||||
|
||||
### 可能的 Binding 结构体
|
||||
|
||||
有了 `@State` 的经验,猜想 `@Binding` 的实现就简单多了
|
||||
|
||||
不妨猜测简化后的 `@Binding` 如下:
|
||||
|
||||
```swift
|
||||
@propertyWrapper
|
||||
struct Binding<T> {
|
||||
let _binding: MyBinding<T>
|
||||
|
||||
let wrappedValue: T {
|
||||
get {
|
||||
self._binding.wrappedValue
|
||||
}
|
||||
nonmutating set {
|
||||
self._binding.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
let projectedValue: Binding<T> {
|
||||
self
|
||||
}
|
||||
|
||||
init(@escaping get: () -> T, @escaping set: (T) -> Void) {
|
||||
self.get = get
|
||||
self.set = set
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这看起来和 `MyBinding<T>` 的定义也太像了!不妨试试合二为一:
|
||||
|
||||
```swift
|
||||
@propertyWrapper
|
||||
struct Binding<T> {
|
||||
let get: () -> T
|
||||
let set: (T) -> Void
|
||||
|
||||
let wrappedValue: Binding<T> {
|
||||
get {
|
||||
self.get()
|
||||
}
|
||||
nonmutating set {
|
||||
self.set(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
let projectedValue: Binding<T> {
|
||||
self
|
||||
}
|
||||
|
||||
init(@escaping get: () -> T, @escaping set: (T) -> Void) {
|
||||
self.get = get
|
||||
self.set = set
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
太神奇了,这样甚至不知不觉中统一了 `Binding<T>` 和 `MyBinding<T>` 的实现!接下来,只要让编译器生成 `synthesized initializer` 的时候适配 `Binding<T>` 的初始化方式 (使用 `get` 和 `set` 参数而不是 `Binding<T>` 对象) 似乎就完成了
|
||||
|
||||
### 展开后的 @Binding 代码
|
||||
|
||||
那么,根据 Property Wrapper 的展开逻辑,我们来推测一下 `@Binding` 展开后的代码:
|
||||
|
||||
```swift
|
||||
struct MySubView: View {
|
||||
// Original declaration
|
||||
// @Binding var parentState: Int
|
||||
|
||||
// Expanded declaration
|
||||
var _parentState: Binding<Int>
|
||||
|
||||
var parentState: Int {
|
||||
get {
|
||||
_parentState.wrappedValue
|
||||
}
|
||||
set {
|
||||
_parentState.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var $parentState: Binding<Int> {
|
||||
get {
|
||||
_parentState.projectedValue
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesized initializer
|
||||
init(parentState: Binding<Int>) {
|
||||
self._parentState = Binding {
|
||||
parentState.wrappedValue
|
||||
}, set: { newValue in
|
||||
parentState.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 一些奇怪的问题记录
|
||||
|
||||
至此,基本完成了对 Swift 中 `@State` 和 `@Binding` 实现原理的深入探索了,但难免还会有一些未解的疑惑
|
||||
|
||||
感谢如今大模型的能力,使得部分奇怪的问题得以解答
|
||||
|
||||
以下列出部分与大模型问答的摘要
|
||||
|
||||
### 手动指定 initializer
|
||||
|
||||
#### 提问
|
||||
|
||||
如果 `@State` 和 `@Binding` 的生成涉及 `synthesized initializer` 的生成,那么如果我自己手动指定了 initializer,是否与前述中自动生成的 initializer 产生冲突?
|
||||
|
||||
#### 回答
|
||||
|
||||
会产生影响,如果手动指定了 initializer,需要手动添加对应的初始化逻辑,例如:
|
||||
|
||||
```swift
|
||||
init(parentState: Binding<Int>, someOtherParams: MyType) {
|
||||
// Add the initialization of bindings
|
||||
self._parentState = Binding {
|
||||
parentState.wrappedValue
|
||||
}, set: { newValue in
|
||||
parentState.wrappedValue = newValue
|
||||
}
|
||||
|
||||
// Other logic
|
||||
}
|
||||
```
|
||||
@ -284,7 +284,7 @@ aside:
|
||||
enable: true
|
||||
icon: fab fa-github
|
||||
text: Follow Me
|
||||
link: https://github.com/Linloir
|
||||
link: https://github.com/xxxxxx
|
||||
card_announcement:
|
||||
enable: true
|
||||
content: This is my Blog
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user