线程安全与互斥

3.线程安全

  • 3.1线程不安全
    • 多个线程并发/并行运行的时候,会导致程序结果的二义性。
    • 举个例子:
    • 如何解决线程不安全,互斥锁,详细内容往下看

4.互斥

  • 4.1互斥的要做的事情:控制线程的访问时序。
    • 当多个线程能够同时访问到临界资源的时候,有可能会导致线程执行的结果产生二义性。
    • 而互斥就是要保证多个线程在访问同一个临界资源,执行临界区代码的时候(非原子性性操作,线程可以被打断),控制访问时序。让一个线程独占临界资源执行完,再让另外一个独占执行。
  • 4.2互斥锁:
    • 互斥锁的原理:互斥锁的本质就是0/1计数器,计数器的取值只能为0或者1。
      • 计数器的值为1:表示互斥锁空闲,当前线程可以获取到互斥锁,从而去访问临界资源;
      • 计数器的值为0:表示互斥锁已被获取,当前线程不可以获取到互斥锁,从而不能访问临界资源;
      • 例子:前提条件是在代码当中用同一个互斥锁,去约束多个线程。
        • 假如有两个线程,线程A,线程B,两个线程要去访问同一个临界资源,为了控制访问时序,加了一个互斥锁,线程在访问临界资源时,需要先拿到互斥锁。假设线程A先获取到互斥锁,线程A就可以去访问临界资源,且计数器的值由1变为0,线程A访问临界资源的过程中,线程B发现计数器的值为0,无法获取互斥锁,线程B就会一直阻塞等待互斥锁,线程B就不能访问临界资源,当线程A 执行完临界区代码,把互斥锁释放了,即计数器的值由0变为1,线程B才有可能能获取到互斥锁,然后去访问临界资源,将计数器的值又由1变为0,其他线程就不能去访问临界资源;就不会存在当一个线程正在访问临界资源时,被另一个线程打断的情况。
        • 还有一种情况:假设线程A先获取到互斥锁,线程A就可以去访问临界资源,且计数器的值由1变为0,由于线程A的临界区代码比较复杂,执行时间比较长,还没等线程A执行完,就被操作系统调度,剥离CPU资源,虽然线程A被切换下来,但是,其他线程还是不能访问临界资源,因为线程A被切换下来的时候,互斥锁也被A带走了,其他线程就无法获取获取互斥锁,只有当线程A再次被调度,执行完临界区代码,释放了互斥锁,其他线程才有可能获取互斥锁,访问临界资源。
    • 需要理解的是:并不是说线程不获取互斥锁不能访问临界资源,而是程序员需要在代码当中用同一个互斥锁,去约束多个线程。否则线程A加锁访问,线程B访问临界资源之前不加锁,那也约束不了线程B
    • 那问题来了,计数器当中的值从O变成1,或者从1变成0,这是咋变换的呢,加加减减吗,肯定不是。
  • 4.3 互斥锁的计数器当中如何保证原子性
    • 为什么计数器当中的值从O变成1,或者从1变成0是原子性的呢?
      • 因为直接使用寄存器当中的值和计数器内存的值交换,而交换是一条汇编指令就可以完成的,汇编指令是最小单位,一步就可以完成,不存在中间状态,因此计数器当中的值从O变成1,或者从1变成0是原子性操作。
    • 加锁的时候:寄存器当中的值设置为(0),(在交换之前,都把寄存器的值初始化为0)
      • 第一种情况:计数器的值为1,说明锁空闲,没有被线程加锁
        • 加锁成功
        • 交换完毕之后,判断寄存器中的值是否为1,如果是1,则加锁成功,如果是0,则加锁失败
        • 交换完毕之后,检测到寄存器中的值为1,那说明原来计数器的值为1,锁空闲
      • 第二种情况:计数器的值为0,说明锁忙碌,被其他线程加锁拿走
        • 加锁失败
        • 交换完毕之后,检测到寄存器中的值为0,那说明原来计数器的值为0,锁忙碌 
      • 写了个伪码,参考参考
  • 4.4互斥锁的接口
    • 4.4.1初始化互斥锁的接口
      • 动态初始化:
        • int pthread_mutex_init (pthread_mutex_t* mutex, const pthread_mutexattr_t*attr)
          • pthread_mutex_t :互斥锁的类型,是一个结构体
          • mutex:互斥锁变量
          • attr:互斥锁的属性,一般传递NULL
      • 静态初始化:
        • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
          • #define PTHREAD_MUTEX_INITIALIZER { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
    • 4.4.2加锁
      • int pthread_mutex_lock(pthread_mutex_t *mutex)
        • 互斥锁的阻塞加锁接口,拿不到锁就阻塞等待,直到拿到锁
        • 将初始化好的互斥锁mutex传递到pthread_mutex_lock函数中,然后进行寄存器和计数器里面的值的交换操作,要么加锁成功,要么加锁失败。
      • 只加锁而不释放锁,这把锁变成死锁
      • int pthread_mutex_trylock(pthread _mutex_t *mutex) ;
        • 互斥锁的非阻塞解锁接口。
        • 拿到锁了,该函数正常返回,返回值为0;拿锁的时候,锁是被其他进程占有的,拿不到锁,也直接返回了,返回错误号以指示错误。
        • 需要搭配循环来使用的!否则加锁失败之后,我们没有判断,直接访问临界资源,就达不到互斥的目的了。
      • int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
        • 调用带有超时时间的加锁接口
          • 锁空闲:
            • 直接加锁成功后返回
          • 锁忙碌:
            • 等待,在等待时间范围内,锁被其他线程释放了,就可以获取互斥锁,加锁成功,函数返回,如果超过了等待时间范围,锁还没有被其他线程释放,该函数直接返回
    • 4.4.3 解锁接口:
      • int pthread_mutex_unlock(pthread_mutex_t * mutex) ;
        • mutex:传递互斥锁变量
      • 1.在while循环结束后解锁
      • 2.在while循环体内部加锁和解锁
        • 为啥会自己先知道锁已被解开,很大因素就是当前线程的时间片还没到,可以继续运行。
      • 3.在线程所有有可能退出的地方都进行解锁!
    • 解锁的时机
      • 在线程所有有可能退出的地方都进行解锁!,否则就有可能导致死锁(退出的线程将互斥锁带走了,其他等待的线程永远不可能拿到互斥锁了)
    • 4.4.4 销毁
      • int pthread_mutex_destroy (pthread_mutex_t *mutex) ;
        • 如果是动态初始化互斥锁的,需要调用销毁接口。如果是静态初始化互斥锁的,就不需要销毁了。

猜你喜欢

转载自blog.csdn.net/sy2453/article/details/124274592