一、线程互斥
1.1 线程间互斥的概念
在学习管道的时候,管道是自带同步与互斥的。而在线程中,当多个线程没有加锁的情况下同时访问临界资源时会发生混乱。在举例之前,先了解几个概念。
- 临界资源:多个线程执行流共享的资源叫做临界资源
- 临界区:每个线程内部访问临界资源的代码叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完
成
1.2 互斥量
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
我们可以通过一个买票的例子,来看这块问题。
int ticket = 2000;
void *STicket(void *asg)
{
while (1)
{
if (ticket > 0)
{
usleep(100);
printf("%s sang ticket:%d \n", (char *)asg, ticket--);
}
else
{
break;
}
}
return NULL;
}
int main()
{
pthread_t t[4];
int i;
for (i = 0; i < 4; i++)
{
char *p = (char *)malloc(sizeof(char) * 64);
sprintf(p, "pthread t%d", i);
pthread_create(&t[i], NULL, STicket, (void *)p);
}
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
pthread_join(t[2], NULL);
pthread_join(t[3], NULL);
return 0;
}
我们在运行结果中可以看到,票的数量本不可能出现负数的,但是在结果中出现了,那么这就是一个问题。
多个线程并发的访问同一块临界资源,我们用t1,t2,t3,t4,来表示四个线程。一开始票的数量有1000张。
《出现问题1》当t1首先访问到票时,判断票还有剩余,于是拿走一张票,票还剩999张。但是这些线程是并发执行的,有可能多个线程同时拿到票,且通过对票进行减减操作,那么这个票是重复了。
《出现问题2》当t3拿到票的时候,刚准备对票进行减减,时间片就到了,线程退出,那么在t3这个线程内把读取到的票的数量保存起来,当t3这个线程有运行时,先恢复上下文数据,然后对山下文数据中保存票的数量进行减减,当t3这个线程完成了操作后,把剩余票的数量进行更新,那么在t3没有运行前,票已经抢完了,但是t3它不知道,然后又把票的数量进行更新了,票又回来了,这个时候又出错了。出现负数的情况就是这样。
在我们判断票是否有剩余的时候,和对票减减的时候,并不是具有原子性的,因为这个时候,其他线程也在进行抢票,可能拿到重复的票。我们可以通过汇编来验证是否具有原子性。
int main()
{
int a = 5;
a--;
return 0;
}
--操作并不是原子性,而是对应了三条汇编:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
想要解决上面的问题,需要做到三点:
- 代码必须要有互斥行为:当一个线程的代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
- 果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
而以上的三点本质就是加一把锁,在Linux上提供的这把锁叫做互斥量。
先要理解这个锁。当多个线程同时要执行临界区的代码,那么谁先申请到这把锁,谁就执行,其他的线程就开始进行等待,等待这把锁被释放,然后申请这把锁。