文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

C 语言编程 — pthread 用户线程操作

2023-09-10 21:06

关注

文章目录

pthread(POSIX Threads)是一套符合 POSIX(Portable Operating System Interface,可移植操作系统接口)的 User Thread 操作 API 标准,定义了一组线程相关的函数(60 多个)和数据类型。pthread API 可以用于不同的操作系统上,因此被称为可移植的线程 API。

在版本较新的 Linux Kernel 中,pthread API 的具体实现是 NPTL(Native POSIX Thread Library)。为了方便描述,在后文中我们使用 pthread 来统称。

pthread 线程库围绕 struct pthread 提供了一系列的接口,用于完成 User Thread 创建、调度、销毁等一系列管理。

在这里插入图片描述

在 pthread 中,使用 TCB(Thread Control Block,线程控制块)来存储 User Thread 的所有信息,TCB 的体量会比 PCB 小非常多。对应的 pthread 结构体如下:

// glibc/nptl/descr.hstruct pthread {    struct pthread *self;           // 指向自身的指针    struct __pthread_internal_list *thread_list;  // 线程列表,指向线程列表的指针,用于实现线程池;    void *(*start_routine)(void*);  // 线程的入口函数,由 pthread_create() 函数传入;    void *arg;                      // 线程的入口函数参数,由 pthread_create() 函数传入;    void *result;                   // 线程的返回值,由线程的入口函数返回;    pthread_attr_t *attr;           // 线程的属性,包括栈保护区大小、调度策略等,由 pthread_create() 函数传入;    pid_t tid;                      // 线程的唯一标识符,由 Kernel 分配;    struct timespec *waiters;       // 等待的时间戳    size_t guardsize;               // 栈保护区大小    int sched_policy;               // 调度策略    struct sched_param sched_params;// 调度参数    void *specific_1stblock;        // 线程私有数据的第一个块    struct __pthread_internal_slist __cleanup_stack;  // 清理函数栈    struct __pthread_mutex_s *mutex_list;  // 线程持有的互斥锁列表    struct __pthread_cond_s *cond_list;    // 线程等待的条件变量列表    unsigned int detach_state:2;     // 线程分离状态,包括分离和未分离两种;    unsigned int sched_priority:30;  // 线程的调度优先级    unsigned int errno_val;          // 线程的错误码};

线程的合并与分离

对于 User Thread 的生命周期管理,首先要明确线程合并和线程分离的概念。

线程的合并与分离是指在多线程程序中,对于已经创建的线程进行结束和回收资源的 2 种操作方式。

需要注意的是,线程的合并和分离操作都必须在目标线程执行结束之前进行,并且必须二选一,否则会导致内存泄露甚至崩溃。

pthread_create() 创建线程

函数作用:用于创建一条新的对等线程,并指定线程的入口函数和参数。pthread 库就会为 User Thread 分配 TCB、PC(程序计数器)、Registers(寄存器)和 Stack(栈)等资源。并将其加入到 Thread Queue 中等待执行。直到 User Thread 被调度到 CPU 时,开始执行线程入口函数。

函数原型

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

pthread_join() 合并线程

函数作用:执行线程合并。阻塞当前的主线程,直到指定线程执行结束,然后获得线程的执行结果,并释放线程的资源。

函数原型

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

在这里插入图片描述

pthread_exit() 线程主动退出

函数作用:线程主动终止自己,返回结果到 pthread_join()。需要注意的是,Main Thread 不应该调用 pthread_exit(),这样会退出整个 User Process。

函数原型

void pthread_exit(void *retval);

在这里插入图片描述

pthread_detach() 分离线程

函数作用:执行线程分离。将指定的线程标记为 “可分离的“,表示该线程在执行结束后会自动释放资源(由资源自动回收机制完成),无需等待主线程回收。另一方面,这也意味这主线程无法获得线程的返回值。

函数原型

int pthread_detach(pthread_t thread);

可以在 pthread_create() 新建线程时,直接指定线程的属性,也可以更改已经存在的线程的属性,包括:

// 定义一个 pthread attribute 实例。pthread_attr_t attr;// 初始化一个 pthread attribute 实例。int pthread_attr_init(pthread_attr_t *attr);// 清除一个 pthread attribute 实例。int pthread_attr_destory(pthread_attr_t *attr);

线程分离属性

线程分离属性,即:将线程设定为 “可分离的"。

函数原型

pthread_attr_setdetachstat(pthread_attr_t *attr, int detachstate);

