Bonjour,
Pour rappel, std::lock sert � bloquer plusieurs mutex en �vitant les deadlocks.
Pr�requis : documentation : http://en.cppreference.com/w/cpp/thread/lock
Hier, par curiosit�, j'ai regard� l'impl�mentation de std::lock de GCC 4.9.2, mais j'ai �t� d��u.
La voici, accompagn�e de l'impl�mentation de std::try_lock :
Pour que ce soit plus facile � suivre, voici le code �quivalent dans le cas o� std::lock a 3 param�tres :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 template<typename _Lock> unique_lock<_Lock> __try_to_lock(_Lock& __l) { return unique_lock<_Lock>(__l, try_to_lock); } template<int _Idx, bool _Continue = true> struct __try_lock_impl { template<typename... _Lock> static void __do_try_lock(tuple<_Lock&...>& __locks, int& __idx) { __idx = _Idx; auto __lock = __try_to_lock(std::get<_Idx>(__locks)); if (__lock.owns_lock()) { __try_lock_impl<_Idx + 1, _Idx + 2 < sizeof...(_Lock)>:: __do_try_lock(__locks, __idx); if (__idx == -1) __lock.release(); } } }; template<int _Idx> struct __try_lock_impl<_Idx, false> { template<typename... _Lock> static void __do_try_lock(tuple<_Lock&...>& __locks, int& __idx) { __idx = _Idx; auto __lock = __try_to_lock(std::get<_Idx>(__locks)); if (__lock.owns_lock()) { __idx = -1; __lock.release(); } } }; /** @brief Generic try_lock. * @param __l1 Meets Mutex requirements (try_lock() may throw). * @param __l2 Meets Mutex requirements (try_lock() may throw). * @param __l3 Meets Mutex requirements (try_lock() may throw). * @return Returns -1 if all try_lock() calls return true. Otherwise returns * a 0-based index corresponding to the argument that returned false. * @post Either all arguments are locked, or none will be. * * Sequentially calls try_lock() on each argument. */ template<typename _Lock1, typename _Lock2, typename... _Lock3> int try_lock(_Lock1& __l1, _Lock2& __l2, _Lock3&... __l3) { int __idx; auto __locks = std::tie(__l1, __l2, __l3...); __try_lock_impl<0>::__do_try_lock(__locks, __idx); return __idx; } /** @brief Generic lock. * @param __l1 Meets Mutex requirements (try_lock() may throw). * @param __l2 Meets Mutex requirements (try_lock() may throw). * @param __l3 Meets Mutex requirements (try_lock() may throw). * @throw An exception thrown by an argument's lock() or try_lock() member. * @post All arguments are locked. * * All arguments are locked via a sequence of calls to lock(), try_lock() * and unlock(). If the call exits via an exception any locks that were * obtained will be released. */ template<typename _L1, typename _L2, typename ..._L3> void lock(_L1& __l1, _L2& __l2, _L3&... __l3) { while (true) { unique_lock<_L1> __first(__l1); int __idx; auto __locks = std::tie(__l2, __l3...); __try_lock_impl<0, sizeof...(_L3)>::__do_try_lock(__locks, __idx); if (__idx == -1) { __first.release(); return; } } }
Le probl�me de cette impl�mentation est que le premier mutex que la fonction essaie de bloquer est toujours le m�me, � savoir celui qui est en 1er param�tre.
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 template<class Lockable1, class Lockable2> int try_lock(Lockable1& lock1, Lockable2& lock2); template<class Lockable1, class Lockable2, class Lockable3> void lock(Lockable1& lock1, Lockable2& lock2, Lockable3& lock3) { while(true) { unique_lock<Lockable1> u1(lock1); // can throw int idx = try_lock(lock2, lock3); // can throw if(idx == -1) { u1.release(); return; } } } template<class Lockable1, class Lockable2> int try_lock(Lockable1& lock1, Lockable2& lock2) { int result = 0; unique_lock<Lockable1> u1(lock1, try_to_lock); // can throw if(u1.owns_lock()) { result = 1; unique_lock<Lockable2> u2(lock2, try_to_lock); // can throw if(u2.owns_lock()) { result = -1; u2.release(); } if(result == -1) u1.release(); } return result; }
Admettons que, par exemple, le 3e mutex soit d�j� bloqu� et que la fonction essaie de bloquer les 3 mutex. La fonction ne va jamais s'arr�ter et va continuellement bloquer et d�bloquer les deux premiers mutex. Il aurait �t� pr�f�rable qu'elle se rappelle que c'est le 3e mutex qu'elle n'a pas r�ussi � bloquer puis qu'elle d�cide, apr�s avoir d�bloquer les deux premiers mutex, de bloquer le 3e en premier.
Boost 1.61.0, lui, poss�des des lock non variadics impl�ment�s correctement (je n'ai pas regard� les autres versions de Boost). Voici l'impl�mentation du boost::lock � 3 param�tres :
Mais comment faire une impl�mentation correcte avec des variadic templates ?
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 namespace boost { namespace detail { // ... template <typename MutexType1, typename MutexType2, typename MutexType3> unsigned lock_helper(MutexType1& m1, MutexType2& m2, MutexType3& m3) { boost::unique_lock<MutexType1> l1(m1); if (unsigned const failed_lock=try_lock_internal(m2,m3)) { return failed_lock; } l1.release(); return 0; } // ... } // ... template <typename MutexType1, typename MutexType2, typename MutexType3> void lock(MutexType1& m1, MutexType2& m2, MutexType3& m3) { unsigned const lock_count = 3; unsigned lock_first = 0; for (;;) { switch (lock_first) { case 0: lock_first = detail::lock_helper(m1, m2, m3); if (!lock_first) return; break; case 1: lock_first = detail::lock_helper(m2, m3, m1); if (!lock_first) return; lock_first = (lock_first + 1) % lock_count; break; case 2: lock_first = detail::lock_helper(m3, m1, m2); if (!lock_first) return; lock_first = (lock_first + 2) % lock_count; break; } } } // ... }
Aujourd'hui, je viens d'en faire une avec du type erasure :
Mais ma premi�re impl�mentation a un inconv�nient :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 class LockableBase { public: virtual ~LockableBase() {} virtual void lock() = 0; virtual bool try_lock() = 0; virtual void unlock() = 0; }; template<class Lockable> class LockableWrapper : public LockableBase { private: Lockable& m_lock; public: explicit LockableWrapper(Lockable& lock) : m_lock(lock) {} ~LockableWrapper() override {} void lock() override { m_lock.lock(); } bool try_lock() override { return m_lock.try_lock(); } void unlock() override { m_lock.unlock(); } }; class LockableAny { private: std::unique_ptr<LockableBase> m_lockBase; public: LockableAny() {} template<class Lockable> LockableAny(Lockable& lock) : m_lockBase(new LockableWrapper<Lockable>(lock)) { } void lock() { assert(m_lockBase); m_lockBase->lock(); } bool try_lock() { assert(m_lockBase); return m_lockBase->try_lock(); } void unlock() { assert(m_lockBase); m_lockBase->unlock(); } }; template<size_t CurrentIndex, class LockableAnyArrayType, class Lockable> void fillLockableAnyArray(LockableAnyArrayType& arrayLocks, Lockable& lock) { arrayLocks[CurrentIndex] = LockableAny(lock); } template<size_t CurrentIndex, class LockableAnyArrayType, class Lockable1, class ...LockableTypes> void fillLockableAnyArray(LockableAnyArrayType& arrayLocks, Lockable1& lock1, LockableTypes&... lockN) { arrayLocks[CurrentIndex] = LockableAny(lock1); fillLockableAnyArray<CurrentIndex + 1>(arrayLocks, lockN...); } template<class Lockable1, class Lockable2, class... LockableN> void lock(Lockable1& lock1, Lockable2& lock2, LockableN&... lockN) { constexpr size_t nbArgs = 2 + sizeof...(LockableN); std::array<LockableAny, nbArgs> arrayLocks; fillLockableAnyArray<0>(arrayLocks, lock1, lock2, lockN...); // std::array<unique_lock<LockableAny>, nbArgs> arrayUniqueLocks; // EDIT 18/09/2016 vers 2h34 : bogue corrigé : ligne déplacée vers l'intérieur de la boucle while size_t firstLock = 0; while(true) { std::array<unique_lock<LockableAny>, nbArgs> arrayUniqueLocks; arrayUniqueLocks[firstLock] = unique_lock<LockableAny>(arrayLocks[firstLock]); size_t lockIndex = (firstLock + 1) % nbArgs; bool lockSuccess = true; for(; lockIndex != firstLock && lockSuccess; lockIndex = (lockIndex+1)%nbArgs) { arrayUniqueLocks[lockIndex] = unique_lock<LockableAny>(arrayLocks[lockIndex], try_to_lock); lockSuccess = arrayUniqueLocks[lockIndex].owns_lock(); } if(lockSuccess) { for(auto& uLock : arrayUniqueLocks) uLock.release(); return; } else { firstLock = (lockIndex + nbArgs - 1) % nbArgs; } } }
Je stocke les objets LockableWrapper dans la m�moire dynamique, ce qui n'est pas performant.
J'ai voulu les mettre dans un std::tuple, mais je n'ai pas r�ussi :
Finalement, dans ma deuxi�me impl�mentation, j'ai trich� en sauvegardant le LockableWrapper directement dans LockableAny. Seule la classe LockableAny a chang� :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 template<class Lockable1, class Lockable2, class... LockableN> void lock(Lockable1& lock1, Lockable2& lock2, LockableN&... lockN) { std::tuple< LockableWrapper<Lockable1>, LockableWrapper<Lockable2>, LockableWrapper<LockableN> > locks( LockableWrapper<Lockable1>(lock1), LockableWrapper<Lockable2>(lock2), LockableWrapper<LockableN>(lockN...) // Ne compile pas. ); // ... }
Les LockableWrapper ont tous la m�me taille et le m�me alignement, car ils contiennent tous la m�me chose : un pointeur vers une table virtuelle et une r�f�rence.
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 class LockableAny { private: std::aligned_storage<sizeof (LockableWrapper<std::mutex>), alignof(LockableWrapper<std::mutex>)>::type m_data; LockableBase& getLockBaseRef() { return reinterpret_cast<LockableBase&>(m_data); } public: LockableAny() {} template<class Lockable> LockableAny(Lockable& lock) { static_assert( sizeof (LockableWrapper<Lockable>) == sizeof (LockableWrapper<std::mutex>) && alignof(LockableWrapper<Lockable>) == alignof(LockableWrapper<std::mutex>), "Hack échoué. Tous les LockableWrapper doivent avoir la même taille et le même alignement." " Sinon, changer le type de m_data en std::unique_ptr<LockableBase> et adapter le code."); new(&m_data) LockableWrapper<Lockable>(lock); } void lock() { getLockBaseRef().lock(); } bool try_lock() { return getLockBaseRef().try_lock(); } void unlock() { getLockBaseRef().unlock(); } };
Le destructeur de LockableWrapper n'est pas appel�, mais ce n'est pas grave : il ne fait rien.
Cependant, c'est assez moche et je crains que ce ne soit pas portable � cause du reinterpret_cast.
En tout cas, je n'ai pas trouv� comment impl�menter correctement std::lock sans passer par des fonctions virtuelles ou des pointeurs de fonction.
Est-ce possible ? Si oui, est-ce que l'un d'entre vous y arriverait ? Le d�fi est lanc� !![]()
Partager