views:

3120

answers:

4

If you have worked with gui toolkits, you know that there is a event-loop/main-loop that should be executed after everything is done, and that will keep the application alive and responsive to different events. For example, for Qt, you would do this in main():

int main() {
    QApplication app(argc, argv);
    // init code
    return app.exec();
}

Which in this case, app.exec() is the application's main-loop.

The obvious way to implement this kind of loop would be:

void exec() {
    while (1) {
        process_events(); // create a thread for each new event (possibly?)
    }
}

But this caps the CPU to 100% and is practicaly useless. Now, how can I implement such an event loop that is responsive without eating the CPU altogether?

Answers are appreciated in Python and/or C++. Thanks.

Footnote: For the sake of learning, I will implement my own signals/slots, and I would use those to generate custom events (e.g. go_forward_event(steps)). But if you know how I can use system events manually, I would like to know about that too.

+8  A: 

Generally I would do this with some sort of counting semaphore:

  1. Semaphore starts at zero.
  2. Event loop waits on semaphore.
  3. Event(s) come in, semaphore is incremented.
  4. Event handler unblocks and decrements the semaphore and processes the event.
  5. When all events are processed, semaphore is zero and event loop blocks again.

If you don't want to get that complicated, you could just add a sleep() call in your while loop with a trivially small sleep time. That will cause your message processing thread to yield it's CPU time to other threads. The CPU won't be pegged at 100% any more, but it's still pretty wasteful.

Eric Petroelje
This sounds tempting, I will have to learn more about threading. Thanks.
fengshaun
+18  A: 

I used to wonder a lot about the same!

A GUI main loop looks like this, in pseudo-code:

void App::exec() {
    for(;;) {
        vector<Waitable> waitables;
        waitables.push_back(m_networkSocket);
        waitables.push_back(m_xConnection);
        waitables.push_back(m_globalTimer);
        Waitable* whatHappened = System::waitOnAll(waitables);
        switch(whatHappened) {
            case &m_networkSocket: readAndDispatchNetworkEvent(); break;
            case &m_xConnection: readAndDispatchGuiEvent(); break;
            case &m_globalTimer: readAndDispatchTimerEvent(); break;
        }
    }
}

What is a "Waitable"? Well, it's system dependant. On UNIX it's called a "file descriptor" and "waitOnAll" is the ::select system call. The so-called vector<Waitable> is a ::fd_set on UNIX, and "whatHappened" is actually queried via FD_ISSET. The actual waitable-handles are acquired in various ways, for example m_xConnection can be taken from ::XConnectionNumber(). X11 also provides a high-level, portable API for this -- ::XNextEvent() -- but if you were to use that, you wouldn't be able to wait on several event sources simultaneously.

How does the blocking work? "waitOnAll" is a syscall that tells the OS to put your process on a "sleep list". This means you are not given any CPU time until an event occurs on one of the waitables. This, then, means your process is idle, consuming 0% CPU. When an event occurs, your process will briefly react to it and then return to idle state. GUI apps spend almost all their time idling.

What happens to all the CPU cycles while you're sleeping? Depends. Sometimes another process will have a use for them. If not, your OS will busy-loop the CPU, or put it into temporary low-power mode, etc.

Please ask for further details!

How would I implement such a waiting system to wait not for system signals, but for my own signals?
fengshaun
As I said, your code only runs in reaction to events. Therefore, if you fire your own event, you're gonna do that as a reaction to some system event. And then it becomes clear that you don't actually need an event system for your custom events. Simply call the handlers directly!
For example, consider a signal "Button::clicked". It is only going to fire in response to a system event (left mouse button release). So your code becomes "virtual void Button::handleLeftRelease(Point) { clicked.invoke(); }" with no need for threads or an event queue or anything.
+3  A: 

I would use a simple, light-weight messaging library called ZeroMQ (http://www.zeromq.org/). It is an open source library (LGPL). This is a very small library; on my server, the whole project compiles in about 60 seconds.

ZeroMQ will hugely simplify your event-driven code, AND it is also THE most efficient solution in terms of performance. Communicating between threads using ZeroMQ is much faster (in terms of speed) than using semaphores or local UNIX sockets. ZeroMQ also be a 100% portable solution, whereas all the other solutions would tie your code down to a specific operating system.

Wow, that's cool.
Eric Petroelje
+8  A: 

Python:

You can look at the implementation of the Twisted reactor which is probably the best implementation for an event loop in python. Reactors in Twisted are implementations of an interface and you can specify a type reactor to run: select, epoll, kqueue (all based on a c api using those system calls), there are also reactors based on the QT and GTK toolkits.

A simple implementation would be to use select:

#echo server that accepts multiple client connections without forking threads

import select
import socket
import sys

host = ''
port = 50000
backlog = 5
size = 1024
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((host,port))
server.listen(backlog)
input = [server,sys.stdin]
running = 1

#the eventloop running
while running:
    inputready,outputready,exceptready = select.select(input,[],[])

    for s in inputready:

        if s == server:
            # handle the server socket
            client, address = server.accept()
            input.append(client)

        elif s == sys.stdin:
            # handle standard input
            junk = sys.stdin.readline()
            running = 0

        else:
            # handle all other sockets
            data = s.recv(size)
            if data:
                s.send(data)
            else:
                s.close()
                input.remove(s)
server.close()
Vasil