Skip to content

Instantly share code, notes, and snippets.

@andlabs
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andlabs/740e742a28f41b81a7af to your computer and use it in GitHub Desktop.
Save andlabs/740e742a28f41b81a7af to your computer and use it in GitHub Desktop.
Maybe we can salvage concurrent UI after all...

New proposal: rather than have methods do things, have methods create requests to do things.

At the top level, there is a channel ui.Do which accepts the results of setter methods:

ui.Do <- label.SetText("Hello")
ui.Do <- checkbox.SetChecked(true)

Getters, however, are a bit more work. Not only do we need to send a request, we need to get something back from it. So getters will require three lines:

req := checkbox.Checked()
ui.Do <- req
checked := (<-req.Result).(bool) // <-req.BoolResult?

This is work; we could probably have a wrapper function:

checked := ui.GetBool(checkbox.Checked())

To get rid of the problem caused by atomicity of event handlers, the event handlers will receive its own private ui.Do equivalent:

button.Clicked = func(c ui.Doer) { // type Doer chan<- [something]
    c <- button2.Disable()
}

While in an event handler, requests on ui.Do will stall.

Unfortunately, it's still possible for things outside the library to screw with us: a timer can fire and start an interaction, for instance. There would need to be some resiliency.

We can solve this for the timer case by providing our own timer event wrapper.

Another possibility is to provide a means to connect foreign events. For instance, something like

type ForeignEvent interface {
    Chan() interface{} // must be a receivable channel of any type
    Stop()
}

func NewForeignEvent(channel interface{}, stop func()) ForeignEvent

// prototype for foreign event handlers:
func ForeignEventHandler(c ui.Doer, which ForeignEvent, v interface{})

and then you just wrap your foreign event and let package ui take care of dispatch and cancellation.

This design unfortunately does not consider dialog box modality, which is still a major issue on Windows, because the ocmmon dialogs are still code-modal... perhaps something like

msgreq := ui.MsgBox(w, "This is a message box!",
    "The w parameter is just a hint to say which window the box should appear on top of. " +
    "Some platforms may not respect it.")
ui.Do <- msgreq
<-msgreq.Result

and then all sends on ui.Do are ignored until the dialog is dismissed, just like with event handlers.

LET THE IMPLEMENTATION NOTES BEGIN

All right, so in this system, no user code will urn on the main thread, not even event handlers.

A second goroutine will be responsible for receiving and dispatching ui.Do and all foreign events. Its loop will be of the form

go func() {
    for {
        select {
        case req := <-ui.Do:
            submit(req)
        case <-stall:
            <-stall
        }
    }
}()

The stall channel is what keeps event handlers free from being overrun by ui.Do; further processing of that won't be performed until the event handler returns, as so:

func dispatch() {
    stall <- struct{}{} // hold the main loop
    c := make(chan *request)
    go func() {
        handler(c)
        close(c)
    }()
    for req := range c {
        dispatch(req)
    }
    stall <- struct{}{} // finished 
}

Now what about foreign events? We can't have a foreign event be dispatched during another event handler!

More importantly, however, is that if an event handler wishes to stop a foreign event, and the foreign event is ready to fire, the foreign event must be discarded. Go select statements won't do, as they choose channels randomly in the case of conflict.

We could potentially have a mutex:

func (e *ForeignEvent) dispatch() {
    e.lastchancelock.Lock()
    defer e.lastchancelock.Unlock()

    if e.stop {
        return
    }
    // ...
}

and have ui.Do itself issue these requests:

go func() {
    value := <-e.channel
    ui.Do <- dispatchRequest{e}
}()

such that if an event handler chooses to stop the event from happening, it'd wind up running this code /on its own thread/:

func (e *ForeignEvent) Stop() {
    e.lastchancelock.Lock()
    defer e.lastchancelock.Unlock()

    e.stop = true
}

The only caveat now is that calling Stop() outside an event handler is racy, but we can limit the cancellation of foreign events to within other event handlers (including other foreign event handlers), as the act of stopping an event is most likely the result of another event happening.

All requests (both get and set) have a response. For setters, the response is just struct{}{}. In addition, I could provide something like MultiRequest, which sends multiple requests as a unit, discarding their result values and only returning when the last one has returned.

@hami9x
Copy link

hami9x commented Jul 7, 2014

The getters feel too clumsy IMO, messing with interface{} and losing types for just this is not good, and the Do channel being exposed is very dangerous.

I think a design like this would solve some of the problems:

We have a ui.Request struct

type Request struct {
    Data interface{}
    Completed chan int
}

Let's make ui.Do() a function, and name the channel as ui.tasks, which is not exposed. Basically ui.Do() is

func Do(req *Request) chan int {
    ui.tasks <- req
    return req.Completed
}

So the Setter code just needs to be like this

ui.Do(label.SetText("Hello"))
ui.Do(checkbox.SetChecked(true))

It's the same, but our channel is protected now :)

What's the point of Completed in my code? I think for Getters they don't modify anything, doing no harm, so it's typically unnecessary to make requests. But in case someone wants synchronous, there's a good way. Do() returns a Completed channel, it gets updated when the main thread completes the operation, so just do this if it's required:

<-ui.Do(checkbox.SetChecked(true))
checked := checkbox.Checked()

It must return the right value here.

What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment