简体   繁体   English

调整原子向量的大小?

[英]Resize a vector of atomic?

How to resize a vector of atomics ?如何调整原子向量的大小?

As an example, the following code does not compile:例如,以下代码无法编译:

#include <iostream>
#include <vector>
#include <atomic>

int main()
{
    std::vector<std::atomic<int>> v;
    v.resize(1000); // Problem here!
    v[0] = 1;
    return 0;
}

Error:错误:

In file included from /usr/local/gcc-4.8.1/include/c++/4.8.1/vector:62:0,
                 from main.cpp:2:
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_construct.h: In instantiation of ‘void std::_Construct(_T1*, _Args&& ...) [with _T1 = std::atomic<int>; _Args = {std::atomic<int>}]’:
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_uninitialized.h:75:53:   required from ‘static _ForwardIterator std::__uninitialized_copy<_TrivialValueTypes>::__uninit_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = std::move_iterator<std::atomic<int>*>; _ForwardIterator = std::atomic<int>*; bool _TrivialValueTypes = false]’
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_uninitialized.h:117:41:   required from ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = std::move_iterator<std::atomic<int>*>; _ForwardIterator = std::atomic<int>*]’
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_uninitialized.h:258:63:   required from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = std::move_iterator<std::atomic<int>*>; _ForwardIterator = std::atomic<int>*; _Tp = std::atomic<int>]’
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_uninitialized.h:281:69:   required from ‘_ForwardIterator std::__uninitialized_move_if_noexcept_a(_InputIterator, _InputIterator, _ForwardIterator, _Allocator&) [with _InputIterator = std::atomic<int>*; _ForwardIterator = std::atomic<int>*; _Allocator = std::allocator<std::atomic<int> >]’
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/vector.tcc:556:42:   required from ‘void std::vector<_Tp, _Alloc>::_M_default_append(std::vector<_Tp, _Alloc>::size_type) [with _Tp = std::atomic<int>; _Alloc = std::allocator<std::atomic<int> >; std::vector<_Tp, _Alloc>::size_type = long unsigned int]’
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_vector.h:667:41:   required from ‘void std::vector<_Tp, _Alloc>::resize(std::vector<_Tp, _Alloc>::size_type) [with _Tp = std::atomic<int>; _Alloc = std::allocator<std::atomic<int> >; std::vector<_Tp, _Alloc>::size_type = long unsigned int]’
main.cpp:8:17:   required from here
/usr/local/gcc-4.8.1/include/c++/4.8.1/bits/stl_construct.h:75:7: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
     { ::new(static_cast<void*>(__p)) _T1(std::forward<_Args>(__args)...); }
       ^
In file included from main.cpp:3:0:
/usr/local/gcc-4.8.1/include/c++/4.8.1/atomic:601:7: error: declared here
       atomic(const atomic&) = delete;
       ^

You can't...你不能...

A std::atomic<T> is neither copy/move-constructible, nor can you assign one std::atomic<T> to another; std::atomic<T>既不能复制/移动构造,也不能将一个std::atomic<T>分配给另一个; this means that it doesn't have the requirements to use std::vector<...>::resize (size_type) .这意味着它没有使用std::vector<...>::resize (size_type)的要求。

23.3.6.2 vector constructors, copy, and assignment [vector.const] 23.3.6.2 vector构造函数、复制和赋值[vector.const]

 void resize (size_type sz);

Requires : T shall be CopyInsertable into *this .要求T应可 CopyInsertable 到*this中。

Note : std::vector::resize (size_type sz, T const& init) isn't applicable either since that requires T to also be MoveInsertable .注意std::vector::resize (size_type sz, T const& init)也不适用,因为这要求T也是MoveInsertable


Proposed resolution提议的决议

You will need to use some other container-type which doesn't require already constructed data to be moved , copied , or copy/move assigned , upon modifying the elements already stored inside.在修改已存储在其中的元素时,您将需要使用一些其他容器类型,这些容器类型不需要移动复制复制/移动分配的已构建数据。

You could also define a wrapper around your std::atomic that fakes copy/moves/assigns, but actually just shallow read/writes the value of the underlying atomic .您还可以在std::atomic周围定义一个包装器,该包装器伪造复制/移动/分配,但实际上只是浅读/写底层atomic的值。

std::deque<std::atomic<int> > can be resized. std::deque<std::atomic<int> >可以调整大小。 Note that std::deque can also provide O(1) random access just like std::vector .请注意, std::deque也可以像std::vector一样提供O(1)随机访问。

Example:例子:

#include <iostream>
#include <deque>
#include <atomic>

void print(std::deque<std::atomic<int> >& q) {
    for (auto& x : q) {
        std::cout << x.load() << ',';
    }
    std::cout << std::endl;
}

int main() {
    std::deque<std::atomic<int> > q;

    q.emplace_back(1);
    q.emplace_back(2);
    // 1,2
    print(q);

    q.resize(4);
    // 1,2,0,0
    print(q);

    q.resize(2);
    // 1,2
    print(q);

    //q.resize(5, 233);
    while (q.size() < 5) {
        q.emplace_back(233);
    }
    // 1,2,233,233,233,
    print(q);

    return 0;
}

resize with value does not work for deque because it requires copying the provided value, while std::atomic is not copyable. resize with value 不适用于deque ,因为它需要复制提供的值,而std::atomic不可复制。 However, it can be achieved with repetitive emplace_back , which does not introduce much performance penalty because the expansion of deque does not require relocation of the existing data.但是,它可以通过重复emplace_back来实现,这不会引入太多的性能损失,因为deque的扩展不需要重新定位现有数据。

References参考

https://en.cppreference.com/w/cpp/container/deque https://en.cppreference.com/w/cpp/container/deque

https://stackoverflow.com/a/37870930/13688160 https://stackoverflow.com/a/37870930/13688160

Invalidating references to std::atomic objects is a recipe for disaster if they're potentially being accessed by multiple threads at the time.如果它们可能同时被多个线程访问,那么使对std::atomic对象的引用无效是灾难的根源。 If you need that, you can potentially use std::dequeue< std::atomic<int> > which allocates piecewise so it never moves an element after allocating.如果需要,您可以使用std::dequeue< std::atomic<int> >分段分配,因此它在分配后永远不会移动元素。 (Note that growing a dequeue invalidates iterators , but not references to any individual element.) (请注意,增加出队会使迭代器无效,但不会引用任何单个元素。)

But the std::dequeue object itself (and its internal linked list of chunks) is still not atomic , so you can't safely resize while other threads are doing things like q[i].store(v, memory_order_release) .但是std::dequeue对象本身(及其内部的块链表)仍然不是 atomic ,因此当其他线程正在执行诸如q[i].store(v, memory_order_release)类的事情时,您无法安全地调整大小。 Only if you've just passed pointers to certain elements to other threads.仅当您刚刚将指向某些元素的指针传递给其他线程时。 (Elements aren't contiguous so there's no guarantee that &q[i+1] == &q[i]+1 , so you can't necessarily give other threads access to whole ranges.) (元素不连续,因此无法保证&q[i+1] == &q[i]+1 ,因此您不一定能让其他线程访问整个范围。)


If you have a std::vector whose data is sometimes accessed by multiple threads, but other times the whole vector is owned exclusively by one thread for operations like resize, this is a use-case for C++20 std::atomic_ref .如果您有一个std::vector其数据有时由多个线程访问,但有时整个向量由一个线程独占用于调整大小等操作,这是C++20 std::atomic_ref的用例

Use std::vector<int> v .使用std::vector<int> v When no other threads could accessing elements in it, or calling functions like v.size() , you can resize it as normal.当没有其他线程可以访问其中的元素或调用v.size()之类的函数时,您可以正常调整它的大小。

While no threads are calling functions that could change the control block at all, you can have any number of threads doing things like虽然没有线程调用可以更改控制块的函数,但您可以让任意数量的线程执行以下操作

   std::vector<int> v;
   // get some elements into v, start some threads
   static_assert(alignof(v[i]) >= std::atomic_ref<int>::required_alignment, "vector elements not guaranteed aligned enough for atomic_ref");

 // while no other threads could be calling v.push_back or any other non-const function except element access
   std::atomic_ref<int>  vi( v[i] );
   vi.store(2, std::memory_order_release);

   vi.fetch_add(1, std::memory_order_relaxed);  // and/or this

   auto tmp = vi.load(std::memory_order_acquire);  // and/or this

(The intended use-case for atomic_ref is to construct a new atomic_ref object every time you want to use it. Don't keep one as a class member anyway, just a local variable.) atomic_ref的预期用例是每次要使用它时都构造一个新的atomic_ref对象。无论如何不要将其保留为类成员,而只是一个局部变量。)

Even a .push_back that doesn't reallocate would not be safe, though.但是,即使是不重新分配的.push_back也不安全。 That will change the members of the std::vector<int> object itself, which aren't atomic.这将改变std::vector<int>对象本身的成员,它们不是原子的。 (And there's no way to make them atomic). (而且没有办法让它们成为原子的)。 So this would be data race UB because other threads are accessing the v object, not just using an int * they already got from v.data() .所以这将是数据竞争 UB,因为其他线程正在访问v对象,而不仅仅是使用他们已经从v.data()获得的int *


How this would break (or not) if you used it wrong如果您使用错误,这将如何破坏(或不破坏)

In practice (but not guaranteed by the standard) it's likely safe for one thread to be doing .push_back() as long as you're below the current capacity, as long as other threads are only indexing into it.在实践中(但标准保证)只要您低于当前容量,一个线程执行.push_back()可能是安全的,只要其他线程仅对其进行索引。 A typical implementation will keep a pointer to .data() , a pointer to the end of the reservation, and a pointer to the end of the in-use area, so .size() is return end()-data() .一个典型的实现会保留一个指向.data()的指针、一个指向预留结束的指针和一个指向使用区域结束的指针,因此.size()return end()-data() .push_back() will only modify the end pointer, but operator[] will only read the .data() pointer. .push_back()只会修改结束指针,但operator[]只会读取.data()指针。

If the library header works that way, then you don't even have data race UB in the actual C++ code seen by the compiler itself.如果库头以这种方式工作,那么在编译器本身看到的实际 C++ 代码中甚至没有数据竞争 UB。 ISO C++ doesn't guarantee the internals, but this is a common implementation. ISO C++ 不保证内部,但这是一个常见的实现。

A debug built might also do bounds checking and read the .end() pointer in operator[] like at() does;构建的调试也可能像at()一样进行边界检查并读取operator[]中的.end()指针; then you'd have an actual data race.那么你就会有一场真正的数据竞赛。 In practice that's also unlikely to be fatal;在实践中,这也不太可能是致命的。 most implementations for most machines don't have tearing within a naturally-aligned pointer-width object, so readers are going to see either an old or new value.大多数机器的大多数实现在自然对齐的指针宽度对象中没有撕裂,因此读者将看到旧值或新值。 But this is definitely not guaranteed to be portable, and race-detectors will potentially notice.但这绝对不能保证是便携的,并且比赛检测器可能会注意到。

But as long as you don't expect other threads to actually be accessing newly-pushed vector elements that have appeared since synchronizing with the pushing thread, it will happen to work on typical machines.但是,只要您不期望其他线程实际上正在访问自与推送线程同步后出现的新推送的向量元素,它就会碰巧在典型的机器上工作。

I would not recommend this nasty hack of actually pushing while other threads are writing .我不建议在其他线程正在写入时实际推送这种讨厌的 hack I describe the details of what would happen just for educational purposes, not as justification for doing so.我描述将发生的事情的细节只是为了教育目的,而不是作为这样做的理由。 If you do this anyway and it breaks, you get to keep both pieces.如果你无论如何都这样做并且它坏了,你可以保留这两部分。

Obviously actually reallocating could lead to crashes, if a thread got a reference to v[i] and then that reference was invalidated before use.显然,如果线程获得对v[i]的引用,然后该引用在使用前无效,那么实际上重新分配可能会导致崩溃。 And/or stale data or missed updates from one thread modifying an element after it had already been copied.和/或过时的数据或来自一个线程在复制元素后修改元素的错过更新。


Feel free to roll your own container with a size or end member used by one thread, with other threads only using the begin / data member.随意滚动您自己的容器,其中一个线程使用的 size 或 end 成员,其他线程仅使用 begin / data 成员。 With no conditional reallocate, it would even Just Work for std::atomic<int> , or if you want reallocation while one thread has exclusive ownership, still use atomic_ref<T> as above.如果没有条件重新分配,它甚至可以为std::atomic<int>工作,或者如果您想要在一个线程拥有独占所有权时重新分配,仍然使用atomic_ref<T>如上所述。 Or you could design the push function to take a T , not an atomic<T> , or just have it work like emplace_back.或者你可以设计push函数来接受一个T ,而不是一个atomic<T> ,或者让它像 emplace_back 一样工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM