首先,我们先了解一下下面一组概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
现在,我们来举一个买票的例子帮助大家更好地理解临界资源。
假如现在有1000张票,有四个人ABCD去抢。每个人抢到票的数量不做限制,直到抢完为止。我们用一段代码来模拟这种情景。
#include
#include
#include
int tickets = 1000;//定义一个全局变量来模拟1000张票
void* TicketGrabbing(void* arg)
{
//四个线程抢票,有人抢到,tickets--
const char* msg = (char*)arg;
while(1)
{
if(tickets > 0)
{
usleep(100);
tickets--;//还有票就继续抢
printf("%s get a ticket: %d\n", msg, tickets);
}
else
{//抢没了
break;
}
}
printf("%s quit\n", msg);
pthread_exit((void*)0);
}
int main()
{
//我们创建四个线程1234来模拟四个人。
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;
}
这段代码看似没什么问题,可是当我们当看到运行结果的时候却发现了问题
为什么会出现这种结果呢,按照我们的想法,在票被抢到0的时候就应该停止抢票,然后线程退出。
这是因为,“tickets–”不是原子操作。ticket–至少要分三步完成。
那么,就有可能出现某一个线程正在进行这三步的时候,另一个线程也来了,上一个线程–还没有完成,所以这个线程看到内存中的数字和上一个线程看到的是一样的,最后内存中的数字是多少就取决于哪个线程后将数据写回内存。
此外,if(tikcets > 0)也不是原子的,那么就可能出现tickets此时已经为1了,一个线程来了,开始if(tikcets > 0),在判断还没完事的时候,另一个线程也来了,这个线程看到的tickets也是1,那么就也开始if(tikcets > 0)。然后就会出现tickets被–多次导致变成负数。
为了避免上述的现象,我们需要一把锁,当多个线程都要访问临界资源的时候,一个线程在访问临界资源之前,申请一把锁,此时,其他线程无法访问临界资源,只能等待锁被释放。这样就避免了上述的问题。
为了帮助大家更好的理解,我来举一个例子。
接下来,我们学习一下互斥量的接口
方法1:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2:动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//第一个参数就是互斥锁
//第二个参数是互斥锁创建方式,一般我们使用NULL默认即可
int pthread_mutex_lock(pthread_mutex_t *mutex);
互斥量处于未锁状态,该函数会将互斥量锁定。若其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。(这就是上面例子中竞争钥匙)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
以上四个接口都是成功返回0,失败返回错误码
当我们在销毁互斥量的时候要注意一下几点
接下来,我们就使用锁来改善一下抢票的代码
#include
#include
#include
pthread_mutex_t mutex;
int tickets = 1000;
void* Gettickets(void* arg)
{
int i = (int)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(tickets > 0)
{
usleep(100);
tickets--;
printf("线程%d抢到一张票,还剩%d张\n", i, tickets);
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
return (void*)0;
}
int main()
{
pthread_t thd[4];
pthread_mutex_init(&mutex, NULL);
int i = 0;
for(i = 0; i < 4; i++)
{
pthread_create(&thd[i], NULL, Gettickets, (void*)i);
}
for(i = 0; i < 4; i++)
{
pthread_join(thd[i], NULL);
}
return 0;
}
显然我们使用了互斥量之后,不会出现上面的问题了
首先,在绝大多数情况,加锁本身都是有损于性能的,这几乎是不可避免的。作为程序员,我们已经尽可能的减少加锁带来的性能开销成本。在多执行流下,对于临界资源的保护,是所有执行流都应该尊所的标准
锁的存在是为了保护临界资源,但是锁本身就是临界资源,所以申请锁的过程必须是原子的。也就是说pthread_mutex_lock(&mutex);这行代码必须是原子的。那么锁的原子性是如何实现的呢?
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
比如说AB进程分别占用pq资源,然后A要申请q资源,由于B不释放q资源,那么A就一直申请不到q资源。同样的,B要申请p资源,A不释放p资源,那么B也一直申请不到p资源。
联系
区别
联系
区别