Skip to content

proposal: Go 2: introduce a new broadcast channel generic type #28157

Closed
@urandom

Description

@urandom

Introduction

The following proposal introduces the concept of broadcast channels (cast). Similar to chans, casts pass values around. Unlike a chan, a cast allows a copy of the same value to be received by multiple recepients.

Problem

Channels, being a one-to-one blocking relationship, are ill suited for tasks requiring the notification of multiple recipients. The prime example of this is eventing systems. Due to the blocking nature of channels, one has to manually orchestrate the multiplication of the payload to all interested parties, which is difficult and error prone. Sometimes it is virtually impossible to manage these lifecycles if the receivers' lifecycles isn't known.

Solution

A broadcast channel provides a mechanism for producers to emit values of a specific type without carying how many receivers there are, or indeed if there are any at all. It is defined as a regular generic type (cast) (using the generic draft proposal syntax), which prevents any naming collisions with existing code.

bcast := make(cast(int), 10)
var recv <-cast(string)

Like its counterpart, a broadcast channel supports the "<-" operator and can be defined with a direction. It can also have a capacity.

A broadcast is never bidirectional. When a direction is not specific, a send direction is assumed:

var b1 cast(func())
var b2 cast<-(func())

b1 == b2

Unlike a channel, a broadcast never blocks on a send, regardless of capacity.

Broadcasting is achieved by way of assigning/converting a sending broadcast to a receiving one. A broadcast channel can only be converted from a sending to a receiving channel, never the other way around. Each receiving copy of the sending channel will in turn receive any value sent through that broadcast channel, whenever they are requesting to receive items (in case the broadcast capacity is 0).

Consider a hypothetical UI library, with its hypothetical 'Button' element:

func (b *Button) clicked() <-cast(ClickEvent) {
   return b.clickCast
}

...

// Processing all clicks of the button somewhere in the app
for ev := range button.clicked() {
}

...

// same button, awaiting the next click event, at some point in the of the lifecycle of the app
ev := <-button.clicked()

A single instance of the button will be able to notify both listeners for its click event. There will be no need to manage the complex lifecycles of any listener, and in turn the receivers need not worry about notifying the button that they have stopped listening. This management will be handled by the Go runtime automatically.

Capacity

As stated earlier, a broadcast channel also supports a capacity, similarly to a channel. On the sender side, the capacity indicates the size of the value buffer queue. With 0 capacity, the buffer size will still be one, but the effects on the receivers will be different. Each time the sender receives a new value, it places it in the tail of the buffer and wakes all receives currently waiting for a value.

When a receiver first tries to read a value, it looks at the capacity of the sender. If it is zero, it sleeps until the sender wakes it up with a fresh value. When it wakes up, it reads the value from the buffer. Subsequent receive operations first check if there is a new value in the buffer, and will use it if one is available.

If the capacity is non-zero, it starts using the value buffer directly. When it reaches the head of the buffer, it blocks until it is woken up and can read a new value from it.

var b := make(cast(int), 4)

for i := 0; i < 5; i++ {
    b <- i
}

r1 := (<-cast(int))(b)

println(<-r1) // 1
println(<-r1) // 2
println(<-r1) // 3

r2 := (<-cast(int))(b)
println(<-r2) // 1

b <- 10
b <- 11

println(<-r1) // 4
println(<-r1) // 10
println(<-r1) // 11
// println(<-r1) blocks


println(<-r2) // 3
println(<-r2) // 4
println(<-r2) // 10
println(<-r2) // 11
// println(<-r2) blocks

Some naive implementation details

When a receiver is read from, the runtime will add it to a list of receivers that are waiting for an item from the sender. When the item is received, the receiver will be removed from the list.

If we consider the buffer queue as a linked list, the item will have a pointer to the next one in the queue. When a receiver requests a new item, it will attempt to get the next item using the pointer. If the next is nil, and the item it holds is the same as one in the front of the queue, that means that no other items are present and it will block until one appears. If the next pointer is nil but the item differs from the first, then that means that the item is already stale and has dropped from the queue. The receiver will therefore use the item from the front as its new current one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions