2020-10-12 18:01:22 (edited by chrisnorman7 2020-10-12 18:02:31)

Hi all,
So here goes with my second article on the subject of my new game library Earwax.

While coding a quiz game I was working on, I decided to use a BufferDirectory (you can read about them here), to load music, earcons, and some other stuff. As it turns out, when you've got a fair few files (13 at the time of writing in the music folder alone), that takes a lot of loading. Add to that that I've created my own wrapper over the Open Trivia DB API, which runs fairly slowly, by dint of it dragging stuff off the internet, and you have yourself a very long loading process at the beginning of your game. Nobody likes things that crash, even if they're only appearing to crash while the Synthizer and requests modules sort their collective lives out, and I thought it was a problem that earwax should be able to handle on its own.

I had me a google, and found the docs for the concurrent.futures Python module, and it's ThreadPoolExecutor class. The idea is that you have code like the following:

from concurrent.futures import ThreadPoolExecutor

pool = ThreadPoolExecutor()

f = pool.submit(print, 'Hello')
while not f.done():
    pass

That's fine for stuff that doesn't have - or need - a window, but I didn't want anything silly happening with Pyglet. It would be nice if you were dumping a database to disk, or performing some other mundane management stuff that didn't need to report back to the main thread.

I won't go into the details (you can see them by reading the code), but the API I came up with is, in my opinion at least, very clean and concise:

from earwax import Game, ThreadedPromise

game: Game = Game()

promise: ThreadedPromise = ThreadedPromise(game.thread_pool)


@promise.register_func
def do_stuff() -> None:
    # Something that takes ages...


promise.run()

OK, so that's all you need. At the minute, it's a bit more code than doing it with pure standard library code, but there's more.

Say you wanted the value that your function returned:

from time import sleep

from earwax import Game, ThreadedPromise

game: Game = Game()

promise: ThreadedPromise = ThreadedPromise(game.thread_pool)


@promise.register_func
def long_task() -> int:
    sleep(5)
    return 6


@promise.event
def on_done(i: int) -> None:
    print(f'The result was {i}.')

Now we're getting somewhere.

But what if the task raises an error? Let's build upon the code from the last example:

@promise.event
def on_error(e: Exception) -> None:
    print('The task raised {e!r}.')

And, just to complete the try / except / finally trinity, we should be able to do something when the task finishes regardless of the return value, or any exceptions raised:

@promise.event
def on_finally() -> None:
    print('We are done.')

Of course, there is also a .cancel() method you need to cancel the promise. The task will most likely finish running, but your events will not be dispatched, so you don't need to worry about inadvertently handling errors or return values. There's also an on_cancel event:

@promise.event
def on_cancel() -> None:
    print('I was cancelled.')

I hope you enjoyed this little article, and hopefully ThreadedPromises will be useful for loading assets, getting stuff from the internet, loading massive map files, and generally being useful little blighters.

As usual, any problems, requests for help, genuflection, or confusion should be directed to the Github issues, or the forum thread.

Take care,
Chris Norman
Selling my soul to andertons.co.uk since 2012.