It’s very hard to tell precisely since both incCounter(1) and incCounter(2) executions are not in sync, neither do the counter value access. We can only know the counter value can be:
- in the range of [2, 4]
- crash the entire
main or do something magical on your system depending various factors due to unsafe access counter by 2 goroutines
This example, however, does show the importance of synchronization and multi-process guarantees, assuming we treat //do weird thing does not affect the process much.
Why 2, 3, 4, and not one of them
If we look at one incCounter, it first loads the value from the shared variable counter into local variable value. Then it increment the value once before saving it back to the shared variable. The process repeat itself 1 time. Since the shared variable starts at 0 (declared variable without value is always start at nil/0), the increment will pump the value from 0 → 1 → 2.
Now if we look at two incCounter, we got a bunch of possibilities.
1. incCounter(1) and incCounter(2) execute at the same time, access the shared variable safely, write back to shared variable safely.
Both incCounter will always load its local value at 0. Then, both increases their respective value to 2 and save it back to shared variable. This is where you can ensure that you always get 2 as minimum counter value.
2. incCounter(2) starts after incCounter(1), access the shared variable safely, write back to shared variable safely.
This is where incCounter(1) increment the value from 0 → 2 and then incCounter(2) increment the value from 2 → 4.
Since incCounter(2) starts after incCounter(1), we can safely say that by the time incCounter(1) updated the counter value to 2 before incCounter(2) is able to load it to its value local variable. By the time incCounter(2), its loaded value’s value is 2, not 0. Hence, it pumped it to 4.
3. incCounter(2) starts later than (not after) incCounter(1), access the shared variable safely, write back to shared variable safely.
This is where incCounter(2) loads its value when incCounter(1) increases the counter from 0 → 1. Hence, incCounter(2) starts at 1 and by the time it finishes it increments, the final value is 3. Since incCounter(2) starts later, it has the final call to write into counter shared variable. That’s how you landed with 3.
4. Regardless which incCounter start first, they either access/write the shared variable simultaneously
The outcome for this possibility is unpredictable depending on operating system, your processor design, etc.
Most of the time, the application crashes. Sometimes, it crashes the entire operating system, requiring you to restart your computer. Sometimes if the processor is designed for such usage, the circumstances I mentioned just now were forgiven.
This is what we called race condition and precisely, data race condition where 2 or more processes racing to access/write into a shared memory.
5. incCounter(1) starts after incCounter(2), access the shared variable safely, write back to shared variable safely.
Same outcome as possibility No. 2.
6. incCounter(1) starts later (not after) incCounter(2), access the shared variable safely, write back to shared variable safely.
Same outcome as possibility No. 3.
Since we are dealing with possibilities in executions itself, there is no guarantees at all. Sometimes, you get landed on possibility No.2 or No.5; Most of the times, you landed with possibility No.1; Sometimes, No.3 or No.6; Sometimes No.4. If you swap a different hardware, the possibilities changes again.
In computer science, you do not want to gamble the circumstances. That’s why we always want to make sure each goroutines (or all processes) are in sync with each others and eliminate all race conditions, be it data/execution. Only then, you can get a guaranteed result while benefiting from the multi-processes capability.