Electron Node addon with AsyncWorker hang on `dispatch_group_wait` on macOS

Viewed 120

I am creating a Node addon to export a video file from macOS Photos library, as it takes a few seconds, I wrapped the code into an AsyncWorker.

The C++ / Objective-C code:

class Napi_PhotosExport_AsyncWorker : public Napi::AsyncWorker
{

  void Execute()
  {
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);

    [[PHImageManager defaultManager] requestExportSessionForVideo:... {

      // this should be called, but never be called.
      dispatch_group_leave(group);
    }];

    // this blocks the current thread and wait for the above callback.
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
  }

};

The code above works in my macOS Cocoa application, but it doesn't work in Electron / Node environment for some reason. I expected the dispatch_group_leave can be called at some point, so that dispatch_group_wait returns properly instead of blocking the thread.

I am looking for some help from both macOS and Electron developers. Maybe Grand Central Dispatcher should not be used in Node addon, and I need to use C++ lock instead?

1 Answers

The reason we always advise against this pattern is that:

  • if the closure is dispatched back to the same thread that is currently waiting, that can deadlock;

  • if you have thread explosion (either here or buried elsewhere in your project), this pattern can also deadlock; and

  • it is simply an inefficient anti-pattern that unnecessarily ties up threads, even if you do not introduce deadlocks.

As a rule, you simply never should be making asynchronous methods tie up threads to make them behave synchronously. Use asynchronous patterns; do not fight them.


BTW, the problem is almost certainly unrelated to the fact that you are using a dispatch group. Semaphores or locks are going to result in the same problem. The problem isn't how you're blocking the thread, but the fact that you are blocking it at all.


If you are doing this so that you can manage dependencies between asynchronous tasks, traditional solutions range from asynchronous NSOperation custom subclass, to third-party promises/futures library. In Swift, we would throw Combine and the async-await concurrency patterns into the discussion, too, but I'm assuming neither of those is an option for you. Needless to say, you could adopt traditional, less elegant patterns to solve this problem, such as recursion (initiating the next task in the completion of the prior one) or nested completion handlers (which, admittedly, can become unwieldy).

Related