POSIX Threads 是一套符合 POSIX 标准的 API,方便开发者设计出 User-level 的多执行绪程序。
先了解执行绪的记忆体分配有助於多执行绪程序的开发。
在同一个 Program 中,多个 Thread 会共用同一个位址空间,每个 Thread 都会分配到一块空间作为自己的 Stack,而指向这些空间起始点的指标就被称为 Stack pointer
。
呼叫函式和一般的跳跃不同,在呼叫结束後必须回到原本呼叫的地方,原本执行中的位址被叫做「回传位址」(return address)。如果说呼叫只会发生一次的话,随便找一个暂存器存回传位址就好了;但是函式呼叫可以一层一层呼叫下去,所以必须把回传位址存在记忆体里。实务上,回传位址被存在记忆体中的堆叠(stack)里。
堆叠,被实作成只能使用堆叠空间最上方位址所存的一个变数。而这个纪录堆叠最上方的纪录空间被称为「堆叠指标」(stack pointer)。x86-64 中,为了方便写呼叫函式的程序,提供了堆叠指标专用的暂存器,和使用这个暂存器的指令。往堆叠上堆资料的操作是「push」,而取出堆叠资料的操作是「pop」。
-- C编译器入门~想懂低阶系统从自干编译器开始~
当执行绪呼叫其他函式时,stack pointer 便会向下移动,这让我们可以有更多空间去存放参数以及局部变数。
当函式执行完毕并返回时,stack pointer 便会移动到原先的位址。
旧的 stack pointer 纪录的地址也会被存放在 Stack 中,这也是函式可以快速返回的原因。
对於函式的流程控制,这部The Call Stack影片有详细的解说。
Pthreads API 中大致共有 100 个函式呼叫,全都以 pthread_ 开头,并可以分为四类:
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);
}
若工作流程用图表呈现,大概是这样:
上图取自该网站。
感谢高魁良前辈的补充,使用
-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);
想知道更多细节可以参考该连结。
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 有 4 个方法:
pthread_cancel
呼叫指定的执行绪。pthread_exit()
。pthread_join
会有什麽後果呢?空闲的执行绪会继续占用资源,直到 Process 结束为止。
换言之,如果是在长期不会结束的应用(像是服务器),那错误的设计便会造成多余的资源浪费。
pthread_join()
还是 pthread_exit()
阿?答案是都可以,只是差在 pthread_exit()
会在执行绪完成任务後退出,让你没有机会执行其他程序。
可以,但要注意函式的生命周期,考虑以下程序码:
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()
执行完成才结束。
终於到最後一天了,可喜可贺可喜可贺!其实我也知道在这30天的期间内还有很多没能讲到的东西,既然都到最...
三天前,我们已成功把静态档案加入 Angular 的专案了,如果感到有点陌生,可以再到以下连结,重新...
这几天研究下来,发现有三个词汇很让人搞不懂,也就是人工智慧(Artifical Intellig...
经过昨天的简介,今天要来讲NPM相对重要的部份:安装套件与管理,分成一般相依性安装、开发相依性安装、...
Day 26 - HBuilderX 与 Native.js API 读取图片 在 Day 25 -...