One of the most important parts of the operating system is the task scheduler. This acts as the maestro of the orchestra, and directs everything with impeccable precision and incredible timing and discipline. This maestro has only one real goal, and that is to ensure that every task has a chance to run through till completion; the when and where of a task's execution, however, is non-deterministic. That is to say, if we gave a task scheduler two identical competing processes one after the other, there is no guarantee that the first process will complete first. This non-deterministic nature is what makes concurrent programming so challenging.
An excellent example that highlights this non-deterministic behavior is the following code:
import threading
import time
import random
counter = 1
def workerA():
global counter
while counter < 1000:
counter += 1
print("Worker A is incrementing counter to {}".format(counter))
sleepTime = random.randint(0,1)
time.sleep(sleepTime)
def workerB():
global counter
while counter > -1000:
counter -= 1
print("Worker B is decrementing counter to {}".format(counter))
sleepTime = random.randint(0,1)
time.sleep(sleepTime)
def main():
t0 = time.time()
thread1 = threading.Thread(target=workerA)
thread2 = threading.Thread(target=workerB)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
t1 = time.time()
print("Execution Time {}".format(t1-t0))
if __name__ == '__main__':
main()
Here in the preceding code, we have two competing threads in Python that are each trying to accomplish their own goal of either decrementing the counter to 1,000, or conversely incrementing it to 1,000. In a single-core processor, there is the possibility that worker A manages to complete its task before worker B has a chance to execute, and the same can be said for worker B. However, there is a third potential possibility, and that is that the task scheduler continues to switch between worker A and worker B an infinite number of times and never completes.
The preceding code, incidentally, also shows one of the dangers of multiple threads accessing shared resources without any form of synchronization. There is no accurate way to determine what will happen to our counter, and as such, our program could be considered unreliable.