当前位置: 首页 > 知识库问答 >
问题:

(C/C++)为什么用全局变量同步一个读取器和一个写入器在/中是有效的?

雷曜灿
2023-03-14
void reader_thread(){
    while(1){
        if(syncToken!=0){
            while(the_vector.length()>0){
                 // ... process the std::vector 
            }
            syncToken = 0;  // let the writer do it's work
        }
        sleep(1);
    }
}

void writer_thread(){
    while(1){
        std::string data = waitAndReadDataFromSomeResource(the_resource);
        if(syncToken==0){
            the_vector.push(data);
            syncToken = 1;  // would syncToken++; be a difference here?
        }
        // drop data in case we couldn't write to the vector
    }
}

虽然这段代码(时间)效率不高,但据我所知,这段代码是有效的,因为两个线程只在全局变量值上进行同步,这样就不会产生未定义的行为。唯一的问题可能发生在并发使用向量时,但这不应该发生,因为只有在0和1之间切换作为同步值,对吗?

更新由于我犯了一个错误,只问了一个是/否的问题,我更新了我的问题为什么,希望得到一个非常具体的案例作为答案。问题本身也似乎根据答案画错了图,所以我将详细说明上面代码中的问题/问题是什么。

在此之前,我想指出,我要求的是一个具体的用例/示例/证明/详细解释,它确切地演示了什么是不同步的。即使是一个C示例代码,让一个示例计数器表现为非单调递增,也只能回答是/否的问题,而不是为什么!我对为什么感兴趣。所以,如果你提供一个例子来证明它有一个问题,我感兴趣的是为什么。

作为参考,我们来看看GCC产生的汇编程序代码的相关部分:

; just the declaration of an integer global variable on a 64bit cpu initialized to zero
syncToken:
.zero   4
.text
.globl  main
.type   main, @function

; writer (Cpu/Thread B): if syncToken == 0, jump not equal to label .L1
movl    syncToken(%rip), %eax
testl   %eax, %eax
jne .L1

; reader (Cpu/Thread A): if syncToken != 0, jump to Label L2
movl    syncToken(%rip), %eax
testl   %eax, %eax
je  .L2

; set syncToken to be zero
movl    $0, syncToken(%rip)

现在我的问题是,我不明白为什么这些指令会不同步。

假设两个线程都运行在自己的CPU核心上,就像线程A运行在核心A上,线程B运行在核心B上一样。初始化是全局的,并且在两个线程开始执行之前完成,所以我们可以忽略初始化,并假设两个线程都以synctoken=0开始;

    null

老实说,我构造了一个运行良好的示例,但它表明,我不明白为什么变量应该不同步,从而两个线程同时执行if块。我的观点是:尽管上下文切换会导致%eax与RAM中的syncToken的实际值不一致,但代码应该做正确的事情,如果if块不是唯一允许运行它的线程,就不执行它。

更新2可以假设syncToken将只像在代码中一样使用,如图所示。不允许其他函数(如waitAndReadDataFromSomeResource)以任何方式使用它

更新3让我们进一步提出一个稍微不同的问题:是否可以使用int syncToken同步两个线程,一个读取器,一个写入器,这样线程就不会通过并发执行if块而始终不同步?如果是-那很有趣^^如果不是-为什么?

共有1个答案

卫才
2023-03-14

简短回答:否,此示例未正确同步,不会(始终)工作。

对于软件来说,通常理解有时工作但不总是工作与坏掉是一回事。现在,您可以问这样一个问题:“在一个ACME品牌的32位微控制器上,用XYZ编译器将中断控制器与前台任务同步在优化级别-O0上,这是否可行”,答案可能肯定是肯定的。但在一般情况下,答案是否定的。事实上,它在任何实际情况下工作的可能性都很低,因为“使用STL”和“足够简单的硬件和编译器工作”的交集可能是空的。

正如其他评论/答案所述,它也是技术上未定义的行为(UB)。真正的实现也可以让UB正常工作。因此,仅仅因为它不是“标准”,它可能仍然可以工作,但它将不是严格符合或可移植的。它是否工作取决于具体的情况,主要取决于处理器和编译器,或许还取决于操作系统。

然而,只要变量访问是同步的,并且语句以“朴素的”程序顺序出现,逻辑就似乎是正确的。writer_thread()在“拥有”向量(syncToken==0)之前不会访问该向量。类似地,reader_thread()在拥有向量之前不会访问它(syncToken==1)。即使没有原子写/读(假设这是一个16位的机器,而syncToken是32位),这仍然会“工作”。

注意1:模式if(flag){...flag=x}是非原子测试集。通常这是一种竞争状态。但在这个非常具体的案例中,这一种族被边缘化了。一般而言(例如,超过一个阅读器或写入器),这也会是一个问题。

注意2:syncToken++比syncToken=1更不可能是原子的。通常情况下,这将是另一个错误行为的风向标,因为它涉及到读-修改-写。在这种具体情况下,应该没有什么区别。

