Skip to content

Commit 2cd0330

Browse files
committed
feat: add tags to multiple documentation files for better categorization
1 parent 4a33cba commit 2cd0330

12 files changed

Lines changed: 155 additions & 10 deletions

File tree

posts/数据结构与算法/题解/LeetCode 136 和 XOR.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
tags:
3+
- 数据结构与算法
4+
- 计算机底层
5+
---
6+
17
# LeetCode 136 和 XOR
28

39
主要借助这个机会简单说下 XOR ( 异或 ) 运算
@@ -53,4 +59,4 @@
5359

5460
## 总结
5561

56-
主要是想顺带写下 XOR 运算特性, 不过具体的位运算使用和设计等挖坑,未来可期
62+
主要是想顺带写下 XOR 运算特性, 不过具体的位运算使用和设计等挖坑,未来可期

posts/游戏开发/ChikaEngine/Job System/JobStorage.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- ChikaEngine
4+
- 并发编程
5+
- 异步与调度
6+
---
7+
18
# JobStorage
29

310
存储Job数据的地方(看名字也非常显然), 看 API 非常好理解其用法用途, 不过还是稍微说下其中的一些设计
@@ -71,4 +78,3 @@ uint32_t index = JobHandle::InvalidIndex;
7178
接着再看其中的 memory_order 的设计, 对于普通的什么 `remainingDependencies` `unfinishedWork` 等都是使用 `relaxed`, 而 `state` 却是 `release`
7279

7380
那么也就是说, 上面那些变量其实只是正常的初始化而已, 自己之间并没有强烈的什么同步关系. 但是 `state` 却被认作是Slot 已经初始化完成的发布点. 也就是当其他线程执行 `slot.state.load(std::memory_order_acquire);` 并且读取到 `Created` 的话, 可以证明当前的 Slot 是在 Storage 层是初始化完成的了. (之后注册 dependency 之类的后面再说, 至少说完成了基础的初始化)
74-

posts/游戏开发/ChikaEngine/Job System/SmallJobFunction.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- ChikaEngine
4+
- C++
5+
- 性能分析
6+
---
7+
18
# SmallJobFunction
29

310
实现一个简单的 Function Wrapper, 让传给 Job System 的 Callable 在 Runtime 打包
@@ -176,4 +183,4 @@ auto callable = [] {};
176183
177184
SmallJobFunction a(callable);
178185
SmallJobFunction b([] {});
179-
```
186+
```

posts/游戏开发/引擎设计/Asset Hot Reload.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- 游戏引擎
4+
- 资源管理
5+
- 软件架构
6+
---
7+
18
# Asset Hot Reload
29

310
如果在运行时遇到了 Assets 发生变化,如何在不重启程序的情况下重载 Assets ? —— 常说的对资产的热重载就是为了解决这样一件事情 (什么 Code Hot Reload 那些更复杂的东西未来可期)

posts/编程语言/C++/Exception.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
tags:
3+
- C++
4+
- 错误处理
5+
---
6+
17
# Exception
28

39
Error 是问题 (~~废话~~), 那么一般来说可以分为编译期发现的和运行期间发现的. 此处简单说下 C++ 提供的一套在 Runtime 的时候对于 Error 的捕获,传播,处理等的机制 —— 也就是 Exception Handling
@@ -421,4 +427,4 @@ catch (const std::filesystem::filesystem_error& e)
421427

422428
> [!note] Don’t try to catch every exception in every function.
423429
424-
不要在各个函数中都写 catch, 而是在明确可以处理某种 exception 的时候再写;
430+
不要在各个函数中都写 catch, 而是在明确可以处理某种 exception 的时候再写;

posts/编程语言/C++/PImpl.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- C++
4+
- 软件架构
5+
- 编译与工具链
6+
---
7+
18
# PImpl
29

