操作系统——并发之锁

锁的基本思想

锁就是一个变量,保存锁在某一时刻的状态,要么是可用的,要么是被占用的

POSIX将锁称为互斥量mutex

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock);
...
pthread_mutex_unlockj(&lock);
  • 允许使用不同锁来保护不同数据结构

锁的实现

  • 需要硬件和操作系统的配合来实现一个可用的锁

锁的评价

  • 提供互斥性
  • 公平性
  • 性能

锁的实现

控制中断

  • 优点:简单
  • 缺点:一个贪婪的程序可能恶意独占cpu;不支持多处理器;关闭中断导致中断丢失,可能引发严重的系统问题

测试并设置(原子交换)

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

实现自旋锁

void init(lock_t *lock)
{
	lock->flag = 0;
}

void lock(lock_t *lock)
{
    // 自旋
	while (TestAndSet(&lock->flag, 1) == 1);
}

void unlock(lock_t* lock)
{
	lock->flag = 0;
}
  • 自旋锁在单cpu上无法使用,因为一个自旋的线程永远不会放弃cpu

  • 评价自旋锁

    • 提供互斥性
    • 不提高公平性保证
    • 性能开销很大

比较并交换

int CompareAndSwap(int *ptr, int expected, ine new)
{
    int actual = *ptr;
    if (actual == expected) *ptr = new;
    return actual;
}
  • 实现自旋锁
void lock(lock_t* lock)
{
	while (CompareAndSwap(&lock->flag, 0, 1) == 1);
}

连接加载+条件式储存

  • 条件是存储指令,只有上一次加载的地址在期间没有更新时才会成功(同时更新刚才链接加载的地址的值)

获取并增加

  • int FetchAndAdd(int *ptr)
    {
    int old = *ptr;
    *ptr = old + 1;
    return old;
    }
    
  • typedef struct lock_t {
    	int ticket;
    	int turn;
    } lock_t;
    
    void lock_init(lock_t *lock) {
        lock->ticket = 0;
        lock->turn = 0;
    }
    
    void lock(lock_t *lock) {
        int myturn = FetchAndAdd(&lock->ticket);
        while (lock->turn != myturn); // spin
    }
    
    void unlock(lock_t *lock) {
    	FetchAndAdd(&lock->turn);
    }
    
  • 提供公平性保障

自旋过多的解决方案

  • 在要自旋时放弃cpu

    • 上下文切换的成本仍然很高
    • 不能保证公平性,可能会有线程饿死
  • 使用队列:休眠代替自旋

    • 必须显示地施加某种控制,决定锁释放时,谁能抢到锁

    • Solaris提供的支持

      • typedef struct

两阶段锁

  • 两阶段锁意识到自旋可能很有用,尤其是在很快就要释放锁的场景
  • 如果第一个自旋阶段没有获得锁,第二阶段调用者会睡眠,直到锁可用

基于锁的并发数据结构

并发计数器

  • 给普通计数器加锁,得到简单的有锁计数器
typedef struct counter_t {
    int value;
    pthread_mutex_t lock;
} counter_t;

void init(counter_t *c) {
    c->value = 0;
    Pthread_mutex_init(&c->lock, NULL);
}

void increment(counter_t *c) {
    Pthread_mutex_lock(&c->lock);
    c->value++;
    Pthread_mutex_unlock(&c->lock);
}

void decrement(counter_t *c) {
    Pthread_mutex_lock(&c->lock);
    c->value--;
    Pthread_mutex_unlock(&c->lock);
}

int get(counter_t *c) {
    Pthread_mutex_lock(&c->lock);
    int rc = c->value;
    Pthread_mutex_unlock(&c->lock);
    return rc;
}
  • 可扩展计数器

    • 理想状态下,多处理器上运行多线程就想单线程一样快,这种状态称为完美扩展

    • 懒惰计数器

      • 每个cpu核心有一个局部计数器和局部锁,同时还有一个全局计数器和全局锁
      • 如果一个核心上的线程想要增加计数器,增加它的局部计数器,通过局部锁保证同步,不同cpu上的线程不会竞争
      • 为了保持全局计数器更新,局部值会定期转移给全局计数器,方法是获取全局锁,让全局计数器加上局部计数器的值,然后局部计数器置零
      • 局部转全局的频度有阈值S决定,S越小懒惰计数器越精确,同时越接近普通非扩展计数器,S越大扩展性越强,但是全局计数器和实际计数的偏差越大。需要在性能和准确性之间折衷

并发链表

  • 调整加锁位置,使之只环绕代码的真正临界区
void List_Init(list_t *L) {
    L->head = NULL;
    pthread_mutex_init(&L->lock, NULL);
}

void List_Insert(list_t *L, int key) {
    // synchronization not needed
    node_t *new = malloc(sizeof(node_t));
    if (new == NULL) {
        perror("malloc");
        return;
    }
    new->key = key;

    // just lock critical section
    pthread_mutex_lock(&L->lock);
    new->next = L->head;
    L->head = new;
    pthread_mutex_unlock(&L->lock);
}

int List_Lookup(list_t *L, int key) {
    int rv = -1;
    pthread_mutex_lock(&L->lock);
    node_t *curr = L->head;
    while (curr) {
        if (curr->key == key) {
            rv = 0;
            break;
        }
        curr = curr->next;
    }
    pthread_mutex_unlock(&L->lock);
    return rv; // now both success and failure
}

并发队列

  • 使用两个锁,一个负责队列头,一个负责队列尾,使得入队操作和出队操作可以并发执行
typedef struct node_t {
	int value;
	struct node_t *next;
} node_t;

typedef struct queue_t {
    node_t *head;
    node_t *tail;
    pthread_mutex_t headLock;
    pthread_mutex_t tailLock;
} queue_t;

void Queue_Init(queue_t *q) {
    node_t *tmp = malloc(sizeof(node_t));
    tmp->next = NULL;
    q->head = q->tail = tmp;
    pthread_mutex_init(&q->headLock, NULL);
    pthread_mutex_init(&q->tailLock, NULL);
}

void Queue_Enqueue(queue_t *q, int value) {
    node_t *tmp = malloc(sizeof(node_t));
    assert(tmp != NULL);
    tmp->value = value;
    tmp->next = NULL;

    pthread_mutex_lock(&q->tailLock);
    q->tail->next = tmp;
    q->tail = tmp;
    pthread_mutex_unlock(&q->tailLock);
}

int Queue_Dequeue(queue_t *q, int *value) {
    pthread_mutex_lock(&q->headLock);
    node_t *tmp = q->head;
    node_t *newHead = tmp->next;
    if (newHead == NULL) {
        pthread_mutex_unlock(&q->headLock);
        return -1; // queue was empty
    }
    *value = newHead->value;
    q->head = newHead;
    pthread_mutex_unlock(&q->headLock);
    free(tmp);
    return 0;
}

并发散列表

  • 使用并发链表,每个散列桶都有一个锁,支持许多并发操作
#define BUCKETS (101)

typedef struct hash_t {
	list_t lists[BUCKETS];
} hash_t;

void Hash_Init(hash_t *H) {
    int i;
    for (i = 0; i < BUCKETS; i++) {
    	List_Init(&H->lists[i]);
    }
}

int Hash_Insert(hash_t *H, int key) {
	int bucket = key % BUCKETS;
	return List_Insert(&H->lists[bucket], key);
}

int Hash_Lookup(hash_t *H, int key) {
	int bucket = key % BUCKETS;
	return List_Lookup(&H->lists[bucket], key);
}

参考自《操作系统导论》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值