Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/php/284.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Multithreading 编译器优化会破坏多线程代码_Multithreading_Concurrency_D - Fatal编程技术网

Multithreading 编译器优化会破坏多线程代码

Multithreading 编译器优化会破坏多线程代码,multithreading,concurrency,d,Multithreading,Concurrency,D,经过艰苦的学习,我现在遇到了另一个问题。要么我做错了什么,要么dmd中现有的编译器优化可以通过对shared变量的读取重新排序来破坏多线程代码 例如,当我使用dmd-O(完全优化)编译一个可执行文件时,编译器会愉快地优化掉该代码中的局部变量O(其中cas是比较和交换函数) 类似于此(请参见下面的分解): 在“优化”中,从内存中读取两次代码cnt,从而冒着另一个线程在其间修改了cnt的风险。优化基本上破坏了比较和交换算法 这是一个错误,还是有一个正确的方法来达到预期的结果?到目前为止,我找到的唯一

经过艰苦的学习,我现在遇到了另一个问题。要么我做错了什么,要么dmd中现有的编译器优化可以通过对
shared
变量的读取重新排序来破坏多线程代码

例如,当我使用
dmd-O
(完全优化)编译一个可执行文件时,编译器会愉快地优化掉该代码中的局部变量
O
(其中
cas
是比较和交换函数)

类似于此(请参见下面的分解):

在“优化”中,从内存中读取两次代码
cnt
,从而冒着另一个线程在其间修改了
cnt
的风险。优化基本上破坏了比较和交换算法

这是一个错误,还是有一个正确的方法来达到预期的结果?到目前为止,我找到的唯一解决方法是使用汇编程序实现代码

完整测试代码和其他详细信息
为了完整起见,这里有一个完整的测试代码,显示了这两个问题(无内存障碍和优化问题)。它在三台不同的Windows机器上为DMD2.049和DMD2.050生成以下输出(假设Dekker的算法没有死锁,这可能会发生):

atomicInc
内部的循环经过充分优化后编译为:

; cnt is stored at 447C10h
; while ( !cas( &cnt, o, o + 1 ) ) o = cnt;
; 1) prepare call cas( &cnt, o, o + 1 ): &cnt and o go to stack, o+1 to eax
402027: mov    ecx,447C10h         ; ecx = &cnt
40202C: mov    eax,[447C10h]     ; eax = o1 = cnt
402031: inc    eax                 ; eax = o1 + 1 (third parameter)
402032: push   ecx                 ; push &cnt (first parameter)
    ; next instruction pushes current value of cnt onto stack
    ; as second parameter o instead of re-using o1
402033: push   [447C10h]    
402039: call   4020BC              ; 2) call cas    
40203E: xor    al,1                ; 3) test success
402040: jne    402027              ; no success try again
; end of main loop
以下是测试代码:

import core.atomic;
import core.thread;
import std.stdio;

enum loops = 0xFFFF;
shared uint cnt;

/* *****************************************************************************
 Implement atomicOp!("+=")(cnt, 1U); with CAS. The code below doesn't work with
 the "-O" compiler flag because cnt is read twice while calling cas and another
 thread can modify cnt in between.
*/
enum threads = 8;

void atomicInc  ( ) { uint o; do { o = cnt; } while ( !cas( &cnt, o, o + 1 ) );}
void threadFunc ( ) { foreach (i; 0..loops) atomicInc; }

void testCas ( ) {
    cnt = 0;
    auto tgCas = new ThreadGroup;
    foreach (i; 0..threads) tgCas.create(&threadFunc);
    tgCas.joinAll;
    writeln( "CAS   : ", cnt == loops * threads ? "passed" : "failed" );
}

/* *****************************************************************************
 Dekker's algorithm. Fails on ia32 (other than atom) because ia32 can re-order 
 read before write. Most likely fails on many other architectures.
*/
shared bool flag1 = false;
shared bool flag2 = false;
shared bool turn2 = false;   // avoids starvation by executing 1 and 2 in turns

void dekkerInc ( ) {
    flag1 = true;
    while ( flag2 ) if ( turn2 ) {
        flag1 = false; while ( turn2 )  {  /* wait until my turn */ }
        flag1 = true;
    }
    cnt++;                   // shouldn't work without a cast
    turn2 = true; flag1 = false;
}

void dekkerDec ( ) {
    flag2 = true;
    while ( flag1 ) if ( !turn2 ) {
        flag2 = false; while ( !turn2 ) { /* wait until my turn */ }
        flag2 = true;
    }
    cnt--;                   // shouldn't work without a cast
    turn2 = false; flag2 = false;
}

void threadDekkerInc ( ) { foreach (i; 0..loops) dekkerInc; }
void threadDekkerDec ( ) { foreach (i; 0..loops) dekkerDec; }

void testDekker ( ) {
    cnt = 0;
    auto tgDekker = new ThreadGroup;
    tgDekker.create( &threadDekkerInc );
    tgDekker.create( &threadDekkerDec );
    tgDekker.joinAll;
    writeln( "Dekker: ", cnt == 0 ? "passed" : "failed" );
}

/* ************************************************************************** */
void main() {
    testCas;
    testDekker;
}

是的,用汇编语言编写。如果您跳过使用cas()函数,而只是在汇编中编写整个atomicInt函数,那么只需要几行代码。在您这样做之前,您可能会反对编译器的优化


最重要的是,您可以使用x86 LOCK INC指令而不是CAS,并且您应该能够将函数简化为一行或两行汇编代码

