线程不安全
//写多线程一定确保是多核的
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#define Thread_num 2
int g_count=0;
void* ThreadEntry(void* arg)
{
(void)arg;
for(int i=0;i<50000;i++)
{
++g_count;
}
return NULL;
}
int main()
{
pthreda_t tid[Thread_num];
for(int i=0;i<Thread_num;++i)
{
pthread_create(&tid[i],NULL,ThreadEntry,NULL);
}
for(int i=0;i<Thread_num;i++)
{
pthead_join(tid[i],NULL); //线程回收
}
printf("g_count=%d\n",g_count);
return 0;
}///Thread_num改成1试试
++g_count 操作:
1.把g_count从内存加载到cpu中。2,执行++(寄存器上的++),对寄存器的内容进行自增。3.cpu中的值写回内存。
两次操作相互影响(2个线程) 线程不安全(多线程环境下,程序执行结果出现预期之外的值)概率性问题。
- 多线程访问的公共资源叫做“临界资源”
- 访问临界资源的代码叫做“临界区”
- 在临界区中使用互斥机制,解决线程不安全机制
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界区资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
互斥量mutex
- 大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获取这种变量。
- 但有的时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
互斥量的接口
初始化互斥量
1.静态分配:
pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER
2.动态分配:
pthread_mutex_init()
销毁互斥量
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
pthread_mutex_destroy();
互斥量加锁和解锁
pthread_mutex_lock()
pthread_mutex_unlock()
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
互斥锁pthread_mutex”挂起等地锁“,一旦线程获取锁失败,就会挂起(进入操作系统提供的一个等待队列中)这个线程什么时候才能恢复执行,也不是其他线程释放锁,立即就能恢复执行
而是其他线程释放锁之后,当前线程还得看操作系统的心情来决定啥时候恢复执行。
互斥锁可以保证线程安全,最终的程序效率收到影响。加锁也会有开销。除此之外,还有一个更严重的问题“死锁”
死锁
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的 资源而处于的一种永久等待状态。
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。(当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放。)
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
比较实用的解决方案,从代码设计的角度来解决死锁问题
1.短 让临界区的代码尽量短
2.平 临界区代码尽量不去调用其他复杂函数
3.快 让临界区代码执行速度快,别做太多耗时的操作
避免死锁算法
- 死锁检测算法
- 银行家算法
线程安全
多个线程并发执行同一份代码,不会出现不同的结果。
重入
同一个函数被不同的执行流调用,当前一个执行流还没有结束,就有其他执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现问题,我们就称该函数是可重入的,否则为不可重入。
常见线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 调用线程不安全函数的函数
常见线程安全的情况
- 每个线程对全局变量或者静态变量,只有读权限,没有写权限,这样的线程一般是安全的。
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果不会存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O函数,因为标准I/O函数大部分是调用不可重入的方式来管理全局数据结构
- 可重入函数体内使用了静态的数据结构
常见不可重入的情况
- 不使用全局变量或者静态变量
- 不使用malloc和new来开辟空间
- 不调用不可重入函数
- 不返回静态数据和全局数据,所有数据都由函数调用者提供
可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数一定是线程安全
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,如果这个重入函数若锁还未释放继续调用,就会产生死锁,因此是不可重入的。
- 如果一个函数中由全局变量,那么这个函数既不是可重入函数也不是线程安全函数
线程同步
条件变量
当一个线程互斥地访问某个变量时,它可能发现在其他线程在改变状态之前,它什么也做不了(不能访问数数据,但是一直调度,产生资源的开销,线程饥饿)。例如一个线程访问队列时,发现队列为空,它只能等待,等待其他线程将一个数据节点插入队列里。
同步概念
在保证数据安全的前提下,让线程能够按照某种特定的访问顺序访问临界资源,从而有效避免饥饿问题
条件变量常用函数
//条件变量初始化
int pthread_cond_init()
//条件变量销毁
int pthread_cond_destroy()
//等待条件满足
//经常搭配互斥锁来使用
int pthread_cond_wait()
pthread_cond_wait() 做了3件事情
1.先释放锁
2.等待条件就绪
3.重新获取锁,准备执行后续的操作
前两步操作必须是在一起的(原子操作),否则可能会错过其他线程的通知信息,导致还是在这傻等!!