1

I'm new to event-driven programming and I would really like to understand better what happens under the hood, when the CPython interpreter goes through the code line by line. So far I have only programmed sequentially, and I have a fairly good idea in mind how the interpreter converts my code into bytecode and then goes from one statement to the next and executes the commands.

But for event-driven programming, I'm totally confused how the interpreter works.

In particular, I'm confused

  1. how the interpreter knows where to jump next in the source code

  2. also how function handlers are called when certain events happen

  3. how the refresh rate of the event loop is handled: Is actually all the code of the function handles run thousand of times per second, but not executed because some kind of "event-has-not-happened" flag say "don't execute this function now"?

To make this discussion more concrete, could you illustrate these points on the following example, taken from this site:

from Tkinter import *

ROOT = Tk()

def ask_for_userinput():
    user_input = raw_input("Give me your command! Just type \"exit\" to close: ")
    if user_input == "exit":
        ROOT.quit()
    else:
        label = Label(ROOT, text=user_input)
        label.pack()
        ROOT.after(0, ask_for_userinput)

LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
ROOT.after(0, ask_for_userinput)
ROOT.mainloop()

Ideally I'd like an explanation in a similar spirit to this article, where it is brilliantly explained from the point of view of how the CPython interpreter works, why some statements are thread-safe and some not, and how thread-safety is achieved.

user47574
  • 163
  • 7

1 Answers1

3

All that an event loop does is call other functions when an event takes place. The graphical subsystem helps out here, by signalling to the event loop that events are waiting to be processed.

Events like keyboard input and mouse interactions (moving the pointer, clicking) are all handled by the graphical subsystem (GUI) and the operating system (OS). Keyboards and mice are hardware devices, and computers use interrupts to record their state for the GUI to pick up.

If you don't touch your keyboard or mouse, an event loop can just do nothing; the loop blocks, and the OS will execute other processes as the loop has signalled it is waiting for something to happen. The OS is in control at this point, the process is not given any CPU time and other processes run instead. Once something happens, then there are events in the queue and the OS can resume the process. Imagine a function call in the event loop that asks if there are more events, and that call won't return until there are.

Once the loop resumes, there are events in the queue to process ('mouse position is now x, y', 'the keyboard input queue contains the characters F, O, and O'). Each event can trigger code you wrote, and registered to be run on that event. For example, you can register a handler to be run when a button is clicked; the event framework has a registry that if the conditions are right ('mouse button click' event happened, cursor is at the right location on the screen, button is active and visible) so knows to call your custom event handler.

Such an event handler is entirely synchronous, if the handler takes a long time to complete you'll notice that your GUI 'freezes', does nothing else, as Python is too busy running that one handler. The usual work-around is to use threads in that case; your event handler quickly starts a separate thread to do the real work, and returns. That way the main thread (with the event loop) can handle the next event, while the OS switches between the work in the extra thread and the main thread.

As to the specific piece of code you posted, that's actually not a very good example to use. It actively ignores GUI input, using the raw_input() function to capture keyboard input from the console instead. The GUI is entirely blocked every time the function runs!

The ask_for_userinput() function is an event handler, it is registered as one with the after() method. after() uses a timer interrupt, (usually implemented with a SIGALRM interrupt) to be called after at least 0 seconds have passed (so as soon as possible, really). Each time it is called it adds a new label to the GUI (just a piece of text) and re-schedules itself. It is not very interesting!

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • +1 So that means regarding a) that the Python interpreter at the `ROOT.mainloop()` statement "forwards" this statement to Tcl, which in turn then uses OS functions to listen for events? I'm not sure if Python threads are the workaround for this, because of the GIL, Python can only run 1 thread. – user47574 Feb 17 '18 at 13:38
  • Is there some CPU clock cycle that determines how fast interrupts can occur, so that the OS resp. event loop can react to them? – user47574 Feb 17 '18 at 13:39
  • @user47574: Python can run plenty of threads. What Python can't do, is run *Python threads in **parallel***. The GIL doesn't prevent *concurrency*, switching between handling GUI events and a heavy longer-running task several hundred times per second. – Martijn Pieters Feb 17 '18 at 13:42
  • @user47574: the GIL means that Python threads can't be used to speed up heavy computation tasks (they'll take just as long as each thread has to wait their turn for CPU time as threads are switched). Non-Python code (extensions) can release the GIL and be scheduled to run on separate CPU cores, so they *can* run in parallel. See [Does Python support multithreading? Can it speed up execution time?](//stackoverflow.com/q/20939299) – Martijn Pieters Feb 17 '18 at 13:43
  • @user47574: it depends heavily on the CPU architecture and the exact OS how interrupts are handled. Those cycles are way, way faster than human reaction times anyway. – Martijn Pieters Feb 17 '18 at 13:44
  • Thanks for all this info. But is my understanding from the first qeustion, "hat the Python interpreter at the ROOT.mainloop() statement "forwards" this statement to Tcl, which in turn then uses OS functions to listen for events?" correct ? – user47574 Feb 17 '18 at 14:01
  • And can you please tell me what the name of the graphical subsystem that you mention is for Windows 10 and, say, Ubuntu 17.04 ? – user47574 Feb 17 '18 at 14:02
  • @user47574: the `mainloop()` function just starts the GUI event loop. Event loops are, by their nature, endless, until an event tells them to exit. That could be a Keyboard Interrupt on the console that started the Python interpreter, or a GUI button event handler calling either [the `quit()` or `destroy()` method](https://stackoverflow.com/q/2307464). – Martijn Pieters Feb 17 '18 at 14:19
  • @user47574: I don't know what GUI subsystems are called, that's not info I normally am interested in. You could just Google that. I can imagine that on Linux it'd be something to do with the X-Window system, but that could have changed. – Martijn Pieters Feb 17 '18 at 14:20
  • @user47574: at any rate, the tkinter main loop is written in C. You can use Tcl to write event handlers, or Python, but the event loop itself is just compiled native machine code produced from C source code. The loop uses OS and GUI library calls to look for events, updates the screen as needed (all those buttons and labels are drawn by the Tk library based on events) and calls event handlers (in Python or any other language). – Martijn Pieters Feb 17 '18 at 14:24