【Linux篇】多线程编程中的互斥与同步:深入理解锁与条件变量的应用
深入理解线程互斥与同步:确保多线程程序高效与安全
- 一. 线程互斥
- 1.1 基本概念
- 1.2 互斥量(mutex)
- 1.2.1 pthread_mutex_init()
- 1.2.2 pthread_mutex_lock()
- 1.2.3 pthread_mutex_unlock()
- 1.2.4 pthread_mutex_destroy()
- 1.3. 互斥量封装
- 1.3.1 Mutex类
- 1.3.2 LockGuard()类
- 二. 线程同步
- 2.1 基本概念
- 2.2 条件变量
- 2.2.1 pthread_cond_init()
- 2.2.2 pthread_cond_signal()
- 2.2.3 pthread_cond_wait()
- 2.2.4 pthread_cond_destroy()
- 2.2.5 综合示例代码:
- 三. 最后
线程互斥与同步是并发编程中的两个重要概念。互斥(Mutual Exclusion)用于防止多个线程在同一时间访问共享资源,避免数据竞争和不一致的情况。通过互斥锁(如 mutex)来保证同一时刻只有一个线程能访问资源。而同步(Synchronization)则是确保线程按特定顺序执行,解决线程之间的协调问题,常用于保证线程间的数据传递或任务完成的顺序性。常见的同步机制有信号量、条件变量等。理解这两个概念有助于避免并发程序中的潜在问题,确保多线程程序的正确性和高效性。
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
👍点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对Linux OS感兴趣的朋友,让我们一起进步!
一. 线程互斥
1.1 基本概念
线程互斥(Mutual Exclusion)是指在多线程程序中,确保多个线程在访问共享资源时,只有一个线程可以在任意时刻访问该资源,避免多个线程同时操作共享资源时出现数据竞争或不一致的情况。
下面通过一个模拟抢票的程序来看看有什么问题?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 100;void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else{break;}}
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}
输出结果:
thread 4 sells ticket:100
…
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
从上述结果可以看出,票数被抢成负数,在实际中票数怎么可能是负数,所以该程序存在问题,问题的根源是:在同一时间多个线程同时进入临界区条件为真后,线程被切换,i–不是原子操作。要解决该漏洞,可以使用锁(互斥量)。
1.2 互斥量(mutex)
为实现线程互斥,通常使用互斥锁(Mutex)。互斥锁是一种同步机制,它允许一个线程在访问共享资源时锁定该资源,其他线程必须等待直到资源被解锁才能访问。这样可以保证同一时刻只有一个线程能访问共享资源,防止出现冲突和不一致。
现在给上述代码在临界区加锁。
- 示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&lock);//加锁if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}
int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}
输出结果变得正常,加上锁,可以让持有锁线程执行完毕后,释放锁,被其他线程占有,因为锁只有一把,就算线程被切换了,其他线程也进入不到临界资源必须等本持有该锁的线程释放锁,这就是原子特性。
下面将详细介绍有关互斥量(mutex)的接口函数。请阅:>
1.2.1 pthread_mutex_init()
- 功能:
pthread_mutex_init 是 POSIX 线程库(pthreads)中用于初始化互斥锁的函数。它用于创建一个互斥锁对象,并为该互斥锁分配必要的资源。
- 函数原型:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- 参数
-
pthread_mutex_t *:这是一个指向互斥锁对象的指针。调用此函数时,mutex 指向的内存空间必须已经被分配。该对象会被初始化为一个互斥锁。
-
const pthread_mutexattr_t * attr:这是一个指向互斥锁属性的指针。可以设置一些特殊的属性,如是否是递归锁等。如果不需要设置属性,可以传入 NULL,即使用默认属性。
- 返回值:
成功时,返回 0。
失败时,返回一个错误码,表示初始化失败。常见的错误码包括:EINVAL:传入的属性无效,ENOMEM:系统资源不足,无法初始化互斥锁。
- 方法2:定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
注意:该锁不需要初始化和手动销毁,如果强行进行pthread_mutex_init,会导致未定义的行。
总结来说,PTHREAD_MUTEX_INITIALIZER 是一种快速且简洁的方式来初始化互斥锁,适用于全局变量或静态变量,但对于需要特殊初始化的情况,还是建议使用 pthread_mutex_init。
1.2.2 pthread_mutex_lock()
- 功能:
pthread_mutex_lock 是一个用于锁定互斥锁的函数,它在多线程编程中用于确保同一时刻只有一个线程能够访问某个共享资源,从而避免资源竞争和数据不一致的情况。 - 函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数:
pthread_mutex_t * mutex:这是指向互斥锁的指针。该锁对象必须在调用 pthread_mutex_lock 之前已经被初始化(可以通过 pthread_mutex_init 或 PTHREAD_MUTEX_INITIALIZER 来初始化)。
- 返回值:
成功时,返回 0。
失败时,返回一个错误码,常见的错误码包括:EINVAL:互斥锁无效,可能是因为互斥锁未被初始化,EDEADLK:发生死锁,如果一个线程尝试对自己已经加锁的互斥锁进行加锁,则会返回这个错误。
-
作用:
-
pthread_mutex_lock 的主要功能是将互斥锁锁定,使得只有一个线程可以进入临界区(即访问共享资源)。在多线程环境下,这种锁机制确保了线程之间的同步,防止了多个线程同时访问共享资源可能导致的数据竞态问题。
-
当线程执行 pthread_mutex_lock 时,如果该互斥锁已经被其他线程锁定,当前线程将会被阻塞(挂起),直到互斥锁被解锁。因此,调用 pthread_mutex_lock 的线程要么成功获取锁,要么在其他线程释放锁后继续执行。
1.2.3 pthread_mutex_unlock()
-
功能:
pthread_mutex_unlock 的作用是释放由 pthread_mutex_lock 或 pthread_mutex_trylock 锁定的互斥锁,允许其他线程访问该互斥锁保护的共享资源。它是实现线程互斥和同步的核心操作之一。 -
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数:
mutex:指向一个已被锁定的互斥锁的指针。调用 pthread_mutex_unlock 时,必须确保互斥锁已经由当前线程锁定。如果线程没有锁定互斥锁而直接调用 pthread_mutex_unlock,会导致未定义行为。
- 返回值:
成功时,返回 0。
失败时,返回一个错误码,常见的错误码包括:EPERM:当前线程没有锁定该互斥锁(即尝试解锁非本线程持有的互斥锁)。
1.2.4 pthread_mutex_destroy()
- 功能:
pthread_mutex_destroy 的作用是销毁一个互斥锁,并释放它所占用的资源。调用此函数时,互斥锁不再可用。通常,它在程序的最后使用,或者在不再需要互斥锁时调用,可以防止资源泄漏,并帮助系统回收资源。
- 函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数:
mutex:指向一个已初始化的互斥锁的指针,该锁在调用 pthread_mutex_destroy 前应该已经被销毁。
- 返回值:
成功时,返回 0。
失败时,返回一个错误码,常见的错误码包括:EBUSY:如果互斥锁仍然被锁定或正在被其他线程使用,不能销毁。EINVAL:如果传入的互斥锁无效,可能是未初始化或已经销毁。
注意事项:
- 在销毁该锁前,必须保证该锁不再被任何一个线程持有,否则会导致EBUSY错误,因此,销毁前要确保互斥锁没有被锁定。
- 不要对一个互斥锁重复销毁,否则导致未定义行为。
- 销毁该锁后,不能在使用该互斥锁,否则导致未定义行为。
互斥量的原理:互斥量本身就是个无符号整数,持有该锁的线程会将al寄存器的内容交换至本身的
寄存器中,该线程/进程所私有,注意是交换,不是拷贝,if(al寄存器的内容 > 0) return 0;否则进行阻塞等待。
1.3. 互斥量封装
1.3.1 Mutex类
职责:封装互斥量生命周期管理(初始化/销毁)及锁操作。
- 示例代码:
class Mutex {
public:Mutex() {pthread_mutex_init(&_mutex, nullptr); // 初始化互斥量}void Lock() {int n = pthread_mutex_lock(&_mutex);if (n != 0) {std::cout << "加锁失败: " << strerror(n) << std::endl; // 错误处理}}void UnLock() {int n = pthread_mutex_unlock(&_mutex);if (n != 0) {std::cout << "解锁失败: " << strerror(n) << std::endl;}}~Mutex() {pthread_mutex_destroy(&_mutex); // 销毁互斥量}private:pthread_mutex_t _mutex; // 底层互斥量对象
};
为了防止多个Mutex对象操作同一个互斥锁,可以将该类的拷贝构造和赋值禁用。拷贝后防止多个Mutex对象对互斥量mutex进行析构,造成程序异常。
// 在Mutex类中添加(C++11起)
Mutex(const Mutex&) = delete;//拷贝构造
Mutex& operator=(const Mutex&) = delete;//赋值
1.3.2 LockGuard()类
为了防止程序员忘了释放资源等问题,可以采用RAII风格的自动管理锁的生命周期。对象作用域结束,自动释放资源。
- 职责:通过RAII自动管理锁的生命周期。
- 示例代码:
class LockGuard {
public://使用explicit避免隐式转换,强制用户明确加锁意图。explicit LockGuard(Mutex& mutex) : _mutex(mutex) {_mutex.Lock(); // 构造函数中加锁}~LockGuard() {_mutex.UnLock(); // 析构函数中解锁}private:Mutex& _mutex; // 引用避免拷贝
};
该类会自动管理锁的生命周期,自动释放资源,防止内存泄漏等问题。
二. 线程同步
2.1 基本概念
线程同步概念机在计算机科学中,通常指的是通过特定机制来保证多个线程在共享资源时不会发生冲突或产生不一致的数据状态。
例如:一个线程频繁地加锁与解锁,会导致其它线程无法得到CPU资源,导致线程饥饿,为了解决该问题引入条件变量。
2.2 条件变量
条件变量(Condition Variable)是多线程编程中的一种同步机制,用于在某些特定条件成立时,通知一个或多个线程继续执行。它通常与互斥锁(mutex)一起使用,以确保线程在等待条件成立时不会同时访问共享资源。
2.2.1 pthread_cond_init()
- 功能:
pthread_cond_init 是 POSIX 线程库中的一个函数,用于初始化条件变量。条件变量允许线程在满足某些条件时进行同步,通常用于线程间的协调工作。 - 函数原型:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
- 参数
-
cond: 指向条件变量的指针。条件变量是一个结构体,通常在使用前通过此指针进行初始化。
-
attr: 一个指向 pthread_condattr_t 结构的指针,该结构用于指定条件变量的属性。通常,NULL 可以作为该参数传入,表示使用默认属性。
- 返回值:
0: 成功。
非0值: 错误代码。
注意:若条件变量未初始化,却被使用,会导致未定义行为。
2.2.2 pthread_cond_signal()
功能:
pthread_cond_signal 用于唤醒一个在指定条件变量上等待的线程。当一个线程调用pthread_cond_signal 时,它会唤醒一个等待条件变量的线程。该线程将会继续执行。注意有且仅唤醒一个在该条件变量下等待的线程。
- 函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
- 参数
cond: 这是一个指向条件变量的指针,类型是 pthread_cond_t。该条件变量应该已经被初始化,通常通过 pthread_cond_init 函数进行初始化。
- 返回值
0: 如果操作成功。
非0值: 如果发生错误,返回一个错误代码。常见的错误码包括 EINVAL(无效的条件变量)。
补充:唤醒在指定条件变量等待的所有线程:pthread_cond_broadcast(pthread_cond_t *cond)
其它的性质同上。
2.2.3 pthread_cond_wait()
功能:
pthread_cond_wait 使当前线程进入等待状态,直到以下两种情况之一发生:
-
该线程被其他线程通过调用 pthread_cond_signal 或 pthread_cond_broadcast 唤醒。
-
等待的条件满足(在进入等待状态之前,线程已经加锁互斥锁,并且此时它会继续等待直到条件满足)。
需要注意,pthread_cond_wait 在被调用时,会释放传入的互斥锁,并使当前线程进入阻塞状态。一旦条件满足并唤醒线程,pthread_cond_wait 会重新加锁互斥锁,然后返回。
- 函数原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
- 参数
-
cond: 这是一个指向条件变量的指针,类型为 pthread_cond_t。该条件变量应该已经被初始化,通常使用 pthread_cond_init 函数进行初始化。
-
mutex: 这是一个指向互斥锁的指针,类型为 pthread_mutex_t。在调用 pthread_cond_wait 之前,必须先锁定这个互斥锁。pthread_cond_wait 会在等待条件满足时自动释放互斥锁,并且在线程被唤醒后重新获取它。
- 返回值
0: 如果操作成功。
非0值: 如果发生错误,返回一个错误代码。常见的错误码包括 EINVAL(无效的条件变量)和 EDEADLK(死锁)。
2.2.4 pthread_cond_destroy()
- 功能:
pthread_cond_destroy 用于销毁先前初始化的条件变量。当线程不再需要使用条件变量时,调用此函数释放与条件变量相关联的资源。在使用该函数前,须确保没有其他线程使用该条件变量。 - 函数原型:
int pthread_cond_destroy(pthread_cond_t *cond);
-
参数
cond: 这是一个指向条件变量的指针,类型为 pthread_cond_t。该条件变量应该已经被初始化,并且在销毁之前,它不能再被使用。 -
返回值
0: 如果成功销毁条件变量。
非0值: 如果销毁失败,返回一个错误代码。常见的错误码包括 EINVAL(无效的条件变量)等。
2.2.5 综合示例代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int shared_data = 0;void* producer(void* arg) {pthread_mutex_lock(&mutex);// 模拟生产者生产数据shared_data = 1;printf("Producer: Data produced\n");// 通知消费者线程pthread_cond_signal(&cond); // 唤醒一个等待线程pthread_mutex_unlock(&mutex);return NULL;
}void* consumer(void* arg) {pthread_mutex_lock(&mutex);// 等待条件变量,直到共享数据被生产者修改while (shared_data == 0) {pthread_cond_wait(&cond, &mutex); // 等待条件满足}// 消费数据printf("Consumer: Data consumed\n");pthread_mutex_unlock(&mutex);return NULL;
}int main() {pthread_t prod_thread, cons_thread;// 创建生产者和消费者线程pthread_create(&prod_thread, NULL, producer, NULL);pthread_create(&cons_thread, NULL, consumer, NULL);// 等待线程结束pthread_join(prod_thread, NULL);pthread_join(cons_thread, NULL);// 销毁条件变量pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);return 0;
}
三. 最后
本文详述了多线程编程中线程互斥与同步的核心机制。互斥部分通过售票程序案例,阐释了互斥量(mutex)如何通过加锁确保共享资源独占访问,并介绍了相关API及C++封装技巧。同步部分聚焦条件变量,解析其与互斥量配合解决线程协调问题的原理,包括虚假唤醒处理、信号/广播机制,并通过生产者-消费者示例展示完整同步流程。强调RAII机制在资源管理中的优势及设计要点,为构建高效线程安全程序提供理论支撑与实践指导。