虽然问题似乎仍然存在,但现在公开了
atomicLoad
,这使得解决方法相对简单。要使
cas
示例工作,只需以原子方式加载
cnt

void atomicInc  ( ) { 
    uint o; 
    do {
         o = atomicLoad(cnt); 
    } while ( !cas( &cnt, o, o + 1 ) );
}
类似地,要使Dekker算法起作用:

// ...
while ( atomicLoad(flag2) ) if ( turn2 ) {
// ...
while ( atomicLoad(flag1) ) if ( !turn2 ) {
// ...
对于ia32以外的体系结构(忽略字符串操作和SSE),也可以重新排序

  • 相对于读取的读取
  • 或相对于写入的写入
  • 或对同一内存位置进行写入和读取

需要额外的记忆障碍。

谢谢您的回答。请注意:我对一个以原子方式递增的函数并不感兴趣。该代码只是一个示例来演示该问题。我的实际代码更复杂。这种行为让我非常惊讶的是,
cas
函数在优化后基本上是不可用的。我还想知道dmd可能还有哪些其他优化(例如,它能否将共享变量访问移过锁)。我觉得这不可能是对的。不过,我同意这个结论:我可能必须用汇编语言编写我的原始函数+1是的,不幸的是,这似乎是许多低级编译器的一个问题(C++也有这个问题)。在他谈到的“无锁哈希表”()克里夫特博士基本上说,他不考虑复杂的无锁结构收费貂皮在C++中,由于这些问题。我不确定我是否同意,但至少可能更难。因此,基本上,在我的代码中,我倾向于将原子代码分离到一个单独的asm文件中,或者完全在asm中进行编码,优化器很好,但有时它们会碍事。你可能应该在digitalmars.D新闻组()上询问这是否是一个已知问题,或者报告一个bug()。@Michal:我刚刚看到你已经在那里询问过(). 谢谢这已经添加到bugzilla了吗?@Trass3r:我不知道。在我们遇到这个问题以及工具链和库中的一些其他问题后,我停止了跟踪D。在邮件列表上与一位投稿人进行了简短的讨论。但是,在这位贡献者发现他的代码很好(IIRC,因为他循环中有一些
asm
代码,防止重新排序)之后,它很快就消失了。提到的另外两个问题(共享周围缺少内存屏障,以及在没有强制转换的情况下使用递增/递减运算符)是已知的。但我也不知道它们是否已经被添加到bugzilla中(或者现在已经修复)。@Trass3r:现在我在下面发布了一个简单的修复方法作为答案。@ratchetfreak:你是对的,
do{},而
更好(实际上,这是C++中的规范方法)。我已相应地修改了我的答复。谢谢[如果您能快速检查编辑,我会很高兴。我的
d
有些生锈。]事实上,以前的版本只是错误的(您在while检查之前没有初始化
o
import core.atomic;
import core.thread;
import std.stdio;

enum loops = 0xFFFF;
shared uint cnt;

/* *****************************************************************************
 Implement atomicOp!("+=")(cnt, 1U); with CAS. The code below doesn't work with
 the "-O" compiler flag because cnt is read twice while calling cas and another
 thread can modify cnt in between.
*/
enum threads = 8;

void atomicInc  ( ) { uint o; do { o = cnt; } while ( !cas( &cnt, o, o + 1 ) );}
void threadFunc ( ) { foreach (i; 0..loops) atomicInc; }

void testCas ( ) {
    cnt = 0;
    auto tgCas = new ThreadGroup;
    foreach (i; 0..threads) tgCas.create(&threadFunc);
    tgCas.joinAll;
    writeln( "CAS   : ", cnt == loops * threads ? "passed" : "failed" );
}

/* *****************************************************************************
 Dekker's algorithm. Fails on ia32 (other than atom) because ia32 can re-order 
 read before write. Most likely fails on many other architectures.
*/
shared bool flag1 = false;
shared bool flag2 = false;
shared bool turn2 = false;   // avoids starvation by executing 1 and 2 in turns

void dekkerInc ( ) {
    flag1 = true;
    while ( flag2 ) if ( turn2 ) {
        flag1 = false; while ( turn2 )  {  /* wait until my turn */ }
        flag1 = true;
    }
    cnt++;                   // shouldn't work without a cast
    turn2 = true; flag1 = false;
}

void dekkerDec ( ) {
    flag2 = true;
    while ( flag1 ) if ( !turn2 ) {
        flag2 = false; while ( !turn2 ) { /* wait until my turn */ }
        flag2 = true;
    }
    cnt--;                   // shouldn't work without a cast
    turn2 = false; flag2 = false;
}

void threadDekkerInc ( ) { foreach (i; 0..loops) dekkerInc; }
void threadDekkerDec ( ) { foreach (i; 0..loops) dekkerDec; }

void testDekker ( ) {
    cnt = 0;
    auto tgDekker = new ThreadGroup;
    tgDekker.create( &threadDekkerInc );
    tgDekker.create( &threadDekkerDec );
    tgDekker.joinAll;
    writeln( "Dekker: ", cnt == 0 ? "passed" : "failed" );
}

/* ************************************************************************** */
void main() {
    testCas;
    testDekker;
}
void atomicInc  ( ) { 
    uint o; 
    do {
         o = atomicLoad(cnt); 
    } while ( !cas( &cnt, o, o + 1 ) );
}
// ...
while ( atomicLoad(flag2) ) if ( turn2 ) {
// ...
while ( atomicLoad(flag1) ) if ( !turn2 ) {
// ...