2021-An introduction to lockless algorithms
An introduction to lockless algorithms
关于
同时,像
Acquire, release, and “happens before”
为了从相对来说简单易懂的同步接口(synchronization primitives)转向
为了回答这些问题,
- 同一个线程内的事件都是按顺序发生的,也称之为
"total ordering( 完全顺序)" 。通俗地讲,对于同一线程的任意两个事件,你总能说出哪个先发生,哪个后发生。 - 如果两个时间不是在同一个线程内的,那么如果事件
P 是在做message send ,而事件Q 是与之相对应的message receive ,那么事件P 就一定发生在事件Q 之前。 - 这种关系是具有传递性的。因此,如果事件
P 发生在事件Q 之前,而事件Q 发生在事件R 之前,那么事件P 也会发生在R 之前。
“在其之前发生” (happen before)的这种关系是
事实证明,这种说法并不仅仅是一个理论而已:
在每一对操作内,具有释放语义的操作与相应的获取语义操作是同步操作
- 在单个线程中,各个操作都是完全顺序的(total ordering)
- 如果一个进行释放动作的操作
P 与一个进行获取动作的操作Q 是同步的(synchronized) ,就意味着操作P 发生在操作Q 之前,哪怕这两个操作是发生在不同的线程中的。 - 像之前一样,这个关系是具有传递性的,也具有
partial ordering 的特性
按照旧的定义,
获取操作和释放操作似乎是一个抽象的概念,但它们确实为许多常见的多线程编程实例进行了简单的解释。例如,如果有两个用户空间线程都需要访问一个全局变量
thread 1 thread 2
-------------------------------- ------------------------
s = "hello";
pthread_create(&t, NULL, t2, NULL);
puts(s);
s = "world";
pthread_join(t, NULL);
puts(s);
那么这里对变量的访问是否安全?是否能确定
pthread_create() 具有释放的语义,并与thread 2 (具有获取语义)的start (即线程开始执行)有同步关系。因此,在线程创建之前所写的任何数据都可以安全地从线程中访问到。thread 2 的exit 操作具有释放语义,并与pthread_join() (其具有获取语义)有同步关系。因此,线程在退出之前写的任何东西都可以在pthread_join() 之后安全地访问到。
请注意,这里成功地在不用
-
如果程序作者想让
thread 2 “看到" thread 1 到目前为止发生的所有事情的效果,那么两个线程需要相互同步,也就是需要在thread 1 中进行释放操作,在thread 2 中进行获取操作。 -
只要知道哪些
API 提供了acquire/release 语义,就可以利用这些API 来写出确保order 的代码。
在了解了释放和获取语义对这种
The message-passing pattern
在前一段中,我们已经看到了
thread 1 thread 2
-------------------------------- ------------------------
a.x = 1;
message = &a; datum = message;
if (datum != NULL)
printk("%d\n", datum->x);
最初这个
a.x = 1; datum = message;
| |
| happens before |
v v
message = &a; datum->x
因为这两个线程的执行并没有同步关系,所以不能保证
为此,我们定义了 “store-release” 和 “load-acquire” 这两种操作。
thread 1 thread 2
-------------------------------- ------------------------
a.x = 1;
smp_store_release(&message, &a); datum = smp_load_acquire(&message);
if (datum != NULL)
printk("%x\n", datum->x);
这样改动之后,如果
a.x = 1;
|
v
smp_store_release(&message, &a); -----> datum = smp_load_acquire(&message);
|
v
datum->x
这样一来就一切可控了。由于可传递性的效果,每当
在
thread 1 thread 2
-------------------------------- ------------------------
a.x = 1;
smp_wmb();
WRITE_ONCE(message, &a); datum = READ_ONCE(message);
smp_rmb();
if (datum != NULL)
printk("%x\n", datum->x);
在这种写法下,释放和获取语义是由两个
不管是
- 各种
ring buffer 方案。ring buffer 中每一项往往都指向其他的数据,一般来说还会有一些head/tail 的信息都是指向ring buffer 中某个位置的索引。给ring buffer 填数据的一方(producer)会使用store-release 操作,这样就能与consumer (读取ring buffer 中的数据的一方)确保同步。 - RCU。在
compiler 看来,我们熟悉的rcu_dereference() 和rcu_assign_pointer() API 跟load-acquire 和store-release 操作是一个效果。得益于除了Alpha 以外的其他处理器中都具有的一些特性,rcu_dereference() 就可以被直接编译成普通的load 操作,此时rcu_assign_pointer() 仍然跟rcu_dereference() 操作是确保同步的,就好像它还是一个load-acquire 操作一样。 - 将指针写入数组时。在下面的
KVM 代码中(当然是简写过的) ,如果kvm_get_vcpu() 可能看到kvm->online_vcpus 增长过了,那么数组中与之关联的哪一项就一定是有效的。
kvm_vm_ioctl_create_vcpu() kvm_get_vcpu()
----------------------------------------- -----------------------------------------------
kvm->vcpus[kvm->online_vcpus] = vcpu; if (idx < smp_load_acquire(&kvm->online_vcpus))
smp_store_release(&kvm->online_vcpus, return kvm->vcpus[idx];
kvm->online_vcpus + 1); return NULL;
除了