【LeetCode】289. 生命游戏

使用额外的状态标识现在、以前的状态,才能达到O(1)的空间复杂度。

【模板写法】遍历周围的格子:

  • 定义一个邻居数组
  • 遍历邻居数组,得到周围的格子

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
int neighbors[3] = {0, 1, -1};

int rows = board.size();
int cols = board[0].size();

// 遍历面板每一个格子里的细胞
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {

// 对于每一个细胞统计其八个相邻位置里的活细胞数量
int liveNeighbors = 0;

for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {

if (!(neighbors[i] == 0 && neighbors[j] == 0)) {
// 相邻位置的坐标
int r = (row + neighbors[i]);
int c = (col + neighbors[j]);

// 查看相邻的细胞是否是活细胞
if ((r < rows && r >= 0) && (c < cols && c >= 0) && (abs(board[r][c]) == 1)) {
liveNeighbors += 1;
}
}
}
}

// 规则 1 或规则 3
if ((board[row][col] == 1) && (liveNeighbors < 2 || liveNeighbors > 3)) {
// -1 代表这个细胞过去是活的现在死了
board[row][col] = -1;
}
// 规则 4
if (board[row][col] == 0 && liveNeighbors == 3) {
// 2 代表这个细胞过去是死的现在活了
board[row][col] = 2;
}
}
}

// 遍历 board 得到一次更新后的状态
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if (board[row][col] > 0) {
board[row][col] = 1;
} else {
board[row][col] = 0;
}
}
}
}
};

【NJU OS】10 状态机模型的应用

状态机模型:理解编译器和现代 CPU

编译器:源代码 S (状态机) → 二进制代码 C (状态机)

C=compile(S)

编译 (优化) 的正确性 (Soundness):

  • S 与 C 的可观测行为严格一致
    • system calls; volatile variable loads/stores; termination

超标量 (superscalar)/乱序执行处理器

  • 允许在状态机上 “跳跃”
  • ilp-demo.c

查看状态机执行

Trace 和调试器

程序执行 = 状态机执行

  • 我们能不能 “hack” 进这个状态机
    • 观察状态机的执行
      • strace/gdb
    • 甚至记录和改变状态机的执行

应用 (1): Time-Travel Debugging

应用 (2): Record & Replay

总结

  • 编程 (状态机) 就是全世界
  • 状态机可以帮我们
    • 建立物理世界的公理体系
    • 理解调试器、Trace, profiler
    • 自动分析程序的执行 (model checker)

【NJU OS】09 操作系统的状态机模型

动手写操作系统

大学的真正意义

将已有的知识和方法重新消化,为大家建立好 “台阶”,在有限的时间里迅速赶上数十年来建立起的学科体系。

“专业世界观” 的学习方法

  • 经典研究论文 (OSDI, SOSP, ATC, EuroSys, …)
  • 久经考验的经典教学材料 (xv6, OSTEP, CSAPP, …)
  • 海量的开源工具 (GNU 系列, qemu, gdb, …)
  • 第三方资料,慎用 (tutorials, osdev wiki, …)

硬件和软件的桥梁

Bare-metal 与程序员的约定

为了让计算机能运行任何我们的程序,一定存在软件/硬件的约定

  • CPU reset 后,处理器处于某个确定的状态
    • PC 指针一般指向一段 memory-mapped ROM
      • ROM 存储了厂商提供的 firmware (固件)
    • 处理器的大部分特性处于关闭状态
      • 缓存、虚拟存储、……
  • Firmware (固件,厂商提供的代码)
    • 将用户数据加载到内存
      • 例如存储介质上的第二级 loader (加载器)
      • 或者直接加载操作系统 (嵌入式系统)

x86 Family: CPU Reset 行

CPU Reset 之后:发生了什么?

  • 从 PC (CS:IP) 指针处取指令、译码、执行……
  • 从 firmware 开始执行
    • ffff0 通常是一条向 firmware 跳转的 jmp 指令

