POSIX 线程
本节介绍 POSIX 线程的使用方法。
线程属性
所有 POSIX 线程属性都通过一个属性对象表示,该属性对象定义为结构体 pthread_attr_t。POSIX 定义了一系列函数设置和读取线程属性值,所以应用程序不需要知道 pthread_attr_t 定义的细节。通常管理线程属性都遵循相同的模式:
- 每个对象与它自己类型的属性对象进行关联(线程与线程属性、互斥量与互斥属性等),一个属性对象包含了多个属性(pthread_attr_t)。因为属性对象的封装性使得应用程序更加容易移植。
- 属性对象有一个初始化函数,将属性设置为默认值。
- 与初始化对应的有一个销毁函数,进行反初始化。
- 每个属性对象都有一个从属性对象获得属性值的函数。由于函数成功时返回 0,失败时返回错误号,所以可以通过把属性值存储在函数的某一个参数指定的内存单元中返回给调用者。
- 每个属性对象都有一个设置属性值的函数,这样属性值通过参数进行传递。
调用 pthread_attr_init 函数可以初始化一个线程属性对象,调用 pthread_attr_destroy 函数可以销毁一个线程属性对象。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *pattr);
int pthread_attr_destroy(pthread_attr_t *pattr);
函数 pthread_attr_init 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是需要初始化的线程属性对象指针。
函数 pthread_attr_destroy 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是需要销毁的线程属性对象指针。
属性对象的初始化即为各个属性赋默认值,POSIX 未限制默认值。属性对象被初始化后,程序通常还需要为单个属性设置合适的值。
销毁属性是属性初始化的反过程,这通常会将属性值设置为无效值,如果初始化过程动态分配了一些系统资源,销毁过程将释放这些资源。SylixOS 中只是将线程选项值设为了无效值。属性对象被销毁后不能再用于线程创建,除非重新初始化。
为了获得一个指定线程的属性对象可以调用 pthread_getattr_np 函数,此函数不是 POSIX 标准的一部分,而是 Linux 和 SylixOS 的一种扩展。SylixOS 还支持 FreeBSD 扩展函数 pthread_attr_get_np。
注:np (non-portable) 意思是不可移植的,属于非 POSIX 标准函数。
#include <pthread.h>
int pthread_attr_get_np(pthread_t thread, pthread_attr_t *pattr);
int pthread_getattr_np(pthread_t thread, pthread_attr_t *pattr);
函数 pthread_attr_get_np 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 thread 是线程句柄。
- 输出参数 pattr 返回线程属性对象。
pthread_attr_get_np 函数和 pthread_getattr_np 函数功能相同,参数类型也相同,在底层实现上,pthread_getattr_np 函数调用了 pthread_attr_get_np 函数。
调用以下函数可以获得或设置 POSIX 线程的名字。
#include <pthread.h>
int pthread_attr_setname(pthread_attr_t *pattr, const char *pcName);
int pthread_attr_getname(const pthread_attr_t *pattr, char **ppcName);
函数 pthread_attr_setname 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性对象指针。
- 参数 pcName 是要设置的线程名字。
函数 pthread_attr_getname 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性对象指针。
- 输出参数 ppcName 返回线程的名字。
调用 pthread_attr_setname 函数可以设置线程的名字,调用 pthread_attr_getname 函数可以获得线程的名字。SylixOS 对线程属性对象初始化默认线程名字是“pthread”。
POSIX 规定的线程属性如下:
栈大小
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *pattr, size_t stSize);
int pthread_attr_getstacksize(const pthread_attr_t *pattr, size_t *pstSize);
函数 pthread_attr_setstacksize 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性对象指针。
- 参数 stSize 是栈大小。
函数 pthread_attr_ getstacksize 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性对象指针。
- 输出参数 pstSize 返回栈的大小。
调用 pthread_attr_setstacksize 函数可以设置线程栈的大小,调用 pthread_attr_getstacksize 函数将获得指定线程的栈大小。SylixOS 对线程属性对象初始化默认栈值为 0,这意味着,栈大小将继承创建者的栈大小。
注意:
设置的栈大小不应该小于 128 字,否则将返回 EINVAL 错误值。
栈地址
#include <pthread.h>
int pthread_attr_setstackaddr(pthread_attr_t *pattr, void *pvStackAddr);
int pthread_attr_getstackaddr(const pthread_attr_t *pattr,
void **ppvStackAddr);
函数 pthread_attr_setstackaddr 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性对象指针。
- 参数 pvStackAddr 是栈地址。
函数 pthread_attr_getstackaddr 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性对象指针。
- 输出参数 ppvStackAddr 返回栈地址。
调用 pthread_attr_setstackaddr 函数可以设置新的栈起始地址,调用 pthread_attr_getstackaddr 函数将获得栈的起始地址。SylixOS 对线程属性对象初始化默认栈地址为 LW_NULL,这意味着系统会自动分配栈空间。
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *pattr,
void *pvStackAddr,
size_t stSize);
int pthread_attr_getstack(const pthread_attr_t *pattr,
void **ppvStackAddr,
size_t *pstSize);
函数 pthread_attr_setstack 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 pvStackAddr 是栈地址。
- 参数 stSize 是栈大小。
函数 pthread_attr_getstack 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 输出参数 ppvStackAddr 返回栈地址。
- 输出参数 pstSize 返回栈大小。
调用 pthread_attr_setstack 函数可以同时设置栈的起始地址和大小,调用 pthread_attr_getstack 函数可以同时获得栈的起始地址和大小。
线程栈警戒区
#include <pthread.h>
int pthread_attr_setguardsize(pthread_attr_t *pattr, size_t stGuard);
int pthread_attr_getguardsize(pthread_attr_t *pattr, size_t *pstGuard);
函数 pthread_attr_setguradsize 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 stGuard 是栈警戒区大小。
函数 pthread_attr_getguradsize 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 输出参数 pstGuard 返回栈警戒区大小。
调用 pthread_attr_setguradsize 函数可以设置栈警戒区大小,调用 pthread_attr_getguradsize 函数将获得栈警戒区大小。SylixOS 对线程属性对象初始化默认栈警戒区大小为 LW_CFG_THREAD_DEFAULT_GUARD_SIZE。
离合状态
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *pattr, int iDetachState);
int pthread_attr_getdetachstate(const pthread_attr_t *pattr,
int *piDetachState);
int pthread_detach(pthread_t thread);
函数 pthread_attr_setdetachstate 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 iDetachState 是离合状态值。
函数 pthread_attr_getdetachstate 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 输出参数 piDetachState 返回离合状态值。
函数 pthread_detach 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 thread 需要分离的线程句柄。
线程离合状态分为合并态和分离态。为合并态时,线程创建新线程将使其自身被阻塞直到新线程退出。
调用 pthread_attr_setdetachstate 函数可以设置线程属性对象的离合状态,调用 pthread_attr_getdetachstate 函数将获得线程属性对象的离合状态。SylixOS 对线程属性对象初始化默认离合状态是合并态(PTHREAD_CREATE_JOINABLE)。
如果线程以合并态创建,则线程可以调用 pthread_detach 函数使其进入分离态,反之不行。
继承调度
#include <pthread.h>
int pthread_attr_setinheritsched(pthread_attr_t *pattr, int iInherit);
int pthread_attr_getinheritsched(const pthread_attr_t *pattr,
int *piInherit);
函数 pthread_attr_setinheritsched 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 iInherit 是继承属性。
函数 pthread_attr_getinheritsched 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 piInherit 返回继承属性。
继承调度决定了创建线程时是从父线程继承调度参数(PTHREAD_INHERIT_SCHED)还是显示地指定(PTHREAD_EXPLICIT_SCHED)。
调用 pthread_attr_setinheritsched 函数可以设置线程属性对象的继承性,调用 pthread_attr_getinheritsched 函数将获得线程属性对象的调度策略(继承性)。SylixOS 对线程属性对象初始化默认调度策略是显式地指定。
调度策略
#include <pthread.h>
int pthread_attr_setschedpolicy(pthread_attr_t *pattr, int iPolicy);
int pthread_attr_getschedpolicy(const pthread_attr_t *pattr, int *piPolicy);
函数 pthread_attr_setschedpolicy 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 iPolicy 是调度策略。
函数 pthread_attr_getschedpolicy 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 输出参数 piPolicy 返回调度策略。
该项指定了创建新线程的调度策略,调度策略有 SCHED_FIFO、SCHED_RR、SCHED_OTHER(SCHED_OTHER 是 POSIX 规定的自定义调度策略,在 SylixOS 中 SCHED_OTHER 等同于 SCHED_RR),这两种调度策略的详细内容见“SylixOS 线程调度”。
调用 pthread_attr_setschedpolicy 函数可以设置线程属性对象的调度策略,调用 pthread_attr_getschedpolicy 函数将获得线程属性对象的调度策略。SylixOS 对线程属性对象初始化默认的调度策略是 SCHED_RR。
调度参数
#include <pthread.h>
int pthread_attr_setschedparam(pthread_attr_t *pattr,
const struct sched_param *pschedparam);
int pthread_attr_getschedparam(const pthread_attr_t *pattr,
struct sched_param *pschedparam);
函数 pthread_attr_setschedparam 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 参数 pschedparam 是调度参数。
函数 pthread_attr_getschedparam 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pattr 是线程属性指针。
- 输出参数 pschedparam 返回调度参数。
线程创建成功后,调度参数允许动态修改,调度参数详细内容见“POSIX 线程调度”。
调用 pthread_attr_setschedparam 函数可以设置线程属性对象的调度参数,调用 pthread_attr_getschedparam 函数将获得线程属性对象的调度参数。SylixOS 对线程属性对象初始化默认的调度参数只设置线程优先级为 LW_PRIO_NORMAL。
线程创建
#include <pthread.h>
int pthread_create(pthread_t *pthread,
const pthread_attr_t *pattr,
void *(*start_routine)(void *),
void *arg);
函数 pthread_create 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 输出参数 pthread 返回线程句柄。
- 参数 pattr 是线程属性对象指针。
- 参数 start_routine 是线程函数。
- 参数 arg 是线程入口函数参数。
调用 pthread_create 函数可以创建一个 POSIX 线程,线程属性对象 pattr 可以通过 pthread_attr_*系列函数来建立或者动态设置,如果 pattr 为 NULL,系统将会设置一个默认的线程属性对象,当 start_routine 指定的线程函数返回时,新线程结束。需要注意的是,线程函数 start_routine 只有一个指针参数 arg,这意味着,如果需要传递多个参数,则需要封装成一个结构体。
POSIX 线程句柄定义为 pthread_t 型变量。线程创建者在线程创建时得到所创建的线程句柄,线程可以通过调用 pthread_self 函数来获得自身的线程句柄,POSIX 还定义了 pthread_equal 函数用于比较两个线程是否相等。
#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t thread1, pthread_t thread2);
函数 pthread_self 原型分析:
- 此函数成功返回当前线程的句柄,失败返回 0。
函数 pthread_equal 原型分析:
- 此函数返回比较结果。
- 参数 thread1 是线程句柄。
- 参数 thread2 是线程句柄。
下面程序展示了 POSIX 线程创建的方法,下面程序调用了函数 pthread_join 来合并线程,这将使主线程等待子线程直到退出(下一节将介绍 pthread_join 函数的具体用法)。
#include <stdio.h>
#include <pthread.h>
#include <time.h>
void *routine (void *arg)
{
fprintf(stdout, "pthread running...\n");
return (NULL);
}
int main (int argc, char *argv[])
{
pthread_t tid;
pthread_attr_t attr;
int ret;
ret = pthread_attr_init(&attr);
if (ret != 0) {
fprintf(stderr, "pthread attr init failed.\n");
return (-1);
}
ret = pthread_create(&tid, &attr, routine, NULL);
if (ret != 0) {
fprintf(stderr, "pthread create failed.\n");
return (-1);
}
pthread_join(tid, NULL);
pthread_attr_destroy(&attr);
return (0);
}
上面程序调用函数 pthread_attr_init 初始化线程属性对象,相应地调用函数 pthread_attr_destroy 进行属性对象的反初始化。实际上,在编程过程中我们习惯将 pthread_create 函数的第二个参数设置为 NULL 以告诉操作系统将线程的属性对象设置为默认值。
在 SylixOS Shell 下运行程序,结果如下:
# ./POSIX_Thread_Creation
pthread running...
线程退出
#include <pthread.h>
void pthread_exit(void *status);
函数 pthread_exit 原型分析:
- 此函数无返回值。
- 参数 status 是线程退出状态码。
线程结束即线程显式或者隐式地调用 pthread_exit 函数,status 通常表示一个整数,也可以指向一个更复杂的数据结构体。线程结束码将被另一个与该线程进行合并的线程得到。
单个线程可以通过 3 种方式退出:
- 线程可以简单地从线程入口函数中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程显式地调用 pthread_exit 函数。
线程可以同步等待另一个线程退出,并获得其退出码。需要注意的是,同步等待的目标线程必须处于合并状态,如下图所示。
#include <pthread.h>
int pthread_join(pthread_t thread, void **ppstatus);
函数 pthread_join 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 thread 是需要 join 的线程句柄。
- 输出参数 ppstatus 是线程退出状态。
调用 pthread_join 函数可以将指定的线程进行合并,调用线程将一直阻塞等待,直到此线程返回,当此线程返回后,pthread_join 函数将通过 ppstatus 参数获得线程的退出码。
如果对线程的返回值并不感兴趣,那么可以把 ppstatus 设置为 NULL,这种情况,调用 pthread_join 函数的线程可以等待指定的线程终止,但并不获取线程的终止状态。
下面的程序展示了通过 pthread_join 来获得线程的退出码。
#include <stdio.h>
#include <pthread.h>
void *routine (void *arg)
{
fprintf(stdout, "thread 1 return.\n");
return ((void *)1);
}
void *routine1 (void *arg)
{
fprintf(stdout, "thread 2 exit.\n");
pthread_exit((void *)2);
}
int main (int argc, char *argv[])
{
pthread_t tid, tid1;
int ret;
void *retval;
ret = pthread_create(&tid, NULL, routine, NULL);
if (ret != 0) {
fprintf(stderr, "pthread create failed.\n");
return (-1);
}
ret = pthread_join(tid, &retval);
if (ret != 0) {
fprintf(stderr, "pthread join thread 1 failed.\n");
return (-1);
}
fprintf(stdout, "thread 1 return code: %ld\n", (long)retval);
ret = pthread_create(&tid1, NULL, routine1, NULL);
if (ret != 0) {
fprintf(stderr, "pthread create failed.\n");
return (-1);
}
ret = pthread_join(tid1, &retval);
if (ret != 0) {
fprintf(stderr, "pthread join thread 2 failed.\n");
perror("pthread_join");
return (-1);
}
fprintf(stdout, "thread 2 exit code: %ld\n", (long)retval);
return (0);
}
上面程序使用了两种线程退出的方法:线程返回和调用 pthread_exit 函数退出。
在 SylixOS Shell 下运行程序,结果如下:
# ./Get_Exit_Code
thread 1 return.
thread 1 return code: 1
thread 2 exit.
thread 2 exit code: 2
通过执行结果可以看出,当一个线程通过调用 pthread_exit 函数退出或者简单地从线程函数返回时,进程中的其他线程可以通过调用 pthread_join 函数获得该线程的退出码。
前面曾介绍过,pthread_create 函数和 pthread_exit 函数的无类型指针参数可以传递一个更复杂的结构体,但是需要注意,这个结构体所使用的内存在调用者完成调用以后必须仍然有效。例如,在调用线程的栈上分配了该结构,那么其他的线程在使用这个结构体时内存内容可能已经改变了。又如,线程在自己的栈上分配了一个结构体,再把指向这个结构体的指针传给 pthread_exit 函数,那么调用 pthread_join 函数的线程试图使用该结构体时,这个栈有可能已经被撤销,这块内存也已另作他用。
线程取消
取消一个线程要确保该线程能够释放其所持有的任何锁、分配的内存,使整个系统保持一致性。在很多复杂情况下要保证这种正确性是有一定困难的。
一种简单的线程取消:取消线程调用一个取消线程的函数,被取消线程死亡。在这种情况下,被取消线程所持有的资源得不到释放。取消线程负责保证被取消者处于可安全取消状态,在一个要求可靠性高的系统中,这种保证非常困难或者无法实现。这种取消称为不受限制的异步取消。
与异步取消相关的是异步取消安全,即一段代码执行期间可以在任意点上被取消而不会引起不一致。满足该条件的函数称为异步取消安全函数。显然,异步取消安全函数不涉及使用互斥锁等。POSIX 标准要求这些函数是异步取消安全函数:pthread_cancel、pthread_setcancelstate、pthread_setcanceltype。
POSIX 标准定义了一种更安全的线程取消机制。一个线程可以以可靠的受控制的方式向进程其他线程发出取消请求,目标线程可以挂起这一请求使实际的取消动作在此后安全的时候进行,称为延迟取消。目标线程还可以定义其被取消后自动被系统调用的线程清除函数。和延迟取消相关的一个概念是取消点。
POSIX 通过为线程定义可取消状态,取消点函数以及一系列取消控制函数实现延迟取消。
线程可以调用 pthread_cancel 函数请求取消一个线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
函数 pthread_cancel 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 thread 是取消的线程句柄。
pthread_cancel 函数和目标线程的取消动作是异步的。根据目标线程的设置不同,取消请求可能被忽略、立即执行或者延迟处理。为了清楚这些动作,这里我们需要先了解线程的取消状态、取消类型和取消点的概念。
取消状态
线程取消状态决定了指定线程是否可以被取消,取消状态分为允许取消和禁止取消,如下表所示。一个线程设置为禁止取消意味着此线程只能从自己返回或者调用 pthread_exit 函数来退出。
#include <pthread.h>
int pthread_setcancelstate(int newstate, int *poldstate);
函数 pthread_setcancelstate 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 newstate 是新状态,如下表所示。
- 输出参数 poldstate 返回之前的状态。
取消状态 | 说明 |
---|---|
LW_THREAD_CANCEL_ENABLE | 允许取消 |
LW_THREAD_CANCEL_DISABLE | 禁止取消 |
调用 pthread_setcancelstate 函数指定 newstate 参数值为 LW_THREAD_CANCEL_ENABLE 来允许取消,值为 LW_THREAD_CANCEL_DISABLE 来禁止取消,如果参数 poldstate 非 NULL,则返回之前的取消状态。
取消类型
取消类型是线程取消的方式,分为异步取消和延迟取消,如下表所示。
#include <pthread.h>
int pthread_setcanceltype(int newtype, int *poldtype);
函数 pthread_setcancelstate 原型分析:
- 此函数成功返回 0,失败返回非 0 值。
- 参数 newtype 是新类型,如下表所示。
- 输出参数 poldtype 返回之前的类型。
取消类型 | 说明 |
---|---|
LW_THREAD_CANCEL_ASYNCHRONOUS | 异步取消 |
LW_THREAD_CANCEL_DEFERRED | 延迟取消 |
调用 pthread_setcanceltype 函数可以设置取消类型,当 poldtype 为非 NULL 时,则返回之前的取消类型。
当设置取消状态为禁止时,对该线程的取消请求将被忽略。当设置取消状态为允许时,如果收到取消请求,则系统的动作由所选择的取消类型决定。
- 异步取消,取消请求立即被执行。
- 延迟取消,取消请求被挂起,直到运行到下一个取消点才被执行。
取消点
在使用延迟取消机制时,一个线程在可以被取消的地方定义取消点,当收到取消请求时,被取消的线程执行到取消点时退出,或者在一个取消点调用被阻塞时退出。通过延迟取消,程序在进入临界区时不需要进行禁止/允许取消操作。
由于在延迟取消时必须在取消点才能被取消,这一限制可能使取消请求被挂起任意长时间。因此,如果某个调用可能使线程被阻塞或者进入某个较长时间的过程,POSIX 要求这些调用属于一个取消点,或者称这些调用为取消点调用,以防止取消请求陷入长时间等待。如下表所示是 SylixOS 中拥有取消点的函数。
函数名 | 函数名 | 函数名 |
---|---|---|
Lw_Thread_Join | send | open |
Lw_Thread_Start | sendmsg | close |
sleep | aio_suspend | read |
Lw_Thread_Cond_Wait | mq_send | pread |
system | mq_timedsend | write |
wait | mq_reltimedsend_np | pwrite |
waitid | mq_receive | readv |
waitpid | mq_timedreceive | writev |
wait3 | mq_reltimedreceive_np | lockf |
wait4 | pthread_barrier_wait | fsync |
reclaimchild | sem_wait | fdatasync |
accept4 | sem_timedwait | pselect |
connect | sem_reltimedwait_np | select |
recv | tcdrain | pause |
recvfrom | fcntl | sigsuspend |
recvmsg | creat | sigwait |
sigwaitinfo | sigtimedwait | msgrcv |
msgsnd | pthread_join | pthread_testcancel |
下面程序是一个线程延时取消的实例,程序创建线程 thread0,线程中设置取消类型为延时取消,因为 sleep 函数是一个拥有取消点的函数,所以程序每次运行到 sleep 函数都会检查线程是否有取消请求,当检查出有取消请求时,将在下一个取消点处,取消线程。
#include <pthread.h>
#include <stdio.h>
void *thread0 (void *arg)
{
int oldstate;
int oldtype;
pthread_setcancelstate(LW_THREAD_CANCEL_ENABLE, &oldstate);
pthread_setcanceltype(LW_THREAD_CANCEL_DEFERRED, &oldtype);
while (1) {
fprintf(stdout, "thread0 running...\n");
sleep(1);
}
return (NULL);
}
int main (int argc, char *argv[])
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thread0, NULL);
if (ret != 0) {
return (-1);
}
sleep(3);
pthread_cancel(tid);
pthread_join(tid, NULL);
fprintf(stdout, "thread0 cancel.\n");
return (0);
}
在 SylixOS Shell 下运行程序,从结果可以看出线程的取消情况。
# ./Thread_Delay_Cancellation
thread0 running...
thread0 running...
thread0 running...
thread0 cancel.
表中所示的函数都是直接或者间接调用 pthread_testcancel 函数实现的,因此目标线程也可以调用 pthread_testcancel 函数来检查取消请求。pthread_testcancel 和其他取消点调用不同之处在于该函数除了创建一个取消点以外什么也不做,即专门为响应取消请求服务。
#include <pthread.h>
void pthread_testcancel(void);
如果 pthread_testcancel 函数没有检查到取消请求,则直接返回到调用者,当检查到有取消请求时将线程删除不再返回到调用者。
线程可以安排它退出时需要调用的函数,这样的函数称为线程清理处理程序,一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,他们的执行顺序与它们注册时相反,如下图所示。
#include <pthread.h>
void pthread_cleanup_pop(int iNeedRun);
void pthread_cleanup_push(void (*pfunc)(void *), void *arg);
函数 pthread_cleanup_pop 原型分析:
- 参数 iNeedRun 表示是否执行。
函数 pthread_cleanup_push 原型分析:
- 参数 pfunc 是要执行的清理函数。
- 参数 arg 是清理函数参数。
如果 iNeedRun 是 0,那么清理函数将不被调用,pthread_cleanup_pop 函数将删除上次 pthread_cleanup_push 调用建立的清理处理程序。
这些函数必须在与线程相同的作用域中以配对的形式使用。
下面程序展示了这两个函数的使用方法。
#include <stdio.h>
#include <pthread.h>
void cleanup (void *arg)
{
fprintf(stdout, "cleanup: %s.\n", (char *)arg);
}
void *routine (void *arg)
{
fprintf(stdout, "thread 1 running...\n");
pthread_cleanup_push(cleanup, "thread1 first");
pthread_cleanup_push(cleanup, "thread1 second");
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void *)1);
}
void *routine1 (void *arg)
{
fprintf(stdout, "thread 2 running...\n");
pthread_cleanup_push(cleanup, "thread2 first");
pthread_cleanup_push(cleanup, "thread2 second");
if (arg) {
return ((void *)2);
}
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void *)2);
}
int main (int argc, char *argv[])
{
pthread_t tid, tid1;
int ret;
void *retval;
ret = pthread_create(&tid, NULL, routine, (void *)1);
if (ret != 0) {
fprintf(stderr, "pthread create failed.\n");
return (-1);
}
ret = pthread_join(tid, &retval);
if (ret != 0) {
fprintf(stderr, "pthread join thread 1 failed.\n");
return (-1);
}
fprintf(stdout, "thread 1 return code: %ld\n", (long)retval);
ret = pthread_create(&tid1, NULL, routine1, (void *)2);
if (ret != 0) {
fprintf(stderr, "pthread create failed.\n");
return (-1);
}
ret = pthread_join(tid1, &retval);
if (ret != 0) {
fprintf(stderr, "pthread join thread 2 failed.\n");
perror("pthread_join");
return (-1);
}
fprintf(stdout, "thread 2 exit code: %ld\n", (long)retval);
return (0);
}
在 SylixOS Shell 下运行程序:
# ./push_test
thread 1 running...
thread 1 return code: 1
thread 2 running...
cleanup: thread2 second.
cleanup: thread2 first.
thread 2 exit code: 2
从程序的运行结果可以看出,两个线程都正常的运行和退出了,而线程 1 并没有调用 cleanup 函数,这正如我们之前所说的 pthread_cleanup_pop 函数参数 iNeedRun 为 0,则不会调用清理函数,然而,线程 2 的 pthread_cleanup_pop 函数参数 iNeedRun 同样为 0 却调用了 cleanup 函数,这说明线程函数退出后调用了清理函数,且调用顺序和安装时的顺序相反。
CPU亲和性
SylixOS 提供了标准的 POSIX 接口来设置线程的亲和关系,下面函数设置线程亲和关系。
#include <pthread_np.h>
int pthread_setaffinity_np(pthread_t thread, size_t setsize, const cpu_set_t *set)
函数 pthread_setaffinity_np 原型分析:
- 此函数成功返回 0,失败返回错误号;
- 参数 thread 是需要亲和的线程句柄;
- 参数 setsize 是 CPU 集大小;
- 参数 set 是需要亲和的 CPU 集。
下面函数可以获取线程亲和关系。
#include <pthread_np.h>
int pthread_getaffinity_np(pthread_t thread, size_t setsize, cpu_set_t *set)
函数 pthread_getaffinity_np 原型分析:
- 此函数成功返回0,失败返回错误号;
- 参数 thread 是需要亲和的线程句柄;
- 参数 setsize 是 CPU 集大小;
- 参数 set 是需要亲和的 CPU 集。
POSIX 提供了标准宏定义来设置 CPU 集。
将n号 CPU 设置的 CPU 集 p 中。
CPU_SET(n, p)
将 n 号 CPU 从 CPU 集 p 中删除。
CPU_CLR(n, p)
判断 n 号 CPU 是否在 CPU 集 p 中。
CPU_ISSET(n, p)
将 CPU 集清0。
CPU_ZERO(p)
说明:
SylixOS目前只支持将线程亲和到一个 CPU 上,CPU 集中存在多个 CPU 时将只亲和到最小 CPU 号上。
下面代码片段,可以将指定的线程亲和到 CPU1 上。
……
cpu_set_t set; /* 定义 CPU 集合 */
CPU_ZERO(&set);
CPU_SET(1, &set);
pthread_setaffinity_np(tid, sizeof(set), &set);
……
算力簇亲和
SylixOS 为应用程序提供了以下 API 用于实时改变任务大小核调度模式:
#include <pthread_np.h>
int pthread_sethetrcc_np(pthread_t thread, int mode, unsigned int cluster);
函数 pthread_sethetrcc_np 原型分析:
- 此函数成功返回0,失败返回错误号;
- 参数 thread 是需要亲和的线程句柄;
- 参数 mode 是调度模式;
- 参数 cluster 是算力簇 ID。
下面宏定义为 POSIX 接口的调度模式
#define PTHREAD_HETRCC_NON_AFFINITY /* 自由调度 */
#define PTHREAD_HETRCC_WEAK_AFFINITY /* 推荐亲和调度 */
#define PTHREAD_HETRCC_STRONG_AFFINITY /* 强算力簇亲和调度 */
其中 PTHREAD_HETRCC_STRONG_AFFINITY 类算力亲和可以继承给自己的子线程,例如任务如果使用 OpenMP 设计并行算法,可以使用此方法让这些并行子线程运行在强算力核心上达到最佳计算性能。
下面函数获取线程的算力簇亲和。
#include <pthread_np.h>
int pthread_gethetrcc_np(pthread_t thread, int *mode, unsigned int *cluster);
函数 pthread_gethetrcc_np 原型分析:
- 此函数成功返回0,失败返回错误号;
- 参数 thread 是需要亲和的线程句柄;
- 参数 mode 是调度模式;
- 参数 cluster 是算力簇 ID。
cluster 参数指定了不同的算力簇,编号从 0 开始,编号越小算力越低。
可以使用以下函数获取当前处理器异构算力簇情况:
#include <unistd.h>
#define _SC_HETRCC_CONF /* 获取当前系统最大异构算力簇 ID 号 */
#define _SC_HETRCC_ONLN /* 获取当前系统在线 CPU 最大异构算力簇 ID 号 */
long sysconf(int name);
算力感知自动适配器
SylixOS 提供的 “算力感知自动调度适配器”则为用户提供了全自动化的动态算力调度能力,用户可无感的在大小核架构上运行各种应用程序。
“算力感知自动调度适配器”基本原则:
- 不降低实时性:自动调度不做核心限制,不向功耗妥协太多,必须保证高优先级任务有合适的 CPU 核心使用。
- 避免激进迁移:算力感知与迁移逐级递进, 避免出现多算力簇颠簸, 尽量保证 CPU 核心 Cache 热度。
- 避免 "小核有难大核围观":自动调度器实现了 “联赛升降级”算法,保证在低算力簇利用率达极限时,算力阈值调度带来的错误判断问题。
使用以下命令注册 “算力感知自动调度适配器”:
insmod /lib/modules/xhcesa.ko
xhcesa.ko 在初始化时会使用自己的方法动态测量 CPU 各算力簇实际算力情况,用户可在 /proc/kernel/cpu_hetrcc 文件查看 xhcesa.ko 测量的 CPU 各算力簇算力与物理算力拓扑情况。例如:
cat /proc/kernel/cpu_hetrcc
CPU ID HETRCC COMPOW ACTIVE
------ ------ -------- ------
0 0 400 YES
1 0 400 YES
2 1 810 YES
3 1 810 YES
以上内容表示当前有三个任务被设置为算力簇 2 WEAK 亲和,当算力簇 2 的 CPU 核心有机会运行这三个任务时,这三个任务会优先运行在算力簇 2 的 CPU 核心上,但 WEAK 亲和仅仅是倾向性亲和,如果这三个任务就绪了,当 2 号算力簇没有执行条件时 (例如有更高优先级任务在运行),SylixOS 为保证实时性,会将这三个任务在小核心算力簇上按照优先级抢占原则进行调度。
随着程序的运行,“算力感知自动调度适配器” 会对任务使用 CPU 算力情况进行实时测量,并根据相关算法进行动态调节,/proc/kernel/hetrcc 文件将会反应当前算力调度情况,需要说明的是:为了避免“算力感知自动调度适配器”与用户自行设计的调度逻辑产生冲突,同时为保证每个任务的实时性,目前自动调度策略只会使用 WEAK 类亲和,其他用户可知的强算力需求任务,用户可以使用 API 进行 STRONG 类型强亲和设置,或者通过 hetrcc 命令进行设置:
hetrcc [tid] [weak|strong] cluster