Re: Sync API for workers

On Wed, Sep 5, 2012 at 2:49 AM, Jonas Sicking <jonas@sicking.cc> wrote:

>  The problem with a "Only allow blocking on children, except that
> window can't block on its children" is that you can never block on a
> computation which is implemented in the main thread. I think that cuts
> out some major use cases since todays browsers have many APIs which
> are only implemented in the main thread.
>

You can't have both--you have to choose one of 1: allow blocking upwards,
2: allow blocking downwards, or 3: allow deadlocks.  (I believe #1 is more
useful than #2, but each proposal can go both ways.  I'm ignoring more
complex deadlock detection algorithms that can allow both #1 and #2, of
course, since that's a lot harder.)

 The fact that all the examples that people have used while we have
> been discussing synchronous messaging have spun event loops in
> attempts to deal with messages that couldn't be handled by the
> synchronous poller makes me very much think that so will web
> developers.
>

getMessage doesn't spin the event loop.  "Spinning the event loop" means
that tasks are run from task queues (such as asynchronous callbacks) which
might not be expecting to run, and that tasks might be run recursively;
none of that that happens here.  All this does is block until a message is
available on a specified port (or ports), and then returns it--it's just a
blocking call, like sync XHR or FileReaderSync.

I also wonder if what you are describing doesn't make more sense when
> communicating with a child worker and blocking on receiving a response
> from it.
>

That's what I meant, based on a use case you brought up: "a library which
implements the synchronous IndexedDB API".  I think that's by far the most
interesting category of use cases raised for this feature so far, the
ability to implement sync APIs from async APIs (or several async APIs).

Another trait that this looses is the ability to terminate a worker as
> soon as we know that a synchronous response can't be sent. I.e. in
> proposal 1 and 2 the implementation can terminate the worker as soon
> as the object with the .reply() function is GCed. Note that this
> doesn't expose any GC behavior since a "forever blocked" worker
> behaves exactly the same as a terminated worker. I.e. neither will
> ever execute any code.
>

It's the same: terminate when the other MessagePort is GC'd or its
port.close is called.

(The MessagePort cross-process GC issues might sometimes prevent that, but
that's just another instance of the issue that already exists.  By the way,
do you happen to remember where that issue was last discussed in detail?
I'd like to refresh my memory on the details of this problem.)

 Fewer APIs isn't the same thing as a simpler API. On the contrary, I
> think trying to fit too much functionality into the same set of
> functions can easily result in more complexity.
>

Sure, but I do think they're the same in this case.

I think this is fairly well illustrated by the set of rules that you
> ended up having to set up in order to make the "blocking permitted"
> flags work out correctly.


Explaining this to users is simple: "if you want to block on a port, it
needs to only ever be transferred above its other side, not below".

And your algorithm produces weird edge
> cases, such as that it matters if someone sets up a message "proxy"
> which forwards all messages from one channel to another, rather than
> just passes on one end of a channel. With such a "proxy" your ports
> end up touching more threads and so are more likely to clear the
> "blocking permitted" flag. In all other cases such a proxy is
> transparent.
>

The previous proposals allow nothing *but* blocking on your parent (or
child), so if you have threads A <-> B <-> C and you want to pass messages
from C to A, you *have* to proxy messages across (and keep thread B alive
forever as a result).  That's a big part of why we have MessagePorts to
begin with.  That aside, the iteration below removes most of the cases
where you can't block, eg. passing a port up to your parent and then down
to a sibling.



This is a bit wordier, but I think it's easier to understand, since all it
cares about is where the ports was originally created.

- Add a "direction" flag to ports, which may have the values "up", "down",
"disallowed" and "initial", and is initially "initial".
- Add an "original thread" value to ports, which is set to the current
thread at MessageChannel creation time.  This value is preserved across
structured clone.
- When a thread receives a port, compare its "original thread" with the
current thread.
  - If the "root" thread of "original thread" is not on the same as that of
the current thread, mark the port as "disallowed".
  - Otherwise, if "original thread" is the current thread, mark the port as
"initial".
  - Otherwise, if "original thread" is a descendant of the current thread,
mark the port as "up".
  - Otherwise, mark the port as "down".

The UI thread and shared workers are "root" threads.  The root thread of a
dedicated worker is its one ancestor thread which is a root thread.

When required to "mark a port as X", do the following:
 - If the port's "direction" flag is not equal to "initial", let X be
"disallowed".
 - If the port's "direction" flag is already equal to X, terminate these
steps.
 - If X is equal to "initial", let X be "disallowed".
 - If X is "up", let opposite be "down".  If X is "down", let opposite be
"up".  Otherwise, let opposite be "disallowed".
 - Set port's "direction" flag be X.
 - Signal the other side of the port, instructing the thread to mark that
port as opposite.

If a blocking getMessage is invoked:
 - If "direction" is not "up", throw an exception.  If, during a blocking
call, the port is no longer marked "up", throw an exception.

This can be summed up for users simply: "to allow blocking on a port, only
transfer it up from where it starts, and only transfer the other side down
from where it starts".  Aside from that, and staying within the same thread
tree, you can pass the ports around however you like.

This also means that for the basic case of libraries that create a
black-boxed thread and return a port to talk to it, you don't really have
to care about any of this--within that tree, you can toss it around all you
want.

I also changed the "ascendant/descendant"-based scheme into "descendant or
not descendant".  This has two big advanges.  First, it means you can pass
ports anywhere up the tree, even to sibling and "uncle" nodes--anything
that isn't a descendant.  This means that in a lot of cases (where ports
come from a "leaf" worker), you don't have to care about this at all.
Second, this allows shared workers to block on their own dedicated
workers.  This means shared workers can use things like "simulated sync
IndexedDB" implementations, too.

To allow blocking on parents instead of children, if that's what we end up
wanting, just flip the "direction is not up" step to "direction is not
down".


On Wed, Sep 5, 2012 at 9:03 PM, Jonas Sicking <jonas@sicking.cc> wrote:

> The part that I dislike about having single channel used for both sync
> and async messaging is that you end up with one or more async
> listeners which expect to get notified about all incoming messages,
> but then you have an API which "steals" a message away from those
> listeners. On top of that it has to do that stealing without any way
> of ensuring ensuring that it actually steals the right message.


That's exactly the reason to use MessagePorts: to categorize messages.

But being (mostly) agnostic to if the other side is using sync
> messages or not doesn't mean that the other side uses both sync and
> async messaging!


But you still have to do extra work on the sending side to support both
sync and async receiving, since you have to hand it the right type of
channel.

Couldn't we just make calling getMessage permanently disable .onmessage
dispatching (perhaps until the port is posted again)?  That would make it
very hard to accidentally use both, while encapsulating knowledge about
which way it's being used to the receiver, so the sender doesn't need to
carefully send the receiver the right "type" of MessageChannel.  (I really
don't feel this is necessary, but I'd prefer it to multiple MessagePort
interfaces.)

-- 
Glenn Maynard

Received on Thursday, 6 September 2012 03:08:28 UTC