Firmware: BIOS vs. UEFI

  • 都是主板/主板上外插设备的软件抽象
    • 支持系统管理程序运行
  • Legacy BIOS (Basic I/O System)
  • UEFI (Unified Extensible Firmware Interface)

Legacy BIOS: 约定

Firmware 必须提供机制,将用户数据载入内存

Legacy BIOS 把第一个可引导设备的第一个扇区加载到物理内存的 7c00 位置
此时处理器处于 16-bit 模式
规定 CS:IP = 0x7c00, (R[CS] << 4) | R[IP] == 0x7c00
可能性1:CS = 0x07c0, IP = 0
可能性2:CS = 0, IP = 0x7c00
其他没有任何约束

调试 QEMU: 确认 Firmware 的行为

鸡和蛋的问题解决

有个原始的鸡:Firmware

  • 代码直接存在于硬件里
  • CPU Reset 后 Firmware 会执行
    • 加载 512 字节到内存 (Legacy Boot)
    • 然后功成身退

Firmware 的另一用处

  • 放置一些 “绝对安全的代码”
    • BIOS 中断 (Hello World 是如何被打印的)
    • ARM Trusted Firmware
      • Boot-Level 1, 2, 3.1, 3.2, 3.3
      • U-Boot: the universal boot loader

今天的 Firmware: UEFI

UEFI 上的操作系统加载

操作系统的状态机模型(需复习)

“操作系统” 的状态机已经启动

操作系统:是个 C 程序

总结

  • 一切皆可调试 (包括 firmware)
    • 理解操作系统是如何被启动的
    • 学会使用 gdb (必备生存技能)
  • 操作系统也是程序
    • AbstractMachine 扩展了程序的语义,仅此而已

【NJU OS】08 并发 Bug 和应对

应对Bug的方法

基本思路:否定你自己

虽然不太愿意承认,但始终假设自己的代码是错的。

Bug 多的根本原因:编程语言的缺陷

软件是需求 (规约) 在计算机数字世界的投影。

更实在的方法:防御性编程

把程序需要满足的条件用 assert 表达出来。

并发 Bug:死锁 (Deadlock)

AA-Deadlock

ABBA-Deadlock

避免死锁

死锁产生的四个必要条件 (Edward G. Coffman, 1971):

  • 互斥:一个资源每次只能被一个进程使用
  • 请求与保持:一个进程请求资阻塞时,不释放已获得的资源
  • 不剥夺:进程已获得的资源不能强行剥夺
  • 循环等待:若干进程之间形成头尾相接的循环等待资源关系

并发 Bug:数据竞争 (Data Race)

数据竞争

不同的线程同时访问同一段内存,且至少有一个是写。

Peterson 算法告诉大家:

你们写不对无锁的并发程序,所以事情反而简单了

用互斥锁保护好共享数据,消灭一切数据竞争

其他类型并发Bug

原子性违反(AV)

顺序违反(Ov)

应对并发Bug的方法

Lockedep

ThreadSanitizer

更多的检查:动态程序分析

动态分析工具:Sanitizers

总结

  • 常见的并发 bug
    • 死锁、数据竞争、原子性/顺序违反
  • 不要盲目相信自己:检查、检查、检查
    • 防御性编程:检查
    • 动态分析:打印 + 检查

【NJU OS】07 真实世界的并发编程

高性能计算中的并发编程

高性能计算程序:特点

  • 系统模拟:天气预报、能源、分子生物学
  • 人工智能:神经网络训练
  • 矿厂:纯粹的 hash 计算
  • HPC-China 100

高性能计算:主要挑战

计算任务如何分解?

  • 计算图需要容易并行化
    • 机器-线程两级任务分解
  • 生产者-消费者解决一切
    • MPI - “a specification for the developers and users of message passing libraries”, OpenMP - “multi-platform shared-memory parallel programming in C/C++ and Fortran”
  • Parallel and Distributed Computation: Numerical Methods

线程间如何通信

  • 通信不仅发生在节点/线程之间,还发生在任何共享内存访问
  • 还记得被 mem-ordering.c 支配的恐惧吗?

数据中心里的并发编程

数据中心程序:特点

以数据 (存储) 为中心

  • 从互联网搜索 (Google)、社交网络 (Facebook/Twitter) 起家
  • 支撑各类互联网应用:微信/QQ/支付宝/游戏/网盘/……

算法/系统对 HPC 和数据中心的意义

  • 你有 1,000,000 台服务器
  • 如果一个算法/实现能快 1%,就能省 10,000 台服务器 一套房 ≈ 50 台服务器(不计运维成本)

数据中心:主要挑战

多副本情况下的高可靠、低延迟数据访问

  • 在服务海量地理分布请求的前提下
    • 数据要保持一致 (Consistency)
    • 服务时刻保持可用 (Availability)
    • 容忍机器离线 (Partition tolerance)

如何用一台 (可靠的) 计算机尽可能多地服务并行的请求

  • 关键指标:QPS, tail latency, …

我们有的工具

  • 线程 (threads)
  • 协程 (coroutines)
    • 多个可以保存/恢复的执行流 (M2 - libco)
    • 比线程更轻量 (完全没有系统调用,也就没有操作系统状态)

数据中心:协程和线程

数据中心

  • 同一时间有数千/数万个请求到达服务器
  • 计算部分
    • 需要利用好多处理器
      • 线程 → 这就是我擅长的 (Mandelbrot Set)
      • 协程 → 一人出力,他人摸鱼
  • I/O 部分
    • 会在系统调用上 block (例如请求另一个服务或读磁盘)
      • 协程 → 一人干等,他人围观
      • 线程 → 每个线程都占用可观的操作系统资源

Go 和 Goroutine

现代编程语言上的系统编程


我们身边的并发编程


总结

  • 并发编程的真实应用场景
    • 高性能计算 (注重任务分解): 生产者-消费者 (MPI/OpenMP)
    • 数据中心 (注重系统调用): 线程-协程 (Goroutine)
    • 人机交互 (注重易用性): 事件-流图 (Promise)

【LeetCode】76. 最小覆盖子串

  • 注意map用法:unordered_map<string, int> differ
  • emplace_back()
  • 范围循环(range-based for loop):for (string &word: words)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public:
unordered_map<char, int> ori, cnt; // 存储字符出现次数的哈希表

// 检查 cnt 中的字符出现次数是否满足 ori 中的要求
bool check() {
for (const auto& p : ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}

// 寻找最小窗口子串
string minWindow(string s, string t) {
for (const auto& c : t) {
++ori[c]; // 统计 t 中每个字符的出现次数
}

int l = 0, r = -1; // 滑动窗口的左右指针
int len = INT_MAX; // 最小窗口子串的长度
int ansL = -1, ansR = -1; // 最小窗口子串的起始和结束位置

while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]]; // 将窗口右侧的字符加入 cnt 中
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1; // 更新最小窗口子串的长度
ansL = l; // 更新最小窗口子串的起始位置
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]]; // 将窗口左侧的字符从 cnt 中减去
}
++l; // 缩小窗口
}
}

return ansL == -1 ? string() : s.substr(ansL, len); // 返回最小窗口子串
}
};

【LeetCode】30. 串联所有单词的子串

  • 注意map用法:unordered_map<string, int> differ
  • emplace_back()
  • 范围循环(range-based for loop):for (string &word: words)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
vector<int> findSubstring(string &s, vector<string> &words) {
vector<int> res;
int m = words.size(), n = words[0].size(), ls = s.size();
for (int i = 0; i < n && i + m * n <= ls; ++i) {
unordered_map<string, int> differ;
for (int j = 0; j < m; ++j) {
++differ[s.substr(i + j * n, n)];
}
for (string &word: words) {
if (--differ[word] == 0) {
differ.erase(word);
}
}
for (int start = i; start < ls - m * n + 1; start += n) {
if (start != i) {
string word = s.substr(start + (m - 1) * n, n);
if (++differ[word] == 0) {
differ.erase(word);
}
word = s.substr(start - n, n);
if (--differ[word] == 0) {
differ.erase(word);
}
}
if (differ.empty()) {
res.emplace_back(start);
}
}
}
return res;
}
};

