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

Безопасный параллельный доступ только для чтения к контейнеру STL

Я хочу получить доступ к контейнеру на основе STL только для чтения из parallel работающих потоков. Без использования каких-либо пользовательских блокировок. Основой следующего кода является С++ 11 с правильной реализацией стандарта.

http://gcc.gnu.org/onlinedocs/libstdc++/manual/using_concurrency.html
http://www.sgi.com/tech/stl/thread_safety.html
http://www.hpl.hp.com/personal/Hans_Boehm/c++mm/threadsintro.html
http://www.open-std.org/jtc1/sc22/wg21/ (текущий проект или N3337, который по существу является С++ 11 с незначительными ошибками и опечатками)

23.2.2 Гонки данных контейнера [container.requirements.dataraces]

В целях избежания расследований данных (17.6.5.9) реализации рассмотрим следующие функции: const, begin, end, rbegin, rend, front, back, data, find, lower_bound, upper_bound, equal_range, в и, за исключением ассоциативных или неупорядоченных ассоциативных контейнеров, Оператор [].

Несмотря на (17.6.5.9), необходимы реализации чтобы избежать гонок данных, когда содержимое содержащегося объекта в различные элементы в одной и той же последовательности, за исключением вектора <bool> , измененный одновременно.

[Примечание: для вектора <int> x с размером больше чем один, x [1] = 5 и * x.begin() = 10 могут выполняться одновременно без гонки данных, но x [0] = 5 и * x.begin() = 10 выполнено одновременно может привести к гонке данных. В качестве исключения для общего правило, для вектора <bool> y, y [0] = true может участвовать в гонке с y [1] = true. - конечная нота]

и

17.6.5.9 Уклонение от гонки данных [res.on.data.races] 1 В этом разделе указаны требования, которые должны выполняться реализацией для предотвращения данных рас (1.10). Каждая стандартная библиотечная функция должна соответствовать каждому если не указано иное. Реализации могут данных в случаях, отличных от указанных ниже.

2 Стандарт С++ библиотечная функция не должна прямо или косвенно обращаться к объектам (1.10), доступный для потоков, отличных от текущей доступ к объектам осуществляется прямо или косвенно с помощью функций аргументы, включая это.

3 Стандартная библиотечная функция С++ должна не прямо или косвенно изменять объекты (1.10), доступные по потокам кроме текущего потока, если только объекты не доступны напрямую или косвенно через функции неконстантных аргументов, включая это.

4 [Примечание: это означает, например, что реализации не могут использовать статический объект для внутренних целей без синхронизации потому что это может привести к гонке данных даже в программах, которые не явно обмениваются объектами между потоками. - конечная нота]

5 Стандартная библиотечная функция С++ не должна косвенно обращаться к объектам доступный через его аргументы или через элементы его контейнера аргументы, за исключением вызова функций, требуемых по его спецификации на этих элементах контейнера.

6 Операции с итераторами, полученные вызов стандартного контейнера библиотеки или функции члена строки может доступ к базовому контейнеру, но не должен его изменять. [Примечание: в в частности, операции с контейнерами, которые лишают законных прав итераторов с операциями с итераторами, связанными с этим контейнером. - конец примечание]

7 Реализации могут делиться своими собственными внутренними объектами между потоки, если объекты не видны пользователям и защищены против гонок данных.

8 Если не указано иное, стандартная библиотека С++ функции должны выполнять все операции исключительно в рамках текущего если эти операции имеют видимые эффекты (1.10) - пользователей.

9 [Примечание: это позволяет реализациям распараллеливать операции если нет видимых побочных эффектов. - конечная нота]

Заключение
Контейнеры не являются потокобезопасными! Но безопасно вызывать функции const для контейнеров из нескольких параллельных потоков. Таким образом, можно выполнять операции только для чтения из параллельных потоков без блокировки. Я прав?

Я притворяюсь, что их не существует какая-либо неисправная реализация, и каждая реализация стандарта С++ 11 верна.

Пример:

// concurrent thread access to a stl container
// g++ -std=gnu++11 -o p_read p_read.cpp -pthread -Wall -pedantic && ./p_read
#include <iostream>
#include <iomanip>
#include <string>
#include <unistd.h>

#include <thread>
#include <mutex>

#include <map>

#include <cstdlib>
#include <ctime>
using namespace std;

// new in C++11
using str_map = map<string, string>;

// thread is new in C++11
// to_string() is new in C++11

mutex m;
const unsigned int MAP_SIZE = 10000;

void fill_map(str_map& store) {
    int key_nr;
    string mapped_value;
    string key;

    while (store.size() < MAP_SIZE) {
        // 0 - 9999
        key_nr = rand() % MAP_SIZE;

        // convert number to string
        mapped_value = to_string(key_nr);
        key = "key_" + mapped_value;

        pair<string, string> value(key, mapped_value);
        store.insert(value);
    }
}

void print_map(const str_map& store) {
    str_map::const_iterator it = store.begin();

    while (it != store.end()) {
        pair<string, string> value = *it;
        cout << left << setw(10) << value.first << right << setw(5) << value.second << "\n";
        it++;   
    }
}

void search_map(const str_map& store, int thread_nr) {
    m.lock();
    cout << "thread(" << thread_nr << ") launched\n";
    m.unlock();

    // use a straight search or poke around random
    bool straight = false;
    if ((thread_nr % 2) == 0) {
        straight = true;
    }

    int key_nr;
    string mapped_value;
    string key;
    str_map::const_iterator it;

    string first;
    string second;

    for (unsigned int i = 0; i < MAP_SIZE; i++) {

        if (straight) {
            key_nr = i;
        } else {
            // 0 - 9999, rand is not thread-safe, nrand48 is an alternative             
            m.lock();
            key_nr = rand() % MAP_SIZE;
            m.unlock();
        }

        // convert number to string
        mapped_value = to_string(key_nr);
        key = "key_" + mapped_value;

        it = store.find(key);

        // check result
        if (it != store.end()) {
            // pair
            first = it->first;
            second = it->second;

            // m.lock();
            // cout << "thread(" << thread_nr << ") " << key << ": "
            //      << right << setw(10) << first << setw(5) << second << "\n"; 
            // m.unlock();

            // check mismatch
            if (key != first || mapped_value != second) {
                m.lock();
                cerr << key << ": " << first << second << "\n"
                     << "Mismatch in thread(" << thread_nr << ")!\n";
                exit(1);

                // never reached
                m.unlock();
            }
        } else {
            m.lock();
            cerr << "Warning: key(" << key << ") not found in thread("
                 << thread_nr << ")\n";
            exit(1);

            // never reached
            m.unlock();
        }
    }
}

int main() {
    clock_t start, end;
    start = clock();

    str_map store;
    srand(0);

    fill_map(store);
    cout << "fill_map finished\n";

    // print_map(store);
    // cout << "print_map finished\n";

    // copy for check
    str_map copy_store = store;

    // launch threads
    thread t[10];
    for (int i = 0; i < 10; i++) {
        t[i] = thread(search_map, store, i);
    }

    // wait for finish
    for (int i = 0; i < 10; i++) {
        t[i].join();
    }
    cout << "search_map threads finished\n";

    if (store == copy_store) {
        cout << "equal\n";
    } else {
        cout << "not equal\n";
    }


    end = clock();
    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

Этот код можно скомпилировать с помощью GCC 4.7 и отлично работает на моей машине.

$echo $?
$ 0

4b9b3361

Ответ 1

Для расчёта данных из спецификации С++ 11 в разделах 1.10/4 и 1.10/21 требуется, по меньшей мере, два потока с неатомным доступом к одному и тому же набору мест памяти, два потока не синхронизированы с относится к доступу к набору мест памяти, и по крайней мере один поток записывается в или изменяет элемент в наборе мест памяти. Итак, в вашем случае, если потоки только читают, вы в порядке... по определению, поскольку ни один из потоков не записывается в один и тот же набор мест памяти, нет расчётов данных, даже если нет явного механизма синхронизации между потоки.

Ответ 2

Да, вы правы. Вы в безопасности, пока поток, заполняющий вектор, заканчивается, прежде чем начнутся темы чтения. Недавно был похожий вопрос.