std::forward_list::insert_after потокобезопасность

В общем std::forward_list безопасно ли, чтобы несколько потоков вызывали insert_after одновременно, если они гарантированно никогда не вызовут его с одним и тем же итератором позиции? Кажется, это может быть безопасно, учитывая, что вставка гарантированно не делает недействительными другие итераторы, а в контейнере нет метода size(), но, может быть, я что-то упускаю?

Редактировать:

Я написал небольшую тестовую программу для пыток, которая отлично работает в Clang без каких-либо блокировок:

#include <forward_list>
#include <iostream>
#include <thread>
#include <vector>

using List = std::forward_list< int >;
using It = List::const_iterator;

void insertAndBranch (List& list, It it, int depth)
{
    if (depth-- > 0) {
        It newIt = list.insert_after (it, depth);
        std::thread thread0 ([&]{ insertAndBranch (list, it, depth); });
        std::thread thread1 ([&]{ insertAndBranch (list, newIt, depth); });
        thread0.join();
        thread1.join();
    }
}

int main()
{
    List list;
    insertAndBranch (list, list.before_begin(), 8);

    std::vector< It > its;
    for (It it = list.begin(); it != list.end(); ++it) {
        its.push_back (it);
    }
    std::vector< std::thread > threads;
    for (It it : its) {
        threads.emplace_back ([&]{ list.insert_after (it, -1); });
    }
    for (std::thread& thread : threads) {
        thread.join();
    }

    for (int i : list) {
        std::cout << i << ' ';
    }
    std::cout << '\n';
}

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


person atb    schedule 17.02.2017    source источник
comment
Возможный дубликат std::list threading push_back, front, pop_front   -  person peval27    schedule 17.02.2017
comment
Дубликат старый, и все изменилось.   -  person Bo Persson    schedule 17.02.2017
comment
ну, OP не определяет стандарт С++.   -  person peval27    schedule 17.02.2017
comment
С++ 14, я только что добавил тег.   -  person atb    schedule 17.02.2017
comment
Каждый раз, когда у вас есть несколько потоков, и один или несколько из них являются записывающими в общую переменную, вам нужна синхронизация. Если нет, у вас есть гонка данных, которая является неопределенным поведением.   -  person NathanOliver    schedule 17.02.2017
comment
@NathanOliver - я думаю, что это чрезмерное упрощение. Например, несколько потоков могут записывать в один и тот же массив, пока они пишут в разные места в этом массиве. Точно так же в моем случае я могу гарантировать, что одновременные вставки никогда не будут выполняться в одной и той же позиции.   -  person atb    schedule 17.02.2017
comment
@NathanOliver Дело в том, что нет общей переменной. См. мой комментарий для получения дополнительной информации.   -  person Shmuel H.    schedule 17.02.2017


Ответы (2)


В общем std::list безопасно ли, чтобы несколько потоков вызывали вставку одновременно, если они гарантированно никогда не вызовут ее с одним и тем же итератором позиции?

Нет. Это было бы небезопасно... (Несмотря ни на что).

Для вставки в std::list потребуется доступ к previous node и next node к позиции итератора и size списка.

Даже если позиции далеко друг от друга, тот простой факт, что std::list::size() должен быть постоянным временем (C++11). Это означает, что каждая вставка будет обновлять std::list::size().


Редактировать:

В общем std::forward_list безопасно ли, чтобы несколько потоков вызывали insert_after одновременно, если они гарантированно никогда не вызовут его с одним и тем же итератором позиции?

Это небезопасно. и не рекомендуется. Ни один контейнер STL не был разработан с защитой потоков.


В любом случае, давайте предположим некоторые основные гарантии, давайте предположим очень простую версию std::forward_list: insert_after изменяет узел, на который указывает ваш итератор, так что узел теперь указывает на недавно вставленный узел, а вновь вставленный узел указывает на следующий узел. Таким образом, это будет «безопасно» только в том случае, если итераторы находятся как минимум в двух узлах друг от друга, а ваш распределитель потокобезопасен.

Иллюстрация:

Initial Forward_list
A -> B -> C -> D -> E

Insert K after C
A -> B -> C x D -> E
           \ /
            K

Как видите, C читается и модифицируется. D может быть прочитано, а K вставлено.


Что касается того, почему я сказал «как минимум два узла», давайте рассмотрим случай с одним узлом: предположим, что два потока хотят вставить K после C и M после D соответственно:

Initial Forward_list
A -> B -> C -> D -> E

Insert K after C
A -> B -> C x D x E
           \ / \ /
            K   M

Из cppreference:

Когда оценка выражения записывает в ячейку памяти, а другая оценка читает или изменяет ту же ячейку памяти, говорят, что выражения конфликтуют.

person WhiZTiM    schedule 17.02.2017
comment
На самом деле я использую std::forward_list, а не std::list в своем коде. Я написал вопрос, используя std::list для простоты, но сейчас я изменил его. Интересно, что у std::forward_list нет метода определения размера, так что это изменит ваш ответ? - person atb; 17.02.2017
comment
В двусвязном списке вы правы, но в односвязном списке единственные два узла, которые нуждаются в модификации, — это узел, на который указывает аргумент position, и вновь созданный узел, ни один из которых не будет прочитан или изменен другим потоком. - person atb; 17.02.2017
comment
@atb, я обновил свой ответ, чтобы опровергнуть ваш комментарий (см. Иллюстрацию). Вам все еще нужно быть как минимум в двух узлах. Все равно это не гарантия. Используйте блокировки или другую библиотеку с потокобезопасным списком - person WhiZTiM; 17.02.2017
comment
На вашей первой диаграмме я не думаю, что D будет читаться. Указатель следующего узла будет взят из C и передан K. Я не могу представить, почему эффективная реализация должна читать из D. - person atb; 17.02.2017
comment
@atb, Это тоже возможно, но дело в том, что хорошие вещи могут случиться, и плохие вещи могут случиться в равной степени. Никаких гарантий. И что бы ни делала любая реализация, она правильная, если она соответствует продиктованному наблюдаемому поведению. - person WhiZTiM; 17.02.2017
comment
Я понимаю, что вы говорите, но есть много вещей, которые гарантируются стандартом. В этом случае я не знаю, что гарантировано или нет, но я надеюсь выяснить это окончательно. Мне придется принять значительный удар по производительности, если мне нужно использовать блокировку, а мой тестовый код работает без нее. - person atb; 17.02.2017
comment
@atb, я только что разговаривал по электронной почте с Говардом Хиннантом по этому вопросу. Хотя стандарт просто не гарантирует этого, Но он также не может придумать, как это могло бы потерпеть неудачу.... С реализация libcxx, я думаю, вы можете идти, если вы можете выполнить свои собственные гарантии. - person WhiZTiM; 17.02.2017

Даже операции read не являются потокобезопасными, когда другой поток записывает в список (см. здесь для дальнейших объяснений); Итак: множественные записи не являются потокобезопасными:

Хотя чтение из списка без блокировки не приведет к повреждению списка, если список будет изменен во время чтения из него другим потоком, любой поток может быть поврежден (т. е. произойдет сбой или будут получены неверные результаты).

Вам нужна какая-то блокировка. Период.

person GhostCat    schedule 17.02.2017
comment
Вы можете быть правы, но мне было бы интересно узнать, почему в этом конкретном случае. - person atb; 17.02.2017
comment
Это не так просто. Если, например, у нас есть форвард-список с одним элементом, то: чтение его данных (a = l.begin()->d;) из одного потока и добавление элемента в другой (l.insert_after (l.begin(), X);) не должно вызывать состояние гонки. Это связано с тем, что в обоих потоках нет доступа к общим данным: первый читает только begin и begin->d, а второй записывает в begin->next. - person Shmuel H.; 17.02.2017
comment
@ШмуэльХ. Насколько я знаю, нет гарантии, что begin является потокобезопасным, если вы не вызовете версию const. begin() может что-то сделать (было бы безумием, если бы он это сделал, но я думаю, что стандарт это позволяет), и теперь у вас есть гонка данных на вызовах begin. - person NathanOliver; 17.02.2017
comment
На самом деле я использую std::forward_list, а не std::list в своем коде. Сначала я написал вопрос, используя std::list для простоты, но теперь я изменил его. Ответ, который вы цитируете, относится к std::list, это изменит ваш ответ здесь? Если нет, можете ли вы объяснить, почему? - person atb; 17.02.2017
comment
@NathanOliver К счастью, это не так. - person T.C.; 17.02.2017
comment
@Т.С. О, классно. Не знал, что они добавили это. Спасибо. - person NathanOliver; 17.02.2017
comment
@atb В стандарте говорится, что, за некоторыми исключениями (связанными в моем комментарии выше), считается, что неконстантные функции-члены изменяют каждую часть вызываемого объекта в целях оценки наличия гонок данных. В качестве более конкретного примера, реализации режима отладки могут иметь дополнительные структуры учета, в которые insert_after может выполнять запись без синхронизации. - person T.C.; 17.02.2017
comment
@Т.С. -- Наверное, я надеялся, что кто-нибудь скажет мне, что это одно из тех исключений. Ни одна из альтернатив (блокировка или запись собственной структуры данных) не особенно привлекательна... но я допускаю, что у меня может не быть выбора. - person atb; 17.02.2017