面试问:你是如何编写互斥锁的

InterviewQ: How do you code a mutex?

本文关键字:何编写 面试      更新时间:2023-10-16

我很不幸失业了,最近一直在面试。我现在两次面对同样的问题,两次被问到这个问题时都不知所措。

"如何编写互斥体代码"?

从概念上讲,我理解互斥锁锁定代码的某个部分,这样多个线程就不能同时进入关键部分,从而消除了数据竞争。第一次我被要求从概念上描述我将如何编码,第二次我被问到如何编码。我一直在谷歌上搜索,但没有找到任何答案。。。有人能帮忙吗?

谢谢。

有很多方法可以实现互斥锁,但它通常从cpu架构提供原子加核减的一些概念这一基本前提开始。也就是说,可以对内存中的整数变量执行加法运算(并返回结果),而不会被另一个试图访问相同内存位置的线程破坏。或者至少是"原子增量"answers"原子减量"。

例如,在现代的英特尔芯片上,有一种叫做XADD的指令。当与LOCK前缀结合使用时,它会自动执行,并使其他内核中的缓存值无效。gcc为这个指令实现了一个名为__sync_add_and_fetch的包装器。Win32实现了一个类似的函数InterlockedIncrement。两者都只是在暗中呼叫LOCK XADD。其他CPU架构应该提供类似的功能。

因此,最基本的互斥锁可以这样实现。这通常被称为"旋转"锁。而且这个廉价的版本不提供递归进入锁的能力。

// A correct, but poorly performant mutex implementation
void EnterLock(int* lock)
{
while (true)
{
int result = LOCK_XADD(lock,1); // increment the value in lock and return the result atomically
if (result == 1)
{
// if the value in lock was successfully incremented 
// from 0 to 1 by this thread. It means this thread "acquired" the lock
return;
}
LOCK XADD(lock,-1); // we didn't get the lock - decrement it atmoically back to what it was
sleep(0); // give the thread quantum back before trying again
}
}
void LeaveLock(int* lock)
{
LOCK XADD(lock,-1); // release the lock. Assumes we successfully acquired it correctly with EnterLock above
}

以上内容的"旋转"性能较差,无法保证任何公平性。优先级较高的线程可以继续赢得EnterLock对优先级较低的线程的争夺。程序员可能会犯错误,用以前没有调用EnterLock的线程调用LeaveLock。您可以扩展以上内容,以便对一个数据结构进行操作,该数据结构不仅包括锁整数,而且还保留了所有者线程id和递归计数的记录。

实现互斥的第二个概念是,操作系统可以提供一个等待和通知服务,这样线程在所有者线程释放它之前就不必旋转。等待锁定的线程或进程可以向操作系统注册自己,使其进入睡眠状态,直到所有者线程释放为止。用操作系统的术语来说,这被称为信号量。此外,操作系统级别的信号量还可以用于实现不同进程之间的锁定,以及CPU不提供原子添加的情况。并且可以用于保证试图获取锁的多个线程之间的公平性。

大多数实现都会尝试旋转多次,然后再返回到进行系统调用。

我不会说这是一个愚蠢的问题。在职位的任何抽象层面上。在高级上,您只需说,您使用标准库或任何线程库。如果你申请编译器开发人员的职位,你需要了解它是如何实际工作的,以及实现需要什么。

要实现互斥,您需要一个锁定机制,也就是说,您需要有一个可以标记为跨所有线程占用的资源。这不是小事。您需要记住,两个核心共享内存,但它们有缓存。这条信息必须保证是真实的。因此,您确实需要对硬件的支持来确保原子性。

如果你研究clang的实现,他们(至少在一次情况下)将实现卸载到pthreads,线程支持中的typedefs:

#if defined(_LIBCPP_HAS_THREAD_API_PTHREAD)
# include <pthread.h>
# include <sched.h>
#elif defined(_LIBCPP_HAS_THREAD_API_WIN32)
#include <Windows.h>
#include <process.h>
#include <fibersapi.h>
#endif

若您深入研究pthreads-reo,您可以找到互锁操作的asm实现。它们依赖于使操作原子化的lockasm关键字,即没有其他线程可以同时执行它们。这消除了比赛条件,并保证了连贯性。

基于此,您可以构建一个lock,用于mutex实现。