【问题标题】:Is it safe, to share an array between threads?在线程之间共享数组是否安全?
【发布时间】:2017-10-02 15:10:14
【问题描述】:

像我在下面的代码中那样在 Promise 之间共享一个数组是否安全?

#!/usr/bin/env perl6
use v6;

sub my_sub ( $string, $len ) {
    my ( $s, $l );
    if $string.chars > $len {
        $s = $string.substr( 0, $len );
        $l = $len;
    }
    else {
        $s = $string;
        $l = $s.chars;
    }
    return $s, $l;
}

my @orig = <length substring character subroutine control elements now promise>;
my $len = 7;
my @copy;
my @length;
my $cores = 4;
my $p = @orig.elems div $cores;
my @vb = ( 0..^$cores ).map: { [ $p * $_, $p * ( $_ + 1 ) ] };
@vb[@vb.end][1] = @orig.elems;

my @promise;
for @vb -> $r {
    @promise.push: start {
        for $r[0]..^$r[1] -> $i {
            ( @copy[$i], @length[$i] ) = my_sub( @orig[$i], $len );
        }
    };
}
await @promise;

【问题讨论】:

  • 承诺的全部意义在于承诺返回一些东西,你不是故意从 start 语句前缀返回任何有用的东西。
  • 但是start 做的比只返回一些东西。我对并行运行代码的并发部分感兴趣,以便我的 CPU 的所有内核都必须工作。
  • 我所说的类似于拿起扳手,然后用它来敲钉子。哪个有效,……我猜。

标签: variables concurrency raku


【解决方案1】:

只需让标有start 语句前缀的代码返回值,以便Perl 6 可以为您处理同步。这是该功能的重点。
然后,您可以等待所有的 Promise,并使用 await 语句获取所有结果。

my @promise = do for @vb -> $r {

    start

      do  # to have the 「for」 block return its values

        for $r[0]..^$r[1] -> $i {
            $i, my_sub( @orig[$i], $len )
        }
}

my @results = await @promise;

for @results -> ($i,$copy,$len) {
  @copy[$i] = $copy;
  @length[$i] = $len;
}

start 语句前缀只是某种与并行性相关的切线。
当您使用它时,您是在说“我现在不需要这些结果,但以后可能会需要”。

这就是它返回 Promise(异步)而不是 Thread(并发)的原因

运行时允许延迟实际运行该代码,直到您最终请求结果,即使这样它也可以在同一个线程中按顺序执行所有这些。

如果实现确实这样做了,如果您通过不断调用.status 方法来轮询Promise 以等待它从Planned 更改为Kept 或@987654338,则可能会导致死锁@,然后才询问其结果。
这是默认调度程序在有任何空闲线程时将开始处理任何Promise 代码的部分原因。


我推荐观看 jnthn 的演讲“Parallelism, Concurrency, and Asynchrony in Perl 6”
slides

【讨论】:

  • 返回值(供await 使用)然后将值复制到正确的位置会使其速度变慢一些。此外,代码对我来说更难阅读。我尝试使用Thread 接口;我没有看到任何速度增益,而且它的级别更低。
  • @sid_com 将其放入数组会导致它在循环处理之前等待。如果您只是将await 放入for 循环中@results 所在的位置,它应该在所有值完成之前开始处理这些值。
【解决方案2】:

没有。





严重地。其他答案似乎对实现做出了太多假设,没有一个经过规范测试。

【讨论】:

    【解决方案3】:

    这取决于您如何定义“数组”和“共享”。就数组而言,有两种情况需要单独考虑:

    • 固定大小的数组(声明为my @a[$size]);这包括具有固定维度的多维数组(例如my @a[$xs, $ys])。它们具有一个有趣的特性,即支持它们的内存永远不必调整大小。
    • 动态数组(声明为my @a),按需增长。实际上,随着时间的推移,它们会随着时间的推移而使用大量内存块。

    就分享而言,也分三种情况:

    • 由于某些并发控制机制或整体程序结构,多个线程在其生命周期内接触数组但一次只能接触它的情况。在这种情况下,从“使用数组的并发操作”的意义上讲,数组永远不会共享,因此不可能发生数据竞争。
    • 只读、非惰性情况。这是多个并发操作访问非惰性数组的地方,但只是为了读取它。
    • 读/写情况(包括由于为数组分配了需要惰性求值的东西而导致读取实际导致写入的情况;请注意,对于固定大小的数组,这种情况永远不会发生,因为它们从不惰性)。

    那么我们可以将安全性总结如下:

                         | Fixed size     | Variable size |
    ---------------------+----------------+---------------+
    Read-only, non-lazy  | Safe           | Safe          |
    Read/write or lazy   | Safe *         | Not safe      |
    

    * 表示警告,虽然从 Perl 6 的角度来看它是安全的,但您当然必须确保您没有使用相同的索引做冲突的事情。

    因此,总而言之,您可以安全地共享固定大小的数组并将其分配给来自不同线程的元素“没问题”(但要注意虚假共享,这可能会让您为此付出沉重的性能损失)。对于动态数组,只有在共享期间才读取它们才是安全的,即使它们不是懒惰的(尽管给定的数组分配大多是急切的,但你不太可能遇到这种情况意外地)。由于不断增长的操作,即使写入不同的元素,也存在数据丢失、崩溃或其他不良行为的风险。

    所以,考虑到原来的例子,我们看到my @copy;my @length; 是动态数组,所以我们不能在并发操作中写入它们。但是,这种情况发生了,因此可以确定代码不安全。

    已经在这里的其他帖子在指出更好的方向方面做得不错,但没有人指出血腥的细节。

    【讨论】:

      【解决方案4】:

      这个答案适用于我对 MoarVM 情况的理解,不确定 JVM 后端(或 Javascript 后端 fwiw)的最新技术。

      • 可以安全地从多个线程中读取标量。
      • 可以从多个线程修改标量而不必担心段错误,但您可能会错过更新:

      $ perl6 -e 'my $i = 0; await do for ^10 { start { $i++ for ^10000 } }; say $i' 46785

      这同样适用于更复杂的数据结构,例如数组(例如,被推送的缺失值)和哈希(被添加的缺失键)。

      因此,如果您不介意丢失更新,那么从多个线程更改共享数据结构应该可以。如果您确实介意缺少更新,我认为这是您通常想要的,您应该按照@Zoffix Znet 和@raiph 的建议,考虑以不同的方式设置您的算法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-02-10
        • 1970-01-01
        • 1970-01-01
        • 2011-11-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多