设定属性后不需要再通过 pthread_detach() 重复设定。

LWP 绑定属性

POSIX 标准引入了 “线程竞争域“ 的概念,即:User Threads 对 CPU 资源发起竞争的范围,并要求至少要实现下列 2 种范围之一:

  1. PTHREAD_SCOPE_PROCESS:User Threads 在 User Process 范围内竞争 CPU 资源。
  2. PTHREAD_SCOPE_SYSTEM:User Threads 在 System 范围内竞争 CPU 资源。

相应的,pthread API 库也提供了 pthread_attr_setscope() 接口来设定 User Threads 的竞争范围。但是,实际上 Linux NPTL 只实现了 PTHREAD_SCOPE_SYSTEM 这一种方式。

具体而言就是 LWP(Light Weight Process)的实现。在还没有 pthread 线程库的早期版本的 Linux 中,只有 Kernel Thread 的概念,User Process 只能通过 kthread_crearte SCI(系统调用接口)来创建 Thread。但这种方式显然会存在 User Space(User Process)和 Kernel Space(Kernel Thread)之间频繁的切换。

为了解决这个问题,POSIX 标准引入了 User Thread 和 LWP 的概念,最早在 Solaris 操作系统中实现。之所以要同时引入 LWP 的目的是为了让实现 User Thread 的 pthread API 接口能够在不同的操作系统中保持良好的兼容性。

而 Linux NPTL 则将 LWP 作为 User Thread 和 Kernel Thread 之间建立映射关系的桥梁,并让 User Threads 能够竞争全局的 CPU 资源,以此来发挥多核处理器平台的并行优势。

当调用 pthread_create() 新建多个 User Threads 时,Kernel 会为这些 User Threads 创建少量的 LWPs,并建立 M:N 的映射关系。这个映射过程是由 Kernel 完成的,开发者无法手动干预。

在这里插入图片描述

CPU 亲和性属性

在 Kernel 中,LWP 同样作为可调度单元,与 kthread_create() 创建的 Kernel Thread 一般,可以被 Kernel Scheduler 识别并根据调度策略调度到不同的 CPU 上执行。

默认情况下,User Thread 依靠 LWP 的可调度能力,会被 Kernel 尽力而为的分配到多个不同的 CPU cores 上执行,以达到负载均衡。但这种分配是随机的,不保证 User Thread 最终在那个 Core 上执行。

相对的,可以通过修改 User Threads 的 CPU 亲和性属性让它们在指定的 cpuset 中竞争。

函数原型

int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t cpusetsize, const cpu_set_t *cpuset);

调度属性

User Thread 的调度属性有 3 类,分别是:调度算法、调度优先级、调度继承权。

调度算法,函数原型

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

调度优先级,函数原型:只在 SCHED_FIFO 和 SCHED_RR 等实时调度算法中生效,User Process 需要以 root 权限运行,且需要显式放弃父线程的继承权。

struct sched_param {int sched_priority;char __opaque[__SCHED_PARAM_SIZE__]; };int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);

调度继承权,函数原型:子线程是否继承父线程的调度算法和调度优先级。

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);

实践多线程的目的往往在于提升应用程序的执行性能,通常有并发和并行这 2 种方式:

  1. 并发程序:并发指在同一时间段内,多线程在同一个 CPU 上执行。并发程序不强制要求 CPU 具备多核计算能力,只要求多个线程在同一个 Core 上进行 “分时轮询” 处理,以此在宏观上实现多线程同时执行的效果。并发程序的执行通常是不确定的,这种不确定性来源于资源之间的相关依赖和竞态条件,可能导致执行的线程间相互等待(阻塞)。并发程序通常是有状态的(非幂等性)。

  2. 并行程序:并行指在同一时刻内,多线程在不同的 CPU core 上同时执行。并行程序会强制要求 CPU 具备多核计算能力,并行程序的每个执行模块在逻辑上都是独立的,即线程执行时可以独立地完成任务,从而做到同一时刻多个指令能够同时执行。并行程序通常是无状态的(幂等性)。

示例程序:

#define _GNU_SOURCE // 用于启用一些非标准的、GNU C 库扩展的特性,例如: 中的 CPU_ZERO 和 CPU_SET 函数。#include #include #include #include #include #define THREAD_COUNT 12  // 12 个对等线程void show_thread_sched_policy_and_cpu(int threadno){    int cpuid;    int policy;    struct sched_param param;    cpuid = sched_getcpu();    pthread_getschedparam(pthread_self(), &policy, &param);    printf("Thread %d is running on CPU %d, ", threadno, cpuid);    switch (policy)    {    case SCHED_OTHER:        printf("SCHED_OTHER\n", threadno);        break;    case SCHED_RR:        printf("SCHDE_RR\n", threadno);        break;    case SCHED_FIFO:        printf("SCHED_FIFO\n", threadno);        break;    default:        printf("UNKNOWN\n");    }}void *thread_func(void *arg){    int i, j;    long threadno = (long)arg;    printf("thread %d start\n", threadno);    sleep(1);    show_thread_sched_policy_and_cpu(threadno);    for (i = 0; i < 10; ++i)    {        // 适当调整执行时长        for (j = 0; j < 10000000000; ++j)        {        }    }    printf("thread %d exit\n", threadno);    return NULL;}int main(int argc, char *argv[]){    long i;    int cpuid;    cpu_set_t cpuset;    pthread_attr_t attr[THREAD_COUNT];    pthread_t pth[THREAD_COUNT];    struct sched_param param;    // 初始化线程属性    for (i = 0; i < THREAD_COUNT; ++i)        pthread_attr_init(&attr[i]);    // 调度属性设置    for (i = 0; i < THREAD_COUNT / 2; ++i)    {        param.sched_priority = 10;        pthread_attr_setschedpolicy(&attr[i], SCHED_FIFO);        pthread_attr_setschedparam(&attr[i], &param);        pthread_attr_setinheritsched(&attr[i], PTHREAD_EXPLICIT_SCHED);    }    for (i = THREAD_COUNT / 2; i < THREAD_COUNT; ++i)    {        param.sched_priority = 20;        pthread_attr_setschedpolicy(&attr[i], SCHED_RR);        pthread_attr_setschedparam(&attr[i], &param);        pthread_attr_setinheritsched(&attr[i], PTHREAD_EXPLICIT_SCHED);    }    // CPU 亲和性属性设置,使用 cpuset(0,1)。    for (i = 0; i < THREAD_COUNT; ++i)    {        pthread_create(&pth[i], &attr[i], thread_func, (void *)i);        CPU_ZERO(&cpuset);        cpuid = i % 2;        CPU_SET(cpuid, &cpuset);        pthread_setaffinity_np(pth[i], sizeof(cpu_set_t), &cpuset);    }    for (i = 0; i < THREAD_COUNT; ++i)        pthread_join(pth[i], NULL);    // 清理线程属性    for (i = 0; i < THREAD_COUNT; ++i)        pthread_attr_destroy(&attr[i]);    return 0;}
$ ps -eLo pid,ppid,tid,lwp,nlwp,class,rtprio,ni,pri,psr,pcpu,policy,stat,comm | awk '$7 !~ /-/{print $0}'  PID  PPID   TID   LWP NLWP CLS RTPRIO  NI PRI PSR %CPU POL STAT COMMAND26031 24641 26032 26032   13 FF      10   -  50   0  0.0 FF  Rl+  test126031 24641 26033 26033   13 FF      10   -  50   1  0.0 FF  Rl+  test126031 24641 26034 26034   13 FF      10   -  50   0  0.0 FF  Rl+  test126031 24641 26035 26035   13 FF      10   -  50   1  0.0 FF  Rl+  test126031 24641 26036 26036   13 FF      10   -  50   0  0.0 FF  Rl+  test126031 24641 26037 26037   13 FF      10   -  50   1  0.0 FF  Rl+  test126031 24641 26038 26038   13 RR      20   -  60   0 33.3 RR  Rl+  test126031 24641 26039 26039   13 RR      20   -  60   1 33.3 RR  Rl+  test126031 24641 26040 26040   13 RR      20   -  60   0 33.3 RR  Rl+  test126031 24641 26041 26041   13 RR      20   -  60   1 33.2 RR  Rl+  test126031 24641 26042 26042   13 RR      20   -  60   0 33.2 RR  Rl+  test126031 24641 26043 26043   13 RR      20   -  60   1 33.3 RR  Rl+  test1

多线程安全(Multi-Thread Safe),就是在多线程环境中,多个线程在同一时刻对同一份共享数据(Shared Resource,e.g. 寄存器、内存空间、全局变量、静态变量 etc.)进行写操作(读操作不会涉及线程安全的问题)时,不会出现数据不一致。

为了确保在多线程安全,就要确保数据的一致性,即:线程安全检查。多线程之间通过需要进行同步通信,以此来保证共享数据的一致性。

pthread 库提供了保证线程安全的方式:

  1. 互斥锁(Mutex):是一种线程安全机制,为共享数据加上一把锁,拥有锁的线程,才可以访问共享数据。以此保护共享数据不被多个线程同时访问。
  2. 条件变量(Condition Variable):是一种线程同步机制,用于判断线程是否满足了特定的竞争条件(Race Condition)。只有满足条件的线程,才可以获得互斥锁,以此来避免死锁的情况。

需要注意的是,线程安全检查的实现会带来一定的系统开销。

在这里插入图片描述

互斥锁(Mutex)

pthread_mutex_init()

函数作用:用于初始化一个互斥锁实体。

函数原型

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

pthread_mutex_lock()

函数作用:User Thread 用于获取互斥锁。如果互斥锁已被 Other User Thread 获得,则当前 User Thread 会阻塞。

函数原型

int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_unlock()

函数作用:User Thread 用于释放互斥锁,互斥锁重回可用状态。如果当前 User Thread 并没有锁,则该函数可能会产生未定义行为。

函数原型

int pthread_mutex_unlock(pthread_mutex_t *mutex);

条件变量(Condition Variable)

互斥锁和条件变量都是多线程同步的工具,但是它们的作用不同:

举例来说,存在全局变量 n(共享数据)被多线程访问。当 TA 获得锁后,在临界区中访问 n,且只有当 n > 0 时,才会释放锁。这意味着当 n == 0 时,TA 将永远不会释放锁,从而造成死锁。

在这里插入图片描述

那么解决死锁的方法,就是设定一个条件:只有当 n > 时,TA 才可以获得锁。而这个条件,就是多线程之间需要同步的信息。即:在多线程环境中,当一个线程需要等待某个条件成立时,才可以获得锁,那么应该使用条件变量来实现。

pthread_cond_init()

函数作用:用于初始化一个条件变量实体。

函数原型

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

pthread_cond_wait()

函数作用:线程用于等待某个条件变量满足。

  1. 当 T1 线程调用 pthread_cond_wait() 时,会自动地释放掉互斥锁,并阻塞线程,开始等待。
  2. 直到另一个 T2 线程调用了 pthread_cond_signal() 或 pthread_cond_broadcast(),以此来通知 T1 条件变量满足了。
  3. 然后 T1 pthread_cond_wait() 重新获取指定的互斥锁并返回。

在这里插入图片描述

函数原型

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

pthread_cond_signal()

函数作用:用于向等待条件变量的线程发送信号,唤醒其中的一个线程。

函数原型

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast()

函数作用:用于向等待条件变量的所有线程发送信号,唤醒所有等待的线程。

函数原型

int pthread_cond_broadcast(pthread_cond_t *cond);

互斥锁和条件变量配合使用

当一个线程需要某个条件成立后才可以访问共享数据时。需要先锁定一个互斥锁,然后检查条件变量,如果条件不满足,则需要挂起并等待。

在这里插入图片描述

在这里插入图片描述

#include pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int data = 0;  // 共享数据void *producer(void *arg){    for (int i = 0; i < 10; i++) {        pthread_mutex_lock(&mutex); // 加锁        data++; // 修改共享数据        pthread_cond_signal(&cond); // 发送信号        pthread_mutex_unlock(&mutex); // 解锁        sleep(1);    }    pthread_exit(NULL);}void *consumer(void *arg){    while (1) {        pthread_mutex_lock(&mutex); // 加锁        while (data == 0) { // 如果没有数据就等待信号            pthread_cond_wait(&cond, &mutex);        }        printf("data = %d\n", data); // 打印共享数据        data--; // 修改共享数据        pthread_mutex_unlock(&mutex); // 解锁        sleep(1);    }    pthread_exit(NULL);}int main(){    pthread_t tid1, tid2;    pthread_create(&tid1, NULL, producer, NULL);    pthread_create(&tid2, NULL, consumer, NULL);    pthread_join(tid1, NULL);    pthread_join(tid2, NULL);    return 0;}

C 语言提供的大部分标准库函数都是线程安全的,但是也有几个常用函数是线程不安全的,也称为不可重入函数,原因是使用了某些全局或者静态变量。

我们知道,全局变量和静态变量分别对应内存中的全局变量区和静态存储区,这些区域都是可以跨线程访问的。在多线程环境中,这些数据如果在没有加锁的情况下并行读写,就会造成 Segmentfault / CoreDump 之类的问题。

来源地址:https://blog.csdn.net/Jmilk/article/details/129473253

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