Content

Processes and Threads

We start learning programming by learning the syntax of a programming language. If one chooses for C/C++ there is no quick entry into such fundamental topics as processes, threads and interprocess communication. This is because we learn to write monolithic programs where all components of the application are in one process. For small applications this is a good strategy, but for real world applications it is not. We need to split up the functionality of an application into different components, there can run on different computers on different locations. This is what we call multiprocessing and distributed computing. We will focus here on the programming facilities in C/C++ for this type of computing.

A program exists as a process in the memory of a computer. Multiprocessing is the ability of an operating system to execute more than one process simultaneously by scheduling CPU time and memory to each individual process. A process consists of an address space divided into data segments and code segments, a program counter, a stack,a state and register sets. The OS can take the process out of memory, store it on disk and reload in memory to run.The program counter remembers which statement should be executed next when the OS loads the process in memory. A stack holds the arguments passed to functions as well as the variables local to functions. A heap holds the remaining memory requirements of the program. The heap is used for the memory allocations that must persist longer than the lifetime of a single function. Multiple processes can work together through interprocess communication and act as one application.

A process can be furhter divided into different threads (multithreading). Each thread is scheduled individually by the OS. When the computer has multiple CPUS, like a dual core processor, then the process (also called the primary thread) and another thread can run simultanously and increasing the speed of the program. A thread shares the process's address space but has its own stack, state and register set. Context switching between processes is more costlier and resource intensive than context switching between threads. Therefore threads are a more efficient way to execute sub tasks within a program than child processes. Furthermore as a thread shares the memory space of a process, threads have direct access to the memory and this also increases performance in the communication between process and threads.

A side effect of the sharing of memory locations is that a synchronization mechanism for accessing shared resources must be used. A thread can use an object called a mutex (an acronym for MUTual EXclusion) to achieve this synchronization. Once a thread has the mutex locked, no other thread can get the mutex, until the owning thread releases (unlocks) it. When more threads are waiting on a locked mutex, some scheduling mechanism must be used to choose which thread can access the resource first after is has been released. This scheduling requires a prioritization. This prioritization can be based on a level of priority or waiting time. There are also other types of synchronization mechanisms like semaphores. Semaphores limit the access to a shared resource to 1 or more threads.

Let us now start experimenting with thread programming. In general the management of processes and threads are OS specific. To write portable code either the functionalities should be part of the programming language or it should be part of a generic library. We use the C++11 standards for threading. For process management C++ has no standards, therefore we will use the Boost.Process. Boost is an open source initiative to enhance the functionalities of the C++ programming language. Often functionalities are first included in the Boost library and later on include in the official C++ standard. The Boost.Process library is currently not an official part of the Boost library.

We start with analyzing the threading facilities of C++11, a summary can be found here. The first example shows how to launch a thread by creating and initializing a variable of type thread. The parameters to this initialization are the function to run in the thread and the parameters to this function. By default the parameters are passed by value. To pass by reference std::ref must be used. After launching the thread the join method let the main thread wait on completion of the thread before it returns.

// Thread example
// C/C++ code generation / runtime library = /MDd
#include <iostream>
#include <thread>

void func(const std::string &s, int &a) {
	a++;
	std::cout<<s<<std::endl;
	std::this_thread::sleep_for(std::chrono::milliseconds(5000));
}
int main()
{
	int a = 0;
	std::thread t1(func,"Hello world",std::ref(a)); // launch thread
	t1.join(); // join with main thread
	std::cout<<a;
	return 0;
}

A program is in essence a server which supplies some services to one or more clients. The program receives requests from clients and maps these requests to available services, then it performs the service and replies something back to the client. For a server to be responsive it must process the requests in a non-blocking mode or asynchronously. The easiest way to achieve this is by using threads. In the example below you see this server philosophy in a very simplistic way. We create an object Server which contains everything to service requests: a request handler, all available services and a timer. The timer we included to give the server scheduling facilities.

// Thread example 2
// C/C++ code generation / runtime library = /MDd
#include <iostream>
#include<string>
#include <thread>

class Server {
public:
	inline bool reqHandler() {
		char req;
		int wait;

		std::cout<<"Request: ";
		std::cin>>req;
		std::cout<<std::endl;
			switch(req) {
				case 'q': 
					return true;
					break;
				case 's':
					std::cout<<"Wait seconds: ";
					std::cin>>wait;
					new std::thread(&Server::service,this,wait);  
					return false;
			}
	}
private:
	void service(int n) {
		std::thread t1(&Server::timer,this,n); //create timer thread which just waits for the worker to start
		t1.join();
		std::cout<<std::this_thread::get_id()<<" waited for: "<<n<<"s"<<std::endl;
	}
	void timer(int n) {
		 std::this_thread::sleep_for(std::chrono::seconds(n));
	}
};

int main()
{
   	Server x=Server();
	while(!x.reqHandler());
	return 0;
}

When we run this code we see that the console window is used by the different threads. The main thread is always asking for a request, but when a thread finishes its work it writes immediately to the console. This is a general problem of having shared resources. In this particular case we should let the main thread control all input and output with the console window.

When there is a shared resource the usage of this shared resource must be synchronized. The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. mutex offers exclusive, non-recursive ownership semantics:

The behavior of a program is undefined if a mutex is destroyed while still owned by some thread. The mutex class is non-copyable.

With a mutex we can easily solve the problem of the console shared output. We declare a global variable g_lock of type std::mutex. Each tread must lock this mutex with g_lock.lock() before it enters a critcal code section. At the end of the section a thread must unlock the mutex. When the mutex is already locked the thread must wait on its release.

// Thread example 1
// C/C++ code generation / runtime library = /MDd
#include <iostream>
#include<string>
#include <thread>
#include <mutex>

std::mutex g_lock_console;

class Server {
public:
	inline bool reqHandler() {
		char req;
		int wait;

		g_lock_console.lock();
		std::cout<<"Request: ";
		std::cin>>req;
		std::cout<<std::endl;
		g_lock_console.unlock();
			switch(req) {
				case 'q': 
					return true;
					break;
				case 's':
					g_lock_console.lock();
					std::cout<<"Wait seconds: ";
					std::cin>>wait;
					std::cout<<std::endl;
					g_lock_console.unlock();
					new std::thread(&Server::service,this,wait); 
					return false;
			}
	}
private:
	void service(int n) {
		std::thread t1(&Server::timer,this,n); //create timer thread which just waits for the worker to start
		t1.join();
		g_lock_console.lock();
		std::cout<<std::endl<<"Response: "<<std::this_thread::get_id()<<" waited for: "<<n<<"s"<<std::endl;
		g_lock_console.unlock();
	}
	void timer(int n) {
		 std::this_thread::sleep_for(std::chrono::seconds(n));
	}
};

int main()
{
	Server x=Server();
	while(!x.reqHandler());
	return 0;
}

References



Copyright 2012 Jacq Krol. All rights reserved. Created July 2012; last updated July 2012.