第十八章. Supplies, Channels 和 Promises

Supplies, Channels, and Promises

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第十八章. Supplies, Channels 和 Promises

Supplies and channels provide ways to send data from one part of a program to another. A Supply is a direct line of communication from a source of data to possibly many consumers. A Channel allows any part of your program to add data to a queue that any part of your program can read.

Supplies 和 channels 提供了将数据从程序的一部分发送到另一个程序的方法。 Supply 是从数据源到可能许多消费者的直接通信线。 Channel 允许程序的任何部分将数据添加到程序的任何部分都可以读取的队列中。

A Promise allows code to run asynchronously (concurrently)—different bits of code can run in overlapping time frames. This is quite handy while employing either Supplys or Channels (or both).

Promise 允许代码异步(并发)运行 - 不同的小片代码可以在重叠的时间帧中运行。使用 Supplys或 Channels(或两者)时,这非常方便。

Supplies

A Supplier emits a message to every Supply that has asked to receive its messages. This happens asynchronously; they do their work as your program does other things. You can process things in the background and handle the results as they come in rather than stopping the entire program to wait for all of the data. Other languages may call this “Publish–Subscribe” (or “PubSub”).

Supplier 向要求接收其消息的每个 Supply 发出消息。这是异步发生的;当程序做其它的事情时它们完成他们的工作。你可以在后台处理事物并在结果进入时处理结果,而不是停止整个程序等待所有数据。其他语言可称之为“发布 - 订阅”(或“PubSub”)。

Here’s a useless example. Set up a Supplier and call .emit to send a message. Since you didn’t define any Supplys that message goes nowhere; it’s gone forever:

这是一个无用的例子。设置 Supplier 并调用 .emit 发送消息。由于你没有定义任何 Supply ,消息无处可用, 它永远消失了:

my $supplier = Supplier.new;
$supplier.emit: 3;

To receive that message ask the Supplier for a Supply (yes, the terminology is a bit thick) by calling .tapwith a Block:

要接收该消息,请通过调用 .tap 来要求 Supplier 提供 Supply(是的,术语有点多):

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;
my $tap      = $supply.tap: { put "$^a * $^a = ", $^a**2 };
$supplier.emit: 3;

The Supply receives the 3 and passes that as the argument to the Block, which then outputs the message:

Supply 接收 3 并将其作为参数传递给 Block,然后 Block 输出消息:

3 * 3 = 9

There are some useful builtin Supplys available. The .interval Supply factory automatically emits the next ordinal number at the number of seconds (possibly fractional) you specify. You don’t specify the Supplier because that’s handled for you:

有一些有用的内置 Supply 可用。 .interval Supply工厂会自动以指定的秒数(可能是小数)发出下一个序号。你没有指定 Supplier,因为已经为你处理好了:

my $fifth-second = Supply.interval: 0.2;
$fifth-second.tap: { say "First: $^a" };

sleep 1;

The output shows five lines. Why only five? There are five-fifths of a second until the program ends once the sleep finishes:

输出显示五行。为什么只有五行?一旦 sleep 结束,程序结束时有五个零点二秒的时间:

First: 0
First: 1
First: 2
First: 3
First: 4

Once you start the tap it continues to handle values asynchronously until the program ends (or you turn off the tap). Two things happen once your program reaches the sleep statement. First, the program waits the amount of time that you specified. Second, the Supplier emits values that the tap handles. Those two things happen concurrently. As you sleep the Supplier is still working. All that in a couple of lines of code!

一旦你开始点击它继续异步处理值,直到程序结束(或你关闭水龙头 tap)。一旦程序到达 sleep 语句,就会发生两件事。首先,程序会等待指定的时间。其次, Supplier 会发出 tap 处理的值。这两件事同时发生。当你睡眠时, Supplier 仍然在工作。所有这些几行代码就完成了!

注意

Concurrency isn’t parallelism. Concurrency allows two different things to progress during overlapping time frames. Parallelism means that two different things happen at the exact same time. People tend to be fuzzy with their definitions, though.

并发不是并行。并发允许在重叠时间帧期间处理两个不同的事情。并行意味着两个不同的事情在同一时间发生。不过,人们往往对其定义模糊不清。

If you took out the sleep statement you wouldn’t get any output—the program would end right away. The Supplier doesn’t keep the program going. If you increase the sleep time to make the program run longer you get more output.

