прадухіліць ЗППП :: атамная пераліву

I have an atomic counter (std::atomic count) which deals out sequentially incrementing values to multiple threads.

uint32_t my_val = ++count;

Перад тым, як атрымаць my_val Я хачу, каб гарантаваць, што прырашчэнне не будзе перапаўнення (г.зн. вярнуцца да 0)

if (count == std::numeric_limits::max())
    throw std::runtime_error("count overflow");

Я думаю, што гэта наіўны чэк, таму што, калі праверка выконваюцца два нітак, перш чым або павялічвае лічыльнік, то другі паток для павелічэння атрымаеце-назад

if (count == std::numeric_limits::max())//if 2 threads execute this
    throw std::runtime_error("count overflow");
uint32_t my_val = ++count;      //before either gets here - possible overflow

Такім чынам, я думаю, мне трэба выкарыстоўваць CAS аперацыі, каб пераканацца, што, калі я павялічваем свой лічыльнік, я сапраўды не дапусціць магчымага перапаўнення.

Так што мае пытанні:

  • Ці з'яўляецца мая рэалізацыя правільна?
  • Ці з'яўляецца гэта так эфектыўна, як гэта можа быць (у прыватнасці, мне трэба, каб праверыць супраць макс двойчы)?

Мой код (з рабочым узорам) наступным чынам:

#include 
#include 
#include 
#include 
#include <thread>

std::atomic count;

uint16_t get_val()//called by multiple threads
{
    uint16_t my_val;
    do
    {
        my_val = count;

       //make sure I get the next value

        if (count.compare_exchange_strong(my_val, my_val + 1))
        {
           //if I got the next value, make sure we don't overflow

            if (my_val == std::numeric_limits::max())
            {
                count = std::numeric_limits::max() - 1;
                throw std::runtime_error("count overflow");
            }
            break;
        }

       //if I didn't then check if there are still numbers available

        if (my_val == std::numeric_limits::max())
        {
            count = std::numeric_limits::max() - 1;
            throw std::runtime_error("count overflow");
        }

       //there are still numbers available, so try again
    }
    while (1);
    return my_val + 1;
}

void run()
try
{
    while (1)
    {
        if (get_val() == 0)
            exit(1);
    }

}
catch(const std::runtime_error& e)
{
   //overflow
}

int main()
{
    while (1)
    {
        count = 1;
        std::thread a(run);
        std::thread b(run);
        std::thread c(run);
        std::thread d(run);
        a.join();
        b.join();
        c.join();
        d.join();
        std::cout << ".";
    }
    return 0;
}
8
uint64_t і праблема вырашана? :)
дададзена аўтар NoSenseEtAl, крыніца
uint64_t і праблема вырашана? :)
дададзена аўтар NoSenseEtAl, крыніца

8 адказы

Так, вам трэба выкарыстоўваць CAS аперацыі.

std::atomic g_count;

uint16_t get_next() {
   uint16_t new_val = 0;
   do {
      uint16_t cur_val = g_count;                                           //1
      if (cur_val == std::numeric_limits::max()) {                //2
          throw std::runtime_error("count overflow");
      }
      new_val = cur_val + 1;                                                //3
   } while(!std::atomic_compare_exchange_weak(&g_count, &cur_val, new_val));//4

   return new_val;
}

The idea is the following: once g_count == std::numeric_limits::max(), get_next() function will always throw an exception.

крокі:

    <�Літый> Атрымаць бягучае значэнне лічыльніка
  1. Калі максімальна, згенеруе выключэнне (без нумара, даступнага больш)
  2. Атрымаеце новае значэнне ў якасці прырашчэння бягучага значэння
  3. Паспрабуйце атамарнага ўсталяваць новае значэнне. Калі не атрымалася ўсталяваць яго (гэта было зроблена ўжо іншым патокам), паспрабуйце яшчэ раз.
6
дададзена
Выдатнае тлумачэнне - дзякуй!
дададзена аўтар Steve Lorimer, крыніца

Так, вам трэба выкарыстоўваць CAS аперацыі.

std::atomic g_count;

uint16_t get_next() {
   uint16_t new_val = 0;
   do {
      uint16_t cur_val = g_count;                                           //1
      if (cur_val == std::numeric_limits::max()) {                //2
          throw std::runtime_error("count overflow");
      }
      new_val = cur_val + 1;                                                //3
   } while(!std::atomic_compare_exchange_weak(&g_count, &cur_val, new_val));//4

   return new_val;
}

