Степан Кольцов — Message passing: многопоточное...

27
Concurrency without mutexes

description

Степан Кольцов, Яндекс. Самые распространённые примитивы многопоточной синхронизации — это mutex и condvar. Эти примитивы плохо работают в случае contention (т. е. когда несколько потоков заходят в одну критическую область) — операции захвата и отпускания лока начинают работать на порядки медленнее и заметно нагружать CPU, при этом непредсказуемо деградирует производительность системы и появляются другие проблемы. Альтернативный подход к многопоточному программированию — это передача сообщений, или message passing. Степан расскажет о том, как устроены мьютексы, почему возникают такие проблемы и как эффективно реализовать подход message passing.

Transcript of Степан Кольцов — Message passing: многопоточное...

Page 1: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Concurrency without mutexes

Page 2: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

What’s wrong with mutex?

• Hard to write safe code

• Mutexes are slow

• Hard to parallelize

Page 3: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Hard to write safe code

Page 4: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

void first() { Guard<Mutex> guard(mutex1); ...}!void second() { Guard<Mutex> guard(mutex2); ...}!void third() { Guard<Mutex> guard(mutex1); ... second(); // possible deadlock}!void fourth() { Guard<Mutex> guard(mutex2); ... first(); // possible deadlock}

Page 5: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

void foo() { // must be locked}!

void bar() { Guard guard; foo(); }void baz() { Guard guard; foo(); }void qux() { foo(); }void quux() { Guard guard; qux(); }void corge() { quux(); }// grault does not lockvoid grault() { qux(); }

Page 6: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Mutexes are expensive

• mutex lock/unlock takes about 1us under contention

• under high load it is almost always a contention

• spinlocks are not worse

Page 7: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

struct spinlock lock = SPINLOCK_INIT;!

void do_smth() { spinlock_lock(&lock); … spinlock_unlock(&lock);}

Spinlock API

Page 8: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

struct spinlock { int locked;};!#define SPINLOCK_INIT { 0 };!void spinlock_lock(struct spinlock* spinlock) { while (!atomic_compare_exchange( &spinlock->locked, 0, 1)) {}}!void spinlock_unlock(struct spinlock* spinlock) { atomic_store(&spinlock->locked, 0, __ATOMIC_SEQ_CST);}

Spinlock impl

Page 9: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Code examples

github.com/stepancheg/no-mutex-c github.com/stepancheg/no-mutex

Page 10: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

struct mutex lock = MUTEX_INIT;!

void do_smth() { mutex_lock(&lock); … mutex_unlock(&lock);}

Mutex API

Page 11: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

struct mutex { int locked; // 1 if locked int count; // number of threads requesting a lock};!#define MUTEX_INIT { 0, 0 };!void mutex_lock(struct mutex* mutex) { atomic_add_fetch(&mutex->count, 1); while (!atomic_compare_exchange(&mutex->locked, 0, 1)) { futex(&mutex->locked, FUTEX_WAIT, 1); }}!void mutex_unlock(struct mutex* mutex) { int left = atomic_add_fetch(&mutex->count, -1); atomic_store(&mutex->locked, 0); if (left > 0) { futex(&mutex->locked, FUTEX_WAKE, 1); }}

Mutex impl

Page 12: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Numbers

lock cmpxchg 8ns

uncont. mutex lock/unlock 11ns

futex_wake 400ns

cont. mutex lock ~500ns

Page 13: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Hard to parallelize

• We want for some app to use 5 cores. How many threads should we allocate?

Page 14: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

There’s a solution!

Page 15: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Message passing/ Actor model

Page 16: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

class BlockingQueue<T> { void Enqueue(T elem) { … } // block if empty Vector<T> DequeueAll() { … }}

BlockingQueue

Page 17: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

class BlockingQueue<T> { Mutex mutex; CondVar condVar; Vector<T> elements;!

void Enqueue(T elem) { mutex.lock(); elements.push(elem); condVar.signal(); mutex.unlock(); }}

Page 18: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

class BlockingQueue<T> { Mutex mutex; CondVar condVar; Vector<T> elements;!

Vector<T> DequeueAll() { mutex.lock(); while (elements.empty()) { condVar.wait(); } Vector<T> r = move elements; mutex.unlock(); return r; }}

Page 19: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Simple message passing with dedicated thread

Page 20: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

// non-blocking queue// mutex+condvarBlockingQueue<Request> queue;!

void runProcessingThread() { for (;;) { Vector<Request> requests = Queue.dequeueAll(); // process requests }}!

void start(Request request) { queue.enqueue(request);}

Page 21: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Actors

Page 22: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

interface Runnable { void run();}!

interface ThreadPoolExecutor { void submit(Runnable);}

Executor

Page 23: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

abstract class Actor { Actor(Executor executor);!

// is not called in parallel protected abstract void act();!

// execute act() // at least once void schedule() { … }}

Actor

Page 24: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

class MyReqProcessor: Actor { MyReqProcessor(Executor exec) { super(exec); }! NonBlockingQueue<Request> queue;! override void act() { // is not called in parallel Vector<Request> reqs = queue.dequeueAll(); // process reqs }! // may be called from different threads void addWork(Request request) { queue.enqueue(request); schedule(); }}

MyReqProcessor

Page 25: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

enum ETaskState { WAITING, RUNNING, RUNNING_GOT_TASKS,};!class Actor: Runnable { Atomic<TaskState> taskState;! void schedule() { if (AtomicSwap(RGT) == WAITING) { executor.submit(this); } }! …}

Actor.schedule

Page 26: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

enum ETaskState { WAITING, RUNNING, RUNNING_GOT_TASKS,};!class Actor: Runnable { Atomic<TaskState> taskState;! override void run() { for (;;) { while (CAS(RGT -> RUNNING)) { fetch tasks act } if (CAS(RUNNING -> WAITING) { return; } } }}

Actor.run

Page 27: Степан Кольцов — Message passing: многопоточное программирование без мьютексов

Thanks