关于并发:Pthread-并发编程三深入理解线程取消机制

3次阅读

共计 11455 个字符,预计需要花费 29 分钟才能阅读完成。

Pthread 并发编程(三)——深刻了解线程勾销机制

根本介绍

线程勾销机制是 pthread 给咱们提供的一种用于勾销线程执行的一种机制,这种机制是在线程外部实现的,仅仅可能在共享内存的多线程程序当中应用。

根本应用


#include <stdio.h>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>


void* task(void* arg) {usleep(10);
  printf("step1\n");
  printf("step2\n");
  printf("step3\n");
  return NULL;
}

int main() {

  void* res;
  pthread_t t1;
  pthread_create(&t1, NULL, task, NULL);
  int s = pthread_cancel(t1);
  if(s != 0) // s == 0 mean call successfully
    fprintf(stderr, "cancel failed\n");
  pthread_join(t1, &res);
  assert(res == PTHREAD_CANCELED);
  return 0;
}

下面的程序的输入后果如下:

step1

在下面的程序当中,咱们应用一个线程去执行函数 task,而后主线程会执行函数 pthread_cancel 去勾销线程的执行,从下面程序的输入后果咱们能够晓得,执行函数 task 的线程并没有执行实现,只打印出了 step1,这阐明线程被勾销执行了。

深入分析线程勾销机制

在上文的一个例子当中咱们简略的应用了一下线程勾销机制,在本大节当中将深入分析线程的勾销机制。在线程勾销机制当中,如果一个线程被失常勾销执行了,其余线程应用 pthread_join 去获取线程的退出状态的话,线程的退出状态为 PTHREAD_CANCELED。比方在下面的例子当中,主线程勾销了线程 t1 的执行,而后应用 pthread_join 函数期待线程执行实现,并且应用参数 res 去获取线程的退出状态,在下面的代码当中咱们应用 assert 语句去判断 res 的后果是否等于 PTHREAD_CANCELED,从程序执行的后果来看,assert 通过了,因而线程的退出状态验证正确。

咱们来看一下 pthread_cancel 函数的签名:

int pthread_cancel(pthread_t thread);

函数的返回值:

  • 0 示意函数 pthread_cancel 执行胜利。
  • ESRCH 示意在零碎当中没有 thread 这个线程。这个宏蕴含在头文件 <errno.h> 当中。

咱们当初应用一个例子去测试一下返回值 ESRCH:

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

int main() {

  pthread_t t;
  int s = pthread_cancel(t);
  if(s == ESRCH)
    printf("No thread with the ID thread could be found.\n");
  return 0;
}

上述程序的会执行打印字符串的语句,因为咱们并没有应用变量 t 去创立一个线程,因而线程没有创立,返回对应的谬误。

pthread_cancel 的执行

pthread_cancel 函数会发送一个勾销申请到指定的线程,线程是否响应这个线程勾销申请取决于线程的勾销状态和勾销类型。

两种线程的勾销状态:

  • PTHREAD_CANCEL_ENABLE 线程默认是开启响应勾销申请,这个状态是示意会响应其余线程发送过去的勾销申请,然而具体是如何响应,取决于线程的勾销类型,默认的线程状态就是这个值。
  • PTHREAD_CANCEL_DISABLE 当开启这个选项的时候,调用这个办法的线程就不会响应其余线程发送过去的勾销申请。

两种勾销类型:

  • PTHREAD_CANCEL_DEFERRED 如果线程的勾销类型是这个,那么线程将会在下一次调用一个勾销点的函数时候勾销执行,勾销点函数有 read, write, pread, pwrite, sleep 等函数,更多的能够网上搜寻,线程的默认勾销类型就是这个类型。
  • PTHREAD_CANCEL_ASYNCHRONOUS 这个勾销类型线程就会立刻响应发送过去的申请,实质上在 pthread 实现的代码当中是会给线程发送一个信号,而后承受勾销申请的线程在信号处理函数当中进行退出。

让线程勾销机制有效

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* func(void* arg)
{pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
  sleep(1);
  return NULL;
}

int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_cancel(t);
  void* res;
  pthread_join(t, &res);
  if(res == PTHREAD_CANCELED)
  {printf("thread was canceled\n");
  }
  return 0;
}

下面的程序不会执行这句话 printf("thread was canceled\n"); 因为在线程当中设置了线程的状态为不开启线程勾销机制,因而主线程发送的勾销申请有效。

在下面的代码当中应用的函数 pthread_setcancelstate 的函数签名如下:

int pthread_setcancelstate(int state, int *oldstate)

其中第二个参数咱们能够传入一个 int 类型的指针,而后会将旧的状态存储到这个值当中。

勾销点测试

在前文当中咱们谈到了,线程的勾销机制是默认开启的,然而当一个线程发送勾销申请之后,只有等到下一个是勾销点的函数的时候,线程才会真正退出勾销执行。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* func(void* arg)
{
  // 默认是 enable  线程的勾销机制是开启的
  while(1);
  return NULL;
}

int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_cancel(t);
  void* res;
  pthread_join(t, &res);
  if(res == PTHREAD_CANCELED)
  {printf("thread was canceled\n");
  }
  return 0;
}

如果咱们去执行下面的代码咱们会发现程序会进行循环,不会退出,因为尽管主线程给线程 t 发送了一个勾销申请,然而线程 t 始终在进行死循环操作,并没有执行任何一个函数,更不必提是一个勾销点函数了。

如果咱们批改下面的代码成上面这样,那么线程就会失常执行退出:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* func(void* arg)
{
  // 默认是 enable  线程的勾销机制是开启的
  // 线程的默认勾销类型是 PTHREAD_CANCEL_DEFERRED
  while(1)
  {sleep(1);
  }
  return NULL;
}

int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_cancel(t);
  void* res;
  pthread_join(t, &res);
  if(res == PTHREAD_CANCELED)
  {printf("thread was canceled\n");
  }
  return 0;
}

下面的代码惟一批改的中央就是在线程 t 当中的死循环处调用了 sleep 函数,而 sleep 函数是一个勾销点函数,因而当主线程给线程 t 发送一个勾销申请之后,线程 t 就会在下一次调用 sleep 函数彻底勾销执行,退出,并且线程的退出状态为 PTHREAD_CANCELED,因而主线程会执行代码 printf("thread was canceled\n");

异步勾销

当初咱们来测试一下 PTHREAD_CANCEL_ASYNCHRONOUS 会呈现什么状况:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* func(void* arg)
{
  // 默认是 enable  线程的勾销机制是开启的
  // 设置勾销机制为异步勾销
  pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
  while(1);
  return NULL;
}

int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_cancel(t);
  void* res;
  pthread_join(t, &res);
  if(res == PTHREAD_CANCELED)
  {printf("thread was canceled\n");
  }
  return 0;
}

下面的程序是能够正确输入字符串 thread was canceled 的,因为在线程执行的函数 func 当中,咱们设置了线程的勾销机制为异步机制,因为线程的默认勾销类型是 PTHREAD_CANCEL_DEFERRED,因而咱们须要批改一下线程的默认勾销类型,将其批改为 PTHREAD_CANCEL_ASYNCHRONOUS,即开始异步勾销模式。

从下面的例子当中咱们就能够领会到线程勾销的两种类型的不同成果了。

线程勾销的后续过程

当一个线程承受到其余线程发送过去的一个勾销申请之后,如果线程响应这个勾销申请,即线程退出,那么上面的几件事儿将会顺次产生:

  • clean-up handlers 将会倒序执行,咱们在文章的后续当中将会举具体的例子对这一点进行阐明。
  • 线程公有数据的析构函数将会执行,如果有多个析构函数那么执行程序不肯定。
  • 线程终止执行,即线程退出。

clean-up handlers

首先咱们须要理解一下什么是 clean-up handlers。clean-up handlers 是一个或多个函数当线程被勾销的时候这个函数将会被执行。如果没有 clean-up handlers 函数被设置,那么将不会调用。

clean-up handlers 接口

在 pthread 当中,有两个函数与 clean-up handlers 相干:

 void pthread_cleanup_push(void (*routine)(void *), void *arg);
 void pthread_cleanup_pop(int execute);

首先咱们来看一下函数 pthread_cleanup_push 的作用:这个函数是将传进来的参数——一个函数指针 routine 放入线程勾销的 clean-up handlers 的栈中,行将函数放到栈顶。

pthread_cleanup_pop 的作用是将 clean-up handlers 栈顶的函数弹出,如果 execute 是一个非 0 的值,那么将会执行栈顶的函数,如果 execute == 0,那么将不会执行弹出来的函数。

以下几点是与下面两个函数密切相关的特点:

  • 如果线程被勾销了:

    • clean-up handlers 将会倒序顺次执行,因为存储 clean-up handlers 的是一个栈构造。
    • 线程公有数据的析构函数将会执行,如果有多个析构函数那么执行程序不肯定。
    • 线程终止执行,即线程退出。
  • 如果线程调用 pthread_exit 函数进行退出:

    • clean-up handlers 同样的,将会倒序顺次执行。
    • 线程公有数据的析构函数将会执行,如果有多个析构函数那么执行程序不肯定。
    • 线程终止执行,即线程退出。
  • 须要留神的是,如果在线程被勾销或者调用 pthread_exit 之前,线程调用 pthread_cleanup_pop 函数弹出一些 handler 那么这些 handler 将不会被执行,如果线程被勾销或者调用 pthread_exit 退出,线程只会调用以后存在于栈中的 handler。
  • 你能够会问为什么 pthread 要给我提供这些机制,试想一下如果在咱们的线程当中申请了一些资源,然而忽然接管到了其余线程发送过去的勾销执行的申请,那么这些资源改如何开释呢?clean-up handlers 就给咱们提供了一种机制帮忙咱们去开释这些资源。
  • 如果线程执行的函数应用 return 语句返回,那么 clean-up handlers 将不会被调用。

上面咱们应用一个例子去理解下面的函数:


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

void handler1(void* arg)
{printf("in handler1 i1 = %d\n", *(int*)arg);
}

void handler2(void* arg)
{printf("in handler2 i2 = %d\n", *(int*)arg);
}

void* func(void* arg)
{
  int i1 = 1, i2 = 2;
  pthread_cleanup_push(handler1, &i1); // 函数在栈底
  pthread_cleanup_push(handler2, &i2); // 函数在栈顶

  printf("In func\n");
  pthread_cleanup_pop(0); // 栈顶的函数 因为传入的参数等于 0 尽管栈顶的函数会被弹出 然而栈顶的函数 handler2 不会被调用 
  pthread_cleanup_pop(1); // 因为传入的参数等于 0 因而栈顶的函数 handler1 会被调用 
  return NULL;
}

int main() 
{
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_join(t, NULL);
  return 0;
}

下面的函数的执行后果如下所示:

In func
in handler1 i1 = 1

在下面的程序当中咱们首先创立了一个线程,让线程执行函数 func,而后退出了两个函数 handler1 和 handler2 作为 clean-up handler。如果你应用了一个 pthread_cleanup_push 必须配套一个对应的 pthread_cleanup_pop 函数。

在函数 func 当中咱们首先退出了两个 handler 到 clean-up handler 栈当中,当初栈当中的数据结构造如下所示:

随后咱们会执行语句 pthread_cleanup_pop(0),因为参数 execute == 0 因而会从栈当中弹出这个函数,然而不会执行。

同样的情理,在执行语句 pthread_cleanup_pop(1) 的时候不仅会弹出函数并且还会执行这个函数。

pthread_exit 与 clean-up handler

在后面的内容当中咱们提到了,如果线程调用 pthread_exit 函数进行退出,clean-up handlers 将会倒序顺次执行。咱们应用上面的程序能够验证这一点:

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

void handler1(void* arg)
{printf("in handler1 i1 = %d\n", *(int*)arg);
}

void handler2(void* arg)
{printf("in handler2 i2 = %d\n", *(int*)arg);
}


void* func(void* arg)
{
  int i1 = 1, i2 = 2;
  pthread_cleanup_push(handler1, &i1);
  pthread_cleanup_push(handler2, &i2);

  printf("In func\n");
  pthread_exit(NULL);
  pthread_cleanup_pop(0);
  pthread_cleanup_pop(1);
  return NULL;
}

int main() 
{
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_join(t, NULL);
  return 0;
}

下面的程序的输入后果如下所示:

In func
in handler2 i2 = 2
in handler1 i1 = 1

从下面程序的输入后果来看的确 clean-up handler 被逆序调用了。

pthread_cancel 与 clean-up handler

在后面的内容当中咱们提到了,如果线程调用 pthread_cancel 函数进行退出,clean-up handlers 将会倒序顺次执行。咱们应用上面的程序能够验证这一点:


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void handler1(void* arg)
{printf("in handler1 i1 = %d\n", *(int*)arg);
}

void handler2(void* arg)
{printf("in handler2 i2 = %d\n", *(int*)arg);
}


void* func(void* arg)
{
  int i1 = 1, i2 = 2;
  pthread_cleanup_push(handler1, &i1);
  pthread_cleanup_push(handler2, &i2);

  printf("In func\n");
  sleep(1);
  pthread_cleanup_pop(0);
  pthread_cleanup_pop(1);
  return NULL;
}

int main() 
{
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_cancel(t);
  void* res;
  pthread_join(t, &res);
  if(res == PTHREAD_CANCELED)
  {printf("thread was cancelled\n");
  }
  return 0;
}

下面程序的数据后果如下所示:

In func
in handler2 i2 = 2
in handler1 i1 = 1
thread was cancelled

从下面的输入后果来看,线程的确被勾销了,而且 clean-up handler 的确也被逆序调用了。

线程公有数据(thread local)

在 pthread 当中给咱们提供了一种机制用于设置线程的公有数据,咱们能够通过这个机制很不便的去解决一下线程公有的数据和场景。与这个机制无关的次要有四个函数:

int pthread_key_create(pthread_key_t *key,void(*destructor)(void*));
int pthread_key_delete(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void * value);
void * pthread_getspecific(pthread_key_t key);
  • pthread_key_create : 这个函数的作用次要是创立一个全局的,所有线程可见的一个 key,而后所有的线程能够通过这个 key 创立一个线程公有的数据,并且咱们能够设置一个析构函数 destructor,当程序退出或者被勾销的时候,如果这个析构函数不等于 NULL,而且线程公有数据不等于 NULL,那么就会被调用,并且将线程公有公有数据作为参数传递给析构函数。
  • pthread_key_delete : 删除应用 pthread_key_create 创立的 key。
  • pthread_setspecific : 通过这个函数设置对应 key 的具体的数据,传入的参数是一个指针 value,如果咱们在后续的代码当中想要应用这个变量的话,那么就能够应用函数 pthread_getspecific 失去对应的指针。
  • pthread_getspecific : 失去应用 pthread_setspecific 函数当中设置的指针 value。

咱们当初应用一个具体的例子深刻了解线程公有数据:



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

pthread_key_t key;

void key_destructor1(void* arg) 
{printf("arg = %d thread id = %lu\n", *(int*)arg, pthread_self());
  free(arg);
}


void thread_local() 
{int* q = pthread_getspecific(key);
  printf("q == %d thread id = %lu\n", *q, pthread_self());
}


void* func1(void* arg)
{printf("In func1\n");
  int* s = malloc(sizeof(int));
  *s = 100;
  pthread_setspecific(key, s);
  thread_local();
  printf("Out func1\n");
  return NULL;
}

void* func2(void* arg)
{printf("In func2\n");
  int* s = malloc(sizeof(int));
  *s = -100;
  pthread_setspecific(key, s);
  thread_local();
  printf("Out func2\n");
  return NULL;
}

int main() {pthread_key_create(&key, key_destructor1);
  pthread_t t1, t2;
  pthread_create(&t1, NULL, func1, NULL);
  pthread_create(&t2, NULL, func2, NULL);

  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  pthread_key_delete(key);
  return 0;
}

下面的程序的执行的一种后果如下:

In func1
In func2
q == -100 thread id = 140082109499136
Out func2
arg = -100 thread id = 140082109499136
q == 100 thread id = 140082117891840
Out func1
arg = 100 thread id = 140082117891840

