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
- 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
The worker thread will use the standard socket interface functions
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.
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.
- 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:8000in 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 usingCode 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.
send()to write its results into the pipe, whose handler it receives as a parameter
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, parameter fn_data points 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; parameter 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: