操作系统导论——并发

并发

并发介绍

为单个进程提供的抽象:线程。多线程程序有多个执行点(多个程序计数器,每个都用于取指令和执行)。每个线程类似于独立进程,只有一点区别:线程共享地址空间,从而能够访问相同的数据。

线程有一个程序计数器,记录程序从哪里获取指令。每个线程有自己的一组用于计算的寄存器。两个线程发生上下文切换时,类似于进程间的上下文切换,进程保存到PCB,线程保存TCB。但是线程之间的上下文切换:地址空间保持不变

进程和线程另一个主要区别在于栈。每个线程都有一个栈

image-20210926093716995

线程创建

eg:运行一个程序,创建了两个线程,每个线程都做了一些独立的工作,在这个例子中,打印“A”和“B”。

主程序创建了两个线程,分别执行mythread()函数,但是传入不同的参数(字符串类型的A或B)。一旦线程创建,可能会立即运行(取决于调度程序),或者处于就绪状态,等待执行,创建了两个线程后,主程序调用pthread_join()函数,等待特定线程完成。

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

void *mythread(void *arg){
        printf("%s\n", (char *) arg);
        return NULL;
}

int main(int argc, char *argv[]){
        pthread_t p1, p2;
        int rc;
        printf("main: begin\n");
        rc = pthread_create(&p1, NULL, mythread, "A"); assert(rc == 0);
        rc = pthread_create(&p2, NULL, mythread, "B"); assert(rc == 0);
        // join waits for the threads to finish
        rc = pthead_join(p1, NULL); assert(rc == 0);
        rc = pthead_join(p2, NULL); assert(rc == 0);
        printf("main: end\n");
        return 0;
}

共享数据

两个线程进行相加操作

mov 0x8094a1c, %eax   // 从0x8094a1c取出变量放入eax
add $0x1, %eax		  // 给eax寄存器的值加1
mov %eax,0x8094a1c	  // eax的值被存回0x8094a1c

假设线程1进入代码区域,取出值 50,然后向寄存器加1,因此eax=51。此时,时钟中断发生,OS将正在运行的线程(包括程序计数器、寄存器,包括eax等)的状态保存到线程TCB。线程2执行,同样也是加1,此时变量的值仍为50,执行完后保存回寄存器,变量为51。然后线程1恢复,准备执行最后一条mov指令。现在eax=51.因此,变量保存到内存中的值为51

image-20210926102404987

这种情况称为竞态条件结果取决于代码的时间执行。由于执行的这段代码的多个线程可能导致竞争状态,因此将此段代码称为临界区临界区是访问共享变量的代码片段,一定不能由多个线程同时执行。真正想要的代码就是所谓的互斥。

线程API

POSIX线程库通过锁来提供互斥进入临界区的那些函数。最基本的一对函数:

int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)

如果你有一段代码是一个临界区,可以通过以下完成:

pthread_mutex_t lock;
pthread_mutex_lock(&lock);
x = x + 1;
pthread_mutex_unlock(&lock);

如果在调用pthread_mutex_lock()时没有其他线程持有锁,线程将获取该锁并进入临界区。如果另一个线程确实持有该锁,那么尝试获取该锁的线程将不会从该调用返回,直到获得该锁(意味着持有该锁的线程通过解锁调用释放该锁)。

另外两个函数:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex
							struct timespec *abs_timeout);

这两个调用用于获取锁。如果锁已被占用,则trylock版本将失败。获取锁的timedlock会在超时或获取锁后返回,以先发生的为准。因此,具有0超时的timedlock则退化为trylock。

条件变量

当线程之间必须发生某种信号时,如果一个线程在等待另一个线程继续执行某操作,条件变量就很有用。以这种方式进行交互的程序使用两个主要函数。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);

第一个函数cond_wait使调用函数进入休眠状态,因此等待其他线程发出信号,通常当程序中的某些内容发生变化时,现在正在休眠的线程可能会关心它。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Pthread_mutex_lock(&lock);
while (ready == 0)
	Pthread_cond_wait(&cond, &lock);
Pthread_mutex_unlock(&lock);

