Thread Synchronization in Linux and Windows Systems, Part 1
July 15, 2019
Story
This article will be useful for those who have never programmed applications with multiple threads but plan to do so in the future.
Introduction
In modern operating systems, each process has its own address space and one thread of control. However, in practice we often face situations requiring several concurrent tasks within a single process and with access to the same process components: structures, open file descriptors, etc.
Organization of a multi-threading model under any circumstances requires simultaneous access to the same resources. This article provides general information about threads in Windows and Linux OSs, and then presents synchronization mechanisms[1] preventing access to shared resources.
This article will be of interest for those who deal with applications porting from one system to another or who create multi-threaded applications in one system and want to know how it is practically realized in the other system. This article will also be useful for those who have never programmed applications with multiple threads but plan to do so in the future.
Thread concept
What are these threads needed for? Why can’t we just create processes? The latter paradigm has been working over the course of many years, but process creation has some disadvantages, with just the few following examples:
- Process creation operation is resource-intensive.
- Processes require complicated mechanisms to access the same resources (named or unnamed pipes, message queues, sockets, etc), while threads automatically gain access to the same address space.
- Performance of multi-threaded processes is higher than single-threaded.
Multithreading allows several threads to be executed as part of one process. A programming model with threads provides developers with a comfortable abstraction of simultaneous execution. One of the advantages of a programme with threads is that it works faster on computers with a multicore processor. Threads use almost no resources when created, or additional plugins such as resource access mechanisms; besides, performance and application interactivity of threads are higher. Apart from address space, all threads use:
- Process regulations
- Signal handlers (settings for working with signals)
- Current directory
- User and group identifier
At the same time, each thread has its own:
- Thread identifier
- Stack
- Set of registers
- Signal mask
- Priority
Main functions for working with threads
At program startup by exec call, a main thread (initial thread) is created. Secondary threads are created by calling pthread_create for Linux or _beginthread(ex) for Windows.
Let’s look more closely at threads creation for Linux:
#include
int pthread_create(
pthread_t *tid,
const pthread_attr_t *attr,
void *(*func)(void *),
void *arg
);
/* Returns 0 in case of a successful completion, positive value in case of an error*/
Each thread has its identifier – pthread_t – and attributes: priority, initial stack size, daemon feature. When creating a thread, it is necessary to indicate the function address that will be executed (func), and also the single pointer argument (arg). Threads in Linux shall be exited either explicitly – by calling pthread_exit function – or inexplicitly – by returning from this function[2]. If under the conditions of the problem it is required to pass several arguments to a thread, it is necessary to use the address of the structure with arguments.
In Windows, threads are created with the help of _beginthread(ex) or CreateThread functions. Both are ?-runtime calls, and the main difference between them is that CreateThread is a “raw” Win32 API, and _beginthread(ex) in its turn calls CreateThread inside of itself. In this article, we will discuss _beginthread(ex) functions. The syntax of _beginthreadex is as follows:
uintptr_t _beginthreadex(
void *security,
unsigned stack_size,
unsigned(__stdcall *start_address)(void *),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);
It can be observed that between pthread_create and _beginthreadex calls there is some vague similarity; however, there are also differences. ?hus, in Windows: security – pointer to the structure SECURITY_ATTRIBUTES, thrdaddr – points to 32-bit variable that receives the thread identifier.
Let’s consider the following example of threads creation[3]:
#include
#ifdef __PL_WINDOWS__
#include
#endif //__PL_WINDOWS__
#ifdef __PL_LINUX__
#include
#endif //__PL_LINUX__
#define STACK_SIZE_IN_BYTES (2097152) //2MB
#ifdef __PL_WINDOWS__
unsigned int __stdcall process_command_thread(void) {
#endif //__PL_WINDOWS__
#if defined (__PL_LINUX__) || (__PL_SOLARIS__) || (__PL_MACOSX__)
void *process_command_thread(void *p) {
#endif //(__PL_LINUX__) || (__PL_SOLARIS__) || (__PL_MACOSX__)
printf("Hello from process command thread\n");
return 0;
}
int main(int argc, char *argv[])
{
#ifdef __PL_WINDOWS__
DWORD process_command_thread_id;
HANDLE h_process_command_thread;
h_process_command_thread = (HANDLE)_beginthreadex(
NULL,
STACK_SIZE_IN_BYTES,
process_command_thread,
NULL,
0,
(unsigned long *)&process_command_thread_id
);
if (h_process_command_thread == NULL)
return -1;
#endif //__PL_WINDOWS__
#ifdef __PL_LINUX__
pthread_t h_process_command_thread;
int h_process_command_thread_initialized;
int ret;
ret = pthread_create(
&h_process_command_thread,
NULL,
process_command_thread,
NULL
);
if (ret != 0)
return -1;
h_process_command_thread_initialized = 1;
#endif // __PL_LINUX__
printf("Hello from main thread\n");
return 0;
}
The output will be the following:
Linux |
Windows |
|
[root@localhost ~]# ./process Hello from main thread [root@localhost ~]# |
C:\>process.exe Hello from main thread C:\> |
|
It is easy to notice that process_command_thread was not run visually. When internal structures used for threads management are initialized by pthread_create or _beginthreadex function, the main thread finishes executing. We can expect a thread exit after calling pthread_join for Linux.
int pthread_join(pthread_t tid, void **retval);
A thread may be either joinable (by default) or detached. When a joinable thread is terminated, information (identifier, termination status, thread counter, etc.) is kept until pthread_join is called.
In the Windows OS, one of the wait-functions may be considered analogous to pthread_join. The wait functions family allows a thread to interrupt its execution and wait for a resource to be released. Let’s take a look at an analogue of pthread_join, which is WaitForSingleObject:
DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);
When this function is called, the first parameter, hObject, identifies the kernel object. This object may be in one of two states: «free» or «busy».
The second parameter, dwMilliseconds, indicates how many milliseconds a thread is ready to wait for the object to be released.
The following example illustrates pthread_join\WaitForSingleObject call:
#ifdef __PL_WINDOWS__
DWORD status = WaitForSingleObject(
h_process_command_thread,
INFINITE
);
switch (status) {
case WAIT_OBJECT_0:
// The process terminated
break;
case WAIT_TIMEOUT:
// The process did not terminate within timeout
break;
case WAIT_FAILED:
// Bad call to function
break;
}
#endif //__PL_WINDOWS__
#ifdef __PL_LINUX__
int status = pthread_join(
h_process_command_thread,
NULL
);
switch (status) {
case 0:
// The process terminated
break;
case default:
// Bad call to function
break;
}
#endif //__PL_LINUX__
#ifdef __PL_WINDOWS__
//Windows code
#endif //__PL_WINDOWS__
#ifdef __PL_LINUX__
//Code for UNIX OS systems
#endif //__PL_LINUX__
[1] For Linux, this article presents the threads interface defined by the POSIX.1-2001 standard (known as «pthreads»).
[2] Threads exit will be covered later in this article.
[3] In this example, as in other examples in this article, code base will be single for both Linux and Windows. The difference will be in the compilation condition:
#ifdef __PL_WINDOWS__
//Windows code
#endif //__PL_WINDOWS__
#ifdef __PL_LINUX__
//Code for UNIX OS systems
#endif //__PL_LINUX__
Eduard Trunov is a software engineer at Auriga. He is responsible for developing new features for high-volume business applications on Linux. Prior to that, he was the pivotal .NET engineer on a project developing and supporting functional automated asset risk management systems for electrical devices. His professional interests include C, .NET, operating systems, and debugging.
This blog is first in a series of four, click here for part 2.