如果你拿走 sleep 语句,你将无法获得任何输出 - 程序将立即结束。Supplier 不会让程序继续运行。如果增加睡眠时间以使程序运行更长时间,则可获得更多输出。

Here’s a counter that will loop forever but only makes one line. The carriage return goes back to the beginning of the line but doesn’t advance the line (terminal buffering might interfere though):

这是一个永远循环但只产生一行的计数器。回车返回到行的开头但不推进行(终端缓冲可能会干扰):

my $fifth-second = Supply.interval: 0.2;
$fifth-second.tap: { print "\x[D]$^a" };

loop { }

Multiple Taps

You aren’t limited to one tap; you can have as many as you like on the same Supply. This program will take two seconds to finish. The first tap will run for two seconds and the second tap will run for the last second:

你不仅限于一个水龙头(tap); 你可以在同一个 Supply 上拥有任意数量的 tap。该程序将需要两秒钟才能完成。第一次点击将运行两秒钟,第二次点击将运行最后一秒:

my $supply = Supply.interval: 0.5;

$supply.tap: { say "First: $^a" };
sleep 1;

$supply.tap: { say "Second: $^a" };
sleep 1;

Each tap labels its output:

每个点击(tap)标记其输出:

First: 0
First: 1
Second: 0
First: 2
First: 3
Second: 1

Notice anything strange here? The second tap started at 0 again instead of getting the same number as the first tap got at the same time. The .interval method creates an on-demand supply. It starts to produce values when a tap asks for them and it generates the interval fresh for each new tap. Each time a tap wants a value it gets the next one in line, independently of any other taps.

注意这里有什么奇怪的东西吗?第二次点击再次从0开始,而不是获得与第一次点击相同的数字。 .interval 方法创建按需供应(supply)。当水龙头要求它们时它开始产生值,并且它为每个新水龙头(tap)产生新鲜的间隔。每次点击(tap)想要一个值时,它就会获得下一个值,与任何其他点击无关。

The code in a tap must completely finish before that code runs again with another value. This ensures that your code doesn’t get confused when it has persistent variables. If this Block ran again before the first run finished then the value of $n would increment a couple of times before the first run could output its message:

在该代码再次使用其他值运行之前,tap中的代码必须完全完成。这可确保你的代码在具有持久变量时不会混淆。如果此 Block 在第一次运行完成之前再次运行,则 $n 的值将在第一次运行输出其消息之前增加几次:

$supply.tap: {
    state $n = 0; $n++;
    sleep 1;  # misses a couple of emitted values!
    say "$n: $^a"
    };

EXERCISE 18.1Create a Supplier that emits lines of input. Tap that so that you output only the names you have not seen previously. You might use the butterfly census file from the Downloads section of the website.

练习18.1创建一个发出输入行的 Supplier。点击它,这样你只输出以前没有见过的名字。你可以使用 website下载部分的蝴蝶人口普查文件。

Live Supplies

A live supply is different from the on-demand ones you’ve encountered so far. It emits a single stream of values that all taps share. When a new value is available the old one is discarded even if no tap has read it. Each new tap starts with the current value from that single stream. Turn an on-demand supply into a live supply with .share:

实时供应(supply)与你目前遇到的按需供应(supply)不同。它会发出所有分流器(taps)共享的单个值流。当有新值可用时,即使没有点击(tap)已读取旧值,也会丢弃旧值。每个新点击(tap)都以该单个流的当前值开始。使用 .share 将按需供应(supply)转变为实时供应(supply):

my $supply   = Supply.interval(0.5).share;

$supply.tap: { say "First: $^a" };
sleep 1;

$supply.tap: { say "Second: $^a" };
sleep 1;

The output is different in two ways. First, the 0 value is missing. The Supply emitted that before the first tap had a chance to see it. After one second the second tap starts and the Supply emits 2; both taps see 2. After that both taps continue to see the same values until the end of the program:

输出在两个方面有所不同。首先,缺少 0 值。在第一次 tap 有机会看到它之前,Supply 就发出了它。一秒钟后,第二次点击开始, Supply 发出 2;两个水龙头(tap)都看到 2.之后,两个水龙头(tap)在程序结束前继续看到相同的值:

First: 1
First: 2
Second: 2
First: 3
Second: 3
First: 4
Second: 4

When you no longer need a tap you can close it; it will no longer receive values:

当你不再需要水龙头(tap)时,你可以关闭它;它将不再接收值:

my $supply = Supply.interval(0.4).share;

my $tap1 = $supply.tap: { say "1. $^a" };
sleep 1;

my $tap2 = $supply.tap: { say "2. $^a" };
sleep 1;

$tap2.close;

sleep 1;

At the start the first tap is handling everything. The second tap starts after the first sleep finishes. Then both taps handle things for a second, then the first tap closes and it’s only the second tap still working:

在开始时,第一个水龙头处理一切。第一次睡眠结束后,第二次点击(tap)开始。然后两个水龙头(taps)都处理了一秒钟,然后第一个水龙头(tap)关闭,只有第二个水龙头(tap)仍在工作:

First: 1
First: 2
First: 3
Second: 3
First: 4
Second: 4
Second: 5
Second: 6
Second: 7

So far this section has dealt with only Supplys that you created. Many other objects can provide a Supply. The .lines method returns a Seq which can turn into a Supply:

到目前为止,本节仅涉及你创建的 Supply。许多其他对象可以提供 Supply.lines 方法返回一个可以转变成 SupplySeq

my $supply = $*ARGFILES.lines.Supply;  # IO::ArgFiles
$supply.tap: { put $++ ~ ": $^a" };

$supply.tap: {
    state %Seen;
    END { put "{%Seen.keys.elems} unique lines" }
    %Seen{$^a}++;
    };

Most things that are Lists (or can turn into Lists) can do this:

大多数列表(或可以转变成列表)可以做到这一点:

my $list = List.new: 1, 4, 9, 16;
my $supply = $list.Supply;
$supply.tap: { put "Got $^a" }

Even an infinite sequence will work:

即使是无限序列也会起作用:

my $seq := 1, 2, * + 1 ... *;
my $supply2 = $seq.Supply;
$supply2.tap: { put "Got $^a" }

Notice that these examples don’t need a sleep to delay the end of the program. They aren’t “on the clock” like .interval; they go through each of their values.

请注意,这些示例不需要 sleep 来延迟程序结束。它们不像 .interval 那样“在时钟上”;他们遍历了每一个值。

EXERCISE 18.2Create a live Supply that emits a number every second. After three seconds, tap it and output the number it emitted. After another three seconds, tap it again to output the same thing. Wait three more seconds, then close the second tap. Finally, after another three seconds close the first tap.

练习18.2创建一个每秒发出一个数字的实时供应(Supply)。三秒钟后,点击(tap)它并输出它发出的数字。再过三秒钟,再次点击(tap)它输出相同的东西。再等三秒钟,然后关闭第二个水龙头(tap)。最后,再过三秒后关闭第一个水龙头(tap)。

Channels

Channels are first-come, first-served queues. They ensure that something is processed exactly once. Anything can put thingys into the channel and anything can take thingys off the channel. The code on either side of the Channel doesn’t need to know about the other. Several threads can share a Channel, but once something asks for the next thingy that thingy disappears from the Channel and can’t be processed by other code.

Channels 是先到先得的队列。他们确保东西只被准确地处理一次。任何东西都可以将东西放入 Channel,任何东西都可以从 Channels 中将东西拿走。Channel 两侧的代码无需了解另一方。几个线程可以共享一个 Channel,但是一旦有东西要求下一个东西,那个东西就会从 Channel 中消失而不能被其他代码处理。

Create a Channel. Add to it with .send and take a thingy with .receive. When you are done with the Channel, .close it:

创建一个 Channel。使用 .send 添加并使用 .receive 接收。处理完 Channel 后,关闭(close)它:

my $channel = Channel.new;
$channel.send: 'Hamadryas';
put 'Received: ', $channel.receive;
$channel.close;

The output shows the value you added:

输出显示你添加的值:

Received: Hamadryas

After the .close you can’t send more values to the Channel. Anything you’ve already added is still in the Channel and available to process. You can .receive until the Channel is empty:

在关闭(.close) Channel 之后,你无法向 Channel 发送更多值。你已添加的任何内容仍在 Channel 中,可供处理。你可以 .receive 直到 Channel 为空:

my $channel = Channel.new;
$channel.send: $_ for <Hamadryas Rhamma Melanis>;
put 'Received: ', $channel.receive;
$channel.close;  # no more sending

while $channel.poll -> $thingy {
    put "while received $thingy";
    }

The while uses .poll instead of .receive. If there is a thingy, .poll returns it. If there are no more thingys currently available it returns Nil (ending the looping):

