Top Banner
Functional Patterns For C++ Multithreading Ovidiu Farauanu
40

Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Feb 18, 2017

Download

Software

Ovidiu Farauanu
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Functional PatternsFor C++ Multithreading

Ovidiu Farauanu

Page 2: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Summary

1. Design Patterns and OOP popularity2. Multithreading and OOP3. Functional Design for Multithreaded programming

Page 3: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

A design pattern systematically names, motivates, and explains a general design that addresses a recurring design problem. It describes the problem, the solution, when to apply the solution, and its consequences.

A mix of guidelines, templates and construction advice

Page 4: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(1) Design Patterns - The Celebrities: GoF

Some well known patterns*:

● Creational: Singleton, Factory, Builder, etc;● Structural: Adapter, Proxy, Facade, Decorator, etc;● Behavioural: Command, Interpreter, State, Strategy, Visitor, Observer, Mediator, etc.

*They are the same in all languages like: C++, Java, C#, etc. And well known. (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)

Why is this? Oh… well, all those languages are object oriented.

Page 5: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(1) Design Patterns - the wrong level of abstraction?

It is logical to use common strategies to solve recurring problems. In really abstract languages it is possible to formalize those strategies and put them into a library. (Peter Norvig, Paul Graham, Edgar Holleis)

Patterns are not "symptoms of not having enough abstraction in your code", but are symptoms of having not enough means of abstraction in your language.

A good alternative to Object Oriented Design patterns is Aspect Oriented Programming. (Java annotation, Python decorators)

Page 6: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(1) Macros (not preproc.) / Templates

● You modify/upgrade/specialize/build the language, adding some code to your libraries ...● It’s all about the macro expander? Just string replacements and not syntax aware!● Programmable programming language (LISP family)● Data is code and security threats (not if happens at compile time only)

Page 7: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(1) Meta-programming

Page 8: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Other types of patterns:

● Architectural patterns● Transactional patterns● Concurrency patterns: critical zone, lock object, guarded suspension, balking, scheduler, read/write lock, producer/consumer, two

step termination, double buffering, asynchronous processing, future, thread pool, double check locking, active object, monitor object, thread specific storage, leader/followers

But … what about some “functional patterns”?

They are not like some UML, object composition recipes, code snippets, etc. But more related to the properties of types (types theory) in the compiler’s type system. (like: transitive immutability, function purity, etc)

Page 9: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(2) Systems Programming & Multithreading

I used to be obsessed with Object Oriented Design Patterns, but “as an engineer I found that I have to stay pragmatic”. The reason is:

Multi-threaded programming is really, really hard.

(especially when not designed carefully)

The problem is shared, mutable data: OOP encourages both.

Page 10: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(2) Sharing + Mutation = Data Race

● Composability: OOP has been very successful because engineers solve problems by dividing them in smaller (or easier to solve) subproblems

● Objects do not compose in the presence of concurrency● OOP-style abstractions hide sharing and mutation● Problem: Sharing + Mutation = Data Race● Locking: Locking itself doesn’t compose● Multicore programming: control over mutations (incl. CPU cache

inconsistencies)

Page 11: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Why I am using “multithreading” instead of “concurrency”?

Most of the talks are going something like:

(a) parallelism is important, so (b) let’s talk about concurrency.

Page 12: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

● Concurrency is concerned with non-deterministic composition of programs

● Parallelism is concerned with asymptotic efficiency of programs with deterministic behavior

Concurrency is required to implement parallelism, but it is also required to implement sequentiality too. Concurrency is not relevant to parallelism.

Page 13: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(2) Concurrency is dangerous!

Page 14: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(2) Concurrency and design decisions

Multithreading is normally painful and must be managed with a lot of care and very good design.

There are a lot of means out there to reach a safe multithreading in your software.

Fact is that a lot of software at least C++ software do not use them.

Florentin Picioroaga

Page 15: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(2) Systems Programming

I’m sorry that I long ago coined the term “objects” for this topic, because it gets many people to focus on the lesser idea. The big idea is “messaging”.

-- Alan Key

Concurrency is not hard (if done with proper tools), locks and threads synchronization are hard.

