On Tuesday, 22 June 2021 14:51:26 CEST Jonathan Wakely wrote:
> With your suggestion
> to also drop std::tuple the number of parameters decides which
> function we call. And we don't instantiate std::tuple. And we can also
> get rid of the __try_to_lock function, which was only used to deduce
> the lock type rather than use tuple_element to get it. That's much
> nicer.

👍

> > How about optimizing a likely common case where all lockables have the
> > same
> > type? In that case we don't require recursion and can manage stack usage
> > much
> > simpler:
> The stack usage is bounded by the number of mutexes being locked,
> which is unlikely to get large, but we can do that.

Right. I meant simpler, because it takes a while of staring at the recursive 
implementation to understand how it works. :)

> We can do it for try_lock too:
> 
>   template<typename _L1, typename _L2, typename... _L3>
>     int
>     try_lock(_L1& __l1, _L2& __l2, _L3&... __l3)
>     {
> #if __cplusplus >= 201703L
>       if constexpr (is_same_v<_L1, _L2>
>                     && (is_same_v<_L1, _L3> && ...))
>         {
>           constexpr int _Np = 2 + sizeof...(_L3);
>           unique_lock<_L1> __locks[_Np] = {
>               {__l1, try_to_lock}, {__l2, try_to_lock}, {__l3,
> try_to_lock}... };

This does a try_lock on all lockabes even if any of them fails. I think that's 
not only more expensive but also non-conforming. I think you need to defer 
locking and then loop from beginning to end to break the loop on the first 
unsuccessful try_lock.

>           for (int __i = 0; __i < _Np; ++__i)
>             if (!__locks[__i])
>               return __i;
>           for (auto& __l : __locks)
>             __l.release();
>           return -1;
>         }
>       else
> #endif
>       return __detail::__try_lock_impl(__l1, __l2, __l3...);
>     }
> 
> > if constexpr ((is_same_v<_L0, _L1> && ...))
> > {
> > constexpr int _Np = 1 + sizeof...(_L1);
> > std::array<unique_lock<_L0>, _Np> __locks = {
> > {__l0, defer_lock}, {__l1, defer_lock}...
> > };
> > int __first = 0;
> > do {
> > __locks[__first].lock();
> > for (int __j = 1; __j < _Np; ++__j)
> > {
> > const int __idx = (__first + __j) % _Np;
> > if (!__locks[__idx].try_lock())
> > {
> > for (int __k = __idx; __k != __first;
> > __k = __k == 1 ? _Np : __k - 1)
> > __locks[__k - 1].unlock();
> 
> This loop doesn't work if any try_lock fails when first==0, because
> the loop termination condition is never reached.

Uh yes. Which is the same reason why the __j loop doesn't start from __first + 
1.

> I find this a bit easier to understand than the loop above, and
> correct (I think):
> 
>   for (int __k = __j; __k != 0; --__k)
>     __locks[(__first + __k - 1) % _Np].unlock();

Yes, if only we had a wrapping integer type that wraps at an arbitrary N. Like 
unsigned int but with parameter, like:

  for (__wrapping_uint<_Np> __k = __idx; __k != __first; --__k)
    __locks[__k - 1].unlock();

This is the loop I wanted to write, except --__k is simpler to write and __k - 
1 would also wrap around to _Np - 1 for __k == 0. But if this is the only 
place it's not important enough to abstract.

> [...]
> @@ -620,15 +632,45 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>     *  @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.
> +   *  and unlock().  If this function 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)
>      {
> -      int __i = 0;
> -      __detail::__lock_impl(__i, 0, __l1, __l2, __l3...);
> +#if __cplusplus >= 201703L

I also considered moving it down here. Makes sense unless you want to call 
__detail::__lock_impl from other functions. And if we want to make it work for 
pre-C++11 we could do

  using __homogeneous
    = __and_<is_same<_L1, _L2>, is_same<_L1, _L3>...>;
  int __i = 0;
  __detail::__lock_impl(__homogeneous(), __i, 0, __l1, __l2, __l3...);


-Matthias

> +      if constexpr (is_same_v<_L1, _L2> && (is_same_v<_L1, _L3> && ...))
> +       {
> +         constexpr int _Np = 2 + sizeof...(_L3);
> +         unique_lock<_L1> __locks[] = {
> +             {__l1, defer_lock}, {__l2, defer_lock}, {__l3, defer_lock}...
> +         };
> +         int __first = 0;
> +         do {
> +           __locks[__first].lock();
> +           for (int __j = 1; __j < _Np; ++__j)
> +             {
> +               const int __idx = (__first + __j) % _Np;
> +               if (!__locks[__idx].try_lock())
> +                 {
> +                   for (int __k = __j; __k != 0; --__k)
> +                     __locks[(__first + __k - 1) % _Np].unlock();
> +                   __first = __idx;
> +                   break;
> +                 }
> +             }
> +         } while (!__locks[__first]);
> +
> +         for (auto& __l : __locks)
> +           __l.release();
> +       }
> +      else
> +#endif
> +       {
> +         int __i = 0;
> +         __detail::__lock_impl(__i, 0, __l1, __l2, __l3...);
> +       }
>      }
> 
>  #if __cplusplus >= 201703L


-- 
──────────────────────────────────────────────────────────────────────────
 Dr. Matthias Kretz                           https://mattkretz.github.io
 GSI Helmholtz Centre for Heavy Ion Research               https://gsi.de
 std::experimental::simd              https://github.com/VcDevel/std-simd
──────────────────────────────────────────────────────────────────────────



Reply via email to