文章目录
- 一、 POSIX 线程库
- 1.1 POSIX标准
- 1.2 Pthreads
- 1.2 数据类型、函数、宏
- 1.21 数据类型
- 1.22 函数
- 1.23 宏
- 二、创建线程
- 三、线程同步
- 四、线程销毁
- 五、示例
- 5.1 完整示例
- 5.2 信号量示例
本专栏上一篇文章是Windows下(MSVC)的线程编程,需要的自行查看。
【C语言】Windows下的C语言线程编程详解
进程:进程是操作系统分配资源的基本单位,每个进程有独立的地址空间,进程间通信需要通过特定的机制。
线程:线程是进程中的一个执行单元,它们共享进程的资源。线程之间的切换比进程之间的切换代价小,因此线程编程可以提高程序的并发性能。
并发与并行:并发是指多个任务在同一时间段内交替执行,而并行是指多个任务在同一时刻同时执行。线程编程可以实现并发执行,但是否能够实现并行执行取决于系统的处理器数量和调度策略。
一、 POSIX 线程库
1.1 POSIX标准
POSIX
标准是一系列确保操作系统之间兼容性的规范,旨在提升软件的可移植性。
POSIX,全称为Portable Operating System Interface
,即“可移植操作系统接口”,是由IEEE计算机协会制定的标准。它的主要目的是定义一套操作系统应该遵循的规则和接口,以保证在不同的操作系统之间能够运行相同的程序,而无需或只需很少的修改。
POSIX标准涵盖了很多方面,包括系统调用、命令行 shell、文件系统接口、线程等。这些标准允许开发者编写的程序能够在支持POSIX的任何操作系统上运行,从而提高了软件的可移植性和互操作性。
总的来说,POSIX标准是Unix和类Unix系统(如Linux、MacOS)设计的核心部分,它通过提供一致的编程接口,帮助确保应用程序的可移植性和兼容性。
注意标准和实现的区别。POSIX标准主要由C语言实现,而C语言标准则主要关注语言本身的特性。
1.2 Pthreads
POSIX线程(英语:POSIX Threads,常被缩写为pthreads
)是POSIX的线程标准,定义了创建和操纵线程的一套API。
实现POSIX线程标准的库常被称作pthreads
,一般用于Unix-like POSIX系统,如Linux、 Solaris。但是Microsoft Windows上的实现也存在,例如直接使用Windows API实现的第三方库pthreads-w32;而利用Windows的SFU/SUA子系统,则可以使用微软提供的一部分原生POSIX API。
Pthreads定义了一套C语言的类型、函数与常量,它以pthread.h
头文件和一个线程库实现。
Pthreads API中大致共有100个函数调用,全都以"pthread_
"开头,并可以分为四类:
- 线程管理,例如创建线程,等待(join)线程,查询线程状态等。
- 互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作
- 条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作
- 使用了互斥锁的线程间的同步管理
POSIX的Semaphore API(包含在semaphore.h
中)可以和Pthreads协同工作,但这并不是Pthreads
的标准。因而这部分API是以"sem_
“打头,而非"pthread_”。
1.2 数据类型、函数、宏
1.21 数据类型
-
pthread_t
:线程句柄(Windows叫句柄,Linux叫标识符,知道即可)。它是一个结构体数据类型,用于唯一标识一个线程。出于移植目的,不能把它作为整数处理,应使用函数pthread_equal()对两个线程ID进行比较。获取自身所在线程id使用函数pthread_self()。 -
pthread_attr_t
:线程属性。主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。主要属性的意义如下:- __detachstate,表示新线程是否与进程中其他线程脱离同步。如果设置为PTHREAD_CREATE_DETACHED,则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。可以在线程创建并运行以后用pthread_detach()来设置。一旦设置为PTHREAD_CREATE_DETACHED状态,不论是创建时设置还是运行时设置,则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
- __schedpolicy,表示新线程的调度策略,包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_setschedparam()来改变。
- __schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。系统支持的最大和最小的优先级值可以用函数sched_get_priority_max和sched_get_priority_min得到。
- __inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。
- __scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
-
pthread_mutex_t
:互斥锁的类型,用于保护共享资源免受多个线程的同时访问。 -
pthread_cond_t
:条件变量的类型,用于线程间的同步,允许线程等待某个特定条件的发生。。 -
pthread_rwlock_t
:读写锁的类型,允许多个线程同时读取共享资源,但只允许一个线程写入。
1.22 函数
线程操纵函数(简介起见,省略参数):
pthread_create()
:创建一个线程pthread_exit()
:终止当前线程pthread_cancel()
:请求中断另外一个线程的运行。被请求中断的线程会继续运行,直至到达某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。POSIX 的取消类型(Cancellation Type)有两种,一种是延迟取消(PTHREAD_CANCEL_DEFERRED),这是系统默认的取消类型,即在线程到达取消点之前,不会出现真正的取消;另外一种是异步取消(PHREAD_CANCEL_ASYNCHRONOUS),使用异步取消时,线程可以在任意时间取消。系统调用的取消点实际上是函数中取消类型被修改为异步取消至修改回延迟取消的时间段。几乎可以使线程挂起的库函数都会响应CANCEL信号,终止线程,包括sleep、delay等延时函数。pthread_join()
:阻塞当前的线程,直到另外一个线程运行结束pthread_kill()
:向指定ID的线程发送一个信号,如果线程不处理该信号,则按照信号默认的行为作用于整个进程。信号值0为保留信号,作用是根据函数的返回值判断线程是不是还活着。pthread_cleanup_push()
:线程可以安排异常退出时需要调用的函数,这样的函数称为线程清理程序,线程可以创建多个清理程序。线程清理程序的入口地址使用栈保存,实行先进后处理原则。由pthread_cancel或pthread_exit引起的线程结束,会次序执行由pthread_cleanup_push压入的函数。线程函数执行return语句返回不会引起线程清理程序被执行。pthread_cleanup_pop()
:以非0参数调用时,引起当前被弹出的线程清理程序执行。pthread_setcancelstate()
:允许或禁止取消另外一个线程的运行。pthread_setcanceltype()
:设置线程的取消类型为延迟取消或异步取消。
线程属性函数:
pthread_attr_init()
:初始化线程属性变量。运行后,pthread_attr_t结构所包含的内容是操作系统支持的线程的所有属性的默认值。pthread_attr_setdetachstate()
:设置线程属性变量的detachstate属性(决定线程在终止时是否可以被joinable)pthread_attr_getdetachstate()
:获取脱离状态的属性pthread_attr_setscope()
:设置线程属性变量的__scope属性pthread_attr_setschedparam()
:设置线程属性变量的schedparam属性,即调用的优先级。pthread_attr_getschedparam()
:获取线程属性变量的schedparam属性,即调用的优先级。pthread_attr_destroy()
:删除线程的属性,用无效值覆盖
互斥锁函数:
pthread_mutex_init()
初始化互斥锁pthread_mutex_destroy()
删除互斥锁pthread_mutex_lock()
:占有互斥锁(阻塞操作)pthread_mutex_trylock()
:试图占有互斥锁(不阻塞操作)。即,当互斥锁空闲时,将占有该锁;否则,立即返回。pthread_mutex_unlock()
: 释放互斥锁pthread_mutexattr_()
: 互斥锁属性相关的函数
条件变量函数:
pthread_cond_init()
:初始化条件变量pthread_cond_destroy()
:销毁条件变量pthread_cond_signal()
: 发送一个信号给正在当前条件变量的线程队列中处于阻塞等待状态的线程,使其脱离阻塞状态,唤醒后继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。一般只给一个阻塞状态的线程发信号。假如有多个线程正在阻塞等待当前条件变量,则根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但pthread_cond_signal在多处理器上可能同时唤醒多个线程,当只能让一个被唤醒的线程处理某个任务时,其它被唤醒的线程就需要继续wait。POSIX规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,有些实现为了简便,在单处理器上也会唤醒多个线程。所以最好对pthread_cond_wait()使用while循环对条件变量是否满足做条件判断。pthread_cond_wait()
: 等待条件变量的特殊条件发生;pthread_cond_wait() 必须与一个pthread_mutex配套使用。该函数调用实际上依次做了3件事:对当前pthread_mutex解锁、把当前线程挂起到当前条件变量的线程队列、被其它线程的信号唤醒后对当前pthread_mutex申请加锁。如果线程收到一个信号被唤醒,将被配套的互斥锁重新锁住,pthread_cond_wait() 函数将不返回直到线程获得配套的互斥锁。需要注意的是,一个条件变量不应该与多个互斥锁配套使用。pthread_cond_broadcast()
: 某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.pthread_condattr_()
: 条件变量属性相关的函数
线程私有存储(Thread-local storage):
pthread_key_create()
: 分配用于标识进程中线程特定数据的pthread_key_t类型的键pthread_key_delete()
: 销毁现有线程特定数据键pthread_setspecific()
: 为指定线程的特定数据键设置绑定的值pthread_getspecific()
: 获取调用线程的键绑定值,并将该绑定存储在 value 指向的位置中
同步屏障函数
pthread_barrier_init()
: 同步屏障初始化pthread_barrier_wait()
:pthread_barrier_destory()
:
其它多线程同步函数:
pthread_rwlock_*()
: 读写锁
工具函数:
pthread_equal()
: 对两个线程的线程标识号进行比较pthread_detach()
: 分离线程pthread_self()
: 查询线程自身线程标识号pthread_once()
: 某些需要仅执行一次的函数。其中第一个参数为pthread_once_t类型,是内部实现的互斥锁,保证在程序全局仅执行一次。
信号量函数,包含在semaphore.h
中:
sem_init
:用于初始化一个无名信号量。它需要三个参数:一个指向信号量对象的指针、一个表示信号量是否在多个进程间共享的标志,以及信号量的初始值。sem_post
:用于增加信号量的值,通常在释放资源时调用。它需要一个指向信号量对象的指针作为参数。sem_wait
:用于减少信号量的值,通常在请求资源时调用。如果信号量的值为0,调用此函数的线程将会被阻塞,直到信号量的值大于0。sem_trywait
:与sem_wait
类似,但它是非阻塞的。如果信号量的值为0,它不会阻塞线程,而是立即返回。sem_getvalue
:用于获取信号量的当前值。它需要一个指向信号量对象的指针和一个用于存储信号量值的指针作为参数。sem_destroy
:用于销毁一个无名信号量。它需要一个指向信号量对象的指针作为参数。
共享内存函数,包含在sys/mman.h
中,链接时使用rt库:
- mmap:把一个文件或一个POSIX共享内存区对象映射到调用进程的地址空间。使用该函数的目的: 1.使用普通文件以提供内存映射I/O 2.使用特殊文件以提供匿名内存映射。 3.使用shm_open以提供无亲缘关系进程间的Posix共享内存区。
- munmap: 删除一个映射关系
- msync:文件与内存同步函数
- shm_open:创建或打开共享内存区
- shm_unlink:删除一个共享内存区对象的名字,删除一个名字仅仅防止后续的open,msq_open或sem_open调用获取成功。
- ftruncate:调整文件或共享内存区大小
- fstat来获取有关该对象的信息
1.23 宏
- PTHREAD_CREATE_JOINABLE:创建的线程可以被其他线程回收资源。
- PTHREAD_CREATE_DETACHED:创建的线程是分离的,不需要其他线程回收资源。
- PTHREAD_CANCEL_ENABLE:允许取消。
- PTHREAD_CANCEL_DISABLE:禁止取消。
- PTHREAD_PROCESS_PRIVATE:线程特定数据只在创建它的进程内可见。
- PTHREAD_PROCESS_SHARED:线程特定数据可以在不同进程间共享。
- PTHREAD_MUTEX_INITIALIZER:静态初始化互斥锁。
- PTHREAD_COND_INITIALIZER:静态初始化条件变量。
二、创建线程
(1)定义线程函数:
线程函数是线程执行的入口点,它的定义形式如下:
void *thread_function(void *arg)
{
// 线程执行的代码
return NULL;
}
这个函数的参数和返回值类型都必须是void*
。
(2)创建线程:
使用pthread_create
函数创建线程,函数原型如下:
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
参数说明:
- thread:指向线程标识符的指针,用于存储新创建的线程ID。
- attr:指向线程属性的指针,用于设置线程的属性,如堆栈大小、调度策略等。如果设置为NULL,则使用默认属性。
- start_routine:线程函数的起始地址。
- arg:传递给线程函数的参数。
(3)等待线程结束:
使用pthread_join
函数等待线程结束,函数原型如下:
int pthread_join(pthread_t thread, void **retval);
参数说明:
- thread:要等待的线程ID。
- retval:用于存储线程函数的返回值。
例:
pthread_t tid; // 线程ID
pthread_create(&tid, NULL, myThreadFunction, NULL);
三、线程同步
前一篇文章讲了线程同步的方式。这里以互斥锁为例。
互斥锁(Mutex):互斥锁是一种用于保护共享资源的机制,确保同一时刻只有一个线程可以访问共享资源。pthread库提供了pthread_mutex_t类型的互斥锁。
- 初始化互斥锁:使用pthread_mutex_init函数初始化互斥锁。
- 锁定互斥锁:使用pthread_mutex_lock函数锁定互斥锁。
- 解锁互斥锁:使用pthread_mutex_unlock函数解锁互斥锁。
- 销毁互斥锁:使用pthread_mutex_destroy函数销毁互斥锁。
(1)定义和初始化互斥锁:
pthread_mutex_t mutex; // 定义互斥锁变量
- 静态初始化:使用
PTHREAD_MUTEX_INITIALIZER
宏,这种方式初始化的互斥锁是静态的,不能销毁。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化
- 动态初始化:使用
pthread_mutex_init()
函数动态地初始化互斥锁,这种方式可以在需要的时候进行销毁。
pthread_mutex_t mutex; // 动态初始化
pthread_mutex_init(&mutex, NULL); // 动态初始化互斥锁
如果需要设置互斥锁的属性,可以创建一个 pthread_mutexattr_t 对象,设置属性,然后将其传递给 pthread_mutex_init() 函数的第二个参数。
(2) 在需要保护的临界区域加锁:
pthread_mutex_lock(&mutex);
// 访问共享资源的代码
(3)在使用完共享资源后解锁:
pthread_mutex_unlock(&mutex);
四、线程销毁
pthread_join(tid, NULL);
在 POSIX 线程(pthreads)编程中,销毁线程实际上是由操作系统自动完成的,当线程的运行终止时,它会被系统自动清理。
-
线程函数返回:线程函数执行完毕后,会返回一个值(通过
pthread_exit
或从线程函数返回),这会导致线程的终止。任何线程都能调用pthread_exit
来结束自己的执行。 -
主线程等待子线程结束:使用
pthread_join
函数,主线程可以阻塞等待特定子线程结束。如果子线程已经结束,它的资源被释放,此时pthread_join
返回。如果子线程还没有结束,主线程会被放入睡眠状态,直到子线程结束为止。 -
分离状态(Detach):如果不想使用
pthread_join
来等待子线程结束,你可以调用pthread_detach
函数将线程置于分离状态。一旦线程处于分离状态,它将在终止时自动释放其资源,无需主线程显式等待。 -
取消线程:可以使用
pthread_cancel
函数来请求取消另一个线程的执行。被取消的线程可以选择清理并自行终止,或者立即终止,具体行为取决于该线程对取消状态的处理策略。 -
线程清理处理:当线程被取消或正常结束时,可以设置清理处理函数,这些函数会在线程退出前被调用,以执行必要的清理工作。可以使用
pthread_cleanup_push
注册这样的清理函数,并用pthread_cleanup_pop
来配对它们。 -
线程资源的释放:线程结束时,它所占用的所有用户级资源应该已经被释放,例如动态分配的内存等。但是,线程使用的系统资源(如文件描述符、互斥锁等)通常由操作系统在线程结束后自动释放。
-
避免僵尸线程:在多线程程序中,确保所有线程都已经结束才让主线程退出是很重要的。否则,未被加入(joined)或分离(detached)的线程可能会变成所谓的“僵尸线程”,它们的进程状态不会释放,直到父进程结束。
-
错误检查:对于线程相关的函数调用(如
pthread_create
,pthread_join
,pthread_detach
等),应始终检查它们的返回值以确保没有发生错误。
五、示例
注意编译时指定链接库 -lpthread
。
5.1 完整示例
2个线程分别打印当前时间:线程创建后即运行,所以创建时间不同。
#include <stdio.h>
#include <unistd.h>
#include<pthread.h>
#include <sys/time.h>
#include<time.h>
void *print_time(void *arg){
struct timeval current_time;
if(gettimeofday(¤t_time, NULL)==0){
char *p = ctime(¤t_time.tv_sec);
printf("Current time is %s.\n",p);
}
else{
return (void *)1;
}
return (void *)0;
}
int main(){
pthread_t tid1,tid2;
int ret1,ret2;
ret1=pthread_create(&tid1, NULL,print_time,(void *)1);
sleep(2);
ret2=pthread_create(&tid2, NULL,print_time,(void *)2);
if(ret1||ret2){
printf("Creat threads ailed.\n");
return 1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
return 0;
}
5.2 信号量示例
现在修改一下,线程只有获得信号量的时候才能打印时间。
- sem_init(&semaphore, 0, 1);:初始化信号量为 1,表示初始时可以访问临界区。
- sem_wait(&semaphore);:在访问临界区之前等待信号量,如果为 0 则阻塞,直到变为大于 0 才继续执行。
- sem_post(&semaphore);:在临界区访问结束后增加信号量,唤醒其他等待的线程。
信号量:
- 信号量的作用
-信号量是一种计数器,用于控制多个线程对共享资源的访问。- 当一个线程想要访问共享资源时,首先要等待信号量。
- 如果信号量的值大于 0,表示资源空闲,线程可以继续执行,并将信号量减 1。
- 如果信号量的值为 0,表示资源已被占用,线程会被阻塞,直到信号量的值变为大于 0。
- 初始化:
int sem_init(sem_t *restrict sem, int pshared, unsigned int value);
- sem: 指向要初始化的信号量的指针。
- pshared: 如果是0,则信号量是进程内共享的;如果是非0值,则用于进程间共享(但很少使用,通常不推荐,因为这涉及到进程间共享内存的复杂性)。
- value: 信号量的初始值。
- P、V 操作:
- P操作 (
sem_wait
或 sem_trywait): 减少信号量的值。如果信号量的值在操作之前是正的,它就简单地减一。如果信号量的值在操作之前是零,线程将被阻塞,直到信号量的值变成正数,然后它才减一。 - V操作 (
sem_post
): 增加信号量的值。这通常用来表示一个线程已经释放了它所持有的资源。
- P操作 (
- 信号量分类:
- 二值信号量:通常用于实现互斥(Mutual Exclusion),确保在任何时刻只有一个线程或进程可以访问关键部分代码或资源。它只有两个值,通常是0(表示资源不可用)和1(表示资源可用)。当一个线程尝试获取值为0的信号量时,它将被阻塞直到信号量的值变为1。
- 计数信号量:是更一般化的同步机制,允许一定数量的线程同时访问某个资源。它的值可以是任何非负整数,代表可用资源的数量。当一个线程想要访问资源时,它会执行P操作,使信号量减一;当线程释放资源时,它会执行V操作,使信号量加一。如果信号量的值小于或等于0,则到达的线程将被阻塞。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <time.h>
sem_t semaphore;
void* printTime(void* arg) {
sem_wait(&semaphore);
time_t now;
struct tm* timeinfo;
time(&now);
timeinfo = localtime(&now);
printf("Thread ID: %ld, Current Time: %s", pthread_self(), asctime(timeinfo));
sleep(2);
sem_post(&semaphore);
return NULL;
}
int main() {
pthread_t tid1, tid2;
sem_init(&semaphore, 0, 1); // 初始化信号量为 1
pthread_create(&tid1, NULL, printTime, NULL);
pthread_create(&tid2, NULL, printTime, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&semaphore);
return 0;
}
把信号量初始值改为2,则2个线程可以同时打印时间: