Lixing,
Using afterCompletion like this calls unlock at the right time, but it is unsafe for another reason. When there is a conflict, the STM has to pick an order in which to execute the transactions. If you try to impose an external order (using locks, for example) that is different, the system will deadlock.
Another way to think of this is that the STM implicitly acquires read or write locks for all of the Ref-s that are used in a transaction, and it automatically detects any deadlock cycles, breaking them by rolling back one of the transactions. Since it isn't aware of the locks that you are using, it can't detect those deadlock cycles.
If you don't have lots of lock contention, one possibility is to use tryLock, and then to trigger transaction rollback if you can't acquire it. Also, if you are always using this inside a transaction (otherwise the lock would be immediately released) it will be slightly faster to use Txn.findCurrent.
def lock(): Unit = {
implicit txn = Txn.findCurrent.get // this will throw an exception if not inside a txn already
if (!realLock.tryLock()) Txn.rollback
Txn.afterCompletion { _ => realLock.unlock() }
}
This is safe, but if you actually need to wait for locks it turns the waiting into busy-loops (not so good).
Another possibility is to build the lock yourself, using a Ref[Boolean] and ScalaSTM's ability to wait
val lockHeld = Ref(false)
def lock(): Unit = atomic { implicit txn =>
if (lockHeld()) retry
lockHeld() = true
}
def unlock(): Unit = {
lockHeld.single() = false
}
def lockForTxnDuration(): Unit = {
implicit txn = Txn.findCurrent.get
lock()
Txn.afterCompletion { _ => unlock() }
}
This lock isn't as fast as the non-STM aware ones (for most cases it will still be sub-microsecond, though), but it cooperates fully with the STM. If you are going to use this approach and call lock() a lot from outside a transaction, let me know and we can optimize lock() a bit for that case.
- Nathan