Правильно ли эта реализация конверта использует атомарность C ++ 11? - PullRequest
4 голосов
/ 25 сентября 2019

Я написал простой класс 'envelope', чтобы убедиться, что я правильно понимаю атомарную семантику C ++ 11.У меня есть заголовок и полезная нагрузка, где писатель очищает заголовок, заполняет полезную нагрузку, а затем заполняет заголовок возрастающим целым числом.Идея состоит в том, что читатель может затем прочитать заголовок, запомнить полезную нагрузку, снова прочитать заголовок, и, если заголовок тот же, читатель может предположить, что он успешно скопировал полезную нагрузку.Это нормально, что читатель может пропустить некоторые обновления, но это не нормально для них, чтобы получить разорванное обновление (где есть смесь байтов из разных обновлений).Существует только один читатель и один писатель.

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

Есть ли риск того, что memcpy будет переупорядочен с атомарнымхранить / загружать звонки?Или нагрузки могут быть переупорядочены друг с другом?Это никогда не прерывается для меня, но, может быть, мне повезло.

#include <iostream>
#include <atomic>
#include <thread>
#include <cstring>

struct envelope {
    alignas(64) uint64_t writer_sequence_number = 1;
    std::atomic<uint64_t> sequence_number;
    char payload[5000];

    void start_writing()
    {
        sequence_number.store(0, std::memory_order::memory_order_release);
    }

    void publish()
    {
        sequence_number.store(++writer_sequence_number, std::memory_order::memory_order_release);
    }

    bool try_copy(char* copy)
    {
        auto before = sequence_number.load(std::memory_order::memory_order_acquire);
        if(!before) {
            return false;
        }
        ::memcpy(copy, payload, 5000);
        auto after = sequence_number.load(std::memory_order::memory_order_acquire);
        return before == after;
    }
};

envelope g_envelope;

void reader_thread()
{
    char local_copy[5000];
    unsigned messages_received = 0;
    while(true) {
        if(g_envelope.try_copy(local_copy)) {
            for(int i = 0; i < 5000; ++i) {
                // if there is no tearing we should only see the same letter over and over
                if(local_copy[i] != local_copy[0]) {
                    abort();
                }
            }
            if(messages_received++ % 64 == 0) {
                std::cout << "successfully received=" << messages_received << std::endl;
            }
        }
    }
}

void writer_thread()
{
    const char alphabet[] = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ"};
    unsigned i = 0;
    while(true) {
        char to_write = alphabet[i % (sizeof(alphabet)-1)];
        g_envelope.start_writing();
        ::memset(g_envelope.payload, to_write, 5000);
        g_envelope.publish();
        ++i;
    }
}

int main(int argc, char** argv)
{
    std::thread writer(&writer_thread);
    std::thread reader(&reader_thread);

    writer.join();
    reader.join();

    return 0;
}

Ответы [ 2 ]

3 голосов
/ 26 сентября 2019

Это называется секлок;он имеет гонку данных просто из-за конфликтующих вызовов memset и memcpy.Были предложения по созданию memcpy -подобного средства для правильного кодирования такого рода; последний вряд ли появится до C ++ 26 (даже если одобрен).

1 голос
/ 27 сентября 2019

Пост Дэвида Херринга весьма поучителен, что memcpy можно переупорядочить после второй загрузки порядкового номера, и в настоящее время нет переносимого способа предотвратить это, потому что загрузки не могут иметь семантику освобождения, а std::atomic_thread_fence не делает '• работать с неатомарными memcpy.

. Непереносимое исправление для платформы x86 - вставить lfence инструкция:

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

Пример:

#include <atomic>
#include <cstring>
#include <emmintrin.h> // _mm_lfence()

struct SeqLock {
    alignas(64) std::atomic<uint64_t> sequence_number{0};
    alignas(64) char payload[5000];

    void start_writing() { sequence_number.fetch_add(1, std::memory_order::memory_order_acquire); }
    void publish() { sequence_number.fetch_add(1, std::memory_order::memory_order_release); }

    bool try_copy(char* copy) {
        auto before = sequence_number.load(std::memory_order::memory_order_acquire);
        if(before & 1)
            return false;
        ::memcpy(copy, payload, 5000);
        _mm_lfence(); // <---------------- LFENCE here.
        auto after = sequence_number.load(std::memory_order::memory_order_relaxed);
        return before == after;
    }
};

Существует список lfence эквивалентов для других платформ .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...