临界区(Critical Section)是指一段代码或资源的访问区域,这段代码在多线程或多进程环境下被多个线程或进程同时访问时,会导致数据不一致或不可预期的问题。因此,临界区需要保护,保证同一时间只有一个线程或进程能够进入执行。
银行柜台取钱
在这种情况下,可以用锁(如互斥锁)来保护临界区,确保只有一个人能同时操作账户余额:
这样,账户余额始终是正确且一致的。
我们需要保护临界区的原因是为了避免竞争条件(Race Condition)的发生,确保程序中的数据一致性和操作正确性。
在多线程或多进程环境下,多个线程或进程可能会同时访问共享资源(如变量、文件、数据库)。如果这些访问是并发进行的,而没有同步机制保护,可能导致以下问题:
在工作场景中,比如网络通信和驱动开发,经常会用到全局变量、文件描述符、链表等共享资源,这些地方的临界区保护尤为重要。
并发执行(Concurrency)是指在同一时间段内,多个任务或进程(线程)能够交替执行或共享计算资源。并发执行的原因可以简单理解为:当多个任务或者进程需要在同一时间段内执行时,操作系统会通过分配计算资源,使它们看似同时进行。以下是一些简化后的原因:
多核CPU:就像一台机器有多个“工人”一起工作,多个任务可以分配给不同的核心,让它们同时进行。
操作系统的“调度员”:操作系统就像一个忙碌的调度员,它把时间分给每个任务,虽然它们轮流使用CPU,但给人感觉是同时进行。
等待I/O:当程序需要等待文件读取或网络请求时,操作系统让其他任务“顶替”使用CPU,直到原任务完成。
设计上的选择:有些程序设计时就决定同时做多件事,比如一个网站可以同时处理多个用户请求。
事件驱动:有些应用(如聊天软件)会等待不同的事件发生,它们不会阻塞,而是同时处理多个请求。
资源争夺:多个任务抢同一个资源时,操作系统让它们轮流使用,这样每个任务看起来都在同时执行。
通过这些方式,多个任务能够在同一时间段内并发执行,从而提高效率。
哪些数据需要加锁保护?
以下是一个表格,列举了哪些数据需要加锁保护,哪些数据不需要加锁保护:
数据类型 | 是否需要加锁保护 | 说明 |
---|---|---|
局部自动变量 | 不需要加锁保护 | 这些变量仅在单个线程的栈中,其他线程无法访问。 |
堆栈上分配的动态数据 | 不需要加锁保护 | 动态分配的内存地址存储在堆栈中,只有当前线程访问。 |
全局变量(多个线程共享) | 需要加锁保护 | 如果多个线程同时访问全局变量,可能会引起数据竞态问题,需加锁保护。 |
静态变量(多个线程共享) | 需要加锁保护 | 静态变量通常在程序运行期间存在,多个线程访问时需加锁保护以避免竞态。 |
堆内存分配的数据(多个线程共享) | 需要加锁保护 | 如果多个线程访问同一块堆内存,需要加锁防止数据竞争。 |
线程局部存储(TLS) | 不需要加锁保护 | 每个线程有独立的TLS,不与其他线程共享数据,线程间不会干扰。 |
互斥量(锁)本身 | 需要加锁保护 | 互斥量在多线程环境中用于保护共享资源,因此本身需要加锁。 |
文件句柄(多个线程共享) | 需要加锁保护 | 如果多个线程同时读写同一个文件句柄,可能会造成资源冲突,需加锁保护。 |
总结:
死锁 是指多个线程在执行过程中,由于竞争资源或通信问题,导致互相等待的情况,从而造成程序无法继续执行。具体来说,死锁会让程序中的线程陷入一种 无休止的等待状态,因为每个线程都在等待其他线程释放它们需要的资源,而这些资源永远得不到释放。
死锁的发生一般需要满足以下四个条件:
互斥条件(Mutual Exclusion):至少有一个资源是以 非共享 的方式被分配给某个线程,即一个资源一次只能被一个线程占用。
占有并等待条件(Hold and Wait):一个线程在持有至少一个资源的同时,等待获取其他线程占用的资源。
非抢占条件(No Preemption):已分配给线程的资源,不能被其他线程抢占,线程必须在完成后释放资源。
循环等待条件(Circular Wait):存在一种线程资源的循环等待关系,假设线程 T1 等待 T2 占有的资源,T2 等待 T3 占有的资源,…,最后 Tn 等待 T1 占有的资源。
如果这四个条件同时成立,就可能发生死锁。
假设有两个线程 T1
和 T2
,以及两个资源 R1
和 R2
:
R1
,等待 R2
。R2
,等待 R1
。这时 T1
和 T2
形成了循环等待的关系,导致死锁。
下面是一个简单的 C 语言死锁示例:
#include
#include
#include
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1_func(void *arg) {
pthread_mutex_lock(&mutex1);
printf("Thread 1: Locked mutex1\n");
// 模拟某些处理
sleep(1);
pthread_mutex_lock(&mutex2);
printf("Thread 1: Locked mutex2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void *thread2_func(void *arg) {
pthread_mutex_lock(&mutex2);
printf("Thread 2: Locked mutex2\n");
// 模拟某些处理
sleep(1);
pthread_mutex_lock(&mutex1);
printf("Thread 2: Locked mutex1\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 创建两个线程
pthread_create(&thread1, NULL, thread1_func, NULL);
pthread_create(&thread2, NULL, thread2_func, NULL);
// 等待两个线程完成
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
thread1_func
线程首先锁定了 mutex1
,然后等待锁定 mutex2
。thread2_func
线程首先锁定了 mutex2
,然后等待锁定 mutex1
。thread1
和 thread2
都在等待对方释放锁,程序无法继续执行。Thread 1: Locked mutex1
Thread 2: Locked mutex2
此时程序会停止,因为两个线程互相等待对方释放锁,无法继续执行下去。
避免死锁:
mutex1
,然后申请 mutex2
,这样就避免了交叉等待。死锁检测与恢复:
使用死锁避免算法:
使用锁的时限或尝试获取锁:
pthread_mutex_trylock
:尝试获取锁,如果获取不到则返回,而不会阻塞线程。// 假设我们有两个资源 mutex1 和 mutex2
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
// 按照相同顺序申请锁,可以避免死锁
void *thread1_func(void *arg) {
pthread_mutex_lock(&mutex1); // 先锁 mutex1
printf("Thread 1: Locked mutex1\n");
sleep(1);
pthread_mutex_lock(&mutex2); // 再锁 mutex2
printf("Thread 1: Locked mutex2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void *thread2_func(void *arg) {
pthread_mutex_lock(&mutex1); // 先锁 mutex1
printf("Thread 2: Locked mutex1\n");
sleep(1);
pthread_mutex_lock(&mutex2); // 再锁 mutex2
printf("Thread 2: Locked mutex2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
pthread_mutex_trylock
避免死锁void *thread_func(void *arg) {
if (pthread_mutex_trylock(&mutex1) != 0) {
printf("Failed to lock mutex1\n");
return NULL;
}
printf("Locked mutex1\n");
sleep(1);
if (pthread_mutex_trylock(&mutex2) != 0) {
printf("Failed to lock mutex2\n");
pthread_mutex_unlock(&mutex1);
return NULL;
}
printf("Locked mutex2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
通过 pthread_mutex_trylock
,如果无法获取锁,则立即返回,而不会造成死锁。
通信导致的死锁通常出现在 并发通信系统 中,其中多个线程或进程通过共享资源(如消息队列、共享内存、套接字等)进行通信。当这些线程或进程之间的通信流程不当时,可能会因为资源竞争或通信等待的交叉关系导致死锁。以下是几个常见的通信死锁场景,并通过代码示例说明。
在以下的示例中,我们考虑两个线程使用 消息队列 或 信号量 进行通信。如果线程之间的同步不当,可能会发生死锁。
在这个例子中,线程 A 向消息队列发送消息,线程 B 从消息队列接收消息,但是线程 A 需要等待线程 B 执行一些操作来继续,它们之间形成了循环等待。
#include
#include
#include
#include
#include
#define MAX_MSG 1
sem_t semA, semB; // 信号量,用来同步两个线程
int message = 0; // 模拟消息队列
void *threadA(void *arg) {
while (1) {
// 等待线程B完成
sem_wait(&semB);
// 发送消息
message = 1;
printf("Thread A: Message sent to thread B\n");
// 等待线程B确认
sem_post(&semA); // 释放给线程B的信号
}
return NULL;
}
void *threadB(void *arg) {
while (1) {
// 等待线程A发送消息
sem_wait(&semA);
if (message == 1) {
printf("Thread B: Received message from A\n");
// 模拟处理消息
message = 0;
sleep(1); // 模拟处理时间
// 发送回信号通知线程A
sem_post(&semB); // 释放给线程A的信号
}
}
return NULL;
}
int main() {
pthread_t tA, tB;
// 初始化信号量
sem_init(&semA, 0, 0);
sem_init(&semB, 0, 0);
// 创建线程
pthread_create(&tA, NULL, threadA, NULL);
pthread_create(&tB, NULL, threadB, NULL);
// 激活通信,开始线程A的工作
sem_post(&semB);
// 等待线程完成
pthread_join(tA, NULL);
pthread_join(tB, NULL);
// 清理资源
sem_destroy(&semA);
sem_destroy(&semB);
return 0;
}
Thread A: Message sent to thread B
然后程序就会停止,两个线程永远互相等待对方的信号,无法继续执行。
另一个常见的情况是使用 信号量 进行同步。如果两个线程同时等待对方的信号量,而没有先释放自己占用的资源,也会造成死锁。
#include
#include
#include
sem_t sem1, sem2;
void* thread1_func(void* arg) {
sem_wait(&sem1); // 获取信号量 sem1
printf("Thread 1 acquired sem1\n");
// 模拟一些工作
sleep(1);
sem_wait(&sem2); // 等待信号量 sem2
printf("Thread 1 acquired sem2\n");
sem_post(&sem2); // 释放信号量 sem2
sem_post(&sem1); // 释放信号量 sem1
return NULL;
}
void* thread2_func(void* arg) {
sem_wait(&sem2); // 获取信号量 sem2
printf("Thread 2 acquired sem2\n");
// 模拟一些工作
sleep(1);
sem_wait(&sem1); // 等待信号量 sem1
printf("Thread 2 acquired sem1\n");
sem_post(&sem1); // 释放信号量 sem1
sem_post(&sem2); // 释放信号量 sem2
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 初始化信号量
sem_init(&sem1, 0, 1);
sem_init(&sem2, 0, 1);
// 创建线程
pthread_create(&thread1, NULL, thread1_func, NULL);
pthread_create(&thread2, NULL, thread2_func, NULL);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁信号量
sem_destroy(&sem1);
sem_destroy(&sem2);
return 0;
}
sem1
后,接着等待 sem2
。sem2
后,接着等待 sem1
。资源分配顺序一致:
sem1
,再获取 sem2
。避免嵌套信号量操作:
超时机制:
sem_timedwait
),避免线程无限期阻塞。死锁检测:
哲学家进餐问题解决方案:
优先级继承:
以下是解决上述代码死锁问题的改进版本:
void* thread1_func(void* arg) {
sem_wait(&sem1); // 按顺序获取 sem1
printf("Thread 1 acquired sem1\n");
sleep(1);
sem_wait(&sem2); // 然后获取 sem2
printf("Thread 1 acquired sem2\n");
sem_post(&sem2); // 按顺序释放 sem2
sem_post(&sem1); // 再释放 sem1
return NULL;
}
void* thread2_func(void* arg) {
sem_wait(&sem1); // 按顺序获取 sem1
printf("Thread 2 acquired sem1\n");
sleep(1);
sem_wait(&sem2); // 然后获取 sem2
printf("Thread 2 acquired sem2\n");
sem_post(&sem2); // 按顺序释放 sem2
sem_post(&sem1); // 再释放 sem1
return NULL;
}
通信资源的竞争:多个线程或进程在进行通信时,必须争夺有限的资源(例如,消息队列、信号量、共享内存等)。如果资源的请求顺序不合理,或者线程之间的同步机制设计不当,就可能引发死锁。
等待通信的交叉关系:死锁通常发生在线程之间由于同步不当,导致它们互相等待对方的操作完成。例如,一个线程等待从另一个线程接收消息,而另一个线程又等待第一个线程发送消息。
没有超时机制或回退机制:如果通信没有设置超时或回退机制,线程可能一直在等待对方的响应,导致死锁。
统一资源请求顺序:确保所有线程或进程在请求共享资源时遵循一致的顺序。这样可以避免产生循环等待的情况。
使用超时机制:在进行通信时,设定超时条件,如果无法获取资源或者没有收到消息,可以中止等待并采取其他措施(例如,重试、放弃或退出)。
死锁检测:使用死锁检测算法定期检查系统状态,确保没有线程进入死锁状态。
设计无死锁的通信协议:可以通过设计避免死锁的通信协议。例如,在发送消息前,确认接收方已经准备好接收消息,或者使用 非阻塞通信 来避免线程阻塞等待。
简化同步机制:尽量避免复杂的锁和同步机制,例如信号量或多个互斥锁的组合使用。可以采用简单的同步机制来减少死锁的风险。
通过这些方式,可以有效减少由于通信而导致的死锁问题。