310
Pointer to Implementation, 其核心思想是让公开类只保存一个指向真正实现对象的指针,保证对外暴露稳定干净的接口,同时隐藏掉内部的实现细节
@@ -85,4 +92,4 @@ class Renderer
8592
8693
然后可以在构造的时候传入不同的实现,比如Vulkan/DX12/OpenGL等
8794
88-
也是隐藏了具体实现细节,某种程度上还是有点像的(
95+
也是隐藏了具体实现细节,某种程度上还是有点像的(

posts/编程语言/C++/function.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- C++
4+
- 函数式编程
5+
- 类型系统
6+
---
7+
18
# function
29

310
可恶,如此常用的东西居然没有单独写一篇,我有罪
@@ -230,4 +237,4 @@ std::function<void()> f2 = f1;
230237

231238
## 总结
232239

233-
可以使用`std::function`实现对callable object的封装,实现类型擦除,同时也可以是 runtime 中存储 Callable Object 的好容器
240+
可以使用`std::function`实现对callable object的封装,实现类型擦除,同时也可以是 runtime 中存储 Callable Object 的好容器

posts/编程语言/C++/并发模型/Aomic.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- C++
4+
- 并发编程
5+
- 同步机制
6+
---
7+
18
# Atomic
29

310
`std::atomic`, 用于定义原子变量, 允许多个线程按照 C++ 内存模型进行读写等操作. 一般用于线程之间共享状态等.
@@ -318,4 +325,4 @@ odl, 值得注意的是, 如果留空, 默认就是 `seq_cst`
318325
319326
原子操作首先保证单次访问不可分割; 而多个线程之间还需要考虑操作之间的**排序、可见性以及同步关系**
320327
321-
`memory_order` 用于描述一次 atomic 操作承担的内存排序语义,并由此参与构造 `synchronizes-with`、`happens-before` 等关系
328+
`memory_order` 用于描述一次 atomic 操作承担的内存排序语义,并由此参与构造 `synchronizes-with`、`happens-before` 等关系
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
tags:
3+
- C++
4+
- 并发编程
5+
- 同步机制
6+
---
7+
8+
# 关于原子操作重新思考
9+
10+
首先对于单个原子操作语句来说, 它在外部看来一定是原子操作(废话), 也就是不存在“中间态”.
11+
12+
## 操作
13+
14+
先重新看待三种操作(先看操作本身, 再考虑 memory order) —— Load, Store, RMW.
15+
16+
Load 对应我们完全不需要考虑什么, 只是单纯的读取; Store 是我们不需要在意旧的值是多少, 只管写入即可; 而 RMW 是我们在需要旧值的前提下(比如迭代数据 / 条件判断等) 在进行修改数据
17+
18+
小心谨慎, 永远不要假定代码顺序就是执行顺序, 代码下一行的状态和上一行状态一致.
19+
20+
```C++
21+
if (state.load() == State::Idle)
22+
{
23+
state.store(State::Loading);
24+
}
25+
```
26+
27+
借助“打脸法”, 我们不妨假设 `store` 的时候 `state` 的状态已经不是 `Idle` 了, 那么这符合我们的业务逻辑吗? 如果不符合说明写的不对. 又因为此时我们是需要知道旧值的前提下进行数据修改, 所以可以比较自然的想到使用 RMW 操作. 那么正确的写法水到渠成 ——
28+
29+
```
30+
auto expected = State::Idle;
31+
state.compare_exchange(expected, State::Loading);
32+
```
33+
34+
如果正着想“我们需要利用旧值和写入整个是一个原子操作, 中途不允许其他线程之类的介入”, 总觉得跳过了什么思考过程, 所以不妨“打脸”, 举反例证明代码的业务正确性 ( 其实只是为了掩饰我对于这种思考模型的不熟练罢了 ).
35+
36+
害, 大抵还是证伪比证明正确更简单罢.
37+
38+
## Memory Order
39+
40+
有了上述理解之后, 我们再看 Memory Order, 这个更需要把业务逻辑代入进行思考.
41+
42+
首先还是 relaxd —— 只关心当前操作本身, 不需要在意其他事情, 所以最简单
43+
44+
然后是 release, 它不单纯是说明了一个数据的存储, 更是说明我需要借助这个原子操作发布“一些数据”(比如借助sequenced-before来“保护”单线程内前面的代码执行等), 所以这个“发布”的感觉其实和生产者的职能比较符合
45+
46+
那么作为一对的 acquire, 说明的就是我现在需要进行一个“整理”, 接收之前可能会遇到的 release 所发布的数据并且进行消费. 可以说和消费者的语义比较吻合.
47+
48+
那么还有一个“中间件” —— acq_rel, 表示消费了前面的数据, 同时写入的新状态作为下一个发布
49+
50+
但是这样说, 在写代码的时候依旧难以去思考, 所以还是沿用打脸 —— 我写下这个代码, 假设就是 relaxed, 那么业务是否会成立?
51+
52+
```C++
53+
// 生产者
54+
data = "Hello"; // A
55+
56+
ready.store( // B
57+
true,
58+
std::memory_order_relaxed);
59+
60+
// 消费者
61+
if (ready.load(std::memory_order_relaxed) == true) // C
62+
{
63+
Use(data); // D
64+
}
65+
```
66+
67+
我们看消费者, 我现在读取到了 ready == true, 那么在没有任何保护的情况下, 可以保证 data 一定被写入吗? 不行, 在C++内存模型中不能保证这件事情, 因为 relaxed 无法建立跨线程的同步. 所以打脸, 不能用 relaxed.
68+
69+
不过此处或许也可以看成一个同步桥? —— 我们知道了 `A -> B``C -> D`, 但是目前使用 relaxed 或许只能建立起 `C` 是读取 `B` 中的数据, 但是并无法建模出一条完整的同步链 `A -> B -> C -> D` 也就是两个线程数据间的桥梁没有建立起来.
70+
71+
所以看业务代码, 有没有要求要搭桥, 要的话就跟据桥的拓扑结构进行 acq 和 rel 的各种选择
72+
73+
等等, 我又想到一种解释 —— 可以看当前的原子语句执行的时候, 除了对于自身的数据的读写等之外, 是否还有承担其他的语义, 比如借助这个操作来保护一些变量等, 如果有, 那么就需要重新考虑 acq/rel 等, 如果没有则可以是 relaxed.

posts/计算机系统/操作系统与并发/生产者-消费者模型.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
tags:
3+
- 并发编程
4+
- 异步与调度
5+
- 同步机制
6+
---
7+
18
# 生产者-消费者模型
29

310
一个重点在关注数据上的建模. 原先最朴素的做法是把生产和消费是串行并且集成在同一个模块内 —— 即生产完成后直接进行消费, 那么此时生产和消费在执行时序上是紧密关联的.
@@ -57,4 +64,4 @@ Consume(data)
5764
- 缓冲两者短时间内的速度差异
5865
- 作为两者之间的数据交接位置
5966
60-
如果 Producer 和 Consumer 位于不同线程,那么还需要进一步解决 Buffer 的线程安全,空时如何等待, 满时如何处理等问题,具体再进行分析罢
67+
如果 Producer 和 Consumer 位于不同线程,那么还需要进一步解决 Buffer 的线程安全,空时如何等待, 满时如何处理等问题,具体再进行分析罢

0 commit comments

Comments
 (0)