并发控制:互斥(Peterson算法、自旋锁、互斥锁和futex)

1、Peterson算法

Peterson算法是一个基于两个线程的的并发算法,所以如果线程多了,那可能就不太奏效了。

在Peterson算法中,假设两个线程分别为A和B,每一个想要进入critical section线程需要做三件事情:

  1. 先声明自己想要进入临界区
  2. 将这次可进入临界区权限设置为对方可进入
  3. 如果临界区权限是自己或者临界区权限是对方并且对方不想进入临界区,那么我们就可以进入临界区

第一步很正常,就是这第二步有点奇怪,为什么我想访问却还要设置可进入临界区的权限为对方呢?举几个例子:

  1. 只有一个线程想要进入临界区:假设线程A想要进入临界区,那么会先将代表A的全局变量设置为可进入,再将临界区权限设置为B,由于B此时不想进入临界区,那么我们就直接进,不用管权限
  2. 两个线程都想进入临界区:这种情况可以概括为先到先得,假设A先将临界区权限设置为B,那么在B设置临界区权限的时候就将原先A设置的权限覆盖成A,那么此时A就检测到权限是自己,而B检测到权限不是自己并且A也想进入临界区,那么A就进入,B就阻塞,知道A离开临界区。

所以很显然Peterson算法在两个线程时是管用的,接下来看一下代码

int volatile x = 0, y = 0, turn;

void TA() {
  while (1) {
    x = 1;                   
    turn = B;               
    while (1) {
      if (!y) break;         
      if (turn != B) break;  
    }
    critical_section(); //进入临界区
    x = 0;                   
  }
}

void TB() {
  while (1) {
    y = 1;                   
    turn = A;                
    while (1) {
      if (!x) break;         
      if (turn != A) break;  
    }
    critical_section();
    y = 0;                   
  }
}

2、并发控制:互斥

在多线程程序中,由于共享资源的存在和指令并发是原子执行的,这就导致race(竞态)现象,以下代码就是一个产生竞态现象的多线程代码

#include<pthread.h>
#include<stdio.h>

int volatile sum = 0;

void* mythread(void* vargp) {
    for(int i = 0; i < n; i++) {
        sum++;
    }
}

int main(int argc, char ** argv) {
    pthread_t tid1, tid2;
    int n = 0;

    if(argc != 2) {
        printf("usage: %s <niters>\n", argv[0]);
        exit(0);
    }

    n = atoi(argv[1]);

    pthread_create(&tid1, NULL, mythread, &n);
    pthread_create(&tid2, NULL, mythread, &n);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    if(sum != (2 * n)) {
        printf("BOOM! sum = %d\n", sum);
    }else {
        printf("OK sum = %d\n", sum);
    }
    exit(0);
}

这段代码很简单,只是创建两个线程,然后分别将全局变量sum加n次,n是在命令行中最为额外的参数传递的,所以结果应该很显然sum=2*n,可是运行过后会发现,有时候是正确的,有时候答案很离谱,造成这种的情况的原因主要是在sum++这段代码被翻译成汇编代码的时候实际上是三条指令,如下:

mov $0x800 , %rax
add $1 , %rax
mov %rax , $0x800

我们假设sum在内存中的地址是0x800,第一条指令将sum从内存取出至寄存器rax中,第二条指令将rax+1,第三条指令将rax写回到内存中。

这就是问题所在,我们都知道cpu是一条指令一条指令执行的,所以我们不能假设cpu以那种方式执行两个线程中的这三条指令,一旦指令顺序不对,那么结果也就不对了。

上述的问题主要是两个线程在执行sum++的这条指令的时候是并发执行的,如果要得到正确的答案,我们就不能让两个线程在执行sum++这种共享资源的时候是并发的,简洁的说我们需要上述三条汇编指令是原子性的,所以我们需要互斥。

2.1 自旋锁(Spin)

