Форум программистов, компьютерный форум, киберфорум
C++
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
 
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24

Вопрос по многопоточности

18.07.2025, 22:33. Показов 13367. Ответов 104

Студворк — интернет-сервис помощи студентам
Здравствуйте, сейчас смотрю книги по многопоточности, возникло несколько вопросов. Почему-то слово "вопрос" нельзя полностью написать в заголовке.

1. Известно, что переменную bool (или int, неважно) может 1 раз записать только 1 поток, остальные только читают, зачем тогда делать её atomic?

2. Улетят ли вызовы notify_one/notify_all вникуда, если они много раз вызваны перед методами, которые ожидают cv?

3. Допустим, есть 5 потоков и есть общий вектор с огромным количеством элементов. Первый поток изменяет только элементы с идексами 0, 5, 10; второй поток - элементы с индексами 1, 6, 11; третий поток - элементы с индексами 2, 7, 12 и т.д. Правильно ли я понимаю, что переброска кэша и связанное с ним замедление программы все равно может происходить, потому что индексы, с которыми работает каждый поток, находятся по соседству?

4. Вот пример потокобезопасной очереди из книги Вилльямса. Зачем при сравнении head с tail в функции get_tail мы используем мьютекс, который тут же перестает блокироваться после того, как мы вышли из функции?

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <fstream>
#include <ios>
#include <string>
#include <numeric>
#include <map>
#include <vector>
#include <queue>
#include <iterator>
#include <memory>
#include <mutex>
#include <type_traits>
#include <algorithm>
#include <atomic>
#include <thread>
#include <chrono>
#include <cassert>
 
// clang -std=c++2a -m64 -o thread_safe_queue.exe thread_safe_queue.cpp
 
template<typename T> class thread_safe_queue {
private:
    struct node {
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;
    };
    std::unique_ptr<node> head;
    node* tail;
 
    mutable std::mutex m_head;
    mutable std::mutex m_tail;
 
public:
    thread_safe_queue(const thread_safe_queue& ) = delete;
    thread_safe_queue() : head(new node()), tail(head.get()) {}
 
    node* get_tail() const {
        std::lock_guard<std::mutex> lg_tail(m_tail);
        return tail;
    }
 
    bool empty() const {
        std::lock_guard<std::mutex> lg_head(m_head);
        std::lock_guard<std::mutex> lg_tail(m_tail);
        return (head.get() == tail);
    }
 
    std::shared_ptr<T> pop() {
        std::lock_guard<std::mutex> lg_head(m_head);
        if (head.get() == get_tail()) { // why ?
            return std::shared_ptr<T>();
        }
 
        std::shared_ptr<T> res(head->data);
        std::unique_ptr<node> old_head = std::move(head);
        head = std::move(old_head->next);
        return res;
    }
 
    void push(const T new_value) {
        std::unique_ptr<node> q(new node);
        std::lock_guard<std::mutex> lg_tail(m_tail);
        tail->data = std::make_shared<T>(std::move(new_value));
        tail->next = std::move(q);
        tail = tail->next.get();
    }
};
 
int main(int argc, char *argv[]) {
    return 0;
}
0
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
18.07.2025, 22:33
Ответы с готовыми решениями:

Соотношение многопоточности приложения c++ и многопоточности на уровне системы?
Возник следующий вопрос: в C++ существует два варианта работы с многопоточностью - std::theard и...

Управление потоками в многопоточности
вопрос простой: что посоветуете почитать по теме для начинающего? с помощью чего проще...

Нужна информация о многопоточности
дайте хорошую статью про создание многопоточных приложений...

104
фрилансер
 Аватар для Алексей1153
6450 / 5651 / 1129
Регистрация: 11.10.2019
Сообщений: 15,049
19.07.2025, 08:42
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Известно, что переменную bool (или int, неважно) может 1 раз записать только 1 поток, остальные только читают, зачем тогда делать её atomic?
если область памяти может одновременно читаться и писаться двумя и более потоками - это UB

