Linux Pthread 常用函数学习与使用

POSIX.1 指定了一组接口(函数、头文件),用于线程编程,通常称为 POSIX 线程或 Pthread。一个进程可以包含多个线程,所有线程都执行相同的程序。这些线程共享相同的全局内存(数据段和堆段),但是每个线程都有自己的栈(自动变量)。

POSIX.1 还要求线程共享一系列其他属性(即,这些属性是进程范围而不是每个线程的):

-进程 ID

-父进程 ID

-进程组 ID 和会话 ID

-控制终端

-用户和组 ID

-打开的文件描述符

-记录锁(请参阅 fcntl(2))

-信号配置

-文件模式创建掩码(umask(2))

-当前目录(chdir(2))和根目录(chroot(2))

-间隔计时器(setitimer(2))和 POSIX 计时器(timer_create(2))

-nice 值(setpriority(2))

-资源限制(setrlimit(2))

-测量 CPU 时间(times(2))和资源(getrusage(2))消耗

除了栈,POSIX.1 还指定了其他各种属性。每个线程各不相同,包括:

-线程ID(pthread_t 数据类型)

-信号掩码(pthread_sigmask(3))

-errno 变量

-备用信号栈(sigaltstack(2))

-实时调度策略和优先级(sched(7))

以下特定于 Linux 的功能也是每个线程特有的

-能力(请参阅capabilities(7))

-CPU 亲和性(sched_setaffinity(2))

大多数 pthread 函数在成功时返回 0,而在失败时返回错误号。请注意,pthreads 函数不会设置 errno。 对于每个可能返回错误的 pthreads 函数 POSIX.1-2001 指定函数永远不会因错误 EINTR 而失败。

线程 ID

进程中的每个线程都有一个唯一的线程标识符(存储在 pthread_t 类型中)。该标识符返回给 pthread_create(3) 的调用者,线程可以使用 pthread_self(3) 获得其自己的线程标识符。

线程 ID 仅在一个进程中保证是唯一的(在所有接受线程 ID 作为参数的 pthreads 函数中,该 ID 从定义上是指与调用程序处于同一进程中的线程)。

在已终止的线程 join 结束或已分离的线程已终止之后,系统可以重用线程 ID。POSIX 描述说:“如果应用程序尝试使用其生存期已结束的线程 ID,则该行为未定义”。

一、pthread_create

创建一个线程。函数定义位于头文件 #include <pthread.h>

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

pthread_create() 函数在调用过程中启动一个新线程。新线程通过调用 start_routine() 开始执行。arg 作为 start_routine() 的唯一参数传递。

新线程以下列方式之一终止:

  1. 调用 pthread_exit(3),指定一个退出状态值,该值对调用 pthread_join(3) 的进程中的另一个线程可用。
  2. 从 start_routine() 返回。这相当于使用 return 语句中提供的值调用 pthread_exit(3)。
  3. 被取消了(参见pthread_cancel(3))。
  4. 进程中的任何线程调用 exit(3),或者主线程执行 main() 返回。这会导致进程中的所有线程终止。

attr 参数指向一个 pthread_attr_t 结构,其内容在线程创建时用于确定新线程的属性;这个结构是使用 pthread_attr_init(3) 和相关函数初始化的。如果 attr 为空,则使用默认属性创建线程。

在返回之前,成功调用 pthread_create() 将新线程的 ID 存储在线程指向的缓冲区中;此标识符用于在后续对其他 pthreads 函数的调用中引用线程。

新线程继承创建线程的信号掩码(pthread_sigmask(3))的副本。新线程的挂起信号集为空(sigpending(2))。新线程不继承创建线程的备用信号栈(sigaltstack(2))。

新线程继承调用线程的浮点环境(fenv(3))。

新线程的 cpu 时间时钟的初始值是 0(参见pthread_getcpuclockid(3))。

Linux 特定细节

新线程继承调用线程的能力集(参见capability(7))和 CPU 亲和性掩码(参见sched_setaffinity(2))的副本。

如果成功,pthread_create() 返回 0;在出现错误时,它返回一个错误号,并且 *thread 的内容是未定义的。

错误码含义:

错误码含义
EAGAIN资源不足,无法创建另一个线程。
EAGAIN遇到了系统对线程数量的限制。有一些限制可能会触发此错误:达到了 RLIMIT_NPROC 软资源限制(通过 setrlimit(2) 设置),它限制了实际用户 ID 的进程和线程的数量;内核系统对进程和线程数量的限制达到了/proc/sys/kernel/threads-max(见 proc(5));或者达到 pid 的最大数量,即 /proc/sys/kernel/pid_max(参见proc(5))。
EINVALattr中的设置无效。
EPERM没有权限设置attr中指定的调度策略和参数。

二、pthread_exit

终止调用线程。函数定义位于头文件 #include <pthread.h>

void pthread_exit(void *retval);

pthread_exit() 函数终止调用线程并通过 retval 返回一个值(如果线程是可接合的),该值对于调用 pthread_join(3) 的进程中的另一个线程是可用的。

pthread_cleanup_push(3) 所建立的任何未被弹出的清理处理程序都将被弹出(与它们被弹出的顺序相反)并执行。如果线程有任何特定于线程的数据,那么在执行清理处理程序之后,将按未指定的顺序调用相应的析构函数。

当线程终止时,进程共享资源(例如互斥、条件变量、信号量和文件描述符)不会被释放,使用 atexit(3) 注册的函数也不会被调用。

在进程的最后一个线程终止后,调用 exit(3) 进程终止,退出状态为 0;因此,释放进程共享资源并调用使用 atexit(3) 注册的函数。

从除主线程之外的任何线程的 start 函数执行返回,将导致对 pthread_exit() 的隐式调用,使用函数的返回值作为线程的退出状态。为了允许其他线程继续执行,主线程应该通过调用 pthread_exit() 而不是 exit(3) 来终止。retval 所指向的值不应该位于调用线程的栈上,因为该栈的内容在线程终止后是未定义的。

三、pthread_self

获取调用线程的 ID。函数定义位于头文件 #include <pthread.h>

pthread_t pthread_self(void);

pthread_self() 函数返回调用线程的 ID。 这与创建该线程的 pthread_create(3) 调用中的 *thread 返回的值相同。此函数始终成功,返回调用线程的 ID。

POSIX.1 允许实现人员自由选择表示线程 ID 的类型。例如,允许使用算术类型或结构表示。因此,无法使用 C 相等运算符(==)来比较 pthread_t 类型的变量;使用 pthread_equal(3) 代替。

线程标识符应该被认为是不透明的:除 pthreads 调用外,任何尝试使用线程 ID 的尝试都是不可移植的,并且可能导致未指定的结果。

线程 ID 仅在一个进程中保证是唯一的。加入终止线程或分离线程终止后,线程 ID 可以重新使用。

pthread_self() 返回的线程 ID 与 gettid(2) 调用返回的内核线程 ID 不同。

四、pthread_cancel

向线程发送取消请求。函数定义位于头文件 #include <pthread.h>

int pthread_cancel(pthread_t thread);

pthread_cancel() 函数向线程 thread 发送一个取消请求。目标线程是否以及何时响应取消请求取决于该线程控制下的两个属性:可取消状态和类型。

线程的可取消状态(由 pthread_setcancelstate(3) 决定)可以启用(新线程的默认状态)或禁用。如果线程已禁用取消,则取消请求将保持排队状态,直到线程启用取消。如果一个线程启用了取消,那么它的可取消性类型决定了何时取消。

由 pthread_setcanceltype(3) 决定的线程的取消类型可以是异步的,也可以是延迟的(新线程的默认值)。异步可取消性意味着线程可以在任何时候被取消(通常是立即取消,但系统不保证这一点)。延迟取消类型意味着取消将被延迟,直到线程接下来调用一个作为取消点的函数。在 pthreads(7) 中提供了一个函数列表,这些函数是或可能是取消点。

当执行取消请求时,线程执行以下步骤(按此顺序):

  1. 将弹出取消清理处理程序(与它们被压入的顺序相反)并调用它们。(参见 pthread_cleanup_push(3))。
  2. 以未指定的顺序调用特定于线程的数据析构函数。(参见 pthread_key_create(3))。
  3. 线程终止。(参见pthread_exit(3))

上述步骤是在 pthread_cancel() 调用中异步发生的;pthread_cancel() 的返回状态仅仅通知调用者取消请求是否已成功排队。

