clinuxnetwork-programminglibevent

What's the purpose of deferred callbacks in libevent?


According to the libevent book:

Deferred callbacks

By default, a bufferevent callbacks are executed immediately when the corresponding condition happens. (This is true of evbuffer callbacks too; we’ll get to those later.) This immediate invocation can make trouble when dependencies get complex. For example, suppose that there is a callback that moves data into evbuffer A when it grows empty, and another callback that processes data out of evbuffer A when it grows full. Since these calls are all happening on the stack, you might risk a stack overflow if the dependency grows nasty enough.

To solve this, you can tell a bufferevent (or an evbuffer) that its callbacks should be deferred. When the conditions are met for a deferred callback, rather than invoking it immediately, it is queued as part of the event_loop() call, and invoked after the regular events' callbacks.

As described above:

  1. The event loop fetches a batch of events, and processes them one by one immediately.
  2. Before the fetched events are processed, any new event won't be fetched and processed.
  3. If an event was marked as BEV_OPT_DEFER_CALLBACKS, then it will be processed after all other events in the same batch are processed.

Provided two callbacks ca and cb. First, ca is called, ca finds evbuffer_A is empty, then writes a message into it.

Then, cb is called, and cb finds evbuffer_A contains a message, then fetch and send it out.

When cb is called, ca's stack has been released. I think there won't be a stack overflow in such a scenario.

So, my question is:

What's the purpose of deferred callbacks in libevent?


Solution

  • The example given in the quoted text is a buffer being filled after one event and emptied after another event.

    Consider this non-event driven pseudo-code for the same example.

    void FillBuffer(...)
    {
        while (BufferNotFull)
        {
            PutDataInBuffer(...);
        }
        If (BufferIsFull)  // emulates buffer-full event
        {
            ReadBuffer(...);
        }
    
        ... more code ...
    }
    
    void ReadBuffer(...)
    {
        while (BufferNotEmpty)
        {
            ReadDataFromBuffer(...);
        }
        If (BufferIsEmpty)  // emulates buffer-empty event
        {
            FillBuffer(...);
        }
    
        ... more code ...
    }
    

    Obviously, the condition of the two if statements are always true. Consequently the functions never complete but keep calling each other again and again. Sooner or later the stack will overflow.

    Now imagine an event driven system where all callbacks happen as soon as the event triggers (i.e. remove the if-statements but remember that the event-system does the same as the if-statements). Like:

    void FillBuffer(...)  // Called on buffer-empty event
    {
        while (BufferNotFull)
        {
            PutDataInBuffer(...);
        }
        // Event buffer-full triggered (i.e. ReadBuffer being called)
    
        ... more code ...
    }
    
    void ReadBuffer(...)  // Called on buffer-full event
    {
        while (BufferNotEmpty)
        {
            ReadDataFromBuffer(...);
        }
        // Event buffer-empty triggered (i.e. FillBuffer being called)
    
        ... more code ...
    }
    

    Such a system will do the same as the non-event driven pseudo-code, i.e. call the next callback before the first finishes.

    Deferred callbacks can solve the problem by letting one callback complete before calling the next callback.