Multithreading

Overview

This tutorial will show you how to work with Mongoose on a multithreaded environment.

When serving static pages or even in dynamic RESTful servers, it is common to return content on the same callback function that handles the request. However, when that request triggers an action that requires a long processing time, we might want to take advantage of a multithreaded environment and spawn a new thread for each request to take care of it.

As explicitly noted in the documentation, Mongoose is not thread-safe:

Since Mongoose's core is not protected against concurrent accesses, make sure that all mg_* API functions are called from the same thread or RTOS task.

To be able to use several threads, we need a mechanism that enables these threads to communicate with the main context in which the event manager is running. The idea behind this is similar to that of a reverse proxy, though now, instead of establishing a connection to our backend server, we connect to a worker thread. There are, then, two connections, as seen in the graph

** GRAPH **

  • C1 is the usual connection from the outside world to our web server
  • C2 is the new connection to the worker thread. It is done by a pipe constructed with two back-to-back sockets; this way, each worker thread reads from the pipe and writes to it.

Our actions will be:

  • We first write our worker thread. This thread will be spawn by a request to a URI and will perform the required task
  • Then we write a callback function to handle the interactions with the pipe. This function will be in charge of tying the two connections together, and it will receive data from the worker thread (via the connection from the event manager to the pipe) and send it as a response in the usual way
  • Then we write the usual HTTP request callback function, though this time, instead of handling the request and sending a response, it will create a pipe and spawn a worker thread to handle the request
  • Finally, we initialize an event manager and add the listener for the HTTP request callback function to the event manager in the usual way

API

The worker thread will use the standard socket interface functions send() and recv().

To create the pipe we'll use this function:

int mg_mkpipe(struct mg_mgr *mgr, mg_event_handler_t fn, void *fn_data, bool udp);

This function will create two interconnected sockets for inter-thread communication; one will be wrapped into a Mongoose connection and the other returned to be passed to a worker thread. When this thread call send() to write to the socket any data, this will wake up the event manager and the function specified in parameter fn will be called and passed parameter fn_data as its argument, receving a MG_EV_READ event.

For detailed information, see the documentation.

Example

This example starts a web server that spawns a thread to handle each request. That thread will sleep for two seconds, simulating a long process, and send a short text response as result.

  • Follow the Build Tools tutorial to setup your development environment.
  • Start a terminal in the project directory; if you've not already done so, clone the Mongoose Library repo
    git clone https://github.com/cesanta/mongoose
    
  • Build the example, this will also start Mongoose:
    cd mongoose/examples/multi-threaded
    make clean all
    
  • Go to http://localhost:8000 in your browser; after two seconds, you'll see 'hi'

What this example does is:

  • for each incoming request, we spawn a separate thread that sleeps for some time to simulate a long processing time; it then produces an output and hands over that output to the request handler function

  • write a simple thread function using send() to write its results into the pipe, whose handler it receives as a parameter

    Code should also check for return code and send the remaining data if not everything was sent. In practice, small sends are smaller than a socket buffer so the call to send() sends everything. If a socketpair is UDP, as in our case, then it is guaranteed to send all data.
  • write a small function to link the connections, so each one receives the other as a parameter. Write a function to unlink them, too

  • write the pipe event handler function. This function will be called when there is outstanding data on the pipe (in our case when the thread worker has written its results), at creation, at closure. Parameter c points to C2, the connection to the pipe; once connections are tied, parameter fn_data will point to C1, the connection to the outside world. This function may get one of these events of interest:

    • MG_EV_OPEN: the pipe has been created, we need to tie the connections
    • MG_EV_READ: data has been received, the thread worker has written its results and we have to send them out
    • MG_EV_CLOSE: the connection has been closed, untie the connections
  • write the HTTP request callback function. It is a bit different than usual since we need to take action on connection closure. Parameter c points to C1, the usual connection to the outside world; once connections are tied, c->fn_data points to C2, the connection to the pipe. This function may get one of these events of interest:

    • MG_EV_HTTP_MSG: the usual event when there is an HTTP request, we create a pipe and spawn a thread to handle this request
    • MG_EV_CLOSE: the connection has been closed, untie the connections
      Note we also added an extra URI to just do regular processing for comparison purposes
  • initialize the event manager, create the listener, and call the event manager in an infinite loop:

Passing data to the worker thread

In typical RESTful servers, there is a GET/POST request with some parameters, and the thread function performs long calculations. This function needs to know some request parameters, these can be passed by using a user structure to pass both the socket and the data itself; be careful not to pass any references to volatile stuff like the HTTP request body or headers, make a copy of everything instead.

An example structure:

The HTTP request event handler allocates memory, duplicates the body, and passes it along with the socket handler to the thread worker:

The worker thread extracts the data, processes it, and frees the allocated memory:

  • Try it by POSTing with curl; after two seconds you'll see your body data echoed back:
    curl http://localhost:8000 -d '{"data": "this"}'
    {"data": "this"}
    

Browse latest code