Но если была гарантия, что переменная уже инициализирована и больше меняться не будет, а только потом были запущены читающие потоки, то всё ок
1
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
20.07.2025, 04:26
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Зачем при сравнении head с tail в функции get_tail мы используем мьютекс, который тут же перестает блокироваться после того, как мы вышли из функции?
Чтобы дождаться push
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Улетят ли..
Улетят. Не связывайтесь с такими низкоуровневыми примитивами синхронизации, моск они выносят изрядно, а толку чуть
1
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
20.07.2025, 15:57  [ТС]
Цитата Сообщение от Igor3D
Чтобы дождаться push
Igor3D, а зачем дожидаться push? Ведь все равно может возникнуть ситуация, когда очередь пустая, а метод pop вызван. Могу предположить, что для тех случаев когда очередь быстро расходуется и обновляется и программа будет работать немного быстрее, если подождать добавление нового элемента в пустую очередь, а затем с ним работать. Правильно я понимаю, что ошибки не будет, просто будет чаще возвращаться пустой элемент при вызове pop?


Цитата Сообщение от Igor3D Посмотреть сообщение
Улетят. Не связывайтесь с такими низкоуровневыми примитивами синхронизации, моск они выносят изрядно, а толку чуть
А что тогда использовать кроме мьютексов и атомиков? В атомиках использовать только consistency, acquire/release тоже не сильно помогают?

Добавлено через 9 минут
Цитата Сообщение от Алексей1153 Посмотреть сообщение
если область памяти может одновременно читаться и писаться двумя и более потоками - это UB
Но если была гарантия, что переменная уже инициализирована и больше меняться не будет, а только потом были запущены читающие потоки, то всё ок
Допустим, 1 поток пишет, 2 читают. В чем именно UB? В том, что один поток может прочесть старое значение, второй новое?

Например, в этом коде может печататься на 123, а цифры в другом порядке, но если перед каждым bool поставить volatile или использовать atomic<bool>, то будет печататься 123, как и ожидали. Понятно, почему работает с atomic или volatile , но почему без них не работает?

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>
#include <thread>
 
using namespace std;
struct A {
    bool b12; // volatile bool b12; // atomic<bool> b12;
    bool b23; // volatile bool b13; // atomic<bool> b13;
public:
    A() : b12(false), b23(false) { }
 
    void printFirst() {
        cout << "1\n";
        b12 = true;
    }
 
    void printSecond() {
        while (!b12);
        cout << "2\n";
        b23 = true;
    }
 
    void printThird() {
        while (!b23);
        cout << "3\n";
    }
};
 
int main() {
    A a;
    thread t1(&A::printFirst,  &a);
    thread t2(&A::printSecond, &a);
    thread t3(&A::printThird,  &a);
    t1.join();
    t2.join();
    t3.join();
 
    return 0;
}
0
фрилансер
 Аватар для Алексей1153
6450 / 5651 / 1129
Регистрация: 11.10.2019
Сообщений: 15,049
20.07.2025, 18:45
Цитата Сообщение от cdcodecpp Посмотреть сообщение
1 поток пишет, 2 читают. В чем именно UB?
в том, что так написано в стандарте. Но - это когда одновременно произойдёт чтение и запись. Если эти события гарантированно разнесены во времени, то всё ок

Цитата Сообщение от cdcodecpp Посмотреть сообщение
поставить volatile
volatile не решает проблему, это всё равно будет UB

std::atomic - решает

А вывод в std::cout здесь может замаскировать проблему, так как к нему доступ синхронизирован внутри класса
1
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
20.07.2025, 21:03
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Допустим, 1 поток пишет, 2 читают. В чем именно UB?
Может и ни в чем. Простой пример. Все нитки видят/читают глобальную переменную flagAbort и любая может ее установить (но не сбросить). Все работает без всяких атомиков/локов. Правда диагностика (напр "sanitizer") ловит это как ошибку - ведь даже 2 нитки "одновременно пишут" (в одну ячейку) не говоря уже об одновременном чтении/записи. Просто задумано так что "это нас устраивает". Хотя так удается проскочить довольно редко.
Цитата Сообщение от cdcodecpp Посмотреть сообщение
А что тогда использовать кроме мьютексов и атомиков? В атомиках использовать только consistency, acquire/release тоже не сильно помогают?
А кто Вас заставляет юзать мьютексы/атомики? Возьмите нормальную систему, напр OpenMP или tbb, и научитесь ей пользоваться. Там хороший ф-ционал даже без всяких дополнительных примитивов. Это гораздо продуктивнее чем написание чего-то своего, "потокобезопасного"
1
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
20.07.2025, 22:18  [ТС]
Цитата Сообщение от Igor3D Посмотреть сообщение
А кто Вас заставляет юзать мьютексы/атомики? Возьмите нормальную систему, напр OpenMP или tbb, и научитесь ей пользоваться. Там хороший ф-ционал даже без всяких дополнительных примитивов. Это гораздо продуктивнее чем написание чего-то своего, "потокобезопасного"
Я думал, что освоение многопоточности надо начать с изучения Вильямса, начал изучать, возникли вопросы.