更现实的是,如果同步不是问题所在,那么编译器的优化器重新排序将是问题所在。优化器可以决定,由于the_vector.push(data)synctoken=1没有依赖关系,所以可以先移动synctoken=1。很明显,这使得reader_thread()在writer_thread()的同时与vector混在一起。

仅仅将syncToken声明为volatile也是不够的。易失性访问只保证针对其他易失性访问进行排序,而不保证在易失性访问和非易失性访问之间进行排序。所以除非矢量也是易变的,否则这仍然是个问题。由于vector可能是一个STL类,因此声明它volatile并不明显。

现在假设同步问题和编译器优化器已经被提交了。您回顾汇编程序代码,并清楚地看到所有内容现在都以正确的顺序出现。最后一个问题是,现代CPU有一个习惯,就是乱序执行和退出指令。由于_vector.push(data)编译到的任何syncToken=1中的最后一条指令之间没有依赖关系,因此处理器可以决定在_vector.push(data)的其他指令完成之前执行movl$0x1,syncToken(%RIP)操作,例如,保存新的长度字段。这与汇编语言操作码的顺序无关。

通常,CPU知道指令#3依赖于指令#1的结果,因此它知道必须在#1之后执行#3。也许指令#2对这两个都没有依赖性,并且可以在这两个之前或之后。这种调度在运行时根据当前可用的CPU资源动态进行。

错误的是,在访问_vector的指令和访问synctoken的指令之间没有显式的依赖关系。但程序仍然隐式地要求对它们进行排序以便正确操作。CPU没有办法知道这一点。

防止重新排序的唯一方法是使用特定CPU的内存栅栏、屏障或其他同步指令。例如,intelmfence指令或PPCsync可以在触摸_vector和synctoken之间插入。只是哪个指令或指令系列,以及它们被要求放置在什么位置,对于CPU的型号和情况是非常具体的。

void reader_thread(){
    while(1){
        MUTEX_LOCK()
        if(the_vector.length()>0){
            std::string data = the_vector.pop();
            MUTEX_UNLOCK();

            // ... process the data
        } else {
            MUTEX_UNLOCK();
        }
        sleep(1);
    }
}

void writer_thread(){
    while(1){
        std::string data = waitAndReadDataFromSomeResource(the_resource);
        MUTEX_LOCK();
        the_vector.push(data);
        MUTEX_UNLOCK();
    }
}
 类似资料:
  • 如何写这个问题?老实说,我不明白这个问题的意思。A) 编写读者和作者优先于读者的解决方案,并评论每个信号量的功能。(记住变量和信号量的定义和初始化)B)读卡器的优先级意味着什么?当一个作家在写作时,到达的读者会发生什么?当编写器结束其操作时会发生什么?

  • CompositeItemWriter:当我需要将项目平均地分给Writer时,似乎会将所有读取的项目传递给所有的Writer。 BacktoBackPatternClassifier:我并不真正需要分类器,因为我是均匀地拆分项目。 有没有另一种方式,让一个读者和多个作者? 或者我可以在Writer中手动创建线程?

  • 我对Spring批处理框架相当陌生。 我在一个作业中创建了两个步骤(我们称之为步骤1和步骤2)。我想把它们并行运行。不仅如此,step2的IteamReader还应该使用step1的itemwriter。 我的第一个问题是,在Spring批量中是否有可能做到这一点?如果是,怎么做? 其次,如果这不可能,还有什么工作可以做呢? 谢了。

  • 我正在尝试创建一些POD值的本地数组(例如,),其中包含编译时已知的固定 的程序集输出: 最新的GCC和MSVC编译器对栈的读写做了基本相同的事情。 如我们所见,读取并写入变量不会被优化掉。在循环开始之前,值被写入位置 调用中访问该位置的< code>array_wrapper.size变量(通过执行一些基于偏移量的奇怪操作)。 为什么? 这只是现代编译器实现中的一个小缺点吗(希望很快就会得到修复

  • 本文向大家介绍C ++中的变量和变量类型是什么?,包括了C ++中的变量和变量类型是什么?的使用技巧和注意事项,需要的朋友参考一下 变量为我们提供了程序可以操纵的命名存储。C ++中的每个变量都有一个特定的类型,该类型确定变量的内存大小和布局。可以存储在该内存中的值的范围;以及可以应用于该变量的一组操作。一个非常简单的变量示例是- 在这里,我们有一个变量my_val,类型为int(integer)

  • 我在实验在不同的关键字和运算符周围是如何解释的,发现以下语法是完全合法的: 错误: 未捕获的引用错误:等待未定义 它似乎试图将解析为变量名。。?我期待着 或者是类似于 意外令牌等待 令我恐惧的是,你甚至可以给它分配一些东西: 如此明显错误的东西不应该导致语法错误吗,就像,,等一样?为什么允许这样做,以及第一个片段中到底发生了什么?