Applied Concurrency: NOT from the ground up

Applied Concurrency: NOT from the ground up

First iteration of this talk was given at the May 2019 session of WebDevTalks.mx meetup, in Colima, México.

3bb2abb35d54dfe7c3179cb2d7c049e7?s=128

Oscar Swanros

May 29, 2019
Tweet

Transcript

  1. NOT from the ground-up. Applied Concurrency

  2. NOT from the ground-up.

  3. Oscar Swanros

  4. Oscar Swanros • @Swanros

  5. Oscar Swanros • @Swanros • oscar@swanros.com

  6. Oscar Swanros • @Swanros • oscar@swanros.com • iOS @ PSPDFKit

  7. Oscar Swanros • @Swanros • oscar@swanros.com • iOS @ PSPDFKit

    • pdfviewer.io
  8. Concurrency

  9. Concurrency

  10. Concurrency • Multiple computations at the same time.

  11. Concurrency • Multiple computations at the same time. • The

    backbone of modern computing.
  12. Concurrency • Multiple computations at the same time. • The

    backbone of modern computing. • Done right, makes our applications more usable.
  13. Examples of concurrent systems

  14. Examples of concurrent systems

  15. Examples of concurrent systems

  16. Examples of concurrent systems

  17. Processor level Memory Level Multiprocess Multithreading

  18. Processes

  19. Processes • A process is seen as a "virtual computer"

    to the program.
  20. Processes • A process is seen as a "virtual computer"

    to the program. • Individually dispose of resources.
  21. Processes • A process is seen as a "virtual computer"

    to the program. • Individually dispose of resources. • Most of the time, they're sandboxed.
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. Process Approach

  29. Process Approach Elixir (BEAM):

  30. Process Approach current = self() child = spawn(fn -> send(current,

    {self(), 1 + 2}) end) receive do {^child, 3} -> IO.puts("Received 3 back") end Elixir (BEAM):
  31. Process Approach current = self() child = spawn(fn -> send(current,

    {self(), 1 + 2}) end) receive do {^child, 3} -> IO.puts("Received 3 back") end Elixir (BEAM): Objective-C (ObjC Runtime):
  32. Process Approach current = self() child = spawn(fn -> send(current,

    {self(), 1 + 2}) end) receive do {^child, 3} -> IO.puts("Received 3 back") end [self performCoordinatedWriting:^BOOL (NSURL *writeURL) { let replaced = [self replaceFileAtURL:writeURL]; if (replaced) { [self clearCache]; } return replaced; } withOptions:0 error:nil]; Elixir (BEAM): Objective-C (ObjC Runtime):
  33. Processes on Apple Platforms

  34. Processes on Apple Platforms

  35. Processes on Apple Platforms

  36. There's a catch!

  37. There's a catch! • Even if the API states that

    you're dealing with a "process", you might not be.
  38. There's a catch! • Even if the API states that

    you're dealing with a "process", you might not be. • This is the case for some VM-backed languages.
  39. There's a catch! • Even if the API states that

    you're dealing with a "process", you might not be. • This is the case for some VM-backed languages. • Erlang/Elixir processes are not OS processes.
  40. There's a catch! • Even if the API states that

    you're dealing with a "process", you might not be. • This is the case for some VM-backed languages. • Erlang/Elixir processes are not OS processes. • The abstraction is still nice.
  41. Processor level Memory Level Multiprocess Multithreading

  42. Processor level Memory Level Multiprocess Multithreading

  43. Processor level Memory Level Multiprocess Multithreading

  44. Threads

  45. Threads • A thread is a "virtual processor".

  46. Threads • A thread is a "virtual processor". • Higher-level

    abstraction.
  47. Threads • A thread is a "virtual processor". • Higher-level

    abstraction. • All threads within the same process have a common heap.
  48. Threads • A thread is a "virtual processor". • Higher-level

    abstraction. • All threads within the same process have a common heap. • Each thread has its own stack.
  49. Threads • A thread is a "virtual processor". • Higher-level

    abstraction. • All threads within the same process have a common heap. • Each thread has its own stack. • Abuse your resources and you get a…
  50. Threads • A thread is a "virtual processor". • Higher-level

    abstraction. • All threads within the same process have a common heap. • Each thread has its own stack. • Abuse your resources and you get a…
  51. How threading works depends heavily on your environment's implementation. ⚠

  52. 1. array = [] 2. 3. 5.times.map do 4. Thread.new

    do 5. 1000.times do 6. array << nil 7. end 8. end 9. end.each(&:join) 10. 11. puts array.size Ruby's MRI's GIL
  53. 1. array = [] 2. 3. 5.times.map do 4. Thread.new

    do 5. 1000.times do 6. array << nil 7. end 8. end 9. end.each(&:join) 10. 11. puts array.size Ruby's MRI's GIL
  54. 1. array = [] 2. 3. 5.times.map do 4. Thread.new

    do 5. 1000.times do 6. array << nil 7. end 8. end 9. end.each(&:join) 10. 11. puts array.size Ruby's MRI's GIL
  55. 1. array = [] 2. 3. 5.times.map do 4. Thread.new

    do 5. 1000.times do 6. array << nil 7. end 8. end 9. end.each(&:join) 10. 11. puts array.size Ruby's MRI's GIL $ ruby pushing_nil.rb 5000 $ jruby pushing_nil.rb 4446 $ rbx pushing_nil.rb 3088
  56. Swift on iOS

  57. 1. var array: [Int] = [] 2. 3. let group

    = DispatchGroup() 4. let sema = DispatchSemaphore(value: 0) 5. let queue = DispatchQueue(label: "async-queue") 6. 7. for _ in 0..<5 { 8. queue.async(group: group, execute: DispatchWorkItem(block: { 9. for _ in 0..<1000 { 10. array.append(0) 11. } 12. })) 13. } 14. 15. group.notify(queue: queue) { 16. sema.signal() 17. } 18. 19. group.wait(timeout: .now() + 10) 20. sema.wait(timeout: .now() + 10) 21. 22. print(array.count) Swift on iOS
  58. None
  59. 1. let queue = DispatchQueue(label: "queue") 2. for _ in

    0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. }
  60. 1. let queue = DispatchQueue(label: "queue") 2. for _ in

    0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. }
  61. 1. let queue = DispatchQueue(label: "queue") 2. for _ in

    0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } 1. let queue = DispatchQueue.global(qos: .background) 2. for _ in 0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. }
  62. 1. let queue = DispatchQueue(label: "queue") 2. for _ in

    0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } 1. let queue = DispatchQueue.global(qos: .background) 2. for _ in 0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } $ thread(23660,0x7000061c0000) malloc: Incorrect checksum for freed object 0x7f9abda00008: probably modified after being freed. $ Corrupt value: 0xffffffe00000000 thread(23660,0x7000061c0000) malloc: *** set a breakpoint in malloc_error_break to debug
  63. 1. let queue = DispatchQueue(label: "queue") 2. for _ in

    0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } 1. let queue = DispatchQueue.global(qos: .background) 2. for _ in 0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } $ thread(23660,0x7000061c0000) malloc: Incorrect checksum for freed object 0x7f9abda00008: probably modified after being freed. $ Corrupt value: 0xffffffe00000000 thread(23660,0x7000061c0000) malloc: *** set a breakpoint in malloc_error_break to debug
  64. 1. let queue = DispatchQueue(label: "queue") 2. for _ in

    0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } 1. let queue = DispatchQueue.global(qos: .background) 2. for _ in 0..<5 { 3. queue.async(group: group, execute: DispatchWorkItem(block: { 4. for _ in 0..<1000 { 5. array.append(0) 6. } 7. })) 8. } $ thread(23660,0x7000061c0000) malloc: Incorrect checksum for freed object 0x7f9abda00008: probably modified after being freed. $ Corrupt value: 0xffffffe00000000 thread(23660,0x7000061c0000) malloc: *** set a breakpoint in malloc_error_break to debug
  65. Making a resource safe. Practical Application

  66. Objective

  67. Objective • Define a resource (Swift)

  68. Objective • Define a resource (Swift) • Explore approaches to

    make it safe in a concurrent environment.
  69. Objective • Define a resource (Swift) • Explore approaches to

    make it safe in a concurrent environment. • Defining safe as: it won't corrupt its data/ internal state when interacted with concurrently.
  70. Unsafe

  71. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18.
  72. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18.
  73. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18. Single threaded.
  74. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18. Single threaded. Unsafe.
  75. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18. Single threaded. Unsafe. Shared memory.
  76. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18. Single threaded. Unsafe. Shared memory. ⛔
  77. Unsafe 1. class Number { 2. private var collection: [Int]

    = [] 3. 4. var value: Int { 5. return collection.count 6. } 7. 8. func add() { 9. collection.append(0) 10. } 11. 12. func substract() { 13. if !collection.isEmpty { 14. collection.removeLast() 15. } 16. } 17. } 18. Fatal error: UnsafeMutablePointer.deinitialize with negative count Fatal error: Can't form Range with upperBound < lowerBound Single threaded. Unsafe. Shared memory. ⛔
  78. Queues

  79. Example

  80. Example 1. class QueuedNumber: Number { 2. private let queue

    = DispatchQueue(label: "accessQueue") 3. 4. override func add() { 5. queue.async { 6. super.add() 7. } 8. } 9. 10. override func substract() { 11. queue.async { 12. super.substract() 13. } 14. } 15. }
  81. Example 1. class QueuedNumber: Number { 2. private let queue

    = DispatchQueue(label: "accessQueue") 3. 4. override func add() { 5. queue.async { 6. super.add() 7. } 8. } 9. 10. override func substract() { 11. queue.async { 12. super.substract() 13. } 14. } 15. }
  82. Queues Pros & Cons

  83. Queues Pros & Cons •Safe multithread reads and writes.

  84. Queues Pros & Cons •Safe multithread reads and writes. •FIFO

    approach to scheduling.
  85. Queues Pros & Cons •Safe multithread reads and writes. •FIFO

    approach to scheduling. •Scalable.
  86. Queues Pros & Cons •Safe multithread reads and writes. •FIFO

    approach to scheduling. •Scalable. •Need to keep an eye on max number of threads.
  87. Queues Pros & Cons •Safe multithread reads and writes. •FIFO

    approach to scheduling. •Scalable. •Need to keep an eye on max number of threads. •Manage timeouts.
  88. Queues Pros & Cons •Safe multithread reads and writes. •FIFO

    approach to scheduling. •Scalable. •Need to keep an eye on max number of threads. •Manage timeouts. •Does not really protect the resources.
  89. Locking

  90. Example

  91. Example 1. class LockedNumber: Number { 2. let lock =

    NSLock() 3. 4. override func add() { 5. lock.lock() 6. super.add() 7. lock.unlock() 8. } 9. 10. override func substract() { 11. lock.lock() 12. super.substract() 13. lock.unlock() 14. } 15. }
  92. Example 1. class LockedNumber: Number { 2. let lock =

    NSLock() 3. 4. override func add() { 5. lock.lock() 6. super.add() 7. lock.unlock() 8. } 9. 10. override func substract() { 11. lock.lock() 12. super.substract() 13. lock.unlock() 14. } 15. }
  93. Locks Pros & Cons

  94. Locks Pros & Cons •Actually protects the resources. •Easier to

    implement. •Beware of lock inversion
  95. Locks Pros & Cons •Actually protects the resources. •Easier to

    implement. •Beware of lock inversion •Can deadlock really easily. •Can get out of hand easily. •Requires a full understanding of how the system works.
  96. Locking-like behavior? • dispatch_group_t (GCD) • semaphores

  97. Interprocess communication or threading are good enough most of the

    times. User-space solutions solve most* of your concurrency issues.
  98. Like… really. Unless you really care about performance.

  99. Queues

  100. Locking

  101. Lock-free programming

  102. Lock-free programming ⚠

  103. What is lock-free programming?

  104. What is lock-free programming? • No locks! • Nothing waits

    on nothing. • Operations are atomic. • Either something is or it isn't. • TAS, CAS. • Enforced at CPU level.
  105. std::atomic_flag std::atomic<T*> std::atomic<bool>

  106. std::atomic_flag std::atomic<T*> std::atomic<bool> • test_and_set • clear • load •

    store • exchange • compare_exchange
  107. None
  108. 1. #include <atomic> 2. 3. class AtomicNumber { 4. private:

    5. std::atomic<int> counter; 6. 7. public: 8. void a_add(int v) { 9. counter.store(v); 10. } 11. 12. int a_get() const { 13. return counter.load(); 14. } 15. }; 16. 17. int main() { 18. auto number = new AtomicNumber; 19. 20. number->a_add(3); 21. number->a_get(); 22. 23. return 0; 24. }
  109. But there's more…

  110. std::memory_order Absent any constraints on a multi-core system, when multiple

    threads simultaneously read and write to several variables, one thread can observe the values change in an order different from the order another thread wrote them. Indeed, the apparent order of changes can even differ among multiple reader threads
  111. std::memory_order memory_order_relaxed memory_order_consume memory_order_acquire memory_order_release memory_order_acq_rel memory_order_seq_cst Absent any constraints

    on a multi-core system, when multiple threads simultaneously read and write to several variables, one thread can observe the values change in an order different from the order another thread wrote them. Indeed, the apparent order of changes can even differ among multiple reader threads
  112. 1. #include <atomic> 3. class AtomicNumber { 4. private: 5.

    std::atomic<int> counter; 6. 7. public: 8. void a_add(int v) { 9. counter.store(v, std::memory_order_release); 10. } 11. 12. int a_get() const { 13. return counter.load(std::memory_order_acquire); 14. } 15. }; 16. 17. int main() { 18. auto number = new AtomicNumber; 19. 20. number->a_add(3); 21. number->a_get(); 22. 23. return 0; 24. }
  113. When to use lock-free programming?

  114. When to use lock-free programming? • When you care about

    performance to the absolute maximum.
  115. When to use lock-free programming? • When you care about

    performance to the absolute maximum. • Highly concurrent, high-throughput systems.
  116. When to use lock-free programming? • When you care about

    performance to the absolute maximum. • Highly concurrent, high-throughput systems. • When you need a low level way to assign prioritization between reads/writes.
  117. When to use lock-free programming? • When you care about

    performance to the absolute maximum. • Highly concurrent, high-throughput systems. • When you need a low level way to assign prioritization between reads/writes. • There's no other option.
  118. –Someone Famous “Type a quote here.”

  119. Takeaways

  120. Takeaways • Go for the higher abstraction if possible.

  121. Takeaways • Go for the higher abstraction if possible. •

    As you go deeper, you get more powers.
  122. Takeaways • Go for the higher abstraction if possible. •

    As you go deeper, you get more powers. • But you've gotta be more careful.
  123. Takeaways • Go for the higher abstraction if possible. •

    As you go deeper, you get more powers. • But you've gotta be more careful. • Choose the right approach for your use case.
  124. Takeaways • Go for the higher abstraction if possible. •

    As you go deeper, you get more powers. • But you've gotta be more careful. • Choose the right approach for your use case. • Have fun!
  125. FAQ Thank you!