Concurrency, according to wikipedia is: the ability of different parts or units of a program, algorithm or problem to be executed out-of order or in partial order, without affecting the final outcome. Concurrency is a property related to multiprocess system either time-shared or parallel and traditionally is has been a tricky area for programmers. Basically, using threads in your programs was a call for problems and the traditional advise was always to avoid them as much as possible.
Anyway, through my refresher journey I will try to bring back and explain most of the main issues and concepts related to concurrent programming and in this first paper I will start for the basics.
Thread and processes
In common operating systems, the way to run different parts of a program in pieces or out of order is either, creating threads or processes. Deep inside the operating systems there is not much difference between former or the later (they are all some kind of task down there) but at the programmer level they are pretty different. As different as they use completely different APIs.
The process API has been around forever, however, threads have had a rough time before they become, let's say, stable. In the early days, there where different libraries you could use with different APIs and different issues. LinuxThreads, Native POSIX Threads, GNU Portable Threads or the POSIX Threads.
Processes on the other hand, have been part of multiprocess operating systems from its inception and the interface to use them have been very stable and well-defined for years.
From a programmer point of view, the main difference between a thread and a process is that, thread belongs to processes and share the process address space, while each process has its own addressing space. What does this means for the programmer?, well, roughly speaking, any global data in your process is accessible for all the threads you create within that process... But any global data in a process is not accessible by other process, even if they are the same binary.
Let's see this with an example
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h> // Needed for wait
#include <sys/wait.h>
int global_var = 1;
int main ()
{
pid_t child;
// Create a new process
if ((child = fork ()) < 0) {
perror ("fork:");
exit (EXIT_FAILURE);
}
if (child == 0) // Child process
{
global_var ++;
printf ("CHILD: GLobal variable : %d\n", global_var);
return 0; // The child process finish
}
else // This is the father
{
int status;
wait (&status);
printf ("FATHER: GLobal variable : %d\n", global_var);
}
return 0;
}
The program above creates a new process using the system call fork
. Then, in the child process it increases a global variable and ends execution. The father, will just wait for the child process to finish and then will print the global variable too.
If you compile and run the program, the result is:
$ make process cc process.c -o process $ ./process CHILD: GLobal variable : 2 FATHER: GLobal variable : 1
When the child process is created all the addressing space of the father is copied (actually is copied on write), that is why the initial value of the variable is 1. From that point on, after that initialisation, the addressing space of the child process is private and independent of the parent process who is not affected at all by the operations of the child.
Let's see what happens when we use a thread:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int global_var = 1;
void *func (void *p) {
global_var ++;
printf ("THREAD: Global variable: %d\n", global_var);
}
int main ()
{
pthread_t tid;
void *r;
if (pthread_create (&tid, NULL, func, NULL) < 0) exit (EXIT_FAILURE);
pthread_join (tid, &r);
printf ("MAIN: Global variable: %d\n", global_var);
return 0;
}
As we can see the program is quite different so the APIs are. In any case, we can see how the program creates a thread using pthread_create
and instruct it to start execution on function func
. Then it just waits for the thread to finish using pthread_join
, instead of wait
. The result of this program is:
$ gcc -o thread thread.c -lpthread $ ./thread THREAD: Global variable: 2 MAIN: Global variable: 2
How to do this with processes?
You may be wondering if it is possible to get the same result we have got using thread using processes. The answer is yes, but in order to achieve that we need to use the so-called IPC API (InterProcess Communication). All operating system having the concept of process also have a way to communicate them.
There are many different ways to achieve that. Using pipes
or named pipes
, unix sockets, socketpairs
,.... But the more generic API, and also the one that matches best the threads API is the so-called System V IPC. System V IPC interface defines three main objects to enable this inter-process communication: Shared Memory, Semaphores and Messages.
Right now, for the simple example that we are working on, we only need shared memory, in order to allow two or more process to share a piece of memory... In other words, to make our global variable, global among processes.
Using this System V IPC element, our original program will change like this:
include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h> // Needed for wait
#include <sys/wait.h>
int main ()
{
pid_t child;
key_t key;
int shared_id;
int *global_var;
// Create a unique key for the shared memory block
key = ftok (".", 'd');
if ((shared_id= shmget (key, sizeof (int), IPC_CREAT | 0666)) < 0) {
perror ("shmget:");
exit (EXIT_FAILURE);
}
// Map shared memory in the father's process address space
global_var = (int*) shmat (shared_id, NULL, 0);
*global_var = 1; // Let's initialise the global var to 1
// Create a new process
if ((child = fork ()) < 0) {
perror ("fork:");
exit (EXIT_FAILURE);
}
if (child == 0) // Child process
{
// Attach the child process to the shared memory
// We already know the shared_id
global_var = (int*) shmat (shared_id, NULL, 0);
*global_var = *global_var + 1;
printf ("CHILD: GLobal variable : %d\n", *global_var);
shmdt ((void*)global_var); // Deattach from shared memory
return 0; // The child process finish
}
else // This is the father
{
int status;
wait (&status);
sleep (1);
printf ("FATHER: GLobal variable : %d\n", *global_var);
shmdt ((void*)global_var);
}
return 0;
}
When we run this program, now the result is the expected one:
$ ./shared_memory CHILD: GLobal variable : 2 FATHER: GLobal variable : 2
Some comments on the previous code:
- The
ftok
function allows us to create an unique identify that we can easily locate from other process because it is associated to an entity in the filesystem. - In the general case, when the child process is a different program, it will also need to call
ftok
andshmget
in order to find out what is the right shared memory identifier - Note that we have to call
shmat
on each process. This function is the one that actually maps the shared memory in the process addressing space. In general, each process may have a completely different address to access the shared memory, however that can be forced using the second parameter and appropriated flags. - Finally, the shared memory block shall be destroyed calling
shmdt
The POSIX way
System V IPC is a classic, still works, and it is maybe the one that may work whenever you have to deal with an old system. For modern system, there is a POSIX based interface, in a sense a bit more straightforward. The overall concepts are exactly the same... it is just that the functions have different names and functionality is now spread between functions in a different way.
The POSIX version of our previous program is this:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <sys/types.h> // Needed for wait
#include <sys/wait.h>
#define SM_FILENAME "/sm"
#define SM_SIZE sizeof(int)
int main ()
{
pid_t child;
key_t key;
int shared_id;
int *global_var;
if ((shared_id = shm_open (SM_FILENAME, O_CREAT | O_RDWR, 0666)) < 0) {
perror ("shm_open1:");
exit (EXIT_FAILURE);
}
ftruncate (shared_id, SM_SIZE);
global_var = (int*) mmap (NULL, SM_SIZE, 0666, MAP_SHARED,
shared_id, 0);
if (global_var == MAP_FAILED) {
perror ("mmap:");
exit (EXIT_FAILURE);
}
*global_var = 1;
printf ("Creating Process...\n");
// Create a new process
if ((child = fork ()) < 0) {
perror ("fork:");
exit (EXIT_FAILURE);
}
if (child == 0) // Child process
{
global_var = (int*) mmap (NULL, SM_SIZE, 0666, MAP_SHARED,
shared_id, 0);
*global_var = *global_var + 1;
printf ("CHILD: GLobal variable : %d\n", *global_var);
close (shared_id);
munmap (global_var, SM_SIZE);
shm_unlink (SM_FILENAME);
return 0; // The child process finish
}
else // This is the father
{
int status;
wait (&status);
sleep (1);
printf ("FATHER: GLobal variable : %d\n", *global_var);
close (shared_id);
munmap (global_var, SM_SIZE);
shm_unlink (SM_FILENAME);
}
return 0;
}
Some comments on this code:
shm_open
actually creates a file at/dev/shm
. This has two implications.- The first one is that the first parameter to
shm_open
, has to be in the form of/some_name
... Any other name will produce an invalid argument error. - The second is that the size of the file is the size of the shared memory. So if the file is just created (using the
O_CREAT
flag), a call toftrunc
is needed to add size to the file.
The POSIX interface makes more sense in the overall UNIX philosophy in the sense that makes more explicit the use of files and memory mapping (actually using the standard mmap
for that). I personally love the System V one... because it was just the first I learned.
Conclusion
In this introductory paper we have gone through the basics on how to create thread and process and also explored the differences between them. We also introduced the two main APIs for inter-process communication on UNIX systems and got ready for the next round!
Continue reading....Concurrency. Race Conditions■