functional patterns for c++ multithreading (c++ dev meetup iasi)

40
Functional Patterns For C++ Multithreading Ovidiu Farauanu

Upload: ovidiu-farauanu

Post on 18-Feb-2017

106 views

Category:

Software


5 download

TRANSCRIPT

Functional PatternsFor C++ Multithreading

Ovidiu Farauanu

Summary

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

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

(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.

(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)

(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)

(1) Meta-programming

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)

(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.

(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)

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.

● 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.

(2) Concurrency is dangerous!

(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

(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.

Two pillars:

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

verification?) ;

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/

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?

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)?

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);

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); }

(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.

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.

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.

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(); }

Transitive “const” in C++

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

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

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.

(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

(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);}

(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).

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

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!

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 ...

First class citizens

● Functions as parameters● Functions as return values

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...

(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.

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.

I would like it to be simple, something like:

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

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