线程的互斥
一、互斥的重要性
在多线程编程中,互斥机制至关重要。当多个线程同时访问临界资源时,如果没有有效的互斥控制,可能会导致数据不一致、资源竞争等问题。通过互斥锁,可以确保在任何时刻只有一个线程能够访问临界资源,从而保证程序的正确性和稳定性。
二、互斥锁的使用步骤详解
1. 定义锁:
pthread_mutex_t mutex;
定义了一个互斥锁变量。这个变量将在后续的步骤中被初始化、加锁、解锁和销毁。
2. 初始化锁:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
函数用于将定义好的互斥锁进行初始化。
◦ 参数中的mutex是要初始化的互斥锁,而attr通常设置为NULL,表示使用默认的锁属性。
◦ 成功返回 0,失败返回非零值。
3. 加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
函数用于对指定的互斥锁进行加锁操作。
◦ 加锁后的代码到解锁部分的代码被视为原子操作,这意味着在这个范围内的代码不会被其他线程中断。
◦ 如果在执行该函数时,互斥锁已经被其他线程占用,那么当前线程将被阻塞,直到互斥锁被释放。
◦ 成功返回 0,失败返回非零值。
4. 解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
函数用于将指定的互斥锁解锁。
◦ 解锁后,代码不再处于排他访问状态,其他线程可以竞争获取互斥锁来访问临界资源。
◦ 通常加锁和解锁操作会成对出现,以确保临界资源的正确访问。
◦ 成功返回 0,失败返回非零值。
5. 销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
函数在使用互斥锁完毕后用于销毁互斥锁。
◦ 成功返回 0,失败返回非零值。
6. trylock:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
函数的功能类似于加锁函数,但它不会阻塞当前线程。
◦ 如果互斥锁可用,则该函数会成功加锁并返回 0;如果互斥锁不可用,则返回非零值,通常是E_AGAIN。
三、使用互斥锁的注意事项
1. 框架设计时应尽量使保护区尽可能短,以减少线程阻塞的时间,提高程序的性能。
2. 在使用互斥锁时,要确保加锁和解锁操作的成对出现,避免出现死锁等问题。
3. 对于互斥锁的初始化和销毁操作,要确保在正确的时机进行,避免资源泄漏。
在这段代码中,互斥锁的作用是确保多个线程对全局变量 A 的自增操作是原子性的,即不会被其他线程中断。如果没有互斥锁,两个线程可能会同时读取 A 的值,然后进行自增操作,这可能会导致结果不一致。通过使用互斥锁,在一个线程对 A 进行操作时,其他线程必须等待,直到该线程完成操作并解锁互斥锁。
练习:模拟了十个人去银行办理业务,银行有三个办理窗口的场景。通过使用互斥锁来确保对共享资源(窗口数量)的安全访问,避免出现资源竞争问题。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex;
int WIN = 3; //三个办理窗口
void* th(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex); //要使用共享资源,加锁
if ( WIN > 0 ) //有窗口
{
WIN--; //占用窗口
pthread_mutex_unlock(&mutex); //解锁
printf("get WIN tid:%lu\n",pthread_self()); //表示在用窗口
sleep(rand()%5); //办理中
printf("relese WIN tid:%lu\n",pthread_self()); //表示离开
pthread_mutex_lock(&mutex);
WIN++; //离开后空出窗口
pthread_mutex_unlock(&mutex);
break;
}
else //没有窗口的情况下
{
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main(int argc, const char *argv[])
{
pthread_t tid[10]; //十个人去银行办业务
pthread_mutex_init(&mutex,NULL); //初始化互斥锁
int i;
for ( i=0; i<10; i++ )
{
pthread_create(&tid[i],NULL,th,NULL);
}
for ( i=0; i<10; i++ )
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex); //销毁互斥锁
return 0;
}
void* th(void* arg)是线程执行的函数。在这个函数中,通过一个无限循环不断尝试获取办理窗口。首先加锁,然后检查窗口数量是否大于 0。如果有窗口可用,占用一个窗口,解锁,打印获取窗口的线程 ID,表示正在使用窗口,然后随机睡眠一段时间模拟办理业务,最后再次加锁,释放窗口,增加窗口数量,解锁并退出循环。如果没有窗口可用,直接解锁。
在这段代码中,互斥锁的作用是确保多个线程在访问和修改共享资源(窗口数量)时不会出现数据不一致的情况。当一个线程检查窗口数量、占用窗口或释放窗口时,其他线程必须等待,直到该线程完成这些操作并解锁互斥锁。这样可以保证窗口数量的正确更新,避免出现多个线程同时占用同一个窗口或者窗口数量错误减少或增加的情况。
线程的同步
一、线程同步的重要性与信号量的作用
在多线程编程中,线程的同步是确保程序正确运行的关键。单纯的互斥锁只能控制对资源的排他性访问,但无法保证线程执行的先后顺序。而信号量机制则通过引入 “有一定先后顺序的对资源的排他性访问”,有效地解决了这个问题,实现了线程之间的同步。
信号量分为无名信号量和有名信号量,分别用于线程间通信和进程间通信,为不同场景下的同步需求提供了灵活的解决方案。
二、信号量使用步骤详解
1. 信号量的定义:
sem_t sem;
定义了一个信号量变量。这个变量将在后续的步骤中被初始化、进行 PV 操作以及销毁。
2. 信号量的初始化:
int sem_init(sem_t *sem, int pshared, unsigned int value);
函数用于将定义好的信号量进行赋值初始化。
◦ 当pshared = 0时,表示用于线程间使用信号量;当pshared!= 0时,表示用于进程间使用信号量。
◦ 对于无名信号量通常是二值信号量,初始值为 0 或 1。0 表示红灯,线程暂停阻塞;1 表示绿灯,线程可以通过执行。
◦ 成功返回 0,失败返回 -1。
3. 信号量的 PV 操作:
◦ P 操作(申请资源):
int sem_wait(sem_t *sem);
函数判断当前信号量是否有资源可用。如果有资源(值为 1),则申请该资源,程序继续运行,并且信号量的值会自动减 1。如果没有资源(值为 0),则线程阻塞等待,一旦有资源则自动申请资源并继续运行程序。成功返回 0,失败返回 -1。
◦ V 操作(释放资源):
int sem_post(sem_t *sem);
函数可以将指定的信号量资源释放,并默认执行信号量值加 1 的操作。线程在该函数上不会阻塞。成功返回 0,失败返回 -1。
4. 信号量的销毁:
int sem_destroy(sem_t *sem);
函数在使用完毕后将指定的信号量销毁。成功返回 0,失败返回 -1。
三、使用信号量的注意事项
1. 在使用信号量进行线程同步时,要确保正确初始化信号量的初始值和参数,以满足具体的同步需求。
2. 在进行 PV 操作时,要注意操作的顺序和时机,避免出现死锁等问题。
3. 对于信号量的销毁操作,要确保在所有线程都不再使用该信号量时进行,避免资源泄漏。
二值信号量
在这段代码中,二值信号量sem_H和sem_W起到了控制线程执行顺序的关键作用。初始状态下,sem_H为 1,使得线程 1 首先执行,打印 “Hello” 后释放sem_W,从而唤醒线程 2。线程 2 打印 “World” 后释放sem_H,又回到线程 1 执行,如此循环交替,确保 “Hello” 和 “World” 按照特定的顺序输出。
计数信号量
在这段代码中,计数信号量 WIN 的初始值为 3,表示有三个可用资源。多个线程可以同时竞争这些资源,当一个线程获取到资源时,信号量的值会减 1;当一个线程释放资源时,信号量的值会加 1。这样可以确保同时使用资源的线程数量不超过信号量的初始值,从而实现对有限资源的并发访问控制。
死锁
一、死锁的危害
死锁是多线程或多进程系统中一种严重的问题,它会导致系统资源被占用却无法释放,使得系统无法正常运行。如果死锁频繁发生,可能会造成系统性能严重下降甚至完全瘫痪,影响到整个应用程序的可用性和可靠性。
二、死锁产生原因详解
1. 系统资源不足:
◦ 当系统中的资源数量有限,而多个进程或线程同时竞争这些资源时,如果每个进程或线程都无法获取到所需的全部资源,就可能陷入死锁状态。例如,有两个进程都需要两种资源 A 和 B,而系统中只有一个 A 资源和一个 B 资源,那么如果一个进程获取了 A 资源,另一个进程获取了 B 资源,它们就会相互等待对方释放资源,从而导致死锁。
2. 进程运行推进顺序不合适:
◦ 进程的执行顺序对于避免死锁至关重要。如果进程以不恰当的顺序请求和释放资源,就可能形成死锁。例如,进程 P1 先请求资源 A,再请求资源 B,而进程 P2 先请求资源 B,再请求资源 A。如果 P1 获得了 A,P2 获得了 B,那么它们就会相互等待,从而产生死锁。
3. 资源分配不当等:
◦ 不合理的资源分配策略也可能导致死锁。例如,如果资源分配算法总是优先满足某些进程的请求,而忽略其他进程的需求,就可能导致部分进程永远无法获取到所需资源,从而陷入死锁。
三、死锁的四个必要条件分析
1. 互斥条件:
◦ 这是死锁产生的基本条件之一。如果资源可以同时被多个进程或线程使用,那么就不会出现死锁。例如,多个进程可以同时读取一个文件,而不会产生死锁。但是,如果资源是排他性的,即一次只能被一个进程或线程使用,那么就有可能出现死锁。
2. 请求与保持条件:
◦ 当一个进程因请求资源而阻塞时,如果它仍然保持着已经获得的资源,就可能导致死锁。例如,一个进程已经获得了资源 A,现在又请求资源 B,但由于 B 资源不可用而被阻塞。此时,该进程仍然持有资源 A,而其他需要资源 A 的进程就无法继续执行,从而可能形成死锁。
3. 不剥夺条件:
◦ 进程已获得的资源在未使用完之前不能被强行剥夺,这也是死锁产生的一个重要条件。如果可以强行剥夺进程的资源,那么当一个进程因请求新资源而被阻塞时,系统可以剥夺它已获得的资源,分配给其他需要的进程,从而避免死锁的发生。
4. 循环等待条件:
◦ 若干进程之间形成一种头尾相接的循环等待资源关系是死锁的典型特征。例如,进程 P1 等待进程 P2 释放资源,进程 P2 等待进程 P3 释放资源,进程 P3 又等待进程 P1 释放资源,这样就形成了循环等待,必然导致死锁。
四、避免死锁的方法
1. 预防死锁:
◦ 通过破坏死锁的四个必要条件之一来避免死锁的发生。例如,可以采用资源静态分配策略,破坏请求与保持条件;或者采用资源剥夺策略,破坏不剥夺条件。
2. 避免死锁:
◦ 在资源分配过程中,通过动态地检测系统状态,确保资源分配不会导致死锁的发生。例如,可以使用银行家算法,在分配资源之前先判断系统是否处于安全状态。
3. 检测死锁:
◦ 定期检测系统中是否存在死锁,如果发现死锁,则采取相应的措施进行解除。例如,可以通过资源分配图等方法检测死锁。
4. 解除死锁:
◦ 一旦检测到死锁,就采取相应的措施解除死锁。例如,可以剥夺某些进程的资源,或者让一些进程回滚到之前的状态,释放它们所占用的资源。