Page 16: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Two pillars:

● Careful design of your software● Good compiler infrastructure (or an “über” static code checker ~ formal

verification?) ;

Page 17: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

POSA (Pattern Oriented Software Architecture)

Volumes: 1 (1996), 2 (2000), 3 (2003), 4-5 (2007)

Frank Buschmann, Kevlin Henney, Douglas C. Schmidt

https://www.dre.vanderbilt.edu/~schmidt/POSA/

Page 18: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Lock Free Synchronization

● Compare and swap● Test and set● Fetch and add● Read-copy-update● Transactional Memory (Software / Hardware in development)

Require hardware support, Lock-free is not wait-free (Wait-free synchronization much harder, Usually specifiable only given a fixed number of threads); implementations of common data structures are available; Lock-free synchronization does not solve contention.

Boost.Lockfree & C++11 STL?

Page 19: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

FP is an eternal promise

FP, not a magic bullet, still have to use spinlocks, memory barriers, etc.

We are systems programmers and need to use a lot of IOs, network, files, etc. → programmer must control the usage of paradigms.

But what about parallel computing (multi-core programming)?

Page 20: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Think function programming as “opposed” to object orientation.

Function types separates data from behavior.

Dijkstra: “Object-oriented programming is an exceptionally bad idea which could only have originated in California”.

But he was wrong (and arogant), it actually originates in Norway (Simula in 60’s);

Page 21: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Side-effects

C doesn't define the order of the side effects (another reason to avoid side effects).

#include <stdio .h>int foo(int n) {printf("Foo got %d\n", n); return(0);}int bar(int n) {printf("Bar got %d\n", n); return(0);}

int main(int argc, char *argv[]) { int m = 0; int (*(fun_array[3]))(); int i = 1; int ii = i/++i; printf("\ni/++i = %d, ",ii); fun_array[1] = foo; fun_array[2] = bar; (fun_array[++m])(++m); }

Page 22: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(3) Immutable data and more

Referential Transparency

● an expression can be replaced with its value without changing the behavior of the program● the same results for a given set of inputs at any point in time (pure functions)

This allows memoization (automatic caching) and parallelization.

No side effects → functions can be evaluated in parallel trivially. (Function that “does” nothing)Advantage: Immutable sharing is never contentious ; no order dependencies

Purity: a contract between functions and their callers: The implementation of a pure function does not access global mutable state.

Page 23: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Transitive “const” in C++

struct A { A(): x_{ new int} {} ~A() { delete x_; } int& x() { return *x_; } const int& x() const { return *x_; }private: int* x_;};

However, it is still possible to write to *x_ from within const member functions of A. This makes it possible for const member functions to have side-effects on the class which are unexpected by the user.

Page 24: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Transitive “const” in C++

C++11's smart pointers also have the property of not being transitively constpointer std::unique_ptr::get() const;typename std::add_lvalue_reference<T>::type operator*() const; pointer operator->() const;

These methods all return non-const pointers and references, even if the method is called on a const std::unique_ptr instance.

Page 25: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Transitive “const” in C++