在已取消的线程终止后,使用 pthread_join(3) 与该线程的 join 将获得 pthread_cancelled 作为该线程的退出状态。(线程 join 是知道取消已经完成的唯一方法)

如果成功,pthread_cancel() 返回 0;在出现错误时,它返回一个非零的错误号。

错误码含义:

ESRCH ———— 找不到 thread。

在 Linux 上,取消是使用信号实现的。在 NPTL 线程实现下,第一个实时信号(即信号 32)用于此目的。在 LinuxThreads 上,使用第二个实时信号(如果有实时信号),否则使用 SIGUSR2。

五、pthread_join

等待线程终止。函数定义位于头文件 #include <pthread.h>

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

pthread_join() 函数等待线程指定的线程终止。如果该线程已经终止,则 pthread_join() 立即返回。线程指定的线程必须是可连接的。

如果 retval 不为NULL,则 pthread_join() 将目标线程的退出状态(即目标线程提供给 pthread_exit(3) 的值)复制到 retval 指向的位置。 如果取消了目标线程,则将 PTHREAD_CANCELED 放置在 retval 指向的位置。

如果多个线程同时尝试加入同一线程,则结果是不确定的。如果取消调用 pthread_join() 的线程,则目标线程将保持可连接状态(即不会被分离)。

如果成功,pthread_join() 返回 0;在出现错误时,它返回一个错误号。

错误码含义:

错误码含义
EDEADLK检测到死锁(例如,两个线程试图相互连接);或 thread 指定调用线程。
EINVALthread 不是可连接线程;另一个线程已经在等待加入该线程。
ESRCH找不到具有 ID thread 的线程。

成功调用 pthread_join() 之后,可以确保调用方目标线程已终止。 然后,调用者可以选择执行线程终止后所需的任何清理操作(例如,释放分配给目标线程的内存或其他资源)。

使用先前已连接的线程进行连接会导致未定义的行为。

无法与可连接的线程(即未分离的线程)连接会产生“僵尸线程”。避免这样做,因为每个僵尸线程都会消耗一些系统资源,并且当累积了足够多的僵尸线程时,将不再可能创建新的线程(或进程)。

没有 waitpid(-1,&status,0) 的 pthreads 类似物,即“与任何终止的线程连接”。如果你认为需要此功能,则可能需要重新考虑应用程序设计。

进程中的所有线程都是对等的:任何线程都可以与进程中的任何其他线程连接。

六、pthread_detach

分离线程,函数定义位于头文件 #include <pthread.h>

int pthread_detach(pthread_t thread);

pthread_detach() 函数将由线程标识的 thread 标记为已分离。当分离的线程终止时,其资源会自动释放回系统,而无需另一个线程与终止的线程联接。

尝试分离已经分离的线程会导致未指定的行为。

如果成功,pthread_detach() 返回 0;在出现错误时,它返回一个错误号。

错误码含义:

错误码含义
EINVALthread 不是可连接线程。
ESRCH找不到具有 ID thread 的线程。

一旦线程被分离,它就不能使用 pthread_join(3) 进行连接,也不能再次进行连接。

可以使用 pthread_attr_setdetachstate(3) 以分离状态创建新线程,以设置 pthread_create(3) 的 attr 参数的分离属性。

分离属性仅决定线程终止时系统的行为;如果进程使用 exit(3) 终止(或者,如果主线程返回),它不会阻止线程终止。

应用程序创建的每个线程都应该调用 pthread_join(3) 或 pthread_detach(),以便释放线程的系统资源。(但是请注意,当进程终止时,没有执行这些操作的任何线程的资源将被释放)

七、pthread_attr_init,pthread_attr_destroy

初始化和销毁线程属性对象。函数定义位于头文件 #include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

pthread_attr_init() 函数使用默认属性值初始化 attr 指向的线程属性对象。在此调用之后,可以使用各种相关函数设置对象的各个属性(参见下面的列表),然后可以在一个或多个创建线程的 pthread_create(3) 调用中使用该对象。

