用关中断和互斥量来保护多线程共享的全局变量

第60节:用关中断和互斥量来保护多线程共享的全局变量

2016-03-15 10:17:29   来源:eefocus   

关键字: 关中断  互斥量  多线程共享  全局变量

开场白:

在前面一些章节中,我提到为了防止中断函数把某些共享数据破坏,在主函数中更改某个数据变量时,应该先关闭中断,修改完后再打开中断;我也提到了网友“红金龙吸味”关于原子锁的建议。经过这段时间的思考和总结,我发现不管是关中断开中断,还是原子锁,其实本质上都是程序在多进程中临界点的数据处理,原子锁有个专用名词叫互斥量,而我引以为豪的状态机程序框架,主函数的switch语句,外加一个定时中断,本质上就是2个独立进程在不断切换并行运行。

为什么要保护多线程共享全局变量?因为,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。

这一节要教大家一个知识点:如何用关中断和互斥量来保护多线程共享的全局变量。

具体内容,请看源代码讲解。

(1)硬件平台:

基于朱兆祺51单片机学习板。

(2)实现功能:

在第5节的基础上略作修改,让蜂鸣器在前面3秒发生一次短叫报警,在后面6秒发生一次长叫报警,如此反复循环。

(3)源代码讲解如下:

#include "REG52.H"

#define const_time_3s 1332 //3秒钟的时间需要的定时中断次数

#define const_time_6s 2664 //6秒钟的时间需要的定时中断次数

#define const_voice_short 40 //蜂鸣器短叫的持续时间

#define const_voice_long 200 //蜂鸣器长叫的持续时间

void initial_myself();

void initial_peripheral();

void delay_long(unsigned int uiDelaylong);

void led_flicker();

void alarm_run();

void T0_time(); //定时中断函数

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

unsigned char ucAlarmStep=0; //报警的步骤变量

unsigned int uiTimeAlarmCnt=0; //报警统计定时中断次数的延时计数器

unsigned int uiVoiceCnt=0; //蜂鸣器鸣叫的持续时间计数器

unsigned char ucLock=0; //互斥量,俗称原子锁

void main()

{

initial_myself();

delay_long(100);

initial_peripheral();

while(1)

{

alarm_run(); //报警器定时报警

}

}

/* 注释一:

* 保护多线程共享全局变量的原理:

* 多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,

* 而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。

* 鸿哥的基本程序框架都是两线程为主,一个是main函数线程,一个是定时函数线程。

*/

void alarm_run() //报警器的应用程序

{

switch(ucAlarmStep)

{

case 0:

if(uiTimeAlarmCnt>=const_time_3s) //时间到

{

/* 注释二:

* 用关中断来保护多线程共享的全局变量:

* 因为uiTimeAlarmCnt和uiVoiceCnt都是unsigned int类型,本质上是由两个字节组成。

* 在C语言中uiTimeAlarmCnt=0和uiVoiceCnt=const_voice_short看似一条指令,

* 实际上经过编译之后它不只一条汇编指令。由于另外一个定时中断线程里也会对这个变量

* 进行判断和操作,如果不禁止定时中断或者采取其它措施,定时函数往往会在主函数还没有

* 结束操作共享变量前就去访问或处理这个共享变量,这就会引起冲突,导致系统运行异常。

*/

ET0=0; //禁止定时中断

uiTimeAlarmCnt=0; //时间计数器清零

uiVoiceCnt=const_voice_short; //蜂鸣器短叫

ET0=1; //开启允许定时中断

ucAlarmStep=1; //切换到下一个步骤

}

break;

case 1:

if(uiTimeAlarmCnt>=const_time_6s) //时间到

{

/* 注释三:

* 用互斥量来保护多线程共享的全局变量:

* 我觉得,在这种场合,用互斥量比前面用关中断的方法更加好。

* 因为一旦关闭了定时中断,整个中断函数就会在那一刻停止运行了,

* 而加一个互斥量,既能保护全局变量,又能让定时中断函数正常运行,

* 真是一举两得。

*/

ucLock=1; //互斥量加锁。 俗称原子锁

uiTimeAlarmCnt=0; //时间计数器清零

uiVoiceCnt=const_voice_long; //蜂鸣器长叫

ucLock=0; //互斥量解锁。 俗称原子锁

ucAlarmStep=0; //返回到上一个步骤

}

break;

}

}

