Безопасно ли для параллелизма вызывать concurrency :: concurrent_vector :: push_back при переборе этого concurrent_vector в другом потоке?

9

push_back , begin , end описаны как параллельный сейф в https://docs.microsoft.com/en-us/cpp/parallel/concrt/reference/concurrent-vector-class?view=vs-2019#push_back

Однако приведенный ниже код утверждает. Возможно, потому что элемент добавлен, но еще не инициализирован.

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
источник

Ответы:

2

Согласно документам , это должно быть безопасно добавить к concurrency::concurrent_vectorитерированию, потому что элементы на самом деле не хранятся непрерывно в памяти, как std::vector:

concurrent_vectorОбъект не перемещался его элементов , когда вы добавляете к нему или изменить его размер. Это позволяет существующим указателям и итераторам оставаться действительными во время параллельных операций.

Однако, глядя на фактическую реализацию push_backв VS2017, я вижу следующее, которое я не считаю поточно-ориентированным:

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

Я должен предположить, что _Internal_push_backздесь, но я бы поспорил, что он выделяет необработанную память для хранения элемента (и указывает последний элемент на этот новый узел), чтобы в следующей строке можно было использовать empposition new. Я полагаю, что _Internal_push_backэто внутренне поточно-ориентированный, но я не вижу никакой синхронизации до нового размещения. Это означает следующее:

  • память получена, а узел "присутствует" (пока не произошло новое размещение)
  • зацикливающийся поток встречает этот узел и выполняет memcmpобнаружение, что они не равны
  • происходит установка нового.

Здесь определенно условия гонки. Я могу самопроизвольно воспроизвести проблему, более того, чем больше тем я использую.

Я рекомендую вам открыть билет с поддержкой Microsoft на этот.

AndyG
источник
1
MSFT обновленная документация github.com/MicrosoftDocs/cpp-docs/commit/...
pidgun