1.如何评价构造的lock?
(1)功能性。就是你这个lock到底能不能有效果。
(2)公平性。你不能让某个thead永远获取不到lock
(3)效率。对于只有一个thread的情况不能因为锁代码导致效率变低。分别对于单核和多核的情况,锁的获取和释放的效率如何
2.如果构造lock?
(1)停止中断
(2)Test-and-Set。硬件提供原子性的操作,从而可以构成一个spin-lock的锁,原子性操作步骤按照如下的C语言来表示。
int TestAndSet(int *old_ptr, int new) {
int old = *old_ptr; // fetch old value at old_ptr3
*old_ptr = new; // store ’new’ into old_ptr4
return old; // return the old value5 }
typedef struct __lock_t {
int flag;
} lock_t;
void init(lock_t *lock) {
// 0 indicates that lock is available, 1 that it is held
lock->flag = 0;
}
void lock(lock_t *lock) {
while (TestAndSet(&lock->flag, 1) == 1)
; // spin-wait (do nothing)
}
void unlock(lock_t *lock) {
lock->flag = 0;
}
从如上的角度分析这个锁,首先是可以确保功能是对的。不过无法保证公平性。在单核的机器上因为大量的无用的循环,效率会很低,不过在多核的机器上效率会有所提高。
(3)Compare-and-swap
int CompareAndSwap(int *ptr, int expected, int new) {
int actual = *ptr;
if (actual == expected)
*ptr = new;
return actual;
}
(4)load-linked and store-conditional
int LoadLinked(int *ptr) {
return *ptr;
}
int StoreConditional(int *ptr, int value) {
if (no one has updated *ptr since the LoadLinked to this address) {
*ptr = value;
return 1; // success!
} else {
return 0; // failed to update
}
}
void lock(lock_t *lock) {
while (1) {
while (LoadLinked(&lock->flag) == 1)
; // spin until it’s zero
if (StoreConditional(&lock->flag, 1) == 1)
return; // if set-it-to-1 was a success: all done
// otherwise: try it all over again
}
}
void unlock(lock_t *lock) {
lock->flag = 0;
}
void lock(lock_t *lock) {
while (LoadLinked(&lock->flag)||!StoreConditional(&lock->flag, 1))
; // spin
}
(5)fetch-and-add
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) {
lock->turn = lock->turn + 1;
}
这种锁和前面提到的几种锁相比会更加公平,因为这种加锁方式相当于给每个thread一个入场券,在前面的thread执行完成后,就肯定会可以得到lock。
3.如何避免空轮转
上面的硬件给出的原子性指令,在用于构造lock 的时候都会产生许多的空转,为了避免这种情况,该是操作系统介入的时候了。可以有如下的方式。
(1)Just yield,baby!
就是说在获取锁的时候不会去死循环,使用系统调用yield(),这样调度器就不会去调度这个thread。这种方式相对于空循环会节省浪费,但是如果线程多,那么每个线程还是需要去执行一次yield(),并且也不能解决公平性问题。
(2)使用等待队列代替简单的调入调出
typedef struct __lock_t {
int flag;
int guard;
queue_t *q;
} lock_t;
void lock_init(lock_t *m) {
m->flag = 0;
m->guard = 0;
queue_init(m->q);
}
void lock(lock_t *m) {
while (TestAndSet(&m->guard, 1) == 1)
; //acquire guard lock by spinning
if (m->flag == 0) {
m->flag = 1; // lock is acquired
m->guard = 0;
} else {
queue_add(m->q, gettid());
m->guard = 0;
park();
}
}
void unlock(lock_t *m) {
while (TestAndSet(&m->guard, 1) == 1)
; //acquire guard lock by spinning
if (queue_empty(m->q))
m->flag = 0; // let go of lock; no one wants it
else
unpark(queue_remove(m->q)); // hold lock (for next thread!)
m->guard = 0;
}
park()系统调用会将thead sleep,unpark(theadId)则是将对应的thread置为ready状态。这2个系统调用是Solaris提供的系统调用。但是上述的方式有个问题,那就是如果一个thread在park()调用前被切换为另外一个thread运行,后一个thread运行完以后加入unpark第一个thread,就会出现wait/wake up 竞态条件,所以Solaris提供了一个改进的系统调用setPark()。代码改为如下:
queue_add(m->q, gettid());
setpark(); // new code
m->guard = 0;
这样如果出现上面的情况,setpark不会让thread 休眠,而是立刻返回。
(3)linux的futex方式
void mutex_lock (int *mutex) {
int v;
/* Bit 31 was clear, we got the mutex (this is the fastpath) */
if (atomic_bit_test_set (mutex, 31) == 0)
return;
atomic_increment (mutex);
while (1) {
if (atomic_bit_test_set (mutex, 31) == 0) {
atomic_decrement (mutex);
return;
}
/* We have to wait now. First make sure the futex value
we are monitoring is truly negative (i.e. locked). */
v = *mutex;
if (v >= 0)
continue;
futex_wait (mutex, v);
}
}
void mutex_unlock (int *mutex) {
/* Adding 0x80000000 to the counter results in 0 if and only if
there are not other interested threads */
if (atomic_add_zero (mutex, 0x80000000))
return;
/* There are other threads waiting for this mutex,
wake one of them up. */
futex_wake (mutex);
}