电子产业一站式赋能平台

PCB联盟网

搜索
查看: 187|回复: 0
收起左侧

同事说: 单核程序放在多核上跑不了~

[复制链接]

36

主题

36

帖子

-1721

积分

限制会员

积分
-1721
发表于 2025-3-8 12:04:00 | 显示全部楼层 |阅读模式
最近跟前同事聊了会天,其中有个话题有点意思,他说以前在单核上跑得溜溜的程序,移植到多核处理器上各种问题,头都大了~于是,想了想今天跟大家聊聊多核处理器中遇到的一个非常常见的编程问题,当然单核在驱动开发的时候也要格外小心,这个问题需要对所用的处理器比较了解,否则软件开发中稍有不注意就会留下了bug。1
指令重排
现代处理器和编译器为了提高性能,会对指令进行重排序优化(包括编译器重排和 CPU 流水线乱序执行),也叫指令重排(Instruction Reordering)。这种优化在单线程环境下是安全的,但在多线程并发场景中可能导致非预期的执行顺序,破坏程序的正确性,也是并发编程中许多问题的根源。
编译器或处理器在不改变程序单线程语义,但是为了性能会重新排列指令的执行顺序。拿可能有朋友会问了,它是如何如何优化性能的,主要如下三点:
提高指令级并行度(ILP):通过流水线填充、乱序执行等技术,充分利用 CPU 资源。减少内存访问延迟:通过重排内存操作,避免 CPU 因等待数据而空闲。优化缓存利用率:调整指令顺序以提升缓存命中率。同时根据操作类型(读/写),重排可分为以下四类:
重排类型原始顺序重排后顺序是否允许Load-LoadLoad A; Load BLoad B; Load A常见(需屏障禁止)Load-StoreLoad A; Store BStore B; Load A部分允许(依赖数据相关性)Store-StoreStore A; Store BStore B; Store A常见(需屏障禁止)Store-LoadStore A; Load BLoad B; Store A几乎所有 CPU 允许(最难处理的类型)其中Store-Load 重排是最危险的类型,可能导致其他线程看到“先读后写”的反直觉行为。
2
指令重排大坑
示例:
// 线程1
a = 1;
flag = true;  // 编译器或 CPU 可能将这两行重排
// 线程2
while (!flag);
print(a);  // 可能看到 a=0(预期是 a=1)
线程1的 a = 1 和 flag = true 可能被重排,导致线程2看到 flag 为 true 时,a 尚未被写入,可能很多单片机的朋友如果过渡到多核开发中这一点是很容易出错的。
那这个坑该怎么填呢?芯片设计的时候就已经跟大家想好办法了,只是这个事情只能交给软件了。
3
内存屏障
内存屏障,其实就是我们所说的内存屏障语句,通过插入屏障指令强制 a = 1 在 flag = true 之前执行,避免重排。
所以其作用就显而易见了,内存屏障会强制将本地缓存的修改刷回主内存(Store 屏障),或使其他核心的缓存失效(Load 屏障),确保可见性。
在 Linux C 代码中,可以使用 __sync_synchronize() 来实现内存屏障。以下是添加了内存屏障的代码示例:
#include
#include
// 共享变量
int a = 0;
int flag = 0;
// 线程 1 的函数
void* thread1_function(void* arg){
    // 设置 a 的值
    a = 1;
    // 插入写内存屏障,确保 a 的写入操作在 flag 设置为 true 之前完成
    __sync_synchronize();
    // 设置 flag 为 true
    flag = 1;
    returnNULL;
}
// 线程 2 的函数
void* thread2_function(void* arg){
    // 等待 flag 变为 true
    while (!flag);
    // 插入读内存屏障,确保在读取 a 的值之前,flag 的更新已经完成
    __sync_synchronize();
    // 打印 a 的值
    printf("a = %d
", a);
    returnNULL;
}
intmain(){
    pthread_t thread1, thread2;
    // 创建线程 1
    if (pthread_create(&thread1, NULL, thread1_function, NULL) != 0) {
        perror("pthread_create");
        return1;
    }
    // 创建线程 2
    if (pthread_create(&thread2, NULL, thread2_function, NULL) != 0) {
        perror("pthread_create");
        return1;
    }
    // 等待线程 1 结束
    if (pthread_join(thread1, NULL) != 0) {
        perror("pthread_join");
        return1;
    }
    // 等待线程 2 结束
    if (pthread_join(thread2, NULL) != 0) {
        perror("pthread_join");
        return1;
    }
    return0;
}
代码解释:__sync_synchronize():这是 GCC 提供的一个内置函数,用于插入一个全内存屏障。它确保在屏障之前的所有内存操作(读和写)都在屏障之后的内存操作之前完成。
线程 1
首先将 a 的值设置为 1。然后插入一个写内存屏障 __sync_synchronize(),确保 a 的写入操作在 flag 设置为 1 之前完成。最后将 flag 的值设置为 1。线程 2
进入一个循环,等待 flag 变为 1。当 flag 变为 1 后,插入一个读内存屏障 __sync_synchronize(),确保在读取 a 的值之前,flag 的更新已经完成。最后打印 a 的值。[/ol]         或许在这种情况下基本都可以用锁来解决,但要写出稳定、高效的代码还得多去挖一挖,或许计算机体系架构值得一读~
回复

使用道具 举报

发表回复

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

本版积分规则


联系客服 关注微信 下载APP 返回顶部 返回列表