模拟实现临界区和临界资源
其实在多线程中,几乎我们访问到的临界区和临界资源较多,所以我们通过多线程就可以很简单的构造这么一个环境。如下代码:
int count = 0;
void* func(void* args)
{
while(1)
{
count++;
sleep(1);
}
pthread_exit((void*)6);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,func,nullptr);
while(1)
{
printf("我和多线程访问的是同一个全局变量count:%d\n",count);
sleep(1);
}
pthread_join(tid,nullptr);
return 0;
}
此时我们相当如实现了主线程和新线程之间的通信,其中全局变量const称之为临界资源,因为他被多个执行流共享,而主线程中的printf和新线程中的const++称之为临界区,因为这些代码对临界资源进行了访问。
互斥和原子性
当多个线程同时多一个临界资源进行访问修改的时候,那么此时就可能会导致不一致的问题,解决该问题的方案叫做互斥,互斥的作用就是保证任何时候只有一个执行流通过临界区进行对临界资源访问。
我们可以自定义实现一个抢票系统来验证的观察以下
int tickets = 1000;//总票数
void* func(void* args)
{
const char* name = (char*)args;
printf("begin:%s\n",name);
while (1)
{
if (tickets > 0)
{
//以微妙为单位
usleep(2000);//1秒 = 1000毫秒 ,1毫秒 = 1000微秒
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 tid[4];
//char str[128];
for(int i = 0;i < 4;i++)
{
//sprintf(str,"thread: %d",i);
char* str=new char[128];
memset(str,0,128);
snprintf(str,128,"thread: %d",i+1);
printf("name:%s\n",str);
//这一块首先我们是将str的首地址传入到func中,虽然说线程栈是私有的,但是每个都指向str数组本身,所以最后一个线程name覆盖
pthread_create(tid + i, NULL, func, (void*)str);
}
for(int i = 0;i < 4;++i)
{
pthread_join(tid[i],NULL);
}
printf("回收线程成功\n");
return 0;
}
我们发现,在末尾结束的时候,票是已经成了负数了,这就是多个线程抢占临界资源导致的结果,出现这个结果的原因是:
--tickte
操作本身不是一个原子操作可以通过互斥来解决上述问题
要解决上述抢票系统并发的情况我们需要做到以下的方式:
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;
销毁互斥量的函数为:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
返回值说明:
销毁互斥量需要注意以下几点:
互斥量加锁的函数如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
返回值说明:
当调用互斥锁(pthread_mutex_lock)时,可能会遇到以下情况:
pthread_mutex_lock
就会陷入阻塞(执行流被挂起),等待互斥量解锁。互斥量解锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明:
返回值说明:
接下来,我们来整体模拟一下以上所有的接口,就通过抢票系统来进行
int tickets = 1000;//总票数
//pthread_mutex_t mutex;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态锁
void* func(void* args)
{
const char* name = (char*)args;
while (1)
{
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(20);
printf("[%s] get a ticket, left: %d\n", name, --tickets);
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
usleep(1000); //充当抢完一张票,后续动作
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main()
{
//初始化锁
//pthread_mutex_init(&mutex,NULL);//动态锁
pthread_t tid[4];
//char str[128];
for(int i = 0;i < 4;i++)
{
//sprintf(str,"thread: %d",i);
char* str = new char[128];
memset(str,0,128);
snprintf(str,128,"thread: %d",i+1);
//这一块首先我们是将str的首地址传入到func中,虽然说线程栈是私有的,但是每个都指向str数组本身,所以最后一个线程name覆盖
pthread_create(tid + i, NULL, func, (void*)str);
}
for(int i = 0;i < 4;++i)
{
pthread_join(tid[i],NULL);
}
printf("回收线程成功\n");
//销毁锁
//pthread_mutex_destroy(&mutex);
return 0;
}
加锁后是怎么实现原子性的
当我们使用互斥量后,其他线程看待我们的态度就变为了这个线程是否上锁和未上锁。
我们通过一个图来理解一下,当线程1进行上锁时来了好多线程,这时他们共同抢占一个资源,当这个资源处于别线程1进行上锁的状态时,其他线程是不可以进入的,至于线程 1 在里面干啥都可以,只要它一直处于上锁状态就行。
此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。
临界区内可以进行线程切换吗?
临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。
其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。
锁是否需要保护?
我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。
既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?
锁实际是一个原子性的,我们只需要保证申请锁的过程是原子的就行,它可以自己保护自己
为什么说申请锁的操作是原子的
下面我们来看看lock和unlock的伪代码:
我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:
例如,此时内存中mutex的值为1,线程申请锁时先将al寄存器中的值清0,然后将al寄存器中的值与内存中mutex的值进行交换。
交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问。
而此后的线程若是再申请锁,与内存中的mutex交换得到的值就是0了,此时该线程申请锁失败,需要被挂起等待,直到锁被释放后再次竞争申请锁。
注意:
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
例如上图所示:如果筷子的数量有限,这时,如果每个人手上分别拿一个筷子,谁也不让谁,此时就产生了死锁。
单执行流可能产生死锁码?
单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。
#include
#include
pthread_mutex_t mutex;
void* Routine(void* arg)
{
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
pthread_exit((void*)0);
}
int main()
{
pthread_t tid;
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid, NULL, Routine, NULL);
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
此时程序一直处于挂起的状态
用ps命令查看该进程时可以看到,该进程当前的状态是Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。
进程运行时被CPU调度,换句话说进程在调度时是需要用到CPU资源的,每个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);
区别:
参数说明:
返回值说明:
#include
#include
using namespace std;
const int nums = 5;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *active(void *args)
{
string name = static_cast<const char *>(args);
while (1)
{
pthread_mutex_lock(&mutex);
// pthread_cond_wait,调用的时候,会自动释放锁。
//起到阻塞作用
pthread_cond_wait(&cond, &mutex);
cout << name << " 活动" << endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tids[nums];
for (int i = 0; i < nums; ++i)
{
char *name = new char[32];
snprintf(name, 32, "thread -- %d", i + 1);
pthread_create(tids + i, nullptr, active, name);
}
cout << "开始等待" << endl;
sleep(3);
cout << "等待结束" << endl;
while(1)
{
cout << "唤醒等待队列的第一个线程" << endl;
pthread_cond_signal(&cond);
sleep(3);
}
for(int i = 0;i < nums;i++)
{
pthread_join(tids[i],nullptr);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
我们可以从图中看出,我们的等待条件变量满足的作用就是起到了阻塞作用和有序。
总结一下:
等待条件变量的代码
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);