Почему CV не был основан на семафоре, когда он был реализован в Linux? - PullRequest
0 голосов
/ 26 февраля 2019

В последнее время мне нужен метод в моей программе, чтобы уведомить поток в другой.Конечно, я хочу, чтобы он шел быстрее и стабильнее.Поэтому я сравнил переменную condition_variable в STL с семафором POSIX в Linux.Я использую gcc-8.2.0 и его библиотеки.Коды, прикрепленные позже.

результаты:

Средняя задержка уведомления CV: 10.752us StD: 3.911us Макс. Задержка: 473us Мин. Задержка: 5us

Средняя задержка уведомления семафора: 10,307us StD: 5,768us Макс. Задержка: 537us Минимальная задержка: 4us

Производительность обоих одинакова, но я думаю, что по крайней мере семафоримеет 3 преимущества:

  1. Нет ложного пробуждения.
  2. Нет утечки уведомлений.
  3. Нет необходимости использовать блокировку / мьютекс.

Мой вопрос таков: почему CV внезапно просыпается, хотя Семафор никогда этого не делает?Почему CV не был основан на семафоре, когда он был реализован в Linux?Семафоры нельзя использовать, когда CV было реализовано в Linux?

О втором преимуществе семафора: мы должны вызвать wait / wait_for / wait_until для CV перед выполнением notify_one / notify_all, иначе уведомление будет потеряно, но это трудно обеспечить в реальных работах.Представление простейшей сцены только с одним производителем и одним потребителем: когда производитель отправляет уведомление, потребитель может прослушивать или обрабатывать данные, полученные с более ранними уведомлениями.Это очень нормальная ситуация, но метод CV не может работать с ним гладко.

Семафор может отлично работать в этой ситуации.Всякий раз, когда вы отправляете sem_post уведомление, оно всегда ждет вас, пока вы не вызовете sem_wait.Просто нравится, что «Сколько вы положили, сколько вы можете получить».

Я знаю, что функция CV wait_for / wait_until может принимать аргумент тайм-аута, который дает нам возможность повторно работать после потериУведомление вместо того, чтобы продолжать тупо ждать, но это еще не механизм событий, которому нравится «уведомление / ответ», скорее нравится цикл проверки занятости, и он будет излишне потреблять больше процессорного времени.Когда я проводил свой тест, я действительно наблюдал более высокое использование процессором CV, чем у семафора.

Эксперты, которые разработали gcc, совсем не новички, как я, поэтому я хочу знать, почему они не использовалиСемафор.Только потому, что это легко приводит к ошибочным программам?Оружие и пушки могут привести к вторжению и смерти, но они также могут быть хорошими инструментами для мира и равновесия.

Это моя программа испытаний:

#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <iomanip>
#include <iostream>
#include <memory>
#include <semaphore.h>
#include <thread>

using namespace std;
using namespace std::chrono;
using namespace std::chrono_literals;

class FakeLock {
public:
   void lock() {};
   void unlock() {};
};

int                        g_iLoops = 1024;

condition_variable         g_cvsPing;
condition_variable_any     g_cvaPing;
sem_t                      g_semPing, g_semPong;

steady_clock::time_point   g_tpStart;
unique_ptr<uint64_t[]>     g_upBuffer;
uint64_t*                  g_puiElaps;
atomic_int64_t             g_aiPos = -1;

// thread body by means of CV
void thdCVS( int iMyId ) {
   /* Although a real lock is not required in many cases, but condition-
    * variable always needs one Arbitrarily. I think it's a small drawback,
    * because I believe std::mutex::lock() is a costly operation, especially
    * some time it must falls into busy checking.
    */
   mutex                mtxToken;
   unique_lock<mutex>   realLock( mtxToken );
   int iLast, iPos;
   iLast = iPos = g_aiPos;
   while ( iPos < g_iLoops ) {
      if ( ( iPos & 1 ) == iMyId ) {
         // phase 1 : as a sender
         // sleep a little while to ensure the peer has been listenning
         this_thread::sleep_for( 1us );

         g_tpStart = steady_clock::now();
         asm volatile( "mfence":::"memory" );
         g_aiPos.store( ( iLast = ++iPos ), memory_order_release );
         g_cvsPing.notify_one();
      } else {
         // phase 0 : as a listenner
         g_cvsPing.wait( realLock );
         iPos = g_aiPos.load( memory_order_acquire );
         asm volatile( "mfence":::"memory" );

         /* calculate only when iPos has been changed, since we might be
          * waked spuriously or the timeout be hitted.
          */
         if ( iPos > iLast && iPos < g_iLoops )
            g_puiElaps[ iPos ] = ( steady_clock::now() - g_tpStart ).count();
      }
   }
};

// thread body by means of CVA
void thdCVA( int iMyId ) {
   // Let's try condition_variable_any with a fake lock.
   FakeLock fakeLock;
   int iLast, iPos;
   iLast = iPos = g_aiPos;
   while ( iPos < g_iLoops ) {
      if ( ( iPos & 1 ) == iMyId ) {
         // phase 1 : as a sender
         // sleep a little while to ensure the peer has been listenning
         this_thread::sleep_for( 1us );

         g_tpStart = steady_clock::now();
         asm volatile( "mfence":::"memory" );
         g_aiPos.store( ( iLast = ++iPos ), memory_order_release );
         g_cvaPing.notify_one();
      } else {
         // phase 0 : as a listenner
         g_cvaPing.wait( fakeLock );
         iPos = g_aiPos.load( memory_order_acquire );
         asm volatile( "mfence":::"memory" );

         /* calculate only when iPos has been changed, since we might be
          * waked spuriously or the timeout hitted.
          */
         if ( iPos > iLast && iPos < g_iLoops )
            g_puiElaps[ iPos ] = ( steady_clock::now() - g_tpStart ).count();
      }
   }
};

// thread body by means of Semaphore
void thdSem( int iMyId ) {
   int iLast, iPos;
   iLast = iPos = g_aiPos;
   while ( iPos < g_iLoops ) {
      if ( ( iPos & 1 ) == iMyId ) {
         // phase 1 : as a sender
         // sleep a little while to keep fairness with thdCV
         this_thread::sleep_for( 1us );

         g_tpStart = steady_clock::now();
         asm volatile( "mfence":::"memory" );
         g_aiPos.store( ( iLast = ++iPos ), memory_order_release );
         sem_post( iMyId == 1 ? & g_semPing : & g_semPong );
      } else {
         // phase 0 : as a listenner
         sem_wait( iMyId == 0 ? & g_semPing : & g_semPong );
         iPos = g_aiPos.load( memory_order_acquire );
         asm volatile( "mfence":::"memory" );

         if ( iPos > iLast && iPos < g_iLoops )
            g_puiElaps[ iPos ] = ( steady_clock::now() - g_tpStart ).count();
      }
   }
   // notify the peer to exit
   // sem_post( & g_semPing );
};

double avgOf( const uint64_t* puiValues, int iCount,
              uint64_t& uiMax, uint64_t& uiMin );
double stdOf( const uint64_t* puiValues, int iCount, double dAvg );

int main( int argc, char** argv ) {
   if ( argc < 2 ) {
      cout << "Usage: " << argv[0] << " loop_count " << endl;
      return EXIT_FAILURE;
   }

   g_iLoops = atoi( argv[1] );
   g_upBuffer = make_unique<uint64_t[]>( g_iLoops );
   g_puiElaps = g_upBuffer.get();
   if ( sem_init( &g_semPing, 0, 0 ) || sem_init( &g_semPong, 0, 0 ) ) {
      cerr << "Failure when create a semaphore." << endl;
      return EXIT_FAILURE;
   }

   uint64_t uiMax, uiMin;
   double dAvgElp, dStdDev;

   g_aiPos = -1;
   thread   thd0( thdCVS, 0 );
   this_thread::sleep_for( 1us );
   thread   thd1( thdCVS, 1 );
   thd0.join();
   thd1.join();
   dAvgElp = avgOf( g_puiElaps, g_iLoops, uiMax, uiMin );
   dStdDev = stdOf( g_puiElaps, g_iLoops, dAvgElp );
   cout << std::fixed << std::setprecision( 3 )
        << "Avg of CV is :" << dAvgElp / 1000   << "us" << endl
        << "StD is:"       << dStdDev / 1000    << "us" << endl
        << "Max delay is:" << uiMax / 1000      << "us" << endl
        << "Min delay is:" << uiMin / 1000      << "us" << endl
        << endl;

   g_aiPos = -1;
   thd0 = thread( thdCVA, 0 );
   this_thread::sleep_for( 1us );
   thd1 = thread( thdCVA, 1 );
   thd0.join();
   thd1.join();
   dAvgElp = avgOf( g_puiElaps, g_iLoops, uiMax, uiMin );
   dStdDev = stdOf( g_puiElaps, g_iLoops, dAvgElp );
   cout << std::fixed << std::setprecision( 3 )
        << "Avg of CVA is :" << dAvgElp / 1000  << "us" << endl
        << "StD is:"       << dStdDev / 1000    << "us" << endl
        << "Max delay is:" << uiMax / 1000      << "us" << endl
        << "Min delay is:" << uiMin / 1000      << "us" << endl
        << endl;

   g_aiPos = -1;
   thd0 = thread( thdSem, 0 );
   this_thread::sleep_for( 1us );
   thd1 = thread( thdSem, 1 );
   thd0.join();
   thd1.join();
   dAvgElp = avgOf( g_puiElaps, g_iLoops, uiMax, uiMin );
   dStdDev = stdOf( g_puiElaps, g_iLoops, dAvgElp );
   cout << std::fixed << std::setprecision( 3 )
        << "Avg of sem is :" << dAvgElp / 1000  << "us" << endl
        << "StD is:"       << dStdDev / 1000    << "us" << endl
        << "Max delay is:" << uiMax / 1000      << "us" << endl
        << "Min delay is:" << uiMin / 1000      << "us" << endl
        << endl;

   sem_destroy( &g_semPing );
   return EXIT_SUCCESS;
};

double avgOf( const uint64_t* puiValues, int iCount,
              uint64_t& uiMax, uint64_t& uiMin ) {
   double dTotal = uiMin = uiMax = puiValues[0];
   for ( int i = 1; i < iCount; ++i ) {
      dTotal += puiValues[i];
      uiMax = std::max<uint64_t>( uiMax, puiValues[i] );
      uiMin = std::min<uint64_t>( uiMin, puiValues[i] );
   }
   return dTotal / iCount;
};

double stdOf( const uint64_t* puiValues, int iCount, double dAvg ) {
   double dDiff, dSum = 0;
   for ( int i = 0; i < iCount; ++i ) {
      dDiff = puiValues[i] - dAvg;
      dSum += dDiff * dDiff;
   }
   return std::sqrt( dSum / iCount );
};
...