Подтвердить что ты не робот

Почему std:: shared_ptr:: unique() устарел?

Какова техническая проблема с std::shared_ptr::unique(), что является причиной ее устаревания в С++ 17?

Согласно cppreference.com, std::shared_ptr::unique() устарел на С++ 17 как

эта функция устарела с С++ 17, потому что use_count является лишь приближением в многопоточной среде.

Я понимаю, что это верно для use_count() > 1: пока я держу ссылку на него, кто-то другой может одновременно отпустить его или создать новую копию.

Но если use_count() возвращает 1 (это то, что меня интересует при вызове unique()), тогда нет другого потока, который мог бы изменить это значение по-разному, поэтому я ожидал бы, что это должно быть безопасно

if (myPtr && myPtr.unique()) {
    //Modify *myPtr
}

Забастовкa >

Результаты моего собственного поиска:

Я нашел этот документ: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html, который предлагает отсрочку в ответ на С++ 17 CD-комментарий CA 14, но я не смог найти этот комментарий сам,

В качестве альтернативы, в этом документе предлагается добавить некоторые примечания, включая следующее:

Примечание. Если несколько потоков могут повлиять на возвращаемое значение use_count(), результат следует рассматривать как приблизительный. В частности, use_count() == 1 не означает, что доступ через ранее уничтоженный shared_ptr в любом смысле завершен. - конец примечания

Я понимаю, что это может быть в случае, когда в настоящее время указывается способ use_count() (из-за отсутствия гарантированной синхронизации), но почему разрешение не просто указать такую ​​синхронизацию и, следовательно, сделать вышеуказанный шаблон безопасным? Если бы существовало фундаментальное ограничение, которое не допускало бы такую ​​синхронизацию (или сделало бы ее чрезвычайно дорогостоящей), то как можно правильно реализовать деструктор?

Обновление:

Я упустил из виду очевидный случай, представленный @alexeykuzmin0 и @rubenvb, потому что до сих пор я использовал unique() только для экземпляров shared_ptr, которые не были доступны для других потоков. Поэтому не было никакой опасности, что этот конкретный случай будет скопирован радикально.

Мне все равно было бы интересно узнать, что такое CA 14, потому что я считаю, что все мои варианты использования для unique() будут работать, пока гарантируется синхронизация с тем, что происходит с разными экземплярами shared_ptr на других потоки. Так что это по-прежнему кажется полезным инструментом для меня, но я могу упустить что-то фундаментальное здесь.

Чтобы проиллюстрировать то, что я имею в виду, рассмотрим следующее:

class MemoryCache {
public:
    MemoryCache(size_t size)
        : _cache(size)
    {
        for (auto& ptr : _cache) {
            ptr = std::make_shared<std::array<uint8_t, 256>>();
        }
    }

    // the returned chunk of memory might be passed to a different thread(s),
    // but the function is never accessed from two threads at the same time
    std::shared_ptr<std::array<uint8_t,256>> getChunk()
    {
        auto it = std::find_if(_cache.begin(), _cache.end(), [](auto& ptr) { return ptr.unique(); });
        if (it != _cache.end()) {
            //memory is no longer used by previous user, so it can be given to someone else
            return *it;
        } else {
            return{};
        }
    }
private:
    std::vector<std::shared_ptr<std::array<uint8_t, 256>>> _cache;
};

Что-то не так с ним (если unique() фактически синхронизируется с деструкторами других копий)?

4b9b3361

Ответ 1

Я думаю, что P0521R0 решает потенциальную гонку данных, злоупотребляя shared_ptr в качестве межпоточной синхронизации. Он говорит, что use_count() возвращает недостоверное значение refcount, и поэтому функция-член unique() будет бесполезной при многопоточности.

