Software Transactional Memory
Pure functional approach
Alexander Granin
graninas@gmail.com
C++ Russia 2018, Saint Petersburg
struct Presentation
{
Introduction1 Functional programming in C++
Introduction2 Parallel and concurrent programming in C++
Theory Software Transactional Memory
Practice Dining philosophers task
Implementation What’s inside?
Limitations What you should be aware of
};
Introduction 1. Functional programming in C++
● Lambdas, closures, functions (almost pure)
● std::function<>
● Combining of pure functions
● Immutability, POD-types
● Templates - pure functional language
● for_each(), recursion
● Type inference
● Pain
std::thread
std::future
std::promise
std::async
Introduction 2. Parallel and concurrent programming in C++
std::atomic
std::mutex
std::condition_variable
Theory. Software Transactional Memory
● Concurrent data model
● Independent parallel computations (“transactions”) over a different parts of the model
● Atomic commit of changed at the end of the computations:
○ Successful, if there are no conflicts of competing changes
○ Failed, if there are conflicts
■ Changes will be rollbacked, transaction will be restarted
● It’s also nice to have:
○ Absence of explicit synchronizations
○ Composable transactions
○ Capability to retry transactions
○ Adequate behavior and safety
● New blocks:
○ atomic_noexcept
○ atomic_cancel
○ atomic_commit
○ synchronized
● Transaction-safe functions
○ transaction_safe
● Pros:
○ Transaction-safe functions
● Cons:
○ Too chaotic
○ Too far from release
○ Bad way to do STM
● Requirements:
○ GCC 6.1
Technical Specification ISO/IEC TS 19841:2015
int f()
{
static int i = 0;
atomic_noexcept
{
++i;
return i;
}
}
Wyatt-STM
WSTM::Atomically ([&](WSTM::WAtomic& at)
{
++repeat1;
v1.Set (v1.Get (at) + v2.Get (at), at);
barrier.wait ();
});
● Pros:
○ Composable transactions
○ Retry operation
○ In production
● Cons:
○ Too imperative: easy to hack
○ Has some unwanted weakness
○ Boost
● Requirements:
○ Visual Studio 2015, GCC 4.9, Clang 3.4
○ Boost 1.57
● https://github.com/bretthall/Wyatt-STM
● CppCon 2015: Brett Hall “Transactional Memory in Practice"
My library: cpp_stm_free
STML<ForkPair>
readForks(const TForkPair& forks)
{
STML<Fork> lm = readTVar(forks.left);
STML<Fork> rm = readTVar(forks.right);
return both(lm, rm, mkForkPair);
};
● Pros:
○ Purely functional: hard to hack
○ Composable monadic transactions
○ Retry operation
○ Adequate behavior
○ Many ways to improve and optimize
● Cons:
○ Highly experimental, Proof of Concept
○ Mind blowing, probably
○ Performance - ??? (will measure later)
● Requirements:
○ GCC 7.2, C++17
● https://github.com/graninas/cpp_stm_free
TVar<int> tMyInt = newTVarIO(10);
TVar operations
TVar<int> tMyInt = newTVarIO(10);
auto someResult = readTVar(tMyInt);
TVar operations
TVar<int> tMyInt = newTVarIO(10);
auto someResult = readTVar(tMyInt);
writeTVar(tMyInt, 20);
TVar operations
TVar<int> tMyInt = newTVarIO(10);
auto someResult = readTVar(tMyInt);
writeTVar(tMyInt, 20);
// Why IO?
// someResult is int?
// Doesn’t return anything, right?
TVar operations
TVar<int> tMyInt = newTVarIO(10);
STML<int> someResult = readTVar(tMyInt);
STML<Unit> m = writeTVar(tMyInt, 20);
Transactions
STML<int> - transactional type, thus:
readTVar - transaction (returns stm::STML<T>)
writeTVar - transaction (returns stm::STML<Unit>)
newTVarIO - not a transaction (returns something else)
// Why IO?
// → Not a transaction.
// someResult is int?
// → Nope. It’s STML<int>.
// Doesn’t return anything, right?
// → Actually returns STML<Unit>.
template <typename A>
STML<TVar<A>> newTVar(const A& val);
template <typename A>
STML<A> readTVar(const TVar<A>& tvar);
template <typename A>
STML<Unit> writeTVar(const TVar<A>& tvar,
const A& val);
template <typename A>
STML<A> retry();
template <typename A>
STML<A> pure(const A& val);
Transactions
newTVar :: A → STML<TVar<A>>
readTVar :: TVar<A> → STML<A>
writeTVar :: TVar<A> → A → STML<Unit>
retry :: STML<A>
pure :: A → STML<A>
Composing transactions
bind ::
STML<A> → (A → STML<B>) → STML<B>
transaction :: STML<int>
transaction = do
tvar ← newTVar (10)
readTVar (tvar)
template <typename A, typename B>
STML<B> bind(const STML<A> ma,
const function<STML<B>(A)>& f);
STML<int> transaction()
{
STML<TVar<int>> t1 = newTVar(10);
STML<int> t2 = bind(t1, [](const TVar<int>& tvar) {
return readTVar(tvar);
});
return t2;
}
STML<TVar<int>> t1 = newTVar(10);
STML<int> t2 = bind(t1, readTVar); // short form without lambda
Context context; // All TVars live in the context
int result1 = atomically(context, t2); // result1 == 10
int result2 = atomically(context, t2); // result2 == 10, also*
// * but can be different on case of many competing threads trying to change this TVar.
Atomic evaluation
STML<TVar<int>> t1 = newTVar(10);
STML<int> t2 = bind(t1, readTVar); // short form without lambda
STML<int> t3 = bind(t2, [](int i) {
if (i == 10) return retry(); // retry if i == 10
return pure(i + 1); // return incremented i otherwise
});
Context context; // All TVars live in the context
int result1 = atomically(context, t2); // result1 == 10
int result2 = atomically(context, t2); // result2 == 10, also*
int result3 = atomically(context, t3); // tries hard to retry the transaction
// * but can be different on case of many competing threads trying to change this TVar.
Retrying
template <typename A, typename B, typename Ret>
STML<Ret> both(const STML<A>& ma,
const STML<B>& mb,
const function<Ret(A, B)>& f);
template <typename A, typename B>
STML<B> sequence(const STML<A>& ma,
const STML<B>& mb);
template <typename B>
STML<B> ifThenElse(const STML<bool>& m,
const STML<B>& mOnTrue,
const STML<B>& mOnFalse);
Useful combinators
// Do both computations separately,
// combine the results with function,
// return combined result
// Do the first computation,
// drop its result,
// do the second computation,
// return its result
// Do the conditional computation `m`,
// if true, do `mOnTrue`
// otherwise, do `mOnFalse`
Practice. Dining philosophers task
enum class ForkState
{
Free,
Taken
};
struct Fork
{
std::string name;
ForkState state;
};
enum class Activity
{
Thinking,
Eating
};
struct TForkPair
{
TVar<Fork> left;
TVar<Fork> right;
};
struct Philosopher
{
std::string name;
TVar<Activity> activity;
TForkPair forks;
};
// Fine grained
TVar<Fork> tFork1 = newTVarIO(someContext, Fork {"1", ForkState::Free});
TVar<Fork> tFork2 = newTVarIO(someContext, Fork {"2", ForkState::Free});
TVar<Fork> tFork3 = newTVarIO(someContext, Fork {"3", ForkState::Free});
// Coarse grained
std::vector<Fork> forks =
{ Fork {"1", ForkState::Free}
, Fork {"2", ForkState::Free}
, Fork {"3", ForkState::Free}
}
TVar<std::vector<Fork>> tForks = newTVarIO(someContext, forks);
Fine vs. Coarse Grained
Taking a fork
STML<bool> takeFork(const TVar<Fork>& tFork)
{
STML<bool> alreadyTaken = withTVar(tFork, isForkTaken);
STML<Unit> takenByUs = modifyTVar(tFork, setForkTaken);
STML<bool> success = sequence(takenByUs, pure(true));
STML<bool> fail = pure(false);
STML<bool> result = ifThenElse(alreadyTaken, fail, success);
return result;
};
const function<bool(Fork)> isForkTaken = [](const Fork& fork) {
return fork.state == ForkState::Taken;
};
const function<Fork(Fork)> setForkTaken = [](const Fork& fork) {
return Fork { fork.name, ForkState::Taken };
};
Taking both forks
STML<bool> takeForks(const TForkPair& forks)
{
STML<bool> leftTaken = takeFork(forks.left);
STML<bool> rightTaken = takeFork(forks.right);
STML<bool> bothTaken = both(leftTaken,
rightTaken,
[](bool l, bool r) { return l && r; });
return bothTaken;
};
Philosopher: changing activity
STML<Activity> changeActivity(const Philosopher& philosopher)
{
STML<Activity> mActivity = readTVar(philosopher.activity);
STML<Activity> mNewActivity = bind(mActivity, [=](Activity currentActivity)
{
if (currentActivity == Activity::Thinking)
{
STML<bool> taken = takeForks(philosopher.forks); // try take forks
STML<Unit> change = writeTVar(philosopher.activity, Activity::Eating);
STML<Unit> m = ifThenElse(taken, change, retry()); // retry if not taken
return sequence(m, pure(Activity::Eating));
}
// more code here
}
return mNewActivity;
}
Running the task
void philosopherWorker(stm::Context& context, const Philosopher& philosopher) {
while (true) {
stm::atomically(context, changeActivity(philosopher));
std::this_thread::sleep_for(std::chrono::seconds(5));
}
}
stm::Context context;
stm::TVar<Fork> tFork1 = stm::newTVarIO(context, Fork {"1", ForkState::Free }, " Fork 1");
stm::TVar<Fork> tFork2 = stm::newTVarIO(context, Fork {"2", ForkState::Free }, " Fork 2");
Philosopher philosopher1 = mkPhilosopher(context, "1", tFork1, tFork2); // default activity TVar
Philosopher philosopher2 = mkPhilosopher(context, "2", tFork2, tFork1); // will be created here
std::thread(philosopherWorker, context, philosopher1);
std::thread(philosopherWorker, context, philosopher2);
[1] (2) Eating, 1=Taken : 2=Taken
[2] (1) Thinking, 2=Taken : 3=Taken
[3] (2) Eating, 3=Taken : 4=Taken
[4] (1) Thinking, 4=Taken : 5=Free
[5] (1) Thinking, 5=Free : 1=Taken
Philosopher 3 changed activity to: Thinking for 3 seconds.
Philosopher 1 changed activity to: Thinking for 3 seconds.
Philosopher 2 changed activity to: Eating for 1 seconds.
Philosopher 4 changed activity to: Eating for 1 seconds.
[1] (3) Thinking, 1=Free : 2=Taken
[2] (2) Eating, 2=Taken : 3=Taken
[3] (3) Thinking, 3=Taken : 4=Taken
[4] (2) Eating, 4=Taken : 5=Taken
[5] (1) Thinking, 5=Taken : 1=Free
Sample console output
Implementation. What’s inside?
Embedded Domain-Specific Language for STM
template <typename A, typename Next>
struct NewTVar
{
A val;
std::function<Next(TVar<A>)> next;
};
template <typename A, typename Next>
struct ReadTVar
{
TVar<A> tvar;
std::function<Next(A)> next;
};
template <typename A, typename Next>
struct WriteTVar
{
TVar<A> tvar;
A val;
std::function<Next(fp::Unit)> next;
};
template <typename A, typename Next>
struct Retry
{
};
STM Algebraic Data Type
template <class Ret>
struct STMF
{
std::variant<NewTVar <std::any, Ret>,
ReadTVar <std::any, Ret>,
WriteTVar <std::any, Ret>,
Retry <std::any, Ret>
> stmf;
};
template <typename Ret>
struct PureF {
Ret ret;
};
template <typename Ret>
struct FreeF {
STMF<STML<Ret>> stmf;
};
template <typename Ret>
struct STML {
std::variant<PureF<Ret>,
FreeF<Ret>
> stml;
};
Free Monad Recursive Algebraic Data Type
template <class Ret>
struct STMF
{
std::variant<NewTVar <std::any, Ret>,
ReadTVar <std::any, Ret>,
WriteTVar <std::any, Ret>,
Retry <std::any, Ret>
> stmf;
};
Free Monad Recursive Algebraic Data Type
STML<A>→
STML<A>
→ FreeF<A>
→ STMF<STML<A>>
→ ReadTVar<std::any, A>
→ [](std::any any) {
return STML<A>
→ PureF<A>
→ std::any_cast<A>(any)
}
Free Monad Recursive Algebraic Data Type
STML<A>→
STML<A>
→ FreeF<A>
→ STMF<STML<A>>
→ ReadTVar<std::any, A>
→ [](std::any any) {
return STML<A>
→ PureF<A>
→ std::any_cast<A>(any)
}
Monadic binding
template <typename A, typename B>
struct BindStmlVisitor;
template <typename A, typename B>
struct BindStmfVisitor;
template <typename A, typename B>
STML<B> bind(const STML<A>& stml, const std::function<STML<B>(A)>& f)
{
BindStmlVisitor<A, B> visitor(f);
std::visit(visitor, stml.stml);
return visitor.result;
}
“Pattern-matching”
template <typename A, typename B>
struct BindStmfVisitor
{
void operator() (const NewTVar<std::any, STML<A>>& method);
void operator() (const ReadTVar<std::any, STML<A>>& method);
void operator() (const WriteTVar<std::any, STML<A>>& method);
void operator() (const Retry <std::any, STML<A>>&);
};
template <typename A, typename B>
struct BindStmlVisitor
{
void operator() (const PureF<A>& variant);
void operator() (const FreeF<A>& variant);
};
Typed / Untyped
template <typename A, typename Next>
struct ReadTVar { // ReadTVar<A, Next>
TVar<A> tvar; // TVar<A>
std::function<Next(A)> next;
ReadTVar<std::any, Next> toAny() const {
std::function<Next(A)> nextCopy = next;
ReadTVar<std::any, Next> m; // ReadTVar<std::any, Next>
m.tvar = TVar<std::any> { tvar.id }; // TVar<std::any>
m.next = [=](const std::any& val) { // A → std::any
A val2 = std::any_cast<A>(val); // std::any → A
return nextCopy(val2);
};
return m;
}
};
TVar Interface & Implementation
using TVarId = utils::GUID;
template <typename T>
struct TVar
{
std::string name;
TVarId id;
};
struct TVarHandle
{
GUID ustamp;
std::any data;
};
using TVars = std::map<TVarId, TVarHandle>;
class Context
{
std::map<TVarId, TVarHandle> _tvars;
std::mutex _lock;
public:
bool tryCommit(const GUID& ustamp, const TVars& stagedTvars);
};
Limitations. What you should be aware of
● Side effects in transactions are prohibited
● Proper binding of transactions
● Proper data model (fine grained vs coarse grained)
● Occasional infinite retry
● Dangerous closures (be careful passing things by reference)
● Starvation
● Deadlock / livelock
● Implementation isn’t optimal by performance (yet)
Thanks for watching!
Alexander Granin
graninas@gmail.com
C++ Russia 2018, Saint Petersburg

Software transactional memory. pure functional approach

  • 1.
    Software Transactional Memory Purefunctional approach Alexander Granin graninas@gmail.com C++ Russia 2018, Saint Petersburg
  • 3.
    struct Presentation { Introduction1 Functionalprogramming in C++ Introduction2 Parallel and concurrent programming in C++ Theory Software Transactional Memory Practice Dining philosophers task Implementation What’s inside? Limitations What you should be aware of };
  • 4.
    Introduction 1. Functionalprogramming in C++ ● Lambdas, closures, functions (almost pure) ● std::function<> ● Combining of pure functions ● Immutability, POD-types ● Templates - pure functional language ● for_each(), recursion ● Type inference ● Pain
  • 5.
    std::thread std::future std::promise std::async Introduction 2. Paralleland concurrent programming in C++ std::atomic std::mutex std::condition_variable
  • 6.
    Theory. Software TransactionalMemory ● Concurrent data model ● Independent parallel computations (“transactions”) over a different parts of the model ● Atomic commit of changed at the end of the computations: ○ Successful, if there are no conflicts of competing changes ○ Failed, if there are conflicts ■ Changes will be rollbacked, transaction will be restarted ● It’s also nice to have: ○ Absence of explicit synchronizations ○ Composable transactions ○ Capability to retry transactions ○ Adequate behavior and safety
  • 7.
    ● New blocks: ○atomic_noexcept ○ atomic_cancel ○ atomic_commit ○ synchronized ● Transaction-safe functions ○ transaction_safe ● Pros: ○ Transaction-safe functions ● Cons: ○ Too chaotic ○ Too far from release ○ Bad way to do STM ● Requirements: ○ GCC 6.1 Technical Specification ISO/IEC TS 19841:2015 int f() { static int i = 0; atomic_noexcept { ++i; return i; } }
  • 8.
    Wyatt-STM WSTM::Atomically ([&](WSTM::WAtomic& at) { ++repeat1; v1.Set(v1.Get (at) + v2.Get (at), at); barrier.wait (); }); ● Pros: ○ Composable transactions ○ Retry operation ○ In production ● Cons: ○ Too imperative: easy to hack ○ Has some unwanted weakness ○ Boost ● Requirements: ○ Visual Studio 2015, GCC 4.9, Clang 3.4 ○ Boost 1.57 ● https://github.com/bretthall/Wyatt-STM ● CppCon 2015: Brett Hall “Transactional Memory in Practice"
  • 9.
    My library: cpp_stm_free STML<ForkPair> readForks(constTForkPair& forks) { STML<Fork> lm = readTVar(forks.left); STML<Fork> rm = readTVar(forks.right); return both(lm, rm, mkForkPair); }; ● Pros: ○ Purely functional: hard to hack ○ Composable monadic transactions ○ Retry operation ○ Adequate behavior ○ Many ways to improve and optimize ● Cons: ○ Highly experimental, Proof of Concept ○ Mind blowing, probably ○ Performance - ??? (will measure later) ● Requirements: ○ GCC 7.2, C++17 ● https://github.com/graninas/cpp_stm_free
  • 10.
    TVar<int> tMyInt =newTVarIO(10); TVar operations
  • 11.
    TVar<int> tMyInt =newTVarIO(10); auto someResult = readTVar(tMyInt); TVar operations
  • 12.
    TVar<int> tMyInt =newTVarIO(10); auto someResult = readTVar(tMyInt); writeTVar(tMyInt, 20); TVar operations
  • 13.
    TVar<int> tMyInt =newTVarIO(10); auto someResult = readTVar(tMyInt); writeTVar(tMyInt, 20); // Why IO? // someResult is int? // Doesn’t return anything, right? TVar operations
  • 14.
    TVar<int> tMyInt =newTVarIO(10); STML<int> someResult = readTVar(tMyInt); STML<Unit> m = writeTVar(tMyInt, 20); Transactions STML<int> - transactional type, thus: readTVar - transaction (returns stm::STML<T>) writeTVar - transaction (returns stm::STML<Unit>) newTVarIO - not a transaction (returns something else) // Why IO? // → Not a transaction. // someResult is int? // → Nope. It’s STML<int>. // Doesn’t return anything, right? // → Actually returns STML<Unit>.
  • 15.
    template <typename A> STML<TVar<A>>newTVar(const A& val); template <typename A> STML<A> readTVar(const TVar<A>& tvar); template <typename A> STML<Unit> writeTVar(const TVar<A>& tvar, const A& val); template <typename A> STML<A> retry(); template <typename A> STML<A> pure(const A& val); Transactions newTVar :: A → STML<TVar<A>> readTVar :: TVar<A> → STML<A> writeTVar :: TVar<A> → A → STML<Unit> retry :: STML<A> pure :: A → STML<A>
  • 16.
    Composing transactions bind :: STML<A>→ (A → STML<B>) → STML<B> transaction :: STML<int> transaction = do tvar ← newTVar (10) readTVar (tvar) template <typename A, typename B> STML<B> bind(const STML<A> ma, const function<STML<B>(A)>& f); STML<int> transaction() { STML<TVar<int>> t1 = newTVar(10); STML<int> t2 = bind(t1, [](const TVar<int>& tvar) { return readTVar(tvar); }); return t2; }
  • 17.
    STML<TVar<int>> t1 =newTVar(10); STML<int> t2 = bind(t1, readTVar); // short form without lambda Context context; // All TVars live in the context int result1 = atomically(context, t2); // result1 == 10 int result2 = atomically(context, t2); // result2 == 10, also* // * but can be different on case of many competing threads trying to change this TVar. Atomic evaluation
  • 18.
    STML<TVar<int>> t1 =newTVar(10); STML<int> t2 = bind(t1, readTVar); // short form without lambda STML<int> t3 = bind(t2, [](int i) { if (i == 10) return retry(); // retry if i == 10 return pure(i + 1); // return incremented i otherwise }); Context context; // All TVars live in the context int result1 = atomically(context, t2); // result1 == 10 int result2 = atomically(context, t2); // result2 == 10, also* int result3 = atomically(context, t3); // tries hard to retry the transaction // * but can be different on case of many competing threads trying to change this TVar. Retrying
  • 19.
    template <typename A,typename B, typename Ret> STML<Ret> both(const STML<A>& ma, const STML<B>& mb, const function<Ret(A, B)>& f); template <typename A, typename B> STML<B> sequence(const STML<A>& ma, const STML<B>& mb); template <typename B> STML<B> ifThenElse(const STML<bool>& m, const STML<B>& mOnTrue, const STML<B>& mOnFalse); Useful combinators // Do both computations separately, // combine the results with function, // return combined result // Do the first computation, // drop its result, // do the second computation, // return its result // Do the conditional computation `m`, // if true, do `mOnTrue` // otherwise, do `mOnFalse`
  • 20.
  • 21.
    enum class ForkState { Free, Taken }; structFork { std::string name; ForkState state; }; enum class Activity { Thinking, Eating }; struct TForkPair { TVar<Fork> left; TVar<Fork> right; }; struct Philosopher { std::string name; TVar<Activity> activity; TForkPair forks; };
  • 22.
    // Fine grained TVar<Fork>tFork1 = newTVarIO(someContext, Fork {"1", ForkState::Free}); TVar<Fork> tFork2 = newTVarIO(someContext, Fork {"2", ForkState::Free}); TVar<Fork> tFork3 = newTVarIO(someContext, Fork {"3", ForkState::Free}); // Coarse grained std::vector<Fork> forks = { Fork {"1", ForkState::Free} , Fork {"2", ForkState::Free} , Fork {"3", ForkState::Free} } TVar<std::vector<Fork>> tForks = newTVarIO(someContext, forks); Fine vs. Coarse Grained
  • 23.
    Taking a fork STML<bool>takeFork(const TVar<Fork>& tFork) { STML<bool> alreadyTaken = withTVar(tFork, isForkTaken); STML<Unit> takenByUs = modifyTVar(tFork, setForkTaken); STML<bool> success = sequence(takenByUs, pure(true)); STML<bool> fail = pure(false); STML<bool> result = ifThenElse(alreadyTaken, fail, success); return result; }; const function<bool(Fork)> isForkTaken = [](const Fork& fork) { return fork.state == ForkState::Taken; }; const function<Fork(Fork)> setForkTaken = [](const Fork& fork) { return Fork { fork.name, ForkState::Taken }; };
  • 24.
    Taking both forks STML<bool>takeForks(const TForkPair& forks) { STML<bool> leftTaken = takeFork(forks.left); STML<bool> rightTaken = takeFork(forks.right); STML<bool> bothTaken = both(leftTaken, rightTaken, [](bool l, bool r) { return l && r; }); return bothTaken; };
  • 25.
    Philosopher: changing activity STML<Activity>changeActivity(const Philosopher& philosopher) { STML<Activity> mActivity = readTVar(philosopher.activity); STML<Activity> mNewActivity = bind(mActivity, [=](Activity currentActivity) { if (currentActivity == Activity::Thinking) { STML<bool> taken = takeForks(philosopher.forks); // try take forks STML<Unit> change = writeTVar(philosopher.activity, Activity::Eating); STML<Unit> m = ifThenElse(taken, change, retry()); // retry if not taken return sequence(m, pure(Activity::Eating)); } // more code here } return mNewActivity; }
  • 26.
    Running the task voidphilosopherWorker(stm::Context& context, const Philosopher& philosopher) { while (true) { stm::atomically(context, changeActivity(philosopher)); std::this_thread::sleep_for(std::chrono::seconds(5)); } } stm::Context context; stm::TVar<Fork> tFork1 = stm::newTVarIO(context, Fork {"1", ForkState::Free }, " Fork 1"); stm::TVar<Fork> tFork2 = stm::newTVarIO(context, Fork {"2", ForkState::Free }, " Fork 2"); Philosopher philosopher1 = mkPhilosopher(context, "1", tFork1, tFork2); // default activity TVar Philosopher philosopher2 = mkPhilosopher(context, "2", tFork2, tFork1); // will be created here std::thread(philosopherWorker, context, philosopher1); std::thread(philosopherWorker, context, philosopher2);
  • 27.
    [1] (2) Eating,1=Taken : 2=Taken [2] (1) Thinking, 2=Taken : 3=Taken [3] (2) Eating, 3=Taken : 4=Taken [4] (1) Thinking, 4=Taken : 5=Free [5] (1) Thinking, 5=Free : 1=Taken Philosopher 3 changed activity to: Thinking for 3 seconds. Philosopher 1 changed activity to: Thinking for 3 seconds. Philosopher 2 changed activity to: Eating for 1 seconds. Philosopher 4 changed activity to: Eating for 1 seconds. [1] (3) Thinking, 1=Free : 2=Taken [2] (2) Eating, 2=Taken : 3=Taken [3] (3) Thinking, 3=Taken : 4=Taken [4] (2) Eating, 4=Taken : 5=Taken [5] (1) Thinking, 5=Taken : 1=Free Sample console output
  • 28.
  • 29.
    Embedded Domain-Specific Languagefor STM template <typename A, typename Next> struct NewTVar { A val; std::function<Next(TVar<A>)> next; }; template <typename A, typename Next> struct ReadTVar { TVar<A> tvar; std::function<Next(A)> next; }; template <typename A, typename Next> struct WriteTVar { TVar<A> tvar; A val; std::function<Next(fp::Unit)> next; }; template <typename A, typename Next> struct Retry { };
  • 30.
    STM Algebraic DataType template <class Ret> struct STMF { std::variant<NewTVar <std::any, Ret>, ReadTVar <std::any, Ret>, WriteTVar <std::any, Ret>, Retry <std::any, Ret> > stmf; };
  • 31.
    template <typename Ret> structPureF { Ret ret; }; template <typename Ret> struct FreeF { STMF<STML<Ret>> stmf; }; template <typename Ret> struct STML { std::variant<PureF<Ret>, FreeF<Ret> > stml; }; Free Monad Recursive Algebraic Data Type template <class Ret> struct STMF { std::variant<NewTVar <std::any, Ret>, ReadTVar <std::any, Ret>, WriteTVar <std::any, Ret>, Retry <std::any, Ret> > stmf; };
  • 32.
    Free Monad RecursiveAlgebraic Data Type STML<A>→ STML<A> → FreeF<A> → STMF<STML<A>> → ReadTVar<std::any, A> → [](std::any any) { return STML<A> → PureF<A> → std::any_cast<A>(any) }
  • 33.
    Free Monad RecursiveAlgebraic Data Type STML<A>→ STML<A> → FreeF<A> → STMF<STML<A>> → ReadTVar<std::any, A> → [](std::any any) { return STML<A> → PureF<A> → std::any_cast<A>(any) }
  • 34.
    Monadic binding template <typenameA, typename B> struct BindStmlVisitor; template <typename A, typename B> struct BindStmfVisitor; template <typename A, typename B> STML<B> bind(const STML<A>& stml, const std::function<STML<B>(A)>& f) { BindStmlVisitor<A, B> visitor(f); std::visit(visitor, stml.stml); return visitor.result; }
  • 35.
    “Pattern-matching” template <typename A,typename B> struct BindStmfVisitor { void operator() (const NewTVar<std::any, STML<A>>& method); void operator() (const ReadTVar<std::any, STML<A>>& method); void operator() (const WriteTVar<std::any, STML<A>>& method); void operator() (const Retry <std::any, STML<A>>&); }; template <typename A, typename B> struct BindStmlVisitor { void operator() (const PureF<A>& variant); void operator() (const FreeF<A>& variant); };
  • 36.
    Typed / Untyped template<typename A, typename Next> struct ReadTVar { // ReadTVar<A, Next> TVar<A> tvar; // TVar<A> std::function<Next(A)> next; ReadTVar<std::any, Next> toAny() const { std::function<Next(A)> nextCopy = next; ReadTVar<std::any, Next> m; // ReadTVar<std::any, Next> m.tvar = TVar<std::any> { tvar.id }; // TVar<std::any> m.next = [=](const std::any& val) { // A → std::any A val2 = std::any_cast<A>(val); // std::any → A return nextCopy(val2); }; return m; } };
  • 37.
    TVar Interface &Implementation using TVarId = utils::GUID; template <typename T> struct TVar { std::string name; TVarId id; }; struct TVarHandle { GUID ustamp; std::any data; }; using TVars = std::map<TVarId, TVarHandle>; class Context { std::map<TVarId, TVarHandle> _tvars; std::mutex _lock; public: bool tryCommit(const GUID& ustamp, const TVars& stagedTvars); };
  • 38.
    Limitations. What youshould be aware of ● Side effects in transactions are prohibited ● Proper binding of transactions ● Proper data model (fine grained vs coarse grained) ● Occasional infinite retry ● Dangerous closures (be careful passing things by reference) ● Starvation ● Deadlock / livelock ● Implementation isn’t optimal by performance (yet)
  • 39.
    Thanks for watching! AlexanderGranin graninas@gmail.com C++ Russia 2018, Saint Petersburg