Basic question about thread interaction

Hi everyone. I’m trying to learn how to use threads in Julia by comparing them to the Python examples from Ramalho’s Fluent Python book Fluent Python, 2nd Edition [Book].

The basic Python example is:

import itertools
import time
from threading import Thread, Event

def spin(msg: str, done: Event) -> None:
    for char in itertools.cycle(r'\|/-'):
        status = f'\r{char} {msg}'
        print(status, end='', flush=True)
        # returns False after s and keep looping
        # or if another treat calls Event.set(), return True and break loop
        if done.wait(.1):
            break
    blanks = ' ' * len(status)
    print(f'\r{blanks}\r', end='')

def slow() -> int:
    # blocks main thread, but releases GIL, so other threads can run
    time.sleep(3)
    # long function finishes
    return 42

def supervisor() -> int:
    done = Event() # starts as False
    spinner = Thread(target=spin, args=('thinking!', done))
    print(f'spinner object: {spinner}')
    spinner.start() # start spin thread
    result = slow() # blocks main thread
    done.set() # spin loop will end
    spinner.join() # wait for spin thread to end
    return result

supervisor()

I have a Julia implementation as (started Julia with 4 threads):

using Base.Threads

function spin(msg::String, done::Threads.Atomic{Bool})
    for char in Iterators.cycle("\\|/-")
        status = "\r$(char) $(msg)"
        print(status)
        flush(stdout)
        sleep(0.1)
        if done[]
            break
        end
    end
    blanks = " " ^ (length(msg)+3)
    print("\r$(blanks)\r")
end

function slow()
    sleep(3)
    return 42
end

function supervisor()
    done = Threads.Atomic{Bool}(false);
    spinner = Threads.@spawn spin("thinking!", done)
    println("spinner object: $(spinner)")
    result = slow()
    done[] = true
    wait(spinner)
    return result
end

supervisor()

My question is twofold. First is this an idiomatic or Julian way to use Threads.Atomic? Secondly, could someone suggest how to accomplish this with Event or Condition? I found the documentation for those 2 types hard to follow. Thanks everyone.

1 Like

May or may not be helpful: GitHub - carstenbauer/LittleBookOfSemaphores.jl: Julia code snippets inspired by the Little Book Of Semaphores

1 Like

Wow, had no idea someone (you) implemented this book in Julia! Not sure if immediately helpful yet, but certainly fascinating :slightly_smiling_face: Thank you for sharing.

Yes, the Atomic use is idiomatic, but it’s wise to put thing inside a try ... finally:

spinner = Threads.@spawn spin("thinking!", done)
result = try
    slow()
finally 
    done[] = true
    wait(spinner)
end

The reason is that if the slow() call throws, the spinner is anyway stopped.

An Event is used when you need to to wait until someone calls notify. E.g.

e = Event()
t = @spawn (doit(); wait(e); something())
dostuff()
notify(e)  # inform the spawned task that we have done stuff
wait(t)  # wait for task to finish

A Condition is similar, but it does not remember that somebody has called notify. You have to be inside a wait when someone calls notify (edge-triggering). If you’re late to call wait, you’ll be waiting for the next notify. On the other hand, an Event will be “set” when someone calls notify, so subsequent waiters will just continue.

Event and Condition are not very well suited for your regular checks in a loop. I don’t think there’s a public interface to check whether an Event has been set, it’s to be waited for, not checked. Though, an Event contains an atomic field set, so if you let your done be an Event, and check for done.set, it will work. But this is an undocumented implementation detail, and you’re better off by just using an Atomic.

There’s also a general timedwait function which can be used like the python done.wait(.1), though it’s polling the supplied function, not waiting via the task scheduler:

timedwait(() -> done, 0.1)
1 Like