Thread Synchronization in Linux and Windows Systems, Part 3
July 29, 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.
Multithreaded applications parallelism
Typically, there are two interrelated but different phenomena in multi-threaded applications: concurrency and parallelism.
Concurrency is the ability of two and more threads to overlap in execution.
Parallelism is the ability to execute two and more threads at the same time.
It is concurrency that causes the majority of complications in streaming – threads can be executed in unpredictable order relative to each other. In case of sharing resources by the threads, this will undoubtedly lead to race conditions.
The term race condition commonly refers to a situation when unsynchronized access to a shared resource for two and more threads leads to erroneous program behavior.
Let’s take a look at an example of a race.
Nowadays it is hard to imagine our life without plastic cards. ATM cash withdrawal became a daily routine long ago: insert a card, enter PIN-code and desired sum. If completed successfully we receive the planned amount of cash. The bank in its turn needs to verify if the money is available by the following algorithm:
- Is there a minimum of X units of money available on the bank account?
- If yes, reduce the balance of the account by X value, dispense X money units to the user.
- Otherwise, generate an error message.
An example of code with race conditions:
int cash_out(struct account *ac, int amount) {
const int balance = ac->balance;
if (balance < amount)
return -1;
ac->balance = balance - amount;
discard_money_routine(amount);
return 0;
}
Race can emerge in a situation when a purchase is paid for online and cash is withdrawn from an ATM “concurrently”.
To avoid a race, it is necessary to introduce the following update to the code:
int cash_out(struct account *ac, int amount) {
lock();
const int balance = ac->balance;
if (balance < amount)
return -1;
ac->balance = balance - amount;
unlock();
discard_money_routine(amount);
return 0;
}
In Windows OS, the code area requiring exclusive access to some shared data is called a «critical section».
The structure type for working with critical sections is CRITICAL_SECTION. Let’s review its fields:
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
Though CRITICAL_SECTION formally does not belong to undocumented structures, Microsoft, nevertheless, believes that there is no need for users to know its organization. In practice, it is a sort of black box. To work with this structure, there is no need to use its field directly, but only through Windows functions, passing to them the address of a corresponding instance of this structure.
The CRITICAL_SECTION structure is initialized by the following call:
void InitializeCriticalSection(PCRITICAL_SECTION pcs);
If we know that CRITICAL_SECTION structure will no longer be needed, then we can delete it with the help of the following call:
void DeleteCriticalSection(PCRITICAL_SECTION pcs);
The code area working with a shared resource shall be preceded by the following call:
void EnterCriticalSection(PCRITICAL_SECTION pcs);
Instead of EnterCriticalSection we can use:
bool TryEnterCriticalSection(PCRITICAL_SECTION pcs);
TryEnterCriticalSection allows a thread to check the resource accessibility and engage in another activity in cases when it is not accessible. In the case of success (the function returned TRUE), it is clear that structure elements are updated, and the resource is locked.
At the end of the code area using a shared resource, there shall always be the following call:
void LeaveCriticalSection(PCRITICAL_SECTION pcs);
LeaveCriticalSection inspects CRITICAL_SECTION structure elements and decreases resource locking counter (LockCount) by 1.
Analogous to CRITICAL_SECTION in Linux OS is the variable mutex pthread_mutex_t. Before using, this variable needs to be initialized – write the value of the constant PTHREAD_MUTEX_INITIALIZER or call to pthread_mutex_init function.
#include
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
To initialize a mutex with default attribute values, it is necessary to pass NULL to attr attribute. Specific mutex attribute values can be located on the help page.
A mutex can be deleted with the help of the following call:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
A mutex is locked by calling to the pthread_mutex_lock function:
int pthread_mutex_lock(pthread_mutex_t *mutex);
If a mutex is already locked, the calling thread will be blocked until the mutex is released. The mutex is unlocked with the help of the pthread_mutex_unlock function:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
If we want to check a resource’s accessibility, then we can use the pthread_mutex_trylock function:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
The above-mentioned function will return EBUSY if a mutex is locked.
All functions for working with a mutex return 0 in the case of success and an error code in the case of failure.
Let’s sum it up. In Windows OS, to work with a shared resource it is necessary to use a critical section and a special type CRITICAL_SECTION. In Linux OS, we can use mutexes of pthread_mutex_t type for the same purpose.
Synchronization functions are recorded in Table 4.
Windows functions |
Linux functions |
InitializeCriticalSection |
pthread_mutex_init() |
EnterCriticalSection |
pthread_mutex_lock() |
LeaveCriticalSection |
pthread_mutex_unlock() |
TryEnterCriticalSection |
pthread_mutex_trylock() |
DeleteCriticalSection |
pthread_mutex_destroy() |
Table 4. Synchronization functions for shared resources.
Thread termination
One of the cases when it is necessary to write a thread termination in practice is mass data processing. A situation is possible when the main thread signals to all threads to exit but one of them is still processing information. If promptitude is a higher priority factor for application performance in comparison to information loss, then the thread needs to be exited and release system resources. This section will cover ways to exit a thread.
A thread can exit in the following ways:
- The thread function returns
- The thread calls the ExitThread function
- Any thread of the process calls the TerminateThread function
- Any thread of the process calls the ExitProcess function
Let’s take a closer look at each of these.
The thread function returns.
A good example of clean code is considered designing a thread function so that the thread is terminated only after the function returns. In Windows OS, this way of thread termination guarantees correct clean-up of resources owned by the thread. In Linux OS, it is necessary to call to one of the join functions in cases where a thread is joinable. In the general case, the following happens:
- The system correctly releases resources taken by the thread.
- The system sets a thread exit code.
- Users counter for this kernel object «thread» is reduced by 1.
In Windows OS, a thread can be forcefully terminated by calling to:
void ExitThread(DWORD dwExitCode);[1]
The thread exit code value will be added to the dwExitCode parameter. It is easy to notice that the function does not have return value, because after the function is called the thread ceases to exist.
In Linux OS, there is a complete analogue for ExitThread:
void pthread_exit(void *rval_ptr);
Argument rval_ptr represents an untyped pointer, containing a return value. This pointer can be obtained by other process threads calling to the pthread_join function.
The function call pthread_join takes the thread to a detached state. This state allows the thread resources to be won back. In case the thread has already been in a detached state, the thread calling to pthread_join will get the ESRCH error code. Sometimes when pthread_join is recalled with the second non- NULL parameter, Segmentation Fault error output is possible.
Any thread of the process calls the TerminateThread function.
One thread can pass a request to forcefully terminate another thread within the same process. In Windows OS, this is organized with the help of the following function:
bool TerminateThread(
HANDLE hThread,
DWORD dwExitCode
);
The above-mentioned function terminated the hThread thread from any other thread. You can add to dwExitCode parameter a value that the system will consider as the thread exit code. After the thread is killed, users counter for this kernel object «thread» will be reduced by 1.
In Linux OS, a similar capability is implemented when one thread can pass a request for forceful termination of another thread within the same process by calling to the pthread_cancel function:
int pthread_cancel(pthread_t tid);
This function needs to be used in conjunction with pthread_setcancelstate and pthread_setcanceltype functions. In case of using pthread_cancel, rval_ptr will be PTHREAD_CANCELED.
Let’s take a closer look at erminateThread and analogous actions in Linux OS:
#ifdef __PL_WINDOWS__
BOOL bret = FALSE;
bret = TerminateThread(h, x);
#endif //__PL_WINDOWS__
#ifdef __PL_LINUX__
int iret = 0, bret;
iret = syscall(SYS_tkill,tid, 0);
if (iret == 0) {
iret = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);
if (iret != 0) {
bret = FALSE;
}
else {
iret = pthread_cancel(h);
if (iret == 0 || iret == ESRCH) {
bret = TRUE;
} else {
wait_thread:
clock_gettime(CLOCK_REALTIME, &wait_time);
ADD_MS_TO_TIMESPEC(wait_time, 1000); //1000 ms
iret = pthread_timedjoin_np(h, NULL, &wait_time);
switch (iret) {
case 0:
bret = TRUE;
break;
case ETIMEDOUT:
if (retries_count++ < 5) // 5 Attempts
{
goto wait_thread;
}
bret = FALSE;
break;
default:
bret = FALSE;
break;
}
}
(void)pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
}
}
else {
bret = TRUE;
}
#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 third in a series of four, click here for part 4.
[1] ExitThread is a function that kills a thread. In practice, it is best to use the _endthreadex function.