并发编程:从入门到放弃
threads.h
互斥:恢复 “块” 之间的原子性、顺序、可见性
lock(&lk)
unlock(&lk)
理解数据竞争
用互斥锁实现并发数据结构
不同的线程 同时访问同一段内存 ,且至少有一个是写 。
Peterson 算法告诉大家:
以下代码概括了你们遇到数据竞争的大部分情况
// Case #1: 上错了锁
void thread1() { spin_lock(&lk1); sum++; spin_unlock(&lk1); }
void thread2() { spin_lock(&lk2); sum++; spin_unlock(&lk2); }
// Case #2: 忘记上锁
void thread1() { spin_lock(&lk1); sum++; spin_unlock(&lk1); }
void thread2() { sum++; }
Abstract Data Type (ADT): A mathematical model for data types, where a data type is defined by its behavior semantics from the point of view of a user of the data, specifically in terms of possible values, possible operations on data of this type, and the behavior of these operations.
- ADT 是接口 (描述数据结构上操作的行为)
- ADT 用某个具体的数据结构实现
学过的数据结构
(面试题) 实现双向链表操作
list_head
)typedef struct node {
void *ptr;
struct node *prev, *next;
} node_t;
node_t head; // 注意不是指针
void list_init(node_t *head) {
head->prev = head->next = head;
}
void list_insert_before(node_t *node, node_t *new) { TODO(); }
void list_insert_after (node_t *node, node_t *new) { TODO(); }
void list_remove(node_t *node) { TODO(); }
白板面试:数据结构、算法……
Google 经典面试
在白板上编程必须
void list_remove(node_t *node) {
// 2021 年百度搜索 “双向链表 删除” 仍然得到这个回答 (WTF)
node->prev->next = node->next;
node->next->prev = node->prev;
}
static inline void list_check(node_t *node) {
assert(node == node->prev->next); // circular list's invariant
assert(node == node->next->prev);
}
void list_remove(node_t *node) {
node_t *prev = node->prev;
node_t *next = node->next;
assert(node != prev && node != next); // possibly prev == next
prev->next = next;
next->prev = prev;
list_check(prev); list_check(next);
}
typedef struct node {
lock_t lock;
void *ptr;
struct node *prev, *next;
} node_t;
呃……好像不太对……
void list_remove_r(node_t *node) {
lock(&node->lock);
list_remove(node);
unlock(&node->lock);
}
typedef struct node {
lock_t lock;
void *ptr;
struct node *prev, *next;
} node_t;
呃……好像还是不太对……
void list_remove_r(node_t *node) {
lock(&node->lock);
lock(&node->prev->lock);
lock(&node->next->lock);
list_remove(node);
unlock(&node->lock);
unlock(&node->prev->lock);
unlock(&node->next->lock);
}
Many operating systems utilized a single lock when first transitioning to multiprocessors, including Sun OS and Linux. In the latter, this lock even had a name, the big kernel lock (BKL).
往简单里写
时刻警惕 “Heisenbug”
假设有一段空闲的内存 (“堆”;已知大小)
malloc(size)
- 返回一个新鲜的对象,内存地址 [x, x+size)
NULL
free(x)
- 无情地和对象 [x, x+size)
分手不要现在就开始设计,先理解问题!
我们需要维护一个已被分配区间的集合
支持的操作
Premature optimization is the root of all evil.
——D. E. Knuth
重要的事情说三遍:
指导思想:$O(n)$ 大小的对象分配后至少有 $\Omega(n)$ 的读写操作,否则就是 performance bug (不应该分配那么多)。
malloc
, Fast and Slow设置两套系统:
人类也是这样的系统
对象越小、分配越频繁,越可能是性能瓶颈
Slab 分配器 (segregated list)
Slab 分配失败
此时的情况:大内存分配
回收小对象 (slab 中的对象)
回收大对象 (回收 slab 或大块内存)
以上就是所有现代 malloc/free 实现的基础
本次课内容与目标
Take-away messages