在初始化相关的锁和条件之后,一个线程检查变量ready是否已经被设置为0以外的值。如果没有,那么线程指示简单地调用等待函数以便休眠,直到其他线程唤醒它。

唤醒线程的代码运行在另外某个线程中,像下面这样:

Pthread_mutex_lock(&lock);
ready = 1;
Pthread_cond_signal(&cond);
Pthread_mutex_unlock(&lock);

在发出信号时(以及修改全局变量ready时),我们是始终确保持有锁。这确保我们不会在代码中意外引入竞态条件。等待调用除了使线程进入睡眠状态外,还会让调用者睡眠时释放锁。

希望原子式执行一系列指令。

锁的基本思想

lock_t mutex;
...
lock(&mutex);
balance = balance +1;
unlock(&mutex);

锁就是一个变量,因此需要声明一个某种类型的变量才能使用。这个锁变量保存了锁在某一时刻的状态。也可以保存其他信息,比如持有锁的线程,或请求获取锁的线程队列,但这些信息都会隐藏起来,锁的使用者不会发现。

调用lock()尝试获取锁,如果没有其他线程持有锁(即它是可用的),该线程会获得锁,进入临界区。这个线程有时被称为锁的持有者(owner)。如果另外一个线程对相同的锁变量(本例中的mutex)调用lock(),因为锁被另一线程持有,该调用不会返回。这样,当持有锁的线程在临界区时,其他线程就无法进入临界区。

锁的持有者一旦调用unlock(),如果没有其他等待线程(即没有其他线程调用过lock()并卡在那里),锁的状态就变成可用了如果有等待线程(卡在lock()里)其中一个会(最终)注意到锁状态的变化获取该锁,进入临界区

控制中断

最早提供的互斥解决方案时临界区关闭中断。通过在进入临界区之前关闭中断(使用特殊的硬件指令),可用保证临界区的代码不会被中断,从而原子地执行。结束之后,重新打开中断,程序正常执行。

缺点:

1.允许所有线程可以执行特权操作,会造成信任滥用

2.不支持多处理器

3.中断丢失可能造成系统问题。

自旋锁

测试并设置指令:

1 int TestAndSet(int *old_ptr, int new) {
2 		int old = *old_ptr; // fetch old value at old_ptr
3 		*old_ptr = new; 	// store 'new' into old_ptr
4 		return old; 		// return the old value
5 } 

返回old_ptr指向的旧值,同时更新为new的新值,这些代码是原子地执行。实现自旋锁

1 	typedef struct lock_t {
2 		int flag;
3 	} lock_t;
4
5 	void init(lock_t *lock) {
6 		// 0 indicates that lock is available, 1 that it is held
7 		lock->flag = 0;
8 	}
9
10 	void lock(lock_t *lock) {
11 		while (TestAndSet(&lock->flag, 1) == 1)
12 		; // spin-wait (do nothing)
13 	}
14
15 	void unlock(lock_t *lock) {
16 		lock->flag = 0;
17 	c} 

首先假设一个线程在运行,调用lock(),没有其他线程持有锁,所以flag是0。当调用TestAndSet(flag, 1)方法,返回0,线程会跳出while循环,获取锁。同时也会原子的设置flag为1,标志锁已经被持有。当线程离开临界区,调用unlock()将flag设置为0,标志没有线程持有锁。

第二种场景:

当有线程持有锁(flag为1),其他线程调用lock(),然后调用testaAndSet,一直返回1,线程会一直自旋。当flag改为0,其他线程调用testAndSet返回0并原子把flag设为1,从而获得锁,进入临界区。

评价自旋锁

自旋锁不提供任何公平性。

在单CPU情况下,性能开销大,自旋一个时间片,浪费CPU周期。

比较并交换

x86是这种硬件原语:

1 	int CompareAndSwap(int *ptr, int expected, int new) {
2 		int actual = *ptr;
3 		if (actual == expected)
4 			*ptr = new;
5 		return actual;
6 	}

基本思路是检测ptr指向的值是否和expected相等;如果是,更新ptr所指向的值为新值。否则,什么也不做。不论哪种情况,都返回该内存地址的实际值,让调用者能够直到执行是否成功。可以用来替换上面lock()

1 	void lock(lock_t *lock) {
2 		while (CompareAndSwap(&lock->flag, 0, 1) == 1)
3 		; // spin
4 	} 

使用队列:休眠替代自旋

提供两个调用:park()能够让调用线程休眠,unpark(threadID)则会唤醒 threadID标识的线程。

1 	typedef struct lock_t {
2 		int flag;
3 		int guard;
4 		queue_t *q;
5 	} lock_t;
6
7 	void lock_init(lock_t *m) {
8 		m->flag = 0;
9 		m->guard = 0;
10 		queue_init(m->q);
11 	}
12
13 	void lock(lock_t *m) {
14 		while (TestAndSet(&m->guard, 1) == 1)
15 			; //acquire guard lock by spinning
16 		if (m->flag == 0) {
17 			m->flag = 1; // lock is acquired
18 			m->guard = 0;
19 		} else {
20 			queue_add(m->q, gettid());
21 			m->guard = 0;
22 			park();
23 		}
24 	}
25
26 	void unlock(lock_t *m) {
27 		while (TestAndSet(&m->guard, 1) == 1)
28 			; //acquire guard lock by spinning
29 		if (queue_empty(m->q))
30 			m->flag = 0; // let go of lock; no one wants it
31 		else
32 			unpark(queue_remove(m->q)); // hold lock (for next thread!)
33 		m->guard = 0;
34 	} 

条件变量

等待某些条件是很正常的,简单的方案是自旋直到条件满足,这是极其低效的。那么,线程应该如何等待一个条件?

定义和程序

线程可以使用条件变量,来等待一个条件变成真。条件变量是一个显式队列,当某些执行状态(即条件 condition)不满足时,线程可以把自己加入队列,等待该条件。

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

int done = 0;    //状态变量
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER; //条件变量初始化

void thr_exit(){
        pthread_mutex_lock(&m); //上锁
        done = 1;
        pthread_cond_signal(&c);
        pthread_mutex_unlock(&m);
}

void *child(void *arg){
        printf("child\n");
        thr_exit();
        return NULL;
}

void thr_join(){
        pthread_mutex_lock(&m);
        while (done == 0)
                pthread_cond_wait(&c,&m); //放锁 休眠
        pthread_mutex_unlock(&m);
}

int main(int argc, char *argv[]){
        printf("parent:begin\n");
        pthread_t p;
        pthread_create(&p, NULL, child, NULL);
        thr_join();
        printf("parent:end\n");
        return 0;
}

wait()的职责是释放锁,并让调用线程休眠(原子地)。

生产者消费者问题

也叫有界缓冲区问题。

在使用管道连接不同程序的输出和输入时,也会使用有界缓冲区,例如:

grep foo file.txt | wc -l 

该例子并发地执行了两个进程,grep进程从file.txt中查找包括 foo 的行,写到标准输出。管道的另一端是wc进程的标准输入,wc统计完行数后打印出结果。因此,grep是生产者,wc是消费者。

因为有界缓冲区是共享资源,所以必须通过同步机制来访问它,以免产生竞态条件。代码实现:

int buffer;
int count = 0; // 0代表生产 , 1代表消费

void put(int value){
	assert(count == 0); 
	count = 1;
	buffer = value;
}

int get(){
	assert(count == 1);
	count = 0;
	return buffer;
}

assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序

也就是说,如果表达式 expression 的值为假(即为 0),那么它将首先向标准错误流 stderr 打印一条出错信息,然后再通过调用 abort 函数终止程序运行否则,assert 无任何作用

以上如果连续生产或者连续消费,则会触发断言。

void *producer(void *arg){
	int i;
	int loops = (int) arg;
	for (i = 0; i < loops; i++){
		put(i);
	}
}

void *consumer(void *arg){
	int i;
	while (1){
		int tmp = get();
		printf("%d\n", tmp);
	}
}

使用条件变量和锁的生产消费问题

cond_t cond;
mutex_t mutex;

void *producer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);           //p1
		if (count == 1)						  //p2
			pthread_cond_wait(&cond, &mutex); //p3
		put(i);								  //p4
		pthread_cond_signal(&cond);			  //p5
		pthread_mutex_unlock(&mutex);		  //p6
	}
}

