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:
The whole app will freeze after a few iterations. The counter will not increase anymore and the button will not accept clicks.
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.
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:
- The timer fires and calls out to its callback function (1)
- 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.
- Eventually the completion handler is called and it will call the JS callback (2) here.
- The timer fires again and it will be added to the dispatch queue to be processed.
- 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.
- 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!