我听说过这些与并发编程有关的词,但是锁、互斥量和信号量有什么区别?
锁只允许一个线程进入被锁定的部分,并且该锁不与任何其他进程共享。
互斥锁与锁相同,但它可以是系统范围的(由多个进程共享)。
semaphore 与互斥锁的作用相同,但允许 x 个线程进入,例如,这可用于限制同时运行的 cpu、io 或 ram 密集型任务的数量。
有关互斥量和信号量之间差异的更详细的文章,请阅读 here。
您还拥有读/写锁,在任何给定时间允许无限数量的读者或 1 个作者。
对这些词有很多误解。
这是来自之前的帖子 (https://stackoverflow.com/a/24582076/3163691),非常适合这里:
1) 临界区 = 用户对象,用于允许在一个进程中仅执行来自许多其他线程的一个活动线程。其他未选择的线程(@获取此对象)进入睡眠状态。
[没有进程间能力,非常原始的对象]。
2) Mutex Semaphore (aka Mutex)= 内核对象,用于允许在不同进程中仅执行来自许多其他线程的一个活动线程。其他未选择的线程(@获取此对象)进入睡眠状态。该对象支持线程所有权、线程终止通知、递归(来自同一线程的多个“获取”调用)和“避免优先级反转”。
[进程间能力,使用非常安全,一种'高级'同步对象]。
3) Counting Semaphore (aka Semaphore)= 用于允许执行来自许多其他线程的一组活动线程的内核对象。其他未选择的线程(@获取此对象)进入睡眠状态。
[但是,进程间功能使用起来不是很安全,因为它缺少以下“互斥”属性:线程终止通知、递归?、“避免优先级反转”?等]。
4)现在,谈论“自旋锁”,首先是一些定义:
关键区域 = 由 2 个或更多进程共享的内存区域。
Lock= 一个变量,其值允许或拒绝进入“关键区域”。 (它可以实现为一个简单的“布尔标志”)。
忙等待=不断测试变量,直到出现某个值。
最后:
自旋锁(又名自旋锁)= 使用忙等待的锁。 (锁的获取是通过 xchg 或类似的原子操作进行的)。
[无线程休眠,主要仅在内核级别使用。用户级代码效率低下]。
作为最后的评论,我不确定,但我可以向你打赌,上面的前 3 个同步对象(#1、#2 和 #3)利用这个简单的野兽(#4)作为其实现的一部分。
祝你有美好的一天!。
参考:
-Qing Li 和 Caroline Yao 合着的嵌入式系统的实时概念(CMP 书籍)。
-Andrew Tanenbaum(培生国际教育)的现代操作系统(第 3 次)。
Jeffrey Richter(Microsoft 编程系列)的 Microsoft Windows 编程应用程序(第 4 期)。
另外,您可以看看:https://stackoverflow.com/a/24586803/3163691
大多数问题都可以使用(i)仅锁,(ii)仅信号量,...或(iii)两者的组合来解决!正如您可能已经发现的那样,它们非常相似:都阻止 race conditions,都具有 acquire()
/release()
操作,都导致零个或多个线程被阻塞/怀疑......真的,关键区别仅在于他们如何锁定和解锁。
锁(或互斥锁)有两种状态(0 或 1)。它可以解锁或锁定。它们通常用于确保一次只有一个线程进入关键部分。
一个信号量有很多状态(0, 1, 2, ...)。它可以被锁定(状态 0)或解锁(状态 1、2、3,...)。一个或多个信号量通常一起使用,以确保当某些资源的单元数已/未达到特定值时(通过向下计数到该值或向上计数到该值),只有一个线程准确地进入临界区)。
对于这两种锁/信号量,在原语处于状态 0 时尝试调用 acquire()
会导致调用线程被挂起。对于锁 - 尝试获取处于状态 1 的锁是成功的。对于信号量 - 尝试在状态 {1, 2, 3, ...} 中获取锁是成功的。
对于状态为 0 的锁,如果之前调用 acquire()
的 same 线程现在调用释放,则释放成功。如果一个不同的线程尝试了这个——它取决于实现/库会发生什么(通常忽略尝试或抛出错误)。对于状态 0 的信号量,任何 线程都可以调用 release 并且它会成功(无论之前哪个线程使用获取将信号量置于状态 0)。
从前面的讨论中,我们可以看到锁具有所有者的概念(可以调用释放的唯一线程是所有者),而信号量没有所有者(任何线程都可以在信号量上调用释放)。
造成很多混乱的原因是,实际上它们是这个高级定义的许多变体。
需要考虑的重要变化:
应该调用 acquire()/release() 什么? -- [变化很大]
您的锁/信号量是否使用“队列”或“集合”来记住等待的线程?
您的锁/信号量可以与其他进程的线程共享吗?
你的锁是“可重入的”吗? -- [通常是的]。
您的锁是“阻塞/非阻塞”吗? -- [通常非阻塞被用作阻塞锁(又名自旋锁)导致忙等待]。
您如何确保操作是“原子的”?
这些取决于您的书/讲师/语言/图书馆/环境。这是一个快速浏览,注意一些语言如何回答这些细节。
C、C++(线程)
互斥锁通过 pthread_mutex_t 实现。默认情况下,它们不能与任何其他进程共享(PTHREAD_PROCESS_PRIVATE),但是互斥锁有一个名为 pshared 的属性。设置后,互斥锁在进程之间共享(PTHREAD_PROCESS_SHARED)。
锁与互斥锁是一样的。
信号量是通过 sem_t 实现的。与互斥锁类似,信号量可以在多个进程的线程之间共享,也可以对单个进程的线程保持私有。这取决于提供给 sem_init 的 pshared 参数。
蟒蛇(线程.py)
锁 (threading.RLock) 与 C/C++ pthread_mutex_ts 基本相同。两者都是可重入的。这意味着它们只能由锁定它的同一线程解锁。 sem_t 信号量、threading.Semaphore 信号量和 theading.Lock 锁是不可重入的——因为任何线程都可以执行解锁锁定/关闭信号量。
互斥锁与锁相同(该术语在 python 中不常用)。
信号量(threading.Semaphore)与 sem_t 大体相同。尽管使用 sem_t,线程 id 的队列用于记住线程在锁定时尝试锁定它时被阻塞的顺序。当一个线程解锁一个信号量时,队列中的第一个线程(如果有的话)被选为新的所有者。线程标识符从队列中取出,信号量再次锁定。但是,使用 threading.Semaphore 时,使用集合而不是队列,因此不会存储线程被阻塞的顺序——集合中的任何线程都可以被选为下一个所有者。
Java (java.util.concurrent)
锁 (java.util.concurrent.ReentrantLock) 与 C/C++ pthread_mutex_t 和 Python 的 threading.RLock 大体相同,因为它也实现了可重入锁。由于 JVM 充当中介,Java 中的进程之间共享锁更加困难。如果线程试图解锁它不拥有的锁,则会抛出 IllegalMonitorStateException。
互斥锁与锁相同(该术语在 Java 中不常用)。
信号量 (java.util.concurrent.Semaphore) 与 sem_t 和 threading.Semaphore 基本相同。 Java 信号量的构造函数接受一个公平布尔参数,该参数控制是使用集合 (false) 还是队列 (true) 来存储等待线程。
理论上,信号量经常被讨论,但在实践中,信号量的使用并不多。一个信号量只保存一个整数的状态,所以它通常相当不灵活,一次需要很多——导致难以理解代码。此外,任何线程都可以释放信号量这一事实有时是不受欢迎的。而是使用更多面向对象/更高级别的同步原语/抽象,例如“条件变量”和“监视器”。
看看 John Kopplin 的 Multithreading Tutorial。
在线程间同步一节中,他解释了事件、锁、互斥、信号量、等待定时器的区别
互斥锁一次只能由一个线程拥有,使线程能够协调对共享资源的互斥访问 临界区对象提供的同步类似于互斥锁对象提供的同步,但临界区对象只能由单个进程 互斥体和临界区之间的另一个区别是,如果临界区对象当前由另一个线程拥有,则 EnterCriticalSection() 会无限期地等待所有权,而与互斥体一起使用的 WaitForSingleObject() 允许您指定一个超时 信号量维持在零和某个最大值之间的计数,限制同时访问共享资源的线程数。
我将尝试用示例来介绍它:
锁:您将使用 lock
的一个示例是共享字典,其中添加了项目(必须具有唯一键)。
锁将确保一个线程不会进入正在检查字典中的项目的代码机制,而另一个线程(即在关键部分中)已经通过了此检查并正在添加该项目。如果另一个线程试图输入一个锁定的代码,它将等待(被阻塞)直到对象被释放。
private static readonly Object obj = new Object();
lock (obj) //after object is locked no thread can come in and insert item into dictionary on a different thread right before other thread passed the check...
{
if (!sharedDict.ContainsKey(key))
{
sharedDict.Add(item);
}
}
信号量:假设您有一个连接池,那么单个线程可能会通过等待信号量获得连接来保留池中的一个元素。然后它使用连接,当工作完成时通过释放信号量来释放连接。
Code example that I love is one of bouncer given by @Patric - 这里是:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace TheNightclub
{
public class Program
{
public static Semaphore Bouncer { get; set; }
public static void Main(string[] args)
{
// Create the semaphore with 3 slots, where 3 are available.
Bouncer = new Semaphore(3, 3);
// Open the nightclub.
OpenNightclub();
}
public static void OpenNightclub()
{
for (int i = 1; i <= 50; i++)
{
// Let each guest enter on an own thread.
Thread thread = new Thread(new ParameterizedThreadStart(Guest));
thread.Start(i);
}
}
public static void Guest(object args)
{
// Wait to enter the nightclub (a semaphore to be released).
Console.WriteLine("Guest {0} is waiting to entering nightclub.", args);
Bouncer.WaitOne();
// Do some dancing.
Console.WriteLine("Guest {0} is doing some dancing.", args);
Thread.Sleep(500);
// Let one guest out (release one semaphore).
Console.WriteLine("Guest {0} is leaving the nightclub.", args);
Bouncer.Release(1);
}
}
}
Mutex 它几乎是 Semaphore(1,1)
并且经常在全球范围内使用(应用程序范围,否则可以说 lock
更合适)。从全局可访问的列表中删除节点时,将使用全局 Mutex
(在删除节点时您希望另一个线程做某事的最后一件事)。当您获取 Mutex
时,如果不同的线程尝试获取相同的 Mutex
,它将进入睡眠状态,直到获取 Mutex
的 SAME 线程释放它。
Good example on creating global mutex is by @deepee
class SingleGlobalInstance : IDisposable
{
public bool hasHandle = false;
Mutex mutex;
private void InitMutex()
{
string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
string mutexId = string.Format("Global\\{{{0}}}", appGuid);
mutex = new Mutex(false, mutexId);
var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
var securitySettings = new MutexSecurity();
securitySettings.AddAccessRule(allowEveryoneRule);
mutex.SetAccessControl(securitySettings);
}
public SingleGlobalInstance(int timeOut)
{
InitMutex();
try
{
if(timeOut < 0)
hasHandle = mutex.WaitOne(Timeout.Infinite, false);
else
hasHandle = mutex.WaitOne(timeOut, false);
if (hasHandle == false)
throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
}
catch (AbandonedMutexException)
{
hasHandle = true;
}
}
public void Dispose()
{
if (mutex != null)
{
if (hasHandle)
mutex.ReleaseMutex();
mutex.Dispose();
}
}
}
然后像这样使用:
using (new SingleGlobalInstance(1000)) //1000ms timeout on global lock
{
//Only 1 of these runs at a time
GlobalNodeList.Remove(node)
}
希望这可以节省您一些时间。
Wikipedia 在 differences between Semaphores and Mutexes 上有一个很棒的部分:
互斥体本质上与二进制信号量相同,有时使用相同的基本实现。它们之间的区别是: 互斥体有一个所有者的概念,也就是锁定互斥体的进程。只有锁定互斥锁的进程才能解锁它。相反,信号量没有所有者的概念。任何进程都可以解锁信号量。与信号量不同,互斥锁提供优先级反转安全。由于互斥锁知道它的当前所有者,因此只要更高优先级的任务开始等待互斥锁,就可以提升所有者的优先级。互斥锁还提供删除安全性,其中持有互斥锁的进程不会被意外删除。信号量不提供此功能。
我的理解是,互斥锁只能在单个进程中使用,但可以跨多个线程使用,而信号量可以跨多个进程使用,并且可以跨对应的线程集使用。
此外,互斥锁是二进制的(它要么被锁定,要么被解锁),而信号量有一个计数的概念,或者一个包含多个锁定和解锁请求的队列。
有人可以验证我的解释吗?我是在 Linux 的上下文中说的,特别是 Red Hat Enterprise Linux (RHEL) 版本 6,它使用内核 2.6.32。
在 Linux 变体上使用 C 编程作为示例的基本案例。
锁:
• 通常是一个非常简单的构造二进制操作,无论是锁定的还是解锁的
• 没有线程所有权、优先级、排序等概念。
• 通常是一个自旋锁,其中线程不断检查锁的可用性。
• 通常依赖于原子操作,例如Test-and-set、compare-and-swap、fetch-and-add 等。
• 通常需要硬件支持原子操作。
文件锁:
• 通常用于协调通过多个进程对文件的访问。
• 多个进程可以持有读锁,但是当任何单个进程持有写锁时,不允许其他进程获得读锁或写锁。
• 示例:flock、fcntl 等。
互斥体:
• Mutex 函数调用通常在内核空间中工作并导致系统调用。
• 它使用所有权的概念。只有当前持有互斥锁的线程才能解锁它。
• Mutex 不是递归的(例外:PTHREAD_MUTEX_RECURSIVE)。
• 通常与条件变量关联使用并作为参数传递给例如pthread_cond_signal、pthread_cond_wait 等。
• 一些UNIX 系统允许多个进程使用互斥锁,尽管这可能并非在所有系统上都强制执行。
信号:
• 这是一个内核维护的整数,其值不允许低于零。
• 可用于同步进程。
• 信号量的值可以设置为大于 1 的值,在这种情况下,该值通常表示可用资源的数量。
• 值限制为 1 和 0 的信号量称为二进制信号量。
锁、互斥量、信号量
这是一个普遍的愿景。细节取决于真实的语言实现
lock
- 线程同步工具。当线程获得锁时,它成为能够执行代码块的单个线程。所有其他线程都被阻塞。只有拥有锁的线程才能解锁
mutex
- 互斥锁。它是一种锁。在某些语言上它是进程间机制,在某些语言上它是 lock
的同义词。例如 Java 在 synchronised
和 java.util.concurrent.locks.Lock
中使用 lock
semaphore
- 允许多个线程访问共享资源。您可以发现mutex
也可以由semaphore
实现。它是一个独立的对象,管理对共享资源的访问。您会发现任何线程都可以signal
解除阻塞。它也用于发信号
Supporting ownership
、maximum number of processes share lock
和 maximum number of allowed processes/threads in critical section
是决定通用名称为 lock
的并发对象的名称/类型的三个主要因素。由于这些因子的值是二元的(有两种状态),我们可以将它们汇总在一个 3*8 的类真值表中。
X(支持所有权?):否(0)/是(1)
Y(#共享进程):> 1 (∞) / 1
Z(CA 中的#processes/threads):> 1 (∞) / 1
X Y Z Name
--- --- --- ------------------------
0 ∞ ∞ Semaphore
0 ∞ 1 Binary Semaphore
0 1 ∞ SemaphoreSlim
0 1 1 Binary SemaphoreSlim(?)
1 ∞ ∞ Recursive-Mutex(?)
1 ∞ 1 Mutex
1 1 ∞ N/A(?)
1 1 1 Lock/Monitor
随意编辑或扩展此表,我已将其发布为可编辑的 ascii 表:)