什么是线程安全?
描述的是多个执行流之间对资源的访问操作的安全问题,线程安全就是线程之间对于资源的访问操作不会造成数据二义性
1.线程不安全的现象
编写了一个黄牛的抢票程序,黄牛就相当于线程,票相当于临界资源(临界资源就是多个线程共同访问的资源)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREADCOUNT 4
int g_titcks = 100;
void* func(void* arg)
{
(void* ) arg;
while(1)
{
if( g_titcks > 0)
{
printf("i am work thread %p , i have titck %d\n",pthread_self(), g_titcks);
g_titcks--;
}
else break;
}
return NULL;
}
int main()
{
pthread_t tid[THREADCOUNT];
for(int i = 0; i < THREADCOUNT; i++)
{
int ret = pthread_create(&tid[i], NULL, func, NULL);
if(ret < 0 )
{
perror("pthread_ create \n");
return -1;
}
}
for(int i = 0; i < THREADCOUNT; i++)
{
pthread_join(tid[i],NULL);
}
return 0;
}
规则:一张票只能被一个黄牛拿到
运行结果:
通过运行结果我们可以发现 有两个线程同时拿到了100这个票,这是不符合规则的,这也就是线程不安全的现象
2.线程不安全的原理
首先我们知道了线程不安全会导致程序结果出现二义性
举个栗子:
现在一个程序中有两个线程,线程A和线程B,并且有一个int类型的全局变量,值为10,线程A和线程B在各自的入口函数当中对全局变量进行++操作。
当线程A拥有cpu之后,对全局变量进行++操作,这个操作并非是原子操作,也就意味着线程A在执行++的过程中是有可能被打断的,假设线程A刚刚将全局变量的数值10读到cpu的寄存器中,就被切换出去了,这时候线程A的程序计数器当中保存下一条执行的指令就是对全局变量+1,上下文信息中保存寄存器的值也就是10,这两个东西在线程A再次拥有cpu时,恢复现场使用
这时候线程B拥有了cpu资源,对全局变量进行了++,并且将10加到了11,回写到内存中。
当线程A再次拥有cpu资源,恢复线程,继续往下执行,从寄存器中当中得到的值仍然是10,加完之后为11,回写到内存中也是11
3.如何解决线程不安全现象
互斥和同步
4.互斥
互斥就是保证在同一时间只能有一个线程对临界资源进行访问及操作确保线程安全
我们使用互斥锁来实现互斥,在使用互斥锁是要注意互斥锁本身也是一个资源,线程也需要先获取互斥锁从而在访问临界资源,多个线程想要保证互斥,必须都去获取互斥锁,否则就无法保证互斥
4.0互斥锁变量类型
pthread_mutex_t
4.1.互斥锁初始化
1.1 动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
mutex:传入互斥锁的地址
attr:属性,一般传递NULL采用默认属性
1.2 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
PTHREAD_MUTEX_INITIALIZER :宏定义了一个结构体的值
4.2.加锁
1.1 阻塞的加锁接口
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex:传入互斥锁的地址
如果mutex中计数器值为1,则pthread_mutex_lock接口直接返回,表示加锁成功,同时mutex中的计数器的值会被置为0
如果mutex中计数器值为0,则pthread_mutex_lock接口会阻塞在函数内部中,直到加锁成功
1.2 非阻塞加锁接口
int pthread_mutex_trylock(pthread_mutex_t *mutex);
mutex:传入互斥锁的地址
当mutex中的计数器值为1,则加锁成功直接返回
当mutex中的计数器值为0,也会返回,但是这时候没有加锁成功,所以不能访问临界资源
1.3 带有超时时间的加锁接口
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
mutex:传入互斥锁的地址,abs_timeout : 等待的时间
1.带有超时时间的加锁接口,当没有获取互斥锁时,会等待abs_timeout时间,在这段时间中加锁成功了,就直接返回,如果等待超时,就直接返回,但是表示加锁失败了,需要循环使用进行加锁
注意:非阻塞接口一般都是配合循环使用
4.3.解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex:传入互斥锁的地址
1.不管是使用哪个接口进行加锁的互斥锁都可以通过这个接口进行解锁
2.解锁的时候,会将互斥锁中的计时器的值置为1,表示其它线程可以获取这个互斥锁
4.4.互斥锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex:传入互斥锁的地址
针对动态初始化的互斥锁进行销毁
4.5.使用互斥锁时应该注意的几点
- 在哪里初始化互斥锁
- 在哪里进行加锁
- 在哪进进行解锁
- 释放互斥锁
4.6.为什么互斥锁可以保证线程安全
线程在访问临界资源时,必须先获取到锁才能访问到临界资源,互斥锁的内部有一个0 / 1 计数器,当互斥锁内部的计数器为1时,表示线程可以获取到互斥锁,只有获取到互斥锁的线程才能访问临界资源,在获取到互斥锁后这个互斥锁中的计数器的值会被置为0,当互斥锁当中的计数器的值为0时,表示线程不能获取到这个互斥锁,就不能访问临界资源,保证了同一时间临界资源的唯一访问性,互斥锁本身也属于临界资源,但由于它当中计数器的值在变化的过程属于原子操作,也保证了同一时间临界资源的唯一访问性。
4.7.通过互斥实现线程安全
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREADCOUNT 4
// 票
int g_titcks = 100;
//定义一个互斥锁‘
pthread_mutex_t g_lock;
void* func(void* arg)
{
(void* ) arg;
while(1)
{
//加锁
pthread_mutex_lock(&g_lock);
if( g_titcks > 0)
{
printf("i am work thread %p , i have titck %d\n",pthread_self(), g_titcks);
g_titcks--;
}
else
{
//解锁
pthread_mutex_unlock(&g_lock);
break;
}
//解锁
pthread_mutex_unlock(&g_lock);
}
return NULL;
}
int main()
{
pthread_t tid[THREADCOUNT];
//互斥锁初始化
pthread_mutex_init(&g_lock,NULL);
for(int i = 0; i < THREADCOUNT; i++)
{
int ret = pthread_create(&tid[i], NULL, func, NULL);
if(ret < 0 )
{
perror("pthread_ create \n");
return -1;
}
}
for(int i = 0; i < THREADCOUNT; i++)
{
pthread_join(tid[i],NULL);
}
//互斥锁销毁
pthread_mutex_destroy(&g_lock);
return 0;
}