A guide to getting started with boost::asio
3. Giving io_service some work to do
Now we can finally get to doing the real work! We will be reusing the previous example as our base, so our examples are multi-threaded ready. If the io_service is the brain and heart of the boost::asio library, the io_service member functions post and dispatch would be the arms and legs. The post function "is used to ask the io_service to execute the given handler, but without allowing the io_service to call the handler from inside this function." The dispatch function "guarantees that the handler will only be called in a thread in which the run(), run_one(), poll() or poll_one() member functions is currently being invoked. The handler may be executed inside this function if the guarantee can be met."
So the fundamental difference is that dispatch will execute the work right away if it can and queue it otherwise while post queues the work no matter what. Both of the functionality are really important as the function we will use will depend on the context that it is being used in. Remember earlier the remarks about how the internals of the work class worked? If the work class were to use dispatch over and over, it might be possible the work never finished for a poll call, but if the work called post, it could.
Let us get started! We will start out with a simple Fibonacci calculation. To make things more interesting we will add in some time delays to show the true nature of the power of boost::asio. We will also reduce the number of worker threads to just 2. The actual value that we will want to use in a multi-threaded program will vary depending on a number of factors, but that will be talked about later.
In this example, starting in main, we post 3 function objects to the io_service via the post function. In this particular case, since the current thread does no call the io_service run or poll function, dispatch would also call the post function and not execute the code right away. After we give the io_service work, through post, we reset the work object to signal once the work has been completed that we wish to exit. Finally, we wait on all the threads to finish as we have with the join_all function.
Our fib function simply calculates the sequence but we add in a time delay to slow things down to see our worker threads in action! We ultimate have to wrap the call with CalculateFib since we care about the return value and we want to see extra debugging information about when the function actually starts and completes.
Running the program, we should see the first two worker threads start on the first two units of work and once one worker thread has finished, it takes up the third unit of work. Once all work has been finished, the program exits.
Congratulations! We have now completed our first job! That was not so bad was it? Our program structure for working with boost::asio will be pretty generic overall. We can setup reusable worker threads to build up a pool of workers and when we send work to the io_service, it simply does it when it can. That is our basic example, let us consider another.
In this example, we show the difference between post and dispatch and how it can get us into trouble if we are not careful with what we do! We will use only one worker thread this time.
If we run the program, we should see the problem here. We wanted an in order display of events, but instead it was out of order. This is because dispatch was used for some events and post for others. Dispatched events can execute from the current worker thread even if there are other pending events queued up. The posted events have to wait until the handler completes before being allowed to be executed. Keep this in mind when programming we can easily code ourselves into serious bugs if we depend on the order of such events!
It should also be noted that if we had more than one worker thread, we would actually get the expected results because of the sleep call, but the problem remains still. If we removed the sleep, we might get any order of output depending on who grabbed the lock mutex first. For example, running the program without the sleep one example output received was 0, 2, 1, 4, 3, 5. We have to be aware of such things when programming at this level so we do not get fooled by "correct" output that was simply a result of having setup our program in such a way that it was possible. These types of bugs are the hardest to track down once they happen so it is imperative we fully understand the API that we are using first before diving too deep in.
That pretty much wraps up, no pun intended, how we will pass our work to the io_service object. We simply setup our program to process the work via poll or run how we need and then we can call dispatch or post as needed. There is a lot of cool stuff we can do using these concepts now! At this point, we can now get into the other useful aspects of the boost::asio library.