Semantics of the new semaphore in glibc 2.21

A new implementation of the core semaphore algorithm was completed for glibc 2.21 which was released on February 6th 2015, addressing the behaviour captured in issue 12674 and strengthening semaphore destruction semantics. Releases of glibc before 2.21 have looser semaphore destruction semantics and code that runs on older versions of glibc must take this into account.

It is not a defect to have a looser set of object destruction semantics, it is simply a choice of implementation, but such a choice makes it harder for application developers to use the synchronization objects. We will discuss destruction semantics next, and the clarifications that were made to POSIX to require stronger semantics.

Destruction semantics set out the rules for the destruction of the synchronization object. Strong destruction semantics are ones where the synchronization object may be destroyed once it is no longer referenced, and the caller knows no other synchronization operations are in progress. These semantics allows application developers to avoid having to use an external secondary synchronization object to control destruction. Once the synchronization object can be acquired or locked, and the caller knows no other synchronization operations are in progress, the object may unlocked and safely destroyed.

The following examples consciously elide error checking of the called functions to make it easier to follow the code and the discussion. All calls to pthread_create, pthread_join, sem_init, sem_post, sem_wait, sem_destory, and malloc should have error checking.

For example the following is legal in a sempahore implementation with strong destruction semantics:

   1 #include <stdlib.h>
   2 #include <pthread.h>
   3 #include <semaphore.h>
   4 
   5 void *
   6 th0 (void *arg)
   7 {
   8   sem_t *sem = (sem_t *) arg;
   9   /* Wait for unlock. Destroy. Free.  */
  10   sem_wait (sem);
  11   sem_destroy (sem);
  12   free (sem);
  13   return NULL;
  14 }
  15 
  16 void *
  17 th1 (void *arg)
  18 {
  19   /* Unlock.  */
  20   sem_post ((sem_t *) arg);
  21   return NULL;
  22 }
  23 
  24 int
  25 main (void)
  26 {
  27   sem_t *sem;
  28   pthread_t th[2];
  29 
  30   /* Initialized locked.  */
  31   sem = (sem_t *) malloc (sizeof (sem_t));
  32   sem_init (sem, 0, 0);
  33 
  34   pthread_create (&th[0], NULL, th0, sem);
  35   pthread_create (&th[1], NULL, th1, sem);
  36 
  37   pthread_join (th[0], NULL);
  38   pthread_join (th[1], NULL);
  39   return 0;
  40 }

Notice that sem_destroy can be called immediately after sem_wait returns without waiting for sem_post to return. The stronger destruction semantics guarantee that once sem_post allows sem_wait to return then it no longer has a reference and destruction is possible immediately in this example.

In an implementation with loose destruction semantics an outer semaphore is required to make the destruction legal:

   1 #include <stdlib.h>
   2 #include <pthread.h>
   3 #include <semaphore.h>
   4 
   5 /* Controls semaphore destruction.  */
   6 sem_t allow_destruction;
   7 
   8 void *
   9 th0 (void *arg)
  10 {
  11   sem_t *sem = (sem_t *) arg;
  12   /* Wait for unlock.  */
  13   sem_wait (sem);
  14   /* Wait for destruction to be allowed. Destroy. Free.  */
  15   sem_wait (&allow_destruction);
  16   sem_destroy (sem);
  17   free (sem);
  18   return NULL;
  19 }
  20 
  21 void *
  22 th1 (void *arg)
  23 {
  24   /* Unlock.  */
  25   sem_post ((sem_t *) arg);
  26   /* Indicate destruction is allowed now.  */
  27   sem_post (&allow_destruction);
  28   return NULL;
  29 }
  30 
  31 int
  32 main (void)
  33 {
  34   sem_t *sem;
  35   pthread_t th[2];
  36 
  37   /* Do not allow destruction initially.  */
  38   sem_init (&allow_destruction, 0, 0);
  39 
  40   /* Initialized locked.  */
  41   sem = (sem_t *) malloc (sizeof (sem_t));
  42   sem_init (sem, 0, 0);
  43 
  44   pthread_create (&th[0], NULL, th0, sem);
  45   pthread_create (&th[1], NULL, th1, sem);
  46 
  47   pthread_join (th[0], NULL);
  48   pthread_join (th[1], NULL);
  49   return 0;
  50 }

The outer semaphore ensures that th0's call to sem_destroy has a happens-after relationship with respect to th1's call of sem_post returning and ensures no use of the semaphore sem by th1, thus allowing the destruction of the object.

The new semaphore work was part of a campaign to help developers by strengthening and clarifying the destruction semantics of the POSIX synchronization objects implemented by glibc. As stated above, issue 12674 was not a bug, but a change in the industry and academic best practice regarding how synchronization objects should behave semantically.

The key clarification required to make broader changes to glibc came with the Austin Group clarification for issue 811. While issue 811 clarifies POSIX mutex destruction semantics it does not cover semaphores or other synchronization primitives, however, the discussion is viewed as setting the standard for what is expected of a high quality implementation. The old semaphore implementation and the new semaphore implementation are both conforming to the POSIX standard, but as we discuss below, the newer implementation is of a higher quality.

It may seem like the existing text for sem_destroy does not have any of the ambiguities that were present for pthread_mutex_destroy, but that is not entirely true. The relevant text from the most recent POSIX issue 8 sem_destroy (2017-04-19) is as follows:

The existing text covers the entry into sem_wait by saying that destruction of a semaphore with currently blocked threads is undefined, but the same situation of concurrent sem_post is not clarified. Of the many possible execution orderings two are interesting for this discussion, the one where sem_post can see the destruction operation done by sem_destroy, and the one where sem_post does not. If sem_post can see the destruction it has a chance to return EINVAL. If sem_post cannot see the destruction then it may trigger a use-after-free or worse memory corruption as it inspects the semaphore memory. The POSIX semaphore specification does not clarify how to make pending sem_post operations safe.

If the implementation assumes sem_destory and sem_post can both reference the same semaphore, then the caller of sem_destroy (or the implementation) must not free any underlying resource until all outstanding sem_post calls return. In the old glibc semaphore implementation, an external synchronization object was required to do that waiting.

The new implementation in glibc 2.21 and later ensures that sem_post will not reference the semaphore after sem_wait can acquire said semaphore. This ensures that once sem_wait returns the application can use its own application logic to decide immediately if it can call sem_destroy and release associated resources (underlying memory). No external synchronization primitive is required because the inherent return of sem_wait provides that synchronization.

In all cases program logic is required to ensure no new sem_post operations are started.

Older releases

From a distribution perspective there is a large cost in backporting the new semaphore implementation. All of the required atomic operations need to be backported and verified on potentially older compilers. The performance and behavioural characteristics of the semaphore are changed, and users of existing releases do not expect that. Assembly implementations of the semaphore operations for x86, x86_64, POWER, and SH were removed and replaced with a maintainable and easier-to-prove-correct implementation in C. Overall the cost of the backport to older releases is risky and provides no benefit since existing code must have already assumed looser destruction semantics. New code can adopt a simpler design using the newer semantics.

In summary: older releases of glibc continue to have looser semaphore destruction semantics and those semantics will not be changed for official release branches. Application developers must be aware of which implementation they are targeting and design accordingly.

None: NewSemaphore (last edited 2017-05-11 20:50:39 by CarlosODonell)