Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Multithreading. Survival guide

Multithreading. Survival guide

Avatar for miserya

miserya

July 14, 2021
Tweet

More Decks by miserya

Other Decks in Programming

Transcript

  1. Why do we need multithreading? • Avoid UI freezing during

    complex task performing (saving, downloading, various scans, calculations, etc.). • Speedup app performance (for example, parallelising of tasks).
  2. • Process is the runtime instance of an application or

    program. A process has its own virtual memory space and system resources. • Thread is a f low of execution in a process. • Task is the abstract concept of work that needs to be performed. User Login task iOS App Process … main thread thread 1 thread N UI drawing Sending request to a backend
  3. • Process is the runtime instance of an application or

    program. A process has its own virtual memory space and system resources. • Thread is a f low of execution in a process. • Task is the abstract concept of work that needs to be performed. macOS Main App Process … main thread thread 1 thread N User Login task UI drawing Sending request to a backend Background Service Process Widget
  4. Queue • Serial queue - а guarantee that only one

    task runs at any given time. • Concurrent queue - allow multiple tasks to run at the same time. The queue guarantees that the tasks start in the order you add them. Serial Queue Task 1 Task 2 Task 3 Task 4 Concurrent Queue Task 1 Task 2 Task 3 Task 4
  5. GCD Queues types • Main queue - runs on the

    main thread and is a serial queue. • Global queues - concurrent queues that are shared by the whole system. There are four such queues with di ff erent priorities : high, default, low, and background. • Custom queues - queues that you create which can be serial or concurrent. Main Queue Drawing UI Drawing More Drawing Global and Custom Queues User-interactive User-initiated Utility Background With same QoS ☝
  6. Synchronous vs Asynchronous • A synchronous function returns control to

    the caller after the task completes. 
 
 DispatchQueue.sync(execute:) • An asynchronous function returns immediately, ordering the task to start but not waiting for it to complete. 
 
 DispatchQueue.async(execute:) a way to dispatch a task Serial Queue Sync task Async task Async task Sync task Async task Async task Concurrent Queue Async task
  7. Synchronous vs Asynchronous a way to dispatch a task let

    dispatchQueue = DispatchQueue(label: "MyCustomQueue", attributes: [ .concurrent ]) dispatchQueue.async { for i in 0..<7 { print("-> Async") } } dispatchQueue.sync { for i in 0..<10 { print("=> Sync") } } dispatchQueue.sync { for i in 0..<10 { print("==> Sync") } } dispatchQueue.async { for i in 0..<10 { print("--> Async") } } -> Async => Sync -> Async => Sync -> Async => Sync -> Async => Sync -> Async => Sync -> Async => Sync -> Async => Sync => Sync => Sync => Sync ==> Sync ==> Sync ==> Sync ==> Sync ==> Sync ==> Sync ==> Sync ==> Sync ==> Sync ==> Sync --> Async --> Async --> Async --> Async --> Async --> Async --> Async --> Async --> Async --> Async => Sync Concurrent Queue -> Async ==> Sync —-> Async
  8. Task • Download image and set it in UIImageView in

    a UITableViewCell. To implement 😸 Title Subtitle Title Subtitle
  9. Task • Download image and set it in UIImageView in

    a UITableViewCell. To implement 😸 Title Subtitle Title Subtitle Global (Concurrent) Queue Main (Serial) Queue Set image URL Download Image Data Set image
  10. • DispatchGroup - a group of tasks that you monitor

    as a single unit. • DispatchSemaphore - an object that controls access to a resource across multiple execution contexts through use of a traditional counting semaphore. Concurrent Queue, let group = DispatchGroup() group.notify() Async task group.enter() group.leave() Async task group.enter() group.leave() Sync task group.enter() group.leave() Concurrent Queue, let semaphore = DispatchSemaphore(value: 2) semaphore.wait() Async task Async task Async task Async task semaphore.wait() semaphore.wait() semaphore.wait() semaphore.signal() semaphore.signal() semaphore.signal() semaphore.signal()
  11. Task • You need to fetch a User Info that

    consists of 3 parts: general, family, and work infos. This is 3 di ff erent API calls. • Only when all information is received - display it on the screen. To implement 🧑🦱 Name Age Family: - 👱 Mom - 👨 Dad - 👶 Brother - 🐆 Cat Work: MacPaw
  12. Task To implement Concurrent Queue, let group = DispatchGroup() group.notify()

    Get General Info group.enter() group.leave() Get Family Info group.enter() group.leave() Get Work Info group.enter() group.leave() • You need to fetch a User Info that consists of 3 parts: general, family, and work infos. This is 3 di ff erent API calls. • Only when all information is received - display it on the screen.
  13. Task • There is a Song downloader app. The user

    is allowed to download 3 songs at once. To implement 🎵 Song 1 duration 🎵 Song 2 duration 🎵 Song 3 duration Song 4 duration Song 5 duration …
  14. Task To implement Concurrent Queue, let semaphore = DispatchSemaphore(value: 3)

    semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() semaphore.wait() Download semaphore.signal() • There is a Song downloader app. The user is allowed to download 3 songs at once.
  15. • Operation - is an abstract class, designed for subclassing.

    Each subclass represents a speci f ic task; • can be paused, resumed, and cancelled; • can depend on other Operations; • is a key-value coding (KVC) and key-value observing (KVO) compliant; • synchronous by default;
  16. • Operation - is an abstract class, designed for subclassing.

    Each subclass represents a speci f ic task; • can be paused, resumed, and cancelled; • can depend on other Operations; • is a key-value coding (KVC) and key-value observing (KVO) compliant; • synchronous by default; • For non-concurrent operations, you typically override only one method: main() • If you are creating a concurrent operation, you need to override the following methods and properties at a minimum: start() isAsynchronous isExecuting isFinished 

  17. • OperationQueue - a queue that regulates the execution of

    operations; • is key-value coding (KVC) and key-value observing (KVO); • allows you to specify the maximum number of queued operations that can run simultaneously; var queue = OperationQueue() queue.maxConcurrentOperationCount = 2 Async Operation Async Operation Sync Operation Async Operation
  18. • BlockOperation - an operation that manages the concurrent execution

    of one or more blocks. let blockOperation = BlockOperation { var result = 0 for i in 1...1000000000 { result += i } }
  19. Tasks To implement 🧑🦳 Friend 1 Age 🧑🦱 Friend 2

    Age 👩🦰 Friend 3 Age 👨 Friend 4 Age 👳 Friend 5 Age … • Get a list of your friends (name, age, photo), display it in the table. • Each photo of your friend should be modi f ied with Instagram f ilter. • Display progress bar during all processes.
  20. Tasks To implement OperationQueue.main Show UITableView Download OperationQueue Download Friends

    Data Filtering OperationQueue Filter Image Report Progress For visible cells Download Image Report Progress Report Progress • Get a list of your friends (name, age, photo), display it in the table. • Each photo of your friend should be modi f ied with Instagram f ilter. • Display progress bar during all processes.
  21. Tasks To implement OperationQueue.main Show UITableView Download OperationQueue Filtering OperationQueue

    Filter Image For visible cells Download Image Download Friends Data Report Progress Report Progress Download Image Report Progress Report Progress Filter Image Report Progress Report Progress • Get a list of your friends (name, age, photo), display it in the table. • Each photo of your friend should be modi f ied with Instagram f ilter. • Display progress bar during all processes.
  22. • Deadlock - a situation where a thread locks a

    critical portion of the code and can halt the application's run loop entirely. • In the context of GCD, you should be very careful when using the dispatchQueue.sync { } calls as you could easily get yourself in situations where two synchronous operations can get stuck waiting for each other. let serialQueue = DispatchQueue(label: "MySerialQueue") serialQueue.async { serialQueue.sync { // <-- deadlock for i in 0..<5 { print(i) } } } Serial Queue Sync Task Async Task waits waits Important Attempting to synchronously execute a work item on the main queue results in deadlock.
  23. • Priority Inversion - a condition where a lower priority

    task blocks a high priority task from executing, which e ff ectively inverts their priorities. • GCD allows for di ff erent levels of priority on its background queues, so this is quite easily a possibility. enum Color: String { case blue = "🔵 " case white = "⚪ " } func output(color: Color, times: Int) { for _ in 1...times { print(color.rawValue) } } let starterQueue = DispatchQueue(label: "com.starter", qos: .userInteractive) let utilityQueue = DispatchQueue(label: "com.utility", qos: .utility) let backgroundQueue = DispatchQueue(label: "com.background", qos: .background) let count = 10 starterQueue.async { backgroundQueue.async { output(color: .white, times: count) } backgroundQueue.async { output(color: .white, times: count) } utilityQueue.async { output(color: .blue, times: count) } utilityQueue.async { output(color: .blue, times: count) } // priority inverted here backgroundQueue.sync {} }
  24. • Priority Inversion - a condition where a lower priority

    task blocks a high priority task from executing, which e ff ectively inverts their priorities. • GCD allows for di ff erent levels of priority on its background queues, so this is quite easily a possibility. Concurrent .userInteractive queue Concurrent .utility queue Concurrent .background queue Async Operation Sync Operation Async Operation Async Operation Async Operation GCD resolves this inversion by raising the QoS of the entire queue to temporarily match the high QoS task; consequently, all the tasks on the .background queue end up running at .userInteractive QoS, which is higher than the utility QoS. And that’s why the utility tasks f inish last!
  25. • Race condition - a producer- consumer problem where one

    thread is creating a data resource while another thread is accessing it. • This is a synchronization problem, and can be solved using locks, semaphores, serial queues, or a barrier dispatch if you're using concurrent queues in GCD. let concurrent = DispatchQueue(label: "com.concurrent", attributes: [ .concurrent ]) var array = [1, 2, 3, 4, 5] func race() { concurrent.async { for i in array { // read access print(i) } } concurrent.async { for i in 0..<10 { array.append(i) // write access } } } for _ in 0...100 { race() } 4 1 6 1 1 4 1 Swift/ContiguousArrayBuffer.swift:580: Fatal error: Index out of range 3 0 3
  26. • Thread explosion - when you use a concurrent queue,

    you run the risk of thread explosion if you’re not careful. • This can happen when you try to submit tasks to a concurrent queue that is currently blocked (e.g. with a semaphore, sync, or some other way.) • Your tasks will run, but the system will likely end up spinning up new threads to accommodate these new tasks, and threads aren’t cheap. More related to old devices
  27. How to avoid this problems? Read - async Write -

    sync NSLock, NSRecursiveLock .barrier @synchronize atomic properties
  28. Task • Let’s f ix the code To implement let

    concurrent = DispatchQueue(label: "com.concurrent", attributes: [.concurrent]) var array = [1,2,3,4,5] func race() { concurrent.async { for i in array { // read access print(i) } } concurrent.async { for i in 0..<10 { array.append(i) // write access } } } for _ in 0...100 { race() }
  29. Task • Write - sync, Read-async To implement let concurrent

    = DispatchQueue(label: "com.concurrent", attributes: [.concurrent]) var array = [1, 2, 3, 4, 5] func race() { concurrent.sync { for i in array { // read access print(i) } } concurrent.async(flags: .barrier) { for i in 0..<10 { array.append(i) // write access } } } for _ in 0...100 { race() } Write Read Write Write waits waits
  30. Task • NSLock To implement let concurrent = DispatchQueue(label: "com.concurrent",

    attributes: [.concurrent]) var array = [1, 2, 3, 4, 5] let lock = NSLock() func race() { concurrent.async { lock.lock() for i in array { // read access print(i) } lock.unlock() } concurrent.async { lock.lock() for i in 0..<10 { array.append(i) // write access } lock.unlock() } } for _ in 0...100 { race() } Write Read Read Write
  31. • GCD is faster than Operation . • GCD is

    easier to start using. • Your related code could be kept together. Comparison GCD vs Operation
  32. GCD vs Operation • The Operation API provides support for

    dependencies. • The Operation and OperationQueue classes have a number of properties that can be observed, using KVO. • Operations can be paused, resumed, and cancelled. • Ability to specify the maximum number of queued operations that can run simultaneously. Comparison
  33. GCD vs Operation • GCD is ideal if you just

    need to dispatch a block of code to a serial or concurrent queue. • The Operation API is great for encapsulating well-de f ined blocks of functionality. What would be the best choice?
  34. NSThread - Use it when you want or need to

    have direct control over the threads you create, • you need f ine-grained control over thread priorities • interfacing with some other subsystem that vends/ consumes thread objects directly and you need to stay on the same page with it. • Useful in real-time applications. NSThread GCD Operation
  35. pthread To improve compatibility between platforms, the POSIX Thread execution

    model was de f ined. Many of the POSIX conformant Operating Systems provide an implementation of pthreads. macOS being one of them, gives us access to pthreads . C-based interface. pthread NSThread GCD Operation
  36. async-await 🥲 🥲 🥲 iOS 15.0+ / macOS 12.0+ func

    fetchWeatherHistory() async -> Int { print("Start fetchWeatherHistory") sleep(1) print("End fetchWeatherHistory") return 2 } func calculateAverageTemperature(for records: Int ) async -> Int { print("Start calculateAverageTemperature") sleep(1) print("End calculateAverageTemperature") return 50 } func upload(result: Int) async -> Int { print("Start upload") sleep(1) print("End upload") return 1 } func processWeather() async { let records = await fetchWeatherHistory() let average = await calculateAverageTemperature(for: records) let response = await upload(result: average) print("Server response: \(response)") } async { await processWeather() } print("End") Start fetchWeatherHistory End End fetchWeatherHistory Start calculateAverageTemperature End calculateAverageTemperature Start upload End upload Server response: 1
  37. Useful links • https://cocoacasts.com/choosing-between-nsoperation-and-grand- central-dispatch • https://medium.com/@roykronenfeld/semaphores-in-swift- e296ea80f860 • https://www.raywenderlich.com/5371-grand-central-dispatch-tutorial-for-

    swift-4-part-2-2 • https://www.viget.com/articles/concurrency-multithreading-in-ios/ • https://medium.com/ f lawless-app-stories/concurrency-visualized-part-3- pitfalls-and-conclusion-2b893e04b97d • https://rderik.com/blog/multithreading-with-pthreads-in-swift/