2

I'm using Crystal bindings for X11 and typical usage looks something like this:

loop do
    event = display.next_event # <- blocking!
    # do stuff with event
end

This works fine but I can't do anything in parallel now because next_event is native C code and thus never yields.

xlib's NextEvent is always blocking, also in C. There's a thread about it with possible alternatives, but polling is slower and more CPU demanding than next_event, so I'd like to stick to that blocking call. Do I have to put this into its separate OS thread? -Dpreview_mt actually solves this, but that's an experimental flag and makes all concurrency threaded which is unnecessary (edit: Just found out, this is not true, there is spawn same_thread: true do syntax). What options do I have?

Edit: I just found this is discussed in more detail in this thread which is worth reading on the topic. There's also FileDescriptor's wait_readable which can resolve this if you can obtain a FD somehow.

phil294
  • 10,038
  • 8
  • 65
  • 98

1 Answers1

4

When a lib function blocks, you can't do anything about it in Crystal. You will need to run it in a separate OS thread in order to be able to run other code in parallel. That's what such methods are designed to be used for.

In Crystal, you don't need preview_mt to just run a separate OS thread. This flag enables multithreaded scheduling for the Crystal runtime. For your use case, you actually just need a dedicated thread to wait for a blocking call to finish. The thread API is not publicly exposed, but you can use Thread.new { } to create a new OS thread and run the proc in that thread.

I think we'll need to add a public API for such use cases to the language. But currently, it's not documented (so it might be subject to change, but that's unlikely).

Johannes Müller
  • 5,581
  • 1
  • 11
  • 25
  • Thanks a lot, makes sense! In my case, I also want to communicate with the thread (Channels), so I guess it does have to be `preview_mt` anyway. – phil294 Jul 20 '22 at 21:25
  • Well you can't really do that when the thread is blocking. That's the whole point. Channels are useful for sending messages within non-blocking concurrency. In other words: You don't need channels to communicate with your blocking thread. – Johannes Müller Jul 21 '22 at 09:12
  • sorry, I was being a bit indistinct here. I meant to say: As far as I understand now, for `display.next_event` Channels are useless, but on top of that I'm [also employing a third GUI thread](https://github.com/phil294/AHK_X11/blob/e90e4cd30cf23893405a643a2a0391596e4f4a23/src/run/gui.cr) which is blocking only *99% of the time* (`LibGtk.main`), and when it's not (requested with `Glib.idle_add`), I want to communicate with it (`msgbox`), so I think this is the only way. At least it now works as expected - with `Thread.new` alone the Channel did not work. – phil294 Jul 21 '22 at 10:36
  • Note there is also an issue about this on GitHub. My comment there https://github.com/crystal-lang/crystal/issues/11778#issuecomment-1193197288 has a code example for how to run a blocking call in a separate thread. It serves as an adapter into Crystal's scheduler. Works without `preview_mt`. – Johannes Müller Jul 23 '22 at 22:30
  • oh, cool stuff! I tried building it like that and it works great so far, this can indeed fix/replace cross-thread channels without preview_mt. However, the workaround eats a lot of cpu when the task takes a long time, because `Fiber.yield` fires over and over again, specially when there are multiple instances of `run_blocking` running. I added a small sleep delay into the loop and it's okay now. Thanks. – phil294 Aug 15 '22 at 22:23