从抢票说起
我们先写一个程序来模拟一下抢票:创建4个线程(代表4个人),票数为100(全局变量int g_tickets = 100;),每个执行流执行之后g_tickets减一,代表抢到了票,后面打印出抢到了第几张票。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define PTHREADCOUNT 4
int g_tickets = 100;
void* myPthread(void* arg)
{
(void)arg;
while(1)
{
if(g_tickets > 0)
{
printf("I am thread:%p,i have ticket %d\n",pthread_self(),g_tickets);
g_tickets--;
}
else
{
break;
}
}
return NULL;
}
int main()
{
pthread_t tid[PTHREADCOUNT];
for(int i = 0;i < PTHREADCOUNT;i++)
{
int ret = pthread_create(&tid[i],NULL,myPthread,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
for(int i = 0;i < PTHREADCOUNT;i++)
{
pthread_join(tid[i],NULL);
}
return 0;
}
运行结果:
从运行结果我们发现有两个不同的线程拿到了同一张"票",这显然是不可以的,这就是由于线程不安全时会引发的问题,那么什么是线程安全呢?
线程安全的概念:多个线程并发执行同一段代码时,会出现不同的结果。常jian
线程不安全的原理
说明
(1) 假设在同一个程序种有两个线程A和B,并且有一个值为100的int类型的全局变量;线程A和线程B在各自的入口函数当中都对全局变量进行++操作
(2) 当线程A拥有CPU之后,对全局变量进行++操作,但是++操作不是原子操作,这也就意味着线程A在执行加的过程中可能会被打断,假设线程A刚刚将全局变量的数值100读到CPU的寄存器当中,就被切出去了;程序计数器当中保存下一条执行的命令,上下文信息中保存程序计数器的值,这两个东西是用来当线程A再次拥有CPU的时候,回复现场使用的
(3) 这时有可能线程B拥有了CPU资源,对全局变量进行了++,并且将100加成了101,回写到内存中了
(4) 当线程A再次拥有CPU资源之后,恢复现场,继续往下执行,从寄存器当中读到的值仍然是100,加完之后为101,回写到内存中也是101
(5) 理论上线程A和线程B各自对全局变量进行了加1操作,全局变量的值应该变成102,但是现在程序计算的结果有可能是101,所以这样的的线程是不安全的
结论
线程不安全会导致程序的二义性
线程不安全的解方法
同步和互斥的相关概念
临界资源:多线程执行流共享的资源都叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:指不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么没完成
同步:指在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。
互斥锁
要想保证互斥,我们就需要用到互斥锁,互斥锁本身也是一个资源,要想保证多个线程都要互斥,就要保证每个线程都去获取互斥锁。
互斥锁的本质:在互斥锁内部有一个计数器,也就是互斥量,它的取值只能为0或者1;当线程获取互斥锁的时候,如果计数器中的值为0,表示当前线程获取不到互斥锁,也就不能再去获取临界资源了;如果计数器中的值为1,则表示当前线程可以获取到互斥锁,也就意味着可以访问临界资源,代码可以执行临界资源当中的代码。
互斥锁的接口:
(1)初始化互斥锁变量
动态初始化:
函数原型:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t*mutex,pthread_mutexattr_t *attr);
参数说明:
mutex:传入互斥变量的地址,pthread_mutex_init会初 始化互斥锁变量
attr:属性,一般传递NULL,采用默认属性
返回值:
成功返回0,失败返回错误码
静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁:
pthread_mutex_lock函数:
函数原型:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数解释:
mutex:传入互斥锁变量的地址
如果mutex当中的计数器的值为1,则pthread_mutex_lock接口就返回了,表示加锁成功,同时计数器中的值会被更改为0,如果mutex当中的计数器的值为,则pthread_mutex_lock接口就阻塞了,thread_mutex_lock接口没有返回,阻塞在该函数内部,直到加锁成功,
pthread_mutex_trylock函数:
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数解释:
mutex:传入互斥锁变量的地址
与 pthread_mutex_lock函数不同,这个接口是非阻塞枷锁接口,也就是说当互斥锁当中的计数器为1,则加锁成功,互斥锁变量中的计数器置为0,也会返回,但是加锁没有成功,不要区访问临界资源,所以这个接口需要搭配循环来使用。
pthread_mutex_timedlock接口:
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *abs_timeout);
这个接口是带有超时时间的接口,也就意味着当不能直接获取互斥锁的时候,会等待abs_timeout时间,如果在这个时间内加锁成功了,直接返回,不需要再继续等待剩余的时间,如果超出该时间,也返回,但是表示枷锁失败,需要循环加锁。
解锁:
知道怎么加锁以后,我们还要知道怎么解锁,解锁我们使用pthread_mutex_unlock接口:
原型:
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁互斥锁:
销毁互斥锁我们使用pthread_mutex_destroy接口:
原型:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
(1)使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
(2)不要销毁一个已经加锁的互斥量
(3)已经销毁的互斥量,要确保后面不会有线程再尝试加锁
加锁与解锁的原则:
在访问临界资源的时候就加锁,在所有线程可能退出的地方进行解锁,否则执行流可能会带着锁退出掉,其它执行流就不能拿到锁了。
知道上面的知识后,我们现在来对我们的抢票程序进行该进,改进版的抢票程序:
//改进版抢票程序
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define PTHREADCOUNT 4
int g_ticket = 100;
pthread_mutex_t lock;
void* myPthread(void* arg)
{
(void)arg;
pthread_mutex_lock(&lock);
while(1)
{
if(g_ticket > 0)
{
printf("I am workPthread:%p,i hava:%d\n",pthread_self(),g_ticket);
g_ticket--;
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
pthread_mutex_unlock(&lock);
}
int main()
{
pthread_t tid[PTHREADCOUNT];
pthread_mutex_init(&lock,NULL);
for(int i = 0;i < PTHREADCOUNT;i++)
{
int ret = pthread_create(&tid[i],NULL,myPthread,NULL);
if(ret < 0)
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
for(int i = 0;i < PTHREADCOUNT;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
运行结果:
同步相关接口
条件变量:当一个线程互斥得访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了,例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。
条件变量相关接口:
(1)初始化与销毁
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t * attr);
参数说明:
cond:条件变量的地址
attr:条件变量的属性,通常传递NULL,采用默认属性
(2)等待
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);、
参数说明:
cond:条件变量
mutex:互斥锁
(3)唤醒
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
作用:唤醒全部pcb等待队列中的线程
int pthread_cond_signal(pthread_cond_t *cond);
作用:唤醒至少一个pcb等待队列中的线程
接下来我们可以利用上面的函数写一个简单的生产者与消费者的例子:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define PTHREADCOUNT 2
int g_bowl = 0;
pthread_mutex_t lock_;
pthread_cond_t pcond;//生产者条件变量
pthread_cond_t ccond;//消费者条件变量
void* productFunc(void* arg)
{
(void)arg;
while(1)
{
pthread_mutex_lock(&lock_);
while(g_bowl > 0)
{
pthread_cond_wait(&pcond,&lock_);
}
g_bowl++;
printf("i am productPthread:%p,i am product:%d\n",pthread_self(),g_bowl);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&ccond);
sleep(1);
}
pthread_mutex_unlock(&lock_);
return NULL;
}
void* consumFunc(void* arg)
{
(void)arg;
while(1)
{
pthread_mutex_lock(&lock_);
while(g_bowl <= 0) {
pthread_cond_wait(&ccond,&lock_);
}
printf("I am consumePthread:%p,i am consume:%d\n",pthread_self(),g_bowl);
g_bowl--;
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&pcond);
sleep(1);
}
pthread_mutex_unlock(&lock_);
return NULL;
}
int main()
{
pthread_mutex_init(&lock_,NULL);//初始化互斥锁
pthread_cond_init(&pcond,NULL);//初始化生产者条件变量
pthread_cond_init(&ccond,NULL);//初始化消费者条件变量
pthread_t product[PTHREADCOUNT];
pthread_t consume[PTHREADCOUNT];
for(int i = 0;i < PTHREADCOUNT;i++)
{
int ret = pthread_create(&product[i],NULL,productFunc,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&consume[i],NULL,consumFunc,NULL);
if(ret < 0)
{
perror("pthead_create");
return -1;
}
}
for(int i = 0;i < PTHREADCOUNT;i++)
{
pthread_join(consume[i],NULL);
pthread_join(product[i],NULL);
}
pthread_mutex_destroy(&lock_);//销毁互斥锁
pthread_cond_destroy(&pcond);//销毁生产者条件变量
pthread_cond_destroy(&ccond);//销毁消费者条件变量
return 0;
}
运行结果: