Test-and-Set的作用是什么?
在阅读了Test-and-Set Wikipedia条目之后,我仍然有一个问题:" Test-and-Set将用于什么目的?"
我意识到我们可以使用它来实现Mutex(如Wikipedia中所述),但是它还有什么其他用途?
解决方案
基本上,鉴于原子性的重要性,它的用途恰好是互斥锁。而已。
测试设置是可以用其他两条指令(非原子指令和更快指令)执行的操作(在多处理器系统上,原子性承担硬件开销),因此通常我们不会出于其他原因而使用它。
完成某些工作后,我们可以随时使用它来将数据写入内存,并确保自启动以来其他线程没有覆盖目标。许多无锁/互斥的算法都采用这种形式。
一个很好的例子是"增量"。
假设有两个线程执行" a = a + 1"。说" a"以值" 100"开头。如果两个线程同时运行(多核),则两个线程都将a加载为100,增加到101并将其存储回a。错误的!
使用test-and-set时,我们说的是"将a设置为101,但前提是它当前的值为100"。在这种情况下,一个线程将通过该测试,而另一个将失败。在失败的情况下,线程可以重试整个语句,这次将" a"加载为" 101"。成功。
通常,这比使用互斥锁更快,因为:
- 在大多数情况下,没有竞争条件,因此无需获取某种互斥量即可进行更新。
- 即使在冲突期间,一个线程也不会被阻塞,而另一个线程旋转并重试的速度要比将其自身挂起以保持某些互斥锁的速度更快。
当我们需要获取共享值,对其进行某些操作并更改该值(假定另一个线程尚未更改它)时,将使用它。
至于实际用途,我最后一次看到它是在并发队列的实现中(可以由多个线程推送/弹出的队列,而无需信号量或者互斥量)。
为什么要使用TestAndSet而不是互斥锁?因为与互斥锁相比,它通常需要较少的开销。在互斥锁需要操作系统干预的情况下,可以将TestAndSet实现为CPU上的单个原子指令。在具有100个线程的并行环境中运行时,代码关键部分中的单个互斥锁可能会导致严重的瓶颈。
假设我们正在编写银行应用程序,并且应用程序要求从帐户中提取10英镑(是的,我是英国人;))。因此,我们需要将当前帐户余额读入局部变量,减去提款额,然后将余额写回到内存中。
但是,如果在读取值和将其写出之间又发生了另一个并发请求,该怎么办?该请求的结果可能会被第一个请求的结果完全覆盖,并且帐户余额可能不正确。
测试设置通过检查覆盖值是否符合期望值来帮助我们解决该问题。在这种情况下,我们可以检查余额是否为我们读取的原始值。由于它是原子的,所以它是不间断的,因此没有人可以在读写之间将地毯从下方拉出。
解决相同问题的另一种方法是对内存位置进行锁定。不幸的是,锁很难正确解决,难以推理,存在可伸缩性问题并且在发生故障时表现不佳,因此它们不是理想的(但绝对实用)的解决方案。测试和设置方法构成了一些软件事务存储的基础,这些存储事务乐观地允许每个事务并发执行,但如果冲突则将它们全部回滚。