int main() {
  int result = 0;
  auto sp1 = std::make_shared<int>(0);  // refcount: 1

  // Start another thread
  std::thread another_thread([&result, sp2 = sp1]{  // refcount: 1 -> 2
    result = 42;  // [W] store to result
    // [D] expire sp2 scope, and refcount: 2 -> 1
  });

  // Do multithreading stuff:
  //   Other threads may concurrently increment/decrement refcounf.

  if (sp1.unique()) {      // [U] refcount == 1?
    assert(result == 42);  // [R] read from result
    // This [R] read action cause data race w.r.t [W] write action.
  }

  another_thread.join();
  // Side note: thread termination and join() member function
  // have happens-before relationship, so [W] happens-before [R]
  // and there is no data race on following read action.
  assert(result == 42);
}

Функция-член unique() не имеет никакого эффекта синхронизации, и не происходит-до отношения от [D] shared_ptr деструктора к [U], вызывающего unique(). Поэтому мы не можем ожидать отношения [W] ⇒ [D] ⇒ [U] ⇒ [R] и [W] ⇒ [R]. ('⇒' означает, что происходит до отношения).


EDITED: Я обнаружил две связанные проблемы LWG; LWG2434. shared_ptr:: use_count() эффективен, LWG2776. shared_ptr unique() и use_count(). Это просто спекуляция, но комитет РГ21 отдает приоритет существующей реализации стандартной библиотеки С++, поэтому они кодируют ее поведение в С++ 1z.

LWG2434 цитата (внимание мое):

shared_ptr и weak_ptr имеют примечания, что их use_count() может быть неэффективным. Это попытка подтвердить рефлексированные реализации (например, которые могут использоваться интеллектуальными указателями Loki). Однако не существует реализаций shared_ptr, которые используют reflinking, особенно после того, как С++ 11 узнал о существовании многопоточности. Каждый использует атомарные refcounts, поэтому use_count() - это просто атомная нагрузка.

LWG2776 цитата (внимание мое):

Удаление ограничения "только отладки" для use_count() и unique() в shared_ptr LWG 2434 ввело ошибку. Для того, чтобы unique() создавал полезное и надежное значение, ему требуется предложение synchronize, чтобы убедиться, что предыдущие обращения через другую ссылку видны успешному вызывающему абоненту unique(). Многие текущие реализации используют расслабленную нагрузку и не предоставляют эту гарантию, так как она не указана в стандарте. Для использования отладки/подсказок это было нормально. Без этого спецификация неясна и, вероятно, вводит в заблуждение.

[...]

Я бы предпочел указать use_count() как только предоставление недостоверного намека на фактическое количество (другой способ сказать только отладки). Или осудите его, как предложил JF. Мы не можем сделать use_count() надежным, не добавляя существенно большего количества ограждений. Мы действительно не хотим, чтобы кто-то ждал use_count() == 2, чтобы определить, что еще один поток получил это далеко. И, к сожалению, я не думаю, что мы сейчас говорим что-нибудь, чтобы было ясно, что ошибка.

Это означало бы, что use_count() обычно использует memory_order_relaxed, а уникальность не указана и не реализована в терминах use_count().

Ответ 2

Рассмотрим следующий код:

// global variable
std::shared_ptr<int> s = std::make_shared<int>();

// thread 1
if (s && s.unique()) {
    // modify *s
}

// thread 2
auto s2 = s;

Здесь у нас есть классическое условие гонки: s2 может (или не может) быть создано как копия s в потоке 2, а поток 1 находится внутри if.

unique() == true означает, что никто не имеет shared_ptr, указывающего на одну и ту же память, но не означает, что никакие другие потоки не имеют прямого доступа к начальному shared_ptr напрямую или через указатели или ссылки.

Ответ 3

Для вашего удовольствия: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

В этом документе содержатся комментарии Национального банка (NB) для собрания Иссакуа. CA 14 гласит:

Удаление ограничения "только отладки" для use_count() и unique() в shared_ptr ввел ошибку: для того, чтобы уникальный() был производят полезную и надежную ценность, для этого требуется условие синхронизации убедитесь, что предыдущие обращения через другую ссылку видны успешный вызывающий объект unique(). Многие современные реализации используют ослабленной нагрузки и не обеспечивают эту гарантию, поскольку она не указана в Стандарте. Для использования отладки/подсказок это было нормально. Без этого спецификация неясна и вводит в заблуждение.