1.介绍
并发是现代计算中的一个基本概念,其中多个任务或进程同时执行。在并行和分布式系统的世界中,理解并发对于开发高效可靠的软件至关重要。
竞态条件(Race Conditions)
当系统的行为依赖于事件的相对时间时,就会出现竞争条件,特别是在并发系统中。当多个线程或进程在没有适当同步的情况下访问共享资源时,就会发生这种情况,从而导致不可预测和可能不正确的结果。
特征:
- 多个线程访问共享资源
- 缺乏适当的同步机制
- 执行顺序会影响最终结果
类型:
- 读-修改-写竞态条件
在这种类型中,多个线程读取共享变量,修改它,并回写结果,可能会覆盖彼此的修改。 - 检查-行动竞态条件
线程检查条件,然后根据该条件采取操作,在检查和操作之间可能会发生变化。 - 复合竞态条件
涉及多个共享资源和线程之间复杂交互的复杂场景。
预防措施
- 互斥(Mutex)
使用锁来确保一次只有一个线程可以访问临界区,用于提供对共享资源的独占访问。可以把互斥锁看作是一个数字锁,它确保在任何给定时刻只有一个线程可以进入代码的临界区。该机制防止多个线程同时修改共享数据,这可能导致不一致或损坏的状态。。 - 原子操作(Atomic Operations)
利用编程语言和硬件提供的原子指令。 - 同步原语(Synchronization Primitives)
- 信号量
信号量维护一个表示可用资源数量的内部计数器。线程可以对信号量执行两种主要操作:等待(减少计数器)和发出信号(增加计数器)。当信号量的计数器达到零时,试图获取信号量的后续线程将被阻塞,直到资源可用为止。
有两种主要类型的信号量:二进制信号量和计数信号量。二进制信号量的功能类似于互斥锁,只允许两种状态——锁定和解锁。 - 条件变量
- 读写锁
代码例子:
#include <stdio.h>
#include <pthread.h>
int shared_counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// Race Condition Version
void* increment_without_mutex(void* arg) {
for (int i = 0; i < 100000; i++) {
shared_counter++; // Unsafe increment
}
return NULL;
}
// Safe Synchronized Version
void* increment_with_mutex(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
shared_counter++; // Safe increment
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t threads[2];
// Demonstrate Race Condition
printf("Demonstrating Race Condition:\n");
shared_counter = 0;
for (int i = 0; i < 2; i++) {
pthread_create(&threads[i], NULL, increment_without_mutex, NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(threads[i], NULL);
}
printf("Final Counter (Unsafe): %d\n", shared_counter);
// Demonstrate Safe Synchronization
printf("\nDemonstrating Safe Synchronization:\n");
shared_counter = 0;
for (int i = 0; i < 2; i++) {
pthread_create(&threads[i], NULL, increment_with_mutex, NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(threads[i], NULL);
}
printf("Final Counter (Safe): %d\n", shared_counter);
return 0;
}
2.同步(Synchronization)
同步是并发编程的基础,它代表了一种复杂的机制,可以确保多个线程或进程之间有序且可预测的交互。在复杂的现代计算环境中,并行处理已成为常态,同步技术为管理共享资源、防止数据竞争和维护系统完整性提供了关键框架。当多个线程试图执行,同步的核心是解决在多线程环境中协调对共享资源的访问。
死锁(Deadlock)
死锁的特征有四个基本条件,它们必须同时存在才能发生死锁。这些条件首先由计算机科学先驱提出,为理解和防止这些复杂的同步故障提供了一个框架。
- 涉及互斥。 即资源不能同时被多个线程共享。这意味着,当一个资源被分配给一个线程时,在显式释放之前,其他线程将无法使用该资源。虽然互斥对于维护数据完整性至关重要,但它也可能造成资源争用。
- 保持和等待场景。 在这种情况下,当前持有一个资源的线程同时请求额外的资源。如果这些额外的资源已经分配给其他线程,则请求线程在保留其初始资源的同时进入等待状态。这就产生了可能导致系统锁定的潜在循环依赖。
- 没有抢占。 在没有抢占的系统中,不能从当前持有资源的线程强制检索资源。一旦资源被分配,它将一直留在线程中,直到该线程主动释放它。这个特性防止了资源分配中的外部干预,从而可能导致死锁情况无限期地持续下去。
- 涉及循环等待。 一组线程在循环链中等待彼此持有的资源。想象一下这样一个场景:线程a持有资源X并想要资源Y,线程B持有资源Y并想要资源Z,线程C持有资源Z但想要资源X。这种循环依赖创造了一个完美的死锁场景,没有可能解决。