C++网络编程入门学习(三)-- 多线程的创建与同步
- 多线程的创建与同步
- 线程的创建
- 线程退出函数
- 线程回收函数
- 线程分离
- 线程同步
- 线程同步的方式
- 互斥锁
- 互斥锁的相关函数
- 互斥锁的使用示例代码
- 死锁
- 死锁的四个必要条件
- 造成死锁的场景
- 避免死锁的方法
- 读写锁
- 读写锁的使用示例
- 条件变量
- 1. 条件变量的概念
- 2. 条件变量的工作原理
- 3. 常用函数
- 信号量
多线程的创建与同步
线程的创建
示例代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void* callback(void* arg) {
for(int i = 0; i < 5; ++i) {
printf("subthread: i = %d\n", i);
}
printf("subthread: %ld\n", pthread_self());
return NULL;
}
int main() {
pthread_t tid; // 定义一个pthread_t类型的变量来存储线程ID
pthread_create(&tid, NULL, callback, NULL); // 创建子线程
for(int i = 0; i < 5; ++i) {
printf("main-thread: i = %d\n", i);
}
printf("main-thread: %ld\n", pthread_self()); // 打印主线程的ID
sleep(3); // 为了保证子线程能够执行,防止主线程抢占时间片执行完后销毁,导致子线程无法执行
return 0;
}
pthread_create
函数的四个参数讲解:
pthread_t *thread
:- 这是一个指向
pthread_t
类型的指针,用于存储新创建线程的ID。通过这个ID,你可以后续对该线程进行操作(如等待线程结束、取消线程等)。
- 这是一个指向
const pthread_attr_t *attr
:- 这个参数用来设置线程的属性。如果传入
NULL
,线程将使用默认属性(如默认栈大小、调度策略等)。如果你想设置特定的线程属性,可以创建一个pthread_attr_t
对象并传入。
- 这个参数用来设置线程的属性。如果传入
void *(*start_routine)(void *)
:- 这是一个指向函数的指针,表示线程执行的起始函数。该函数的返回类型必须是
void*
,并且只能接受一个void*
类型的参数。在线程执行完毕后,可以通过该函数返回的指针来获取结果。
- 这是一个指向函数的指针,表示线程执行的起始函数。该函数的返回类型必须是
void *arg
:- 这个参数是传递给线程起始函数的参数。你可以将任何数据结构的指针传递给该参数。在线程内部,你需要将其转换回原始类型来使用。在这个例子中,传入的是
NULL
,表示不需要传递任何参数。
- 这个参数是传递给线程起始函数的参数。你可以将任何数据结构的指针传递给该参数。在线程内部,你需要将其转换回原始类型来使用。在这个例子中,传入的是
线程退出函数
pthread_exit
函数
定义:
pthread_exit
是一个用于显式终止调用线程的函数,其原型如下:
void pthread_exit(void *retval);
参数:
retval
:传递给等待该线程结束的其他线程的返回值,可以是任何类型的指针(如void*
)。
特性:
- 当调用
pthread_exit
时,线程会立即停止执行,释放它所占用的资源(如线程栈),并将retval
返回给其他线程。 - 其他线程可以通过
pthread_join
函数来获取该返回值。 - 调用
pthread_exit
不会影响其他线程的运行,程序会继续运行。
线程回收函数
pthread_join
是 POSIX 线程库中的一个函数,主要用于使调用线程(通常是主线程)等待指定的子线程终止。通过这个函数,主线程可以同步等待子线程的完成并获得子线程的返回值。它通常用于线程间的协调,以确保主线程不会在子线程执行完毕之前退出。
int pthread_join(pthread_t thread, void **retval);
参数解析:
pthread_t thread
:- 线程标识符,用于指定要等待的线程。它通常是在线程创建时,通过
pthread_create
返回的pthread_t
类型变量。
- 线程标识符,用于指定要等待的线程。它通常是在线程创建时,通过
void **retval
:- 这是一个二级指针,用于接收线程的退出状态(即子线程的返回值)。如果你不关心线程的返回值,可以将其设置为
NULL
。 - 子线程可以通过
pthread_exit
或return
返回某个值,该值会被存储在retval
指向的指针中。
- 这是一个二级指针,用于接收线程的退出状态(即子线程的返回值)。如果你不关心线程的返回值,可以将其设置为
返回值:
- 成功时返回
0
。 - 失败时返回错误码,常见错误包括:
ESRCH
: 指定的线程不存在。EINVAL
: 线程不可被等待,或者线程已经处于分离状态(detached)。EDEADLK
: 导致死锁的情况,例如一个线程试图等待自己。
pthread_join
的主要功能:
- 等待子线程结束:
- 主线程调用
pthread_join
后,会阻塞执行,直到指定的子线程结束。它确保主线程不会在子线程结束之前退出或执行其他操作。
- 主线程调用
- 获取子线程的返回值:
- 通过
pthread_join
的第二个参数retval
,主线程可以获得子线程通过pthread_exit
或return
返回的结果。这对于需要从子线程获取处理结果的场景非常有用。
- 通过
- 防止僵尸线程:
- 如果主线程不等待子线程结束而直接退出,子线程可能成为“僵尸线程”,即其资源没有被释放。使用
pthread_join
可以确保子线程终止后其资源被回收,避免资源泄漏。
- 如果主线程不等待子线程结束而直接退出,子线程可能成为“僵尸线程”,即其资源没有被释放。使用
示例代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
// 定义一个结构体类型Test,包含两个整数成员
struct Test {
int num;
int age;
};
// 子线程的回调函数,接受一个void*类型的参数
void* callback(void* arg) {
for(int i = 0; i < 5; ++i) {
printf("subthread: i = %d\n", i);
}
printf("subthread: %ld\n", pthread_self());
// 将传入的参数arg转换为Test结构体指针类型
struct Test *t = (struct Test*)arg;
// 修改结构体中的成员值
t->num = 100;
t->age = 6;
pthread_exit(t); // 通过pthread_exit函数将结构体指针t作为线程的返回值
return NULL;
}
int main() {
pthread_t tid; // 定义一个pthread_t类型变量来存储线程ID
struct Test t; // 定义一个Test结构体变量t,用于传递给子线程
// 创建子线程,执行callback函数,并将结构体t的地址作为参数传递给子线程
pthread_create(&tid, NULL, callback, &t);
// 打印主线程的线程ID
printf("main-thread: %ld\n", pthread_self());
void *ptr; // 定义一个void*类型指针,用于接收子线程的返回值
pthread_join(tid, &ptr); // 主线程等待子线程结束,并通过ptr获取子线程返回的值(t的地址)
// 打印主线程中结构体t的成员值,注意此时t的值已经被子线程修改
printf("num = %d, age = %d\n", t.num, t.age);
return 0;
}
线程分离
pthread_detach
函数用于将一个线程的状态设置为分离状态,使得该线程在终止时可以自动释放所有与其相关的资源,而不需要其他线程调用 pthread_join
来显式回收它的资源。
pthread_detach
函数的原型
int pthread_detach(pthread_t thread);
thread
:要分离的线程的线程 ID。- 返回值:如果成功,返回 0;如果失败,返回一个错误码。
pthread_detach
的用途
当你确定不需要主线程或者其他线程通过 pthread_join
来等待某个线程结束时,可以使用 pthread_detach
将该线程分离。这种操作对于那些不需要返回结果的线程尤其有用,因为它避免了内存泄漏的问题——线程终止后,操作系统会自动回收它的资源。
线程同步
假设有4个线程A、B、C、D,当前一个线程A对内存中的共享资源进行访问的时候,其他线程B, C, D都不可以对这块内存进行操作,直到线程A对这块内存访问完毕为止,B,C,D中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
线程同步的方式
- 互斥锁(Mutex):
- 定义:互斥锁是一种常用的同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问该资源。
- 使用:通过
pthread_mutex_lock
加锁,pthread_mutex_unlock
解锁。只有持有锁的线程才能访问临界区。
- 条件变量(Condition Variable):
- 定义:条件变量是一种同步机制,允许线程在某些条件满足时被唤醒。
- 使用:结合互斥锁使用,通过
pthread_cond_wait
阻塞线程,pthread_cond_signal
或pthread_cond_broadcast
唤醒一个或多个等待的线程。
- 读写锁(Read-Write Lock):
- 定义:读写锁允许多个线程同时读取资源,但在写入时,必须独占该资源。
- 使用:通过
pthread_rwlock_rdlock
获取读锁,pthread_rwlock_wrlock
获取写锁,pthread_rwlock_unlock
解锁。
- 信号量(Semaphore):
- 定义:信号量是一种计数器,用于控制对共享资源的访问。
- 使用:信号量的值表示可以访问资源的线程数量。使用
sem_wait
减少信号量值,使用sem_post
增加信号量值。
互斥锁
互斥锁(mutex)是一种同步原语,用于控制对共享资源的访问,确保在任何时刻只有一个线程可以访问该资源,以防止数据竞争和不一致性。互斥锁是多线程编程中非常重要的工具。
互斥锁的基本概念
- 锁定和解锁:
- 线程在访问共享资源之前会请求互斥锁,成功获取后才能继续执行。
- 访问完共享资源后,线程必须释放互斥锁,以便其他线程可以获得访问权限。
- 避免竞争条件:
- 通过互斥锁,可以防止多个线程同时修改共享资源,从而避免数据损坏或不一致的情况。
- 死锁风险:
- 如果多个线程相互等待对方释放锁,可能会导致死锁,因此需要小心设计互斥锁的使用。
pthread_mutex_t
类型
pthread_mutex_t
是 POSIX 线程库中用于定义互斥锁的类型。它是一个结构体,用于存储锁的状态和相关信息。
在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。
互斥锁的相关函数
-
初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数说明
pthread_mutex_t \*mutex
:- 指向要初始化的互斥锁的指针。
- 在调用此函数之前,该互斥锁必须已经被声明(通常是定义为
pthread_mutex_t
类型)。
const pthread_mutexattr_t \*attr
:- 指向互斥锁属性对象的指针,可以用来设置互斥锁的行为。
- 如果设置为
NULL
,将使用默认属性。 - 属性对象可用于控制互斥锁的类型(如普通互斥锁、递归互斥锁等),但是如果不需要特殊的锁类型,通常直接传入
NULL
。
返回值
- 返回 0 表示成功。
- 返回错误码表示初始化失败,常见的错误包括:
EINVAL
:提供的属性参数无效。ENOMEM
:内存不足,无法分配互斥锁的内部结构。
-
修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明
pthread_mutex_t *mutex
:- 指向要加锁的互斥锁的指针。
- 这个互斥锁必须已经被初始化(通常通过
pthread_mutex_init
函数)。
返回值
- 返回 0 表示成功加锁。
- 返回错误码表示加锁失败,常见的错误包括:
EINVAL
:提供的互斥锁无效,可能是未初始化或已经被销毁。EDEADLK
:死锁检测,调用线程已经持有该互斥锁,再次尝试加锁将导致死锁。
-
尝试加锁
pthread_mutex_trylock
函数用于尝试对互斥锁进行加锁,但与pthread_mutex_lock
不同的是,pthread_mutex_trylock
如果互斥锁已经被其他线程占用,则不会阻塞当前线程,而是立即返回一个错误码。int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数说明
pthread_mutex_t *mutex
:- 指向要加锁的互斥锁的指针。
- 这个互斥锁必须已经被初始化。
返回值
- 返回 0 表示成功加锁。
- 返回错误码表示加锁失败,常见的错误包括:
EINVAL
:提供的互斥锁无效,可能是未初始化或已经被销毁。EBUSY
:互斥锁当前已被其他线程占用,调用线程未能获得锁。
-
对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明
pthread_mutex_t *mutex
:- 指向要解锁的互斥锁的指针。
- 该互斥锁必须已经被初始化,并且当前线程必须持有该锁。
返回值
- 返回 0 表示成功解锁。
- 返回错误码表示解锁失败,常见的错误包括:
EINVAL
:提供的互斥锁无效,可能是未初始化或已经被销毁。EPERM
:调用线程没有持有该互斥锁,试图解锁未被自己加锁的互斥锁。
不是所有的线程都可以对互斥锁解锁,哪个线程加的锁, 哪个线程才能解锁成功。
-
释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明
pthread_mutex_t *mutex
:- 指向要销毁的互斥锁的指针。
- 该互斥锁必须已经被初始化,并且在销毁之前,所有的线程都应当释放对它的持有。
返回值
- 返回 0 表示成功销毁互斥锁。
- 返回错误码表示销毁失败,常见的错误包括:
EINVAL
:提供的互斥锁无效,可能是未初始化或已经被销毁。EBUSY
:互斥锁仍然在使用中,可能有线程仍然持有该锁。
互斥锁的使用示例代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#define MAX 100
// 全局变量
int number;
// 全局变量, 多个线程共享
pthread_mutex_t mutex; // 创建互斥锁
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// 如果线程A加锁成功, 不阻塞
// 如果B加锁成功, 线程A阻塞
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);
number = cur;
pthread_mutex_unlock(&mutex);
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
return NULL;
}
void* funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// a加锁成功, b线程访问这把锁的时候是锁定的
// 线程B先阻塞, a线程解锁之后阻塞解除
// 线程B加锁成功了
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
number = cur;
pthread_mutex_unlock(&mutex);
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
// 销毁互斥锁
// 线程销毁之后, 再去释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
死锁
死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,使得它们无法继续执行。简单来说,死锁发生时,每个线程都在等待对方释放资源,导致程序无法进行。
死锁的四个必要条件
- 互斥条件:
- 至少有一个资源是被非共享的,即每次只能被一个线程占用。
- 保持并等待条件:
- 一个线程至少持有一个资源,并等待获取其他资源。
- 不剥夺条件:
- 已分配给线程的资源在未使用完之前,不能被其他线程强制夺取。
- 循环等待条件:
- 存在一个线程等待链,形成闭环,即线程A等待线程B持有的资源,线程B等待线程C持有的资源,…,最终线程Z又等待线程A持有的资源。
造成死锁的场景
- 资源竞争:
- 两个或多个线程同时竞争多个资源。当每个线程持有一个资源并请求另一个时,可能导致死锁。
- 顺序资源请求:
- 如果线程在请求资源时总是以不同的顺序请求,可能导致死锁。例如,线程1请求资源A和B,而线程2请求资源B和A。
- 嵌套锁:
- 当一个线程已经持有一个锁并试图获取另一个锁,而另一个线程已经持有第二个锁并试图获取第一个锁时,可能会导致死锁。
- 线程优先级问题:
- 在优先级较高的线程等待优先级较低的线程释放资源时,可能导致死锁,尤其在实时系统中更为常见。
- 长时间持有锁:
- 如果线程在持有锁的情况下进行长时间的计算或等待,可能会导致其他线程长时间等待,从而增加死锁的可能性。
避免死锁的方法
- 资源请求顺序:
- 所有线程以相同的顺序请求资源,避免形成循环等待。
- 限制资源数量:
- 限制每个线程所持有的资源数量,避免持有过多资源。
- 超时机制:
- 在请求资源时设置超时,如果请求资源失败,则释放已经持有的资源。
- 使用死锁检测机制:
- 定期检测系统状态,如果发现死锁,可以采取措施,如终止某些线程以打破死锁。
- 避免长时间持有锁:
- 尽量缩小临界区的范围,减少持有锁的时间。
读写锁
读写锁是一种特殊的同步机制,允许多个线程同时读取共享资源,但在写入时必须独占该资源。这种机制对于读多写少的场景非常有效,可以提高并发性能。
读写锁是一把锁,锁的类型为pthread_rwlock_t
,有了类型之后就可以创建一把互斥锁了:
pthread_rwlock_t rwlock;
- 读锁(Read Lock):
- 允许多个线程同时读取资源。
- 当一个线程持有读锁时,其他线程也可以获取读锁,但不能获取写锁。
- 写锁(Write Lock):
- 只允许一个线程对资源进行写操作。
- 当一个线程持有写锁时,其他任何线程(无论是读锁还是写锁)都不能访问该资源。
- 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
- 参数:
- rwlock: 读写锁的地址,传出参数
- attr: 读写锁属性,一般使用默认属性,指定为NULL
- 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
rwlock:指向一个已经初始化的读写锁对象的指针。调用此函数的线程需要传入正确的锁对象。
- 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
// 这个函数可以有效的避免死锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock
函数用于尝试获取读写锁的读锁。与 pthread_rwlock_rdlock
不同的是,pthread_rwlock_tryrdlock
是非阻塞的,如果无法立即获取读锁,它将返回一个错误,而不会阻塞当前线程。
- 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock
函数用于获取读写锁的写锁。写锁是独占的,意味着在一个线程持有写锁的情况下,其他线程无法获取读锁或写锁。这种机制保证了对共享资源的安全写入,避免了数据竞争和不一致性。
- 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
// 这个函数可以有效的避免死锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
- 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
读写锁的使用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#define Max 50
int number = 0; // 全局变量
pthread_rwlock_t rwlock; // 定义读写锁
// 写的线程的处理函数
void* writeNum(void* arg)
{
for(int i = 0; i < Max; ++i)
{
pthread_rwlock_wrlock(&rwlock);
int cur = number;
cur ++;
number = cur;
printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
pthread_rwlock_unlock(&rwlock);
// 添加sleep目的是要看到多个线程交替工作
usleep(rand() % 100);
}
return NULL;
}
// 读线程的处理函数
// 多个线程可以如果处理动作相同, 可以使用相同的处理函数
// 每个线程中的栈资源是独享
void* readNum(void* arg)
{
for(int i = 0; i < Max; ++i)
{
pthread_rwlock_rdlock(&rwlock);
printf("--全局变量number = %d, tid = %ld\n", number, pthread_self());
pthread_rwlock_unlock(&rwlock);
usleep(rand() % 100);
}
return NULL;
}
int main() {
pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
// 3个写线程, 5个读的线程
pthread_t wtid[3];
pthread_t rtid[5];
for(int i = 0; i < 3; ++i)
pthread_create(&wtid[i], NULL, writeNum, NULL);
for(int i = 0; i < 5; ++i)
pthread_create(&rtid[i], NULL, readNum, NULL);
// 释放资源
for(int i = 0; i < 3; ++i)
pthread_join(wtid[i], NULL);
for(int i = 0; i < 5; ++i)
pthread_join(rtid[i], NULL);
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
条件变量
条件变量(Condition Variables)是用于线程同步的一种机制,它允许线程在某些条件满足前进入等待状态,避免忙等待。线程可以通过条件变量进行等待,直到另一个线程发出信号通知条件发生变化时被唤醒。这种机制常与互斥锁结合使用,以保护共享资源的访问。
条件变量和互斥锁虽然都能阻塞线程,但它们的作用和使用场景有所不同。为了更好地理解它们的区别,下面将从功能、使用场景和行为等方面进行对比讲解,并通过例子说明。
- 功能对比
- 互斥锁(Mutex):
- 用于确保同一时刻只有一个线程访问共享资源,从而保护共享数据的完整性,防止数据竞争。
- 互斥锁通过阻塞线程来防止其他线程在某个线程访问资源时也去操作该资源。
- 条件变量(Condition Variable):
- 条件变量用于让线程等待某个条件的发生,而不是立即访问资源。它让线程在条件不满足时进入等待状态,直到条件满足后被唤醒。
- 条件变量通常配合互斥锁使用,控制多个线程根据特定条件进行同步操作。
- 使用场景
- 互斥锁的使用场景:
- 适用于保护临界区的场景,多个线程共享某个资源,且同一时刻只能有一个线程访问或修改这个资源。例如,多个线程同时写入一个文件。
- 条件变量的使用场景:
- 适用于等待某个条件发生的场景,线程需要根据某种状态或条件来决定何时继续执行。例如,生产者-消费者模式中,消费者线程需要等待生产者线程生成数据。
- 行为对比
- 互斥锁的行为:
- 当一个线程持有互斥锁时,其他尝试获取该锁的线程会被阻塞,直到持锁的线程释放锁为止。
- 互斥锁通常不会主动“唤醒”其他线程,除非锁被释放,其他线程才能竞争获取锁。
- 条件变量的行为:
- 条件变量使线程进入等待状态,直到某个线程发出信号(signal或broadcast)来通知条件发生变化,唤醒等待的线程。
- 条件变量依赖互斥锁保护共享数据,确保线程在被唤醒后能安全地重新检查条件。
1. 条件变量的概念
- 等待队列:条件变量维护一个等待队列,任何一个线程可以等待某个条件的发生,直到条件被满足时,被唤醒。
- 信号:一个线程在某个条件满足时发出信号,唤醒正在等待该条件的其他线程。
- 互斥锁:条件变量一般和互斥锁结合使用,互斥锁保护共享数据的完整性,而条件变量则让线程有条件地等待某个状态的改变。
2. 条件变量的工作原理
当某个线程等待某个条件变量时:
- 它会释放与该条件变量关联的互斥锁,并进入等待状态。
- 当条件满足时,另一个线程发出信号,唤醒这个等待的线程。
- 被唤醒的线程在重新获得互斥锁后,继续执行。
3. 常用函数
pthread_cond_init:初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
- cond:指向条件变量的指针。
- attr:条件变量的属性,通常设置为
NULL
使用默认属性。
pthread_cond_wait:等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
- cond:指向条件变量的指针。
- mutex:指向与条件变量关联的互斥锁的指针。
- 该函数会自动释放互斥锁并使线程进入等待状态,直到条件变量被唤醒。
pthread_cond_signal:发送条件变量信号(唤醒一个线程)
int pthread_cond_signal(pthread_cond_t *cond);
- cond:指向条件变量的指针。
- 该函数会唤醒一个等待该条件变量的线程。
pthread_cond_broadcast:广播条件变量信号(唤醒所有等待的线程)
int pthread_cond_broadcast(pthread_cond_t *cond);
- 该函数唤醒所有等待该条件变量的线程。
pthread_cond_destroy:销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
- cond:指向条件变量的指针。
- 用于销毁不再需要的条件变量。
信号量
信号量(Semaphore) 是一种用于实现线程同步的机制,类似于互斥锁,但它不仅仅限制单个线程对共享资源的访问,还允许多个线程同时访问一定数量的资源。
信号量的概念
信号量可以被看作是一个计数器,它表示可用资源的数量。根据使用场景,信号量可以分为以下两种:
- 计数信号量(Counting Semaphore):允许多个线程同时访问资源。计数器的值可以大于1,表示有多少个线程可以访问共享资源。
- 二进制信号量(Binary Semaphore):相当于互斥锁,计数器只有0和1两个值,0表示资源被占用,1表示资源可用。
信号量的两个主要操作
- P操作(wait):也称为“测试操作”或“减操作”。线程试图获取资源时,执行P操作。信号量的值减1。如果结果是负数,则线程被阻塞,等待资源可用。
- V操作(signal):也称为“释放操作”或“加操作”。线程释放资源时,执行V操作。信号量的值加1,如果有其他线程在等待资源,则唤醒一个等待的线程。
信号量的基本用法
信号量适合于控制多个线程对一定数量的资源的访问,或用于线程之间的协作。例如,如果有5个线程要访问3个共享资源,信号量可以限制同时只有3个线程进入临界区。
信号量的简单例子
下面是一个使用信号量的示例代码,它演示了如何控制多个线程访问一个有限的资源。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem; // 定义一个信号量
void* thread_func(void* arg) {
int num = *((int*)arg); // 获取线程编号
sem_wait(&sem); // P操作,尝试进入临界区
printf("Thread %d: Entering critical section\n", num);
sleep(2); // 模拟线程正在使用资源
printf("Thread %d: Leaving critical section\n", num);
sem_post(&sem); // V操作,离开临界区,释放资源
return NULL;
}
int main() {
pthread_t threads[5]; // 创建5个线程
int thread_num[5]; // 用于给每个线程分配编号
sem_init(&sem, 0, 3); // 初始化信号量,允许最多3个线程同时进入临界区
// 创建5个线程
for (int i = 0; i < 5; ++i) {
thread_num[i] = i;
pthread_create(&threads[i], NULL, thread_func, &thread_num[i]);
}
// 等待5个线程执行完毕
for (int i = 0; i < 5; ++i) {
pthread_join(threads[i], NULL);
}
sem_destroy(&sem); // 销毁信号量
return 0;
}
代码说明
sem_t sem;
定义了一个信号量。sem_init(&sem, 0, 3);
初始化信号量,并将计数器设置为3,表示最多允许3个线程进入临界区。sem_wait(&sem);
在每个线程中执行P操作,尝试获取信号量。如果信号量大于0,线程进入临界区,信号量减1;否则,线程阻塞,等待其他线程释放资源。sem_post(&sem);
执行V操作,释放信号量,信号量加1。如果有其他线程在等待,它们可以继续进入临界区。
信号量与互斥锁的区别
- 互斥锁:通常只允许一个线程访问临界区,保证线程对共享资源的独占访问。
- 信号量:可以允许多个线程同时访问资源,控制线程的数量,适用于资源有限的场景。
信号量的应用场景
- 资源管理:信号量可以用来控制对有限资源的访问,比如数据库连接池。
- 生产者-消费者问题:信号量可以协调生产者和消费者之间的操作,确保生产者生产的资源数量和消费者消耗的数量保持平衡。
通过信号量,可以有效地管理多线程程序中的资源竞争,防止资源争夺导致的冲突。