void *consumer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);				//c1
		if (count == 0)							//c2
			pthread_cond_wait(&cond, &mutex);	//c3
		int tmp = get();						//c4
		pthread_cond_signal(&cond);				//c5
		pthread_mutex_unlock(&mutex);			//c6
		printf("%d\n", tmp);
	}
}

如果有两个消费者,1个生产者,c1消费时无数据,去睡眠,开始生产,执行p1 p2 p4 p5(通知c1消费) p6 p1 p2 p3(去睡眠),此时,c2消费者抢先执行 c1 c2 c4 c5(生产者就绪) c6 c1 c2 c3(睡眠),c1消费者执行时会发现没有数据。image-20210928145228731

仍有问题的方案:用while代替if

cond_t cond;
mutex_t mutex;

void *producer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);           //p1
		while (count == 1)					  //p2
			pthread_cond_wait(&cond, &mutex); //p3
		put(i);								  //p4
		pthread_cond_signal(&cond);			  //p5
		pthread_mutex_unlock(&mutex);		  //p6
	}
}

void *consumer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);				//c1
		while (count == 0)						//c2
			pthread_cond_wait(&cond, &mutex);	//c3
		int tmp = get();						//c4
		pthread_cond_signal(&cond);				//c5
		pthread_mutex_unlock(&mutex);			//c6
		printf("%d\n", tmp);
	}
}

这同样也会导致问题,c1 取完数据,睡眠,c2执行,没有数据,睡眠,此时生产者也在睡眠!!

image-20210928145317731

所以信号需要有指向性消费者不应该唤醒消费者,而应该只唤醒生产者

单值缓冲区的生产消费问题

cond_t empty, fill;
mutex_t mutex;

void *producer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);           //p1
		while (count == 1)					  //p2
			pthread_cond_wait(&empty, &mutex); //p3
		put(i);								  //p4
		pthread_cond_signal(&fill);			  //p5
		pthread_mutex_unlock(&mutex);		  //p6
	}
}

void *consumer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);				//c1
		while (count == 0)						//c2
			pthread_cond_wait(&fill, &mutex);	//c3
		int tmp = get();						//c4
		pthread_cond_signal(&empty);			//c5
		pthread_mutex_unlock(&mutex);			//c6
		printf("%d\n", tmp);
	}
}

生产者等待条件变量empty,发信号给变量fill。相应地,消费者线程等待fill,发信号给empty。解决了消费者再也不会唤醒生产者,生产者也不会唤醒消费者。

最终的生产消费方案

put()get()方法

int buffer[MAX];
int fill = 0;
int use = 0;
int count = 0;

void put(int value){
	buffer[fill] = value;
	fill = (fill + 1) % MAX;
	count++;
}

int get(){
	int temp = buffer[use];
	use = (use + 1) % MAX;
	count--;
	return tmp;
}
cond_t empty, fill;
mutex_t mutex;

void *producer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);           //p1
		while (count == 1)					  //p2
			pthread_cond_wait(&empty, &mutex); //p3
		put(i);								  //p4
		pthread_cond_signal(&fill);			  //p5
		pthread_mutex_unlock(&mutex);		  //p6
	}
}

void *consumer(void *arg){
	int i;
	for (i = 0; i < loops; i++){
		pthread_mutex_lock(&mutex);				//c1
		while (count == 0)						//c2
			pthread_cond_wait(&fill, &mutex);	//c3
		int tmp = get();						//c4
		pthread_cond_signal(&empty);			//c5
		pthread_mutex_unlock(&mutex);			//c6
		printf("%d\n", tmp);
	}
}

信号量 Semaphores

可以使用信号量作为锁和条件变量。如何使用?

信号量的定义

sem_wait 对应 p操作 sem_post 对应v操作,信号量为负数时睡眠

#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1);

第三个参数将信号量初始化为1,第二个参数表示信号量在同一进程的多个线程共享的。

二值信号量(锁)

使用信号量来实现锁

sem_t m;
sem_init(&m, 0, 1);

sem_wait(&m);
// 临界区
sem_post(&m);

image-20210929090816492

信号量用作条件变量

sem_t s;

void *child(void *arg){
	printf("child begin\n");
	sem_post(&s);
	return NULL;
}

int main(int argc, char *argv[]){
	sem_init(&s, 0, 0);
	printf("parent begin\n");
	pthread_t c;
	pthread_create(c, NULL, child, NULL);
	sem_wait(&s);
	printf("parent end\n");
	return 0;
}

生产消费问题

1 	sem_t empty;
2 	sem_t full;
3
4 	void *producer(void *arg) {
5 		int i;
6 		for (i = 0; i < loops; i++) {
7 		sem_wait(&empty); // line P1
8 		put(i); // line P2
9 		sem_post(&full); // line P3
10 		}
11 }
12
13 	void *consumer(void *arg) {
14 		int i, tmp = 0;
15 		while (tmp != -1) {
16 			sem_wait(&full); // line C1
17 			tmp = get(); // line C2
18 			sem_post(&empty); // line C3
19 			printf("%d\n", tmp);
20 		} 
21	}

这个代码会产生两个生产者同时调用put 会被覆盖的情况

下面增加互斥

1 sem_t empty;
2 sem_t full;
3 sem_t mutex;
4
5 void *producer(void *arg) {
6 	int i; 
7 	for (i = 0; i < loops; i++) {
8 		sem_wait(&mutex); // line p0 (NEW LINE)
9 		sem_wait(&empty); // line p1
10 		put(i); // line p2
11 		sem_post(&full); // line p3
12 		sem_post(&mutex); // line p4 (NEW LINE)
13 	}
14 }
15
16 void *consumer(void *arg) {
17 	int i;
18 	for (i = 0; i < loops; i++) {
19 		sem_wait(&mutex); // line c0 (NEW LINE)
20 		sem_wait(&full); // line c1
21 		int tmp = get(); // line c2
22 		sem_post(&empty); // line c3
23 		sem_post(&mutex); // line c4 (NEW LINE)
24 		printf("%d\n", tmp);
25 	}
26 }
27
28 int main(int argc, char *argv[]) {
29 // ...
30 sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with...
31 sem_init(&full, 0, 0); // ... and 0 are full
32 sem_init(&mutex, 0, 1); // mutex=1 because it is a lock (NEW LINE)
33 // ...
34 } 

这个代码会产生死锁问题

一个生产和一个消费,假设消费者先运行,池中没有数据,去睡眠,但此时因为c0行消费者持有锁,生产者卡在p0行,因为无法获取锁进入临界区,所以产生死锁

正确版本

1 sem_t empty;
2 sem_t full;
3 sem_t mutex;
4
5 void *producer(void *arg) {
6 	int i; 
7 	for (i = 0; i < loops; i++) {
8 		sem_wait(&empty); // line p0  // 改变了互斥量的位置
9 		sem_wait(&mutex); // line p1 (NEW LINE)// 改变了互斥量的位置
10 		put(i); // line p2
11 		sem_post(&mutex); // line p3 (NEW LINE)
12 		sem_post(&full); // line p4
13 	}
14 }
15
16 void *consumer(void *arg) {
17 	int i;
18 	for (i = 0; i < loops; i++) {
19 		sem_wait(&full); // line c0  // 改变了互斥量的位置
20 		sem_wait(&mutex); // line c1 (NEW LINE) // 改变了互斥量的位置
21 		int tmp = get(); // line c2
22 		sem_post(&mutex); // line c3 (NEW LINE)
23 		sem_post(&empty); // line c4
24 		printf("%d\n", tmp);
25 	}
26 }
27
28 int main(int argc, char *argv[]) {
29 // ...
30 sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with...
31 sem_init(&full, 0, 0); // ... and 0 are full
32 sem_init(&mutex, 0, 1); // mutex=1 because it is a lock (NEW LINE)
33 // ...
34 } 

常见并发问题

非死锁缺陷

违反原子性缺性

1 Thread 1::
2 if (thd->proc_info) {
3 	...
4 	fputs(thd->proc_info, ...); //打印出值
5 	...
6 }
7
8 Thread 2::
9 thd->proc_info = NULL;

