Introduction au lock - free programming avec std :: atomics Meetup C++ 25 novembre 2014 Montpellier Cyril Comparon
Introduction au
lock-free programmingavec std::atomics
Meetup C++25 novembre 2014Montpellier
Cyril Comparon
Multi-tasking
ConcurrenceLe système est composé de plusieurs tâches s’exécutant de manière (partiellement) indépendante.
ParallélismeCes tâches peuvent en plus s’exécuter au même moment.
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Multi-tasking
Multi-threadingLes différentes tâches sont des exécutions distinctes du même programme, partageant le même espace mémoire.
thread1 thread2
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Multi-tasking
Multi-threadingLes différentes tâches sont des exécutions distinctes du même programme, partageant le même espace mémoire.
thread1 thread2
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Multi-tasking
Thread-safetyUne structure de donnée partagée est thread-safe si elle garantit un fonctionnement prédictible et reste dans un état valide (non corrompu) quel que soit l’ordre d’appel de ses méthodes.
Plusieurs implémentations possibles:
- Re-entrancy, thread-local storage, immutable objects outils/work-arounds
- Mutual exclusion
- Atomic operations
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Compteur non thread-safe
class Count{public:
Count(): m_val(0)
{}
int value() const{
return m_val;}
void add(int v){
m_val += v;}
private:int m_val;
};
#include <iostream>#include <future>
Count count;
void incrementer(){
for(int i=0; i<20000000; i++)count.add(1);
}
void decrementer(){
for(int i=0; i<10000000; i++)count.add(-2);
}
void main(){
std::future<void> future1(std::async(incrementer));std::future<void> future2(std::async(decrementer));future1.wait();future2.wait();std::cout << "Val = " << count.value() << std::endl;
}
Compteur thread-safe avec un mutex
#include <mutex>
class Count{public:
Count() : m_val(0) {}
int value() const{
std::lock_guard<std::mutex> lock(m_mutex);return m_val;
}
void add(int v){
std::lock_guard<std::mutex> lock(m_mutex);m_val += v;
}
private:mutable std::mutex m_mutex;int m_val;
};
#include <iostream>#include <future>
Count count;
void incrementer(){
for(int i=0; i<20000000; i++)count.add(1);
}
void decrementer(){
for(int i=0; i<10000000; i++)count.add(-2);
}
void main(){
std::future<void> future1(std::async(incrementer));std::future<void> future2(std::async(decrementer));future1.wait();future2.wait();std::cout << "Val = " << count.value() << std::endl;
}
principales opérations atomiques de std::atomics
En C++11, principalement trois opérations atomiques ont été standardisées, car le plus souvent supportées par le hardware :
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
int exchange(int desired)Atomically replaces the underlying value with desired. The operation is read-modify-write operation.
bool compare_exchange(int &expected,
int desired)
a.k.a CASAtomically compares the value stored in *this with the value pointed to by expected, and if those are equal, replaces the former with desired (performs read-modify-write operation). Otherwise, loads the actual value stored in *this into *expected (performs load operation).
int fetch_add(int incr)
Atomically replaces the current value with the result of arithmetic addition of the value and incr. The operation is read-modify-write operation.
Compteur thread-safe avec un mutex
#include <mutex>
class Count{public:
Count() : m_val(0) {}
int value() const{
std::lock_guard<std::mutex> lock(m_mutex);return m_val;
}
void add(int v){
std::lock_guard<std::mutex> lock(m_mutex);m_val += v;
}
private:mutable std::mutex m_mutex;int m_val;
};
#include <iostream>#include <future>
Count count;
void incrementer(){
for(int i=0; i<20000000; i++)count.add(1);
}
void decrementer(){
for(int i=0; i<10000000; i++)count.add(-2);
}
void main(){
std::future<void> future1(std::async(incrementer));std::future<void> future2(std::async(decrementer));future1.wait();future2.wait();std::cout << "Val = " << count.value() << std::endl;
}
Compteur lock-free
#include <atomic>
class Count{public:
Count() : m_val(0) {}
int value() const{
return m_val;}
void add(int v){
m_val.fetch_add(v);}
private:std::atomic<int> m_val;
};
#include <iostream>#include <future>
Count count;
void incrementer(){
for(int i=0; i<20000000; i++)count.add(1);
}
void decrementer(){
for(int i=0; i<10000000; i++)count.add(-2);
}
void main(){
std::future<void> future1(std::async(incrementer));std::future<void> future2(std::async(decrementer));future1.wait();future2.wait();std::cout << "Val = " << count.value() << std::endl;
}
Compteur lock-free
#include <atomic>
class Count{public:
Count() : m_val(0) {}
int value() const{
return m_val;}
void add(int v){
m_val.fetch_add(v);}
private:std::atomic<int> m_val;
};
#include <iostream>#include <future>
Count count;
void incrementer(){
for(int i=0; i<20000000; i++)count.add(1);
std::cout << "Incrementer finished!" << std::endl;}
void decrementer(){
for(int i=0; i<10000000; i++)count.add(-2);
std::cout << “Decrementer finished!" << std::endl;}
void main(){
std::future<void> future1(std::async(incrementer));std::future<void> future2(std::async(decrementer));future1.wait();future2.wait();std::cout << "Val = " << count.value() << std::endl;
}
std::atomics<T> supporte n’importe quel type
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
T std::atomic::exchange(T desired){
std::lock_guard<std::mutex> lock(m_mutex);T ret = m_value;m_value = desired;return ret;
}
bool std::atomic::compare_exchange(T &expected, T desired){
std::lock_guard<std::mutex> lock(m_mutex);if(m_value == expected) {
m_value = desired;return true;
} else {expected = m_value;return false;
}}
T std::atomic::fetch_add(T incr){
std::lock_guard<std::mutex> lock(m_mutex);T ret = m_value;m_value += incr;return ret;
}
Conclusion
Mutual exclusion
Atomic operations
Questions en vrac
Pourquoi tu me dis que c’est compliqué alors que ça a l’air tout simple ?
Pourquoi je n’envoie pas tout sur mon GPU qui est vachement plus puissant que mon CPU et il paraît que tout le monde fait que ça maintenant ?
Pourquoi on s’embête avec tout ça alors qu’il existe des langages et des outils de plus haut niveau qui me cachent toute la complexité ?
Y a-t-il des application pratiques intéressantes ?
Est-ce spécifique au C++ ?
Autres questions ?
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Les prochaines fois
- Bas niveau :• Pourquoi ça ne devrait pas marcher ?
Weak cache coherency, out-of-order execution• Pourquoi ça marche quand même ?
Memory ordering / memory barriers
- Le C++11 memory model
- Penser en termes de transactions (ACID)
- Algorithmes lock-free un peu plus intéressants
- Différents niveaux de lock-freedom
- Le problème ABA
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Remerciements
CppReferencehttp://en.cppreference.com/w/cpp/atomic
Herb Sutter – atomic<> Weapons (2012)http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2
Herb Sutter – Lock-Free Programminghttp://channel9.msdn.com/Events/CPP/C-PP-Con-2014/Lock-Free-Programming-or-Juggling-Razor-Blades-Part-I
The Concurrency Kithttp://concurrencykit.org
Wikipediahttp://en.wikipedia.org
Cyril Comparon – Meetup C++ Montpellier 25/11/2014
Remerciements
CppReferencehttp://en.cppreference.com/w/cpp/atomic
Herb Sutter – atomic<> Weapons (2012)http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2
Herb Sutter – Lock-Free Programminghttp://channel9.msdn.com/Events/CPP/C-PP-Con-2014/Lock-Free-Programming-or-Juggling-Razor-Blades-Part-I
The Concurrency Kithttp://concurrencykit.org
Wikipediahttp://en.wikipedia.org
Cyril Comparon – Meetup C++ Montpellier 25/11/2014