在计算机编程的世界里,互斥锁(mutex)和信号量(semaphore)是两个基础而重要的概念。但在深入讨论这些概念之前,让先回到计算机程序的早期时代:那个有着糟糕发型、大量发胶和臭氧层逐渐稀薄的时代。
或许已经知道,或者如果是一个资深人士,甚至可能还记得,在80年代和90年代初的快乐时光里,可以在一台机器上同时运行同一个程序多次:多个窗口一个接一个地打开,可以继续打开越来越多的相同程序的窗口。有时,由于计算机资源不足,这会导致崩溃。
今天,在大多数情况下,希望程序在每台机器上只运行一次。例如,想象一下在同一台计算机上两次打开同一个Excel文档,两个人分别在每个文档上工作。
显然,除非重命名一个文档,否则如何管理两个实例中的更改?因此,MS Office只允许一次运行一个文档实例。让也假设拥有一个会计软件,它由办公室的人运行。没有理由在同一台机器上多次运行它。这也是用户友好设计的问题。如果用户不知道软件正在运行并启动了它的新实例,它将不会运行,而是将焦点带到已经运行的一个实例上。
还有另一个方面:有时,需要确保代码不会同时访问共享资源。把代码想象成在运行一个铁路网:不希望两列火车同时在同一条轨道上运行——希望它们同步运行,每列火车都按照它独特的时间表运行。程序需要同步运行其所有组件和资源(尽管有些程序是异步运行的)。
现在理解了问题,可以介绍解决方案之一:
互斥锁,全称为互斥对象(Mutual Exclusion Object)。C++标准库提供了std::mutex,它是一个用于保护共享资源不被多个线程或实例同时访问的机制。互斥锁是一种原始类型,提供独占的、非递归的所有权语义:
拥有互斥锁的程序、程序中的线程以及代码块,都是受保护的,这样其他实例或线程就无法运行相同的程序、线程或代码块——它们将被锁定,这将确保在任何给定时间只运行一次。
当另一个资源已经锁定了互斥锁时,尝试获取所有权将返回一个false值,这样调用方就知道它已经被锁定了。回到例子,如果会计软件的互斥锁被锁定,将找到已经运行的软件实例,并将焦点转移到它上面。
把互斥锁想象成语音信箱——当有人打电话给座机并在语音信箱中开始录制消息时,没有人可以同时打电话并留下消息——语音信箱被锁定了。当一个人完成录制消息时,另一个人可以打电话并留下消息。在这种情况下,“互斥锁”是局部于语音信箱的。可以给其他人留言,但在某人录制消息的过程中,特定语音信箱是被锁定的。当然,可以设计一个可以同时录制多达五个或十个消息的语音信箱,但大多数语音信箱按设计只允许单次录制。这是互斥锁的基本思想——目前不需要了解更多。
另一种控制资源访问的方法是使用信号量,它帮助同步程序。这种方法是由荷兰计算机科学家Edsger W. Dijkstra发明的,他最初在1965年设计信号量作为火车信号的方法。信号量是一个在线程之间共享的变量,可以简化为一个队列和一个计数器的数据结构。计数器初始化为0或大于0的值。使用这种结构,信号量运行两个非常简单的核心操作:
wait——当信号量被wait获取时,如果计数器不为0,其他线程仍然可以使用它。所以,如果初始值是十,这意味着允许十个线程使用这个资源,当它们每个获取信号量时,计数器就会减少,直到达到0。
signal或release——一旦线程完成使用资源,signal或release操作被用来释放信号量,那么在这种情况下,计数器就会增加。
如果这一切都太令人困惑,只要把信号量想象成Covid-19时期的便利店保安:在任何给定时刻,只有少数人被允许进入封闭的场所。有一个队列,然后有人被允许进入。在信号量中,“wait”意味着让某人进入,并减少可以被允许进入的人数计数器,如果那个计数器现在是0,就没人可以进去。当有人离开商店(“release”),计数器增加一个,所以另一个人可以进去。也可以将其比作火车信号机制,这是它的原始设计。