在同一react块中使用不同的线程调度程序时会发生什么?

时间:2019-05-22 07:48:01

标签: asynchronous perl6

这是对is whenever signal() in react block order dependent? 的跟进问题。

以下使用默认调度程序$*SCHEDULER的代码使用户可以在以下事件循环中按CTRL-C来立即退出:

use v6;
my %scheduler;
my $use-default-scheduler = True;
if $use-default-scheduler {
    %scheduler = scheduler => $*SCHEDULER;
}    
react {
    whenever signal(SIGINT, |%scheduler) {
        say "Got signal";
        exit;
    }
    whenever Supply.from-list($*IN.lines, |%scheduler) {
        say "Got line";
        exit if $++ == 1 ;
    }
}

我对在同一react循环中使用两个不同的线程调度程序会发生什么感兴趣?如果我使用默认线程调度程序Supply.from-list()而不是$*SCHEDULER,请在上述代码中设置$use-default-scheduler = False。现在,用户无法通过按react来立即退出CTRL-C块。如果他按CTRL-C,程序将一直挂起,直到按Enter。

那么这里实际发生了什么? react一次只关注一个事件循环吗? (我在这里想象两个事件循环,一个用于SIGINT信号的第一个whenever中使用的默认调度程序,另一个用于$*IN.lines电源中的事件循环)。因此,react现在专注于from-list()的{​​{1}}调度程序,但是在此事件循环中以某种方式忽略了SIGINT吗?因此,按$*IN.lines不会更改CTRL-C块的状态吗?

1 个答案:

答案 0 :(得分:9)

要查看实际发生的情况,让我们重新编写程序以涵盖react的工作。我将忽略许多细节,这些细节对手头的问题并不太重要。

为了使事情更紧凑,我将重写所提供程序的这一部分:

react {
    whenever signal(SIGINT, |%scheduler) {
        say "Got signal";
        exit;
    }
    whenever Supply.from-list($*IN.lines, |%scheduler) {
        say "Got line";
        exit if $++ == 1 ;
    }
}

首先,react { ... }确实与await supply { ... }类似-也就是说,它点击了supply { }块并await的结尾。

await supply {
    whenever signal(SIGINT, |%scheduler) {
        say "Got signal";
        exit;
    }
    whenever Supply.from-list($*IN.lines, |%scheduler) {
        say "Got line";
        exit if $++ == 1 ;
    }
}

但是supply块是什么? supplyreact这样)的本质是:

  • 并发控制,因此它将一次处理一条消息(因此我们需要某种锁定;为此它使用Lock::Async
  • 完成和错误传播(在明显的作弊中,我们将使用Promise来实现这一点,因为我们只想要react部分;真实的东西会产生结果{{ 1}},我们可以将Supply的值放入)
  • 在没有未完成的订阅时自动完成(我们将跟踪emit中的订阅)

因此,我们可以将程序重写为如下形式:

SetHash

请注意,这里的调度程序或事件循环什么都没有。 await do { # Concurency control my $lock = Lock::Async.new; # Completion/error conveyance my $done = Promise.new; # What's active? my %active is SetHash; # An implementation a bit like that behind the `whenever` keyword, but with # plenty of things that don't matter for this question missing. sub whenever-impl(Supply $s, &block) { # Tap the Supply my $tap; $s.tap: # When it gets tapped, add the tap to our active set. tap => { $tap = $_; %active{$_} = True; }, # Run the handler for any events { $lock.protect: { block($_) } }, # When this one is done, remove it from the %active list; if it's # the last thing, we're done overall. done => { $lock.protect: { %active{$tap}:delete; $done.keep() unless %active; } }, # If there's an async error, close all taps and pass it along. quit => { $lock.protect: -> $err { .close for %active.keys; $done.quit($err); } } } # We hold the lock while doing initial setup, so you can rely on having # done all initialization before processing a first message. $lock.protect: { whenever-impl signal(SIGINT, |%scheduler), { say "Got signal"; exit; } whenever-impl Supply.from-list($*IN.lines, |%scheduler), { say "Got line"; exit if $++ == 1 ; } } $done } supply不在乎消息的来源,它只是在乎通过react实施的自身完整性。还要注意,它也没有引入任何并发:它实际上只是一个并发控制结构。

通常,人们将Lock::Asyncsupply与数据源一起使用,react在其中,并立即获得控制权。然后,我们继续设置其他tap块,退出设置阶段,并且该锁可用于我们收到的任何消息。这种行为是您通常遇到的所有耗材都会得到的。 whenever就是这种情况。当您给signal(...)一个明确的调度程序,并传入Supply.from-list(...)时,也是这种情况;在这种情况下,它会调度从池中的$*SCHEDULER读取的循环,并立即交出控制权。

当我们遇到不是那样的行为时,问题就来了。如果我们点击$*IN,则默认情况下是从当前线程上的Supply.from-list($*IN.lines)进行读取,以生成$*IN的值,因为emit使用Supply.from-list作为其值。默认。那怎么办?只需运行要求立即运行的代码即可!

但是,这又给我们带来了一个谜。鉴于CurrentThreadScheduler不是可重入的,那么如果我们:

  1. 获取锁以进行设置
  2. 调用Lock::Async上的tap,它会同步运行并尝试Supply.from-list(...)的值
  3. 尝试获取锁,以便我们可以处理该值

然后,我们将陷入僵局,因为我们正试图获取一个已经由我们持有的不可重入的锁。的确,如果您在此处运行该程序的desugar,这正是发生的情况:它挂起了。但是,原始代码不会挂起;它的表现有点尴尬。有什么作用?

真正的实现之一是在设置阶段检测这种情况;然后继续执行,并在设置阶段完成后继续执行。这意味着我们可以做类似的事情:

emit

解决问题。我不会在这里重现那个的乐趣,但是应该充分狡猾地使用my $primes = supply { .emit for ^Inf .grep(*.is-prime); } react { whenever $primes { .say } whenever Promise.in(3) { done } } / gather