├── .gitignore ├── README.md ├── build.sbt ├── project └── build.properties ├── src └── main │ └── scala │ ├── CancelFiber.scala │ ├── FFI.scala │ ├── PrintThread.scala │ ├── Resources.scala │ ├── RunLoop.scala │ ├── RunWithFiber.scala │ ├── RunWithoutFiber.scala │ ├── ShiftOnFuture.scala │ ├── ShiftOnOnePool.scala │ └── ShiftOnTwoPools.scala └── sync-async-boundary.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/metals,scala,sbt,bloop,visualstudiocode,intellij 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=metals,scala,sbt,bloop,visualstudiocode,intellij 4 | 5 | ### Bloop ### 6 | .bloop/ 7 | 8 | ### Intellij ### 9 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 10 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 11 | 12 | # User-specific stuff 13 | .idea 14 | 15 | ### Metals ### 16 | .metals/ 17 | project/**/metals.sbt 18 | 19 | ### SBT ### 20 | # Simple Build Tool 21 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 22 | 23 | dist/* 24 | target/ 25 | lib_managed/ 26 | src_managed/ 27 | project/project 28 | project/boot/ 29 | project/plugins/project/ 30 | .history 31 | .cache 32 | .lib/ 33 | .bsp 34 | 35 | ### Scala ### 36 | *.class 37 | *.log 38 | 39 | ### VisualStudioCode ### 40 | .vscode 41 | 42 | ### VisualStudioCode Patch ### 43 | # Ignore all local history of files 44 | .ionide 45 | 46 | # End of https://www.toptal.com/developers/gitignore/api/metals,scala,sbt,bloop,visualstudiocode,intellij 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Concurrency In Scala with Cats-Effect 2 | 3 | This text serves as an introduction to the topic of concurrent asynchronous effects in Scala, based on the [Cats-Effect](https://typelevel.org/cats-effect/) library. 4 | 5 | However, many of the concepts presented here are applicable not only to other Scala effect libraries as well, but also to any system or programming language that deals with concurrency and asynchronous programming. 6 | 7 | Note: 8 | It should be pointed out that the intention of the text is not to provide a "better documentation". 9 | First, because existing material on the subject is pretty good already (you can find some links in the [References](#references) section), and secondly, because I don't consider myself anything remotely near being an expert in the field. These are simply my notes that I kept while I was exploring the topic, and that I'm willing to share with whomever might find them useful. 10 | 11 | All code snippets are based on Cats-Effect 2, since Cats-Effect 3 wasn't yet out at the time of writing this text. 12 | 13 | ## Table of Contents 14 | 15 | - [Introduction](#introduction) 16 | - [Asynchronous boundary](#asynchronous-boundary) 17 | - [Threading](#threading) 18 | - [Threads and thread pools](#threads-and-thread-pools) 19 | - [Java Executors](#java-executors) 20 | - [Scheduling and ExecutionContext](#scheduling-and-executioncontext) 21 | - [Cats-Effect IO basics](#cats-effect-io-basics) 22 | - [Overview](#overview) 23 | - [Synchronous methods](#synchronous-methods) 24 | - [Asynchronous methods (FFI)](#asynchronous-methods-ffi) 25 | - [Resource handling](#resource-handling) 26 | - [Fibers](#fibers) 27 | - [Definition](#definition) 28 | - [Fibers in the code](#fibers-in-the-code) 29 | - [Continuations](#continuations) 30 | - [Run loop](#run-loop) 31 | - [Cooperative yielding](#cooperative-yielding) 32 | - [ContextShift](#contextshift) 33 | - [Examples](#examples) 34 | - [IO vs Future](#io-vs-future) 35 | - [Leaking fibers](#leaking-fibers) 36 | - [Summary](#summary) 37 | - [Cats-Effect 3](#cats-effect-3) 38 | - [Fibers outside of Scala](#fibers-outside-of-scala) 39 | - [Project Loom](#project-loom) 40 | - [Green threads](#green-threads) 41 | - [References](#references) 42 | 43 | ## Introduction 44 | 45 | First, let's see some useful definitions in the context of threads: 46 | 47 | - Blocking: 48 | Thread is put to sleep and resumed later (e.g. until mutex is released). 49 | 50 | - Non-blocking: 51 | Opposite of blocking - no threads are being put to sleep. 52 | 53 | - Synchronous: 54 | Thread completes the task, either by success or failure, before reaching any task after it. 55 | 56 | - Asynchronous: 57 | Thread starts some task T and offloads it to another thread, which allows it to immediately proceed with task T+1 before T finishes. 58 | 59 | - Concurrency: 60 | State in which multiple threads have their tasks interleaved. 61 | 62 | - Parallelism: 63 | State in which multiple threads run simultaneously. 64 | 65 | Note that concurrency isn't the same as parallelism: we could have concurrency without parallelism (effects are interleaved, but everything is done by one single CPU core), and we could have parallelism without concurrency (multiple cores are running multiple standalone threads that don't interleave). In this text, we are interested in the concurrent aspect of our programs, and we don't care whether some of the tasks are done in parallel or not. 66 | 67 | Also, there seems to be a lot of confusion around blocking vs synchronous and non-blocking vs asynchronous. This confusion mainly comes from the fact that some people use "blocking" in the context of application code, in which case it's indeed pretty much the same thing as "synchronous". However, "blocking" can also have completely different semantics - it can mean that a thread is put in a `BLOCKED` state (one of six available thread states on the JVM). If that's what you consider to be "blocking" - a physical system property rather than a conceptual one like "synchronous" - then the difference becomes quite visible. 68 | 69 | For example, spinlock is a mechanism that serves as an alternative to mutex. It ensures that a thread cannot acquire some resource until it's been released by the thread that's currenlty using it. So, this system is synchronous, but it's not blocking - instead of being put into blocked state, the thread is kept alive spinning forever in a loop, constantly polling for the resource until it's released. If you're working on the operating system level, this makes a huge difference - spinlock doesn't need the heavy context switching required for scheduling / descheduling a thread, but it hogs the CPU and is therefore only used for short time periods. However, to us application code writers it doesn't honestly make a big difference. We know that a "blocked thread" cannot be used to perform some useful work and for all we care it's sitting around wasted until it's unblocked. 70 | 71 | This text doesn't dive into low level details where blocking vs synchronous makes a difference. Semantics of "blocking a task" are "current task has to finish before any subsequent task can start", which is the same thing as "synchronous". In the same fashion, for all intents and purposes, "non-blocking" is the same thing as "asynchronous". 72 | 73 | ## Asynchronous boundary 74 | 75 | Concurrency means that a single logical thread can have its tasks distributed across different threads. Or, from the thread perspective, that a single thread can execute interleaved tasks coming from different logical threads. 76 | 77 | In order to be able to do so, we have to use asynchronous operations. Once the task has been initiated, it crosses the *asynchronous boundary*, and it resumes somewhere else. 78 | 79 | Here are a couple of famous related quotes: 80 | 81 | > "A Future represents a value, detached from time" - Viktor Klang ([link](https://monix.io/docs/current/eval/task.html#comparison-with-scalas-future)) 82 | 83 | > "Asynchronous process is a process that continues its execution in a different place or time than the one it started in" - Fabio Labella ([link](https://www.youtube.com/watch?v=x5_MmZVLiSM&t=6m35s)) 84 | 85 | > "A logical thread offers a synchronous interface to an asynchronous process" - Fabio Labella ([link](https://www.youtube.com/watch?v=x5_MmZVLiSM&t=11m12s)) 86 | 87 | All of the above quotes revolve around the same concept of asynchronous boundary; after our computation passes that boundary, we cannot be sure *when* it will be finished, which is why we should not block while waiting for it. We're also not sure *where* it will be finished - another OS thread, another node in our physical network, somewhere in the cloud etc. This text deals with the details of execution on OS threads (hence the relevance of the third quote), and it will not touch upon any other scenario of asynchronous execution, such as distributing the tasks over nodes in a cluster. 88 | 89 | Speaking of third quote, bear in mind that saying "synchronous interface" comes from the fact that all our code is basically synchronous. We write commands one after another and they are executed one after another. So when you see a term "synchronous code", pay attention to the context - maybe someone meant "code that doesn't use asynchronous effects (e.g. it blocks on every asynchronous call)". In this text, however, "synchronous code" means just any code, because all code we write is in nature synchronous. It is via usage of asynchronous effects in our code that we get to cross the asynchronous boundary and model asynchronously executed operations. 90 | 91 | Let's see how tasks are executed by OS threads (each letter-number combination represents one task): 92 | 93 | ![sync-async](sync-async-boundary.png) 94 | 95 | After crossing the asynchronous boundary, the tasks get interleaved across threads in the thread pool. Some of them might even get executed on some thread from another thread pool. This is where the property of concurrency comes into play. 96 | 97 | ## Threading 98 | 99 | ### Threads and thread pools 100 | 101 | JVM threads map 1:1 to the operating system’s native threads. 102 | When CPU stops executing one thread and starts executing another thread, OS needs to store the state of the earlier task and restore the state for the current one. 103 | This context switch is expensive and sacrifices *throughput*. 104 | In ideal world we would have a fixed number of tasks and at least the same number of CPU threads; then every task would run on its own dedicated thread and throughput would be maximal, because context switches wouldn't exist. 105 | 106 | However, in the real world there are things to consider: 107 | - There will be external requests from the outside that need to be served 108 | - Even if there are no external requests and no I/O (e.g. we're just mining bitcoin), some work-stealing is bound to happen anyway, for example JVM's garbage collector 109 | 110 | This is why it's useful to sacrifice some throughput to achieve *fairness*. High fairness makes sure that all tasks get their share of the CPU time and no task is left waiting for too long. 111 | 112 | Asynchronous operations can be divided into three groups based on their thread pool requirements: 113 | 114 | - Non-blocking asynchronous operations, e.g. HTTP requests, database calls 115 | - Blocking asynchronous operations, e.g. reading from the file system 116 | - CPU-heavy operations, e.g. mining bitcoin 117 | 118 | These three types of operations require significantly different thread pools to run on: 119 | 120 | - Non-blocking asynchronous operations: 121 | - Bounded pool with a very low number of threads (maybe even just one), with a very high priority. These threads will basically just sit idle most of the time and keep polling whether there is a new async IO notification. Time that these threads spend processing a request directly maps into application latency, so it's very important that no other work gets done in this pool apart from receiving notifications and forwarding them to the rest of the application. 122 | 123 | - Blocking asynchronous operations: 124 | - Unbounded cached pool. Unbounded because blocking operation can (and will) block a thread for some time, and we want to be able to serve other I/O requests in the meantime. Cached because we could run out of memory by creating too many threads, so it’s important to reuse existing threads. 125 | 126 | - CPU-heavy operations: 127 | - Fixed pool in which number of threads equals the number of CPU cores. This is pretty straightforward. Back in the day the "golden rule" was number of threads = number of CPU cores + 1, but "+1" was coming from the fact that one extra thread was always reserved for I/O (as explained above, we now have separate pools for that). 128 | 129 | Remember: whenever you are in doubt over which thread pool best suits your needs, optimal solution is to benchmark. 130 | 131 | ### Java Executors 132 | 133 | In Java, thread pools are modeled through `Executor` / `ExecutorService` interface. The latter provides termination capabilities and some utility functions. 134 | 135 | Two most commonly used `Executor` implementations are: 136 | - [ThreadPoolExecutor](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html) introduced in Java 5: 137 | Thread pool that executes each submitted task using one of possibly several pooled threads. 138 | 139 | - [ForkJoinPool](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html) introduced in Java 7: 140 | Work-stealing thread pool that tries to make use of all your CPU cores by splitting up larger chunks of work and assigning them to multiple threads. 141 | If one of the threads finishes its work, it can steal tasks from other threads that are still busy. 142 | You can set the number of threads to be used in the pool, bounded by some configured minimum and maximum. 143 | 144 | There are many online sources on the difference between the two - I personally like [this one](http://www.h-online.com/developer/features/The-fork-join-framework-in-Java-7-1762357.html). 145 | 146 | Utility methods for obtaining various `Executor` implementations are available in the [Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html) class. 147 | 148 | Here are some recommendations on which implementation to use for each of the scenarios described earlier: 149 | 150 | - Non-blocking asynchronous operations: 151 | - `Executors.newFixedThreadPool` 152 | 153 | - Blocking asynchronous operations: 154 | - `Executors.newCachedThreadPool` 155 | 156 | - CPU-heavy operations: 157 | - For long-running tasks: `Executors.newFixedThreadPool` 158 | - For many small tasks: `new ForkJoinPool` (not available in `Executors`) 159 | 160 | ### Scheduling and ExecutionContext 161 | 162 | Now that the thread pools are set up, we are ready to start submitting tasks to them. 163 | In our program, we will simply submit a task for execution, and it will get executed at some point when it's assigned to a thread. 164 | This assignment is done by the *scheduler*. 165 | 166 | There are two ways scheduling can be achieved: 167 | 168 | - Preemptive scheduling: 169 | Scheduler suspends the currently running task in order to execute another one 170 | - Cooperative scheduling (or "cooperative yielding"): 171 | Tasks suspend themselves, meaning that the currently running task at some point voluntarily suspends its own execution, so that the scheduler can give the thread to other tasks 172 | 173 | Role of the scheduler is played by the `ExecutionContext` ([docs](https://docs.scala-lang.org/overviews/core/futures.html#execution-context)). 174 | Every `ExecutionContext` schedules threads only within its assigned thread pool. 175 | 176 | In Scala, there is one global `ExecutionContext`. 177 | It is backed by the `ForkJoinPool` and it is available as `ExecutionContext.global`. 178 | 179 | Whichever underlying Java executor you rely on, your level of granularity is going to be threads. 180 | Threads don't cooperate. 181 | They execute their given set of commands, and the operating system makes sure that they all get some chunk of the CPU time. That's why I can be typing this text, listening to music and compiling some code, all at the same time. 182 | 183 | So, in order to allow tasks submitted to the `ExecutionContext` to use the principle of cooperative yielding, we have to explore the concept of **fibers**, which belong to a more general concept of [green threads](#green-threads). 184 | 185 | We will explore fibers and cooperative yielding in [later sections](#fibers), but before we do that, we need to become familiar with some programming constructs from the Cats-Effect library. 186 | 187 | ## Cats-Effect IO basics 188 | 189 | ### Overview 190 | 191 | Type `IO` ([docs](https://typelevel.org/cats-effect/datatypes/io.html)) is used for encoding side effects as pure values. 192 | In other words, it allows us to model operations from the other side of asynchronous boundary in our synchronous code. 193 | 194 | There are two main groups of `IO` values - those that model: 195 | - synchronous computations 196 | - asynchronous computations 197 | 198 | A somewhat different definition is that `IO` consists of two things: 199 | - FFI (Foreign Function Interface) for side-effectful asynchronous functions; see section [Asynchronous methods (FFI)](#asynchronous-methods-ffi)). Examples: `async` and `cancelable`. 200 | - Combinators defined either directly inside `IO` or coming from type classes. Examples: `pure`, `map`, `flatMap`, `delay`, `start` etc. 201 | 202 | ### Synchronous methods 203 | 204 | Here are several ways of producing synchronous `IO` values in a "static" way: 205 | ```scala 206 | object IO { 207 | ... 208 | def pure[A](a: A): IO[A] 209 | def delay[A](body: => A): IO[A] // same as apply() 210 | def suspend[A](thunk: => IO[A]): IO[A] 211 | ... 212 | } 213 | ``` 214 | 215 | There is one more important method that we will be using heavily, but this one is not static; it's defined as a class method on values of type `IO`: 216 | 217 | ```scala 218 | class IO[A] { 219 | ... 220 | def start(implicit cs: ContextShift[IO]): IO[Fiber[IO, A]] 221 | ... 222 | } 223 | ``` 224 | 225 | **Pure:** 226 | 227 | Wraps an already computed value into `IO` context, for example `IO.pure(42)`. 228 | Comes from the [Applicative](https://typelevel.org/cats/typeclasses/applicative.html) type class (note: from [Cats](https://typelevel.org/cats/), not Cats-Effect). 229 | 230 | **Delay:** 231 | 232 | Used for suspension of synchronous side effects. 233 | Comes from the [Sync](https://typelevel.org/cats-effect/typeclasses/sync.html) type class. 234 | Note that `IO.apply` calls `IO.delay`, which means we can call `delay` using the shorthand form `IO(value)`. 235 | 236 | **Suspend:** 237 | 238 | Method `suspend` also suspends a synchronous side effect and it also comes from [Sync](https://typelevel.org/cats-effect/typeclasses/sync.html), but this time it's an effect that produces an `IO`. 239 | Note that: 240 | ```scala 241 | IO.pure(x).flatMap(f) <-> IO.suspend(f(x)) 242 | ``` 243 | 244 | ### Asynchronous methods (FFI) 245 | 246 | `IO` also serves as an FFI - a [Foreign Function Interface](https://en.wikipedia.org/wiki/Foreign_function_interface). 247 | Most common usage of the term FFI is to serve as a translation layer between different programming languages, but here the context is a bit different: `IO` translates side-effectful asynchronous Scala operations into pure, referentially-transparent values. 248 | 249 | Translating such operations into `IO` world is done primarily via the following two methods: 250 | 251 | ```scala 252 | object IO { 253 | ... 254 | def async[A](k: (Either[Throwable, A] => Unit) => Unit): IO[A] 255 | def cancelable[A](k: (Either[Throwable, A] => Unit) => CancelToken[IO]): IO[A] 256 | ... 257 | } 258 | ``` 259 | 260 | **Async:** 261 | 262 | Method `async` is used for modeling asynchronous operations in our Cats-Effect code. 263 | 264 | It comes from the [Async](https://typelevel.org/cats-effect/typeclasses/async.html) type class. 265 | 266 | Its signature is: 267 | 268 | ```scala 269 | def async[A](k: (Either[Throwable, A] => Unit) => Unit): F[A] 270 | ``` 271 | 272 | (Note: in [Cats-Effect 3]((https://github.com/typelevel/cats-effect/issues/634)), `Async` will contain more methods) 273 | 274 | Method `async` provides us with a way to describe an asynchronous operation (that is, operation that happens on the other side of an asynchronous boundary) in our synchronous code. 275 | 276 | Let's say that there is some callback-based method `fetchUser` which retrieves an user from the database and possibly returns an error in case something went wrong. 277 | The user of this method will provide a callback which will do something with the received user or react to the received error. 278 | Fetching method and its callback could look something like this: 279 | 280 | ```scala 281 | def fetchUser(userId: UserId): Future[User] 282 | def callback(result: Try[User]): Unit 283 | ``` 284 | 285 | How do we now model this asynchronous operation in synchronous code? 286 | That's what methods like `onComplete` are for (see [Future Scaladoc](https://www.scala-lang.org/api/current/scala/concurrent/Future.html)). 287 | We say that `onComplete` models an operation that happens on the other side of the asynchronous boundary, and it serves as an interface to our synchronous code. 288 | 289 | Let's use `onComplete` to implement a helper function that, given a `Future`, provides us with a synchronous model of the underlying asynchronous process: 290 | 291 | ```scala 292 | def asyncFetchUser(fetchResult: Future[User])(callback: Try[User] => Unit): Unit = 293 | fetchResult.onComplete(callback) 294 | ``` 295 | 296 | We can say that `onComplete` is a method for providing a *description* (or a model) of some asynchronous process by translating it across the asynchronous boundary to our synchronous code. 297 | 298 | So finally, what `Async` gives us is a method from such a description to an effect type `F`, in our case `IO`. 299 | We could therefore explain the signature of `async` with the following simplification: 300 | 301 | ```scala 302 | def async[A](k: (Either[Throwable, A] => Unit) => Unit): F[A] 303 | ``` 304 | translates to 305 | ```scala 306 | def async[A](k: Callback => Unit): F[A] 307 | ``` 308 | which further translates to 309 | ```scala 310 | def async[A](k: AsyncProcess): F[A] 311 | ``` 312 | 313 | For example, if method `fromFuture` weren't already [implemented](https://github.com/typelevel/cats-effect/blob/dd8607baed11da140688d24e467ce76159517910/core/shared/src/main/scala/cats/effect/internals/IOFromFuture.scala#L28) for `IO`, we could implement it as: 314 | 315 | ```scala 316 | def fromFuture[A](future: => Future[A]): IO[A] = 317 | Async[IO].async { cb => 318 | future.onComplete { 319 | case Success(a) => cb(Right(a)) 320 | case Failure(e) => cb(Left(e)) 321 | } 322 | } 323 | ``` 324 | 325 | We don't care about what callback `cb` really does. 326 | That part is handled by the implementation of `async`. 327 | Its purpose is to provide users of `async` method with a way of signaling that the asynchronous process has completed. 328 | "Here, call this when you're done". 329 | 330 | You can find the full code to play around with in the [code repository](https://github.com/slouc/concurrent-effects/blob/master/src/main/scala/FFI.scala). 331 | 332 | **Cancelable:** 333 | 334 | Method `cancelable` is present in `IO`, just like `async`, and they have similar signatures. 335 | Just like `async`, it creates an `IO` instance that executes an asynchronous process on evaluation. 336 | But unlike `async`, which comes from [Async](https://typelevel.org/cats-effect/typeclasses/async.html) type class, `cancelable` comes from [Concurrent](https://typelevel.org/cats-effect/typeclasses/concurrent.html) type class. 337 | 338 | If you understand `async`, `cancelable` is simple. 339 | The difference is: 340 | - `async` models an asynchronous computation and puts it inside an `IO` effect. 341 | We use the callback to signal the completion of the asynchronous computation. 342 | 343 | - `cancelable` models a cancelable asynchronous computation and puts it inside an `IO` effect. 344 | We use the callback to signal the completion of the asynchronous computation, 345 | and we provide an `IO` value which contains the code that should be executed if the asynchronous computation gets canceled. 346 | This value is of type `IO[Unit]`, declared in the signature by using the type alias `CancelToken[IO]`. 347 | 348 | It's important to emphasize that `cancelable` does not produce an `IO` that is cancelable by the user. 349 | You cannot say: 350 | ```scala 351 | val io = IO.cancelable(...) 352 | io.cancel // or something like that 353 | ``` 354 | Instead, what `cancelable` does is - it takes a foreign (meaning it comes from outside of our IO world) asynchronous computation that is cancelable in its nature, and puts it in the `IO` context. 355 | So, it allows us to model asynchronous computations in the same fashion that `async` does, but with the extra ability to define an effect that will be executed if that asynchronous computation gets canceled. 356 | 357 | For example, such asynchronous computation could be a running thread, a database connection, a long poll to some HTTP API etc., and by using `cancelable` we can translate that foreign computation into `IO` world and define what should happen if that computation "dies" (e.g. somebody kills the database connection). 358 | 359 | This is how we could modify our previous `async` example to include the cancelation feature: 360 | ```scala 361 | def fromFutureCancelable[A](future: => Future[A]): IO[A] = 362 | IO.cancelable { cb => 363 | future.onComplete { 364 | case _ => // don't use the callback! 365 | } 366 | IO(println("Rollback the transaction!")) 367 | } 368 | ``` 369 | Notice how we don't call the `cb` callback any more. 370 | By not calling `cb`, we can emulate a long running operation, one that we have enough time to cancel (remember, `cb` is used to denote the completion of the asynchronous computation that we are modelling). 371 | 372 | If you run the [code](https://github.com/slouc/concurrent-effects/blob/master/src/main/scala/FFI.scala), you will get an infinite computation that can be stopped by manually killing the process, which then shows "Rollback the transaction!". 373 | If you use `async` instead of `cancelable` (also provided in the code repository), you will notice that there is no "Rollback the transaction!" line. 374 | 375 | ### Resource handling 376 | 377 | Resource handling refers to the concept of acquiring some resource (e.g. opening a file, connecting to the database etc.) and releasing it after usage. 378 | 379 | Cats effect model resource handling via [Resource](https://typelevel.org/cats-effect/datatypes/resource.html) type. 380 | 381 | Here's an example (pretty much c/p-ed from the Cats website): 382 | 383 | ```scala 384 | def mkResource(s: String): Resource[IO, String] = { 385 | val acquire = IO(println(s"Acquiring $s")) *> IO.pure(s) 386 | def release(s: String) = IO(println(s"Releasing $s")) 387 | Resource.make(acquire)(release) 388 | } 389 | 390 | val r = for { 391 | outer <- mkResource("outer") 392 | inner <- mkResource("inner") 393 | } yield (outer, inner) 394 | 395 | override def run(args: List[String]): IO[ExitCode] = 396 | r.use { case (a, b) => IO(println(s"Using $a and $b")) }.map(_ => ExitCode.Success) 397 | ``` 398 | 399 | For-comprehension that builds the value `r` operates on the `Resource` layer (hence, `Resource` is a monad). 400 | We can easily compose multiple `Resource`s by flatmapping through them. 401 | 402 | The output of the above program is: 403 | 404 | ``` 405 | Acquiring outer 406 | Acquiring inner 407 | Using outer and inner 408 | Releasing inner 409 | Releasing outer 410 | ``` 411 | 412 | As you can see, `Resource` takes care of the LIFO (Last-In-First-Out) order of acquiring / releasing. 413 | 414 | If something goes wrong, `Resource`s are released: 415 | 416 | ```scala 417 | val r = for { 418 | outer <- mkResource("outer") 419 | _ <- Resource.liftF(IO.raiseError(new Throwable("Boom!"))) 420 | inner <- mkResource("inner") 421 | } yield (outer, inner) 422 | ``` 423 | results with 424 | ``` 425 | Acquiring outer 426 | Releasing outer 427 | java.lang.Throwable: Boom! 428 | ``` 429 | and 430 | ```scala 431 | val r = for { 432 | outer <- mkResource("outer") 433 | inner <- mkResource("inner") 434 | _ <- Resource.liftF(IO.raiseError(new Throwable("Boom!"))) 435 | } yield (outer, inner) 436 | ``` 437 | results with 438 | ``` 439 | Acquiring outer 440 | Acquiring inner 441 | Releasing inner 442 | Releasing outer 443 | java.lang.Throwable: Boom! 444 | ``` 445 | 446 | For more details on resource handling in Cats-Effect, refer to this [excellent blog post](https://medium.com/@bszwej/composable-resource-management-in-scala-ce902bda48b2). 447 | 448 | ## Fibers 449 | 450 | 451 | ### Definition 452 | 453 | You can think of fibers as lightweight threads which use cooperative scheduling, unlike actual threads which use preemptive scheduling. 454 | 455 | Note that in some contexts / languages fibers are also known as *coroutines*; "fiber" is usually used in the system-level context, while coroutines are used in the language-level context. 456 | However, in Scala "fiber" is the preferred term. 457 | 458 | Unlike OS and JVM threads which are managed in the kernel space, fibers are managed in the user space. 459 | In other words, fibers are a made-up construct that lives on the heap because we bring it to life in our own code (well, library code - see next section). 460 | 461 | As a mental model, it's useful to think of fibers as a yet another level of abstraction above threads: 462 | 463 | ``` 464 | Fibers 465 | ------- 466 | Threads 467 | ------- 468 | CPU 469 | ``` 470 | 471 | Fibers map to threads many-to-few, same as how threads map to CPU cores. 472 | 473 | There is one important difference and one important similarity when it comes to these two layers of abstraction (fibers on threads vs threads on CPU). 474 | 475 | Difference: 476 | 477 | As mentioned [earlier](#scheduling-and-executioncontext), threads are scheduled on the CPU *preemptively*, whereas fibers are scheduled on threads *cooperatively* 478 | 479 | Similarity: 480 | 481 | **Blocking on one level means de-scheduling on the level below**. This is a very powerful insight. 482 | CPU doesn't know anything about blocking; it just keeps running threads in circles. 483 | What we call a "blocked thread" is simply, from CPU's perspective, a thread that got de-scheduled and will be run again later at some point. 484 | Same principle becomes even more important on the layer above, because it makes us realise that blocking a fiber doesn't block the underlying thread. 485 | Blocking a fiber simply means it will step aside, yielding its thread to other fibers, and wait until it's scheduled again. 486 | Threads are a system resource and we should try not to block them, but when it comes to fibers - block away! 487 | 488 | Depending on your code, as well as the library that you are using, you can decide to have your fibers cooperate more often, thus achieving fairness, or to cooperate less often, thus sacrificing some fairness for more throughput (concepts of fairness and throughput have been [introduced earlier](#threads-and-thread-pools)). 489 | 490 | ### Fibers in the code 491 | 492 | I want to explicitly point out that fiber in Scala is a **made-up, custom construct**, not some native resource like process or thread. 493 | Project [Loom](https://wiki.openjdk.java.net/display/loom/Main) (also see this [blogpost](https://blog.softwaremill.com/will-project-loom-obliterate-java-futures-fb1a28508232?gi=c5487dba95ec)) is aiming to introduce fibers as native JVM constructs, but until that happens, fibers in Scala will continue to be a manually-implemented thing. 494 | This also means that they might have some minor differences in implementation across different libraries (e.g. [cats-effect](https://typelevel.org/cats-effect/datatypes/fiber.html) vs [ZIO](https://zio.dev/docs/overview/overview_basic_concurrency)). 495 | 496 | Let's see some code: in Cats-Effect, fiber is a construct with `cancel` and `join`: 497 | ```scala 498 | trait Fiber[F[_], A] { 499 | def cancel: F[Unit] 500 | def join: F[A] 501 | } 502 | ``` 503 | 504 | Joining a fiber can be thought of as blocking for completion, but only on semantic level. 505 | Remember, blocking a fiber doesn't really block the underlying thread, because the thread can keep running other fibers. 506 | 507 | A program defined as `IO` value can be executed on a fiber. 508 | `IO` type uses the method `start` (available as long as there is an implicit `ContextShift[IO]` in the scope) to start its execution on a fiber. `ContextShift` will be explained later; for now, think of it as Cats-Effect version of `ExecutionContext`. 509 | It's basically a reference to the desired thread pool that should execute the fiber. 510 | Note that it is most likely going to be removed in Cats-Effect 3; see [cats-effect 3](#cats-effect-3) section. 511 | 512 | By using `start`, we can make two or more `IO`s run in parallel. 513 | It is also perfectly possible to describe the whole `IO` program without ever invoking `start`; this simply means that the whole `IO` program will run on a single fiber. 514 | 515 | Here is some very simple code that demonstrates how `IO` describes side effects and runs them on a single fiber (there will be more examples in the [ContextShift](#contextshift) section): 516 | 517 | ```scala 518 | import cats.effect.{ExitCode, IO, IOApp} 519 | 520 | object MyApp extends IOApp { 521 | 522 | def io(i: Int): IO[Unit] = 523 | IO(println(s"Hi from $i!")) // short for IO.delay or IO.apply 524 | 525 | val program1 = for { 526 | _ <- io(1) 527 | _ <- io(2) 528 | } yield ExitCode.Success 529 | 530 | override def run(args: List[String]): IO[ExitCode] = program1 531 | } 532 | ``` 533 | 534 | You will notice that the main object extends `IOApp`. This is a very useful Cats-Effect trait that allows us to describe our programs as `IO` values, without having to actually run them manually by using `unsafeRunSync` or similar methods. Remember how we said earlier that invoking `start` on some `IO` requires an implicit instance of `ContextShift[IO]` in order to define the `ExecutionContext` (and hence the thread pool) to run on? Well, `IOApp` comes with a default `ContextShift` instance, which you can also override if you want to. This is why we didn't have to explicitly define any implicit `ContextShift[IO]` instance in our code. 535 | 536 | For-comprehension is working on `IO` layer; we could flatMap `io1` into a bunch of other `IO`s, for example reading some stuff from the console, then displaying some more stuff, then doing an HTTP request, then talking to a database, etc. 537 | 538 | Now let's see what happens if we want to run some `IO` on a separate fiber: 539 | 540 | ```scala 541 | ... 542 | val program2 = for { 543 | fiber <- io(1).start 544 | _ <- io(2) 545 | _ <- fiber.join 546 | } yield ExitCode.Success 547 | ... 548 | ``` 549 | 550 | We define a chain of `IO`s, and then at some point we run some part of that chain on a separate fiber. That's the only difference from the previous program - parts where we `start` and `join` the fiber. Invoking `io1.start` produces an `IO[Fiber[IO, Unit]]`, which means we get a handle over the new fiber which we can then join later (which means waiting for completion), or cancel it on error, or keep it running until some external mechanism tells us to cancel it, etc. 551 | 552 | It's important to realize which exact instructions in the example above get executed on which fiber. After we started the execution of `io1` on a separate fiber, everything we did afterwards was done in parallel to the original fiber. We say that the code captured in `io1` was the *source* for the new fiber. 553 | 554 | As a small exercise, try adding a small sleep to method `io`: 555 | 556 | ```scala 557 | def io(i: Int): IO[Unit] = IO({ 558 | Thread.sleep(3000) 559 | println(s"Hi from $i!") 560 | }) 561 | ``` 562 | 563 | If you now measure the execution time between `program1` and `program2`, you will see that `program1` runs in six seconds, while `program2` runs in slightly over three seconds. You can find this code [in the repository](https://github.com/slouc/concurrent-effects/blob/master/src/main/scala/RunWithFiber.scala). 564 | 565 | ### Continuations 566 | 567 | In the previous section, we have seen that fiber runs a set of instructions (the source). 568 | Any `IO` can be run on a fiber as long as there is a `ContextShift` type class instance available for it. 569 | This is done by calling `.start` on it, which needs an instance of a `ContextShift` in order to know which thread pool to run the fiber on. 570 | 571 | 572 | Basically, this is what a fiber really is under the hood - it's a *continuation* (set of chained instructions) with a *scheduler* (in this case, `ContextShift`). 573 | 574 | Let's explain this further: 575 | 576 | - Continuation is a stack of function calls that can be stopped and stored in the heap at some point (with yield) and restarted afterward (with run). We have just seen how we can build up the continuation as a series of flatmapped instructions wrapped in an `IO`. 577 | 578 | - Scheduler schedules fibers on a thread pool so that the execution of fiber's code can be carried by multiple worker threads. Once we do `.start` on an `IO`, we start it on a separate fiber, and scheduler schedules it on the thread pool. In Cats-Effect, the role of the scheduler is performed by `ContextShift`, which uses the underlying Scala `ExecutionContext`. 579 | 580 | ### Run loop 581 | 582 | Each fiber is associated with a *run loop* that executes the instructions from the source (that is, from the continuation) one by one. 583 | 584 | Run loop needs access to two JVM resources: 585 | - execution context (forking & yielding) 586 | - scheduled executor service (sleeping before getting submitted again) 587 | 588 | Run loop builds up a stack of tasks to be performed. 589 | We could model the stack in Scala code like this: 590 | 591 | ```scala 592 | sealed trait IO[+A] 593 | case class FlatMap[B, +A](io: IO[B], k: B => IO[A]) extends IO[A] 594 | case class Pure[+A](v: A) extends IO[A] 595 | case class Delay[+A](eff: () => A) extends IO[A] 596 | ``` 597 | 598 | Now, let's say we have the following program: 599 | ```scala 600 | val program = for { 601 | _ <- IO(println(s"What's up?")) 602 | input <- IO(readLine) 603 | _ <- IO(println(s"Ah, $input is up!")) 604 | } yield ExitCode.Success 605 | ``` 606 | 607 | Run loop stack for that program would then look something like this: 608 | ```scala 609 | FlatMap( 610 | FlatMap( 611 | Delay(() => print("What's up?")), 612 | (_: Unit) => Delay(() => readLine) 613 | ), 614 | input => Delay(() => println(s"Ah, $input is up!")) 615 | ) 616 | ``` 617 | ([here](https://github.com/typelevel/cats-effect/blob/master/core/shared/src/main/scala/cats/effect/internals/IORunLoop.scala) is the link to the actual Cats-Effect `IO` run loop) 618 | 619 | When the program is run at the "end of the world", the stack is submitted to the scheduler. 620 | Is it submitted all at once? Or is it submitting one layer of FlatMap at a time, each time yielding back when the task is completed, allowing other fibers to run? 621 | 622 | Actually, this is up to the library / construct being used: 623 | 624 | - Scala Future: Yields back on every `FlatMap` 625 | - IO: Yields back when the whole stack is completed 626 | - Monix Task: Yields back every N operations (at the moment of writing this, I believe N = 1024 by default, but don't take my word for it). 627 | 628 | This means that Scala `Future` optimizes for fairness, `IO` for throughput, and `Monix` takes the middle approach. 629 | Note that it's not impossible to turn things the other way around: we could prevent `Future` from yielding all the time (and thus optimize for throughput) by using a non-shifting instance of `ExecutionContext`, and we could manually force `IO` to shift after every operation (thus optimizing for fairness). 630 | 631 | You can think of `Future` as "fairness opt-out" and `IO` as "fairness opt-in". 632 | 633 | Note on cancellation inside the run-loop: when a cancellation command has been issued for some running `IO`, it can only be cancelled at two particular points, and one such point is inserted by the library on every 512 flatMaps in the run loop stack. 634 | The other one is at the asynchronous boundary (see [Context shift](#context-shift) section). 635 | 636 | ### Cooperative yielding 637 | 638 | The relationship between fibers and threads is the following: 639 | - It is possible to have each fiber running on its dedicated thread 640 | - It is possible to have all fibers running on only one thread 641 | - It is possible to have one fiber switching between multiple threads 642 | - Usually, you will want to have M to N mapping (M = fibers, N = threads), with M > N 643 | - Whenever multiple fibers need to compete for the same thread, they will cooperatively yield to each other, thus allowing each fiber to run a bit of its work and then allow some other fiber to take the thread 644 | - How often and at which point fiber yields depends on the underlying implementation; in Cats-Effect, it won't yield until you tell it to, which allows fine-tuning between fairness and throughput 645 | 646 | Because the number of fibers is usually higher than the number of threads in a given thread pool, fibers need to yield control to each other in order to make sure that all fibers get their piece of the CPU time. 647 | For example, in a pool with two threads that's running three fibers, one fiber will be waiting at any given point. 648 | 649 | In Cats-Effect 2, cooperative yielding is controlled via `ContextShift`. 650 | 651 | ### ContextShift 652 | 653 | Note that `ContextShift` is most likely going to be removed in Cats-Effect 3; see [Cats-Effect 3](#cats-effect-3) section. 654 | However, core principles explained here are useful to understand, because they will still be relevant in the next version. 655 | 656 | In Cats-Effect, submitting the fiber to a thread pool is done via `ContextShift` construct. 657 | It has two main abilities: to run the continuation on some `ExecutionContext`, and to shift it to a different `ExecutionContext`. 658 | 659 | Here's the trait: 660 | 661 | ```scala 662 | trait ContextShift[F[_]] { 663 | def shift: F[Unit] 664 | def evalOn[A](ec: ExecutionContext)(f: F[A]): F[A] 665 | } 666 | ``` 667 | 668 | You can think of it as a type class, even though it is not really a valid type class because it doesn't have the coherence restriction - a type can implement a type class in more than one way. For example, you might want to have a bunch of instances of `ContextShift[IO]` lying around, each constructed using a different `ExecutionContext` and representing a different thread pool (one for blocking I/O, one for CPU-heavy stuff, etc.). Constructing an instance of `ContextShift[IO]` is easy: `val cs = IO.contextShift(executionContext)`. 669 | 670 | Method `shift` is how we achieve fairness. 671 | Every fiber will be executed synchronously until shifted, at which point other fibers will have the chance to advance their work. 672 | 673 | Don't confuse `shift` from `ContextShift` with `IO.shift`. 674 | The semantics are the same, but they come in slightly different forms. 675 | `IO` version has the following two overloads of `shift` method: 676 | 677 | ```scala 678 | def shift(implicit cs: ContextShift[IO]): IO[Unit] 679 | def shift(ec: ExecutionContext): IO[Unit] 680 | ``` 681 | 682 | These two methods are similar in nature - they both shift to the desired thread pool, one by providing the Scala's `ExecutionContext`, the other one by providing a `ContextShift`. 683 | It is recommended to use `ContextShift` by default, and to provide `ExecutionContext` only when you need fine-grained control over the thread pool in use. 684 | Note that you can simply provide the same `ContextShift` / `ExecutionContext` that you're already running on, which will have the effect of cooperatively yielding to other fibers on the same thread pool, same as `shift` from the type class 685 | (you can even invoke it simply as `IO.shift`, as long as you have your `ContextShift` available implicitly). 686 | 687 | So, just to repeat, `ContextShift` can perform a "shift" which either moves the computation to a different thread pool or sends it to the current one for re-scheduling. 688 | Point at which the shift happens is often referred to as *asynchronous boundary*. 689 | Concept of asynchronous boundary has been described in the [Asynchronous boundary](#asynchronous-boundary) section, and now it has been revisited in the Cats-Effect context. 690 | 691 | Asynchronous boundary is one of two places at which an `IO` can be cancelled (the other one is every 512 flatMaps in the run loop; see the [Run loop](#run-loop) section). 692 | 693 | ### Examples 694 | 695 | Method `shift` will be demonstrated on two examples. 696 | 697 | #### Example 1: Single pool 698 | 699 | First, we will use a thread pool with only one thread, and we will start two fibers on that thread. 700 | Note that I'm removing some boilerplate to save space (`IOApp` etc.), but you can find the full code in the repository. 701 | Also note that `Executors.newSingleThreadExecutor` and `Executors.newFixedThreadPool(1)` are two alternative ways of declaring the same thing. I will use the latter, simply to keep the consistency with examples that use multi-threaded pools. 702 | ```scala 703 | val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 704 | val cs: ContextShift[IO] = IO.contextShift(ec) 705 | 706 | def loop(id: String)(i: Int): IO[Unit] = 707 | for { 708 | _ <- IO(printThread(id)) 709 | _ <- IO(Thread.sleep(200)) 710 | result <- loop(id)(i + 1) 711 | } yield result 712 | 713 | val program = for { 714 | _ <- loop("A")(0).start(cs) 715 | _ <- loop("B")(0).start(cs) 716 | } yield ExitCode.Success 717 | ``` 718 | Method `printThread` is a printline statement that includes the thread identifier for extra clarity: 719 | ```scala 720 | def printThread(id: String) = { 721 | val thread = Thread.currentThread.getName 722 | println(s"[$thread] $id") 723 | } 724 | ``` 725 | 726 | Code is pretty straightforward - we have a recursive loop that goes on forever, and all it does is print out some ID ("A" or "B"). 727 | 728 | What gets printed out when we run the above program is an endless stream of "A", because first fiber never shifts (that is, never cooperatively yields) so the second fiber never gets a chance to run. 729 | 730 | Now let's add the shifting to the above code snippet: 731 | ```scala 732 | def loop(id: String)(i: Int): IO[Unit] = for { 733 | _ <- IO(printThread(id)) 734 | _ <- IO.shift(cs) // <--- now we shift! 735 | result <- loop(id)(i + 1) 736 | } yield result 737 | ``` 738 | 739 | What gets printed out in this case is an alternating sequence of "A"s and "B"s: 740 | ``` 741 | ... 742 | [pool-1-thread-1] A 743 | [pool-1-thread-1] B 744 | [pool-1-thread-1] A 745 | [pool-1-thread-1] B 746 | [pool-1-thread-1] A 747 | ... 748 | ``` 749 | 750 | Even though we have only one thread, there are two fibers running on it, and by telling them to `shift` after every iteration, they can work cooperatively together. 751 | At any given point only one fiber is running on the thread, but soon it backs away and gives the other fiber an opportunity to run on the same thread. 752 | 753 | Before we move on, let's see what happens if we run this example without spawning any separate fibers, but we keep the `shift` inside the loop. So basically we just remove the `start` parts: 754 | ```scala 755 | val program = for { 756 | _ <- loop("A")(0) // .start(cs) 757 | _ <- loop("B")(0) // .start(cs) 758 | } yield ExitCode.Success 759 | ``` 760 | What we get is: 761 | ``` 762 | [ioapp-compute-0] A 763 | [pool-1-thread-1] A 764 | [pool-1-thread-1] A 765 | [pool-1-thread-1] A 766 | ... 767 | ``` 768 | The program started looping on "A" on the default main thread, and "B" never got its chance to run. 769 | After the first loop cycle, "A" was then shifted to the thread pool defined by `cs`. 770 | Each subsequent shift had the effect of yielding within the same thread, but it had no visible effect in this case, because there are no other fibers competing for the thread. 771 | But don't forget - we still did introduce an asynchronous boundaries with every `shift` though. 772 | So even if there's only one fiber running on the thread pool, that doesn't mean that `shifting` on that thread pool has no consequences (for example, every time we `shift` we set a checkpoint at which cancellation can happen if requested, as explained in the [ContextShift](#contextshift) section). 773 | 774 | #### Example 2: Two pools 775 | 776 | In the second example, we will have the same two fibers, but this time each fiber will get its own thread pool with a single thread. 777 | 778 | ```scala 779 | val ec1 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 780 | val ec2 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 781 | 782 | val cs1: ContextShift[IO] = IO.contextShift(ec1) 783 | val cs2: ContextShift[IO] = IO.contextShift(ec2) 784 | 785 | def loop(id: String)(i: Int): IO[Unit] = for { 786 | _ <- IO(print(id)) 787 | _ <- if (i == 10) IO.shift(cs1) else IO.unit 788 | result <- loop(id)(i + 1) 789 | } yield result 790 | 791 | val program = for { 792 | _ <- loop("A")(0).start(cs1) 793 | _ <- loop("B")(0).start(cs2) 794 | } yield ExitCode.Success 795 | ``` 796 | 797 | We get: 798 | ``` 799 | [pool-1-thread-1] A 800 | [pool-2-thread-1] B 801 | [pool-1-thread-1] A 802 | [pool-2-thread-1] B 803 | [pool-1-thread-1] A 804 | [pool-2-thread-1] B 805 | ... 806 | ``` 807 | 808 | This time each fiber has the opportunity to run, because each is running on its own thread (it's the operating system's job to make sure the CPU runs a little bit of each thread all the time). 809 | We would have observed the same behaviour if we had used a single pool with two threads, e.g. `Executors.newFixedThreadPool(2)` (try it out!). 810 | 811 | Now, pay attention to the shift that happens on the 10th iteration: 812 | 813 | ```scala 814 | ... 815 | _ <- if (i == 10) IO.shift(cs1) else IO.unit 816 | ... 817 | ``` 818 | 819 | At the 10th iteration of the loop, each `IO` will shift to thread pool number one. At that point, both fibers are going to get scheduled on the same thread (the only one in that pool), and there will be no subsequent `shifts`. 820 | So soon after initial "ABAB..." we will suddenly stop seeing "B"s: 821 | ``` 822 | ... 823 | [pool-2-thread-1] B 824 | [pool-1-thread-1] A 825 | [pool-2-thread-1] B 826 | [pool-1-thread-1] A 827 | [pool-1-thread-1] A 828 | [pool-1-thread-1] A 829 | ... 830 | ``` 831 | 832 | If we would keep shifting now (e.g. by saying `i > 10` instead of `i == 10`), we would keep getting "A"s and "B"s interchangeably like we have so far. 833 | But we only shifted once, both loops to the same `ContextShift` (that is, to the same single threaded thread pool), and we stopped shifting at that point. 834 | So both fibers ended up stuck on a single thread, and without further shifts, one of them will starve. 835 | 836 | ### IO vs Future 837 | 838 | What do you think happens if we swap `IO` for a `Future` in the cases we saw earlier? 839 | 840 | Let's start with the single threaded example: 841 | 842 | ```scala 843 | implicit val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 844 | 845 | def printThread(id: String) = Future { 846 | val thread = Thread.currentThread.getName 847 | println(s"${LocalDateTime.now} [$thread] $id") 848 | } 849 | 850 | def loop(id: String)(i: Int): Future[Unit] = 851 | for { 852 | _ <- printThread(id) 853 | _ <- Future(Thread.sleep(200)) 854 | result <- loop(id)(i + 1) 855 | } yield result 856 | 857 | val program = for { 858 | _ <- loop("A")(0) 859 | _ <- loop("B")(0) 860 | } yield ExitCode.Success 861 | 862 | Await.result(program, Duration.Inf) 863 | ``` 864 | As expected, this prints out `[pool-1-thread-1] A` indefinitely. 865 | 866 | But what happens if we now change to `Executors.newFixedThreadPool(2)`? 867 | In the case of fibers, "A" and "B" would be executed concurrently, and we would see them taking turns in being printed out. 868 | 869 | But with `Future`s, we get 870 | ``` 871 | [pool-1-thread-1] A 872 | [pool-1-thread-2] A 873 | [pool-1-thread-1] A 874 | [pool-1-thread-2] A 875 | [pool-1-thread-2] A 876 | [pool-1-thread-1] A 877 | ``` 878 | Note how threads are still taking turns, but they are both are executing "A". 879 | 880 | Why does this happen? 881 | 882 | On every `flatMap` call (`map` too), `Future` needs to have access to the `ExecutionContext`: 883 | ```scala 884 | def flatMap[S](f: T => Future[S])(implicit executor: ExecutionContext): Future[S] 885 | ``` 886 | In every step of the for-comprehension, `Future` will dispatch its computation back to our two-threaded pool (note: I heard that implementation of `Future` might change in this regard and that calls to `ExecutionContext` are going to be "batched" to improve performance, but I couldn't find any official source for this at the time of writing). 887 | 888 | This explains why we see alternating threads taking turns in computing "A". 889 | 890 | But why is there no "B"? Because there are no fibers. Remember, with `IO` we ran two separate fibers on the same `ContextShift` (that is, on the same thread pool) by using `.start`, and we shifted from one to another whenever we invoked `shift`. 891 | And because `IO` is lazy, `loop` didn't run endlessly over and over again inside the first step of the for-comprehension before even getting to the second one. 892 | Instead, we lazily defined two (endless) `IO` computations and we declared that we wanted to run them on separate fibers, either on the same thread pool or on separate ones (we had both situations throughout the examples). 893 | Then, once we executed the full program, we observed the behaviour of two fibers running on the thread pool(s), either in a cooperative way or in a selfish way, depending on whether we `shifted` or not. 894 | 895 | But with `Future`s, there is no concept of a fiber. 896 | This means that, instead of defining two separate fibers in our two-step for-comprehension, we simply defined a chain of two computations, both being infinitely recursive. 897 | So what happens is that loop "A" runs indefinitely, forever calling itself recursively, and our code never even gets the chance to run the loop "B". 898 | But on each recursive call of the "A" loop, underlying `ExecutionContext` delegates the computation to one of the two available threads, which is why we saw the threads alternating. 899 | 900 | Note that we would have observed the same behaviour using `IO` if we hadn't started the loops on separate fibers using `.start`. 901 | 902 | ### Leaking fibers 903 | 904 | In real world scenarios, you want to join started fibers when you're done with them (unless you cancel them). 905 | But there's a lurking danger if you're using multiple fibers: 906 | 907 | ```scala 908 | val f1 = for { 909 | f1 <- IO(Thread.sleep(1000)).start 910 | _ <- f1.join 911 | _ <- IO(println("Joined f1")) 912 | } yield () 913 | 914 | val f2 = for { 915 | f2 <- IO.raiseError[Unit](new Throwable("boom!")).start 916 | _ <- f2.join 917 | _ <- IO(println("Joined f2")) 918 | } yield () 919 | 920 | val program = (f1, f2).parMapN { 921 | case _ => ExitCode.Success 922 | } 923 | ``` 924 | 925 | In the above example, not only will we never see "Joined f2", but we will also never see "Joined f1". 926 | Fiber `f2` will explode and fiber `f1` will leak. 927 | 928 | Fibers should therefore always be used within a safe allocation mechanism, otherwise they might leak resources when cancelled. 929 | In the [Resource handling](#resource-handling) section, one such mechanism has been shown, using the `Resource` construct. 930 | 931 | Here is an example of using that mechanism to assure safety upon fiber cancelation: 932 | 933 | ```scala 934 | def safeStart[A](id: String)(io: IO[A]): Resource[IO, Fiber[IO, A]] = 935 | Resource.make(io.start)(fiber => fiber.cancel >> IO(println(s"Joined $id"))) 936 | 937 | val r1 = safeStart("1")(IO(Thread.sleep(1000))) 938 | val r2 = safeStart("2")(IO.raiseError[Unit](new Throwable("boom!"))) 939 | 940 | val program = (r1.use(_.join), r2.use(_.join)).parMapN { 941 | case _ => ExitCode.Success 942 | } 943 | ``` 944 | 945 | This time you will notice that both "Joined 1" and "Joined 2" got printed out, which means that both fibers got joined and didn't leak. 946 | 947 | ### Summary 948 | 949 | Type class `ContextShift` gives us the ability to execute an effect on some desired thread pool via `evalOn` by providing the `ExecutionContext`, or to `shift`, which re-schedules the fiber in the current thread pool and enables cooperative yielding. 950 | 951 | We get the same two abilities in `IO`, but in that case `shift` takes the thread pool as a parameter, either as `ExecutionContext` or (implicit) `ContextShift`. 952 | This means that we can cooperatively yield to another fiber within the same thread pool by passing the reference to the current one, and we can also shift to a different one. In other words, `IO.shift` provides both the type class `shift` functionality, and the type class `evalOn` functionality. 953 | 954 | In case of `Future`, there are no fibers. We pass an `ExecutionContext` to each map / flatMap call, which means that every `Future` computation might be executed on a different thread (this is up to the passed `ExecutionContext` and how it decides to schedule the work). 955 | What we cannot do with `Future`s, however, is define two concurrent computations that will reuse the same thread cooperatively. 956 | 957 | ## Cats-Effect 3 958 | 959 | At the time of writing this text, Cats-Effect 3 was still in the [proposal](https://github.com/typelevel/cats-effect/issues/634) phase. 960 | 961 | Here are some important changes that are happening (there are many more, but I'm focusing on those that directly affect the things explained in this text): 962 | 963 | 1. `ContextShift` is being removed. 964 | 965 | Even though Cats-Effect 3 still isn't out, the decision to remove `ContextShift` has already been made. 966 | But that doesn't mean that the principles explained in the previous couple of sections are becoming deprecated and irrelevant. 967 | 968 | First of all, `evalOn` will still exist; we need the ability to run a fiber on a given thread pool. 969 | It will simply take `ExecutionContext` as a parameter instead of `ContextShift`. 970 | However, it's now being constrained in a way that it will move all of the actions to the given thread pool, reverting back to the enclosing thread pool when finished (as opposed to Cats-Effect 2 which reverts back to the default pool). 971 | This is explained further in point 3. 972 | 973 | 2. Method `shift` is being removed. 974 | 975 | In Cats-Effect 2, method `shift` has two main roles: 976 | - shifting to a desired thread pool, which will be done by `Async#evalOn` described in the previous point 977 | - cooperative yielding, which will be done by `yield` / `yielding` / `cede` / `pass` / whatever name is eventually agreed upon, and which will be part of the `Concurrent` type class (this method will actually be a bit more general, but that's more of an implementation detail). 978 | 979 | Note that this means removing `shift` from three different places: 980 | - `ContextShift.shift` is being removed completely (see point 1) 981 | - `Async.shift(executionContext)` is being replaced by `Async[F[_]].evalOn(f, executionContext)` (note that the former is the companion object, while the latter is the type class; I omitted the "implicitly", you get the point) 982 | - `IO.shift(executionContext)` and `IO.shift(contextShift)` are being replaced by `Async[IO].evalOn(executionContext)` (although there might be an `IO.evalOn(executionContext)` for convenience) 983 | 984 | 3. `Async` type class will now hold a reference to the running `ExecutionContext`. 985 | This will enable fallback to the parent `ExecutionContext` once a fiber has terminated. 986 | Consider the following Cats-Effect 2 code: 987 | ```scala 988 | val ec1 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 989 | val ec2 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 990 | val ec3 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 991 | 992 | val cs: ContextShift[IO] = IO.contextShift(ec1) 993 | 994 | def io(s: String) = IO(println(s"$s: ${Thread.currentThread.getName}")) 995 | 996 | val r = cs 997 | .evalOn(ec2)(io("A").flatMap(_ => 998 | cs.evalOn(ec3)(io("B1")).flatMap(_ => io("B2")) 999 | )) 1000 | ``` 1001 | 1002 | There are three distinct `IO`s, which we can refer to as "A", "B1" and "B2". 1003 | Our intention is to run "A" on `ec2` and then chain it into a mini-chain "B1" -> "B2". 1004 | Thread pools are defined as follows: 1005 | - `ContextShift` is running on `ec1` 1006 | - "A" is running on `ec2` 1007 | - "B1" → "B2" is a flatmapped chain that follows after "A", and first part of that chain runs on `ec3` 1008 | 1009 | The million dollar question is - which thread pool does "B2" run on? 1010 | Answer: on `ec1`. This is very unintuitive. It would feel more natural if, after finishing "B1" on `ec2`, the follow-up "B2" would run on whatever "A" was running on. Instead, we fall back all the way to the default `ExecutionContext` that our `ContextShift` was initialised with. 1011 | 1012 | In Cats-Effect 3, this will be fixed. 1013 | 1014 | ## Fibers outside of Scala 1015 | 1016 | ### Project Loom 1017 | 1018 | Working with concurrent effects that has been described so far relies on the concept of fibers implemented by Scala libraries such as Cats-Effect, ZIO and Monix. 1019 | There is an initiative, however, to move the fibers from custom library Scala code to the virtual machine itself. 1020 | 1021 | [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html) is a proposal for adding fibers to the JVM. This way, fibers would become native-level constructs which would exist on the call stack instead of as objects on the heap. 1022 | 1023 | In Project Loom, fibers are called **virtual threads**. If you take a look at the basic [description of a virtual thread](https://www.baeldung.com/java-virtual-thread-vs-thread#virtual-thread-composition), you will see that: 1024 | 1025 | > "It is a continuation and a scheduler that, together, make up a virtual thread. " 1026 | 1027 | You might recall that we said the same thing in the [continuations](#continuations) section: 1028 | 1029 | > This is what a fiber really is under the hood - it's a *continuation* with a *scheduler*. 1030 | 1031 | Even though Loom's virtual threads are based on the same principles as the fiber mechanisms we explored in this article, there are still some implementation-specific details you would need to become familiar with. 1032 | At the time of writing this text, [latest update](http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.html) on Project Loom had some interesting information about that. 1033 | 1034 | 1035 | ### Green threads 1036 | 1037 | Fibers are an implementation of [green threads](https://en.wikipedia.org/wiki/Green_threads). 1038 | Green threads are present in many languages. 1039 | In some of them they are very similar to fibers, in some a bit different, but they all fit under the umbrella of "lightweight threads that are scheduled by a runtime library or a virtual machine, managed in the user space instead of in the kernel space, usually using cooperative instead of preemptive scheduling". 1040 | 1041 | Here are some examples: 1042 | 1043 | - Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) ([this](https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md) is a good doc) 1044 | - Go [Goroutines](https://tour.golang.org/concurrency/1) 1045 | - Haskell [green threads](https://books.google.de/books?id=rIVcDgAAQBAJ&pg=PA235&lpg=PA235&dq=ghc+green+threads&source=bl&ots=coVrvChTgf&sig=ACfU3U1vbMQzpNTxCb__KpdW_jmLGbB7AQ&hl=en&sa=X&ved=2ahUKEwjS_9_E7-joAhVbUhUIHSULDF0Q6AEwEHoECA0QLw#v=onepage&q=ghc%20green%20threads&f=false) (don't have a better link) 1046 | - Erlang [processes](https://www.tutorialspoint.com/erlang/erlang_processes.htm) 1047 | - Julia [Tasks](https://docs.julialang.org/en/v1/base/parallel/) 1048 | - Common Lisp via [green-threads](https://github.com/thezerobit/green-threads) library 1049 | - And many others 1050 | 1051 | ## References 1052 | 1053 | - Cats-effect documentation: https://typelevel.org/cats-effect/ 1054 | - Cats-effect repo: https://github.com/typelevel/cats-effect 1055 | - Cats-effect 3 proposal: https://github.com/typelevel/cats-effect/issues/634 1056 | - Monix Task documentation: https://monix.io/docs/current/eval/task.html 1057 | - Fabio Labella - How do Fibers work: https://www.youtube.com/watch?v=x5_MmZVLiSM 1058 | - Pawel Jurczenko - Modern JVM Multithreading: https://pjurczenko.github.io/modern-jvm-multithreading.html 1059 | - Bartłomiej Szwej - Composable resource management in Scala: https://medium.com/@bszwej/composable-resource-management-in-scala-ce902bda48b2 1060 | - Daniel Spiewak's gist: https://gist.github.com/djspiewak/46b543800958cf61af6efa8e072bfd5c 1061 | - Java Executors: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html 1062 | - Fork Join Pool vs Thread Pool Executor: http://www.h-online.com/developer/features/The-fork-join-framework-in-Java-7-1762357.html 1063 | - Adam Warski about Loom: https://blog.softwaremill.com/will-project-loom-obliterate-java-futures-fb1a28508232?gi=c5487dba95ec 1064 | - Loom update: http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.html 1065 | - Loom-fiber repo: https://github.com/forax/loom-fiber 1066 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "ConcurrentEffects" 2 | 3 | version := "1.0" 4 | 5 | scalaVersion := "2.12.5" 6 | 7 | libraryDependencies += "org.typelevel" %% "cats-core" % "2.1.0" 8 | libraryDependencies += "org.typelevel" %% "cats-effect" % "2.1.3" 9 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /src/main/scala/CancelFiber.scala: -------------------------------------------------------------------------------- 1 | import java.util.concurrent.Executors 2 | 3 | import cats.effect.{ContextShift, ExitCode, Fiber, IO, IOApp, Resource} 4 | import cats.implicits._ 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | object CancelFiberWithLeak extends IOApp { 9 | 10 | val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) 11 | implicit val cs: ContextShift[IO] = IO.contextShift(ec) 12 | 13 | val f1 = for { 14 | f1 <- IO(Thread.sleep(1000)).start 15 | _ <- f1.join 16 | _ <- IO(println("Joined f1")) 17 | } yield () 18 | 19 | val f2 = for { 20 | f2 <- IO.raiseError[Unit](new Throwable("boom!")).start 21 | _ <- f2.join 22 | _ <- IO(println("Joined f2")) 23 | } yield () 24 | 25 | val program = (f1, f2).parMapN { 26 | case _ => ExitCode.Success 27 | } 28 | 29 | override def run(args: List[String] = List()): IO[ExitCode] = program 30 | } 31 | 32 | object CancelFiberSafely extends IOApp { 33 | 34 | val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) 35 | implicit val cs: ContextShift[IO] = IO.contextShift(ec) 36 | 37 | def safeStart[A](id: String)(io: IO[A]): Resource[IO, Fiber[IO, A]] = 38 | Resource.make(io.start)(fiber => fiber.cancel >> IO(println(s"Joined $id"))) 39 | 40 | val r1 = safeStart("1")(IO(Thread.sleep(1000))) 41 | val r2 = safeStart("2")(IO.raiseError[Unit](new Throwable("boom!"))) 42 | 43 | val program = (r1.use(_.join), r2.use(_.join)).parMapN { 44 | case _ => ExitCode.Success 45 | } 46 | 47 | override def run(args: List[String] = List()): IO[ExitCode] = program 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/FFI.scala: -------------------------------------------------------------------------------- 1 | import FFI._ 2 | import cats.effect.{Async, Concurrent, ExitCode, IO, IOApp} 3 | import cats.implicits._ 4 | 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Future 7 | import scala.util.{Failure, Success} 8 | 9 | object FFI { 10 | 11 | case class User(id: String) 12 | 13 | // this is the asynchronous process that we want to translate 14 | // from the "foreign" world into our IO-based code 15 | def fetchUser(userId: String): Future[User] = Future(User(userId)) 16 | } 17 | 18 | object AsyncExample extends IOApp { 19 | 20 | def fromFuture[A](future: => Future[A]): IO[A] = 21 | IO.async { cb => 22 | future.onComplete { 23 | case Success(a) => cb(Right(a)) 24 | case Failure(e) => cb(Left(e)) 25 | } 26 | } 27 | 28 | override def run(args: List[String] = List()): IO[ExitCode] = 29 | for { 30 | user <- fromFuture(fetchUser("User_42")) 31 | _ <- IO(println(s"User ID = ${user.id}")) 32 | } yield ExitCode.Success 33 | } 34 | 35 | object CancelableExample extends IOApp { 36 | 37 | def fromFutureAsync[A](future: => Future[A]): IO[A] = 38 | IO.async { cb => 39 | future.onComplete { 40 | case _ => // don't use the callback! 41 | } 42 | // this will never happen bcs we used `async` instead of `cancelable`! 43 | IO(println("Rollback the transaction!")) 44 | } 45 | 46 | def fromFutureCancelable[A](future: => Future[A]): IO[A] = 47 | IO.cancelable { cb => 48 | future.onComplete { 49 | case _ => // don't use the callback! 50 | } 51 | IO(println("Rollback the transaction!")) 52 | } 53 | 54 | // also try out fromFutureAsync! 55 | override def run(args: List[String] = List()): IO[ExitCode] = 56 | for { 57 | user1 <- fromFutureCancelable(fetchUser("User_42")) 58 | _ <- IO(println(s"User ID = ${user1.id}")) 59 | } yield ExitCode.Success 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/PrintThread.scala: -------------------------------------------------------------------------------- 1 | import java.time.LocalDateTime 2 | 3 | trait PrintThread { 4 | 5 | def printThread(id: String) = { 6 | val thread = Thread.currentThread.getName 7 | println(s"[$thread] $id") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/Resources.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.{ExitCode, IO, IOApp, Resource} 2 | 3 | object Resources extends IOApp with PrintThread { 4 | 5 | def mkResource(s: String): Resource[IO, String] = { 6 | val acquire = IO(println(s"Acquiring $s")) *> IO.pure(s) 7 | def release(s: String) = IO(println(s"Releasing $s")) 8 | Resource.make(acquire)(release) 9 | } 10 | 11 | val r = for { 12 | outer <- mkResource("outer") 13 | inner <- mkResource("inner") 14 | } yield (outer, inner) 15 | 16 | override def run(args: List[String]): IO[ExitCode] = 17 | r.use { case (a, b) => IO(println(s"Using $a and $b")) }.map(_ => ExitCode.Success) 18 | 19 | } 20 | 21 | object ResourcesWithErrors extends IOApp with PrintThread { 22 | 23 | def mkResource(s: String): Resource[IO, String] = { 24 | val acquire = IO(println(s"Acquiring $s")) *> IO.pure(s) 25 | def release(s: String) = IO(println(s"Releasing $s")) 26 | Resource.make(acquire)(release) 27 | } 28 | 29 | val r = for { 30 | outer <- mkResource("outer") 31 | inner <- mkResource("inner") 32 | _ <- Resource.liftF(IO.raiseError(new Throwable("Boom!"))) 33 | } yield (outer, inner) 34 | 35 | override def run(args: List[String]): IO[ExitCode] = 36 | r.use { case (a, b) => IO(println(s"Using $a and $b")) }.map(_ => ExitCode.Success) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/RunLoop.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.{ExitCode, IO} 2 | 3 | import scala.io.StdIn.readLine 4 | 5 | object RunLoop { 6 | 7 | sealed trait IO[+A] 8 | case class FlatMap[B, +A](io: IO[B], k: B => IO[A]) extends IO[A] 9 | case class Pure[+A](v: A) extends IO[A] 10 | case class Delay[+A](eff: () => A) extends IO[A] 11 | 12 | val program = for { 13 | _ <- IO(println(s"What's up?")) 14 | input <- IO(readLine) 15 | _ <- IO(println(s"Ah, $input is up!")) 16 | } yield ExitCode.Success 17 | 18 | val stack: FlatMap[String, Unit] = FlatMap( 19 | FlatMap( 20 | Delay(() => print("What's up?")), 21 | (_: Unit) => Delay(() => readLine) 22 | ), 23 | input => Delay(() => println(s"Ah, $input is up!")) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/RunWithFiber.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.{ExitCode, IO, IOApp} 2 | 3 | object RunWithFiber extends IOApp { 4 | 5 | def io(i: Int): IO[Unit] = IO({ 6 | Thread.sleep(3000) 7 | println(s"Hi from $i!") 8 | }) 9 | 10 | val program = for { 11 | startTime <- IO(System.currentTimeMillis()) 12 | fiber <- io(1).start 13 | _ <- io(2) 14 | _ <- fiber.join 15 | endTime <- IO(System.currentTimeMillis()) 16 | _ <- IO(println(s"Elapsed: ${endTime - startTime} ms")) 17 | } yield ExitCode.Success 18 | 19 | override def run(args: List[String] = List()): IO[ExitCode] = program 20 | 21 | // Hi from 1! 22 | // Hi from 2! 23 | // Elapsed: 3371 ms 24 | } -------------------------------------------------------------------------------- /src/main/scala/RunWithoutFiber.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.{ExitCode, IO, IOApp} 2 | 3 | object RunWithoutFiber extends IOApp { 4 | 5 | def io(i: Int): IO[Unit] = IO({ 6 | Thread.sleep(3000) 7 | println(s"Hi from $i!") 8 | }) 9 | 10 | val program = for { 11 | startTime <- IO(System.currentTimeMillis()) 12 | _ <- io(1) 13 | _ <- io(2) 14 | endTime <- IO(System.currentTimeMillis()) 15 | _ <- IO(println(s"Elapsed: ${endTime - startTime} ms")) 16 | } yield ExitCode.Success 17 | 18 | override def run(args: List[String]): IO[ExitCode] = program 19 | 20 | // Hi from 1! 21 | // Hi from 2! 22 | // Elapsed: 6077 ms 23 | } -------------------------------------------------------------------------------- /src/main/scala/ShiftOnFuture.scala: -------------------------------------------------------------------------------- 1 | import java.util.concurrent.Executors 2 | 3 | import cats.effect.ExitCode 4 | 5 | import scala.concurrent.duration.Duration 6 | import scala.concurrent.{Await, ExecutionContext, Future} 7 | 8 | object ShiftOnFuture extends App with PrintThread { 9 | 10 | implicit val ec = 11 | ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) 12 | 13 | def loop(id: String)(i: Int): Future[Unit] = 14 | for { 15 | _ <- Future(printThread(id)) 16 | _ <- Future(Thread.sleep(200)) 17 | result <- loop(id)(i + 1) 18 | } yield result 19 | 20 | val program = for { 21 | _ <- loop("A")(0) 22 | _ <- loop("B")(0) 23 | } yield ExitCode.Success 24 | 25 | Await.result(program, Duration.Inf) 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/ShiftOnOnePool.scala: -------------------------------------------------------------------------------- 1 | import cats.effect.{ContextShift, ExitCode, IO, IOApp} 2 | 3 | import java.util.concurrent.Executors 4 | import scala.concurrent.ExecutionContext 5 | 6 | object ShiftOnOneThread extends IOApp with PrintThread { 7 | 8 | val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 9 | val cs: ContextShift[IO] = IO.contextShift(ec) 10 | 11 | def loop(id: String)(i: Int): IO[Unit] = 12 | for { 13 | _ <- IO(printThread(id)) 14 | _ <- IO.shift(cs) // <--- now we shift! 15 | _ <- IO(Thread.sleep(200)) 16 | result <- loop(id)(i + 1) 17 | } yield result 18 | 19 | val program = for { 20 | _ <- loop("A")(0).start(cs) 21 | _ <- loop("B")(0).start(cs) 22 | } yield ExitCode.Success 23 | 24 | override def run(args: List[String]): IO[ExitCode] = program 25 | } 26 | 27 | object ShiftOnTwoThreads extends IOApp with PrintThread { 28 | 29 | val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) 30 | val cs: ContextShift[IO] = IO.contextShift(ec) 31 | 32 | def loop(id: String)(i: Int): IO[Unit] = 33 | for { 34 | _ <- IO(printThread(id)) 35 | _ <- IO(Thread.sleep(200)) 36 | result <- loop(id)(i + 1) 37 | } yield result 38 | 39 | val program = for { 40 | _ <- loop("A")(0).start(cs) 41 | _ <- loop("B")(0).start(cs) 42 | } yield ExitCode.Success 43 | 44 | override def run(args: List[String]): IO[ExitCode] = program 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/ShiftOnTwoPools.scala: -------------------------------------------------------------------------------- 1 | import java.util.concurrent.Executors 2 | 3 | import cats.effect.{ContextShift, ExitCode, IO, IOApp} 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | object ShiftOnTwoPools extends IOApp with PrintThread { 8 | 9 | val ec1 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 10 | val ec2 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 11 | 12 | val cs1: ContextShift[IO] = IO.contextShift(ec1) 13 | val cs2: ContextShift[IO] = IO.contextShift(ec2) 14 | 15 | def loop(id: String)(i: Int): IO[Unit] = 16 | for { 17 | _ <- IO(printThread(id)) 18 | _ <- if (i == 10) IO.shift(cs1) else IO.unit 19 | _ <- IO(Thread.sleep(200)) 20 | result <- loop(id)(i + 1) 21 | } yield result 22 | 23 | val program = for { 24 | _ <- loop("A")(0).start(cs1) 25 | _ <- loop("B")(0).start(cs2) 26 | } yield ExitCode.Success 27 | 28 | override def run(args: List[String]): IO[ExitCode] = program 29 | } 30 | 31 | object ShiftOnTwoPoolsWithFallback extends IOApp with PrintThread { 32 | 33 | val ec1 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 34 | val ec2 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 35 | val ec3 = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) 36 | 37 | val cs: ContextShift[IO] = IO.contextShift(ec1) 38 | 39 | def io(s: String) = IO(println(s"$s: ${Thread.currentThread.getName}")) 40 | 41 | val r = cs 42 | .evalOn(ec2)(io("A").flatMap(_ => 43 | cs.evalOn(ec3)(io("B1")).flatMap(_ => io("B2")) 44 | )) 45 | 46 | override def run(args: List[String]): IO[ExitCode] = 47 | r.map(_ => ExitCode.Success) 48 | } 49 | -------------------------------------------------------------------------------- /sync-async-boundary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slouc/concurrency-in-scala-with-ce/ed30fd2bef9b38489fba6f36d646623f563ac384/sync-async-boundary.png --------------------------------------------------------------------------------