Linux线程简单理解3

线程不安全

//写多线程一定确保是多核的
#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.重新获取锁,准备执行后续的操作
前两步操作必须是在一起的(原子操作),否则可能会错过其他线程的通知信息,导致还是在这傻等!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值