目录 文章目录 目录pthread 线程库TCB 结构体线程的生命周期管理线程的合并与分离pthread_create() 创建线程pthread_join() 合并线程pthread_exi
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 库就会为 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);
函数作用:执行线程合并。阻塞当前的主线程,直到指定线程执行结束,然后获得线程的执行结果,并释放线程的资源。
函数原型:
int pthread_join(pthread_t thread, void **retval);
函数作用:线程主动终止自己,返回结果到 pthread_join()。需要注意的是,Main Thread 不应该调用 pthread_exit(),这样会退出整个 User Process。
函数原型:
void pthread_exit(void *retval);
函数作用:执行线程分离。将指定的线程标记为 “可分离的“,表示该线程在执行结束后会自动释放资源(由资源自动回收机制完成),无需等待主线程回收。另一方面,这也意味这主线程无法获得线程的返回值。
函数原型:
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() 重复设定。
POSIX 标准引入了 “线程竞争域“ 的概念,即:User Threads 对 CPU 资源发起竞争的范围,并要求至少要实现下列 2 种范围之一:
相应的,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 完成的,开发者无法手动干预。
在 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 种方式:
并发程序:并发指在同一时间段内,多线程在同一个 CPU 上执行。并发程序不强制要求 CPU 具备多核计算能力,只要求多个线程在同一个 Core 上进行 “分时轮询” 处理,以此在宏观上实现多线程同时执行的效果。并发程序的执行通常是不确定的,这种不确定性来源于资源之间的相关依赖和竞态条件,可能导致执行的线程间相互等待(阻塞)。并发程序通常是有状态的(非幂等性)。
并行程序:并行指在同一时刻内,多线程在不同的 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, ¶m); 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], ¶m); 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], ¶m); 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;}
CPU 调度亲和性效果。
User Thread 和 LWP(12 + 1)绑定关系与调度策略效果。
$ 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 库提供了保证线程安全的方式:
需要注意的是,线程安全检查的实现会带来一定的系统开销。
函数作用:用于初始化一个互斥锁实体。
函数原型:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
函数作用:User Thread 用于获取互斥锁。如果互斥锁已被 Other User Thread 获得,则当前 User Thread 会阻塞。
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
函数作用:User Thread 用于释放互斥锁,互斥锁重回可用状态。如果当前 User Thread 并没有锁,则该函数可能会产生未定义行为。
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
互斥锁和条件变量都是多线程同步的工具,但是它们的作用不同:
举例来说,存在全局变量 n(共享数据)被多线程访问。当 TA 获得锁后,在临界区中访问 n,且只有当 n > 0 时,才会释放锁。这意味着当 n == 0 时,TA 将永远不会释放锁,从而造成死锁。
那么解决死锁的方法,就是设定一个条件:只有当 n > 时,TA 才可以获得锁。而这个条件,就是多线程之间需要同步的信息。即:在多线程环境中,当一个线程需要等待某个条件成立时,才可以获得锁,那么应该使用条件变量来实现。
函数作用:用于初始化一个条件变量实体。
函数原型:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
函数作用:线程用于等待某个条件变量满足。
函数原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
函数作用:用于向等待条件变量的线程发送信号,唤醒其中的一个线程。
函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
函数作用:用于向等待条件变量的所有线程发送信号,唤醒所有等待的线程。
函数原型:
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 之类的问题。
--结束END--
本文标题: C 语言编程 — pthread 用户线程操作
本文链接: https://lsjlt.com/news/402619.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-04-01
2024-04-03
2024-04-03
2024-01-21
2024-01-21
2024-01-21
2024-01-21
2023-12-23
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0