TReentrantLock
(可重入锁)允许效果性地、安全地并发访问某些可变状态,从而允多个纤程读取其中的状态(因为并发读取是安全的),但是只有一个纤程可以修改状态(以防止数据损坏)。此外,TReentrantLock
是使用 STM 实现的,读写操作都可以当作事务进行提交,因此可以将其用作纯 ZIO effect 解决方案的基石,并在内部允许以简单且可组合的方式锁定多个状态(感谢 STM)。
TReentrantLock 是可重入
的读/写锁。可重入锁是一个纤程可以多次取得而不会对其自身造成阻塞的锁。在难以跟踪您是否已经获得锁的情况下,此功能很有用。如果锁是不可重入的,则在你获得该锁后再次去获得它时会被阻塞,从而实际上导致了死锁。
语义
该锁对读取方和写入方都提供了重新获得读取或写入锁的重入保证。在释放由写纤程持有的所有写锁之前,不允许读纤程进入。除非没有其他纤程持有写入锁,或者想要写入的纤程已经持有读取锁且没有其他纤程也持有读取锁,否则不允许写纤程进入。
此锁还允许从读锁升级到写锁(自动),以及从写锁降级到读锁(前提是您从读锁升级到写锁是自动的)。
创建一个可重入锁
import zio.stm._
val reentrantLock = TReentrantLock.make
获取一个读锁
import zio.stm._
val program =
(for {
lock <- TReentrantLock.make
_ <- lock.acquireRead
rst <- lock.readLocked // lock is read-locked once transaction completes
wst <- lock.writeLocked // lock is not write-locked
} yield rst && !wst).commit
获取一个写锁
import zio._
import zio.stm._
val writeLockProgram: UIO[Boolean] =
(for {
lock <- TReentrantLock.make
_ <- lock.acquireWrite
wst <- lock.writeLocked // lock is write-locked once transaction completes
rst <- lock.readLocked // lock is not read-locked
} yield !rst && wst).commit
多个纤程可同时持有读锁
import zio._
import zio.stm._
val multipleReadLocksProgram: UIO[(Int, Int)] = for {
lock <- TReentrantLock.make.commit
fiber0 <- lock.acquireRead.commit.fork // fiber0 acquires a read-lock
currentState1 <- fiber0.join // 1 read lock held
fiber1 <- lock.acquireRead.commit.fork // fiber1 acquires a read-lock
currentState2 <- fiber1.join // 2 read locks held
} yield (currentState1, currentState2)
锁升级和降级
如果您的纤程已经持有读锁,则可以将其升级为写锁,前提是(除了当前纤程外)没有其他读取纤程持有该锁。
import zio._
import zio.stm._
val upgradeDowngradeProgram: UIO[(Boolean, Boolean, Boolean, Boolean)] = for {
lock <- TReentrantLock.make.commit
_ <- lock.acquireRead.commit
_ <- lock.acquireWrite.commit // upgrade
isWriteLocked <- lock.writeLocked.commit // now write-locked
isReadLocked <- lock.readLocked.commit // and read-locked
_ <- lock.releaseWrite.commit // downgrade
isWriteLockedAfter <- lock.writeLocked.commit // no longer write-locked
isReadLockedAfter <- lock.readLocked.commit // still read-locked
} yield (isWriteLocked, isReadLocked, isWriteLockedAfter, isReadLockedAfter)
在有争议的情况下获取写锁定
只有满足以下条件之一,才能立即获取写锁定:
- 没有其他的(写入)锁持有人。
- 当前的纤程已经持有读取锁,且没有其他方持有读取锁。
如果以上两种情况都不满足,则尝试获取写锁定将在语义上阻塞当前纤程。这是一个示例,说明仅当所有其他读取器(尝试获取写锁的纤程除外)释放对(读或写)锁的锁定后,该纤程才能获得写锁。
import zio._
import zio.clock._
import zio.console._
import zio.stm._
import zio.duration._
val writeLockDemoProgram: URIO[Console with Clock, Unit] = for {
l <- TReentrantLock.make.commit
_ <- putStrLn("Beginning test")
f1 <- (l.acquireRead.commit *> ZIO.sleep(5.seconds) *> l.releaseRead.commit).fork
f2 <- (l.acquireRead.commit *> putStrLn("read-lock") *> l.acquireWrite.commit *> putStrLn("I have upgraded!")).fork
_ <- (f1 zip f2).join
} yield ()
此例中纤程 f1 获得读取锁定,并在释放它之前休眠 5 秒钟。同时纤程 f2 尝试获取读锁定,并立即尝试获取写锁定。但是,f2 必须在语义上阻塞大约5 秒钟才能获得写锁,因为 f1 到时才将释放其对锁的保留,然后 f2 才能获取写锁定。
更安全的方法(readLock
and writeLock
)
对于一些简单的使用案例,应避免使用 acquireRead
,acquireWrite
,releaseRead
和 releaseWrite
,而应该使用诸如 readLock
和 writeLock
之类的方法。借助 Managed
构造,readLock
和 writeLock
可以自动获取并释放锁。下面的程序是一个比上面的例子更安全的版本,它确保一旦通过可重入锁完成操作,我们就不会占用(会释放)任何资源。
import zio._
import zio.clock._
import zio.console._
import zio.stm._
import zio.duration._
val saferProgram: URIO[Console with Clock, Unit] = for {
lock <- TReentrantLock.make.commit
f1 <- lock.readLock.use_(ZIO.sleep(5.seconds) *> putStrLn("Powering down")).fork
f2 <- lock.readLock.use_(lock.writeLock.use_(putStrLn("Huzzah, writes are mine"))).fork
_ <- (f1 zip f2).join
} yield ()