在下面的程序当中咱们首先定一个全局变量 key,而后应用 pthread_key_create 函数进行创立,启动了两个线程别离执行函数 func1 和 func2,在两个函数当中都创立了一个线程公有变量(应用函数 pthread_setspecific 进行创立),而后这两个线程都调用了同一个函数 thread_local,然而依据下面的输入后果咱们能够晓得,尽管是两个线程调用的函数都雷同,然而不同的线程调用输入的后果是不同的(通过观察线程的 id 就能够晓得了),而且后果是咱们设置的线程局部变量,当初咱们应该可能领会这线程公有数据的成果了。

在后面的内容当中咱们提到了,当一个线程被勾销的时候,第二步操作就是调用线程公有数据的析构函数。



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

pthread_key_t key;

void key_destructor1(void* arg) 
{printf("arg = %d thread id = %lu\n", *(int*)arg, pthread_self());
  free(arg);
}


void thread_local() 
{int* q = pthread_getspecific(key);
  printf("q == %d thread id = %lu\n", *q, pthread_self());
}

void* func1(void* arg)
{printf("In func1\n");
  int* s = malloc(sizeof(int));
  *s = 100;
  pthread_setspecific(key, s);
  thread_local();
  printf("Out func1\n");
  sleep(2);
  printf("func1 finished\n");
  return NULL;
}

void* func2(void* arg)
{printf("In func2\n");
  int* s = malloc(sizeof(int));
  *s = -100;
  pthread_setspecific(key, s);
  thread_local();
  printf("Out func2\n");
  sleep(2);
  printf("func2 finished\n");
  return NULL;
}


int main() {pthread_key_create(&key, key_destructor1);
  pthread_t t1, t2;
  pthread_create(&t1, NULL, func1, NULL);
  pthread_create(&t2, NULL, func2, NULL);
  sleep(1);
  pthread_cancel(t1);
  pthread_cancel(t2);
  void* res1, *res2;
  pthread_join(t1, &res1);
  pthread_join(t2, &res2);
  if(res1 == PTHREAD_CANCELED) 
  {printf("thread1 was canceled\n");
  }

  if(res2 == PTHREAD_CANCELED) 
  {printf("thread2 was canceled\n");
  }
  pthread_key_delete(key);
  return 0;
}

下面的程序的输入后果如下所示:

In func1
In func2
q == 100 thread id = 139947700033280
Out func1
q == -100 thread id = 139947691640576
Out func2
arg = 100 thread id = 139947700033280
arg = -100 thread id = 139947691640576
thread1 was canceled
thread2 was canceled

这一个程序和第一个线程公有的示例程序不一样的是,下面的程序在主线程当中勾销了两个线程 t1 和 t2 的执行,从下面的程序的输入后果咱们也能够产出两个线程的代码并没有齐全执行胜利,而且线程的退出状态的确是 PTHREAD_CANCELED。咱们能够看到的是两个线程的析构函数也被调用了,这就能够验证了咱们在后面提到的,当一个线程退出或者被勾销执行的时候,线程的线程本地数据的析构函数会被调用,而且传入个析构函数的参数是线程本地数据的指针,咱们能够在析构函数当中开释对应的数据的空间,回收内存。

总结

在本篇文章当中次要给大家深刻介绍了线程勾销机制的各种细节,并且应用一些测试程序去一一验证了对应的具体景象,整个线程的勾销机制总结起来并不简单,具体如下:

  • 线程能够设置是否响应其余线程发送过去的勾销申请,默认是开启响应。
  • 线程能够设置响应勾销执行的类型,一种是异步执行,这种状态是通过信号实现的,线程承受到信号之后会立马退出执行,一种是 PTHREAD_CANCEL_DEFERRED 只有在下一次遇到是勾销点的函数的时候才会退出线程的执行。
  • 当线程被勾销执行了,clean-up handlers 将会倒序顺次执行。
  • 当线程被勾销执行了,线程公有数据的析构函数也会被执行。

咱们能够应用上面的流程图来示意整个流程:(以下图片来源于网络)

在本篇文章当中次要介绍了一些根底了线程本人的个性,并且应用一些例子去验证了这些个性,帮忙咱们从根本上去了解线程,其实线程波及的货色切实太多了,在本篇文章外面只是列举其中的局部例子进行应用阐明,在后续的文章当中咱们会持续深刻的去谈这些机制,比方线程的调度,线程的勾销,线程之间的同步等等。

更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHu…

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0