Learning Clojure/Concurrent Programming
Refs
[edit | edit source]A Ref, like a Var, is a storage cell for holding another object, but a Ref is intended to serve a very different purpose, that of storing concurrently shared data.
- A Ref can only be modified inside a block of code called a transaction.
- A Ref can be read at any time, including outside of a transaction.
- In a transaction, changes to the Ref by other threads are not seen. A transaction sees the value a Ref had at the start of the transaction; if the transaction itself changes the Ref's value, that is the value seen in the rest of the transaction (or until it changes the value again).
- Changes to a Ref made in a transaction will not be seen by other threads until the transaction completes and has been committed.
- If an exception causes exit out of a transaction, changes in that transaction will never be committed and are hence lost.
- A transaction commit will fail if, during the transaction's run, a Ref modified by this transaction is successfully committed to by another transaction.
- A transaction is automatically retried until it successfully commits. (Consequently, transactions usually should be side-effect free lest the side effects get performed multiple times.)
Effectively, when two transactions overlapping in time modify the same Ref, the first to finish will succeed but the second will fail and retry. The concurrent modification of shared data is thereby sequenced into discrete chunks of code, each with a consistent view of that shared data.
Sometimes, however, you want Ref-mutating operations to succeed regardless of other transactions: a commute operation applies a mutating operation to a Ref, changing its value for the remainder of the transaction like normal, but the commit will not fail on account of commits to this Ref by other transactions: if no other transaction has committed to the Ref, this transaction's local Ref value is committed; otherwise, the transaction-local value is discarded and instead the mutating operation is applied again, this time to the new current Ref value, and the resulting value is committed. In practice, commute operations should be commutative (hence the name): a set of commutative operations can be applied in any order to get the same result. For instance, incrementing a counter is commutative such that it only matters how many times a counter is incremented, not the order in which the increments are performed.
Agents
[edit | edit source]Unlike a Var or Ref, an Agent is always just a single reference. An Agent is modified only by requests sent to a queue, where the requests are processed asynchronously in a thread pool but guaranteed to run in the order the requests are received. The state of the agent acted upon in these requests is the state established by the previously processed request (which is not necessarily the state at the time of the request because, when a new request is made, earlier requests may still be pending).
An agent can be read at any time, but the value read is the current one, not the value resulting from the completion of all pending requests. If you want the "latest" value, you can await the agent, meaning you can block the current thread until all pending requests on the agent have gone through (not including new requests made during this blocking time).
When a request is made in a transaction, the request isn't submitted to the queue until the transaction successfully commits.