Class/Object

monix.catnap

ConcurrentChannel

Related Docs: object ConcurrentChannel | package catnap

Permalink

final class ConcurrentChannel[F[_], E, A] extends ProducerF[F, E, A] with ChannelF[F, E, A]

ConcurrentChannel can be used to model complex producer-consumer communication channels.

It exposes these fundamental operations:

Example

import cats.implicits._
import cats.effect._
import monix.execution.Scheduler.global

// For being able to do IO.start
implicit val cs = SchedulerEffect.contextShift[IO](global)
// We need a `Timer` for this to work
implicit val timer = SchedulerEffect.timer[IO](global)

// Completion event
sealed trait Complete
object Complete extends Complete

def logLines(consumer: ConsumerF[IO, Complete, String], index: Int): IO[Unit] =
  consumer.pull.flatMap {
    case Right(message) =>
      IO(println("Worker $$index: $$message"))
        // continue loop
        .flatMap(_ => logLines(consumer, index))
    case Left(Complete) =>
      IO(println("Worker $$index is done!"))
  }

for {
  channel <- ConcurrentChannel[IO].of[Complete, String]
  // Workers 1 & 2, sharing the load between them
  task_1_2 = channel.consume.use { ref =>
    (logLines(ref, 1), logLines(ref, 2)).parSequence_
  }
  consumers_1_2 <- task_1_2.start // fiber
  // Workers 3 & 4, receiving the same events as workers 1 & 2,
  // but sharing the load between them
  task_3_4 = channel.consume.use { ref =>
    (logLines(ref, 3), logLines(ref, 4)).parSequence_
  }
  consumers_3_4 <- task_3_4.start // fiber
  // Pushing some samples
  _ <- channel.push("Hello, ")
  _ <- channel.push("World!")
  // Signal there are no more events
  _ <- channel.halt(Complete)
  // Await for the completion of the consumers
  _ <- consumers_1_2.join
  _ <- consumers_3_4.join
} yield ()

Unicasting vs Broadcasting vs Multicasting

Unicasting: A communication channel between one producer and one ConsumerF. Multiple workers can share the load of processing incoming events. For example in case we want to have 8 workers running in parallel, you can create one ConsumerF, via consume and then use it for multiple workers. Internally this setup uses a single queue and whatever workers you have will share it.

Broadcasting: the same events can be sent to multiple consumers, thus duplicating the load, as a broadcasting setup can be created by creating and consuming from multiple ConsumerF via multiple calls to consume. Internally each ConsumerF gets its own queue and hence messages are duplicated.

Multicasting: multiple producers can push events at the same time, provided the channel's type is configured as a MultiProducer.

Back-Pressuring and the Polling Model

When consumers get created via consume, a buffer gets created and assigned per consumer.

Depending on what the BufferCapacity is configured to be, the initialized consumer can work with a maximum buffer size, a size that could be rounded to a power of 2, so you can't rely on it to be precise. See consumeWithConfig for customizing this buffer on a per-consumer basis, or the ConcurrentChannel.withConfig builder for setting the default used in consume.

On push, when the queue is full, the implementation back-pressures until the channel has room again in its internal buffer(s), the task being completed when the value was pushed successfully. Similarly ConsumerF.pull (returned by consume) awaits the channel to have items in it. This works for both bounded and unbounded channels.

For both push and pull, in case awaiting a result happens, the implementation does so asynchronously, without any threads being blocked.

Multi-threading Scenario

This channel supports the fine-tuning of the concurrency scenario via ChannelType.ProducerSide (see ConcurrentChannel.withConfig) and the ChannelType.ConsumerSide that can be specified per consumer (see consumeWithConfig).

The default is set to MultiProducer and MultiConsumer, which is always the safe choice, however these can be customized for better performance.

These scenarios are available:

The default is MPMC, because that's the safest scenario.

import cats.implicits._
import cats.effect.IO
import monix.execution.ChannelType.{SingleProducer, SingleConsumer}
import monix.execution.BufferCapacity.Bounded

val channel = ConcurrentChannel[IO].withConfig[Int, Int](
  producerType = SingleProducer
)

val consumerConfig = ConsumerF.Config(
  consumerType = Some(SingleConsumer)
)

for {
  producer  <- channel
  consumer1 =  producer.consumeWithConfig(consumerConfig)
  consumer2 =  producer.consumeWithConfig(consumerConfig)
  fiber1    <- consumer1.use { ref => ref.pull }.start
  fiber2    <- consumer2.use { ref => ref.pull }.start
  _         <- producer.push(1)
  value1    <- fiber1.join
  value2    <- fiber2.join
} yield {
  (value1, value2)
}

Note that in this example, even if we used SingleConsumer as the type passed in consumeWithConfig, we can still consume from two ConsumerF instances at the same time, because each one gets its own internal buffer. But you cannot have multiple workers per ConsumerF in this scenario, because this would break the internal synchronization / visibility guarantees.

WARNING: default is MPMC, however any other scenario implies a relaxation of the internal synchronization between threads.

This means that using the wrong scenario can lead to severe concurrency bugs. If you're not sure what multi-threading scenario you have, then just stick with the default MPMC.

Credits

Inspired by Haskell's Control.Concurrent.ConcurrentChannel, but note that this isn't a straight port — e.g. the Monix implementation has a cleaner, non-leaky interface, is back-pressured and allows for termination (via halt), which changes its semantics significantly.

Linear Supertypes
ChannelF[F, E, A], ProducerF[F, E, A], Serializable, Serializable, AnyRef, Any
Ordering
  1. Alphabetic
  2. By Inheritance
Inherited
  1. ConcurrentChannel
  2. ChannelF
  3. ProducerF
  4. Serializable
  5. Serializable
  6. AnyRef
  7. Any
  1. Hide All
  2. Show All
Visibility
  1. Public
  2. All

