5

As far as I know, crystal cycles Fibers with io, meaning that if one fiber is waiting for io, crystal will switch to an another fiber.

What if we spawn two fibers but one of them does constant computation/loop with no io?

For example, with the code below server doesn't respond to any http requests

spawn do
  Kemal.run
end

spawn do
  # constant computation/loop with no IO
  some_func
end

Fiber.yield
# or sleep
Oguz Bilgic
  • 3,392
  • 5
  • 36
  • 59
  • 3
    That's correct. You can call Fiber.yield to move to a next available fiber. – asterite Dec 10 '18 at 21:11
  • Already doing that in the main thread/fiber, the issue I have is the first fiber (web server) doesn't get time to handle any requests because the second fiber doesn't have any io and runs constantly @asterite – Oguz Bilgic Dec 10 '18 at 21:15
  • 2
    What I'm saying is to put `Fiber.yield` in the loop that's inside `some_func` (and using `sleep` as the last line of main) – asterite Dec 10 '18 at 22:58
  • @asterite so I have to manually manage (pause with Fiber.yield) the non-io fiber so that the first fiber gets cpu time – Oguz Bilgic Dec 11 '18 at 01:14
  • 3
    Yes, at least right now. Eventually when we have parallelism it could happen that both fibers will run in parallel. – asterite Dec 11 '18 at 09:36
  • Thank you @asterite, If you want to answer the question I can pick yours – Oguz Bilgic Dec 11 '18 at 20:03
  • It's fine, they are just bits on a server :-) – asterite Dec 12 '18 at 00:21

1 Answers1

1

By default, Crystal uses cooperative multitasking. To implement it, the Crystal runtime provides Fibers. Due to their cooperative nature, you have to yield execution from time to time (e.g. with Fibers.yield):

Fibers are cooperative. That means execution can only be drawn from a fiber when it offers it. It can't be interrupted in its execution at random. In order to make concurrency work, fibers must make sure to occasionally provide hooks for the scheduler to swap in other fibers. [...]

When a computation-intensive task has none or only rare IO operations, a fiber should explicitly offer to yield execution from time to time using Fiber.yield to break up tight loops. The frequency of this call depends on the application and concurrency model.

Note that CPU intense operations are not the only source for starving other Fibers. When calling C libraries that may block, the Fiber will also wait the operation to complete. An example would be a long-polling operation, which will wait for the next event or eventually time out (e.g. rd_kafka_poll in Kafka). To prevent that, prefer async API version (if available), or use a short polling interval (e.g. 0 for Kafka poll) and shift the sleep operation to the Crystal runtime, so the other Fibers can run.

In 2019, Crystal introduced support for parallelism. By running multiple worker threads, you can also prevent one expensive computation for starving all other operations. However, you have to be careful as the responsiveness (and maybe even correctness) of the program could then depend on the number of workers (e.g. with only one worker, it will still hang). Overall, yielding occasionally in time-extensive operations seems to be the better solution, even if you end up using multiple workers for the improved performance on multi-core machines.

Philipp Claßen
  • 41,306
  • 31
  • 146
  • 239