临界资源和临界区
进程之间如果要进行通信我们需要先创建第三方资源,让不同的进程看到同一份资源,由于这份第三方资源可以由操作系统中的不同模块提供,于是进程间通信的方式有很多种。进程间通信中的第三方资源就叫做临界资源,访问第三方资源的代码就叫做临界区。
而多线程的大部分资源都是共享的,线程之间进行通信不需要费那么大的劲去创建第三方资源。
互斥和原子性
在多线程情况下,如果这多个执行流都自顾自的对临界资源进行操作,那么此时就可能导致数据不一致的问题。解决该问题的方案就叫做互斥,互斥的作用就是,保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。
原子性指的是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成。
例如,下面我们模拟实现一个抢票系统,我们将记录票的剩余张数的变量定义为全局变量,主线程创建四个新线程,让这四个新线程进行抢票,当票被抢完后这四个线程自动退出。
#include
#include
#include
int tickets = 1000;
void* TicketGrabbing(void* arg)
{
const char* name = (char*)arg;
while (1){
if (tickets > 0){
usleep(10000);
printf("[%s] get a ticket, left: %d\n", name, --tickets);
}
else{
break;
}
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
运行结果显然不符合我们的预期,因为其中出现了剩余票数为负数的情况。
该代码中记录剩余票数的变量tickets就是临界资源,因为它被多个执行流同时访问,而判断tickets是否大于0、打印剩余票数以及–tickets这些代码就是临界区,因为这些代码对临界资源进行了访问。
剩余票数出现负数的原因:
为什么–ticket不是原子操作?
我们对一个变量进行–,我们实际需要进行以下三个步骤:
要解决上述抢票系统的问题,需要做到三点:
要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。
初始化互斥量
初始化互斥量的函数叫做pthread_mutex_init,该函数的函数原型如下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
返回值说明:
调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
销毁互斥量
销毁互斥量的函数叫做pthread_mutex_destroy,该函数的函数原型如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
销毁互斥量需要注意:
互斥量加锁
互斥量加锁的函数叫做pthread_mutex_lock,该函数的函数原型如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
mutex:需要加锁的互斥量。
返回值说明:
互斥量加锁成功返回0,失败返回错误码。
调用pthread_mutex_lock时,可能会遇到以下情况:
互斥量解锁
互斥量解锁的函数叫做pthread_mutex_unlock,该函数的函数原型如下:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明:
返回值说明:
加锁后的原子性体现在哪里?
引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。
例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了。
此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。
临界区内的线程可能进行线程切换吗?
临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。
其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。
锁是否需要被保护?
我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。
既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?
锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。
如何保证申请锁的过程是原子的?
我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:
注意:
注意: 线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数被重入进入。
不保护共享变量的函数。
函数状态随着被调用,状态发生变化的函数。
返回指向静态变量指针的函数。
调用线程不安全函数的函数。
单执行流可能产生死锁吗?
单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。
什么叫做阻塞?
进程运行时是被CPU调度的,换句话说进程在调度时是需要用到CPU资源的,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的。
在运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等待队列。
例如,当某一个进程在被CPU调度时,该进程需要用到锁的资源,但是此时锁的资源正在被其他进程使用:
注意: 这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。
除此之外,还有一些避免死锁的算法,比如死锁检测算法和银行家算法。
同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
例如,现在有两个线程访问一块临界区,一个线程往临界区写入数据,另一个线程从临界区读取数据,但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行写入操作,直到临界区被写满,此后该线程就一直在进行申请锁和释放锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法进行数据的读取,引入同步后该问题就能很好的解决。
条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。
条件变量主要包括两个动作:
条件变量通常需要配合互斥锁一起使用。
初始化条件变量
初始化条件变量的函数叫做pthread_cond_init,该函数的函数原型如下:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数说明:
返回值说明:
调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁条件变量
销毁条件变量的函数叫做pthread_cond_destroy,该函数的函数原型如下:
int pthread_cond_destroy(pthread_cond_t *cond);
参数说明:
返回值说明:
销毁条件变量需要注意:
等待条件变量满足
等待条件变量满足的函数叫做pthread_cond_wait,该函数的函数原型如下:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数说明:
返回值说明:
唤醒等待
唤醒等待的函数有以下两个:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
区别:
参数说明:
返回值说明:
总结一下:
等待条件变量的代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
唤醒等待线程的代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);