Value Members

  1. final def !=(arg0: Any): Boolean

    Permalink
    Definition Classes
    AnyRef → Any
  2. final def ##(): Int

    Permalink
    Definition Classes
    AnyRef → Any
  3. final def ==(arg0: Any): Boolean

    Permalink
    Definition Classes
    AnyRef → Any
  4. final def asInstanceOf[T0]: T0

    Permalink
    Definition Classes
    Any
  5. def awaitConsumers(n: Int): F[Boolean]

    Permalink

    Awaits for the specified number of consumers to be connected.

    Awaits for the specified number of consumers to be connected.

    This is an utility to ensure that a certain number of consumers are connected before we start emitting events.

    n

    is a number indicating the number of consumers that need to be connected before the returned task completes

    returns

    a task that will complete only after the required number of consumers are observed as being connected to the channel

    Definition Classes
    ConcurrentChannelProducerF
  6. def clone(): AnyRef

    Permalink
    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  7. def consume: Resource[F, ConsumerF[F, E, A]]

    Permalink

    Create a ConsumerF value that can be used to consume events from the channel.

    Create a ConsumerF value that can be used to consume events from the channel.

    Note in case multiple consumers are created, all of them will see the events being pushed, so a broadcasting setup is possible. Also multiple workers can consumer from the same ConsumerF value, to share the load.

    The returned value is a Resource, because a consumer can be unsubscribed from the channel, with its internal buffer being garbage collected.

    Definition Classes
    ConcurrentChannelChannelF
    See also

    consumeWithConfig for fine tuning the internal buffer of the created consumer

  8. def consumeWithConfig(config: Config): Resource[F, ConsumerF[F, E, A]]

    Permalink

    Version of consume that allows for fine tuning the underlying buffer used.

    Version of consume that allows for fine tuning the underlying buffer used.

    config

    is configuration for the created buffer, see ConsumerF.Config for details

    Definition Classes
    ConcurrentChannelChannelF
    Annotations
    @UnsafeProtocol()
  9. final def eq(arg0: AnyRef): Boolean

    Permalink
    Definition Classes
    AnyRef
  10. def equals(arg0: Any): Boolean

    Permalink
    Definition Classes
    AnyRef → Any
  11. def finalize(): Unit

    Permalink
    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( classOf[java.lang.Throwable] )
  12. final def getClass(): Class[_]

    Permalink
    Definition Classes
    AnyRef → Any
  13. def halt(e: E): F[Unit]

    Permalink

    Stops the channel and sends a halt event to all current and future consumers.

    Stops the channel and sends a halt event to all current and future consumers.

    Consumers will receive a Left(e) event after halt is observed.

    Note that if multiple halt events happen, then only the first one will be taken into account, all other halt messages are ignored.

    Definition Classes
    ConcurrentChannelProducerF
  14. def hashCode(): Int

    Permalink
    Definition Classes
    AnyRef → Any
  15. final def isInstanceOf[T0]: Boolean

    Permalink
    Definition Classes
    Any
  16. final def ne(arg0: AnyRef): Boolean

    Permalink
    Definition Classes
    AnyRef
  17. final def notify(): Unit

    Permalink
    Definition Classes
    AnyRef
  18. final def notifyAll(): Unit

    Permalink
    Definition Classes
    AnyRef
  19. def push(a: A): F[Boolean]

    Permalink

    Publishes an event on the channel.

    Publishes an event on the channel.

    If the internal buffer is full, it asynchronously waits until the operation succeeds, or until the channel is halted.

    If the channel has been halted (via halt), then nothing gets published, the function eventually returning a false value, which signals that no more values can be published on the channel.

    Example

    import cats.implicits._
    import cats.effect.Sync
    
    sealed trait Complete
    object Complete extends Complete
    
    def range[F[_]](from: Int, until: Int, increment: Int = 1)
      (channel: ConcurrentChannel[F, Complete, Int])
      (implicit F: Sync[F]): F[Unit] = {
    
      if (from != until)
        channel.push(from).flatMap {
          case true =>
            range(from + increment, until, increment)(channel)
          case false =>
            F.unit // we need to stop
        }
      else // we're done, close the channel
        channel.halt(Complete)
    }
    a

    is the message to publish

    returns

    a boolean that is true if the value was pushed on the internal queue and the producer can push more values, or false if the channel is halted and cannot receive any more events

    Definition Classes
    ConcurrentChannelProducerF
  20. def pushMany(seq: Iterable[A]): F[Boolean]

    Permalink

    Publishes multiple events on the channel.

    Publishes multiple events on the channel.

    If the channel has been halted (via halt), then the publishing is interrupted, the function returning a false value signalling that the channel was halted and can no longer receive any more events.

    Example

    import cats.implicits._
    import cats.effect.Sync
    
    sealed trait Complete
    object Complete extends Complete
    
    def range[F[_]](from: Int, until: Int, increment: Int = 1)
      (channel: ConcurrentChannel[F, Complete, Int])
      (implicit F: Sync[F]): F[Unit] = {
    
      channel.pushMany(Range(from, until, increment)).flatMap {
        case true =>
          channel.halt(Complete)
        case false =>
          F.unit // was already halted, do nothing else
      }
    }
    seq

    is the sequence of messages to publish on the channel

    returns

    a boolean that is true if all the values were pushed on the internal queue and the producer can push more values, or false if the channel is halted and cannot receive any more events

    Definition Classes
    ConcurrentChannelProducerF
  21. final def synchronized[T0](arg0: ⇒ T0): T0

    Permalink
    Definition Classes
    AnyRef
  22. def toString(): String

    Permalink
    Definition Classes
    AnyRef → Any
  23. final def wait(): Unit

    Permalink
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  24. final def wait(arg0: Long, arg1: Int): Unit

    Permalink
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  25. final def wait(arg0: Long): Unit

    Permalink
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )

Inherited from ChannelF[F, E, A]

Inherited from ProducerF[F, E, A]

Inherited from Serializable

Inherited from Serializable

Inherited from AnyRef

Inherited from Any

Ungrouped