Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Thinking Outside the Synchronisation Quadrant

951 views

Published on

Presented at code::dive (2016-11-16)
Video available at https://www.youtube.com/watch?v=yl25p91flLY

Ask programmers what comes to mind when you say concurrency and most are likely to say threads. Ask what comes to mind when you say threads and most are likely to say locks or synchronisation. These assumptions are so deeply held that they define and constrain how programmers are taught and think about concurrency: thread safety is almost synonymous with the avoidance of race conditions and the guarded protection of mutable state. But this is only one quadrant of four possibilities, a quadrant diagram partitioned by mutable–immutable along one axis and shared–unshared along another. Modern C++ supports programmers in all four quadrants, not just the synchronisation quadrant. From immutability to actors, this talk will take a look at patterns and practices that encourage thinking and coding outside the locked box.

Published in: Software
  • Be the first to comment

Thinking Outside the Synchronisation Quadrant

  1. 1. Thinking Outside the Synchronisation Quadrant @KevlinHenney
  2. 2. Architecture represents the significant design decisions that shape a system, where significant is measured by cost of change. Grady Booch
  3. 3. Concurrency
  4. 4. Concurrency Threads
  5. 5. Concurrency Threads Locks
  6. 6. Architecture is the art of how to waste space. Philip Johnson
  7. 7. Architecture is the art of how to waste time.
  8. 8. Mutable Immutable Unshared Shared Unshared mutable data needs no synchronisation Unshared immutable data needs no synchronisation Shared mutable data needs synchronisation Shared immutable data needs no synchronisation
  9. 9. Mutable Immutable Unshared Shared Unshared mutable data needs no synchronisation Unshared immutable data needs no synchronisation Shared mutable data needs synchronisation Shared immutable data needs no synchronisation The Synchronisation Quadrant
  10. 10. Systems have properties β€” capabilities, features, characteristics, etc. β€” inside and out.
  11. 11. Functional Operational Developmental
  12. 12. Functional Operational Developmental
  13. 13. This is the monstrosity in love, lady, that the will is infinite, and the execution confined; that the desire is boundless, and the act a slave to limit. William Shakespeare Troilus and Cressida
  14. 14. Multitasking is really just rapid attention-switching. And that'd be a useful skill, except it takes us a second or two to engage in a new situation we've graced with our focus. So, the sum total of attention is actually decreased as we multitask. Slicing your attention, in other words, is less like slicing potatoes than like slicing plums: you always lose some juice. David Weinberger
  15. 15. http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/
  16. 16. completion time for single thread 𝑑 = 𝑑1
  17. 17. division of labour 𝑑 = 𝑑1 𝑛
  18. 18. 𝑑 = 𝑑1 1 βˆ’ 𝑝 𝑛 βˆ’ 1 𝑛 portion in parallel Amdahl's law
  19. 19. 𝑑 = 𝑑1 1 βˆ’ 𝑝 𝑛 βˆ’ 1 𝑛 + π‘˜ 𝑛 𝑛 βˆ’ 1 2 typical communication overhead inter-thread connections (worst case)
  20. 20. 𝑑 = 𝑑1 1 βˆ’ 𝑝 𝑛 βˆ’ 1 𝑛 + π‘˜ 𝑛 𝑛 βˆ’ 1 2
  21. 21. template<typename TaskIterator, typename Reducer> void map_reduce( TaskIterator begin, TaskIterator end, Reducer reduce) { std::vector<std::thread> threads; for(auto task = begin; task != end; ++task) threads.push_back(std::thread(*task)); for(auto & to_join : threads) to_join.join(); reduce(); }
  22. 22. Command-line tools can be 235x faster than your Hadoop cluster Adam Drake http://aadrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html
  23. 23. Functional Operational Developmental
  24. 24. A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in. In a multithreaded environment, the lack of understanding and the resulting problems are greatly amplified, almost to the point of panic if you are paying attention. John Carmack http://www.gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php
  25. 25. Some people, when confronted with a problem, think, "I know, I'll use threads," and then two they hav erpoblesms. Ned Batchelder https://twitter.com/#!/nedbat/status/194873829825327104
  26. 26. Shared memory is like a canvas where threads collaborate in painting images, except that they stand on the opposite sides of the canvas and use guns rather than brushes. The only way they can avoid killing each other is if they shout "duck!" before opening fire. Bartosz Milewski "Functional Data Structures and Concurrency in C++" http://bartoszmilewski.com/2013/12/10/functional-data-structures-and-concurrency-in-c/
  27. 27. There are several ways to address the problem of deadlock... http://www.cs.rpi.edu/academics/courses/fall04/os/c10/index.html
  28. 28. Just ignore it and hope it doesn't happen. Ostrich Algorithm http://www.cs.rpi.edu/academics/courses/fall04/os/c10/index.html
  29. 29. Detection and recovery β€” if it happens, take action. http://www.cs.rpi.edu/academics/courses/fall04/os/c10/index.html
  30. 30. Dynamic avoidance by careful resource allocation β€” check to see if a resource can be granted, and if granting it will cause deadlock, don't grant it. http://www.cs.rpi.edu/academics/courses/fall04/os/c10/index.html
  31. 31. Prevention β€” change the rules. http://www.cs.rpi.edu/academics/courses/fall04/os/c10/index.html
  32. 32. Functional Operational Developmental
  33. 33. habitable
  34. 34. Habitability is the characteristic of source code that enables programmers, coders, bug-fixers, and people coming to the code later in its life to understand its construction and intentions and to change it comfortably and confidently.
  35. 35. Habitability makes a place livable, like home. And this is what we want in software β€” that developers feel at home, can place their hands on any item without having to think deeply about where it is.
  36. 36. testable
  37. 37. Simple Testing Can Prevent Most Critical Failures An Analysis of Production Failures in Distributed Data-Intensive Systems https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-yuan.pdf
  38. 38. We want our code to be unit testable. What is a unit test?
  39. 39. A test is not a unit test if: β–ͺ It talks to the database β–ͺ It communicates across the network β–ͺ It touches the file system β–ͺ It can't run at the same time as any of your other unit tests β–ͺ You have to do special things to your environment (such as editing config files) to run it. Michael Feathers http://www.artima.com/weblogs/viewpost.jsp?thread=126923
  40. 40. A unit test is a test of behaviour whose success or failure is wholly determined by the correctness of the test and the correctness of the unit under test. Kevlin Henney http://www.theregister.co.uk/2007/07/28/what_are_your_units/
  41. 41. What do we want from unit tests?
  42. 42. When a unit test passes, it shows the code is correct.
  43. 43. When a unit test fails, it shows the code is incorrect.
  44. 44. isolated
  45. 45. asynchronous
  46. 46. sequential
  47. 47. Future Immediately return a β€˜virtual’ data objectβ€” called a futureβ€”to the client when it invokes a service. This future [...] only provides a value to clients when the computation is complete.
  48. 48. std::future<ResultType> iou = std::async(function); ... ResultType result = iou.get();
  49. 49. joiner<ResultType> iou = thread(function); ... ResultType result = iou(); "C++ Threading", ACCU Conference, April 2003 "More C++ Threading", ACCU Conference, April 2004 "N1883: Preliminary Threading Proposal for TR2", JTC1/SC22/WG21, August 2005
  50. 50. immutable
  51. 51. In functional programming, programs are executed by evaluating expressions, in contrast with imperative programming where programs are composed of statements which change global state when executed. Functional programming typically avoids using mutable state. https://wiki.haskell.org/Functional_programming
  52. 52. Many programming languages support programming in both functional and imperative style but the syntax and facilities of a language are typically optimised for only one of these styles, and social factors like coding conventions and libraries often force the programmer towards one of the styles. https://wiki.haskell.org/Functional_programming
  53. 53. William Cook, "On Understanding Data Abstraction, Revisited"
  54. 54. [](){}
  55. 55. [](){}()
  56. 56. https://twitter.com/mfeathers/status/29581296216
  57. 57. To keep our C++ API boundary simple, we [...] adopted one-way data flow. The API consists of methods to perform fire-and-forget mutations and methods to compute view models required by specific views. To keep the code understandable, we write functional style code converting raw data objects into immutable view models by default. As we identified performance bottlenecks through profiling, we added caches to avoid recomputing unchanged intermediate results. The resulting functional code is easy to maintain, without sacrificing performance. https://code.facebook.com/posts/498597036962415/under-the-hood-building-moments/
  58. 58. Immutable Value Define a value object type whose instances are immutable. The internal state of a value object is set at construction and no subsequent modifications are allowed.
  59. 59. const
  60. 60. &
  61. 61. &&
  62. 62. Copied Value Define a value object type whose instances are copyable. When a value is used in communication with another thread, ensure that the value is copied.
  63. 63. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int get_year() const; int get_month() const; int get_day_in_month() const; ... void set_year(int); void set_month(int); void set_day_in_month(int); ... };
  64. 64. Just because you have a getter, doesn't mean you should have a matching setter.
  65. 65. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int get_year() const; int get_month() const; int get_day_in_month() const; ... void set(int year, int month, int day_in_month); ... }; today.set(2016, 11, 16);
  66. 66. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int get_year() const; int get_month() const; int get_day_in_month() const; ... }; today = date(2016, 11, 16);
  67. 67. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int get_year() const; int get_month() const; int get_day_in_month() const; ... }; today = date { 2016, 11, 16 };
  68. 68. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int get_year() const; int get_month() const; int get_day_in_month() const; ... }; today = { 2016, 11, 16 };
  69. 69. "Get something" is an imperative with an expected side effect.
  70. 70. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int get_year() const; int get_month() const; int get_day_in_month() const; ... };
  71. 71. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int year() const; int month() const; int day_in_month() const; ... };
  72. 72. class date { public: date(int year, int month, int day_in_month); date(const date &); date & operator=(const date &); ... int year() const; int month() const; int day_in_month() const; date with_year(int) const; ... };
  73. 73. Builder Introduce a builder that provides separate methods for constructing and disposing of each different part of a complex object, or for combining cumulative changes in the construction of whole objects.
  74. 74. class date { public: ... int year() const; int month() const; int day_in_month() const; date with_year(int) const; ... };
  75. 75. class date { public: ... int year() const; int month() const; int day_in_month() const; date with_year(int new_year) const { return { new_year, month(), day_in_month() }; } ... };
  76. 76. class date { public: ... int year() const; int month() const; int day_in_month() const; date with_year(int new_year) const { return new_year == year ? *this : date { new_year, month(), day_in_month() }; } ... };
  77. 77. class date { public: ... int year() const; int month() const; int day_in_month() const; date with_year(int) const; date with_month(int) const; date with_day_in_month(int) const ... };
  78. 78. Asking a question should not change the answer. Bertrand Meyer
  79. 79. Asking a question should not change the answer, and nor should asking it twice!
  80. 80. Referential transparency is a very desirable property: it implies that functions consistently yield the same results given the same input, irrespective of where and when they are invoked. That is, function evaluation depends lessβ€”ideally, not at allβ€”on the side effects of mutable state. Edward Garson "Apply Functional Programming Principles"
  81. 81. // "FTL" (Functional Template Library :->) // container style template<typename ValueType> class container { public: typedef const ValueType value_type; typedef ... iterator; ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; ... container & operator=(const container &); ... };
  82. 82. template<typename ValueType> class set { public: typedef const ValueType * iterator; ... set(std::initializer_list<ValueType> values); ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; iterator find(const ValueType &) const; std::size_t count(const ValueType &) const; iterator lower_bound(const ValueType &) const; iterator upper_bound(const ValueType &) const; pair<iterator, iterator> equal_range(const ValueType &) const; ... private: ValueType * members; std::size_t cardinality; }; set<int> c { 2, 9, 9, 7, 9, 2, 4, 5, 8 };
  83. 83. template<typename ValueType> class array { public: typedef const ValueType * iterator; ... array(std::initializer_list<ValueType> values); ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & operator[](std::size_t) const; const ValueType & front() const; const ValueType & back() const; const ValueType * data() const; ... private: ValueType * elements; std::size_t length; }; array<int> c { 2, 9, 9, 7, 9, 2, 4, 5, 8 };
  84. 84. In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified. Such data structures are effectively immutable, as their operations do not (visibly) update the structure in-place, but instead always yield a new updated structure. http://en.wikipedia.org/wiki/Persistent_data_structure (A persistent data structure is not a data structure committed to persistent storage, such as a disk; this is a different and unrelated sense of the word "persistent.")
  85. 85. template<typename ValueType> class vector { public: typedef const ValueType * iterator; ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & operator[](std::size_t) const; const ValueType & front() const; const ValueType & back() const; const ValueType * data() const; vector pop_front() const; vector pop_back() const; ... private: ValueType * anchor; iterator from, until; };
  86. 86. template<typename ValueType> class vector { public: typedef const ValueType * iterator; ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & operator[](std::size_t) const; const ValueType & front() const; const ValueType & back() const; const ValueType * data() const; vector pop_front() const; vector pop_back() const; ... private: ValueType * anchor; iterator from, until; };
  87. 87. template<typename ValueType> class vector { public: typedef const ValueType * iterator; ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & operator[](std::size_t) const; const ValueType & front() const; const ValueType & back() const; const ValueType * data() const; vector popped_front() const; vector popped_back() const; ... private: ValueType * anchor; iterator from, until; };
  88. 88. template<typename ValueType> class vector { public: typedef const ValueType * iterator; ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & operator[](std::size_t) const; const ValueType & front() const; const ValueType & back() const; const ValueType * data() const; vector popped_front() const; vector popped_back() const; ... private: ValueType * anchor; iterator from, until; };
  89. 89. I still have a deep fondness for the Lisp model. It is simple, elegant, and something with which all developers should have an infatuation at least once in their programming life. Kevlin Henney "A Fair Share (Part I)", CUJ C++ Experts Forum, October 2002
  90. 90. lispt
  91. 91. template<typename ValueType> class list { public: class iterator; ... std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & front() const; list popped_front() const; list pushed_front() const; ... private: struct link { link(const ValueType & value, link * next); ValueType value; link * next; }; link * head; std::size_t length; };
  92. 92. Hamlet: Yea, from the table of my memory I'll wipe away all trivial fond records. William Shakespeare The Tragedy of Hamlet [Act I, Scene 5]
  93. 93. Garbage collection [...] is optional in C++; that is, a garbage collector is not a compulsory part of an implementation. Bjarne Stroustrup http://stroustrup.com/C++11FAQ.html
  94. 94. assert( std::get_pointer_safety() == std::pointer_safety::strict);
  95. 95. Ophelia: 'Tis in my memory locked, and you yourself shall keep the key of it. William Shakespeare The Tragedy of Hamlet [Act I, Scene 3]
  96. 96. A use-counted class is more complicated than a non-use- counted equivalent, and all of this horsing around with use counts takes a significant amount of processing time. Robert Murray C++ Strategies and Tactics
  97. 97. template<typename ValueType> class vector { public: typedef const ValueType * iterator; ... bool empty() const; std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & operator[](std::size_t) const; const ValueType & front() const; const ValueType & back() const; const ValueType * data() const; vector popped_front() const; vector popped_back() const; ... private: std::shared_ptr<ValueType> anchor; iterator from, until; }; Uses std::default_delete<ValueType[]>, but cannot be initialised from std::make_shared
  98. 98. template<typename ValueType> class list { public: class iterator; ... std::size_t size() const; iterator begin() const; iterator end() const; const ValueType & front() const; list popped_front() const; list pushed_front() const; ... private: struct link { link(const ValueType & value, std::shared_ptr<link> next); ValueType value; std::shared_ptr<link> next; }; std::shared_ptr<link> head; std::size_t length; };
  99. 99. { list<Anything> chain; std::fill_n( std::front_inserter(chain), how_many, something); } On destruction, deletion of links is recursive through each link, causing the stack to blow up for surprisingly small values of how_many.
  100. 100. Instead of using threads and shared memory as our programming model, we can use processes and message passing. Process here just means a protected independent state with executing code, not necessarily an operating system process. Russel Winder "Message Passing Leads to Better Scalability in Parallel Systems"
  101. 101. Languages such as Erlang (and occam before it) have shown that processes are a very successful mechanism for programming concurrent and parallel systems. Such systems do not have all the synchronization stresses that shared- memory, multithreaded systems have. Russel Winder "Message Passing Leads to Better Scalability in Parallel Systems"
  102. 102. template<typename ValueType> class channel { public: void send(const ValueType &); bool try_receive(ValueType &); private: ... };
  103. 103. template<typename ValueType> class channel { public: void send(const ValueType &); bool try_receive(ValueType &); private: std::deque<ValueType> fifo; };
  104. 104. template<typename ValueType> class channel { public: void send(const ValueType & to_send) { fifo.push_back(to_send); } ... };
  105. 105. template<typename ValueType> class channel { public: ... bool try_receive(ValueType & to_receive) { bool received = false; if (!fifo.empty()) { to_receive = fifo.front(); fifo.pop_front(); received = true; } return received; } ... };
  106. 106. template<typename ValueType> class channel { public: void send(const ValueType &); bool try_receive(ValueType &); private: std::mutex key; std::deque<ValueType> fifo; };
  107. 107. void send(const ValueType & to_send) { std::lock_guard<std::mutex> guard(key); fifo.push_back(to_send); }
  108. 108. bool try_receive(ValueType & to_receive) { bool received = false; if (key.try_lock()) { std::lock_guard<std::mutex> guard(key, std::adopt_lock); if (!fifo.empty()) { to_receive = fifo.front(); fifo.pop_front(); received = true; } } return received; }
  109. 109. template<typename ValueType> class channel { public: void send(const ValueType &); void receive(ValueType &); bool try_receive(ValueType &); private: std::mutex key; std::condition_variable_any non_empty; std::deque<ValueType> fifo; };
  110. 110. void send(const ValueType & to_send) { std::lock_guard<std::mutex> guard(key); fifo.push_back(to_send); non_empty.notify_all(); }
  111. 111. void receive(ValueType & to_receive) { std::lock_guard<std::mutex> guard(key); non_empty.wait( key, [this] { return !fifo.empty(); }); to_receive = fifo.front(); fifo.pop_front(); }
  112. 112. https://twitter.com/richardadalton/status/591534529086693376
  113. 113. std::string fizzbuzz(int n) { return n % 15 == 0 ? "FizzBuzz" : n % 3 == 0 ? "Fizz" : n % 5 == 0 ? "Buzz" : std::to_string(n); }
  114. 114. void fizzbuzzer(channel<int> & in, channel<std::string> & out) { for (;;) { int n; in.receive(n); out.send(fizzbuzz(n)); } }
  115. 115. int main() { channel<int> out; channel<std::string> back; std::thread fizzbuzzing(fizzbuzzer, out, back) for (int n = 1; n <= 100; ++n) { out.send(n); std::string result; back.receive(result); std::cout << result << "n"; } ... }
  116. 116. int main() { channel<int> out; channel<std::string> back; std::thread fizzbuzzing(fizzbuzzer, out, back) for (int n = 1; n <= 100; ++n) { out << n; std::string result; back >> result; std::cout << result << "n"; } ... }
  117. 117. void fizzbuzzer(channel<int> & in, channel<std::string> & out) { for (;;) { int n; in >> n; out << fizzbuzz(n); } }
  118. 118. template<typename ValueType> class channel { public: void send(const ValueType &); void receive(ValueType &); bool try_receive(ValueType &); void operator<<(const ValueType &); void operator>>(ValueType &); private: std::mutex key; std::condition_variable_any non_empty; std::deque<ValueType> fifo; };
  119. 119. template<typename ValueType> class channel { public: void send(const ValueType &); void receive(ValueType &); bool try_receive(ValueType &); void operator<<(const ValueType &); receiving operator>>(ValueType &); private: std::mutex key; std::condition_variable_any non_empty; std::deque<ValueType> fifo; };
  120. 120. template<typename ValueType> class channel { public: void send(const ValueType &); void receive(ValueType &); bool try_receive(ValueType &); void operator<<(const ValueType & to_send) { send(to_send); } receiving operator>>(ValueType & to_receive); { return receiving(this, to_receive); } ... };
  121. 121. class receiving { public: receiving(channel * that, ValueType & to_receive) : that(that), to_receive(to_receive) { } receiving(receiving && other) : that(other.that), to_receive(other.to_receive) { other.that = nullptr; } operator bool() { auto from = that; that = nullptr; return from && from->try_receive(to_receive); } ~receiving() { if (that) that->receive(to_receive); } private: channel * that; ValueType & to_receive; };
  122. 122. std::string fizzbuzz(int n) { if (n < 1 || n > 100) throw std::domain_error( "fizzbuzz(n) is defined for n in [1..100]") return n % 15 == 0 ? "FizzBuzz" : n % 3 == 0 ? "Fizz" : n % 5 == 0 ? "Buzz" : std::to_string(n); }
  123. 123. void fizzbuzzer(channel<int> & in, channel<std::any> & out) { for (;;) { try { int n; in >> n; out << fizzbuzz(n); } catch (...) { out << std::current_exception(); } } }
  124. 124. int main() { channel<int> out; channel<std::any> back; std::thread fizzbuzzing(fizzbuzzer, out, back) for (int n = 1; n <= 100; ++n) { out << n; std::any result; back >> result; if (result.type() == typeid(std::string)) std::cout << std::any_cast<std::string>(result) << "n"; } ... }
  125. 125. template<typename ValueType> class channel { public: channel(); channel(std::function<void(const ValueType &)>); void send(const ValueType &); void receive(ValueType &); bool try_receive(ValueType &); void operator<<(const ValueType &); receiving operator>>(ValueType &); private: std::mutex key; std::condition_variable_any non_empty; std::deque<ValueType> fifo; };
  126. 126. int main() { channel<int> out; channel<std::any> back( [](const any & received) { if (received.type() == typeid(std::exception_ptr)) std::rethrow_exception(any_cast<std::exception_ptr>(received)); }); std::thread fizzbuzzing(fizzbuzzer, out, back) ... }
  127. 127. int main() { ... for (int n = 1; n <= 100; ++n) { try { out << n; std::any result; back >> result; std::cout << std::any_cast<std::string>(result) << "n"; } catch (std::domain_error & caught) { std::cout << caught.what() << "n"; } } ... }
  128. 128. Pipes and Filters Divide the application's task into several self-contained data processing steps and connect these steps to a data processing pipeline via intermediate data buffers.
  129. 129. Simple filters that can be arbitrarily chained are more easily re-used, and more robust, than almost any other kind of code. Brandon Rhodes http://rhodesmill.org/brandon/slides/2012-11-pyconca/
  130. 130. Multithreading is just one damn thing after, before, or simultaneous with another. Andrei Alexandrescu
  131. 131. Actor-based concurrency is just one damn message after another.
  132. 132. No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient. John Carmack http://www.gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php
  133. 133. Programming in a functional style makes the state presented to your code explicit, which makes it much easier to reason about, and, in a completely pure system, makes thread race conditions impossible. John Carmack http://www.gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php
  134. 134. Think outside the synchronisation quadrant...
  135. 135. All computers wait at the same speed.

Γ—