Est-il sûr pour la concurrence d'appeler concurrency :: concurrent_vector :: push_back tout en itérant sur ce concurrent_vector dans un autre thread?

9

push_back , begin , end sont décrits comme sûrs simultanés dans https://docs.microsoft.com/en-us/cpp/parallel/concrt/reference/concurrent-vector-class?view=vs-2019#push_back

Cependant, le code ci-dessous s'affirme. Probablement parce que l'élément est ajouté mais pas encore initialisé.

struct MyData
   {
   explicit MyData()
      {
      memset(arr, 0xA5, sizeof arr);
      }
   std::uint8_t arr[1024];
   };

struct MyVec
   {
   concurrency::concurrent_vector<MyData> v;
   };

auto vector_pushback(MyVec &vec) -> void
   {
   vec.v.push_back(MyData{});
   }

auto vector_loop(MyVec &vec) -> void
   {
   MyData myData;
   for (auto it = vec.v.begin(); it != vec.v.end(); ++it)
      {
      auto res = memcmp(&(it->arr), &(myData.arr), sizeof myData.arr);
      assert(res == 0);
      }
   }

int main()
{
   auto vec = MyVec{};
   auto th_vec = std::vector<std::thread>{};
   for (int i = 0; i < 1000; ++i)
      {
      th_vec.emplace_back(vector_pushback, std::ref(vec));
      th_vec.emplace_back(vector_loop, std::ref(vec));
      }

   for(auto &th : th_vec)
      th.join();

    return 0;
}
pidgun
la source

Réponses:

2

Selon les documents , il devrait être sûr de les ajouter à un concurrency::concurrent_vectorcertain temps en les itérant car les éléments ne sont pas réellement stockés de manière contiguë en mémoire comme std::vector:

Un concurrent_vectorobjet ne déplace pas ses éléments lorsque vous l'ajoutez ou le redimensionnez. Cela permet aux pointeurs et aux itérateurs existants de rester valides pendant les opérations simultanées.

Cependant, en regardant la mise push_backen œuvre réelle de VS2017, je vois ce qui suit, qui, je pense, n'est pas thread-safe:

iterator push_back( _Ty &&_Item )
{
    size_type _K;
    void *_Ptr = _Internal_push_back(sizeof(_Ty), _K);
    new (_Ptr) _Ty( std::move(_Item));
    return iterator(*this, _K, _Ptr);
}

Je dois spéculer _Internal_push_backici, mais je parierais qu'il alloue de la mémoire brute pour stocker l'élément (et pointe le dernier élément vers ce nouveau nœud) afin que la ligne suivante puisse utiliser l'emplacement nouveau. J'imagine que _Internal_push_backc'est thread-safe en interne, mais je ne vois aucune synchronisation se produire avant la nouvelle mise en place. Cela signifie que ce qui suit est possible:

  • la mémoire est obtenue et le noeud est "présent" (mais l'emplacement nouveau n'est pas arrivé)
  • le thread en boucle rencontre ce nœud et effectue memcmppour découvrir qu'ils ne sont pas égaux
  • emplacement nouveau arrive.

Il y a certainement une condition de course ici. Je peux reproduire spontanément le problème, d'autant plus que j'utilise plus de threads.

Je vous recommande d'ouvrir un ticket avec le support Microsoft sur celui-ci.

AndyG
la source