Regardless if the threading behavior changed between version of python, without using a lock, the behavior of incrementing num
across multiple unsynchronized threads is going to be non-deterministic at best. Even with multiple runs on the same interpreter on the same PC, it could generate different results. Because you never know when a context switch on a thread could occur.
This statement:
num += 1
Is just shorthand for something nearly equivalent to this at run time.
REGISTER = num # read memory location into a local registerREGISTER = REGISTER + 1 # increment the valuenum = REGISTER # store back to memory
And since any thread could get preempted by another thread or get scheduled on a different core, or the print call itself could introduce weird timing issues. There's all the cache coherency issue of multiple cores. It's entirely possible something like this is happening at run time.
THREAD 1: REGISTER = num # T1 reads 0 into register<context switch> THREAD 2: REGISTER = num #T2 reads "0" into register REGISTER = REGISTER + 1 #T2 increments register to "1" num = REGISTER #T2 copies register value back to memory<context switch back to thread 1, REGISTER is restored to "0" from before><but the memory location for num is now 1> THREAD 1: REGISTER = REGISTER + 1 #T1 increments register to "1" num = REGISTER #T1 copy register value ("1") back to memory
So as above, it's very easy for two threads to have overlapping access to a variable.
You need a lock if you want consistent behavior of num
getting incremented to 5. An easy update:
lock = Lock() class meThread(threading.Thread): def run(self): global num global lock time.sleep(random.randint(1,3)) # -------------------------------- lock.acquire() num += 1 tmp = num # save value a local tmp value for the subsequent print lock.release() # -------------------------------- print(self.name+'set num to '+str(tmp)) # print the value of what num was incremented to while in the lock, not what num is now
Everything you need to know is here.