pthread_attr_setaffinity_np(3), 
pthread_attr_setdetachstate(3),
pthread_attr_setguardsize(3), 
pthread_attr_setinheritsched(3),
pthread_attr_setschedparam(3), 
pthread_attr_setschedpolicy(3),
pthread_attr_setscope(3), 
pthread_attr_setstack(3),
pthread_attr_setstackaddr(3), 
pthread_attr_setstacksize(3),
pthread_create(3), 
pthread_getattr_np(3),
pthread_setattr_default_np(3), 
pthreads(7)

在已经初始化的线程属性对象上调用 pthread_attr_init() 会导致未定义的行为。

当不再需要线程属性对象时,应该使用 pthread_attr_destroy() 函数销毁它。销毁线程属性对象对使用该对象创建的线程没有影响。

一旦线程属性对象被销毁,就可以使用 pthread_attr_init() 重新初始化它。销毁的线程属性对象的任何其他使用都有未定义的结果。

pthread_attr_t 类型应被视为不透明的:除通过 pthreads 函数之外,对该对象的任何访问都是不可移植的,并且会产生不确定的结果。

如果成功,这些函数返回 0;在出现错误时,它们返回一个非零的错误号。

POSIX.1 记录了pthread_attr_init() 的 ENOMEM 错误;在 Linux 上,这些功能始终会成功(但是可移植且面向未来的应用程序仍应处理可能的错误返回)。

八、例程

下面的例子使用了 pthread 函数家族中的 pthread_create、pthread_cancel、pthread_join 和 pthread_setcancelstate。

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

#define handle_error_en(en, msg) \
    do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

static void* thread_func(void *ignored_argument) {
    int s;
    /* 暂时禁用取消功能,这样我们就不会立即响应取消请求 */
    s = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    if (s != 0) handle_error_en(s, "pthread_setcancelstate");

    printf("thread_func(): started; cancellation disabled\n");
    sleep(5);
    printf("thread_func(): about to enable cancellation\n");

    s = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    if (s != 0) handle_error_en(s, "pthread_setcancelstate");
    /* sleep()是一个取消点 */
    sleep(1000); /* 睡眠时应该被取消 */
    /* 永远不要到这里 */
    printf("thread_func(): not canceled!\n");
    return NULL;
}

int main(void){
    pthread_t thr;
    void *res;
    int s;
    /* 启动一个线程,然后向它发送一个取消请求 */
    s = pthread_create(&thr, NULL, &thread_func, NULL);
    if (s != 0) handle_error_en(s, "pthread_create");
    sleep(2); /* 给线程一个开始执行的机会 */
    printf("main(): sending cancellation request\n");
    s = pthread_cancel(thr);
    if (s != 0) handle_error_en(s, "pthread_cancel");
    /* 与thread连接,查看其退出状态 */
    s = pthread_join(thr, &res);
    if (s != 0) handle_error_en(s, "pthread_join");
    if (res == PTHREAD_CANCELED)
        printf("main(): thread was canceled\n");
    else
        printf("main(): thread wasn't canceled (shouldn't happen!)\n");
    exit(EXIT_SUCCESS);
}

编译程序

gcc pthread.c -o pthread -lpthread

运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHdwJt13-1589456971201)(C7E619811C264BDCAF3C072E8CB4452E)]

thread_func(): started; cancellation disabled
main(): sending cancellation request
thread_func(): about to enable cancellation
main(): thread was canceled

子线程先启动,然后禁用了可取消功能,接着主线程发送取消请求到子线程,这个时候子线程在睡眠,取消请求被排队,当子线程唤醒之后,马上激活了可取消功能。当再次进入睡眠时,因为 sleep() 函数是一个取消点,子线程被取消。主线程调用 pthread_join 等待就返回了,最后主线程运行到了终点。

参考资料:

  1. http://man7.org/linux/man-pages/man7/pthreads.7.html
  2. http://man7.org/linux/man-pages/man3/pthread_create.3.html
  3. http://man7.org/linux/man-pages/man3/pthread_exit.3.html
  4. http://man7.org/linux/man-pages/man3/pthread_self.3.html
  5. http://man7.org/linux/man-pages/man3/pthread_cancel.3.html
  6. http://man7.org/linux/man-pages/man3/pthread_join.3.html
  7. http://man7.org/linux/man-pages/man3/pthread_detach.3.html
  8. http://man7.org/linux/man-pages/man3/pthread_attr_init.3.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TYYJ-洪伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值