template<class T, class Deleter = std::default_delete<T>> class transitive_ptr : public std::unique_ptr<T,Deleter>{public: // inherit typedefs for the sake of completeness typedef typename std::unique_ptr<T,Deleter>::pointer pointer; typedef typename std::unique_ptr<T,Deleter>::element_type element_type; typedef typename std::unique_ptr<T,Deleter>::deleter_type deleter_type; typedef const typename std::remove_pointer<pointer>::type* const_pointer; using std::unique_ptr<T,Deleter>::unique_ptr; // add transitive const version of get() pointer get() { return std::unique_ptr<T,Deleter>::get(); } const_pointer get() const { return std::unique_ptr<T,Deleter>::get(); } // add transitive const version of operator*() typename std::add_lvalue_reference<T>::type operator*() { return *get(); } typename std::add_lvalue_reference<const T>::type operator*() const { return *get(); }

Page 26: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Transitive “const” in C++

// add transitive const version of operator->() pointer operator->() { return get(); } const_pointer operator->() const { return get();}};

Page 27: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Function purity

● Global variables (references, location referenced pointers and static storage, incl. locals) cannot be written to

● Such variables cannot be read from, either unless they are invariant (immutable)● Pure functions can only call other pure functions● Parameters to a pure function can be mutable but calls cannot be cached

Note: Purity is not always desirable or achievable, not a silver bullet

Page 28: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Other advantages of purity

● Pure functions can be executed asynchronously. (std::async) This means that not only can the function be executed lazily (for instance using a std::promise), it can also be farmed out to another core (this will become increasingly important as more cores become commonplace).

● They can be hot swapped (meaning replaced at runtime), because they do not rely on any global initialization or termination state.

Page 29: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(3) Function as object (functor)

Functor - is simply any object that can be called as if it is a function, an object of a class that defines operator().

In this case function composition is similar to object composition, but with a subtle difference: behavior and data are not coupled.

⇒ Increased modularity: Because of functional purity, a part of a program cannot mess with another. → Easy refactoring

Page 30: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(3) Composition

// C++14#include <utility>

template<typename G, typename F>auto operator*(G&& g, F&& f) { return [g,f](auto&& t) { return g(f(std::forward<decltype(t)>(t))); }; }

// Usage sampleauto const f = [](int v) { return v - 1.f; };auto const g = [](float v) { return static_cast<int>(v); };auto const h = g * f;

int main(int argc, const char* argv[]) { return h(1);}

Page 31: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(3) Applicative / Concatenative

This is the basic reason Unix pipes are so powerful: they form a rudimentary string-based concatenative programming language.

Lazy evaluation: offers iterators that never invalidate (a problem that occurs when traversing shared mutable containers).

Page 32: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

The type of a concatenative function is formulated so that it takes any number of inputs, uses only the topmost of these, and returns the unused input followed by actual output. These functions are essentially operating on a list-like data structure, one that allows removal and insertion only at one end. And any programmer worth his salt can tell you what that structure is called….

A STACK

Page 33: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Partial application

using namespace std;using namespace std::placeholders;

template<typename T>T add(T left, T right) { return left + right; }

int main() { auto bound_function = std::bind(add<int>, _1, 6); // Here _1 is a placeholder}

Problem: neither the compiler nor the runtime will ever complain!

Page 34: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

template<typename T, typename X, typename Y>auto cancel(T func, X left, Y right)->function<decltype(func(left, right))(X)> { return bind(func, left, _1);}

int main() { auto bound_function = cancel(add<int>, 6, 11); cout << bound_function(22) << endl;}

// bind generates a forwarding call wrapper for f. // Calling this wrapper is equivalent to invoking f with some of its arguments bound to args.

// Side effects can be “ignored” or “denied” via partial application ...

Page 35: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

First class citizens

● Functions as parameters● Functions as return values

Page 36: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Memoization & Thunking

Memoization is an old term, which often means tabling a function's result. Probably, no modern compiler does that because it's extremely expensive??

Lambda lifting: an expression containing a free variable is replaced by a function applied to that variable. (similar to “move field” refactoring operation on OOP designs)

Monadic lifting...

Page 37: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

(3) Types are functions, not classes

Typedefs of pointer to functions?

How do you define a “callback” in Java? An interface with a single method, and a class that implements that interface.

Page 38: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Suspenders

template<class T>class Susp {public: explicit Susp(std::function<T()> f) : _f(f) {} T get() { return _f(); }private: std::function<T()> _f;};

int x = 2;int y = 3;Susp<int> sum([x, y]() { return x + y; });...int z = sum.get();

● If the function is not pure, we may get different values each time; ● if the function has side effects, these may happen multiple times; ● if the function is expensive, the performance will suffer.

All these problems may be addressed by memoizing the value.

Page 39: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

I would like it to be simple, something like:

auto CounterFactory = [](int j) { auto i = j; return [i]() { i++; return i; };};

Page 40: Functional Patterns for C++ Multithreading (C++ Dev Meetup Iasi)

Thanks to ...

● Andrei Alexandrescu & Walter Bright (D and C++)● Bartosz Milewski (Haskell and C++, D devel.)● Scott Wlaschin ( )● Also thanks to Rust and Go communities