操作系统并发性(二):锁

锁的基本思想

锁的基本思想与现实中的锁是一致的。

比如有一个保险柜中存放着若干现金,由A,B二人共同使用。当A存钱、放钱的时候需要对保险柜上锁,B同理。

至于为什么需要上锁,通俗来讲就是在计算机中可能出现这样的情况:A,B同时向保险柜存钱,此时由于操作系统可能引入不合时宜的中断,计算机可能会遗漏A、B其中一人的操作。

详见:操作系统并发性(一):线程、原子性

锁的基本功能

锁的基本功能是阻止多个线程同时进入临界区。

当线程对锁执行lock操作时,如果锁已经被其他线程锁住,那么可以认为它会阻塞知直到锁被其他线程解开(线程可能进入休眠状态,即告诉操作系统不要将CPU资源分配给自己,也可能一直自旋,即始终占用CPU资源执行while测试语句)。如果锁没有被锁住,那么这个线程会将锁上锁。

执行unlock就是将锁解开。

锁的API

我们以A、B同时递增计数器为例,说明锁在程序中的使用。

#include <pthread.h>
//数据结构
pthread_mutex_t lock;
//初始化
pthread_mutex_init(&lock, NULL);	//第二个参数为配置,通常填入NULL表示使用默认配置
//上锁,解锁
pthread_mutex_lock(&lock);

多线程程序的难点不在于API的使用,而是如何设计出逻辑正确或者线程安全的代码,这需要严密的逻辑,并且考虑到各种极特殊情况。

#include <iostream>
#include <pthread.h>

using namespace std;

struct counter_t{
    int value;
    pthread_mutex_t lock;
}c;

void* mythred(void* arg)
{ 
    cout << "begin" << static_cast<char*>(arg) << endl;

    pthread_mutex_lock(&c.lock);
    for (int i = 0; i < 1e7; i++)
        c.value += 1;
    cout << "done" << static_cast<char*>(arg) << endl;
    pthread_mutex_unlock(&c.lock);
    return nullptr;
}
int main()
{
    cout << "hello world" << endl;
    pthread_t p1, p2;
    char A[] = "A", B[] = "B";
    pthread_mutex_init(&c.lock, NULL);
    pthread_create(&p1, NULL, mythred, static_cast<void*>(&A));
    pthread_create(&p2, NULL, mythred, static_cast<void*>(&B));

    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    cout << c.value << endl;
} 

结果:
在这里插入图片描述

评价锁

  1. 锁是否有效:就是看锁能否正确的完成其功能:防止多个线程同时进入临界区。
  2. 公平性:如果有多个线程在等待锁解开,那么谁将占有锁?该过程是否公平?是否会出现饿死的情况?
  3. 性能:引入锁的时间开销如何?锁能否适用于多个线程的情况?

实现锁的机制

简单标志

struct lock_t{
	int flag;
}
void init(lock_t* mutex){
	mutex -> flag = 0;
}
void lock(lock_t* mutex){
	while (mutex -> flag == 1)	//p1
		;	//spin-wait*(do nothing)
	mutex->flag = 1;	//p2
}
void unlock(lock_t* mutex){
	mutex -> flag = 0;
}

该方案体现了锁的基本功能,但他是有问题的。

如果A线程在执行p2之前系统转向B线程,那么会出现A、B同时上锁的情形。

测试并设置

上面的方案的问题出在测试的语句p1与设置的语句p2不具有原子性(参见:)

所以我们需要硬件提供支持。在x86架构中,硬件提供了xchg指令。

我们用如下C语言代码说明原子性的测试并设置指令做了什么:

int TestAndSet(int* old_ptr, int new){
	int old = *old_ptr;
	*old_ptr = new;
	return old;
}

//用该语句实现锁:
struct lock_t{
	int flag;
}
void init(lock_t* mutex){
	mutex -> flag = 0;
}
void lock(lock_t* mutex){
	while (TestAndSet(&mutxe->flag, 1) == 1)	//p1
		;	//spin-wait*(do nothing)
	mutex->flag = 1;	//p2
}
void unlock(lock_t* mutex){
	mutex -> flag = 0;
}

比较并交换

硬件支持还有比较并交换原语,它比测试并设置要强大一些。

int CompareAndSwap(int* ptr, int expected, int new)
{
	int actual = *ptr;
	if (actual == expected)
		*ptr = new;
	return actual;
}

void lock(lock_t* mutex){
	while (CompareAndSwap(&mutxe->flag, 0, 1) == 1)	//p1
		;	//spin-wait*(do nothing)
}
void unlock(lock_t* mutex){
	mutex -> flag = 0;
}

链接加载与条件存储

该指令在MIPS架构中广泛应用。

int LoadLinked(int* ptr){
	return *ptr;
}

int StoreConditional(int* ptr, int value){
	if (ptr指向的值在期间没有改变)
	{
		*ptr = value;
		return 1;
	}
	else
		return 0;
}

void lock(lock_t* mutex){
	while (LoadLinked(&mutxe->flag) == 0 && 
				StoreConditional(&mutex->flag, 1) == 1)	
		;	//spin-wait*(do nothing)
}
void unlock(lock_t* mutex){
	mutex -> flag = 0;
}

按照我个人的理解,这个方案也是对简单标志方案的改进。前面提到,简单方案出错是因为测试指令与设置指令直接可能出现中断。在这里我允许中断,但通过条件存储指令确保其它线程在中断期间没有改变标志的值。比较测试并设置方案,该方案具有更好的性能。

如何避免自旋

前面提到的几种机制,都是自旋锁,即等待的线程重复执行while测试指令,始终占有CPU资源。这种锁的性能是很低的(很多CPU时钟片段被没有意义的自旋占据)

最简单的方法是通过硬件的支持,规定等待的线程让出CPU资源。

这种方案仍有不足,如果等待的线程数特别多,那么CPU会进行多次无意义的上下文切换。

休眠代替自旋

《操作系统导论》教材介绍了使用等待队列,加入休眠机制的方案。

实在是天资愚笨,没看明白书上的代码。

大概是说,如果一个线程在等待锁,那么就让它休眠,并加入等待队列中,等占有锁的线程释放锁时,它会从等待队列中唤醒一个线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值