上述例子中,假如线程1先检查proc_info非空,在打印该值之前,被线程2中断,线程2将proc_info的值设为NULL,此时如果线程1再打印该值,则会产生空指针异常。

违反顺序缺陷

1 Thread 1::
2 void init() {
3 	...
4 	mThread = PR_CreateThread(mMain, ...);
5 	...
6 }
7
8 Thread 2::
9 void mMain(...) {
10 	...
11 	mState = mThread->State;
12 	...
13 } 

如果线程2在线程1执行之前就执行,mThread未被初始化,则会引用空指针崩溃。

违反顺序的正式定义为:“两个内存访问的预期顺序被打破了(A应该在B执行之前执行)

死锁缺陷

代码库中,组件之间会有复杂的依赖。以及封装隐藏实现细节,导致了死锁。

产生死锁的4个条件

  • 互斥:线程对于需要的资源进行互斥的访问(例如一个线程抢到锁)
  • 持有并等待:线程持有了资源(已持有锁),同时又在等待其他资源(获得其他锁)
  • 非抢占:线程获得的资源(锁)不能被抢占
  • 循环等待:线程之间存在一个环路,环路上每个线程都额外持有一个资源,而这个资源优势下一个线程要申请的

预防死锁

循环等待

最直接的方式就是获取锁时提供一个全序,可能很难做到,例如系统有两个锁,每次都先申请L1然后申请L2。

偏序可能是一种有用的方法,安排锁的获取并避免死锁。

通过锁的地址来强制锁的顺序

比如有一个函数:do_something(mutex t *m1, mutex t *m2),如果函数总是先抢 m1,然后 m2,那么当一个线程调用 do_something(L1, L2),而另一个线程调 用 do_something(L2, L1)时,就可能会产生死锁。

可以如下解决:

if (m1 > m2) { // grab locks in high-to-low address order
 pthread_mutex_lock(m1);
 pthread_mutex_lock(m2);
} else {
 pthread_mutex_lock(m2);
 pthread_mutex_lock(m1);
}							// 假设m1和m2不是同一个锁
持有并等待

可以通过给获取锁的操作上锁来实现原子性

1 lock(prevention);
2 lock(L1);
3 lock(L2);
4 ...
5 unlock(prevention); 

先抢到 prevention 这个锁,保证了在抢锁的过程中,不会有不合时宜的线程切换,从而避免了死锁

问题:不适用于封装,不知道需要准确地抢哪些锁,并且提前抢到这些锁。因为要提前抢到所有锁(同时),而不是在真正需要的时候,所以可能降低了并发、

非抢占

trylock()函数会尝试获得锁,或者返回−1,表示锁已经被占有

1 top:
2 lock(L1);
3 if (trylock(L2) == -1) {  // 返回-1,表示锁已被占有,进入if
4 	unlock(L1);				// 释放L1
5 	goto top;
6 } 

会引来一个新的问题:活锁(livelock)。两个线程有可能一直 重复这一序列,又同时都抢锁失败。(可以在循环结束的时候,随机等待一个时间,然后再重复整个动作)

互斥

最后的预防方法是完全避免互斥。通常来说,代码都会存在临界区,因此很难避免互斥。无等待(wait-free)数据结构的思想,通过硬件指令,可以构造出不需要锁的数据结构。

比较并交换(compare-and-swap)指令,是一种由硬件提 供的原子指令,做下面的事:

1 int CompareAndSwap(int *address, int expected, int new) {
2 	if (*address == expected) {
3 		*address = new;
4 		return 1; // success
5 	}
6 return 0; // failure
7 } 

假定想原子地给某个值增加特定的数量。可以这样实现:

1 void AtomicIncrement(int *value, int amount) {
2 	do {
3 		int old = *value;
4 	} while (CompareAndSwap(value, old, old + amount) == 0);
5 } 

无须获取锁,更新值,然后释放锁这些操作,使用比较并交换指令,反复尝试将值更新到新的值。这种方式没有使用锁,因此不会有死锁(有可能产生活锁)

posted @ 2021-10-08 09:29  FailBetter  阅读(165)  评论(0编辑  收藏  举报