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.