Examining the I/O service in the Boost.Asio library

The core object of the Boost::Asio namespace is io_service. The I/O service is a channel that is used to access operating system resources and establish communication between our program and the operating system that performs I/O requests. There is also an I/O object that has the role of submitting I/O requests. For instance, the tcp::socket object will provide a socket programming request from our program to the operating system.

Using and blocking the run() function

One of the most frequently used functions in the I/O service object is the run() function. It is used to run the io_service object's event processing loop. It will block the next statement program until all the work in the io_service object is completed and there are no more handlers to be dispatched. If we stop the io_service object, it will no longer block the program.

Note

In programming, event is an action or occurrence detected by a program, which will be handled by the program using the event handler object. The io_service object has one or more instances where events are handled, which is event processing loop.

Now, let's take a look at the following code snippet:

/* unblocked.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;

  io_svc.run();

  std::cout << "We will see this line in console window." << std::endl;

  return 0;
}

We save the preceding code as unblocked.cpp and then run the following command to compile it:

g++ -Wall -ansi -I ../boost_1_58_0 unblocked.cpp -o unblocked -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

When we run the program, the following output gets displayed:

We will see this line in console window.

However, why do we still obtain the line of text in the console even though previously we knew that the run() function blocks the next function after it is invoked? This is because we have not given any work to the io_service object. Since there is no work for io_service to do, the io_service object should not block the program.

Now, let's give the io_service object some work to do. The program for this will look like as shown in the following code:

/* blocked.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;
  boost::asio::io_service::work worker(io_svc);

  io_svc.run();

  std::cout << "We will not see this line in console window :(" << std::endl;

  return 0;
}

Give the preceding code the name blocked.cpp and then compile it by typing the following command in our console window:

g++ -Wall -ansi -I ../boost_1_58_0 blocked.cpp -o blocked -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

If we run the program by typing blocked in our console, we will not see the line of text anymore since we have added the following code line:

boost::asio::io_service::work work(io_svc);

The work class is responsible for telling the io_service object when the work starts and when it has finished. It will make sure that the run() function in the io_service object will not exit during the time the work is underway. Also, it will make sure that the run() function does exit when there is no unfinished work remaining. In our preceding code, the work class informs the io_service object that it has work to do, but we do not define what the work is. Therefore, the program will be blocked infinitely and it will not show the output. The reason it has been blocked is because the run() function is invoked even though we can still terminate the program by pressing Ctrl + C.

Using the non-blocking poll() function

Now, we will leave the run() function for a while and try to use the poll() function. The poll() function is used to run ready handlers until there are no more ready handlers remaining or until the io_service object has been stopped. However, in contrast with the run() function, the poll() function will not block the program.

Let's type the following code that uses the poll() function and save it as poll.cpp:

/* poll.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;

  for(int i=0; i<5; i++) {
    io_svc.poll();
    std::cout << "Line: " << i << std::endl;
  }

  return 0;
}

Then, compile poll.cpp by using the following command:

g++ -Wall -ansi -I ../boost_1_58_0 poll.cpp -o poll -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

Because there is no work that the io_service object has to do, the program should display the five lines of text as follows:

Using the non-blocking poll() function

However, what if we give work to the io_service object when we use the poll() function? To find out the answer, let's type the following code and save it as pollwork.cpp:

/* pollwork.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;
  boost::asio::io_service::work work(io_svc);

  for(int i=0; i<5; i++) {
    io_svc.poll();
    std::cout << "Line: " << i << std::endl;
  }

  return 0;
}

To compile pollwork.cpp, use the following command:

g++ -Wall -ansi -I ../boost_1_58_0 pollwork.cpp -o pollwork -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

The difference between the poll.cpp file and the pollwork.cpp file is only the following line:

boost::asio::io_service::work work(io_svc);

However, if we run pollwork.exe, we will obtain the same output as that of poll.exe. This is because, as we know from before, the poll() function will not block the program while there is more work to do. It will execute the current work and then return the value.

Removing the work object

We can also unblock the program by removing the work object from the io_service object, but we have to use a pointer to the work object in order to remove the work object itself. We are going to use the shared_ptr pointer, a smart pointer provided by the Boost libraries.

Let's use the modified code of blocked.cpp. The code for this will be as follows:

/* removework.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;
  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(io_svc)
  );

  worker.reset();

  io_svc.run();

  std::cout << "We will not see this line in console window :(" << std::endl;

  return 0;
}

Save the preceding code as removework.cpp and compile it using the following command:

g++ -Wall -ansi -I ../boost_1_58_0 removework.cpp -o removework -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

When we run removework.cpp, compared to blocked.cpp, which will block the program infinitely, the following line of text will be displayed to us:

Removing the work object

Now, let's dissect the code. As we can see in the preceding code, we used the shared_ptr pointer to instantiate the work object. With this smart pointer provided by Boost, we no longer need to manually delete memory allocation in order to store the pointer since it guarantees that the object pointed to will be deleted when the last pointer is destroyed or reset. Do not forget to include shared_ptr.hpp inside the boost directory as the shared_ptr pointer is defined in the header file.

We also add the reset() function to reset the io_service object when it prepares for a subsequent run() function invocation. The reset() function has to be invoked before any invocation of the run() or poll() functions. It will also tell the shared_ptr pointer to automatically destroy the pointer we created. More information about the share_ptr pointer can be found at www.boost.org/doc/libs/1_58_0/libs/smart_ptr/shared_ptr.htm.

The preceding program explains that we have successfully removed the work object from the io_service object. We can use this functionality if we intend to finish all the pending work even though it hasn't actually been finished yet.

Dealing with many threads

We have only dealt with one thread for one io_service object so far. If we want to deal with more threads in a single io_service object, the following code will explain how to do this:

/* multithreads.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <iostream>

boost::asio::io_service io_svc;
int a = 0;

void WorkerThread() {
  std::cout << ++a << ".
";
  io_svc.run();
  std::cout << "End.
";
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(io_svc)
  );

  std::cout << "Press ENTER key to exit!" << std::endl;

  boost::thread_group threads;
  for(int i=0; i<5; i++)
    threads.create_thread(WorkerThread);

  std::cin.get();

  io_svc.stop();

  threads.join_all();

  return 0;
}

Give the preceding code the name mutithreads.cpp and then compile it using the following command:

g++ -Wall -ansi -I ../boost_1_58_0 multithreads.cpp -o multithreads -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58

We include the thread.hpp header file so that we can use the thread object defined inside the header file. The thread itself is a piece sequence of instructions that can be run independently, so we can run multiple threads at once.

Now, run mutithreads.exe in our console. I obtained the following output by running it:

Dealing with many threads

You might obtain a different output because all the threads that are set up as a pool of threads are equivalent to each other. The io_service object may choose any one of them randomly and invoke its handler, so we cannot guarantee whether or not the io_service object will choose a thread sequentially:

for(int i=0; i<5; i++)
  threads.create_thread(WorkerThread);

Using the preceding code snippet, we can create five threads to display lines of text as you can see in the previous screenshot. The five lines of text will be enough for this example to view the order of nonconcurrent flow:

std::cout << ++a << ".
";
io_svc.run();

In every thread that is created, the program will invoke the run() function to run the work of the io_service object. Calling the run() function once is insufficient because all nonworkers will be invoked after the run() object finishes all its work.

After creating five threads, the program runs the work of the io_service object:

std::cin.get();

After all the work is run, the program waits for you to press the Enter key from the keyboard using the preceding code snippet:

io_svc.stop();

Once the user presses the Enter key, the program will hit the preceding code snippet. The stop() function will notify the io_service object that all the work should be stopped. This means that the program will stop the five threads that we have:

threads.join_all();

The join_all() function will then continue with all the unfinished threads, and the program will wait until all the processes in all the threads are complete. The preceding code snippet will continue the following statement inside the WorkerThread() block:

std::cout << "End.
";

So after we press the Enter key, the program will finish its remaining code and we will obtain the rest of the output as follows:

Dealing with many threads
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset