Thread Synchronization in Linux and Windows Systems, Part 2

By Eduard Trunov

Software Engineer

Auriga

July 22, 2019

Story

Thread Synchronization in Linux and Windows Systems, Part 2

This article will be useful for those who have never programmed applications with multiple threads but plan to do so in the future.

In Windows code base, there is a constant INFINITE, which is passed by the second parameter. This constant indicates that a thread waits for an event infinitely. The constant is declared in WinBase.h and is defined as 0xFFFFFFFF (or -1).

In addition, Windows code includes WAIT_TIMEOUT. This condition is not represented in Linux. In practice, this restriction is bypassed with the help of the following functions:

int pthread_tryjoin_np(pthread_t thread, void **retval) [1];

int pthread_timedjoin_np(

        pthread_t thread, 

        void **retval, 

        const struct timespec *abstime

);

 

               If you refer to the pthread_tryjoin_np help page, you can see that EBUSY may be an error, and WaitForSingleObject cannot inform us about it. To know the state of the thread and identify its exit code, it is necessary to call the function:

 

BOOL GetExitCodeThread(HANDLE hThread, PDWORD pdwExitCode);

 

               The exit code is returned as a variable that pdwExitCode points to. If the thread has not terminated when the function is called, then STILL_ACTIVE identifier is filled as the variable. If the call is successful, then the function returns TRUE.

Let’s consider a case of pthread_tryjoin_np function usage for Linux and GetExitCodeThread\WaitForSingleObject function for Windows.

 

#ifdef __PL_WINDOWS__

      DWORD dwret;

      BOOL bret;

      DWORD h_process_command_thread_exit_code;

      if (h_process_command_thread != NULL) {

            bret = GetExitCodeThread(

                  h_process_command_thread,

                  &h_process_command_thread_exit_code

            );

            if (h_process_command_thread_exit_code == STILL_ACTIVE) {

                  dwret = WaitForSingleObject(

                        h_process_command_thread,

                        5000  // 5000ms

                  );

                  switch (dwret) {

                        case WAIT_OBJECT_0:

                              // everything from this point on is good break;

                        case WAIT_TIMEOUT:

                        case WAIT_FAILED:

                        default:

                              SetLastError(dwret);

                              break;

                  }

            }

      }

#endif //__PL_WINDOWS__

#ifdef __PL_LINUX__

      int iret;

      struct timespec wait_time = { 0 };

      if (h_process_command_thread_initialized == 1) {

            iret = pthread_tryjoin_np(

                  h_process_command_thread,

                  NULL

            );

            if ((iret != 0) && (iret != EBUSY)) {

                  //TODO: process the error

            }

            if (iret == EBUSY) {

                  clock_gettime(CLOCK_REALTIME, &wait_time);

                  ADD_MS_TO_TIMESPEC(wait_time, 5000);

                  iret = pthread_timedjoin_np(

h_process_command_thread,

NULL,

&wait_time

);

                  switch (iret) {

                        case 0:

                              // everything from this point on is good

                              break;

                        case ETIMEDOUT:

                        case EINVAL:

                        default:

                              break;

                  }

            }

      }

#endif //__PL_LINUX__

 

An attentive reader will notice that ADD_MS_TO_TIMESPEC is a macro that is not represented in Linux OS. Macros are added to wait_time 5000 ms, but macros implementation falls outside the scope of this article. It also should be mentioned that in Linux we need to introduce a separate variable, h_process_command_thread_initialized, because pthread_t is unsigned long (in general), and we cannot verify it.

            Let’s sum up the results. Linux and Windows OSs provide an opportunity to create threads inside of an application. In Windows OS the type is HANDLE, and in Linux – pthread_t. In case of creating a joinable thread in Linux OS it is necessary to write pthread_join, even if we are sure that the thread has terminated. This practice will help us to avoid system resources leakage.

            Discussed functions are recorded in Table 1.

Linux functions

Windows functions

pthread_create

beginthreadex

pthread_join

WaitForSingleObject(.., INFINITE)

pthread_timedjoin_np

GetExitCodeThread\WaitForSingleObject

pthread_tryjoin_np

GetExitCodeThread

Table 1. Functions for thread synchronization in Windows and Linux OSs.

Events

Events are instances of kernel objects variation. Events inform about an operation termination and are normally used when a thread performs initialization and then signals to another thread that it can continue working. The initializing thread transforms the «event» object into an unsignaled state, after which it proceeds with its operations. When completed, it releases the event to signaled state. In its turn, the other thread that has been waiting for the event to change its state to signaled, resumes, and again becomes scheduled.

Let’s take a look at functions for working with «event» objects in Windows and Linux OSs.

In Windows OS an «event» object is created with the CreateEvent function:

 

HANDLE CreateEvent(

      PSECURITY_ATTRIBUTES psa,

      BOOL fManualReset,

      BOOL fInitialState,

      PCSTR pszName

);

 

Let’s bring to a sharper focus fManualReset and fInitialState parameters. FManualReset parameter of BOOL type informs the system about a need to create a manual-reset event (TRUE) or an auto-reset event (FALSE). The fInitialState parameter determines the initial state of the event: signaled (TRUE) or unsignaled (FALSE).

After the event is created, there is a possibility to manage the state. To transit an event to a signaled state, you need to call:

 

BOOL SetEvent(HANDLE hEvent);

 

To change the event state to unsignaled, you need to call:

 

BOOL ResetEvent(HANDLE hEvent);

 

            To wait for an event signal, you need to use the already-familiar-to-us WaitForSingleObject function.

            In Linux OS, an «event» object represents an integer descriptor. An integer «event» object is created with the eventfd function:

 

int eventfd(unsigned int initval, int flags);

 

The initval parameter is a kernel serviced counter. The flags parameter is required for eventfd behavior modification, which may be EFD_CLOEXEC, EFD_NONBLOCK, or EFD_SEMAPHORE. If terminated successfully, eventfd returns a new file descriptor, which can be used to link the eventfd object.

Analogous to SetEvent, we can use an eventfd_write call:

 

ssize_t eventfd_write(int fd, const void *buf, size_t count);

 

When write is called from the buffer, an 8-byte integer value is added to the counter. The maximum counter value can be a 64-bit unsigned minus 1. In case of a successful function call, the written number of bytes is returned.

Before we discuss a ResetEvent analogue, let’s take a look at the poll function.

 

#include

int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

 

The poll function allows an application to simultaneously block several descriptors and receive notifications as soon as any of them is ready for reading or writing. Work of poll (generally) can be described as follows:

  1. Notify when any of the descriptors is ready for an input–output operation.
  2. If none of the descriptors is ready, go to sleep mode until one or more descriptors are ready.
  3. In case there are available descriptors ready for input–output, handle them without blocking.
  4. Go back to step 1.

Linux OS offers three entities for multiplexed input–output: interface for selection (select), polling (poll), extended polling (epoll).

Those who have experience working with select might appreciate the advantage of poll, which uses a more effective method with three groups of descriptors based on bit masks. Poll call works with a single array of nfds pollfd structures that file descriptors point to.

Let’s take a look at the pollfd structure definition:

 

struct pollfd {

      int fd; /* file descriptor */

      short events; /* requested events */

      short revents; /* returned events */

};

There is a file descriptor indicated in each pollfd structure that will be tracked. Several file descriptors may be passed to the poll function (pollfd array of structures). The number of elements in an fdarray array is determined by the nfds argument.

To communicate what events we are interested in to the kernel, it is necessary to write one or more values from Table 2 in the events field for each element from the array. After returning from poll functions, the kernel specifies what events occurred for each of the descriptors.

 

Name

Events

Revents

Description

POLLIN

+

+

Data are available for reading (except for high priority)

POLLRDNORM

+

+

Regular data (priority 0) are available for reading

POLLRDBAND

+

+

Data with non-zero priority are available for reading

POLLPRI

+

+

High priority data are available for reading

POLLOUT

+

+

Data are available for writing

POLLWRNORM

+

+

Analogous to POLLOUT

POLLWRBAND

+

+

Data with non-zero priority are available for writing

POLLERR

 

+

Error occurred

POLLHUP

 

+

Connection lost

POLLNVAL

 

+

There is a mismatch between the descriptor and the open file

Table 2. Possible values of events and revents flags of poll function.

 

Argument timeout defines the wait time for occurrence of specified events. There are three possible values for timeout.

  • timeout = -1: The wait time is infinite (INFINITE in WaitForSingleObject).
  • timeout = 0: The wait time is equal to 0, which indicates that it is necessary to inspect all specified descriptors and give the control back to the calling program.
  • timeout > 0: Wait for no longer than timeout milliseconds.

Having reviewed the poll function, we can draw a conclusion that there is an analogy to WaitForSingleObject for “event” objects in Windows OS.

 

Let’s move to the ResetEvent analogue for Linux.

#ifdef __PL_LINUX__

      struct pollfd wait_object;

      uint64_t event_value;

      int ret;

      if (eventfd_descriptor > 0) { // Descriptor created by eventfd(0,0)          

wait_object.fd = eventfd_descriptor;

            wait_object.events = POLLIN;

            wait_object.revents = 0;

            ret = poll(&wait_object, 1, 0); // Do not wait

            if (ret < 0) { // Error

            } else {

                  if ((wait_object.revents & POLLIN) != 0) {

                        iret = eventfd_read(eventfd_descriptor, &event_value);

                        if (iret != 0) { // Error }

                  }

            }

      }

#endif //__PL_LINUX__

 

Initially we check that eventfd_descriptor is greater than zero[2] (really, this was originally created by eventfd function without errors). After that, we  initialize the pollfd function and run poll. Execution of poll is required to check if there is available data for reading. If such data are available, we will read it.

Through the lens of all of the above-described, let’s reflect the outcome in Table 3:

Windows functions

Linux functions

CreateEvent

eventfd

SetEvent

eventfd_write

ResetEvent

poll/eventfd_read

WaitForSingleObject

poll

Table 3. Main functions for working with events in Windows and their analogues in 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 second in a series of four, click here for part 3.


[1] Based on reference description, this function is a nonstandard GNU-extension, which requires a “_np” (nonportable) suffix.

[2] As it is a file descriptor, it will be the least unusable positive numeric value. It is supposed that an application does not close STDIN_FILENO, otherwise it requires a different verification.

Categories
Software & OS