The idea is the following: once g_count == std::numeric_limits::max(), get_next() function will always throw an exception.

крокі:

    <�Літый> Атрымаць бягучае значэнне лічыльніка
  1. Калі максімальна, згенеруе выключэнне (без нумара, даступнага больш)
  2. Атрымаеце новае значэнне ў якасці прырашчэння бягучага значэння
  3. Паспрабуйце атамарнага ўсталяваць новае значэнне. Калі не атрымалася ўсталяваць яго (гэта было зроблена ўжо іншым патокам), паспрабуйце яшчэ раз.
6
дададзена
Выдатнае тлумачэнне - дзякуй!
дададзена аўтар Steve Lorimer, крыніца

Калі эфектыўнасць з'яўляецца вялікай праблемай, то я б параіў не быць гэтак строгім на чэку. Я мяркую, што пры нармальным выкарыстанні перапаўнення не будзе праблемай, але вам сапраўды патрэбен поўны спектр 65K (ваш прыклад выкарыстоўвае UInt16)?

Было б прасцей, калі выказаць здагадку, што нейкі максімум на колькасці патокаў, вы працуеце. Гэта разумны мяжа, паколькі ні адна праграма не мае неабмежаваную колькасць паралелізм. Так што калі ў вас ёсць N тэмы Вы можаце проста паменшыць мяжа перапаўнення 65K - N . Для параўнання, калі перапаўненне не патрэбны CAS:

uint16_t current = count.load(std::memory_order_relaxed);
if( current >= (std::numeric_limits::max() - num_threads - 1) )
    throw std::runtime_error("count overflow");
count.fetch_add(1,std::memory_order_relaxed);

Гэта стварае ўмова мяккага пераліву. Калі два паток прыходзіць сюды адразу абодва яны будуць патэнцыйна прайсці, але гэта нармальна, так як сам граф зменных ніколі не пераліваецца. Любыя будучыя заезды ў гэтай кропцы будзе лагічна перапаўненне (пакуль лічыльнік не будзе зноў памяншаецца).

2
дададзена
Я проста выкарыстоўваць uint16_t таму патэнцыял Перапаўненне адбываецца раней. Мне падабаецца ваша ідэя, але лік патокаў у цяперашні час вядома, і апошняе, што я хачу зрабіць, гэта выбраць чароўны верхнюю мяжу і затым ён перасягнуў у нейкі момант у будучыні
дададзена аўтар Steve Lorimer, крыніца
@lori, колькасць якіх не можа быць вядома, але павінен быць нейкі разумны верхні мяжа. Вы можаце паказаць 1000 нітак, ці нават 10K, калі вы хочаце. Або выкарыстоўвайце uint_32 , а затым проста паказаць мільён.
дададзена аўтар edA-qa mort-ora-y, крыніца
Гэта разумнае, дзякуй за абмен.
дададзена аўтар Jon Chesterfield, крыніца

Калі эфектыўнасць з'яўляецца вялікай праблемай, то я б параіў не быць гэтак строгім на чэку. Я мяркую, што пры нармальным выкарыстанні перапаўнення не будзе праблемай, але вам сапраўды патрэбен поўны спектр 65K (ваш прыклад выкарыстоўвае UInt16)?

Было б прасцей, калі выказаць здагадку, што нейкі максімум на колькасці патокаў, вы працуеце. Гэта разумны мяжа, паколькі ні адна праграма не мае неабмежаваную колькасць паралелізм. Так што калі ў вас ёсць N тэмы Вы можаце проста паменшыць мяжа перапаўнення 65K - N . Для параўнання, калі перапаўненне не патрэбны CAS:

uint16_t current = count.load(std::memory_order_relaxed);
if( current >= (std::numeric_limits::max() - num_threads - 1) )
    throw std::runtime_error("count overflow");
count.fetch_add(1,std::memory_order_relaxed);

Гэта стварае ўмова мяккага пераліву. Калі два паток прыходзіць сюды адразу абодва яны будуць патэнцыйна прайсці, але гэта нармальна, так як сам граф зменных ніколі не пераліваецца. Любыя будучыя заезды ў гэтай кропцы будзе лагічна перапаўненне (пакуль лічыльнік не будзе зноў памяншаецца).

2
дададзена
Я проста выкарыстоўваць uint16_t таму патэнцыял Перапаўненне адбываецца раней. Мне падабаецца ваша ідэя, але лік патокаў у цяперашні час вядома, і апошняе, што я хачу зрабіць, гэта выбраць чароўны верхнюю мяжу і затым ён перасягнуў у нейкі момант у будучыні
дададзена аўтар Steve Lorimer, крыніца
@lori, колькасць якіх не можа быць вядома, але павінен быць нейкі разумны верхні мяжа. Вы можаце паказаць 1000 нітак, ці нават 10K, калі вы хочаце. Або выкарыстоўвайце uint_32 , а затым проста паказаць мільён.
дададзена аўтар edA-qa mort-ora-y, крыніца
Гэта разумнае, дзякуй за абмен.
дададзена аўтар Jon Chesterfield, крыніца

Калі эфектыўнасць з'яўляецца вялікай праблемай, то я б параіў не быць гэтак строгім на чэку. Я мяркую, што пры нармальным выкарыстанні перапаўнення не будзе праблемай, але вам сапраўды патрэбен поўны спектр 65K (ваш прыклад выкарыстоўвае UInt16)?

Было б прасцей, калі выказаць здагадку, што нейкі максімум на колькасці патокаў, вы працуеце. Гэта разумны мяжа, паколькі ні адна праграма не мае неабмежаваную колькасць паралелізм. Так што калі ў вас ёсць N тэмы Вы можаце проста паменшыць мяжа перапаўнення 65K - N . Для параўнання, калі перапаўненне не патрэбны CAS:

uint16_t current = count.load(std::memory_order_relaxed);
if( current >= (std::numeric_limits::max() - num_threads - 1) )
    throw std::runtime_error("count overflow");
count.fetch_add(1,std::memory_order_relaxed);

Гэта стварае ўмова мяккага пераліву. Калі два паток прыходзіць сюды адразу абодва яны будуць патэнцыйна прайсці, але гэта нармальна, так як сам граф зменных ніколі не пераліваецца. Любыя будучыя заезды ў гэтай кропцы будзе лагічна перапаўненне (пакуль лічыльнік не будзе зноў памяншаецца).

2
дададзена
Я проста выкарыстоўваць uint16_t таму патэнцыял Перапаўненне адбываецца раней. Мне падабаецца ваша ідэя, але лік патокаў у цяперашні час вядома, і апошняе, што я хачу зрабіць, гэта выбраць чароўны верхнюю мяжу і затым ён перасягнуў у нейкі момант у будучыні
дададзена аўтар Steve Lorimer, крыніца
@lori, колькасць якіх не можа быць вядома, але павінен быць нейкі разумны верхні мяжа. Вы можаце паказаць 1000 нітак, ці нават 10K, калі вы хочаце. Або выкарыстоўвайце uint_32 , а затым проста паказаць мільён.
дададзена аўтар edA-qa mort-ora-y, крыніца
Гэта разумнае, дзякуй за абмен.
дададзена аўтар Jon Chesterfield, крыніца

Мне здаецца, што ёсць яшчэ ўмова гонкі, дзе Колькасць будзе ўсталяваны ў 0 на імгненне так, што іншы паток будзе бачыць значэнне 0.

Assume that count is at std::numeric_limits::max() and two threads try to get the incremented value. At the moment that Thread 1 performs the count.compare_exchange_strong(my_val, my_val + 1), count is set to 0 and that's what Thread 2 will see if it happens to call and complete get_val() before Thread 1 has a chance to restore count to max().

1
дададзена

Мне здаецца, што ёсць яшчэ ўмова гонкі, дзе Колькасць будзе ўсталяваны ў 0 на імгненне так, што іншы паток будзе бачыць значэнне 0.

Assume that count is at std::numeric_limits::max() and two threads try to get the incremented value. At the moment that Thread 1 performs the count.compare_exchange_strong(my_val, my_val + 1), count is set to 0 and that's what Thread 2 will see if it happens to call and complete get_val() before Thread 1 has a chance to restore count to max().

1
дададзена

Мне здаецца, што ёсць яшчэ ўмова гонкі, дзе Колькасць будзе ўсталяваны ў 0 на імгненне так, што іншы паток будзе бачыць значэнне 0.

Assume that count is at std::numeric_limits::max() and two threads try to get the incremented value. At the moment that Thread 1 performs the count.compare_exchange_strong(my_val, my_val + 1), count is set to 0 and that's what Thread 2 will see if it happens to call and complete get_val() before Thread 1 has a chance to restore count to max().

1
дададзена