Re: Sync API for workers

What I really dislike about this is that the worker can't send the port
directly to a UI thread if it's a nested worker; it has to send it to its
parent, who has to forward it to its parent, and so on.  That seems like
it'll make it hard to implement libraries, since libraries needs to have
its fingers in every one of your workers' main message port, or the author
needs to invent a new message routing mechanism (which is something message
ports are supposed to do for us).

For example, the below example can't be easily modularized and made into a
library.  If regular ports could be used, you could wrap both sides in
"initAlertHandling(messagePort)", handing it a port to communicate over.

The deadlock prevention algorithm I proposed earlier attempted to address
this, but it was too complex.  It attempted to allow as close to arbitrary
message passing as possible without allowing deadlocks; maybe there's a
less permissive approach that would be simple enough.

Here's an alternate proposal.  It attempts to allow transferring these
ports in any way, across regular MessagePorts, but it no longer tries to
implement blocking with regular MessagePorts.  It keeps the
SyncMessageChannel, MessagePortSyncSide and MessagePortAsyncSide interfaces
of your proposal to simplify things:

- When a SyncMessageChannel is created, store an identifier for the current
thread in both ports, called the port's initial thread.  This property
stays the same as the port is transferred around.
- Add a property to both ports called "transferred first", initially null.
 If MessagePortSyncSide is transferred and its "transferred first" property
is null, set its "transferred first" to true and the "transferred first"
property of its corresponding MessagePortAsyncSide to false.  The same is
true in reverse.  (This is possible because, the first time either port is
transferred, they're obviously still in the same thread.)
- If either type of port is transferred to an illegal thread, the recipient
thread automatically calls close() on the port.
- Descendants of a MessagePortSyncSide's initial thread are always legal
threads.  Additionally, if the port's transferred first value is true, the
initial thread itself is also a legal thread.
- Ancestors of a MessagePortSyncSide's initial thread are always legal
threads.  Additionally, if the port's transferred first value is false, the
initial thread itself is also a legal thread.

Like the previous proposal, this requires that the browser can get a view
of the thread tree.  Since all of the checking happens in the thread that
receives the port, and not the sending port, there are no race conditions
due to not knowing the target of a port in advance.

The "transferred first" field allows creating a SyncMessageChannel, and
then either 1: transferring the MessagePortSyncSide to a child worker, or
transferring MessagePortAsyncSide upwards to a parent worker or the UI
thread.  You can't do both; once you transfer a port once, that property is
immutable.

Simple put, if you transfer either port across the boundary defined by the
initial thread, the ports are shut down to prevent the possibility of
deadlocks.

>   any waitForMessage();

With regular messaging you have an Event containing a .data property with
the posted message.  Here, you just have the message.  That'll make adding
metadata difficult.  Are we sure that's what we want?  (One thing we'd lose
is the message's origin; I'm not sure on a quick reading if this is meant
to be supported for message channels, but it looks like it is.)

An aside: one thing you'll want to be able to do is block on multiple
sources, possibly with a timeout, like select().  I think the way this API
would allow that is for the user to create a "multiplexer" thread that does
all of the listening asynchronously, and then a single blocking port is
used between the mux thread and the real thread.

On Sun, Oct 13, 2013 at 2:39 PM, Jonas Sicking <jonas@sicking.cc> wrote:

> * yield only works within generators in JS.
>

And if that's like Python, yield only goes up one level, and then the
caller has to yield too.  Generators are useless for wrapping async APIs
unless you structure your entire codebase around it.  It's very useful (its
incarnation in Python, at least), but has no relevance to the problems the
API this thread is discussing.

To give a the simplest example of what you can do with this API: implement
confirm() in workers.

--- worker ---
({
    var channel = new SyncMessageChannel();
    postMessage({
        port: channel.asyncPort
    });
    var _confirmPort = channel.syncPort;
    confirm = function(message)
    {
        _confirmPort.postMessage(message);
        return _confirmPort.waitForMessage().result;
    }
})();

if(confirm("Delete everything?")) ...

--- main thread
var worker = createWorker(); // create the above worker
worker.onmessage = function(e)
{
    var confirmPort = event.data.port;
    confirmPort.onmessage = function(e)
    {
        var answer = confirm(e.data);
        confirmPort.postMessage(answer);
    }
}
---

This gives a blocking confirm() that works in workers, that can be used
with no other special work in the worker.  (Generators would not allow
that.)

-- 
Glenn Maynard

Received on Monday, 14 October 2013 01:11:34 UTC