HOWTO detect that a pthreads thread has exited

This question came up in a discussion on the Poly/ML mailing list. How can you tell that a pthreads thread has exited, so that you can clean up any resources dedicated to that thread? Here is a program that demonstrates one approach. It is a cleaned up and enhanced version of some code I posted to the mailing list at the time, and thought might be useful for others.

The idea is to use a pthread_key_t with a destructor function. If a thread assigns a value to that key, then the destructor function is called on the value when the thread exits.

/**
 * pthread_destructor - Demonstration of using a pthread key to detect that a
 * thread has exited.
 *
 * Written in 2013 by Jerry James <loganjerry@gmail.com>.
 *
 * To the extent possible under law, the author has dedicated all copyright
 * and related and neighboring rights to this software to the public domain
 * worldwide. This software is distributed without any warranty.
 *
 * You should have received a copy of the CC0 Public Domain Dedication along
 * with this software.  If not, see
 * <http://creativecommons.org/publicdomain/zero/1.0/>.
 */

#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>

/**
 * Size of the user input buffer.
 */
#define DESTRUCTOR_BUFSIZ (1 << 10U)

/**
 * Type used to initialize threads.
 */
struct destructor_init {

  /**
   * The condition variable used to make this thread exit.
   */
  pthread_cond_t di_exit;

  /**
   * This thread's number.
   */
  size_t di_number;

  /**
   * This thread's internal identifier.
   */
  pthread_t di_thread;

  /**
   * Whether the thread is still running
   */
  int di_running;
};

/**
 * The One Big Lock that protects everything.
 */
static pthread_mutex_t destructor_lock = PTHREAD_MUTEX_INITIALIZER;

/**
 * Key to per-thread values.
 */
static pthread_key_t destructor_key;

/**
 * Buffer for reading input from the user.
 */
static char destructor_buffer[DESTRUCTOR_BUFSIZ];

/**
 * Give the user a clue.
 *
 * @param[in] progname the name of the executable
 */
static void
destructor_usage (const char *progname)
{
  fprintf (stderr, "Usage: %s num_threads\n\
       where num_threads is between 0 and 1000, exclusive\n",
       progname);
}

/**
 * Destructor function run on the shared key when a thread exits.
 *
 * @param[in] value a pointer to the number of this thread
 */
static void
key_destructor (void *value)
{
  size_t *number = (size_t *) value;
  printf ("This is thread %zu signing off!\n", *number);
}

/**
 * Function run by each child thread.
 *
 * @param[in] arg a pointer to the destructor_init structure for this thread
 * @return @c NULL
 */
static void *
destructor_child (void *arg)
{
  struct destructor_init *init = (struct destructor_init *) arg;
  pthread_setspecific (destructor_key, &init->di_number);
  pthread_mutex_lock (&destructor_lock);
  pthread_cond_wait (&init->di_exit, &destructor_lock);
  pthread_mutex_unlock (&destructor_lock);
  init->di_running = 0;
  return NULL;
}

int
main (int argc, char *argv[])
{
  struct destructor_init *inits;
  size_t i, num_children;
  int err;

  /* Get the number of child threads to spawn */
  if (argc != 2)
    {
      destructor_usage (argv[0]);
      return EXIT_FAILURE;
    }
  num_children = strtoul (argv[1], NULL, 0);
  if (num_children < 1U || num_children > 999U)
    {
      destructor_usage (argv[0]);
      return EXIT_FAILURE;
    }

  /* Set up the children array */
  inits = (struct destructor_init *) calloc (num_children, sizeof (*inits));
  if (inits == NULL)
    {
      fputs ("Out of memory.\n", stderr);
      return EXIT_FAILURE;
    }

  /* Create the key for the thread-specific values */
  err = pthread_key_create (&destructor_key, key_destructor);
  if (err != 0)
    {
      errno = err;
      printf ("pthread_key_create: %m\n");
      free (inits);
      return EXIT_FAILURE;
    }

  /* Spawn the children */
  for (i = 0U; i < num_children; i++)
    {
      pthread_cond_init (&inits[i].di_exit, NULL);
      inits[i].di_number = i;
      inits[i].di_running = 1;
      pthread_create (&inits[i].di_thread, NULL, destructor_child, &inits[i]);
    }

  /* Ask the user which thread should exit next */
  for (;;)
    {
      size_t threadno;
      char *endptr;

      fputs ("Choose a thread, or ENTER to exit: ", stdout);
      fflush (stdout);
      if (fgets (destructor_buffer, DESTRUCTOR_BUFSIZ, stdin) == NULL)
	break;
      threadno = strtoul (destructor_buffer, &endptr, 0);
      if (endptr == destructor_buffer)
	{
	  if (destructor_buffer[0] == '\n' && destructor_buffer[1] == '\0')
	    break;
	  printf ("Enter a nonnegative integer less than %zu, or a blank line to exit.\n",
		  num_children);
	}
      else if (inits[threadno].di_running)
	{
	  pthread_mutex_lock (&destructor_lock);
	  pthread_cond_signal (&inits[threadno].di_exit);
	  pthread_mutex_unlock (&destructor_lock);
	  pthread_join (inits[threadno].di_thread, NULL);
	  pthread_cond_destroy (&inits[threadno].di_exit);
	}
      else
	{
	  printf ("Thread %zu has already exited.\n", threadno);
	}
    }

  /* Kill the rest of the threads */
  for (i = 0U; i < num_children; i++)
    {
      if (inits[i].di_running)
	{
	  pthread_mutex_lock (&destructor_lock);
	  pthread_cond_signal (&inits[i].di_exit);
	  pthread_mutex_unlock (&destructor_lock);
	  pthread_join (inits[i].di_thread, NULL);
	  pthread_cond_destroy (&inits[i].di_exit);
	}
    }

  /* Cleanup and exit */
  pthread_key_delete (destructor_key);
  free (inits);
  return EXIT_SUCCESS;
}