Uploaded image for project: 'Titanium SDK/CLI'
  1. Titanium SDK/CLI
  2. TIMOB-26673

iOS: Race conditions in async APIs (e.g. timers)


    • Type: Bug
    • Status: Closed
    • Priority: High
    • Resolution: Fixed
    • Affects Version/s: Release 8.0.0
    • Fix Version/s: Release 8.0.0
    • Component/s: iOS
    • Labels:


      Async functions can trigger a race conditions which leads to an internal dead lock inside JavaScriptCore. This happens because we often reschedule block execution using TiThreadPerformOnMainThread.

      Note that these race conditions only happen if a lot of async stuff happens at the same time. This example uses a timer with a 5ms interval to force the deadlock to happen. It is very unlikely to happen under normal conditions.

      Steps to reproduce the behavior
      Run the following code in a classic app on an iOS device:

      let counter = 0;
      const win = Ti.UI.createWindow({ layout: 'vertical' });
      const statusLabel = Ti.UI.createLabel({ top: 100, text: 'Running ...' });
      const counterLabel = Ti.UI.createLabel({ top: 5 });
      const testButton = Ti.UI.createButton({ top: 6, title: 'Click me!' });
      testButton.addEventListener('click', () => Ti.API.info('expected'));
      function triggerRaceCondition() {
        counterLabel.text = counter++;
        console.log(`[${counter}] requestUserNotificationSettings`);
        Ti.App.iOS.UserNotificationCenter.requestUserNotificationSettings(e => {
          console.log(`[${counter}] requestUserNotificationSettings callback`);
      const triggerInterval = setInterval(() => triggerRaceCondition(), 5);
      setTimeout(() => {
        statusLabel.text = 'Finished!';
      }, 1000);

      Actual behavior
      The whole app will freeze after a few iterations. The counter will not increase anymore and the button will not accept clicks.

      Expected behavior
      The counter should increase for about one second and then stop. The status should switch from "Running" to "Finished". The button should accept click events and print "expected" to the console.

      Additional information
      This is caused by TiThreadPerformOnMainThread which reschedules a block on the main queue when not running on main thread.

      In this particular test case various things happen which ultimately lead to the deadlock inside JSCore:

      1. The timer fires and calls out to its callback function (1)
      2. Now, this function contains an async function, namely Ti.App.iOS.UserNotificationCenter.requestUserNotificationSettings. Under the hood this will call getNotificationSettingsWithCompletionHandler: of UNUserNotificationCenter. The completion handler for this method will be called on a separate thread. This is important for the race condition to happen.
      3. Eventually the completion handler is called and it will call the JS callback (2) here.
      4. The timer fires again and it will be added to the dispatch queue to be processed.
      5. Inside the callback (2) we make use of console.log. Since the callback was called from a different thread, KrollMethod will reschedule the console.log call to the main thread here. This will schedule the method call behind the previously scheduled timer.
      6. The timer callback (1) is now called which also want's to use console.log. However, the pending call from callback (2) still has a lock on that. Boom, deadlock!




            • Assignee:
              jvennemann Jan Vennemann
              jvennemann Jan Vennemann
              Vijay Singh
              Keerthi Mahalingam (Inactive)
            • Watchers:
              5 Start watching this issue


              • Created:

                Backbone Issue Sync

                • Titanium SDK/CLI <> Titanium Mobile
                  Synced with:
                  Sync status:
                  Last received:
                  Last sent:

                  Git Integration