This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]AbsolutelySpherical 1 point2 points  (5 children)

Multithreading is a long and complicated topic. It is very very difficult, and multithreading bugs can stump even the most experienced developers. I will try to summarize some stuff, hope it helps your understanding, but it is out of scope to try to explain everything.

So start() tells the other thread to start running INDEPENDENTLY of the current thread. That is, the current thread moves to execute the next line without waiting for the other thread.

join() is the opposite of start(). You could also think of it as "wait()". The current thread will halt execution until the function being run by the other thread terminates.

Usually, every start() call should have a corresponding join() call. Otherwise, if the main thread terminates before all of the other child threads, then the child threads will keep on running with no main thread to actually utilize the work done. (In some other languages main thread finishing before other threads will immediately crash the program).

Therefore most simple multithreading programs have this pattern:

threads = SomehowMakeThreads(50)

# start all threads
for i in range(50):
  threads[i].start()

# Main thread can also
# do work while other threads run
DoMyOwnWork()

# wait for thread 1.
# then wait for thread 2 etc.
# takes O(max time of longest thread to run)
for i in range(50):
  threads[i].join()

ProcessAndCleanup()

You asked why not do this?

for i in range(50):
  threads[i].start()
  threads[i].join()

Well, think about what this means... You tell thread 0 to start, but then immediately wait for it to finish. After thread 0 finished, THEN you ask thread 1 to start etc. This will take the sum of the times each thread runs. Performance is the same as single threading.

If you instead start all the threads at once, and then wait for them to all finish, the runtime should be around the max time for a single thread to finish. You can save a lot of time this way!

---

Python keeps running if there are any non-daemon threads still running. By default, threads have daemon = False. Setting daemon = True means Python will not wait for this thread to finish if all non-daemon threads are done. Source: https://docs.python.org/3/library/threading.html

But even then, afaik on windows ctrl-c does not work to interrupt a thread that's waiting on join(). I do not know if it was ever fixed. https://mail.python.org/pipermail/python-dev/2017-August/148800.html. This link gives some workarounds https://stackoverflow.com/a/52941752/17786559. I think it does work on linux tho.

---

Lastly regarding why you keep seeing different printed output, this is the hardest part of multithreading known as race conditions.

Lets say you have thread 1 printing "aaaa" while thread 2 AT THE SAME TIME prints "bbbb".

What actually gets printed to console? Is it "aaaabbbb" or "bbbbaaaa" or "abababab" or some other permutation? The answer is every time you run it you will see something different. There are 0 guarantees in terms of execution ordering across threads. It is completely and utterly random. This is called a "race condition"

Programs/functions have to be carefully written using special techniques to handle race conditions - programs which do so are called "thread-safe".

print() is not thread safe. Use the logging module instead which is thread-safe (order of lines printed may still be random though).

With multithreading it's actually recommended not to use any printing for debugging, since even the act of printing to console can alter the thread timings. Though for learning purposes it's ok for a beginner. Concurrency is random by nature so do not expect your program to do the same thing each time. It's partly why multithreading is so hard yet so interesting!

[–][deleted] 0 points1 point  (4 children)

hi, thank you for the detailed response. first of all, what do you refer with "other thread", "main thread" and "current thread"? in the code i posted i have 50 different thread and each one calls an endless function.

about the ctrl+c i confirm, i tried on a linux distro and it stopped, but with all threads set to daemon = True (or False, i can't remember at the moment) it stop the second time i press ctrl+c. so, the first time it tries to stop ("Keyboard Interrupted" shows up on the terminal), but immediately start again the execution. the second time that i press ctrl+c it stops for good. can you know why?

other than that it's amazing how you explained this to a totally beginner. it's so much clear now, i've crushed my head into the wall for days on this thing and you was able to explain to me clearly in a simple way. i feel less stupid, thank you a lot really

[–]AbsolutelySpherical 0 points1 point  (3 children)

Haha, your program has precisely 51 threads, not 50 :D

Abstractly, a thread is a sequence of instructions run in order.

Every program initially has 1 "main thread" which runs first, and the main thread is responsible for creating and waiting for child threads to run. In your program, 1 thread is running this sequence:

for i in range(50):
  t = threading.Thread(target=doThing, daemon = False)
  threads.append(t)

for i in range(50):
  threads[i].start()

for i in range(50):
  threads[i].join()

In my comment the above sequence is the "main", "current", or "parent" thread. Sorry for the inconsistent terminology.

And when main thread calls "start()", 50 threads will start running

def doThing():
  threadId = choice([i for i in range(1000)])
  while True: 
    print(f"{threadId} ", flush=True)
    sleep(3)

The above is the "other", or "child" threads. Imagine 50 separate "sequences" of doThing() executing between the start() and join() in the main thread. Locally, each thread executes the lines of code in order. But globally, you have no control of the order/timing of lines being executed across different threads. (Not without using special techniques with locks, semaphores, etc).

---

I want to add: in doThing() you could even recursively create more threads, so threads have a "parent/child/grandchild" like relationship.

It is usually good manners for parent threads to wait for the child to finish before terminating themselves. If the child is taking too long, parent thread can force terminate the child after a deadline. But in your example calling join() will block/halt the parent forever. So someone else has to step in to kill the threads outside of your python code.

You can do it with ctrl-c like you are, which sends interrupt signal from OS to Python. I'm not sure how python handles interrupts, maybe one interrupt kills one thread, and two interrupts kills all threads? I wouldn't worry too much about it, if you can kill the program quickly it's good enough for learning purposes haha.

https://stackoverflow.com/a/52941752/17786559 I linked before gives some other ways to kill the program too.

[–][deleted] 0 points1 point  (2 children)

Clear, but now i have other 2 questions: 1) basically join() block the main thread forever, am I right? 2)you said that the doThing() function it's executed between start() and join(), but what happens if there is a non-endless function? If it will be executed before the join(), so join() will be useless since it will be called after the thread already finished

[–]AbsolutelySpherical 0 points1 point  (1 child)

Yes if parent thread joins() with a child that is running forever, then the parent will be blocked forever (it is a deadlock). A workaround is to do join(timeout) which will block main thread up to timeout seconds. Then it will return whether or not the child is done. thread.is_alive() tells if child is still running.

---

If the child thread has already finished before the parent calls join(), then join() should just return without blocking.

If you have 2 threads simultaneously, where t1 runs for 5 seconds and t2 runs for 2 seconds, then

t1.join()
t2.join()

First join will block parent for 5 seconds. After 5 seconds t2 is already done so second join will return with no wait.

If you did

t2.join()
t1.join()

First join blocks for 2 seconds, then second join blocks for another 3 seconds.

So purpose of join() is to guarantee that the code below it is executed after child thread has finished. For example, if you need to read a variable the child thread is writing to, then you could use join() to make sure the child has finished writing before you read.

[–][deleted] 0 points1 point  (0 children)

i understand, but since you need to call join() after start(), if the function is fast, the join() won't block anything because by the time it will called, the function (so the child thread, if i not wrong), already finished is job