3 线程
约 5213 字大约 17 分钟
2025-06-15
如果一个进程的多个线程同时使用到系统的标准输入输出,那么只会有一个线程能够使用到
pthread_t定义于<pthreadtype.h>中
typedef unsigned long int pthread_t
获取线程id 返回当前线程的线程id
pthread_t pthread_self(void)
- 创建线程
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine)(void*),void* arg);
thread:线程id指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符本质上是一个unsigned long类型 attr:pthead_attr_t结构体,这个参数可以用来设置线程的属性,如优先级、栈大小等。如果不需要定制线程属性,可以传入NULL,此时线程将采用默认属性。 *void*(*start_routine)(void*):函数指针,它定义了新线程开始执行时的入口点。这个函数必须接受一个void*类型的参数,并返回void*类型的结果 arg:start_routine:传递给新线程函数的参数,可以是一个指向任意类型数据的指针 成功返回0,失败返回非0
其中传入的线程函数返回值是void*
类型的,函数参数只有一个也是void*
类型的,在函数内根据需要进行类型转换,如果需要传入多个参数,可以把它们打包成一个结构体,然后将这个结构体转成一个void*
类型当作唯一的参数传入
- 等待线程
阻塞当前线程,直到等待的线程终止 thread:指定线程ID retval:一个可选参数,用于接收线程结束后传递的返回值。如果非空,pthread_join会在成功时将线程的exit status复制到*retval所指向的内存位置如果线程没有显式地通过pthread_exit提供返回值,则该参数将被设为NULL或忽略 成功返回0,失败返回1
int pthread_join(pthread_t thread,void** retval);
- 线程分离
将子线程和主线程分离,分离后子线程由内核调度控制资源的回收。同时,父线程如果退出那么资源不会被释放,分离出去的子线程还可以继续执行。等他执行完再进行资源回收
int pthread_detach(pthread_t thread);
- 取消线程 向目标线程发送取消请求,目标线程是否和何时响应取决于它的取消状态和取消类型
取消状态(CancelabilityState):可以是enabled(默认)或disabled。如果取消状态为禁用,则取消请求会被挂起,直至线程启用取消功能。如果取消状态为启用,则线程的取消类型决定它何时取消。 取消类型(CancelabilityType):可以是 asynchrqnous(异步)或 deferred(被推迟,默认值)。 asynchronous:意味着线程可能在任何时候被取消(通常立即被取消,但系统并不保证这一点) deferred:被推迟意味着取消请求会被挂起,直至被取消的线程执行取消点(cancellationpoint)函数时才会真正执行线程的取消操作。
取消点函数:是在POSIX线程库中专门设计用于检查和处理取消请求的函数。当被取消的线程执行这些函数时,如果线程的取消状态是enabled且类型是deferred,则它会立即响应取消请求并终止执行。man 7 pthreads可以看到取消点函数列表。
取消的执行
- 同步取消 在子线程需要取消的地方设置一个取消点函数,父线程里面在需要取消子进程的地方调用
pthread_cancel
当子线程执行到取消点函数的时候就会被取消。 - 异步取消 而异步取消是不需要取消点函数的,当什么时候调用取消函数时就什么时候取消,而且线程在执行的时候执行的顺序和时间也是不一样的,也就是说异步取消的时机是不固定的
- 不取消 设置了不取消状态后,即使子进程中有取消点函数,父进程也调用了取消函数但子进程依旧会继续执行不会取消
成功返回0,失败返回非零的错误码 大多数情况下都是可以取消成功的,取消请求发送后返回0表示函数已经执行成功,但目标进程可能并没有取消,因为发送请求是异步的
int pthread_cancel(pthread_t thread);
取消操作和pthread_cancel函数的调用是异步的,这个函数的返回值只能告诉调用者取消请求是否成功发送。当线程被成功取消后,通过pthread_join和线程关联将会获得PTHREAD_CANCELED作为返回信息,这是判断取消是否完成的唯一方式
设置线程取消状态 PTHREAD_CANCEL_ENABLE:启用取消功能 PTHREAD_CANCEL_DISABLE:禁用取消功能 state目标状态 oldstate指针,用于返回历史状态 成功返回0,失败返回非零错误码
int pthread_setcancelstate(int state,int* oldstate);
设置线程取消类型 PTHREAD_CANCEL_DEFERRED:设置取消类型为推迟PTHREAD_CANCEL_ASYNCHRONOUS:设置取消类型为异步 type目标类型 oldtype指针,用于接收历史类型 成功返回0,失败返回非零错误码
int pthread_setcanceltype(int type,int* oldtype);
- 关闭线程 当前线程调用关闭自己 结束关闭调用该方法的线程,并返回一个内存指针用于存放结果 retval:将执行的结果返回给父线程,需要调用pthread_join获取 即传递给父进程的
pthread_join
函数中的retval的参数中了,结果可以是任意类型的,但传递给retval时必须转成void*
类型,然后父线程接收到后再转换回来
void pthread_exit(void* retval);
线程同步
锁机制
<pthread.h> 锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。包括上面的互斥锁在内,常见的锁机制共有三种:
- 互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码
- 读写锁(Reader/WriterLocks):允许多个读者同时读共享数据,但写者的访问是互斥的。
- 自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景。
互斥锁
pthread mutex_t是一个定义在头文件<pthreadtypes.h>中的联合体类型的别名,其声明如下。
typedef union {
struct __pthread_mutex_s __data;
char size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
}pthread_mutex_t;
- 初始化锁 静态初始化可以不用摧毁锁,程序结束系统自动回收,但动态初始化的锁需要手动摧毁
静态初始化 PTHREAD_MUTEX_INITIALIZER是POSIX线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被锁定和解锁,而不需要在程序运行时显式调用初始化函数。 示例:
static pthread_mutex_t counter_mutex =PTHREAD_MUTEX_INITIALIZER;
动态初始化 return: 成功返回0,失败返回错误码
int pthread_mutex_init (pthread_mutex_t *restrict __mutex,const pthread_mutexattr_t *restrict __mutexattr)
- 加锁 阻塞式加锁,如果此时锁被占则阻塞 mutex:用于存储获取的锁 返回获取锁结果成功为0,失败为非0
int pthread_mutex_lock(pthread_mutex_t* mutex);
非阻塞式加锁,如果此时锁被占立即返回而不是阻塞 成功为0,失败:如果锁此时被占则返回EBUSY
int pthread_mutex_trylock(pthread_mutex_t* mutex);
- 解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);
- 释放锁 返回释放锁的结果成功为0,失败为非0
int pthread_mutex_destroy (pthread_mutex_t *__mutex)
读写锁
对于读者同一时间可以由多个读者获取读锁 对于写者,只能有一个写者获取写锁,只有它释放之后其他写者才可继续获取 当写者获取锁时,如果有读者在读写者也是不能获取锁的 同理,写者写的时候读者也是不能获取锁的
typedef union {
struct __pthread_rwlock_arch_t __data;
char __size[__SIZEOF_PTHREAD_RWLOCK_T];
long int __align;
}pthread_rwlock_t;
读写锁函数和互斥锁是类似的,只不过名字不同为rwlock
- 初始化
静态初始化 为rwlock指向的读写锁分配所有需要的资源,并将锁初始化为未锁定状态。读写锁的属性由attr参数指定,如果attr为NULL则使用默认属性当锁的属性为默认时,可以通过宏PTHREAD_RWLOCK_INITIALIZER初始化,即pthread_rwlock_trwlock=PTHREAD_RWLoCK_INITIALIZER
效果和调用当前方法并为attr传入NULL是一样的
动态初始化 rwlock读写锁 attr读写锁的属性 成功则返回0,否则返回错误码
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
- 加锁 加读锁 应用一个读锁到rwlock指向的读写锁上,并使调用线程获得读锁。如果写线程持有锁,调用线程无法获得读锁,它会阻塞直至获得锁。 成功返回0,失败返回错误码
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
加写锁 应用一个写锁到rwlock指向的读写锁上,并使调用线程获得写锁。只要任意线程持有读写锁,则调用线程无法获得写锁,它将阻塞直至获得写锁。 成功返回0,失败返回错误码
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
- 摧毁锁 销毁rwlock指向的读写锁对象,并释放它使用的所有资源。当任何线程持有锁的时候销毁锁,或尝试销毁一个未初始化的锁,结果是未定义的。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
写饥饿
如果一个写者想要写数据,而此时有读者正在读,但写者获取不了读写锁,而后又来了许多读者它们可以获取读写锁从而继续读,这就是写饥饿
可以通过修改读写锁的属性pthread_rwlockattr_t更改读写的优先级 默认情况下是读优先
typedef union{
char __size[__SIZEOF_PTHREAD_RWLOCKATTR_T];
long int __align;
}pthread_rwlockattr_t;
将attr指向的属性对象中的"锁类型"属性设置为pref规定的值 pref有以下取值:
- PTHREAD_RWLOCK_PREFER_READER_NP:默认值,读线程拥有更高优先级。当存在阻塞的写线程时,读线程仍然可以获得读写锁。只要不断有新的读线程,写线程将一直保持饥饿"。
- PTHREAD_RWLOCK_PREFER_WRITER_NP:写线程拥有更高优先级。这一选项被 glibc忽略。
- PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:写线程拥有更高优先级,在当前系统环境下,它是有效的,将锁类型设置为该值以避免写饥饿。
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr,int pref);
自旋锁
在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。 自旋锁相对于其他锁的优点是它们在锁被占用时会持续检查锁的状态(即“自旋”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免了线程上下文切换的开销。自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用。
restrict关键字
restrict是一个C99标准引入的关键字,用于修饰指针,它的作用是告诉编译器,被修饰的指针是编译器所知的唯一一个可以在其作用域内用来访问指针所指向的对象的方法。 这样一来,编译器可以放心地执行代码优化,因为不存在其他的别名(即其他指向同一内存区域的指针)会影响到这块内存的状态。
[!warning] Title 如果定义的变量有多个指针指向它而还使用restrict关键字,这样会引发未知的错误
函数参数使用restrict修饰,相当于约定:函数执行期间,该参数指向的内存区域不会被其它指针修改。
typedef union{
struct __pthread__cond_s __data;
char __size[__SIZEOF_PTHREAD_COND_T];
__extension__ long long int __align;
}pthread_cond_t;
条件变量
条件变量的主要作用不是处理线程同步, 而是进行线程的阻塞。
如果在多线程程序中只使用条件变量无法实现线程的同步, 必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的 假设有A-Z 26个线程,这26个线程共同访问同一把互斥锁,如果线程A加锁成功,那么其余B-Z线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。 一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为pthread_cond_t
被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用。
- 初始化
- cond: 条件变量的地址
- attr: 条件变量属性, 一般使用默认属性, 指定为NULL
- return: 成功返回0, 失败返回错误码
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
- 阻塞线程 线程调用此函数阻塞自身 这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现共享资源的数据混乱。 在阻塞线程时候,如果线程已经对互斥锁
mutex
上锁,那么会将这把锁打开,这样做是为了避免死锁 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex
互斥锁锁上,继续向下访问临界区
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
struct timespec
struct timespec {
time_t tv_sec; //秒
long tv_nsec; //纳秒
};
- 唤醒线程
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
当唤醒后还会对线程进行判定,如果不满足条件还是会阻塞到pthread_cond_wait
的位置
- 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
示例: 生产者生产商品,消费者消费商品,只要链表为空就阻塞消费者线程,该消费者线程阻塞的前已经上锁了,所以后续的消费者线程也会因为互斥锁而阻塞;当然也会阻塞生产者,但消费者调用的pthread_cond_wait
函数中会把已获取的互斥锁解锁;解锁后若消费者线程获取到互斥锁又会阻塞,直到生产者线程获取到互斥锁开始生产所以生产者线程能够继续获取互斥锁。生产者线程生产之后解锁互斥锁发送信号取消消费者线程的阻塞
消费者里面对链表为空的判断不能改为if,因为当阻塞的消费者线程收到信号后会从pthread_cond_wait
位置继续执行也就是再走一次while循环判断链表是否为空。若改成if循环,其他消费者消费后链表又为空时,由于被唤醒的消费者线程已经过了if判断所以会直接开始消费,但此时链表已经为空了。
此时是假设生产者的生产没有限制,若生产者也有限制时就需要再创建一个条件变量,不同的条件使用不同的条件变量,生产者给消费者的条件变量发信号解锁消费者,消费者给生产者的条件变量发送信号解锁生产者。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
// 链表
typedef struct Node{
int number;
struct Node* next;
}Node;
Node* head=NULL;
// 生产者
void* produce(void* arg){
while (1)
{
pthread_mutex_lock(&mutex);
Node* newNode=(Node*)malloc(sizeof(Node));
newNode->number=rand()%100;
newNode->next=head;
head=newNode;
printf("produce:%ld,number:%d\n",pthread_self(),newNode->number);
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&cond);
usleep(rand()%3);
}
return NULL;
}
// 消费者
void* consume(void* arg){
while (1)
{
pthread_mutex_lock(&mutex);
while (head==NULL)
{
// 阻塞消费者线程
pthread_cond_wait(&cond, &mutex);
}
Node* temp=head;
head=temp->next;
printf("consume:%ld,number:%d\n",pthread_self(),temp->number);
free(temp);
pthread_mutex_unlock(&mutex);
usleep(rand()%3);
}
}
int main(){
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t producer[5],consumer[5];
for(int i=0;i<5;++i){
pthread_create(&producer[i], NULL, (void*)produce, NULL);
pthread_create(&consumer[i], NULL, (void*)consume, NULL);
pthread_join(producer[i], NULL);
pthread_join(consumer[i], NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
信号量
<semaphore.h> 不仅可以解决不同线程之间的同步问题还可以解决不同进程之间的同步问题
相比于条件变量更加简便
sem_t sem;
- 初始化信号量
- sem:信号量变量地址
- pshared:
- 0:线程同步
- 非0:进程同步
- value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞了
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 阻塞
sem_wait
:函数被调用,sem中的资源就会被消耗1个, 资源数-1 当线程调用这个函数,并且sem
中的资源数>0
,线程不会阻塞,线程会占用sem
中的一个资源,因此资源数-1,直到sem
中的资源数减为0
时,资源被耗尽,因此线程也就被阻塞了。sem_trywait
:当线程调用这个函数,并且sem
中的资源数>0
,线程不会阻塞,线程会占用sem
中的一个资源,因此资源数-1,直到sem
中的资源数减为0
时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
int sem_wait(sem_t *sem);
//当资源数为0时不阻塞
int sem_trywait(sem_t *sem);
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 解除阻塞 调用该函数给sem中的资源数+1,如果有线程在调用
sem_wait
、sem_trywait
、sem_timedwait
时因为sem
中的资源数为0
被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
int sem_post(sem_t *sem);
- 释放资源
int sem_destroy(sem_t *sem);
获取资源数 查看sem
中现在拥有的资源个数,通过第参数sval
将数据传出 返回值返回资源数,和参数sval一样。
int sem_getvalue(sem_t *sem, int *sval);
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>
#include<semaphore.h>
sem_t semproduce,semconsume;
// 链表
typedef struct Node{
int number;
struct Node* next;
}Node;
Node* head=NULL;
// 生产者
void* produce(void* arg){
while (1)
{
sem_wait(&semproduce);
Node* newNode=(Node*)malloc(sizeof(Node));
newNode->number=rand()%100;
newNode->next=head;
head=newNode;
printf("produce:%ld,number:%d\n",pthread_self(),newNode->number);
sem_post(&semconsume);
usleep(rand()%3);
}
return NULL;
}
// 消费者
void* consume(void* arg){
while (1)
{
sem_wait(&semconsume);
Node* temp=head;
head=temp->next;
printf("consume:%ld,number:%d\n",pthread_self(),temp->number);
free(temp);
sem_post(&semproduce);
usleep(rand()%3);
}
return NULL;
}
int main(){
sem_init(&semproduce, 0, 1);
sem_init(&semconsume, 0, 0);
pthread_t producer[5],consumer[5];
for(int i=0;i<5;++i){
pthread_create(&producer[i], NULL, (void*)produce, NULL);
pthread_create(&consumer[i], NULL, (void*)consume, NULL);
pthread_join(producer[i], NULL);
pthread_join(consumer[i], NULL);
}
sem_destroy(&semproduce);
sem_destroy(&semconsume);
return 0;
}
当总资源量为1时可以不使用互斥锁,这种情况下信号量也能完成线程同步。 但当总资源量不为1时就需要使用互斥锁来实现线程同步了 对于消费者和生产者加锁和解锁都要在信号量wait和post之间进行,如果加锁在了信号量的wait之前,若当时资源数为0,线程就会被阻塞在wait位置也就不能继续向下执行,而不能向下执行就无法给生产者(消费者)发送post信号量导致所有线程一直阻塞。如果加锁在wait之后,若资源数为0就阻塞在wait位置上等待拥有资源后可继续执行,有资源mutex自然能够顺利执行也就不会导致死锁
贡献者
版权所有
版权归属:PinkDopeyBug