互斥由来以及相关概念
运行如下代码可以看到,这里是一个抢票的逻辑,让五个线程同时去抢100张票,如果对线程不加以限制的话,会产生票会变为负数也就是过度抢票的情况。
int tickets=100;
void* route(void* args)
{
char* id=(char*)args;
while(1)
{
if(tickets > 0)
{
usleep(1000);
printf("我是线程%s,正在进行抢票,票还剩%d张\n",id,tickets);
tickets--;
}
else
{
printf("票已经抢完了\n");
break;
}
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
这里就需要提到几个概念,例如像这里可以被多个线程的执行流共享的资源就叫做临界资源,其中每个线程对临界资源进行访问的代码就被叫做临界区。如果不对线程进行相关的限制,则会可能出现对临界资源的过度访问从而引发错误。因此Linux下的线程便有了互斥这个概念。互斥指的是在任何时刻,互斥可以保证有且仅有一个执行流能够进入临界区来访问临界资源,这样可以对临界资源起到保护作用。同时这里还有原子性的概念,所谓原子性正是一个操作不会被任何调度机制打断的操作,即要么该操作完成要么就是未完成,有且仅有这两种状态。 上面代码出现逻辑错误,使得过度抢票正是可能有以下原因:
- if语句判断为真后,但是线程被切换走,先去运行其他线程,导致再被切回来的时候票的数量已经小于0了
- 在usleep过程中,可能有其他进行访问了临界资源
- tickets–的操作本身就是一个非原子性的操作。–的操作对应3条汇编指令
load :将共享变量ticket从内存加载到寄存器中 update : 更新寄存器里面的值,执行-1操作 store :将新值,从寄存器写回共享变量ticket的内存地址 在这个过程中tickets的值可能已经发生改变了。 为了解决多个线程访问临界资源的问题,代码必须做到以下几点: 1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。 2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。 3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。 因此这就引入了锁的概念,也就是互斥量。
互斥量
互斥量可以形象的比喻为一把锁。想要访问某个临界资源,需要带着这把锁进行访问,这样可以保证临界资源同时只能有一个执行流能够进行访问。下面来认识一下互斥量的接口函数。
初始化互斥量
从上图中可以看到,互斥量是一种新的数据类型pthread_mutex_t。该变量有两种初始化方式:第一种是使用宏定义的变量在初始化的时候进行定义,例如:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
第二种方法则是使用初始化函数进行初始化 其中第一个参数是一个输出型参数,是需要进行初始化的互斥量 第二个参数一般设置为NULL即可。
销毁互斥量
在上图中,除了创建互斥量以外还有对互斥量进行删除的接口函数。pthread_mutex_destroy该函数可以对创建的互斥量进行销毁,就像使用malloc等动态申请的空间需要手动进行销毁。这里的销毁只需要传入需要销毁的互斥量即可。 但是需要注意以下几点: 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 不要销毁一个已经加锁的互斥量 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
加锁和解锁
在申请了互斥量之后,可以将该锁给予各个线程,某个线程获得锁的过程就叫做加锁,而将锁释放则是进行解锁操作,相关的函数接口如下: 这里的pthread_mutex_lock和pthread_mutex_unlock函数传入参数都很简单,只需要传入对应的互斥量即可,下面来尝试一下使用锁来改进一下抢票的程序。
int tickets=100;
pthread_mutex_t mtx;
void* route(void* args)
{
char* id=(char*)args;
while(1)
{
pthread_mutex_lock(&mtx);
if(tickets > 0)
{
usleep(1000);
printf("我是线程%s,正在进行抢票,票还剩%d张\n",id,tickets);
tickets--;
}
else
{
printf("票已经抢完了\n");
pthread_mutex_unlock(&mtx);
break;
}
pthread_mutex_unlock(&mtx);
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mtx,nullptr);
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mtx);
return 0;
}
这里可以看到,使用了加锁之后,只能同时允许一个线程进行对临界资源的访问。避免了对临界资源过度访问的情况,但是这有时会引入一个问题:就是某个线程可能在申请锁并释放锁后反复进行再次申请,使得其他线程没办法竞争到锁,从而导致线程饥饿问题,这个问题需要使用同步来解决,可以参考下一篇博客中介绍同步的内容。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。例如某个进程可能申请锁后并没有进行释放进程便结束或者切换走,此时锁也被保存在上下文数据中一起带走了,其他线程也没办法申请到锁。 产生死锁有如下4个必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
因此为了避免死锁问题可以从如下几个方面入手: 1.破坏上面的四个必要条件 2.加锁顺序一致 3.避免锁未释放情况 4.资源一次性释放
|