¿Es seguro para la concurrencia llamar concurrencia :: concurrent_vector :: push_back mientras itera sobre ese concurrent_vector en otro hilo?

9

push_back , begin , end se describen como concurrentes seguros en https://docs.microsoft.com/en-us/cpp/parallel/concrt/reference/concurrent-vector-class?view=vs-2019#push_back

Sin embargo, el siguiente código está afirmando. Probablemente porque el elemento se agrega pero aún no se ha inicializado.

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
fuente

Respuestas:

2

Según los documentos , debería ser seguro agregar un concurrency::concurrent_vectortiempo iterando sobre él porque los elementos no se almacenan contiguamente en la memoria como std::vector:

Un concurrent_vectorobjeto no reubica sus elementos cuando lo agrega o cambia su tamaño. Esto permite que los punteros e iteradores existentes sigan siendo válidos durante las operaciones concurrentes.

Sin embargo, al observar la implementación real de push_backVS2017, veo lo siguiente, que no creo que sea seguro para subprocesos:

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);
}

Tengo que especular _Internal_push_backaquí, pero apostaría a que asigna memoria en bruto para almacenar el elemento (y apunta el último elemento hacia este nuevo nodo) para que la siguiente línea pueda usar el emplazamiento nuevo. Me imagino que _Internal_push_backes internamente seguro para subprocesos, sin embargo, no veo ninguna sincronización antes del emplazamiento nuevo. Lo que significa que lo siguiente es posible:

  • se obtiene memoria y el nodo está "presente" (sin embargo, el emplazamiento nuevo no ha sucedido)
  • el hilo de bucle encuentra este nodo y realiza memcmppara descubrir que no son iguales
  • nuevo emplazamiento sucede.

Definitivamente hay una condición de carrera aquí. Puedo reproducir espontáneamente el problema, más aún, mientras más hilos utilizo.

Recomiendo que abra un ticket con soporte de Microsoft en este caso.

AndyG
fuente