测试和设置是做啥用的?
Posted
技术标签:
【中文标题】测试和设置是做啥用的?【英文标题】:What is Test-and-Set used for?测试和设置是做什么用的? 【发布时间】:2008-09-23 13:22:00 【问题描述】:在阅读了 Test-and-Set Wikipedia entry 之后,我仍然想知道“Test-and-Set 的用途是什么?”
我知道您可以使用它来实现 Mutex(如***中所述),但它还有什么其他用途?
【问题讨论】:
【参考方案1】:一个很好的例子是“增量”。
假设有两个线程执行a = a + 1
。假设a
以值100
开头。如果两个线程同时运行(多核),则两者都将a
加载为100
,递增到101
,并将其存储回a
。错了!
使用 test-and-set,您说的是“将 a
设置为 101
,但前提是它当前具有值 100
。”在这种情况下,一个线程将通过该测试,但另一个线程将失败。在失败的情况下,线程可以重试整个语句,这次将a
加载为101
。成功。
这通常比使用互斥锁更快,因为:
-
大多数情况下不存在竞争条件,因此无需获取某种互斥锁即可进行更新。
即使在冲突期间,一个线程也不会被阻塞,而另一个线程只是旋转并重试比将自身挂起以等待某个互斥体更快。
【讨论】:
@jason-cohen:这实际上是对Compare and Swap 的描述。测试和设置通常只涉及值 0 和 1。set 部分是指将指定内存位置的值设置为 1。它返回之前的值,1 或 0,并执行所有这些都在一个原子操作中。 @GregSlepak 是正确的,这是 compare_and_swap。 test_and_set() 接受一个指向目标的布尔指针,将其设置为 TRUE 并返回指针的原始值。如果test_and_set(&lock)的返回值(即&lock的原始值)为真,则进入临界区。 同意@GregSlepak,上面的回复是比较和交换的一个例子,而不是测试和设置。【参考方案2】:您在完成一些工作后想要将数据写入内存的任何时候都可以使用它,并确保自您开始以来另一个线程没有覆盖目标。很多lock/mutex-free algorithms 都采用这种形式。
【讨论】:
【参考方案3】:假设您正在编写一个银行应用程序,并且您的应用程序请求从帐户中提取 10 英镑(是的,我是英国人;))。所以需要将当前账户余额读入一个局部变量,减去取款,然后将余额写回内存。
但是,如果在读取值和写出值之间发生另一个并发请求怎么办?该请求的结果可能会被第一个完全覆盖,并且帐户余额将不正确。
Test-and-set 通过检查覆盖的值是否是您认为的值来帮助我们解决该问题。在这种情况下,您可以检查余额是否是您读取的原始值。由于它是原子的,因此它是不可中断的,因此没有人可以在读取和写入之间从您身下拉出地毯。
解决相同问题的另一种方法是对内存位置进行锁定。不幸的是,锁非常难以正确,难以推理,存在可伸缩性问题并且在面对故障时表现不佳,因此它们不是理想(但绝对实用)的解决方案。测试和设置方法构成了一些软件事务内存的基础,它乐观地允许每个事务同时执行,但如果它们发生冲突则以回滚它们为代价。
【讨论】:
【参考方案4】:考虑到原子性的巨大重要性,基本上,它的用途正是用于互斥锁。就是这样。
Test-and-set 是一种可以使用另外两条指令执行的操作,非原子指令和更快(在多处理器系统上原子性承担硬件开销),因此通常您不会出于其他原因使用它。
【讨论】:
【参考方案5】:当您需要获取共享值、对其执行某些操作并更改该值时使用它,假设另一个线程尚未更改它。
至于实际用途,我最后一次看到它是在并发队列的实现中(可以由多个线程推送/弹出而不需要信号量或互斥体的队列)。
为什么要使用 TestAndSet 而不是互斥锁?因为它通常比互斥锁需要更少的开销。在互斥体需要操作系统干预的情况下,可以将 TestAndSet 实现为 CPU 上的单个原子指令。在具有 100 多个线程的并行环境中运行时,关键代码部分中的单个互斥锁可能会导致严重的瓶颈。
【讨论】:
mutex 内部可能使用测试和设置。你如何比较 TestAndSet 和互斥锁。我觉得比较不合适 在 Python 中,互斥对象明确地有一个“testandset”方法。文档将其称为“原子”,带有引号,这并不能激发信心,但它确实表明这两个概念不是,呃,互斥的。【参考方案6】:也可以用来实现自旋锁:
void spin_lock(struct spinlock *lock)
while (test_and_set(&lock->locked));
【讨论】:
【参考方案7】:Test-and-set Lock (TLS) 用于实现进入临界区。
TLS <destination> <origin>
一般来说,TLS 是由两个步骤组成的原子操作:
-
将值从
origin
复制到destination
将origin
的值设置为1
让我们看看如何使用 TLS CPU 指令实现一个简单的互斥锁进入临界区。
我们需要一个将用作共享资源的内存单元。我们称之为lock
。对我们来说重要的是,我们可以为这个内存单元设置 0 或 1 值。
然后进入临界区会是这样的:
enter_critical_section:
TLS <tmp>, <lock> ; copy value from <lock> to <tmp> and set <lock> to 1
CMP <tmp>, #0 ; check if previous <lock> value was 0
JNE enter_critical_section ; if previous <lock> value was 1, it means that we didn't enter the critical section, and must try again
RET ; if previous <lock> value was 0, we entered critical section, and can return to the caller
要离开临界区,只需将 lock
的值设置回 0:
leave_critical_section:
MOV <lock>, #0
RET
附言
例如,在 x86 中有一个XCHG instruction,它允许与另一个寄存器交换注册表/内存的值。
XCHG <destination> <origin>
XCHG指令进入临界区的实现:
enter_critical_section:
MOV <tmp>, #1
XCHG <tmp>, <lock>
CMP <tmp>, #0
JNE enter_critical_section
RET
【讨论】:
以上是关于测试和设置是做啥用的?的主要内容,如果未能解决你的问题,请参考以下文章
SetPixelFormat() 中的 PIXELFORMATDESCRIPTOR 参数是做啥用的?
cursor.setNotificationUri() 是做啥用的?