本文为笔者学习阶段的一个记录,多有错漏与描述不清之处,欢迎大家批评指正。
前言
为什么在操作系统中多线程需要使用互斥锁、信号量等技术?
为什么说多线程环境中全局变量不安全?
本文将对上述问题进行阐述。
一、全局变量为什么不安全?
首先从 cpu 的角度思考对一个变量的 -1 操作需要做什么。
这里需要简单代入一些 cpu 概念,cpu 是有寄存器的,为了不带入太多无关知识,此处仅介绍 r0 - r12 寄存器。
r0 - r12为通用寄存器,用于存储任意数据。
cpu 在将变量 money
进行 -1 操作时需要执行以下三个步骤:
① 首先需要将 money
的值从内存中读取存来存放到某一个寄存器中,例如 r1。
② 随后使用汇编指令计算 r1 - 1,计算结果存放至 r2。
③ 最后将 r2 的值写入 money
所在的地址。
假设一个场景, money
= 10,2 个线程同时去执行 money
-=1,那么运算过程可能是这样的:
① 线程 1 读取了 money
,值为 10,存放至 r1 = 10。
② 发生线程切换。
③ 线程 2 读取了 money
,此时 money
还是等于10,线程 2 将它存放至 r3。
④ 线程 2 执行汇编指令计算 r3 - 1,将计算结果 9 存放至 r4 ,随后写入到 money
的地址中。
⑤ 线程切换回线程 1
⑥ 线程 1 执行汇编指令计算 r1 - 1,将计算结果 9 存放至 r4 ,随后写入到 money
的地址中。
what,两个线程都执行了 money
-= 1,结果现在 money
= 9 !!!
简单总结
,即使是最简单的变量计算,cpu 也需要使用多个指令完成,而线程之间是会随时发生切换的,因此就有可能出现计算过程被打断的问题,而这个问题可能会导致数据异常。
注意,这里的例子并不完全符合汇编知识,只是为了大致描述问题。
二、如何让全局变量变得安全
2.1 多线程同步技术
各操作系统基本都会为开发者提供线程同步技术以保障多线程之间全局数据的交互安全。
简单的说
,线程同步技术主要提供以下两种功能:
① 原子操作
② 睡眠唤醒机制
原子操作:
即该操作已经小的不能再小了,就像原子一样不可再被分割
了。
操作系统通过汇编语法实现原子操作,使变量操作变为最小单元,不再会被打断,解决了前面提到的全局变量不安全问题。
睡眠与唤醒:
假如线程 1 长时间占用一个变量,那么线程 2 会进入长时间等待,在等待期间需要不断地访问该变量是否被使用完成,这就会导致 cpu 占用高。而睡眠唤醒机制可以让线程 2 进入睡眠,让出 cpu 资源给其他线程使用,当线程 1 使用完变量后再主动唤醒线程 2。
Linux 系统为我们提供了以下多线程同步技术:
互斥锁、自旋锁、读写锁、条件变量、信号量。
2.2 互斥锁
2.2.1 互斥锁简介
简单概括
:互斥锁用于保护多线程下的资源访问(指全局变量,也可以理解为代码块),使资源同一时间只能被一个线程访问,当多个线程访问被保护的资源时,后访问的线程会被阻塞睡眠,直到前一个线程使用完毕。
当一个线程需要使用被互斥锁保护的资源时,首先需要对互斥锁进行上锁,若该锁处于未上锁状态则可以成功上锁,随后该线程占用资源,使用完成后解锁互斥锁。
当一个线程对互斥锁上锁时,发现互斥锁已经处于上锁状态,则说明该资源正在被其他线程访问,该线程会进入睡眠,直到互斥锁被解锁。
2.2.2 互斥锁相关函数
互斥锁操作可以使用以下函数:
– 初始化与释放互斥锁
互斥锁在使用前需要初始化,使用完后需要销毁,函数如下:
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex:指向互斥锁的指针。
attr:指定互斥锁的属性,可传入NULL表示使用默认属性。
返回值:成功返回0,失败返回非0值。
– 解锁与上锁
pthread_mutex_lock 和 pthread_mutex_trylock 用于互斥锁的上锁。
对已经上锁的互斥锁 lock 会导致线程睡眠,直到锁被释放,而 trylock 则会返回错误。
pthread_mutex_unlock函数用于互斥锁的解锁,函数原型如下
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex:指向互斥锁的指针。
返回值:尝试上锁成功返回0。若互斥锁已被上锁则lock函数会阻塞,而trylock返回错误码EBUSY。若出错返回错误值。
– 获取与设置互斥锁属性
在使用 pthread_mutxt_init 函数初始化互斥锁时,可传入一个 attr 结构体来指定互斥锁的属性
(1)销毁attr结构体
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
(2)初始化attr结构体
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
(3)获取互斥锁属性存入attr结构体中
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
(4)设置attr中所指示的互斥锁属性
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
attr:线程互斥锁属性结构体指针,该结构体会被传入pthread_mutxt_init中。
type:互斥锁的类型,可选值有以下三种:
-- PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁。若互斥锁处于未锁定状态,或者已由其他线程锁定,对其解锁会导致不确定结果。
-- PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查,但是由于错误检测会导致效率变慢。它在以下三种情况都会返回错误:
-- 同一线程对同一互斥锁加锁两次
-- 线程对由其他线程锁定的互斥锁进行解锁
-- 线程对处于未锁定状态的互斥锁进行解锁
-- PTHREAD_MUTEX_RECURSIVE:递归互斥锁,允许对一个已经上锁的互斥锁重复上锁,并且会记录维护这个重复上锁次数。当解锁次数不等于加锁次数时,该互斥锁不会释放。
-- PTHREAD_MUTEX_DEFAULT : 默认互斥锁,当使用pthread_mutxt_init函数初始化互斥锁传入NULL属性,或者使用宏在定义的同时初始化互斥锁时都会使用该配置,它类似于PTHREAD_MUTEX_NORMAL。
返回值:成功返回0,失败返回非0值。
2.2.3 互斥锁使用示例
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
/* 创建的线程数量 */
#define PTHERAD_COUNT 2
static pthread_mutex_t mutex;
static long totalcount = 100000000;
static void * new_pthread(void * arg)
{
long count = 0;
/* 循环争夺资源total */
for( ; ; ){
/* 互斥锁上锁 */
pthread_mutex_lock(&mutex);
/* 让总资源-1,同时记录抢到的资源+1 */
totalcount --;
/* 互斥锁解锁 */
count ++;
pthread_mutex_unlock(&mutex);
/* 当总资源为0时线程退出 */
if(totalcount <= 0){
pthread_exit((void *)count);
}
}
}
/* 该函数执行以后会创建两个新的线程
这两个线程会共同争夺资源total,total总数为1000 0000
可以发现如果不使用互斥锁会导致两个线程抢到的资源加起来远远超过total总数
这是因为对于共享资源的同时访问造成了不可预料的问题出现
当使用互斥锁之后这个问题就不会再出现
*/
int main(int argc, char **argv)
{
int ret = 0,i = 0;
void * pret = NULL;
pthread_t pthread_id[PTHERAD_COUNT];
/* 初始化一个线程互斥锁 */
pthread_mutex_init(&mutex, NULL);
/* 循环创建多个线程 */
for(i = 0;i < PTHERAD_COUNT;i ++){
ret = pthread_create(&a