Cycles
Cross-thread blocking
The interface for blocking across threads now works as follows:
- When one thread
T1wishes to block on a queryQbeing executed by another threadT2, it invokesRuntime::try_block_on. This will check for cycles. Assuming no cycle is detected, it will blockT1untilT2has completed withQ. At that point,T1reawakens. However, we don't know the result of executingQ, soT1now has to "retry". Typically, this will result in successfully reading the cached value. - While
T1is blocking, the runtime moves its query stack (aVec) into the shared dependency graph data structure. WhenT1reawakens, it recovers ownership of its query stack before returning fromtry_block_on.
Cycle detection
When a thread T1 attempts to execute a query Q, it will try to load the value for Q from the memoization tables. If it finds an InProgress marker, that indicates that Q is currently being computed. This indicates a potential cycle. T1 will then try to block on the query Q:
- If
Qis also being computed byT1, then there is a cycle. - Otherwise, if
Qis being computed by some other threadT2, we have to check whetherT2is (transitively) blocked onT1. If so, there is a cycle.
These two cases are handled internally by the Runtime::try_block_on function. Detecting the intra-thread cycle case is easy; to detect cross-thread cycles, the runtime maintains a dependency DAG between threads (identified by RuntimeId). Before adding an edge T1 -> T2 (i.e., T1 is blocked waiting for T2) into the DAG, it checks whether a path exists from T2 to T1. If so, we have a cycle and the edge cannot be added (then the DAG would not longer be acyclic).