POSIX Thread 介绍

POSIX Threads 是一套符合 POSIX 标准的 API,方便开发者设计出 User-level 的多执行绪程序。

开始之前

先了解执行绪的记忆体分配有助於多执行绪程序的开发。

thread 空间分配

在同一个 Program 中,多个 Thread 会共用同一个位址空间,每个 Thread 都会分配到一块空间作为自己的 Stack,而指向这些空间起始点的指标就被称为 Stack pointer

呼叫函式和一般的跳跃不同,在呼叫结束後必须回到原本呼叫的地方,原本执行中的位址被叫做「回传位址」(return address)。如果说呼叫只会发生一次的话,随便找一个暂存器存回传位址就好了;但是函式呼叫可以一层一层呼叫下去,所以必须把回传位址存在记忆体里。实务上,回传位址被存在记忆体中的堆叠(stack)里。
堆叠,被实作成只能使用堆叠空间最上方位址所存的一个变数。而这个纪录堆叠最上方的纪录空间被称为「堆叠指标」(stack pointer)。x86-64 中,为了方便写呼叫函式的程序,提供了堆叠指标专用的暂存器,和使用这个暂存器的指令。往堆叠上堆资料的操作是「push」,而取出堆叠资料的操作是「pop」。
-- C编译器入门~想懂低阶系统从自干编译器开始~

当执行绪呼叫其他函式时,stack pointer 便会向下移动,这让我们可以有更多空间去存放参数以及局部变数。
当函式执行完毕并返回时,stack pointer 便会移动到原先的位址。

旧的 stack pointer 纪录的地址也会被存放在 Stack 中,这也是函式可以快速返回的原因。

ref

对於函式的流程控制,这部The Call Stack影片有详细的解说。

进入正题

Pthreads API 中大致共有 100 个函式呼叫,全都以 pthread_ 开头,并可以分为四类:

  • 执行绪管理,例如建立执行绪,等待 (join) 执行绪,查询执行绪状态等。
  • Mutex lock: 建立、摧毁、锁定、解锁、设定属性等操作
  • Condition Variable: 建立、摧毁、等待、通知、设定与查询属性等操作
  • 使用了互斥锁的执行绪间的同步管理

POSIX 的 Semaphore API 可以与 POSIX threads 一同运作,但 Semaphore API 并非 threads standard 的一部分,其定义在 POSIX.1b, Real-time extensions (IEEE Std 1003.1b-1993) standard 内。

而本篇文章要介绍的是第一项: 执行绪管理的部分。

建立新的执行绪

我们可以利用 POSIX Thread 建立具有一个执行绪以上的 Process,第一个 Thread 会负责运行 main() 中的程序码。若要建立一个以上的执行绪,我们可以使用 pthread_create :

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

其中 void *(*start_routine) (void *) 用语言表达的话,可以解释成:

一个指标它带有一个指向 void 型态资料的指标,并且,它会返回指向 void 型态资料的指标。

如果仍无法理解上述的程序码,建议读者可以去复习重拾 C 语言::函式指标

看完 posix_create 的定义以後,可以看看以下范例:

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

void *busy(void *ptr) {
// ptr will point to "Hi"
    puts("Hello World");
    return NULL;
}
int main() {
    pthread_t id;
    pthread_create(&id, NULL, busy, "Hi");
    while (1) {} // Loop forever
}

等待执行绪完成工作

如果要等待我们建立的执行绪完成工作,需要使用 pthread_join :

int pthread_join(pthread_t thread, void **retval);

查看定义後,进一步改写原本的程序码:

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

void *busy(void *ptr) {
// ptr will point to "Hi"
    puts("Hello World");
    return NULL;
}
int main() {
    void *result;
    pthread_t id;
    pthread_create(&id, NULL, busy, "Hi");
    pthread_join(id, &result);
}

除了上面的范例,我们可以用 pthread_exit() 再做一次改写:

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

void *busy(void *ptr) {
// ptr will point to "Hi"
    puts("Hello World");
    pthread_exit(NULL);
}
int main() {
    pthread_t id;
    pthread_create(&id, NULL, busy, "Hi");
    pthread_join(id, NULL);
}

若工作流程用图表呈现,大概是这样:

pthread join

上图取自该网站

Compile your code!

感谢高魁良前辈的补充,使用 -lpthread 仅连结 pthread library,而不会对 thread 的编译设定进行最佳化,导致 pre-defined macros 会无法使用。

本系列都是采用 gcc 作为 C 语言的编译器,若使用到 Pthread 必须在编译时添加参数: -pthread

gcc source.c -pthread -o source

编译完成後,便可以启动可执行档。

./source

取消指定的执行绪

PThread 提供了 API,让我们可以取消已建立的 POSIX Thread。

int pthread_cancel(pthread_t thread);

想知道更多细节可以参考该连结

exit 和 pthread_exit 的差异

pthread_exit() 如果放在 main() 函式,是用来确保所有用 POSIX Thread API 建立的执行绪已经完成。

int main() {
  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, myfunc, "Jabberwocky");
  pthread_create(&tid2, NULL, myfunc, "Vorpel");
  pthread_exit(NULL); 

  // No code is run after pthread_exit
  // However process will continue to exist until both threads have finished
}

如果不使用 pthread_exit() 或是 pthread_join() 而直接使用 exit(),Process 会在一派发完执行绪後结束 (也就是执行绪根本还没开始处理任务):

int main() {
  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, myfunc, "Jabberwocky");
  pthread_create(&tid2, NULL, myfunc, "Vorpel");
  exit(42); //or return 42;

  // No code is run after exit
}

如果还有疑问,也可以参考 stackoverflow 上的问答串。

总结

最後,笔者统整一下本篇介绍的 POSIX Thread API 的重要知识点:

如何终止 Thread

终止 Thread 有 4 个方法:

  • 等到 Thread 指派的任务 Return。
  • pthread_cancel 呼叫指定的执行绪。
  • 使用 pthread_exit()
  • 终止 Process。

如果不使用 pthread_join 会有什麽後果呢?

空闲的执行绪会继续占用资源,直到 Process 结束为止。
换言之,如果是在长期不会结束的应用(像是服务器),那错误的设计便会造成多余的资源浪费。

我该用 pthread_join() 还是 pthread_exit() 阿?

答案是都可以,只是差在 pthread_exit() 会在执行绪完成任务後退出,让你没有机会执行其他程序。

我可以在执行绪中传送 Stack pointer 到另一个执行绪吗?

可以,但要注意函式的生命周期,考虑以下程序码:

pthread_t start_threads() {
  int start = 42;
  pthread_t tid;
  pthread_create(&tid, 0, myfunc, &start); // ERROR!
  return tid;
}

等到 myfunc 开始执行时,start_threads() 的生命早就走到尽头了!这样一来,我们根本无法确定原先存放 start 变数内容的记忆体现在存放什麽东西。
为了避免这个情况发生,我们可以用 pthread_join 改写范例程序:

void start_threads() {
  int start = 42;
  void *result;
  pthread_t tid;
  pthread_create(&tid, 0, myfunc, &start); // OK - start will be valid!
  pthread_join(tid, &result);
}

这样一来,start_thread() 的生命周期就会被延後到 myfunc() 执行完成才结束。

Reference


<<:  Vue.js指令(v-on)绑定(DAY29)

>>:  [Day28]Solidity实作2

Day 30 关於这次的铁人赛,那些没能提到的事情

终於到最後一天了,可喜可贺可喜可贺!其实我也知道在这30天的期间内还有很多没能讲到的东西,既然都到最...

Day 16:把做好的 HTML 加入 Angular 吧!

三天前,我们已成功把静态档案加入 Angular 的专案了,如果感到有点陌生,可以再到以下连结,重新...

Day8:原来机器学习这个词跟我想的不太一样

  这几天研究下来,发现有三个词汇很让人搞不懂,也就是人工智慧(Artifical Intellig...

Day15 NodeJS-NPM II

经过昨天的简介,今天要来讲NPM相对重要的部份:安装套件与管理,分成一般相依性安装、开发相依性安装、...

Day 26 - HBuilderX 与 Native.js API 读取图片

Day 26 - HBuilderX 与 Native.js API 读取图片 在 Day 25 -...