Spec-Zone.ru › C++

Транзакционная память (TM TS)

В разработке Эта страница находится в стадии разработки
Экспериментальная функция Функциональность, описанная на этой странице, входит в техническую спецификацию транзакционной памяти ISO/IEC TS 19841:2015 (TM TS)

Транзакционная память — это механизм синхронизации параллельного выполнения, который объединяет группы операторов в транзакции, обладающие следующими свойствами:

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

Типичные реализации используют аппаратную транзакционную память, если она поддерживается, и в пределах её доступности (например, пока набор изменений не станет слишком большим), а затем переходят к программной транзакционной памяти, обычно реализуемой с помощью оптимистической параллельности: если другая транзакция обновила одну из переменных, используемых транзакцией, её выполнение незаметно повторяется. По этой причине транзакции, допускающие повторное выполнение («атомарные блоки»), могут вызывать только функции, безопасные для транзакций.

Обратите внимание: доступ к переменной внутри транзакции и вне транзакции без дополнительной внешней синхронизации является гонкой данных.

Если поддерживается проверка возможностей, описанные здесь возможности обозначаются константой-макросом __cpp_transactional_memory со значением, равным или превышающим 201505.

Блоки синхронизации

synchronized compound-statement

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

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

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

int f()
{
    static int i = 0;
    synchronized { // begin synchronized block
        std::cout << i << " -> ";
        ++i;       // each call to f() obtains a unique value of i
        std::cout << i << '\n';
        return i;  // end synchronized block
    }
}

int main()
{
    std::vector<std::thread> v(10);
    for (auto& t : v)
        t = std::thread([] { for (int n = 0; n < 10; ++n) f(); });
    for (auto& t : v)
        t.join();
}

Вывод:

0 -> 1
1 -> 2
2 -> 3
...
99 -> 100

Выход из блока синхронизации любым способом (достижение конца блока, выполнение goto, break, continue или return либо выбрасывание исключения) завершает блок и синхронизируется со следующим блоком в едином линейном порядке, если завершённый блок был внешним. Поведение не определено, если для выхода из блока синхронизации используется std::longjmp.

Вход в блок синхронизации с помощью goto или switch не допускается.

Хотя блоки синхронизации выполняются так, как если бы они находились под глобальной блокировкой, предполагается, что реализации будут анализировать код внутри каждого блока и использовать оптимистическую параллельность (с поддержкой аппаратной транзакционной памяти, если она доступна) для кода, безопасного для транзакций, и минимальную блокировку для кода, небезопасного для транзакций. Если блок синхронизации вызывает невстроенную функцию, компилятору может потребоваться выйти из спекулятивного выполнения и удерживать блокировку на протяжении всего вызова, если только функция не объявлена как transaction_safe (см. ниже) или не используется атрибут [[optimize_for_synchronized]] (см. ниже).

Атомарные блоки

atomic_noexcept compound-statement

atomic_cancel compound-statement

atomic_commit compound-statement

1) Если выбрасывается исключение, вызывается std::abort.
2) Если выбрасывается исключение, вызывается std::abort, за исключением случаев, когда это исключение относится к исключениям, используемым для отмены транзакции (см. ниже); в этом случае транзакция отменяется: значения всех ячеек памяти в программе, изменённых побочными эффектами операций атомарного блока, восстанавливаются до значений, которые они имели в момент начала выполнения атомарного блока, а раскрутка стека продолжается обычным образом.
3) Если выбрасывается исключение, транзакция фиксируется обычным образом.

Исключения, используемые для отмены транзакции в блоках atomic_cancel, — это std::bad_alloc, std::bad_array_new_length, std::bad_cast, std::bad_typeid, std::bad_exception, std::exception и все исключения стандартной библиотеки, производные от него, а также специальный тип исключения std::tx_exception<T>.

compound-statement в атомарном блоке не может выполнять выражения или операторы, а также вызывать функции, не являющиеся transaction_safe (это ошибка времени компиляции).

// each call to f() retrieves a unique value of i, even when done in parallel
int f()
{
    static int i = 0;
    atomic_noexcept { // begin transaction
//  printf("before %d\n", i); // error: cannot call a non transaction-safe function
        ++i;
        return i; // commit transaction
    }
}

Выход из атомарного блока любым способом, кроме выбрасывания исключения (достижение конца блока, goto, break, continue, return), фиксирует транзакцию. Поведение не определено, если для выхода из атомарного блока используется std::longjmp.

Функции, безопасные для транзакций

Функцию можно явно объявить безопасной для транзакций, используя ключевое слово transaction_safe в её объявлении.

В объявлении лямбда-выражения оно указывается либо сразу после списка захвата, либо сразу после mutable (если используется).

extern volatile int * p = 0;
struct S
{
    virtual ~S();
};
int f() transaction_safe
{
    int x = 0;  // ok: not volatile
    p = &x;     // ok: the pointer is not volatile
    int i = *p; // error: read through volatile glvalue
    S s;        // error: invocation of unsafe destructor
}
int f(int x) { // implicitly transaction-safe
    if (x <= 0)
        return 0;
    return x + f(x - 1);
}

Если функция, не являющаяся безопасной для транзакций, вызывается через ссылку или указатель на функцию, безопасную для транзакций, поведение не определено.


Указатели на функции, безопасные для транзакций, и указатели на безопасные для транзакций функции-члены неявно преобразуются соответственно в указатели на функции и указатели на функции-члены. Не определено, будет ли результирующий указатель сравниваться на равенство с исходным.

Виртуальные функции, безопасные для транзакций

Если конечная функция-переопределитель функции transaction_safe_dynamic не объявлена как transaction_safe, её вызов в атомарном блоке приводит к неопределённому поведению.

Стандартная библиотека

Помимо добавления нового шаблона исключения std::tx_exception, техническая спецификация транзакционной памяти вносит следующие изменения в стандартную библиотеку:

  • явно объявляет следующие функции как transaction_safe:
    • std::forward, std::move, std::move_if_noexcept, std::align, std::abort, глобальный оператор operator new по умолчанию, глобальный оператор operator delete по умолчанию, std::allocator::construct, если вызываемый конструктор безопасен для транзакций, std::allocator::destroy, если вызываемый деструктор безопасен для транзакций, std::get_temporary_buffer, std::return_temporary_buffer, std::addressof, std::pointer_traits::pointer_to, каждая невиртуальная функция-член всех типов исключений, поддерживающих отмену транзакции (см. выше atomic_cancel)
  • явно объявляет следующие функции как transaction_safe_dynamic
    • каждая виртуальная функция-член всех типов исключений, поддерживающих отмену транзакции (см. выше atomic_cancel)
  • требует, чтобы все операции, безопасные для транзакций у аллокатора X, были безопасными для транзакций у X::rebind<>::other

Атрибуты

Атрибут [[optimize_for_synchronized]] может применяться к деклараторам в объявлении функции и должен указываться в первом объявлении функции.

Если функция объявлена как [[optimize_for_synchronized]] в одной единице трансляции, а в другой единице трансляции та же функция объявлена без [[optimize_for_synchronized]], программа некорректна; диагностика не требуется.

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

std::atomic<bool> rehash{false};

// maintenance thread runs this loop
void maintenance_thread(void*)
{
    while (!shutdown)
    {
        synchronized
        {
            if (rehash)
            {
                hash.rehash();
                rehash = false;
            }
        }
    }
}

// worker threads execute hundreds of thousands of calls to this function 
// every second. Calls to insert_key() from synchronized blocks in other
// translation units will cause those blocks to serialize, unless insert_key()
// is marked [[optimize_for_synchronized]]
[[optimize_for_synchronized]] void insert_key(char* key, char* value)
{
    bool concern = hash.insert(key, value);
    if (concern)
        rehash = true;
}

Ассемблерный код GCC без атрибута: вся функция выполняется последовательно

insert_key(char*, char*):
        subq        $8, %rsp
        movq        %rsi, %rdx
        movq        %rdi, %rsi
        movl        $hash, %edi
        call        Hash::insert(char*, char*)
        testb        %al, %al
        je        .L20
        movb        $1, rehash(%rip)
        mfence
.L20:
        addq        $8, %rsp
        ret

Ассемблерный код GCC с атрибутом:

transaction clone for insert_key(char*, char*):
        subq        $8, %rsp
        movq        %rsi, %rdx
        movq        %rdi, %rsi
        movl        $hash, %edi
        call        transaction clone for Hash::insert(char*, char*)
        testb        %al, %al
        je        .L27
        xorl        %edi, %edi
        call        _ITM_changeTransactionMode # Note: this is the serialization point
        movb        $1, rehash(%rip)
        mfence
.L27:
        addq        $8, %rsp
        ret

Примечания

Ключевые слова

atomic_cancel, atomic_commit, atomic_noexcept, synchronized, transaction_safe, transaction_safe_dynamic

Поддержка компиляторами

Эта техническая спецификация поддерживается GCC начиная с версии 6.1 (для включения требуется -fgnu-tm). Более ранний вариант этой спецификации поддерживался в GCC начиная с версии 4.7.

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/transactional_memory

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API