运行如下代码可以看到,这里是一个抢票的逻辑,让五个线程同时去抢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下的线程便有了互斥这个概念。互斥指的是在任何时刻,互斥可以保证有且仅有一个执行流能够进入临界区来访问临界资源,这样可以对临界资源起到保护作用。同时这里还有原子性的概念,所谓原子性正是一个操作不会被任何调度机制打断的操作,即要么该操作完成要么就是未完成,有且仅有这两种状态。
上面代码出现逻辑错误,使得过度抢票正是可能有以下原因:
互斥量可以形象的比喻为一把锁。想要访问某个临界资源,需要带着这把锁进行访问,这样可以保证临界资源同时只能有一个执行流能够进行访问。下面来认识一下互斥量的接口函数。
从上图中可以看到,互斥量是一种新的数据类型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.资源一次性释放