void T0_time() interrupt 1

{

TF0=0; //清除中断标志

TR0=0; //关中断

if(ucLock==0) //互斥量判断

{

if(uiTimeAlarmCnt<0xffff) //设定这个条件,防止uiTimeAlarmCnt超范围。

{

uiTimeAlarmCnt++; //报警的时间计数器,累加定时中断的次数,

}

if(uiVoiceCnt!=0)

{

uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫

beep_dr=0; //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。

}

else

{

; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。

beep_dr=1; //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。

}

}

TH0=0xf8; //重装初始值(65535-2000)=63535=0xf82f

TL0=0x2f;

TR0=1; //开中断

}

void delay_long(unsigned int uiDelayLong)

{

unsigned int i;

unsigned int j;

for(i=0;i

{

for(j=0;j<500;j++) //内嵌循环的空指令数量

{

; //一个分号相当于执行一条空语句

}

}

}

void initial_myself() //第一区 初始化单片机

{

beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

TMOD=0x01; //设置定时器0为工作方式1

TH0=0xf8; //重装初始值(65535-2000)=63535=0xf82f

TL0=0x2f;

}

void initial_peripheral() //第二区 初始化外围

{

EA=1; //开总中断

ET0=1; //允许定时中断

TR0=1; //启动定时中断

}

总结陈词:

从下一节开始我准备用几章节的内容来讲常用的数**算程序。这些程序经常要用在计算器,工控,以及高精度的仪器仪表等领域。C语言的语法中不是已经提供了+,-,*,/这些运算符号吗?为什么还要专门写算法程序?因为那些运算符只能进行简单的运算,一旦数据超过了unsigned long(4个字节)的范围就会出错。而这种大数据算法的程序是什么样的?欲知详情,请听下回分解----大数据的加法运算。

<think>我们正在讨论多线程环境下安全访问共享全局变量的方法。根据引用内容,我们可以总结出几种常见方法: 1. 使用锁机制(如互斥锁)来保护共享全局变量(引用[1][3]) 2. 使用线程局部存储(ThreadLocal)让每个线程拥有自己的变副本(引用[2]) 此外,我们还可以扩展其他方法,如使用原子操作、使用不可变数据结构等。 下面详细说明这些方法: ### 1. 使用锁机制 锁是最常用的同步机制。当一个线程访问共享全局变量时,先获取锁,操作完成后释放锁,其他线程才能访问。这样可以避免多个线程同时修改数据导致的不一致。 **示例代码(基于引用[3]的Python示例):** ```python import threading # 共享全局变量 counter = 0 # 创建锁 lock = threading.Lock() def increment(): global counter for _ in range(100000): # 获取锁 lock.acquire() counter += 1 # 释放锁 lock.release() threads = [] for i in range(10): t = threading.Thread(target=increment) threads.append(t) t.start() for t in threads: t.join() print(f"Counter value: {counter}") # 预期结果1000000 ``` **优点**:保证对共享互斥访问,确保数据一致性。 **缺点**: - 锁的使用增了编程复杂度。 - 不当使用可能导致死锁(如引用[2]所述)。 - 频繁锁解锁可能影响性能。 ### 2. 使用线程局部存储(ThreadLocal) ThreadLocal为每个线程提供独立的变副本,线程之间互不干扰(如引用[2]所述)。 **示例代码(Python):** ```python import threading # 创建ThreadLocal对象 local_data = threading.local() def show_value(): # 获取当前线程关联的值 value = getattr(local_data, 'value', None) print(f"{threading.current_thread().name}: {value}") def set_value(v): # 设置当前线程的值 local_data.value = v show_value() threads = [] for i in range(3): t = threading.Thread(target=set_value, args=(i,)) threads.append(t) t.start() for t in threads: t.join() # 在主线程中访问(未设置过,所以为None) show_value() # 输出:MainThread: None ``` **优点**: - 无需锁,避免了同步开销。 - 每个线程操作自己的数据,安全高效。 **缺点**: - 不适用于需要共享数据的场景(各线程数据独立)。 - 如果确实需要共享,则不能使用此方法。 ### 3. 使用原子操作 对于一些基本数据类型(如整数),可以使用原子操作。原子操作是不可中断的一个或一系列操作,能够保证在多线程环境下的正确性。 在Python中,可以使用`queue`模块(线程安全的队列)或者使用`atomic`操作(但Python标准库没有直接的原子整数,可以使用`ctypes`或者第三方库如`atomic`)。 **示例(使用`queue`实现线程安全的计数器):** 虽然队列通常用于通信,但我们可以利用队列来实现一个计数器: ```python import threading import queue # 使用队列来模拟原子操作(实际上队列内部使用了锁) counter_queue = queue.Queue() counter_queue.put(0) def increment(): for _ in range(100000): count = counter_queue.get() count += 1 counter_queue.put(count) threads = [] for i in range(10): t = threading.Thread(target=increment) threads.append(t) t.start() for t in threads: t.join() print(f"Counter value: {counter_queue.get()}") # 预期结果1000000 ``` 或者使用`multiprocessing.Value`配合锁(跨进程,但同样适用于多线程): ```python import threading from multiprocessing import Value, Lock counter = Value('i', 0) lock = Lock() def increment(): global counter for _ in range(100000): with lock: counter.value += 1 # ... 同上 ``` **注意**:原子操作通常只适用于简单的数据类型操作(如减),复杂操作仍需锁。 ### 4. 使用不可变数据结构 如果共享数据是只读的,或者通过创建新数据来更新(而不是原地修改),则可以避免同步问题。因为不可变数据在读取时无需锁。 **示例(使用元组):** 不可变数据在修改时需要创建新对象。例如,使用一个全局的不可变字典,每次更新都创建新字典(在Python中,元组、冻结集合等是不可变的,但字典列表是可变的,所以可以使用`namedtuple`或自定义不可变类)。 **缺点**:频繁创建对象可能带来性能开销。 ### 总结 | 方法 | 适用场景 | 优缺点 | |------------------|----------------------------------|-----------------------------------------------| | 锁机制 | 需要共享读写 | 安全但可能死锁,性能开销 | | ThreadLocal | 线程间无需共享数据 | 无锁高效,但数据不共享 | | 原子操作 | 简单数据类型的简单操作 | 高效,但适用范围有限 | | 不可变数据结构 | 读多写少,或写操作不频繁 | 读取安全,但写操作可能开销大 | **推荐选择**: - 如果多个线程需要读写同一个变,且操作不是原子的(比如递增),优先考虑**锁机制**。 - 如果每个线程只需要自己的数据,使用**ThreadLocal**。 - 如果是简单的计数器,考虑**原子操作**(如使用`queue`或`multiprocessing.Value`)。 - 如果数据主要是读取,很少修改,可以使用**不可变数据结构**。 此外,还可以考虑使用更高级的同步机制,如信号、条件变等,或者使用线程安全的数据结构(如`queue.Queue`)。 ### 如何避免死锁? 使用锁时,注意: - 避免嵌套锁:如果需要多个锁,确保所有线程以相同的顺序获取锁。 - 使用超时机制:`lock.acquire(timeout=5)`避免无限等待。 - 使用上下文管理器(`with lock:`)确保锁的释放。 ### 多线程编程建议 1. 尽避免使用全局变量,使用参数传递或队列通信。 2. 如果必须使用共享数据,尽缩小锁的代码范围(只锁必要的部分)。 3. 使用线程池管理线程,避免频繁创建销毁线程。 ### 相关问题 §§相关问题§§ 1. 如何避免使用锁时出现死锁? 2. 在多线程编程中,除了锁ThreadLocal,还有哪些同步机制? 3. 如何设计线程安全的计数器? </think>### 多线程环境下安全访问共享全局变量的方法 在多线程环境中安全访问共享全局变量,本质是解决**数据竞争**(Data Race)问题。以下是核心方法及其实现原理: --- #### 1. **互斥锁(Lock)** [^1][^3] 通过锁机制强制同一时刻仅一个线程访问共享。 - **原理**: 线程访问变前需获取锁(`acquire()`),操作完成后释放锁(`release()`)。其他线程在锁未释放时阻塞等待。 - **Python示例**: ```python import threading counter = 0 # 共享全局变量 lock = threading.Lock() # 创建锁 def increment(): global counter lock.acquire() # 获取锁 try: counter += 1 # 临界区操作 finally: lock.release() # 释放锁 threads = [threading.Thread(target=increment) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print(counter) # 输出 10 (线程安全) ``` - **优点**: 严格保证数据一致性,适用于复杂操作(如修改链表)。 - **缺点**: - 不当使用易导致死锁(如嵌套锁未释放)[^2] - 频繁锁降低并发性能(锁争用开销) --- #### 2. **线程局部存储(ThreadLocal)** [^2] 为每个线程创建独立的变副本,避免共享。 - **原理**: 全局变量被映射到线程独立的存储空间,线程仅访问自身副本。 - **Python示例**: ```python import threading local_data = threading.local() # 创建ThreadLocal对象 def worker(): local_data.value = 0 # 每个线程初始化独立副本 local_data.value += 1 # 修改不影响其他线程 print(threading.current_thread().name, local_data.value) threads = [threading.Thread(target=worker) for _ in range(3)] for t in threads: t.start() # 输出示例:Thread-1:1, Thread-2:1, Thread-3:1 (线程隔离) ``` - **优点**: - 无需锁,性能高 - 天然避免数据竞争 - **缺点**: 不适用于需线程间共享数据的场景(如全局计数器)。 --- #### 3. **原子操作(Atomic Operations)** 利用硬件支持的不可中断操作。 - **原理**: 特定操作(如整数增减)在CPU指令级保证原子性,无需锁。 - **Python实现**(使用`ctypes`): ```python import ctypes import threading counter = ctypes.c_int(0) # 原子整型 def increment(): for _ in range(1000): # 原子递增(等价于CPU的LOCK ADD指令) ctypes.pythonapi.PyThread_atomic_add(ctypes.byref(counter), 1) threads = [threading.Thread(target=increment) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print(counter.value) # 输出 10000 ``` - **优点**: 性能极高(无锁竞争)。 - **缺点**: 仅支持简单数据类型(整数、布尔等)。 --- #### 4. **不可变数据结构(Immutable Data)** 通过禁止修改数据避免竞争。 - **原理**: 全局变量定义为只读(如Python的`tuple`),更新时创建新对象。 - **示例**: ```python # 使用元组(不可变)代替列表 global_config = ("server_ip", 8080) # 初始化后不可修改 # 更新配置需创建新对象 def update_config(): new_config = (global_config[0], 9090) # 生成新元组 return new_config # 其他线程读取时始终看到完整版本 ``` - **优点**: 读取无需同步,天然线程安全。 - **缺点**: 写操作开销大(需复制数据),不适用高频更新场景。 --- ### 🔒 方法对比与选择建议 | **方法** | **适用场景** | **性能** | **复杂度** | |-------------------|----------------------------------|----------|------------| | 互斥锁 | 复杂数据结构修改(如字典、链表) | 低 | 高 | | ThreadLocal | 线程独立状态(如用户会话ID) | 高 | 中 | | 原子操作 | 简单计数器、标志位 | 极高 | 低 | | 不可变数据结构 | 配置信息等读多写少场景 | 中 | 低 | > **最佳实践**: > - 优先用**原子操作**或**ThreadLocal**减少锁竞争[^2] > - 对复杂逻辑使用**锁**,但需确保: > - 锁范围最小化(仅包裹临界区) > - 避免嵌套锁防止死锁[^2] > - 用**不可变数据**替代可变全局变量[^1] --- ### ⚠️ 常见陷阱 1. **死锁**:多个锁相互等待(如A等B, B等A)。 **解决方案**:按固定顺序获取锁,或使用带超时的`lock.acquire(timeout=5)`[^3]。 2. **锁粒度不当**: - 锁粒度过大(如锁住整个函数)⇒ 并发性下降 - 锁粒度过小(如分步修改关联数据)⇒ 数据不一致 3. **虚假共享(False Sharing)**: 多核CPU中,无关变因位于同一缓存行导致无谓同步。 **解决方案**:内存对齐(如Python的`ctypes`填充字节)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值