OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 4441|回复: 17

多线程操作共享变量的问题,求大佬解答!

[复制链接]

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
发表于 2020-12-9 10:28:45 | 显示全部楼层 |阅读模式
volatile 解决多线程中数据不一致  , 每次读取数据都去主内存中读取 , 而不是去线程栈中读取 (线程栈里边可能存放的是旧值)。但是我发现网上好多程序都没加volatile ,只用了信号量同步 , 是不是最好应该都加上关键字修饰呢 。。
QQ图片20201209102511.png
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

3

主题

1907

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4106
金钱
4106
注册时间
2018-8-14
在线时间
696 小时
发表于 2020-12-9 12:48:33 | 显示全部楼层
其实不太能理解你说的数据不一致问题, 多线程中有共享数据的安全性问题。用信号同步是为安性的考虑。
volatile 并不解决多线程中安全性的问题, 它只解决内存及寄存器之间能及时保持一致。
全用上volatile 可以吗, 当然可以, 只不过要付代价, 代价是CPU的资源。
它好比你用计算器连续加10个数, 当然是10个数都加完, 才把答案抄到纸上, 那有每加一个数都把答案抄到纸上的?
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 13:27:42 | 显示全部楼层
edmund1234 发表于 2020-12-9 12:48
其实不太能理解你说的数据不一致问题, 多线程中有共享数据的安全性问题。用信号同步是为安性的考虑。
vol ...

volatile int a;
void Task1()
{
        // 申请信号量
        ...
        if(a==5)
        {
                //        执行代码
        }
        ...
        // 释放信号量
}

void Task2()
{
        // 申请信号量
        ...
        a++;
        ...
        // 释放信号量
}
volatile 保证每次都从主内存读取, 都是最新的值
而信号量  a++并不是原子性操作,可能在进行加1的过程中,即加1的过程还没结束,其他线程开始读取a当前数值(因此读取到的仍是旧值)
所以加上信号量 变为原子类操作

所以 ,我感觉是volatile 和 信号量都要加上。
我理解那里有偏差吗?
大佬多多指教~~

回复 支持 反对

使用道具 举报

3

主题

1907

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4106
金钱
4106
注册时间
2018-8-14
在线时间
696 小时
发表于 2020-12-9 13:42:24 | 显示全部楼层
zhang123zzz 发表于 2020-12-9 13:27
volatile int a;
void Task1()
{

而信号量  a++并不是原子性操作,可能在进行加1的过程中,即加1的过程还没结束,其他线程开始读取a当前数值(因此读取到的仍是旧值),

这不是新旧的问题, 所谓安全性问题是这样

开始时A=2;

X线程 执行 A++ , 这是 读-修改-写 操作, 当执行完第一步 读 初另一线程Y抢占。
Y线程 执行 A|=4, 之后返回 X线程
X线程 继续执行 余下的修改-写 操作, 最后A=2

这不是新旧的问题, 而是Y线程的A|=4不见了, A现在是==3, 这就不安全的共享数据, 它跟volatile无关, volatile也不能解决这问题。
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 13:54:40 | 显示全部楼层
edmund1234 发表于 2020-12-9 13:42
而信号量  a++并不是原子性操作,可能在进行加1的过程中,即加1的过程还没结束,其他线程开始读取a当前数 ...

这种问题 只能通过加锁 来实现吧 。
但是同时会不会出现   两个线程数据不一致呢  ,B修改后,可能A线程读取的是寄存器的副本里的值
这时是不是要加上volatile了 。
我感觉如果要严谨的话 ,volatile和信号量都要加上吧 。。
感谢回复
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 14:27:34 | 显示全部楼层
edmund1234 发表于 2020-12-9 13:42
而信号量  a++并不是原子性操作,可能在进行加1的过程中,即加1的过程还没结束,其他线程开始读取a当前数 ...

大佬,在吗? :
回复 支持 反对

使用道具 举报

3

主题

1907

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4106
金钱
4106
注册时间
2018-8-14
在线时间
696 小时
发表于 2020-12-9 14:51:09 | 显示全部楼层
zhang123zzz 发表于 2020-12-9 13:54
这种问题 只能通过加锁 来实现吧 。
但是同时会不会出现   两个线程数据不一致呢  ,B修改后,可能A线程 ...


上锁当然是个办法, 但很多应用是用不起锁的, 这所谓多线程, 可以简单到中断服务函数与主程序的共享数据, 比如所有中断都是通过某一全局变量通知主函数所发生的中断, 而这个全局变量就有安全性的问题, 能轻易上锁?主程序上锁了后发生中断, 要改这变量, 在中断等主程序解锁?

volatile其实很简单, 但解释起来特别麻烦, 因为它需要你理解ARM的机理, ARM的其一特性就是, 所有运算, 加减乘除与或....都只能在寄存器里运算, 简单如A++; 都不都能直接的对内存+1, 必须是先加载到寄存器才可以加1, 之后再把寄存器的内容存回内存。
正正是这个特性, ARM的编译器都会进行优化, 一通运算中多次用到的同一变量, 只在最开始时从内存加载到寄存器, 在运算的最后才把它更新到内存去。
但如果运算是被包含在while(1){ }内, 那编译器大多只会在进while(1)之前, 从内存加载到寄存器, 只后就不再加载, 这时候就需要用volatile告诉编译器在哪儿给我从新把内存的内容加载寄存器。


u32 ShareVar;

while(1) {

  if ((volatile u32)ShareVar) {
    if (ShareVar & 1) ......

    if (ShareVar & 2) ......
   
    ShareVar=0;
  }
}
回复 支持 反对

使用道具 举报

3

主题

1907

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4106
金钱
4106
注册时间
2018-8-14
在线时间
696 小时
发表于 2020-12-9 14:58:51 | 显示全部楼层
u32 ShareVar;

while(1) {

  if ((volatile u32)ShareVar) { //让编译器在这给我从内存重新加载到寄存器
    if (ShareVar & 1) ......     // <<<---- 只用寄存器的值来运算

    if (ShareVar & 2) ......     // <<<---- 只用寄存器的值来运算
   
    ShareVar=0;                  // 在运算的最后, 编译器会自动把寄存器的内容保存到内存去
  }
}
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 15:02:11 | 显示全部楼层
edmund1234 发表于 2020-12-9 14:51
上锁当然是个办法, 但很多应用是用不起锁的, 这所谓多线程, 可以简单到中断服务函数与主程序的共享 ...

  按照您的说法 ,  中断函数  或者多线程共享的变量都需要加volatile 修饰
  因为要取出最新的数据(保证数据一致), 都要从主内存读取, 寄存器中可能存放的是旧的数据。
回复 支持 反对

使用道具 举报

0

主题

65

帖子

0

精华

高级会员

Rank: 4

积分
523
金钱
523
注册时间
2017-12-21
在线时间
76 小时
发表于 2020-12-9 15:06:08 | 显示全部楼层
用volatile修饰变量其实就是告诉编译器对这个变量操作不能被优化,
比如
volatile int a;
a=1;a=0;a=1;a=0;这样会产生4条赋值语句
但要是
int a;
a=1;a=0;a=1;a=0;这实际只会产生最后一条赋值语句,因为编译器认为前3个连续赋值没有意义而被优化掉.
如果a是某个IO脚寄存器,就会造成输出不对了.
回复 支持 反对

使用道具 举报

3

主题

1907

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4106
金钱
4106
注册时间
2018-8-14
在线时间
696 小时
发表于 2020-12-9 15:19:25 | 显示全部楼层
zhang123zzz 发表于 2020-12-9 15:02
按照您的说法 ,  中断函数  或者多线程共享的变量都需要加volatile 修饰
  因为要取出最新的数据( ...

不是, 这是两个问题
不同线程之间共享同数据会不安全, 这个安全性问题volatile是解决不了的, 如果说是跑系统就用信号或Queue来解决, 跑裸机的就用 主程序在更新共享数据前先禁能中断的方法。

volatile是解决运算中没有从内存更新的问题。
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 15:32:37 | 显示全部楼层
edmund1234 发表于 2020-12-9 15:19
不是, 这是两个问题
不同线程之间共享同数据会不安全, 这个安全性问题volatile是解决不了的, 如果说 ...

多线程中 , 可以通过信号量、关调度器 、或者禁止中断的方式来保证安全 。  
我的疑惑是 , 这个线程修改完之后 ,另一个线程读取寄存器的值可能是旧值 ,这样两个线程数据不一致吧 。

volatile 解决多个线程之间 数据不一致的问题 ,  每次都去内存中读取最新的值 。
而 信号量 保证线程安全     。。

所以我想着 ,对于一些 多线程共享的变量   不应该既要保证线程的安全  又要 保证
数据一致性吗 ?      
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 15:34:02 | 显示全部楼层
我太菜了
回复 支持 反对

使用道具 举报

3

主题

808

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3888
金钱
3888
注册时间
2017-3-7
在线时间
1694 小时
发表于 2020-12-9 15:48:01 | 显示全部楼层
没错,两个都有是最好的
回复 支持 反对

使用道具 举报

3

主题

1907

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4106
金钱
4106
注册时间
2018-8-14
在线时间
696 小时
发表于 2020-12-9 16:12:12 | 显示全部楼层
zhang123zzz 发表于 2020-12-9 15:32
多线程中 , 可以通过信号量、关调度器 、或者禁止中断的方式来保证安全 。  
我的疑惑是 , 这个线程 ...

我不太同意这一句, 每次都去内存中读取最新的值。
在运算的过程还更新? 这在很多情况下会出问题的。
只在每个循环中最开始时更新 , 好比是

u32 ShareVar;

while(1) {

  if ((volatile u32)ShareVar) { //让编译器在这给我从内存重新加载到寄存器
    if (ShareVar & 1) ......     // <<<---- 只用寄存器的值来运算

    if (ShareVar & 2) ......     // <<<---- 只用寄存器的值来运算
   
    ShareVar=0;                  // 在运算的最后, 编译器会自动把寄存器的内容保存到内存去
  }
}
回复 支持 反对

使用道具 举报

2

主题

459

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4513
金钱
4513
注册时间
2018-5-14
在线时间
964 小时
发表于 2020-12-9 17:16:44 | 显示全部楼层
既然用rtos了,尽量避免全局变量。用信号和队列方式,实在避免不了比如中断,就用锁保证该变量只有一个线程在操作。还有的办法就是A线程只写,B线程只读。
回复 支持 反对

使用道具 举报

9

主题

35

帖子

0

精华

初级会员

Rank: 2

积分
110
金钱
110
注册时间
2019-7-28
在线时间
38 小时
 楼主| 发表于 2020-12-9 19:09:14 | 显示全部楼层
姚先起 发表于 2020-12-9 17:16
既然用rtos了,尽量避免全局变量。用信号和队列方式,实在避免不了比如中断,就用锁保证该变量只有一个线程 ...

“线程A只写 , 线程B只读” 这句话不太理解
一个共享变量 , 比如说线程A  要读取 线程B处理后的值
先不考虑 用队列的这种情况(用队列肯定是最优的)  
线程A 变量如果被编译器优化了 , 线程A读取寄存器的副本 , 这不就造成了数据不同步吗 ?
回复 支持 反对

使用道具 举报

2

主题

459

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
4513
金钱
4513
注册时间
2018-5-14
在线时间
964 小时
发表于 2020-12-10 08:45:42 | 显示全部楼层
zhang123zzz 发表于 2020-12-9 19:09
“线程A只写 , 线程B只读” 这句话不太理解
一个共享变量 , 比如说线程A  要读取 线程B处理后的值
...

首先,你都是定义全局变量了,肯定这个变量不在任务栈中,所以变量a只有一个地址。不存在副本问题,会出现的就是原子操作问题。如果你只是共享这个变量内容,a在一个X线程中循环操作,在另一个Y线程中循环读取就没问题。如果你要考虑Y线程是要读取a最新在X线程获取的值,那么就是信号量或者队列等手段,因为Y线程是不知道a在X线程已经更新的,需要用rtos函数进行通知。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-6-23 15:38

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表