Discussion forum for David Beazley

Curio and tkinter's mainloop


A friend of mine wrote a program that used curio and tkinter, and it ran tkinter’s mainloop in the main thread and curio.run in another thread. I needed to run tkinter with curio at some point too, and I figured out a way to run tkinter’s mainloop in curio. It works like this:

import curio
import functools
import tkinter as tk

    from _tkinter import DONT_WAIT
except ImportError:
    # _tkinter.DONT_WAIT is there on cpython and pypy, but providing a
    # fallback doesn't hurt
    DONT_WAIT = 2

async def tk_mainloop(root):
        dooneevent = root.tk.dooneevent
    except AttributeError:
        # probably running in pypy
        dooneevent = _tkinter.dooneevent

    while True:
        await curio.sleep(0)

        # stop if the root window is destroyed
        except tk.TclError:

# simple example

async def counter():
    # this is here just to prove that tk_mainloop() doesn't block
    i = 0
    while True:
        print("counter:", i)
        i += 1
        await curio.sleep(1)

async def main():
    root = tk.Tk()
    button = tk.Button(root, text="Click me",
                       command=functools.partial(print, "you clicked me"))
    countertask = await curio.spawn(counter())
    await tk_mainloop(root)
    await countertask.cancel()


dooneevent() calls Tcl_DoOneEvent(3tcl), and it runs Tk’s mainloop by one iteration.

My code works, but there are some problems with it. Most of these are probably not too hard to fix and there might already be a workaround that I haven’t found yet.

  • The code is heavy on the CPU because curio’s main loop runs as fast as possible, and nothing slows it down. I have no idea how to fix this. Urwid’s mainloops seem to just call time.sleep(something_small).
  • Doing curio.run(curio.spawn(stuff)) to spawn an async function from a tkinter callback doesn’t work.

I think it would be nice to see something like a curio.tkinter submodule for writing tkinter code with curio. Using root.tk.dooneevent(DONT_WAIT) and curio.sleep(0) feels like a hack, and it would be really nice to just do button['command'] = some_async_func and have it run correctly in curio’s mainloop.


Thank you for finding dooneevent for me. I was looking for a solution to merge these two event loops. Here’s what I found:

dooneevent returns 1 if it found any events to process. 0 if it found nothing to do.

My goals are (1) to make sure both loops get a chance to run, but also that (2) swapping between them on one task/thread doesn’t starve the rest of the system for processor time.

So I call dooneevent repeatedly, (with sleep(0) often enough to keep things from seeming blocked) for as long as its returning 1. then sleep for about a ‘frame’'s worth of time before giving it control again.

I changed the relevant portion of your code to this:

while True:
    while dooneevent(DONT_WAIT): # added while loop
        if i % 10==0:
            await curio.sleep(0) #every 10 loop of dooneevent staying busy, let curio process also.
    #once tkinter is NOT busy sleep tkinter's thread for a frame
    await curio.sleep(0.04)     #changed from 0 to .04 to keep from spinning the cpu like crazy.
    #frame interval equals the reciprocal of FPS
    #0.04   represents 25 FPS This is enough for most business apps to seem responsive
    #0.0333 represents 30 FPS
    #0.02   represents 50 FPS This is enough for most games to seem responsive, depending on the processing power available, and what additional io is happening.
    #0.01   represents 100 FPS
    # stop if the root window is destroyed
    except tk.TclError:

probably both the 10 in if i % 10==0 and the interval in the longer sleep call ought to be put in default params or constants at the top of the file to invite direct tweaking rather than code fiddling.

I notice this is an old thread. Are there newer solutions than this?