自旋锁的概念很简单,自旋锁的实现主要来自于一条专用机器指令:exchange(xchg)指令,他们都有汇编指令的前缀lock+对应指令组成,而该lock指令会将总线锁住,并且不接受任何中断,所以这条指令是能保证原子性的。主要用exchange指令来实现互斥,具体步骤如下:

  1. 初始时锁在一个位置(x)并且没有任何人持有
  2. 每一个想要进入临界区的人先进行和x位置的值进行交换
  3. 然后比较交换得到的值是不是锁,如果不是锁,那么就进行自旋,一直重复2、3操作直到获得锁
  4. 离开临界区时释放所持有的锁

显然这种方式能保证互斥,也就是临界区同一时间内只有一个线程访问。以下是代码实现,在xchg函数中使用了内联汇编,详情请翻阅资料。

int xchg(volatile int *addr, int newval){
	int result;
	asm volatile("lock xchg %0, %1" : 
		: "=r" (val), "+m" (*addr) : "m" (*ptr) , "0" (val)
	);
	return result;
}

// 自旋锁
int locked = 0;
void lock() {
	while(xchg(&locked, 1));
}
void unlock(){
	xchg(&locked, 0);
}

自旋锁的缺陷:

  1. lock和unlock执行时是原子的,指令无法乱序
  2. 除了进入临界区的线程,处理器的上的其他线程都在空转
  3. 时间片轮转:获得自旋锁的线程在就绪队列中排队,其他线程获得CPU会空转,实现了100%的资源浪费

自旋锁的特点:

  1. 快的时候很快:锁的争抢比较少,只需要一条原子指令的开销即可获得锁,也就是更快的fast-path
  2. 慢的时候很慢:多个线程争抢时,只有一个线程获得锁,其他的均会自旋等待。如果持有自旋锁的线程切换或睡眠,会发生100%的cpu资源浪费(一核有难,八核围观)

性能评价的维度:

  1. 空间复杂度
  2. 时间复杂度
  3. scalability伸缩性:多处理器性能的度量,难以严谨的计算(CPU功耗受温度影响,系统中的其他进程)

2.2 互斥锁(Mutex)

前面的自旋锁造成的性能损失是很严重的,只适合在某些特殊场景使用,而互斥锁在比较温和。

那么要改进自旋锁,只用c代码是做不到的(c代码只能计算)。

  • 把锁的实现放到操作系统里就好了
  • syscall(SYSCALL_lock, &lk);试图获得lk,但如果失败,可以选择自旋或者切换到其他线程(很显然大部分是后者)
  • syscall(SYSCALL_unlock, &lk);释放lk,如果有等待锁的线程就唤醒它

具体细节为:

  1. 当一个线程需要访问共享资源时,首先尝试获取互斥锁。如果锁没有被其他线程持有,则该线程获取到锁,并可以访问共享资源。
  2. 如果锁已经被其他线程持有,则当前线程会被阻塞,并进入等待队列,直到锁被释放。
  3. 当持有锁的线程完成对共享资源的访问后,释放互斥锁,此时等待队列中的线程中的一个(通常是先到先得)将获得锁并可以开始访问共享资源。

互斥锁的核心在于保证对共享资源的互斥访问,避免了多个线程同时修改共享资源导致的数据不一致问题。然而,互斥锁也可能引发死锁问题,即多个线程相互等待对方释放锁而无法继续执行的情况。因此,在使用互斥锁时需要小心设计和使用。

由于互斥锁在获取锁和等待锁时都会陷入syscall中,所以它是更慢的fast-path(快速获得锁),更快的slow-path(等待不再占用cpu)的特点,那么有没有一种锁能够两个都很快呢,有的,那就是futex

2.3 Futex(Fast Userspace Mutex) = Spin + Mutex

futex是一种用户空间的快速互斥量机制,由于自旋锁获得锁很快,而互斥锁没有得到锁时也不太占用cpu,那么就将二者结合,futex在获得锁时和自旋锁一样,在用户空间操作,通过一条原子执行xchg就可以得到锁而不需要调用syscall,而获得锁失败时,会将线程进入休眠状态从而等他一个条件(锁释放)唤醒,而futex可以用来实现互斥锁,条件变量等高级同步原语。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皮城大学生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值