while 使用 .poll 而不是 .receive。如果有东西,.poll 会返回它。如果当前没有更多东西可用,则返回Nil(结束循环):

Received: Hamadryas
while received Rhamma
while received Melanis

When .poll returns Nil you don’t know if there will ever be more thingys available. If the Channel is still open something can add more thingys; if the Channel is closed there will never be anything more to .receive. Calling .fail closes the Channel, and .receive will throw an error if you call it again. You can CATCH the Exception to end the loop:

.poll 返回 Nil 时,你不知道是否会有更多可用的东西。如果 Channel 仍处于打开状态,可以添加更多东西; 如果 Channel 关闭,将永远不会有更多的东西可以接收(.receive)。调用 .fail 会关闭 Channel,如果再次调用,则 .receive 会抛出错误。你可以捕获异常以结束循环:

my $channel = Channel.new;
$channel.send: $_ for <Hamadryas Rhamma Melanis>;
put 'Received: ', $channel.receive;
$channel.fail('End of items');   # X::AdHoc

loop {
    CATCH {
        default { put "Channel is closed"; last }
        }
    put "loop received: ", $channel.receive;
    }

Instead of a loop you can tap the Channel; it calls .receive for you:

你可以点击(tapChannel 代替 loop 循环;它为你调用 .receive

my $channel = Channel.new;
$channel.send: $_ for <Hamadryas Rhamma Melanis>;
put 'Received: ', $channel.receive;
$channel.fail('End of items');

$channel.Supply.tap: { put "Received $_" }
CATCH { default { put "Channel is closed" } }

The output is the same either way:

输出相同:

Received: Hamadryas
loop received: Rhamma
loop received: Melanis
Channel is closed

EXERCISE 18.3Create a Channel and tap it. Send lines of input to the Channel but only print the ones with prime line numbers.

练习18.3创建一个 Channel 并点按(tap)它。将输入行发送到 Channel 但仅打印具有主要行号的输入。

Promises

A Promise is a bit of code that will produce a result sometime later, and that later might not be soon. It schedules work to happen in another thread while the rest of your program moves on. These are the underpinnings of Raku’s concurrency and they do most of the hard work for you.

Promise 是一小片会在稍后产生结果的代码,所说的"稍后"可能不会很快。它安排工作在另一个线程中发生,而程序的其余部分继续。这些是 Raku 并发性的基础,它们为你完成了大部分艰苦的工作。

Every Promise has a status. It might be waiting to run, currently running, or finished. How it finishes decides its status: a Promise is Kept when it succeeds or Broken when it fails. While it’s working it’s Planned.

每个 Promise 都有一个状态。它可能正在等待运行,当前正在运行或已完成。它如何完成决定它的状态:Promise 在成功时是 Kept,在失败时是 Broken。当它正在运行时是 Planned

A simple Promise is a timer. The .in method makes a Promise that will be kept after the number of seconds you specify:

一个简单的 Promise 是一个计时器。 .in 方法生成一个 Promise,它将在你指定的秒数后变成 kept 状态:

my $five-seconds-from-now = Promise.in: 5;

loop {
    sleep 1;
    put "Promise status is: ", $five-seconds-from-now.status;
    }

At first the Promise is Planned. After five seconds (roughly) the Promise converts to Kept. At that point you know that five seconds have passed:

起初 PromisePlanned。五秒钟后(大致), Promise 转换为 Kept。那时你知道已经过了五秒钟:

Promise status is: Planned
Promise status is: Planned
Promise status is: Planned
Promise status is: Planned
Promise status is: Kept
Promise status is: Kept
...

You don’t need to continually check the Promise. Use .then to set up code to run when it is kept:

你不需要不断检查 Promise。使用 .then 设置代码在 kept 时运行:

my $five-seconds-from-now = Promise.in: 5;
$five-seconds-from-now.then: { put "It's been 5 seconds" };

Nothing happens when you run this program; the Promise isn’t kept before the program ends. Planned Promises don’t prevent the program from ending.

运行此程序时没有任何反应;在程序结束之前不保留 Promise。计划的 Promise 不会阻止程序结束。

You could give your program enough time for five seconds to elapse. A sleep extends the program time:

你可以给你的程序足够的时间,让你的时间过去五秒钟。睡眠(sleep)延长了程序时间:

my $five-seconds-from-now = Promise.in: 5;
$five-seconds-from-now.then: { put "It's been 5 seconds" };

sleep 7;

Now you see the output from the code in .then:

现在你看到 .then 中代码的输出:

It's been 5 seconds

Waiting for Promises

Instead of sleeping (and guessing the time you need to be idle), you can use await, which blocks your program until the Promise is either kept or broken:

你可以使用 await 而不是 sleep (并猜测你需要空闲的时间)来阻塞你的程序,直到 Promise 被保留或损坏:

my $five-seconds-from-now = Promise.in: 5;
$five-seconds-from-now.then: { put "It's been 5 seconds" };

await $five-seconds-from-now;

These examples use await because you need the program to keep running. In something more interesting your program is likely doing a lot of other work, so you might not need to keep the program alive.

这些示例使用 await,因为你需要程序继续运行。更有趣的是,你的程序很可能会做很多其他的工作,所以你可能不需要让程序活着。

Instead of a relative time you can use .at with an absolute time. That could be an Instant value or something that you can coerce to an Instant (or a Numeric value that represents an Instant):

你可以使用绝对时间的 .at 而不是相对时间。这可以是 Instant 值或者你可以强制为 Instant(或表示 Instant 的数值)的值:

my $later = Promise.at: now + 7;
$later.then: { put "It's now $datetime" };

await $later;

The start keyword creates a Promise. When the code completes the Promise is finished:

start 关键字创建一个 Promise。代码完成后, Promise 完成:

my $pause = start {
    put "Promise starting at ", now;
    sleep 5;
    put "Promise ending at ", now;
    };
await $pause;

The output shows the start and end of the Promise:

输出显示 Promise 的开始和结束:

Promise starting at Instant:1507924913.012565
Promise ending at Instant:1507924918.018444

A Promise is broken if it throws an Exception. You can return all the False values you like, but until youfail or throw an Exception with an error your Promise will be kept. This succeeds even though it returns False:

如果 Promise 抛出异常,它就会被破坏。你可以返回你喜欢的所有 False 值,但是直到你失败或抛出一个带有错误的 Exception,你的 Promise 将被保留。即使它返回 False,这也会成功:

my $return-false = start {
    put "Promise starting at ", now;
    sleep 5;
    put "Promise ending at ", now;
    return False;  # still kept
    };
await $return-false;

This example breaks the Promise because you explicitly fail:

下面这个示例打破了 Promise,因为你显式地调用了 fail

my $five-seconds-from-now = start {
    put "Promise starting at ", now;
    sleep 5;
    fail;
    put "Promise ending at ", now;
    };
await $five-seconds-from-now;

You get part of the output, but the fail stops that Block before you get the rest of the output:

你获得了输出的一部分,但 fail 会在你获得其余输出之前停止该 Block

Promise starting at Instant:1522698239.054087
An operation first awaited:
  in block <unit> at ...

Died with the exception:
    Failed
      in block  at ...

Waiting for Multiple Promises

The Await can take a list of Promises:

Await 可以接收一个 Promise 列表:

put "Starting at {now}";
my @promises =
    Promise.in( 5 ).then( { put '5 finished' } ),
    Promise.in( 3 ).then( { put '3 finished' } ),
    Promise.in( 7 ).then( { put '7 finished' } ),
    ;

await @promises;

put "Ending at {now}";

The program doesn’t end until all of the Promises are kept:

在所有的 Promise 变成 kept 状态之前,程序不会结束:

Starting at Instant:1524856233.733533
3 finished
5 finished
7 finished
Ending at Instant:1524856240.745510

If any of the Promises are broken then the entire await is done and the planned Promises are abandoned:

如果任何一个 Promise 被破坏,则整个 await 完成并且计划的 Promise 被放弃:

put "Starting at {now}";
my @promises =
    start { sleep 5; fail "5 failed" },
    Promise.in( 3 ).then( { put '3 finished' } ),
    Promise.in( 7 ).then( { put '7 finished' } ),
    ;

await @promises;

put "Ending at {now}";

If the .in( 3 ) Promise is kept then the one with start fails:

如果 .in( 3 ) Promise 变为 kept 状态 ,那么带有 start 的那个 Promise 失败:

Starting at Instant:1524856385.367019
3 finished
An operation first awaited:
  in block <unit> at await-list.p6 line 9

Died with the exception:
    5 failed
      in block  at await-list.p6 line 4

Managing Your Own Promises

In the previous examples there was something else managing the Promises for you. You can do that all yourself. Start by making a bare Promise:

在前面的例子中,还有其他东西为你管理 Promise 。你可以自己做。首先制作一个裸的 Promise

my $promise = Promise.new;

Check its status by smart matching against the constants from PromiseStatus (which you get for free):

通过与 PromiseStatus(你免费获得)的常量进行智能匹配来检查其状态:

put do given $promise.status {
    when Planned { "Still working on it" }
    when Kept    { "Everything worked out" }
    when Broken  { "Oh no! Something didn't work" }
    }

At this point $promise is planned and will stay that way. This will loop forever:

在这一点上,$promise 计划并将保持这种方式。这将永远循环:

loop {
    put do given $promise.status {
        when Planned { "Still working on it" }
        when Kept    { "Everything worked out" }
        when Broken  { "Oh no! Something didn't work" }
        }

    last unless $promise.status ~~ Planned;
    sleep 1;
    }

You can use now to note the start time and to check that if it’s five seconds later to make your own .at or .in. Some time after five seconds you call .keep to change the status:

你现在可以使用 now 来记录开始时间,并检查是否在五秒后制作你自己的 .at.in。五秒后的一段时间,你可以调用 .keep 改变状态:

my $promise = Promise.new;

my $start = now;
loop {
    $promise.keep if now > $start + 5;
    given $promise.status {
        when Planned { put "Still working on it" }
        when Kept    { put "Everything worked out" }
        when Broken  { put "Oh no! Something didn't work" }
        }

    last unless $promise.status ~~ Planned;
    sleep 1;
    }

Now the loop stops after five seconds:

现在循环在五秒后停止:

Still working on it
Still working on it
Still working on it
Still working on it
Still working on it
Everything worked out

This Promise can still call code with .then:

这个 Promise 仍然可以使用 .then 调用代码:

my $promise = Promise.new;
$promise.then: { put "Huzzah! I'm kept" }

my $start = now;
loop { ... } # same as before

The output shows the output from the .then code:

输出显示 .then 代码的输出:

Still working on it
Still working on it
Still working on it
Still working on it
Still working on it
Everything worked out
Huzzah! I'm kept

Or you might break the Promise. Either way your .then code runs, and you need to distinguish between those cases. The .then code has one argument; that’s the Promise itself. If you don’t name the argument it’s in $_:

或者你可能破坏了 Promise。无论哪种方式你的 .then 代码运行,你需要区分这些情况。 .then 代码有一个参数;这就是 Promise 本身。如果你没有将参数命名为 $_

my $promise = Promise.new;
$promise.then: {
    put do given .status {
        when Kept { 'Huzzah!' }
        when Broken { 'Darn!' }
        }
    }

my $start = now;
loop {
    $promise.break if now > $start + 5;
    last unless $promise.status ~~ Planned;
    sleep 1;
    }

Promise Junctions

You can use Junctions to create an über-Promise. The .allof method creates a Promise that is kept if all of its included Promises are kept:

你可以使用 Junction 来创建一个超级 Promise.allof 方法创建一个 Promise,如果它包含的所有 Promise 都变成 kept 状态,则该 Promise 为 kept 状态:

my $all-must-pass = await Promise.allof:
    Promise.in(5).then( { put 'Five seconds later' } ),
    start { sleep 3; put 'Three seconds later'; },
    Promise.at( now + 1 ).then( { put 'One second later' } );
put $all-must-pass;

The .anyof Promise is kept if any of its included Promises are kept. All except one of them can be broken and the larger Promise is still kept:

如果 .anyof Promise 中的任何一个 Promise 是 kept 状态,那么该 Promise 为 kept 状态。除了其中一个之外的所有部分都可以是 broken 状态,而更大的 Promise 仍然是 kept 状态:

my $any-can-pass = await Promise.anyof:
    Promise.in(5).then( { put 'Five seconds later' } ),
    start { sleep 3; put 'Three seconds later'; fail },
    Promise.at( now + 1 ).then( { put 'One second later' } );
put $any-can-pass;

Both of these succeed. In the .allof case you see the output from all three Promises. Then you see the output from one of the Promises from .anyof. Not all of those Promises need to finish because the overall Promise already knows it can succeed:

这两个都成功了。在 .allof 情况下,你可以看到所有三个 Promise 的输出。然后你会看到来自 .anyof 的一个 Promise 的输出。并非所有 Promise 都需要完成,因为整体 Promise 已经知道它可以成功:

One second later
Three seconds later
Five seconds later
True
One second later
True

Reactive Programming

A react Block allows you to run some code when new values are available. It keeps running until it runs out of values to handle. It’s similar to an event loop. Here’s a very simple example:

当有新值可用时, react Block允许运行一些代码。它一直运行,直到它用完了要处理的值。它类似于事件循环。这是一个非常简单的例子:

react {
    whenever True { put 'Got something that was true' }
    }

END put "End of the program";

You use whenever to supply values to the Block of code. In this case you have the single value True. This isn’t a conditional expression or a test, as in if or while. The Block reacts to that single value and runs the whenever code. After that there are no more values and the Block exits:

你可以使用 wheneverBlock 块提供值。在这个例子中,你具有单个值 True。这不是条件表达式或测试,如 ifwhileBlock 对该单个值作出反应并运行 whenever 代码。之后没有更多的值, Block 退出:

Got something that was true
End of the program

You might be tempted to think of this as a looping construct, but it’s not quite the same thing. It’s not doing everything in the react Block then starting the Block again. The whenever for True only runs once, instead of running forever as you’d expect with a loop:

你可能会想到这是一个循环结构,但它并不完全相同。它没有在 react Block中执行所有操作,然后再次启动BlockTruewhenever 只运行一次,而不是像你期望的那样永远运行:

loop {
    if True { put 'Got something that was true'  }
    }

Change the whenever from True to a Supply.interval and you never see the end-of-program message:

wheneverTrue 更改为 Supply.interval,你永远不会看到程序结束消息:

my $supply = Supply.interval: 1;

react {
    whenever $supply { put "Got $^a" }
    }

END put "End of the program";

As long as the Supply has values for whenever, the react Block keeps going:

只要 Supply 具有 whenever 可用的值,react Block 就会继续:

Got 0
Got 1
Got 2
...

You could have both the Supply and the True at the same time:

你可以同时拥有 SupplyTrue

my $supply = Supply.interval: 1;

react {
    whenever $supply { put "Got $^a" }
    whenever True { put 'Got something that was true' }
    }

END put "End of the program";

The whenever with the Supply reacts immediately and outputs the first value in the Supply. The whenever with the True reacts next and exhausts its values (the single True). After that the Supply continues until you give up and interrupt the program:

wheneverSupply 立即作出反应并输出 Supply 中的第一个值。接下来 wheneverTrue 做出反应并耗尽其值(单个 True)。之后,Supply 继续,直到你放弃并中断程序:

Got 0
Got something that was true
Got 1
Got 2
...

If you reverse the whenevers the True will probably react first:

如果你把两个 whenever 的顺序对调,那么 True 可能会先做出反应:

my $supply = Supply.interval: 1;

react {
    whenever True { put 'Got something that was true' }
    whenever $supply { put "Got $^a" }
    }

END put "End of the program";

The output is slightly different, but there’s nothing that says this has to be the case. Perhaps future implementations will choose differently. This is concurrency; you can’t depend on strict order of happenings:

输出略有不同,但没有任何说法必须如此。未来的实现可能会有不同的选择。这是并发;你不能依赖严格的发生顺序:

Got something that was true
Got 0
Got 1
Got 2
...

Instead of interrupting the program to get the react to stop, you can do it from within the Block with done. You can use a Promise with .in to provide a value after some interval:

你可以在 Block 使用 done 中完成,而不是中断程序以使响应停止。你可以使用带有 .inPromise 在一段时间后提供一个值:

my $supply = Supply.interval: 1;

react {
    whenever $supply { put "Got $^a" }
    whenever True { put 'Got something that was true' }
    whenever Promise.in(5) { put 'Timeout!'; done }
    }

END put "End of the program";

After five seconds the Promise is kept and the whenever kicks in. It outputs the timeout message and uses done to end the react:

五秒后, Promise 变为 kept,然后 whenever 执行。它输出超时消息并使用 done 结束 react

Got 0
Got something that was true
Got 1
Got 2
Got 3
Got 4
Got 5
Timeout!
End of the program

Add another react and the process starts over with a fresh Supply:

添加另一个 react,然后重新开始一个新的 Promise

my $supply = Supply.interval: 1;

react {
    whenever $supply { put "Got $^a" }
    whenever True { put 'Got something that was true' }
    whenever Promise.in(5) { put 'Timeout!'; done }
    }

put "React again";

react {
    whenever $supply { put "Got $^a" }
    }

END put "End of the program";

The output for the Supply starts again, but at the beginning of the interval:

Promise 的输出再次开始,但在间隔开始时:

Got 0
Got something that was true
Got 1
Got 2
Got 3
Got 4
Timeout!
React again
Got 0
Got 1

EXERCISE 18.4Modify the double react example to use a live Supply instead of an on-demand one. How does the output change?

练习18.4修改双反应示例以使用实时供应而不是按需供应。输出如何变化?

Reacting in the Background

The react is a way that you can respond to values when they are available. So far you’ve seen the react as a top-level Block. It keeps running—and holds up the rest of the program—until it’s done.

react 是一种可以在值可用时响应值的方式。到目前为止,你已经看到了作为顶级Block的 react。它一直运行 - 并持有程序的其余部分 - 直到完成。

Instead, you most likely want your react to do its work in the background as your program does other things. You can wrap the react in a Promise with a start. That allows the react to work in a thread as the rest of the program continues:

相反,你最有可能希望你的 react 能够在后台执行其工作,因为你的程序会执行其他操作。你可以用 startreact 包装在 Promise 中。这允许 react 在程序的其余部分继续时在线程中工作:

my $supply = Supply.interval: 1;

my $promise = start {
    react {
        whenever $supply { put "Got $^a" }
        whenever True { put 'Got something that was true' }
        whenever Promise.in(5) { put 'Timeout!'; done }
        }
    }

put 'After the react loop';

await $promise;
put 'After the await';

END put "End of the program";

The first line of the output is from the put after the start block. The react is starting its work, but it’s not blocking the rest of the program:

输出的第一行来自 start 块之后的 putreact 开始了它的工作,但它没有阻塞程序的其余部分:

After the react loop
Got 0
Got something that was true
Got 1
Got 2
Got 3
Got 4
Timeout!
After the await
End of the program

Take it up a notch. Add a Channel into it. Move the Supply inside the whenever. When that Supply has a value it executes the Block to output the same thing it did before. It also sends the value to the Channel if it is a multiple of 2.

把它提升一个档次。添加一个 Channel。把 Supply 移动到 whenever 里。当该 Supply 有一个值时,它执行 Block 以输出它之前执行的相同操作。如果它是 2 的倍数,它还会将值发送到 Channel

Add a second whenever to read the values available on the Channel. You need to convert the Channel to a Supply; that’s easy because there’s a .Supply method. The whenever taps that Supply:

添加第二个 whenever 以读取 Channel 上可用的值。你需要将 Channel 转换为 Supply; 这很简单,因为有一个 .Supply 方法。whenever 点击 Supply

my $channel = Channel.new;

my $promise = start {
    react {
        whenever Supply.interval: 1
            { put "Got $^a"; $channel.send: $^a if $^a %% 2 }
        whenever $channel.Supply
            { put "Channel got $^a" }
        whenever True
            { put 'Got something that was true' }
        whenever Promise.in(5)
            { put 'Timeout!'; done }
        }
    }

put 'After the react loop';

await $promise;
put 'After the await';

END put "End of the program";

The output is mostly the same as before with the Channel output inserted:

插入 Channel 输出时输出与以前大致相同:

After the react loop
Got 0
Got something that was true
Channel got 0
Got 1
Got 2
Channel got 2
Got 3
Got 4
Channel got 4
Timeout!
After the await
End of the program

EXERCISE 18.5Use IO::Notification to output a message every time there’s a change to a file you specify on the command line.

练习18.5使用 IO::Notification 在每次对命令行指定的文件进行更改时输出消息。

Summary

Promises are the basis of concurrency, and there are various ways that you can create them to get what you what. Decompose your problem into independent bits and run them as Promises, which can run in separate threads (or maybe even on different cores). With those, Supplys and Channels provide a way to pass data between disconnected parts of your program. To get the most out of all of these you need to think differently from the procedural stuff you’ve seen so far. You’ll get that with practice.

Promise是并发的基础,有多种方法可以创建它们来获得你的东西。将你的问题分解为独立的部分并将它们作为Promise 运行,它可以在不同的线程中运行(甚至可以在不同的核心上运行)。有了这些,SupplyChannel 提供了一种在程序的断开连接部分之间传递数据的方法。为了充分利用所有这些,你需要与迄今为止看到的程序性内容进行不同的思考。通过练习你会掌握它。

comments powered by Disqus