文章目录
- 💂 个人主页:风间琉璃
- 🤟 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
- 💬 如果文章对你有
帮助
、欢迎关注
、点赞
、收藏(一键三连)
和订阅专栏
哦
前言
提示:这里可以添加本文要记录的大概内容:
一、线程
在操作系统中,线程是程序执行的最小单元
,它是进程的一部分,共享相同的地址空间和大部分资源(如全局变量、打开的文件等)。相比于进程,线程的创建、销毁和切换开销更小,能够更高效地利用系统资源。
- 地址空间:进程拥有独立的地址空间,每个进程有自己的内存空间;而线程
共享
同一个进程的地址空间。- 资源占用:进程间切换开销大,资源消耗多;线程切换开销小,效率高。
- 并发性:进程间通信需要额外的机制(如管道、消息队列),而线程间通信
直接共享数据
。- 独立性:进程是独立的执行实体,相互之间不受影响;
线程共享进程的执行环境
,相互影响较大。
1.线程的使用
Linux 提供了 POSIX 线程库(pthread)
,是最常用的多线程编程接口。通过 pthread 库,开发人员可以方便地创建、同步和管理线程,实现并发执行的程序。示例如下:
#include <pthread.h>
#include <stdio.h>
void* working(void* arg) {
printf("子线程ID: %ld\n", pthread_self());
printf("Hello from thread\n");
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, working, NULL);
pthread_join(thread, NULL);
printf("主线程ID: %ld\n", pthread_self());
printf("Thread finished\n");
return 0;
}
注意,在使用pthread库时,确保在编译时添加 -pthread 选项来链接 pthread 库,这样编译器会自动将 pthread 库链接到可执行程序中。
- 使用
g++ 编译器
编译命令:
g++ -pthread linux_thread.cpp -o linux_threadcmake
链接指令:
target_link_libraries(linux_thread pthread )x
不然可能会发生如下报错:
在多线程编程中,每个线程都有一个唯一的线程ID
,可以使用 pthread_self() 函数获取当前线程的线程ID。这个ID类型为 pthread_t
,它实际上是一个无符号长整型数,用于标识和管理线程。
pthread_t pthread_self(void); // 返回当前线程的线程ID
在一个进程中调用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: 作为
实参
传递到 start_routine 指针指向的函数内部
- 返回值:线程创建成功返回0,创建失败返回对应的错误号
注意,在创建过程中要保证线程函数与规定的函数指针类型一致:void*(*start_routine) (void *),其返回值为void*,其参数也为void*。
线程创建成功后,需要调用pthread_join()函数
,其作用是等待指定的线程结束执行,主要用于线程之间的同步
。如果不使用 pthread_join 函数,会导致以下影响:
主线程可能会提前结束
如果主线程没有等待其他线程完成就提前退出,那么其他线程可能会在主线程退出时被强制终止,从而导致资源无法正确释放和程序状态不一致的问题。线程资源可能不会得到正确释放
每个线程结束时,系统会回收线程使用的资源(如栈空间等),但是如果没有使用 pthread_join 等待线程结束,这些资源可能无法及时释放,从而导致资源泄漏或者系统资源利用效率降低。线程间数据共享可能出现问题
如果线程之间有共享的全局变量或者其他资源,没有使用适当的同步机制(比如 pthread_join),可能会导致数据竞争和不一致性,影响程序的正确性和可靠性。
如下图所示:
在这个例子中,主线程没有调用 pthread_join(thread, NULL) 等待子线程完成。这种情况下,主线程可能会先于子线程结束,导致子线程无法正常完成其任务
,输出结果可能不会包含子线程的输出信息(“Thread started”)。这种情况下,子线程的执行可能会被强制终止,导致资源管理问题和程序状态不可预测性。
2.线程相关函数
1.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: 传递给线程函数的参数。可以是 NULL。
2.pthread_join()线程回收函数
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收
,线程库中提供的线程回收函叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
int pthread_join(pthread_t thread, void **retval);
//thread: 需要等待的线程的线程 ID。
//retval: 指向指针的指针,用于存储线程的退出状态。如果不需要获取线程的返回值,可以传递 NULL。
3.pthread_exit()线程退出函数
pthread_exit用于显式地终止调用线程,并返回一个退出状态。调用这个函数的线程会立即终止执行,而其他线程继续运行。如果主线程调用 pthread_exit,那么整个进程不会终止,进程会继续运行直到所有其他线程终止。
void pthread_exit(void *retval);
// retval: 指向线程退出状态的指针。可以是任何类型的指针,通常用来传递线程的退出状态或返回值。
注意,在线程函数中显式地调用 pthread_exit 可以确保线程正确地终止并返回指定的退出状态。如果线程函数执行完毕没有调用 pthread_exit,线程也会正常终止,但返回值将是 NULL。
4.pthread_detach()线程分离函数
pthread_detach 用于将一个线程设置为分离状态
,使得该线程的资源在其结束后由系统自动回收。调用 pthread_detach 后,线程终止时,其资源(如线程栈)会被自动释放,主线程无需调用 pthread_join 来回收这些资源。在分离状态的线程结束后,主线程无法获取该线程的退出状态,也不能对该线程进行 pthread_join 操作。
int pthread_detach(pthread_t thread);
**适用于线程的生命周期独立于创建线程的线程的情况。**例如,后台服务线程或定时任务线程,它们在完成后自动清理资源,主线程不需要关注它们的状态
二、线程的同步与互斥
线程同步
是指在多线程程序中,通过各种机制确保多个线程在访问共享资源时不会发生冲突或数据不一致
,从而保证程序的正确性和一致性。线程同步的目的是协调线程的执行顺序和状态
,避免由于并发执行而导致的竞态条件和不一致性问题。所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
线程同步的关键点:
避免
竞态条件
:竞态条件发生在两个或多个线程尝试同时访问和修改共享资源时,可能导致不一致的结果。同步机制可以确保在某个时刻只有一个线程能够访问资源,从而避免竞态条件。确保数据一致性: 同步机制保证线程间的数据一致性和正确性。例如,在某个线程修改共享数据时,其他线程必须等待直到修改完成。
协调线程执行: 有时需要线程按照特定的顺序执行或者在满足某些条件时才继续执行。同步机制可以帮助协调线程的执行顺序。
如下代码所示:两个代码交替打印数字。
#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 50
// 全局变量
int number;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
int cur = number;
cur++;
usleep(10);
number = cur;
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)
{
int cur = number;
cur++;
number = cur;
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_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
结果如下:
虽然每个线程内部循环了50次每次数一个数,但是最终没有数到100,通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。
因为两个线程在操作共享变量 number 时没有进行同步保护,导致竞态条件(Race Condition)的发生。两个线程在计数的时候需要分时复用CPU时间片
,并且测试程序中调用了sleep()导致线程的CPU时间片没用完就被迫挂起了,这样就能让CPU的上下文切换
(保存当前状态, 下一次继续运行的时候需要加载保存的状态)更加频繁,更容易再现数据混乱的这个现象。
在计算机系统中,CPU的寄存器
、一级缓存、二级缓存和三级缓存
是独占的,用于存储处理数据和线程的状态信息。当数据被CPU处理完成后,通常需要再次写入到物理内存
中。此外,物理内存中的数据也可以通过文件I/O操作写入到磁盘中。
在测试程序中,两个线程共享全局变量number。当线程进入运行态开始计数时,会从物理内存加载数据,然后在CPU中进行运算,最后将结果更新回物理内存。如果两个线程能够顺利完成这一流程,得到的结果将是正确的。
然而,若线程A在执行过程中失去CPU时间片并被挂起,最新的数据将无法更新到物理内存。这时,线程B开始运行并从物理内存中读取数据,显然它读取的是旧数据,只能基于旧数据继续计数。随后,线程B也可能失去CPU时间片并被挂起,而线程A重新获得CPU时间片变成运行态。线程A的第一件事是将上次未更新的数据写入内存,这将导致线程B之前更新的数据被覆盖,从而使线程B的工作无效。最终,这种情况会导致某些数据被重复多次。
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源
。
找到临界资源之后,再找出与临界资源相关的上下文代码,这样就得到了一个代码块
,这个代码块可以称为临界区
。确定好临界区(临界区越小越好)之后,就可以进行线程同步了。线程同步的大致处理思路如下:
1.
加锁
:
- 在临界区代码的上方,添加
加锁函数
,对临界区加锁。- 任何线程调用这行代码时,将会锁上这把锁,其他线程只能阻塞在锁上。
2.
解锁
:
- 在临界区代码的下方,添加
解锁函数
,对临界区解锁。- 离开临界区的线程会将锁打开,这样其他等待的线程就可以进入临界区了。
1.互斥锁 (Mutex)
互斥锁(Mutex)
是一种用于线程同步的机制,它可以确保在任何时刻只有一个线程能够访问共享资源,从而避免数据竞争和不一致性。
互斥锁是一个二进制锁,具有两种状态:锁定和未锁定。使用互斥锁可以保证一个线程在进入临界区之前先锁定该互斥锁,在离开临界区时解锁该互斥锁。其他线程在尝试进入临界区时,如果发现互斥锁已被锁定,就会被阻塞,直到该锁被释放为止。
互斥锁的使用:
1️⃣.创建互斥锁
在Linux中互斥锁的类型为pthread_mutex_t
,创建该类型的变量就得到了一把互斥锁,在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。
pthread_mutex_t mutex;
2️⃣.初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// mutex: 互斥锁变量的地址
// attr: 互斥锁的属性, 一般使用默认属性即可, 这个参数指定为NULL
3️⃣.加锁
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock用于对指定的互斥锁进行加锁操作。如果互斥锁已经被其他线程锁定,那么调用该函数的线程将会被阻塞,直到互斥锁被解锁为止。当互斥锁当前是未锁定状态时,调用该函数会立即获得锁,并进入临界区。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
用于尝试加锁互斥锁。如果互斥锁已经被其他线程锁定,该函数不会阻塞,而是立即返回一个错误码。这个函数适用于需要尝试获取锁但不希望阻塞的场景。
4️⃣.访问临界区
5️⃣.解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_unlock用于对指定的互斥锁进行解锁操作。调用该函数会释放互斥锁,并使其他被阻塞在该锁上的线程有机会获得锁。解锁操作通常在临界区代码执行完毕后调用
,以确保临界区的线程安全。
6️⃣.销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy用于销毁一个互斥锁对象,并释放与该锁相关的所有资源。这个函数在互斥锁不再需要使用时调用。注意,互斥锁在销毁前必须是未锁定状态,否则会导致未定义的行为。
使用示例:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#define MAX 50
int number = 0; // 全局变量,作为共享资源
pthread_mutex_t mutex; // 互斥锁
// 线程处理函数A
void* funcA_num(void* arg) {
for (int i = 0; i < MAX; ++i) {
pthread_mutex_lock(&mutex); // 加锁
number++;
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex); // 解锁
usleep(10); // 短暂休眠,模拟工作负载并允许线程切换
}
return NULL;
}
// 线程处理函数B
void* funcB_num(void* arg) {
for (int i = 0; i < MAX; ++i) {
pthread_mutex_lock(&mutex); // 加锁
number++;
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex); // 解锁
usleep(10); // 短暂休眠,模拟工作负载并允许线程切换
}
return NULL;
}
int main() {
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;
}
死锁
是指两个或多个线程在执行过程中,因为竞争资源或者由于彼此通信而造成的一种互相等待的现象,如果没有外力干涉,那么它们将无法继续执行下去。简单地说,死锁是多线程编程中由于线程相互等待导致的程序无法继续执行的一种状态。
造成死锁的场景有如下几种:
- 加锁之后忘记解锁
- 重复加锁, 造成死锁
- 在程序中有多个共享资源, 因此有很多把锁,随意加锁,导致相互被阻塞
2.读写锁 (Read-Write Lock)
在 Linux 中,读写锁(read-write lock)是一种同步机制,用于保护共享资源,以允许多个线程同时读取资源
,但在某个线程写入资源时阻止其他线程访问。读写锁是互斥锁的升级版, 在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。读写锁的优点在于,它能够在读操作频繁、写操作较少的场景下提高并发性能。
在 Linux 中,读写锁通过 pthread_rwlock_t 类型
和相关函数来实现。常用函数包括:
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
// 销毁读写锁。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 获取读锁。
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 获取写锁。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 释放读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
读写锁既可以锁定读操作,也可以锁定写操作,锁中记录了以下信息:
- 锁的状态:锁定/打开
- 锁定的操作类型:读/写操作。使用读写锁锁定读操作,需要先解锁才能锁定写操作,反之亦然。
- 哪个线程将这把锁锁上了
读写锁的使用方式与互斥锁类似:找到共享资源,确定临界区,在临界区的开始位置加锁(读锁/写锁),在临界区的结束位置解锁。
读写锁的特点:
- 读锁:当使用读写锁的读锁锁定临界区时,线程对临界区的访问是并行的。读锁是共享的,多个线程可以同时持有读锁。
- 写锁:当使用读写锁的写锁锁定临界区时,线程对临界区的访问是串行的。写锁是独占的,一个线程持有写锁时,其他线程不能获取读锁或写锁。
- 优先级:如果读写锁分别对两个临界区加了读锁和写锁,两个线程同时访问这两个临界区时,访问写锁临界区的线程继续运行,而访问读锁临界区的线程将被阻塞,因为写锁的优先级高于读锁。
在程序中如果所有的线程都对共享资源进行写操作,使用读写锁没有优势,与互斥锁相同。如果程序中对共享资源的操作既有读也有写,并且读操作比写操作多,读写锁更具优势。因为读锁是共享的,能够提高读操作的并行性,从而提高性能。
示例:读写锁在多线程环境下的使用,以保护共享资源 shared_resource。读线程可以并发地读取共享资源,而写线程需要独占访问共享资源。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// 声明一个读写锁
pthread_rwlock_t rwlock;
// 共享资源
int shared_resource = 0;
// 读线程函数
void* reader(void* arg) {
for (int i = 0; i < 5; ++i) {
// 加读锁
pthread_rwlock_rdlock(&rwlock);
// 读取共享资源并打印
printf("Reader %ld: shared_resource = %d\n", (long)arg, shared_resource);
// 解读锁
pthread_rwlock_unlock(&rwlock);
// 休眠1秒
sleep(1);
}
return NULL;
}
// 写线程函数
void* writer(void* arg) {
for (int i = 0; i < 5; ++i) {
// 加写锁
pthread_rwlock_wrlock(&rwlock);
// 修改共享资源并打印
shared_resource++;
printf("Writer %ld: shared_resource = %d\n", (long)arg, shared_resource);
// 解写锁
pthread_rwlock_unlock(&rwlock);
// 休眠1秒
sleep(1);
}
return NULL;
}
int main() {
pthread_t readers[5], writers[2];
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建5个读线程
for (int i = 0; i < 5; ++i) {
pthread_create(&readers[i], NULL, reader, (void*)i);
}
// 创建2个写线程
for (int i = 0; i < 2; ++i) {
pthread_create(&writers[i], NULL, writer, (void*)i);
}
// 等待所有读线程结束
for (int i = 0; i < 5; ++i) {
pthread_join(readers[i], NULL);
}
// 等待所有写线程结束
for (int i = 0; i < 2; ++i) {
pthread_join(writers[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
3.条件变量 (Condition Variable)
条件变量是一种线程同步机制,允许线程在某些条件满足前进行等待
。条件变量通常与互斥锁一起使用,以避免竞态条件。条件变量的主要操作包括等待条件满足和通知其他等待线程条件已满足。
一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为pthread_cond_t
。被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用
。
// 初始化条件变量
pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 销毁条件变量
pthread_cond_destroy(pthread_cond_t *cond);
// 等待条件变量,调用此函数的线程将进入等待状态,并释放关联的互斥锁
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// 通知至少一个等待条件变量的线程
pthread_cond_signal(pthread_cond_t *cond);
// 通知所有等待条件变量的线程
pthread_cond_broadcast(pthread_cond_t *cond);
注意pthread_cond_wait通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下处理:
- 在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做是为了避免死锁
- 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区
以下是一个生产者-消费者问题的示例,使用条件变量来同步生产者和消费者线程。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE]; // 缓冲区
int count = 0; // 当前缓冲区中的元素数量
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t cond_producer; // 生产者条件变量
pthread_cond_t cond_consumer; // 消费者条件变量
// 生产者线程函数
void* producer(void* arg) {
for (int i = 0; i < 20; ++i) {
pthread_mutex_lock(&mutex); // 加锁
// 如果缓冲区已满,等待生产者条件变量
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_producer, &mutex);
}
// 生产数据
buffer[count++] = i;
printf("Produced: %d\n", i);
// 通知消费者有数据可消费
pthread_cond_signal(&cond_consumer);
pthread_mutex_unlock(&mutex); // 解锁
usleep(rand() % 100000); // 模拟生产时间
}
return NULL;
}
// 消费者线程函数
void* consumer(void* arg) {
for (int i = 0; i < 20; ++i) {
pthread_mutex_lock(&mutex); // 加锁
// 如果缓冲区为空,等待消费者条件变量
while (count == 0) {
pthread_cond_wait(&cond_consumer, &mutex);
}
// 消费数据
int item = buffer[--count];
printf("Consumed: %d\n", item);
// 通知生产者可以继续生产
pthread_cond_signal(&cond_producer);
pthread_mutex_unlock(&mutex); // 解锁
usleep(rand() % 100000); // 模拟消费时间
}
return NULL;
}
int main() {
pthread_t prod, cons; // 生产者和消费者线程
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_cond_init(&cond_producer, NULL); // 初始化生产者条件变量
pthread_cond_init(&cond_consumer, NULL); // 初始化消费者条件变量
pthread_create(&prod, NULL, producer, NULL); // 创建生产者线程
pthread_create(&cons, NULL, consumer, NULL); // 创建消费者线程
pthread_join(prod, NULL); // 等待生产者线程完成
pthread_join(cons, NULL); // 等待消费者线程完成
pthread_mutex_destroy(&mutex); // 销毁互斥锁
pthread_cond_destroy(&cond_producer); // 销毁生产者条件变量
pthread_cond_destroy(&cond_consumer); // 销毁消费者条件变量
return 0;
}
4.信号量 (Semaphore)
信号量 (Semaphore) 是一种用于进程同步的机制,是一种特殊的变量,主要用于多线程编程中来管理对共享资源的访问。信号量通过两个基本操作来实现同步控制:P操作(wait,等待)
和V操作(signal,信号)
。信号量可以分为两类:
- 计数信号量:其值可以为非负整数,用于控制对一个资源的多个副本的访问。
- 二进制信号量:其值只有0和1,类似于互斥锁,用于控制对单一资源的访问。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为sem_t
对应的头文件为<semaphore.h>
:
#include <semaphore.h>
sem_t sem;
信号量常用函数:
// 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
int sem_destroy(sem_t *sem);
- sem:信号量变量地址
- pshared:
0:线程同步
非0:进程同步
- value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);
当线程调用这个函数,sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时,资源被耗尽,因此线程也就被阻塞。
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_trywait(sem_t *sem);
当线程调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时,资源被耗尽,但是线程不会被阻塞,直接返回错误号
,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
// 调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);
调用该函数会将sem中的资源数+1,如果有线程在调用sem_wait、sem_trywait、sem_timedwait时因为sem中的资源数为0被阻塞,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);
通过这个函数可以查看sem中现在拥有的资源个数,通过第二个参数sval将数据传出。
下面是一个简单的例子,展示了如何在生产者-消费者问题中使用信号量进行同步控制。
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int count = 0;
sem_t empty; // 信号量,表示空缓冲区数量
sem_t full; // 信号量,表示满缓冲区数量
pthread_mutex_t mutex; // 互斥锁,保护缓冲区
void* producer(void* arg) {
for (int i = 0; i < 20; ++i) {
sem_wait(&empty); // P操作,等待空缓冲区
pthread_mutex_lock(&mutex); // 加锁, 不要加到上一句,可能会产生死锁
buffer[count++] = i; // 生产数据
printf("Produced: %d\n", i);
pthread_mutex_unlock(&mutex); // 解锁
sem_post(&full); // V操作,增加满缓冲区数量
usleep(rand() % 100000); // 模拟生产时间
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 20; ++i) {
sem_wait(&full); // P操作,等待满缓冲区
pthread_mutex_lock(&mutex); // 加锁
int item = buffer[--count]; // 消费数据
printf("Consumed: %d\n", item);
pthread_mutex_unlock(&mutex); // 解锁
sem_post(&empty); // V操作,增加空缓冲区数量
usleep(rand() % 100000); // 模拟消费时间
}
return NULL;
}
int main() {
pthread_t prod, cons;
sem_init(&empty, 0, BUFFER_SIZE); // 初始化空缓冲区信号量,值为BUFFER_SIZE
sem_init(&full, 0, 0); // 初始化满缓冲区信号量,值为0
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_create(&prod, NULL, producer, NULL); // 创建生产者线程
pthread_create(&cons, NULL, consumer, NULL); // 创建消费者线程
pthread_join(prod, NULL); // 等待生产者线程完成
pthread_join(cons, NULL); // 等待消费者线程完成
sem_destroy(&empty); // 销毁信号量
sem_destroy(&full); // 销毁信号量
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
结束语
感谢阅读吾之文章,今已至此次旅程之终站 🛬。
吾望斯文献能供尔以宝贵之信息与知识也 🎉。
学习者之途,若藏于天际之星辰🍥,吾等皆当努力熠熠生辉,持续前行。
然而,如若斯文献有益于尔,何不以三连为礼?点赞、留言、收藏 - 此等皆以证尔对作者之支持与鼓励也 💞。