本期主题:
操作系统中的原子操作、自旋锁的底层实现
往期链接:
linux设备驱动中的并发
1. 问题描述
在操作系统中,创建了多个线程,多个线程如果都访问同一个变量,没有做好互斥就会造成错误,就像下面的例子:
#include "thread.h"
#define N 100000
long sum = 0;
void Tsum() {
for (int i = 0; i < N; i++) {
sum++;
}
}
int main() {
create(Tsum);
create(Tsum);
join();
printf("sum = %ld\n", sum);
}
测试结果:
执行10次,结果全都不对
根本原因:
没有做多线程之间的管理
2. 原子操作
我们可以使用 lock
汇编命令来保证原子操作
“lock addq $1, %0”:这条汇编指令执行一个加法操作,并使用 lock 前缀来确保操作是原子的。
- lock 前缀:在多处理器系统中,lock 前缀用于确保接下来的指令是原子的。它会锁定总线,防止其他处理器访问内存。
- addq $1, %0:将立即数 1 加到操作数 %0 上,q 表示操作数是 64 位的(quad word)
源码如下:
#include "thread.h"
#define N 100000
long sum = 0;
void Tsum() {
for (int i = 0; i < N; i++) {
// sum++;
// 这条命令可以保证原子加
asm volatile("lock addq $1, %0": "+m"(sum));
}
}
int main() {
create(Tsum);
create(Tsum);
join();
printf("sum = %ld\n", sum);
}
测试结果:正确
其实c++本身提供了 atmoic的库,但是由于我们这里是讲解原理,所以就不写atmoic的相关方式了,感兴趣的朋友可以自己试一下。
3. 自旋锁
自旋锁(spinlock)是一种用于多线程编程的锁机制。它是一种忙等待锁,当一个线程尝试获取锁时,如果锁已经被其他线程占用,该线程不会进入睡眠状态,而是持续地在循环中检查锁是否可用,直到获取锁或达到超时时间。
1. 自旋锁的优点包括:
- 低开销:自旋锁不涉及线程上下文切换,适用于锁持有时间较短的场景。
- 简单实现:实现相对简单,适用于在硬件级别实现同步的情况。
2. 自旋锁的缺点包括:
- 资源浪费:自旋锁在等待锁释放期间会消耗CPU资源,因此不适用于锁持有时间较长的场景。
- 潜在死锁:如果没有妥善处理,可能会导致死锁。
我们可以使用xchg命令
来实现自旋锁,xchg命令介绍:
“lock xchg %0, %1”: lock前缀保证在多处理器系统中操作的原子性。xchg是交换指令,用于交换两个操作数的值。
#include "thread.h"
#define N 100000
int xchg(volatile int *addr, int newval) {
int result;
//用于交换addr地址对应的值和newval,并返回旧值
asm volatile ("lock xchg %0, %1"
: "+m"(*addr), "=a"(result) : "1"(newval));
return result;
}
int locked = 0;
//locked被交换为1,返回的旧值只有是0的情况下才能退出循环
void lock() { while (xchg(&locked, 1)) ; }
//locker被交换为0
void unlock() { xchg(&locked, 0); }
long sum = 0;
void Tsum() {
for (int i = 0; i < N; i++) {
lock();
sum++;
unlock();
}
}
int main() {
create(Tsum);
create(Tsum);
join();
printf("sum = %ld\n", sum);
}
测试结果:
4. 其他
这是thread.h源码,创建进程的头文件:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>
#define NTHREAD 64
enum { T_FREE = 0, T_LIVE, T_DEAD, };
struct thread {
int id, status;
pthread_t thread;
void (*entry)(int);
};
struct thread tpool[NTHREAD], *tptr = tpool;
void *wrapper(void *arg) {
struct thread *thread = (struct thread *)arg;
thread->entry(thread->id);
return NULL;
}
void create(void *fn) {
assert(tptr - tpool < NTHREAD);
*tptr = (struct thread) {
.id = tptr - tpool + 1,
.status = T_LIVE,
.entry = fn,
};
pthread_create(&(tptr->thread), NULL, wrapper, tptr);
++tptr;
}
void join() {
for (int i = 0; i < NTHREAD; i++) {
struct thread *t = &tpool[i];
if (t->status == T_LIVE) {
pthread_join(t->thread, NULL);
t->status = T_DEAD;
}
}
}
__attribute__((destructor)) void cleanup() {
join();
}