【LeetCode】15. 三数之和

排序 + 双指针

时间复杂度:O(N^2)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
// 枚举 a
for (int first = 0; first < n; ++first) {
// 需要和上一次枚举的数不相同
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
// c 对应的指针初始指向数组的最右端
int third = n - 1;
int target = -nums[first];
// 枚举 b
for (int second = first + 1; second < n; ++second) {
// 需要和上一次枚举的数不相同
if (second > first + 1 && nums[second] == nums[second - 1]) {
continue;
}
// 需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (second == third) {
break;
}
if (nums[second] + nums[third] == target) {
ans.push_back({nums[first], nums[second], nums[third]});
}
}
}
return ans;
}
};

【LeetCode】68. 文本左右对齐

模拟

当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格;
当前行不是最后一行,且只有一个单词:该单词左对齐,在行末填充空格;
当前行不是最后一行,且不只一个单词:

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Solution {
// blank 返回长度为 n 的由空格组成的字符串
string blank(int n) {
return string(n, ' ');
}

// join 返回用 sep 拼接 [left, right) 范围内的 words 组成的字符串
string join(vector<string> &words, int left, int right, string sep) {
string s = words[left];
for (int i = left + 1; i < right; ++i) {
s += sep + words[i];
}
return s;
}

public:
vector<string> fullJustify(vector<string> &words, int maxWidth) {
vector<string> ans;
int right = 0, n = words.size();
while (true) {
int left = right; // 当前行的第一个单词在 words 的位置
int sumLen = 0; // 统计这一行单词长度之和
// 循环确定当前行可以放多少单词,注意单词之间应至少有一个空格
while (right < n && sumLen + words[right].length() + right - left <= maxWidth) {
sumLen += words[right++].length();
}

// 当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格
if (right == n) {
string s = join(words, left, n, " ");
ans.emplace_back(s + blank(maxWidth - s.length()));
return ans;
}

int numWords = right - left;
int numSpaces = maxWidth - sumLen;

// 当前行只有一个单词:该单词左对齐,在行末填充剩余空格
if (numWords == 1) {
ans.emplace_back(words[left] + blank(numSpaces));
continue;
}

// 当前行不只一个单词
int avgSpaces = numSpaces / (numWords - 1);
int extraSpaces = numSpaces % (numWords - 1);
string s1 = join(words, left, left + extraSpaces + 1, blank(avgSpaces + 1)); // 拼接额外加一个空格的单词
string s2 = join(words, left + extraSpaces + 1, right, blank(avgSpaces)); // 拼接其余单词
ans.emplace_back(s1 + blank(avgSpaces) + s2);
}
}
};

【NJU OS】06 并发控制:同步

线程同步

生产者-消费者问题

99% 的实际并发问题都可以用生产者-消费者解决。

条件变量:万能同步方法

同步问题:分析

任何同步问题都有先来先等待的条件。

线程 join (thread.h, sum.c)

  • 等所有线程结束后继续执行,否则等待

条件变量:实现生产者-消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
void Tproduce() {
mutex_lock(&lk);
if (count == n) cond_wait(&cv, &lk);
printf("("); count++; cond_signal(&cv);
mutex_unlock(&lk);
}

void Tconsume() {
mutex_lock(&lk);
if (count == 0) cond_wait(&cv, &lk);
printf(")"); count--; cond_signal(&cv);
mutex_unlock(&lk);
}

条件变量:实现并行计算

1
2
3
4
5
6
7
8
9
10
void producer() {
P(&empty); // P()返回 -> 得到手环
printf("("); // 假设线程安全
V(&fill);
}
void consumer() {
P(&fill);
printf(")");
V(&empty);
}

信号量

哲学家进餐问题

总结

  • 实现同步的方法
    • 条件变量、信号量;生产者-消费者问题
    • Job queue 可以实现几乎任何并行算法
  • Copyrights © 2019-2024 Hxy
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信