belozierov / SwiftCoroutine

Swift coroutines for iOS, macOS and Linux.
https://belozierov.github.io/SwiftCoroutine
MIT License
836 stars 51 forks source link

An equivalent of Go's `select` statement? #28

Open ckornher opened 4 years ago

ckornher commented 4 years ago

Another feature request: Go has a select statement that facilitates the use of multiple channels simultaneously for timers, out-of-band control, etc.

Are you planning to add something similar?

https://tour.golang.org/concurrency/5

belozierov commented 4 years ago

@ckornher I'm not familiar with go lang, but select looks like an actor. You can do something like this:

// Message types for actor
enum CounterMessages {
    case increment, getCounter(CoPromise<Int>)
}

let actor = DispatchQueue.global().actor(of: CounterMessages.self) { receiver in
    var counter = 0
    for message in receiver {
       switch message {
       case .increment:
            counter += 1
        case .getCounter(let promise):
           promise.success(counter)
       }
    }
}

DispatchQueue.concurrentPerform(iterations: 100_000) { _ in
    actor.offer(.increment)
}

let promise = CoPromise<Int>()
promise.whenSuccess { print($0) }
actor.offer(.getCounter(promise))
actor.close()
FabianTerhorst commented 4 years ago

In golang you can use select to read from multiple channels in the same coroutine. E.g.

for i := 0; i < n; i++ {
        select {
        case msgFromChannel1 := <-channel1:
            println("received", msgFromChannel1)
        case msgFromChannel2 := <-channel2:
            println("received", msgFromChannel2)
        }
    }
belozierov commented 4 years ago

@FabianTerhorst @ckornher Hi, it looks really great in golang. We can create some "select" channel to combine results from several channels. It may look something like this (may still need to think about which channel closes or cancels whom):

extension CoChannel {

   func addSubchannel(_ channel: CoChannel) {
      // Cancel subchannel on complete
      whenComplete(channel.cancel)
      // Offer value when receive result
      channel.whenReceive { _ = $0.map(self.offer) }
   }

   func addSubchannel<T>(_ channel: CoChannel<T>, transformer: @escaping (T) -> Element) {
      // Cancel subchannel on complete
      whenComplete(channel.cancel)
      // Offer value when receive result
      channel.whenReceive { result in
         guard let value = try? result.get() else { return }
         self.offer(transformer(value))
      }
   }
}

Then the use may look like this:

enum Foo {
   case caseA(Int), caseB(String)
}

let channelA: CoChannel<Int>
let channelB: CoChannel<String>

let selectChannel = CoChannel<Foo>()
selectChannel.addSubchannel(channelA, transformer: Foo.caseA)
selectChannel.addSubchannel(channelB, transformer: Foo.caseB)

DispatchQueue.global().startCoroutine {
   for foo in selectChannel.makeIterator() {
      switch foo {
      case .caseA(let a):
         print(a)
      case .caseB(let b):
         print(b)
      }
   }
}
ckornher commented 4 years ago

We can create some "select" channel to combine results from several channels.

Yes, this would work except that I think that it suffers from the same problem that I ran into when I tried to use the library as-is. I could not avoid a case where the logic captures values from the "subchannels" while an element is being processed.

Golang never buffers elements within a select and it is safe, for example, to fail the processing of an element and stop a "goroutine" without any other "future" elements being taken from channels in the select.

Golang's select is often used to have a measure of "out-of-band" control over a "goroutine" and buffering would defeat this sort of logic.

Golang select applies a pseudo-random fairness algorithm for "subchannels" that have data. There would be advantages to having priority for channels in some situations, but fairness should be an option, at least.

Golang's select has a default case that executes if none of the "subchannels` has elements. It is rarely used, in my experience, and almost never within a loop, so it would be a "nice to have" feature IMO.

I do not see a way around something like a multi-channel await() method. Pushing elements back to the front of a channel could re-order elements when multiple coroutines are reading a channel. This is common in Go's "pipeline" pattern.

belozierov commented 4 years ago

@ckornher @FabianTerhorst Thank you for explanation. It seems that select is really important functionality that I've missed. I need some free time to think how to implement it properly.

azerum commented 5 days ago

@ckornher I wonder what is a real-world use case for select that cannot be implemented (or too error-prone to implement) using put/take. Could you maybe share some?

For educational purposes, I am implementing Go channels myself. So far I only have put and take operations, and for my simple use cases on project, it was enough so far. I want to keep no. of core operations minimum to keep the implementation correct. It seems like select is not expressible in terms of put/take due to reordering. I wonder if it's necessary to have

Thanks

Edit: nevermind, I should just read Effective Go