"TBB provides threadsafe containers and some parallel algorithms, whereas OpenMP is more of a way to parallelise existing code."
"TBB предоставляет потокобезопасные контейнеры и некоторые параллельные алгоритмы, тогда как OpenMP — это скорее способ распараллелить существующий код."

Похоже, что с TBB надо ознакомиться, а OpenMP изучить более внимательно, так? Посоветуйте, пожалуйста, литературу, а также то, с чего начать с TBB или с OpenMP .
0
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
20.07.2025, 23:51
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Похоже, что с TBB надо ознакомиться, а OpenMP изучить более внимательно, так? Посоветуйте, пожалуйста, литературу, а также то, с чего начать с TBB или с OpenMP .
А Вы с какой целью интересуетесь? Что Вы хотите от "многопоточности"? Например
Мое приложение на компе с 4 ядрами считает по меньшей мере в 3 раза быстрее!
Тогда конечно прямая дорога к OpenMP, не уметь им пользоваться просто неприлично (даже если Вы потом предпочтёте что-нибудь другое). Подключаете либу и разбираетесь с опциями компилятора, это несложно. Доку OpenMP посмотреть не помешает, но для начала достаточно освоить одну директиву
C++
1
#pragma omp parallel for
Необязательно, но вполне возможно что это окажется хорошим решением для доброй половины Ваших задач. А когда "результат налицо" - изучать намного легче.
0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
21.07.2025, 13:50
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Ведь все равно может возникнуть ситуация, когда очередь пустая, а метод pop вызван. Могу предположить, что для тех случаев когда очередь быстро расходуется и обновляется и программа будет работать немного быстрее, если подождать добавление нового элемента в пустую очередь, а затем с ним работать. Правильно я понимаю, что ошибки не будет, просто будет чаще возвращаться пустой элемент при вызове pop?
Если вопрос в том, будет ли ошибкой чтение tail не под блокировкой в функции get_tail(), то конечно же будет.
Это конкурентная операция чтения и она, как минимум, должна быть атомарной.
Но, что еще более важно, тут необходимо обеспечить межпоточную синхронизацию: в функции push() три операции записи (на самом деле их там сколь угодно много), и поток, выполняющий pop(), должен увидеть их именно в том порядке, в котором их делает поток, выполняющий push() (что будет при нарушении консистентности данных, можете пофантазировать самостоятельно). Мьютекс обеспечивает синхронизацию acquire/release.

Цитата Сообщение от cdcodecpp Посмотреть сообщение
В чем именно UB? В том, что один поток может прочесть старое значение, второй новое?
UB означает отсутствие гарантий поведения, описанного в стандарте. Причем поведения всего кода, а не только "ub-содержащего" фрагмента. А поскольку в стандарте описанно поведение абстрактной машины, то, если хотите подробностей, каким может быть это поведение на практике, изучайте особенности различных компиляторов и различных архитектур процессоров (хотя проще просто усвоить правила высокоуровнего языка и не забивать голову малополезной информацией).

Цитата Сообщение от cdcodecpp Посмотреть сообщение
3. Допустим, есть 5 потоков и есть общий вектор с огромным количеством элементов. Первый поток изменяет только элементы с идексами 0, 5, 10; второй поток - элементы с индексами 1, 6, 11; третий поток - элементы с индексами 2, 7, 12 и т.д. Правильно ли я понимаю, что переброска кэша и связанное с ним замедление программы все равно может происходить, потому что индексы, с которыми работает каждый поток, находятся по соседству?
Да, правильно.

Цитата Сообщение от cdcodecpp Посмотреть сообщение
Я думал, что освоение многопоточности надо начать с изучения Вильямса
Правильно, продолжайте.
2
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
21.07.2025, 19:07  [ТС]
Цитата Сообщение от zayats80888 Посмотреть сообщение
Если вопрос в том, будет ли ошибкой чтение tail не под блокировкой в функции get_tail(), то конечно же будет.
Это конкурентная операция чтения и она, как минимум, должна быть атомарной.
Я правильно понимаю, что блокировка должны быть для того, чтобы не допустить UB, а оно возникает, если происходит чтение и запись по одному и тому же адресу? А так нам не важно,старое или новое значение мы прочли? Даже если после прочтения старого значения, для которого tail == head, метод push вызывался много раз и tail обновился, важно не прочесть истинное текущее значение tail, а недопустить чтение вместе с записью для одного и того же адреса, которое приводит к UB?

Цитата Сообщение от zayats80888 Посмотреть сообщение
Но, что еще более важно, тут необходимо обеспечить межпоточную синхронизацию: в функции push() три операции записи (на самом деле их там сколь угодно много), и поток, выполняющий pop(), должен увидеть их именно в том порядке, в котором их делает поток, выполняющий push() (что будет при нарушении консистентности данных, можете пофантазировать самостоятельно). Мьютекс обеспечивает синхронизацию acquire/release.
Насколько я понял, надо только чтобы не было UB из-за одновременного чтения pop-потоком и записью push-потока. Если очередь не пустая, то tail != head, метод push может делать что угодно с tail, главное, что из-за мьютекса m_head только 1 поток может исполнить метод pop.
Если очередь пустая, то мы получаем старое значение tail (tail==head), возвращаем пустое значение, а далее нам не важно, что делает push, главное, что с помощью мьютекса внутри get_tail сравнение происходит уже с копией старого значение и не произошло UB по причине одновременного чтения и записи по одному адресу. А то, что значение tail в пустой очереди могло поменяться, это уже никак не волнует, правильно?



Цитата Сообщение от zayats80888 Посмотреть сообщение
и поток, выполняющий pop(), должен увидеть их именно в том порядке, в котором их делает поток, выполняющий push()
Зачем pop-потоку видеть то, что делает push-поток? Либо очередь не пустая, тогда осуществляется доступ к разным элементам, либо пустая, тогда pop возвращает пустой элемент. Что в этом рассуждении ошибочно?
0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
21.07.2025, 20:13
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Зачем pop-потоку видеть то, что делает push-поток? Либо очередь не пустая, тогда осуществляется доступ к разным элементам, либо пустая, тогда pop возвращает пустой элемент. Что в этом рассуждении ошибочно?
Ну как зачем? "pop-поток" читает информацию записанную "push-потоком". Как вы при отсутствии мьютекса гарантируете, что первый видит то-же самое, что и второй?
Например, представьте, что очередь пуста, и два потока "одновременно" осуществляют доступ - один pop(), второй push().
Тот, который делает push(), увидел изменение tail (очередь не пуста), но не увидел, например, изменения в head->next (он по-прежнему nullptr), что в этом случае произойдет?
0
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
21.07.2025, 21:41
Цитата Сообщение от zayats80888 Посмотреть сообщение
Если вопрос в том, будет ли ошибкой чтение tail не под блокировкой в функции get_tail(), то конечно же будет. Это конкурентная операция чтения и она, как минимум, должна быть атомарной.
В какой-то статье (про std) видел термин "atomistic", т.е. значение полное (а не пол-указателя старые, пол-указателя новые). Наверно здесь Вы это имели ввиду
Цитата Сообщение от zayats80888 Посмотреть сообщение
Но, что еще более важно, тут необходимо обеспечить межпоточную синхронизацию: в функции push() три операции записи (на самом деле их там сколь угодно много), и поток, выполняющий pop(), должен увидеть их именно в том порядке, в котором их делает поток, выполняющий push() (что будет при нарушении консистентности данных, можете пофантазировать самостоятельно).
Хотелось бы конечно "помучить" (пофантазировать). Тем более что поиск возможного "пробоя" далеко не легкое занятие. Навскидку я его не вижу, как бы я искал. Пусть push
C++
1
2
3
4
tail->data = std::make_shared<T>(std::move(new_value));
<----  вытеснен здесь
tail->next = std::move(q);
tail = tail->next.get();
Чем это грозит коду pop? Или наоборот, как pop может подгадить когда push вернет управление? "Увидеть их именно в том порядке" как-то не очень ясно, кто чего "видит"?
Цитата Сообщение от zayats80888 Посмотреть сообщение
Мьютекс обеспечивает синхронизацию acquire/release.
Не понимаю что это значит
Цитата Сообщение от zayats80888 Посмотреть сообщение
Правильно, продолжайте.
Пока не попадете в дурдом? Ну какой смысл в этом мучительно трудном коде? Зачем выносить моск двумя локами? Нужна потокобезопасная очередь? Да хотя бы взял обычную и прикрылся везде спиннером (одним). Не говоря уже о том чтобы взять готовую
0
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
21.07.2025, 22:36  [ТС]
Цитата Сообщение от zayats80888 Посмотреть сообщение
Например, представьте, что очередь пуста, и два потока "одновременно" осуществляют доступ - один pop(), второй push().
Тот, который делает push(), увидел изменение tail (очередь не пуста), но не увидел, например, изменения в head->next (он по-прежнему nullptr), что в этом случае произойдет?
Вы, наверное, имели ввиду не push, а pop, иначе как при пустой очереди push увидел изменение значения tail. Потока же только 2.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
    std::shared_ptr<T> pop() {
        std::lock_guard<std::mutex> lg_head(m_head);
        if (head.get() == get_tail()) { // считывают tail, UB, если push меняет tail
            return std::shared_ptr<T>();
        }
 
        std::shared_ptr<T> res(head->data);
        std::unique_ptr<node> old_head = std::move(head);
        head = std::move(old_head->next);
        return res;
    }
 
    void push(const T new_value) {
        std::unique_ptr<node> q(new node);
        std::lock_guard<std::mutex> lg_tail(m_tail);
        tail->data = std::make_shared<T>(std::move(new_value)); // проблем нет, нет разыменования или изм. значения
        tail->next = std::move(q); // проблем нет, нет разыменования или изм. значения
        tail = tail->next.get(); // переписывает tail, UB, если pop считывает tail
    }
