First let me just say that I don’t write a lot of concurrent code, it’s not what I do and I’m not very good at it. Because of this, I have very little faith in my own ability to understand what’s going on and I need a very clear specification to have any chance of understanding what I’m doing. For example, lets say I’ve come up with this code:
stuff = {}
def e1_thread1(work_q: SimpleQueue):
sleep(0.5)
stuff[5] = "cool"
work_q.put(5)
def e1_thread2(work_q: SimpleQueue):
while (item := work_q.get()) != None:
print("got some new stuff!", stuff[item])
Can I be certain that sufficient synchronization has happened so that the new item is present and consistent in the stuff dict? I would guess so, but theoretically the queue could have been written to only synchronize the items in the queue and nothing else. In reality it’s difficult to write lock-free queues and I would assume that it’s very unlikely that a lock was not taken and released while adding to the queue. That should be sufficient to make the update to the stuff dict visible as well. So while I can guess that this is intended to work I can’t actually point to a specification that says it’s guaranteed, I can only test it and hope for the best.
While the above is probably fairly clear cut, we can make incremental changes to the code and try to figure out when exactly I would run into dragons.
stuff = {}
def e2_thread1(event: Event):
sleep(0.5)
stuff[5] = "cool"
event.set()
def e2_thread2(event: Event):
event.wait()
print("got some new stuff!", stuff[5])
Lets swap the queue for an Event. Here it’s not as obvious to me that some underlying lock is being taken. Maybe this is lock free? Maybe it’s some OS thingamajig? I really don’t have a clue. So can I still rely on the dict update to always be visible? I guess I can test the code and hope for the best…
flag: bool = False
def e3_thread1():
global flag
sleep(0.5)
stuff[5] = "cool"
flag = True
def e3_thread2():
while not flag:
pass
print("got some new stuff!", stuff[5])
Lets go further and just have a boolean flag, what more could we need, right? Is this still correct and can I rely on the dict update to always be visible? I guess I can test the code and hope for the best…
Today, on my machine, using Linux with Python 3.10.12, temperature of 20C and a relative humidity of 32%, all of the above “works”. Will any of them work tomorrow? Who can say? Since I don’t have any clear specification available, I can only conclude that all of these examples are equally good (or bad).
Just for fun we can go further and do:
text = ""
def e5_thread1():
global text
for i in range(100000):
text += "a"
def e5_thread2():
global text
for i in range(100000):
text += "b"
and when I test this it does indeed produce a string of length 200000. Amazing! I guess I can conclude I never need any form of synchronization in python, everything just works! That must be why none of this seems to be documented anywhere…
So, to summarize, in other languages (I have mainly done c++) concurrency is still very hard, but at least I have some specification that tells me what I need to do and what I can expect. If I keep things simple enough then I can just about figure out how to write a working program and convince my self that I know why it works.
In python I need to rely purely on intuition and testing, and I am not smart enough to produce working threaded code in this way and there is no way that I can convince myself that I know why something works and that it will keep working tomorrow.
(Sorry for the long post…)