Atomicity and Isolation
Assume accounts X and Y contain $100 and $200, respectively. Now, consider the simple task of transferring $10 from account X to account Y. It can be decomposed into a withdrawal task and a deposit task (in any order). Further, assume these tasks either succeed or fail, e.g., withdrawal succeeds by taking out $10 from the account or fails to remove any amount from the account.
Transfer(X, Y):
Withdraw(X, 10)
Deposit(Y, 10)
When we say transfer should be atomic, we mean
- when transfer succeeds, we should observe that both withdrawal and deposit have succeeded.
- when transfer fails, we should observe both withdrawal and deposit have failed.
Atomicity: ensure either all or none of the sub-tasks succeed.
If you are wondering “what about the case where one of withdrawal or deposit fails and the other succeeds?”, then the above requirement disallows such cases. So, in an atomic system P, such cases will either not be allowed to occur or transformed into one of the above two cases when they occur.
When we say transfer should occur in isolation, we mean the effects of sub-tasks of transfer should be hidden from the rest of the system until the transfer completes. Since it is hard to precisely define the the rest of the system, we often say transfer M should occur in isolation from transfer N, i.e., when transfer N is happening, transfer M should not see the effects of the sub-tasks of transfer N and vice versa.
Isolation: ensures the effects of sub-tasks are hidden until the task completes.
Now consider four possible scenarios.
- The transfer is executed neither atomically nor in isolation.
In this case, if the deposit fails, then $10 would be lost as it was withdrawn from account X but not deposited into account Y. Further, while the transfer is happening, we can observe the amount in account X has decreased by $10. Actions taken depending on this observation (e.g., reporting account X has $90) can be “wrong” despite being based on correct information, e.g., report a decrease in balance with no evidence of transfer. - The transfer is executed non-atomically but in isolation.
As in the above case, if the deposit fails, then $10 would be lost in this case as it was withdrawn from account X but not deposited into account Y. However, due to isolation, we can observe the amount in account X has decreased by $10 only after the transfer completes. So, we are left in an inconsistent state as in scenario 1, i.e., decrease in balance with no evidence of transfer. - The transfer is executed atomically but not in isolation.
In this case, due to atomicity, if the deposit fails, then $10 would not be lost as it will be put back into account X to make the withdrawal fail and, consequently, make the transfer fail. However, we can observe the amount in account X decreased by $10 while the transfer is happening and take a “wrong” action as in scenario 1, e.g., incorrectly report account X has $90. - The transfer is executed atomically but not in isolation.
In this case, due to isolation, we can observe the changes to the accounts only after the transfer completes. Due to atomicity, if the deposit fails, then $10 would not be lost as it will be put back into account X to make the withdrawal fail and, consequently, make the transfer fail.
Note that, atomicity is local property limited to one task while isolation is a non-local property that spans multiple tasks that need to be isolated from each other.
In databases, transactions are used to achieve both atomicity and isolation (as in the above scenarios) and transaction primitives ensure both atomicity and isolation.
In concurrent/parallel programming, we rely on mutual exclusion to only achieve isolation. Unlike with transactions, atomicity is achieved by the actions performed within the mutual exclusion region and not ensured by mutual exclusion primitives (except transactional primitives such as STM).