Если эти 2 строки исполняются одновременно, то UB, т.к. один пишет, другой читает, мьютекс предотвращает такую одновременность.
C++
1
2
if (head.get() == get_tail()) 
tail = tail->next.get();
А здесь уже проблем нет, т.к адрес, на который указывает tail, не меняется, а сам tail не разыменовывается.
C++
1
2
        tail->data = std::make_shared<T>(std::move(new_value)); // проблем нет, нет разыменования или изм. значения
        tail->next = std::move(q); // проблем нет, нет разыменования или изм. значения
0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
21.07.2025, 23:56

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
Наверно здесь Вы это имели ввиду
Я не имел ввиду ничего специфичного, только то, что написанно в стандарте ([intro.races-17]).

Цитата Сообщение от Igor3D Посмотреть сообщение
"Увидеть их именно в том порядке" как-то не очень ясно, кто чего "видит"?
...
Не понимаю что это значит
Вот тут подробно описан порядок выполнения программы в пределах одного потока и в многопоточном приложении:
[basic.exec]
Так вот, под "порядком" я имел ввиду, что нужны гаранти того, что, например, запись tail->data в push happens before чтения head->data в pop.
Цитата Сообщение от Igor3D Посмотреть сообщение
Пока не попадете в дурдом? Ну какой смысл в этом мучительно трудном коде?
Понятие "трудно" субъективно, а этот код "цветочки" в сравнении с реально существующими "мозгодробилками".
И это база языка, которую неплохо бы освоить. В конце концов кому-то же придется в этом разбираться и поддерживать, когда все существующие обитатели "дурки" отойдут дел.



cdcodecpp, вы пока летите вперед паровоза. Пока просто примите то, что чтение tail не должно быть конкурентным с его записью, поэтому нужен mutex.
Кликните здесь для просмотра всего текста
Чуть позже, в 5 главе, по-моему, вы познакомитесь с атомарными операциями и узнаете, что порядок модификации памяти объектов одним потоком может отличаться от наблюдаемого другим, т.е.:
C++
1
2
3
4
5
6
7
// один поток
a = 1;
b = 2;
 
// другой поток
if (b == 2)
  assert(a == 1); // может выстрелить, т.к. для этого потока событие модификации 'a' еще не произошло
0
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
22.07.2025, 00:09  [ТС]
Цитата Сообщение от zayats80888 Посмотреть сообщение
Пока просто примите то, что чтение tail не должно быть конкурентным с его записью, поэтому нужен mutex.
Правильно я понимаю, мьютекс предотвращает одновременное исполнение строк

C++
1
2
if (head.get() == get_tail())  // из метода pop
tail = tail->next.get(); // из метода push
, которое грозит UB, а так все нормально и если бы у нас была уверенность, что те строки, котороые я написал, никак не могут выполниться одновременно, то остальные строки никак друг другу не мешали и не приводили бы к UB?
Кроме предотвращения данного UB мьютекс решает ещё какие-либо проблемы?
Появятся ли проблемы без мьютекса именно у тех 2 строк, которые написаны ниже?

C++
1
2
        tail->data = std::make_shared<T>(std::move(new_value)); // проблем нет, нет разыменования или изм. значения
        tail->next = std::move(q); // проблем нет, нет разыменования или изм. значения
Добавлено через 3 минуты
Цитата Сообщение от zayats80888 Посмотреть сообщение
Чуть позже, в 5 главе, по-моему, вы познакомитесь с атомарными операциями и узнаете, что порядок модификации памяти объектов одним потоком может отличаться от наблюдаемого другим, т.е.:
Читал про это. Но мне кажется, что в случае очереди вопрос в другом.
0
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
22.07.2025, 01:15
Цитата Сообщение от zayats80888 Посмотреть сообщение
Пока просто примите то, что чтение tail не должно быть конкурентным с его записью, поэтому нужен mutex.
А то что? Что мне будет если пишу/читаю одновременно? Получу старое значение tail (но не мусор) и не сделаю pop (который был возможен)? Так мне все равно с этим надо считаться, push мог просто случиться позже. Вот если бы стал лезть внутрь старого tail - тогда да. Детский пример
C++
1
2
3
4
5
6
7
8
9
10
auto theMax = data[0];
 
#pragma omp parallel for
for (int i = 0; i < count; ++i) {
 if (data[i] > theMax) {
  Lock();
  if (data[i] > theMax) 
   theMax = data[i];
 } 
}
Часто говорят "двойная проверка", резонно, взятие мутекса далеко не бесплатно. Но ведь мы одновременно читаем и пишем! (UB). И что, так делать нельзя? Или "уже нельзя"? Или как?

Не по теме:

За ссылки на стандарт спасибо, но его нужно читать только когда совершенно точно известно что найти. По-другому у меня никогда не выходило :)

0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
22.07.2025, 15:38
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Правильно я понимаю, мьютекс предотвращает одновременное исполнение строк, которое грозит UB, а так все нормально и если бы у нас была уверенность, что те строки, котороые я написал, никак не могут выполниться одновременно, то остальные строки никак друг другу не мешали и не приводили бы к UB?
Нет, не совсем. Я пытался вам объяснить простым языком, что еще важно упорядочивание операций, но вы это все как-то пропускаете мимо, рановато пока, видимо. Так же у вас неправильное представление о UB.
К UB приводят не какие-то события во время выполнения, а ошибки в написании кода.
UB буквально переводится как неопределенное поведение.
Т.е. нет смысла вообще рассуждать, как и что будет происходить в работающей программе, её поведение не определено с точки зрения языка.
На самом деле она конечно же ведет себя вполне определенно, и вы можете это поведение исследовать анализируя машинный код, но это все не имеет смысла и я попытаюсь объяснить почему.
Нет такого понятия "одновременно" в языке. Вы пытаетесь представить выполнение кода так, как он написан на высокоуровневом языке, да еще и построчно. Но нужно понимать, что это всего лишь умозрительный эксперимент, который не будет иметь отношения к реальности. Такой подход может как помогать в усвоении материала, так и вредить. В вашем случае скорее второе.
Вы должны понимать, одна строчка кода на с++ будет превращена компилятором в несколько(иногда сотни и тысячи) машинных инструкций, среди которых будут инструкции записи и чтения памяти. В погоне за производительностью компилятору позволено переупорядочивать эти инструкции так, как он считает нужным, если это не нарушает требования стандарта.
Кликните здесь для просмотра всего текста
Например, если на определенной платформе операции записи в соседние ячейки памяти быстрее, а ваш код развернулся в последовательность записей по адресам A и B в одной строке, и A + 1 в следующей, то очевидной оптимизацией будет переупорядочить их в последовательность A, A + 1, B.
Мало нам вольностей компиляторов, так еще и процессоры стремятся извлекать и выполнять инструкции вне очереди. Можете вы себе во всем этом хаосе представить, что какая-то строчка кода с++ выполнилась, а какая-то еще нет? Сомневаюсь. Если к этому еще добавить многоядерность и различные стратегии по поддержанию когерентности кэша, то представить процесс выполнения многопоточного приложения на "физическом" уровне вообще не представляется возможным. Хорошо, что нам это и не нужно! У нас относительно простой и непротиворечивый набор правил, описанных в стандарте. Да, они абстрактны, как леммы и теоремы математики и, на первый взгляд, не имеют отношения к реальным машинам, выполняющим наши программы (поверьте, это не так). Вы очень облегчите себе жизнь, если научитесь анализировать код с++, опираясь только на эти правила и не пытаясь представить, а что там на самом деле будет делать железяка. Это самый правильный путь.
Кликните здесь для просмотра всего текста
Да, знание "нижнего" уровня компьютерных технологий прояснит суть и причины высокоуровневых правил, но не является необходимым. Если вы не собираетесь работать на этом уровне, конечно же. Просто там огромный объем информации.

Итак, резюмируя вышесказанное, отвечу на ваш вопрос:
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Появятся ли проблемы без мьютекса именно у тех 2 строк, которые написаны ниже?
1) Без мьютекса в функции get_tail() весь код некорректен, он не соответствует стандарту и содержит UB. Говорить о проблемах в каких либо строках не имеет смысла.
2) Если вас интересует, почему чтение tail мы делаем под мьютексом, а чтение остального без (и нет ли тут проблемы), то ответ: захват мьютекса в get_tail() как бы решает две задачи - предоставляет исключительный доступ к tail напрямую(как бы делая его атомарным) и опосредованно упорядочивая (предоставляет единый глобальный порядок модификации памяти) доступ к остальному. Но подробно об этом вы узнаете чуть позже, пока не заморачивайтесь по этому поводу, всему свое время.

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
А то что?
А то приедет дедушка Страуструп с ремнем и покажет, что значит UB!
Что за детский сад? Что вы от меня хотите?
Вы вроде ясно дали понять, что это трудно, вы в этом не разбираетесь и, самое главное, не хотите разбираться из-за опасений попасть в дурку. Зачем я буду тратить свое время на жевание кактуса?
Цитата Сообщение от Igor3D Посмотреть сообщение
За ссылки на стандарт спасибо, но его нужно читать только когда совершенно точно известно что найти. По-другому у меня никогда не выходило :)
Я написал, что вам надо найти и обосновать, даже термин написал:
Цитата Сообщение от zayats80888 Посмотреть сообщение
happens before
.
Если подойдете к вопросу серьезно, но возникнут трудности в понимании конкретных пунктов стандарта, я постараюсь помочь.
Заниматься демагогией с вами я не собираюсь.

0
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
22.07.2025, 23:07  [ТС]
Цитата Сообщение от zayats80888 Посмотреть сообщение
Да, они абстрактны, как леммы и теоремы математики и, на первый взгляд, не имеют отношения к реальным машинам, выполняющим наши программы (поверьте, это не так). Вы очень облегчите себе жизнь, если научитесь анализировать код с++, опираясь только на эти правила и не пытаясь представить, а что там на самом деле будет делать железяка. Это самый правильный путь.
Мне известно, что команды могут переупорядочиваться, ваш пример с
C++
1
assert(a == 1);
понятен, я встречал его в книге. Скажите, пожалуйста, правильно я понимаю, что достаточно просто знать, что возникает UB не только когда 2 и больше потока пытаются переписать одно и то же значение, но и когда один читает, а второй пишет? Этого достаточно в виде "леммы" для понимания?

А если бы команды не переупорядочивались компилятором и процессором, то всё равно было бы UB?
0
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
23.07.2025, 00:46
Цитата Сообщение от zayats80888 Посмотреть сообщение
А то приедет дедушка Страуструп с ремнем и покажет, что значит UB!
Что за детский сад? Что вы от меня хотите?
Хочу услышать Ваше мнение по поводу UB при "одновременном" чтении/записи, пример с поиском максимума выше. Варианты ответов

1. Такое UB недопустимо, ошибка, нужно исправлять! Даже если сейчас нет проблем, они могут возникнуть в будущем!
Такие ответы легко даются, когда "меня это не коснется", когда не нужно править старый код, часто писаный другими в незапамятные времена

2. Ничем особенным это UB не грозит, да, читающий "увидит" старое значение, это необязательно катастрофа. И вообще: "работает - не лезь" (хотя это конечно ничего не доказывает, баги всплывали и через годы).
Да, это мое мнение

3. Хз, что такое "UB" - никто не знает. На всякий случай сделайте theMax atomic'ом, хуже не будет
Вполне нормальный ответ практика

4. Это может быть корректно исправлено, для этого надо <..>.
Идеальный ответ которого не случится. Ну, для полноты картины/списка
Цитата Сообщение от zayats80888 Посмотреть сообщение
Вы вроде ясно дали понять, что это трудно, вы в этом не разбираетесь и, самое главное, не хотите разбираться..
Это совсем не так, тема мне интересна. Но у меня другой подход. Вот прямолинейное, лобовое решение (тот же поиск макс). Что здесь плохо? Если это просто "можно пережить" - зачем вникать во все эти немалые тонкости? Согласитесь, ответа как-то "не прозвучало". А если опыта маловато - тем более. Напр ТС не упомянул что параллельный поиск макс на практике неэффективен (при разумном count), скорость будет хуже чем с одним потоком. Ну может просто не хотел отвлекаться "от главного"
0
1967 / 823 / 114
Регистрация: 01.10.2012
Сообщений: 4,832
Записей в блоге: 2
23.07.2025, 01:46
Или вот др пример из той же книги (аттач). Далее там довольно пространные объяснения вникнуть в которые непросто. Поэтому "верно ли я понимаю что"
Установка atomic (data_ready = true) гарантирует что ВСЕ изменения сделанные пишущим потоком до этого места (в том числе и с вектором data) будут "видимы" (т.е. корректно считаны) читающим
Или это не так? Когда-то читал об этом/подобном, но прошло уже много лет
Миниатюры
Вопрос по многопоточности  
1
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
23.07.2025, 01:46
Помогаю со студенческими работами здесь

изучение многопоточности
с чего стоит начать изучение многопоточности? есть базовые знания по С++, основы ООП. пытался...

Объясните принцип создания многопоточности
Здраствуйте, объясните пожалйста как сделать программу многопоточной, у меня есть одна программа, в...

Менеджмент жесткого диска при многопоточности
Пусть у меня 4-ех ядерный процессор, и запущено 4 рабочих потока (в одном процессе). Казалось бы,...

Реализация многопоточности в консоли
Доброго времени суток. Не могу разобраться в многопоточности. Реализовано перемещение по меню с...

Сравнение многопоточности С++11 и WinAPI
У меня скорее теоретический вопрос, чем практический. Есть ли разница работы с многопоточностью в...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
20
Ответ Создать тему
Новые блоги и статьи
Использование SDL3-callbacks вместо функции main() на Android, Desktop и WebAssembly
8Observer8 24.01.2026
Если вы откроете примеры для начинающих на официальном репозитории SDL3 в папке: examples, то вы увидите, что все примеры используют следующие четыре обязательные функции, а привычная функция main(). . .
моя боль
iceja 24.01.2026
Выложила интерполяцию кубическими сплайнами www. iceja. net REST сервисы временно не работают, только через Web. Написала за 56 рабочих часов этот сайт с нуля. При помощи perplexity. ai PRO , при. . .
Модель сукцессии микоризы
anaschu 24.01.2026
Решили писать научную статью с неким РОманом
http://iceja.net/ математические сервисы
iceja 20.01.2026
Обновила свой сайт http:/ / iceja. net/ , приделала Fast Fourier Transform экстраполяцию сигналов. Однако предсказывает далеко не каждый сигнал (см ограничения http:/ / iceja. net/ fourier/ docs ). Также. . .
http://iceja.net/ сервер решения полиномов
iceja 18.01.2026
Выкатила http:/ / iceja. net/ сервер решения полиномов (находит действительные корни полиномов методом Штурма). На сайте документация по API, но скажу прямо VPS слабенький и 200 000 полиномов. . .
Расчёт переходных процессов в цепи постоянного тока
igorrr37 16.01.2026
/ * Дана цепь(не выше 3-го порядка) постоянного тока с элементами R, L, C, k(ключ), U, E, J. Программа находит переходные токи и напряжения на элементах схемы классическим методом(1 и 2 з-ны. . .
Восстановить юзерскрипты Greasemonkey из бэкапа браузера
damix 15.01.2026
Если восстановить из бэкапа профиль Firefox после переустановки винды, то список юзерскриптов в Greasemonkey будет пустым. Но восстановить их можно так. Для этого понадобится консольная утилита. . .
Сукцессия микоризы: основная теория в виде двух уравнений.
anaschu 11.01.2026
https:/ / rutube. ru/ video/ 7a537f578d808e67a3c6fd818a44a5c4/
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru