How pthread_exit() and pthread_cancel() calls cleanup routine in Linux?

Viewed 547

I have observed while learning concept about pthread_cleanup_push, that pthread_exit() and pthread_cancel() impact on pthread_cleanup_pop() in different ways. Below is the code sample.

void push_routine_1(void * arg) {
    printf(" Push Routine 1\n");
}

void push_routine_2(void * arg) {
    printf(" Push Routine 2\n");
}

void push_routine_3(void * arg) {
    printf(" Push Routine 3\n");
}

void push_routine_4(void * arg) {
    printf(" Push Routine 4\n");
}

void * thread_routine(void * arg) {
    pthread_cleanup_push(push_routine_1, NULL);
    pthread_cleanup_push(push_routine_2, NULL);
    pthread_cleanup_push(push_routine_3, NULL);
    pthread_cleanup_push(push_routine_4, NULL);

    pthread_exit(NULL);

    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
}

Above program gives such output:

Push Routine 4
Push Routine 3 
Push Routine 2 
Push Routine 1

While using pthread_cancel(pthread_self()) instead of pthread_exit(NULL), gives output as below

Push Routine 3 
Push Routine 2 
Push Routine 1

What is the reason behind not executing push_routine_4() while using pthread_cancel()?

1 Answers

I don't think you may assume that pthread_cancel(pthread_self()) is exactly the same as pthread_exit(NULL).

POSIX does not tell me what to expect pthread_cancel(pthread_self()) to do. But I note that it says:

2.9.5 Thread Cancellation

The thread cancellation mechanism allows a thread to terminate the execution of any other thread in the process in a controlled manner.

where "any other thread" does make me wonder. But I don't think that's the issue.

The default 'cancelability type' is PTHREAD_CANCEL_DEFERRED. So, pthread_cancel() will not take effect until the next 'Cancellation Point' in the cancelled thread. So your code will continue in to the first pthread_cleanup_pop(1), which is not a 'Cancellation Point', which will then printf(" Push Routine 4\n"), which may be a 'Cancellation Point'.

I adjusted your code as follows:

static volatile unsigned step ;

void push_routine_1(void * arg) {
  step += 10 ;
  printf(" Push Routine 1 -- %u\n", step);
  step += 10 ;
}

void push_routine_2(void * arg) {
  step += 100 ;
  printf(" Push Routine 2 -- %u\n", step);
  step += 100 ;
}

void push_routine_3(void * arg) {
  step += 1000 ;
  printf(" Push Routine 3 -- %u\n", step);
  step += 1000 ;
}

void push_routine_4(void * arg) {
  step += 10000 ;
  printf(" Push Routine 4 -- %u\n", step);
  step += 10000 ;
}

void * thread_routine(void * arg)
{
  unsigned frag = *(unsigned*)arg ;

  printf("Start %u\n", frag) ;

  step = 0 ;
  pthread_cleanup_push(push_routine_1, NULL); step += 10 ;
  pthread_cleanup_push(push_routine_2, NULL); step += 100;
  pthread_cleanup_push(push_routine_3, NULL); step += 1000 ;
  pthread_cleanup_push(push_routine_4, NULL); step += 10000 ;

  step++ ;
  if (frag == 0)
    pthread_exit(NULL) ;        // 11111
  else
    pthread_cancel(pthread_self()) ;

  step++ ;                      // 11112

  if (frag == 2)
    printf(" Continue %u\n", step) ;

  step++ ;                      // 11113
  pthread_cleanup_pop(1);       // (4)
  step++ ;                      // xxxx4
  pthread_cleanup_pop(1);       // (3)
  step++ ;                      // xxxx5
  pthread_cleanup_pop(1);       // (2)
  step++ ;                      // xxxx6
  pthread_cleanup_pop(1);       // (1)
  step++ ;                      // xxxx7
}

int
main(Unused int argc, Unused char* argv[])
{
  unsigned frag ;

  for (frag = 0 ; frag < 3 ; ++frag)
    {
      pthread_t thr ;

      pthread_create(&thr, NULL, thread_routine, &frag) ;
      pthread_join(thr, NULL) ;

      printf(" Finally %u\n", step) ;
    } ;

  return 0 ;
}

And the result was:

Start 0
 Push Routine 4 -- 21111
 Push Routine 3 -- 32111
 Push Routine 2 -- 33211
 Push Routine 1 -- 33321
 Finally 33331
Start 1
 Push Routine 4 -- 21113
 Push Routine 3 -- 22113
 Push Routine 2 -- 23213
 Push Routine 1 -- 23323
 Finally 23333
Start 2
 Continue 11112
 Push Routine 4 -- 21112
 Push Routine 3 -- 32112
 Push Routine 2 -- 33212
 Push Routine 1 -- 33322
 Finally 33332

So for:

  • 'Start 0' we see that (as expected) the thread proceeds no further than pthread_exit(NULL).

  • 'Start 1' we see that the thread gets to pthread_cleanup_pop(1); // (4), but push_routine_4() does not get past the printf().

    I believe this shows that the cancellation process starts once that printf() completes.

  • 'Start 2' we see that push_routine_4() is invoked after the printf(" Continue %u\n", step), and before the step++ which follows it.

    I believe this shows that the cancellation process starts once that printf(" Continue ...) completes.

On my machine, Linux 5.6.8 running glibc 2.30, it appears that printf() is a 'Cancellation Point', after the function completes. The effect you get suggests perhaps printf() is a 'Cancellation Point' before it prints anything ? [Or there is some other reason the printf() in push_routine_4() does not produce any output.]

Related