ar
Feedback
Грокаем C++

Грокаем C++

الذهاب إلى القناة على Telegram

Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

إظهار المزيد
9 384
المشتركون
+924 ساعات
+147 أيام
+1430 أيام
أرشيف المشاركات
🛠🛠Мечтаете прокачать свои навыки в электронике и программировании для работы с микроконтроллерами? Это реальный шанс освоит
🛠🛠Мечтаете прокачать свои навыки в электронике и программировании для работы с микроконтроллерами? Это реальный шанс освоить отладку встроенного ПО на практике! Приглашаем на открытый урок по отладке программ для AVR и PIC в Proteus и Multisim! - Вы познакомитесь с особенностями моделирования работы программного обеспечения микроконтроллеров AVR и PIC в программах Proteus Professional и NI Multisim. - Вы узнаете особенности моделирования аналоговых и цифровых схем и их взаимодействия с микроконтроллерами. Всё это — в интерактивном формате с реальными примерами. Присоединяйтесь! 🔥Регистрируйтесь на урок 2 декабря, в 20:00 мск и получите скидку на большое обучение «Электроника и электротехника»: https://otus.pw/Bqjc/ Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576

А вам когда-нибудь снился С++ в кошмарах?)
А вам когда-нибудь снился С++ в кошмарах?)

​​Могу ли я вызвать функцию main? #опытным Вопрос из разряда "а что если" и особо практического смысла не имеет. Но когда это нас останавливало? Без знаний стандарта на этот вопрос вряд ли можно ответить правильно, но я попробую хотя бы приблизится к этому. Прородителем плюсов был язык С, поэтому давайте сначала посмотрим, что там творится по этому поводу. В C нет запрета вызывать main(). А все, что не запрещено - разрешено. Вот и вы легко можете вызвать main() из любого места программы. Можно даже рекурсивно вызвать главную функцию и из этого можно придумать что-то более менее рабочее. Например:
#include <stdio.h>
int main (int argc, char *argv[]) {
    printf ("Running main with argc = %d, last = '%s'\n",
        argc, argv[argc-1]);
    if (argc > 1)
        return main(argc - 1, argv);
    return 0;
}
Если это запускать, как 'recursive_main 1 2 3', то вывод будет такой:
Running main with argc = 4, last = '3'
Running main with argc = 3, last = '2'
Running main with argc = 2, last = '1'
Running main with argc = 1, last = './recursive_main'
Но вы скорее попадете на переполнение стека от неконтролируемой рекурсии, чем сделаете что-то полезное. Ну и конечно, в С сложно сделать так, чтобы код выполнялся в глобальной области. А вот в С++ это сделать суперпросто. В конструкторах глобальных объектов. И вот здесь уже интересно. Инициализация глобальных переменных имеет свой определенный порядок. Что будет, если мы в этот порядок вклинимся и запустим программу раньше? Такое чувство, что ничего хорошего мы не получим. В случае с рекурсией любой код теоретически может уйти в переполнение, поэтому как будто и не особо важно, main это или нет. Не запрещать же рекурсию из-за возможности ее никогда не остановить. Но вот если мы можем прервать подготовку программы к вызову main ее преждевременным вызовом - у нас 100% возникнут проблемы. Скорее всего это одна из мажорных причин, почему в С++ нельзя вызывать main никаким образом. Если происходит обратное, то программа считается ill-formed. Компиляторы по идее должны тут же прервать компиляцию при упоминании main в коде. Но вы же знаете эти компиляторы. Слишком много им свободы дали. Скорее всего, вы сможете скомпилировать свой код с вызовом main и все заработает. Только если прописать какой-нибудь --pedantic флаг, то вам скажут "атата, так делать низя". В общем, не думаю, что у вас было желание когда-то вызвать main. Однако сейчас вы точно знаете, что так делать нельзя) Follow the rules. Stay cool. #cppcore #goodoldc

👍Как применить принципы ООП в языке С для создания сложных программ? Узнайте на бесплатном уроке онлайн-курса «Программист С
👍Как применить принципы ООП в языке С для создания сложных программ? Узнайте на бесплатном уроке онлайн-курса «Программист С» — «Язык Cи и ООП: пошаговая разработка видеоплеера»: регистрация Разберем практический пример разработки видеоплеера с использованием объектно-ориентированного подхода: - узнаете особенности реализации ООП в языке С - рассмотрим проектирование архитектуры видеоплеера: от интерфейсов к реализации - проведем практику: пошаговая разработка основных компонентов плеера. Также будет обработка ошибок и управление памятью в объектно-ориентированном стиле; live-coding: демонстрация работы с форматами видео и аудио в С. В результате получите: - готовый прототип видеоплеера на С с использованием ООП подходов и практические навыки применения паттернов проектирования в Си. 🔥После вебинара вы сможете продолжить обучение на курсе по спеццене, в том числе, в рассрочку. Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576

​​Еще один способ залочить много мьютексов Последний пост из серии. Вот у мьютекса есть метод lock, который его захватывает. А разработчики stdlib взяли и сделали функцию std::lock, которая лочит сразу несколько замков. Также у мьютекса есть метод try_lock, который пытается в неблокирующем режиме его захватить. "Авось да получится". И видимо по аналогии с lock в стандартной библиотеке существует свободная функция std::try_lock, которая пытается захватить несколько мьютексов так же в неблокирующем режиме.
template< class Lockable1, class Lockable2, class... LockableN >  
int try_lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );
То есть в этой функции банально в цикле на каждом из переданных аргументов вызывается try_lock и, если какой-то из вызовов завершился неудачно, то все занятые замки освобождаются и возвращается индекс мьютекса, на котором выполнение зафейлилось. Если все завершилось успешно aka все замки захвачены, возвращается -1. Чтобы с помощью std::try_lock наверняка захватить все замки, нужно крутиться в горячем цикле и постоянно вызывать std::try_lock, пока она не вернет -1. Непонятно, зачем эта функция нужна в прикладном программировании при наличии std::scoped_lock и std::lock, которые еще и удобно оборачивают все эти циклы, скрывая все эти кишки в деталях реализации. Единственное, что пришло в голову - реализация своего scoped_lock'а с блэкджеком и другим алгоритмом предотвращения дедлока. Этот алгоритм должен быть чем-то похож на поддавки, но как-то изменен. У кого есть кейсы применения - отпишитесь в комменты. В общем, на мой взгляд, это пример избыточного апи. Если человек сам что-то свое мудрит, то ему особо не сложно самому цикл написать. Однако обычные пользователи стандартных примитивов синхронизации возможно никогда в своей жизни этого не применят. Все-таки стандартная библиотека должна быть сборником решения актуальных проблем реальных, а не воображаемых,людей. Stay useful. Stay cool. #concurrency

⚡️Истовый инженер теперь в Telegram! В канале публикуют полезные статьи для инженеров и программистов со всех направлений. Во
⚡️Истовый инженер теперь в Telegram! В канале публикуют полезные статьи для инженеров и программистов со всех направлений. Вот, что мы лично прочитали и где поставили лайк: — Что изучить про алгоритмы и структуры данных разработчикам на С++ ( читать ) — CodeChecker: анализируем большой проект на С++ быстро, эффективно и бесплатно ( читать ) — 5 способов писать эффективный код на Go: от нейминга переменных до архитектуры ( читать ) — Ужасно подробные ошибки в API: пишем инструмент для работы с ними на Go ( читать ) 📝 и ещё 100+ полезных статей, лекций и кейсов от практиков Подписываемся, чтобы не потерять полезный канал @ultimate_engineer

Почему не используют стратегию блокировки по адресам? #опытным Точного ответа от разработчиков стандартной библиотеки мы не услышим, но я приведу некоторые рассуждения, которые могут натолкнуть на некоторые мысли. Начнем с того, что локать один мьютекс - это норма. Все так делают, никто от этого не помер. Проблемы и эти ваши дедлоки начинаются только тогда, когда поток по какой-то причине блокируется с уже захваченным локом. А именно это и происходит при вызове метода lock(). Поток мытается захватить мьютекс и если не получается - блокируется до того момента, пока мьютекс не освободится. Поэтому любая схема с последовательным вызовом методов lock() будет подвержена дедлокам. А в схеме с упорядоченным по адресам блокировкой именно так и происходит. Да, эта схема безопасна, если все мьютексы будут захватываться только так. Но в реальных системах все намного сложнее. А что если в одном потоке замки будут лочиться через scoped_lock по адресной схеме, а в другом потоке - одиночно?
            1 поток            |            2 поток              |
-------------------------------|---------------------------------|
 lock(mutex2) // УСПЕШНО       |                                 |
                               | scoped_lock()                   |
                               |    lock(mutex1) // УСПЕШНО      |
                               |    lock(mutex2) // ОЖИДАНИЕ ... |
 lock(mutex1) // ОЖИДАНИЕ...   |                                 |
В этом случае настанет дедлок. Спасибо за пример Сергею Борисову. Ну или другую ситуацию рассмотрим: есть 4 замка l1, l2, l3, l4. Поток захватил замок с самым большим адресом l4 и надолго(потенциально навсегда) заблокировался. Но другие треды продолжают нормально работать. И они иногда захватывают пары мьютексов. Все продолжается нормально, пока один из потоков не пытается залочить l3 и l4. Из-за ордеринга захватится l3, а дальше поток будет ждать освобождения l4 aka заблокируется. Дальше другой поток будет пытаться захватить l2 и l3. Он захватит l2 и будет дожидаться l3. Логику можно продолжать и дальше. Таким образом из-за одного мьютекса и немного поломанного потока может остановиться вся программа.
std::mutex l1, l2, l3, l4;
// Пусть я как-то гарантирую, что они пронумерованы в порядке
 возрастания адресов и std::scoped_lock(sc_lock для краткости) 
работает с помощью сортировки по адресам

1 поток      |     2 поток    |     3 поток    |       4 поток       
-------------|----------------|----------------|----------------
 l4.lock();  |                |                |
//blocks here|                |                |
             |sc_lock(l3, l4);|                |
             | // lock l3     |                |
             | // blocks on l4|                |
                              |sc_lock(l2, l3);|
                              | // lock l2     |
                              | // blocks on l3|
                              |                | sc_lock(l1, l2);
                                               | // lock l1      
                                               | // blocks on l2
Примеры немного преувеличены, но тем не менее они говорят о том, что схема с адресами не совсем безопасна. Так может тогда вообще не будем блокироваться при уже захваченном мьютексе? Именно это и делают в реализации стандартной библиотеки. Первый захват мьютекса происходит через обычный lock(), а остальные мьютексы пытаются заблокировать через try_lock. Можно сказать, что это lock-free взятие замка. Если мьютекс можно захватить - захватываем, если нет, то не блокируемся и дальше продолжаем исполнение. Так вот в случае, если хотя бы один try_lock для оставшихся замков вернул false, то реализация освобождает все захваченные замки и начинает попытку снова. Такой алгоритм позволит избежать неприятных последствий обеих ситуаций, представленных выше. ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ #concurrency

​​Порядок взятия замков. Ч2 #опытным Так в каком же порядке блокируются мьютексы в std::scoped_lock? Как я уже и говорил - в неопределенном. Но и здесь можно немного раскрыть детали.
The objects are locked by an unspecified series of calls to locktry_lock, and unlock.
Mutex-like объекты блочатся недетерминированной серией вызовов методов lock(), unlock() и try_lock(). Алгоритм можно представить некой игрой в поддавки. Мы пытаемся поочереди захватить мьютексы. И если на каком-то из какой-то из них занят, то мы не ждем, пока он освободится. Мы освобождаем все свои мьютексы, давая возможность другим потокам их захватить, и после этого начинаем пытаться захватывать замки заново. Зачем так сложно? А просто физически не может произойти ситуации, когда два потока захватили по набору замков и ждут, пока другие освободятся(а это и есть дедлок). Один из потоков точно пожертвует захваченными ресурсами в пользу другого и исполнение продолжится. При запуске кода из предыдущего поста вы можете увидеть вот такую картину(но не гарантирую):
128616222426688 Lock at address 0x56aef94a31e0 is acquired.
128616222426688 Try lock at address 0x56aef94a3220. Success
128616222426688 Try lock at address 0x56aef94a3260. Failed
128616222426688 Lock at address 0x56aef94a3220 is released.
128616222426688 Lock at address 0x56aef94a31e0 is released.
128616222426688 Lock at address 0x56aef94a31e0 is acquired.
128616222426688 Try lock at address 0x56aef94a3220. Success
128616222426688 Try lock at address 0x56aef94a3260. Failed
128616222426688 Lock at address 0x56aef94a3220 is released.
128616211940928 Lock at address 0x56aef94a3260 is acquired.
128616211940928 Try lock at address 0x56aef94a3220. Success
128616211940928 Try lock at address 0x56aef94a31e0. Success
Надо понимать, что это многопоточка и каких-то упорядоченных логов между потоками быть не может, поэтому надо немного напрячь извилины. (0x56aef94a31e0 - первый мьютекс, 0x56aef94a3220 - второй, 0x56aef94a3260 - третий) Смотрим. Поток 128616222426688 локает первый замок, пытается локнуть второй и делает это успешно, а вот третий не получается. Значит он освобождает свои два и пытается начать заново. Дальше видим такую же картину - на третьем мьютексе try_lock прошел неудачно -> освобождаем имеющиеся. Тут просыпается второй поток 128616211940928. И пишет, что он сразу заполучил третий замок. На самом деле он заблочил его еще до начала этой ситуации, так как первый поток не мог залочить третий мьютекс. Просто поток 128616211940928 уснул между локом и выводом на консоль. И дальше пытается захватить второй и первый замки и у него это успешно получается. То есть поток 128616222426688 пожертвовал своими захваченными замками в пользу потока 128616211940928. Вот так выглядит реализация функции std::lock(которая лежит под капотом std::scoped_lock) в gcc:
template<typename _L1, typename _L2, typename... _L3>
void lock(_L1& __l1, _L2& __l2, _L3&... __l3)
{
#if __cplusplus >= 201703L
  if constexpr (is_same_v<_L1, _L2> && (is_same_v<_L1, _L3> && ...))
  {
    constexpr int _Np = 2 + sizeof...(_L3);
    unique_lock<_L1> __locks[] = {
      {__l1, defer_lock}, {__l2, defer_lock}, {__l3, defer_lock}...
    };
    int __first = 0;
    do {
      __locks[__first].lock();
      for (int __j = 1; __j < _Np; ++__j)
      {
        const int __idx = (__first + __j) % _Np;
        if (!__locks[__idx].try_lock())
        {
          for (int __k = __j; __k != 0; --__k)
          __locks[(__first + __k - 1) % _Np].unlock();
          __first = __idx;
          break;
        }
      }
    } while (!__locks[__first].owns_lock()); 
    for (auto& __l : __locks)
      __l.release();
  }
  else
#endif
  {
    int __i = 0;
    __detail::__lock_impl(__i, 0, __l1, __l2, __l3...);
  }
}
Кто сможет - разберется, но что тут происходит в сущности - я описал выше. Give something up to get something else. Stay cool. #concurrency #cpp17

Порядок взятия замков. Ч1 #опытным В этом посте я намеренно совершил одну ошибку, как байт на комменты и следующие посты. Но вы как-то пропустили ее, хотя и разбирали ту же самую тему в комментариях.
Cтандартное решение этой проблемы дедлока из постов выше - лочить замки в одном и том же порядке во всех потоках.
На самом деле залочивание мьютексов в одном и том же порядке - не общепринятая концепция решения проблемы блокировки множества замков. Это лишь одна из стратегий. И она не используется в стандартной библиотеке! Когда-то у меня тоже была уверенность, что std::scoped_lock блочит мьютексы в порядоке их адресов. Условно, в начале лочим замок с меньшим адресом. Потом с большим и так далее. Но как я и написал в середине того же поста, что std::scoped_lock вообще не гарантирует никакого порядка залочивания. Гарантируется только что такой порядок не может привести к дедлоку. Давайте посмотрим на следующий пример:
std::mutex log_mutex;

struct MyLock {
  MyLock() = default;
  
  void lock() {
    mtx.lock();
    std::lock_guard lg{log_mutex};
    std::cout << "Lock at address " << &mtx << " is acquired." << std::endl;
  }

  bool try_lock() {
    auto result = mtx.try_lock();
    std::lock_guard lg{log_mutex};
    std::cout << std::this_thread::get_id() << " Try lock at address " << &mtx << ". " << (result ? "Success" : "Failed") << std::endl;
    return result;
  }
  
  void unlock() {
    mtx.unlock();
    std::lock_guard lg{log_mutex};
    std::cout << "Lock at address " << &mtx << " is released." << std::endl;
  }

private:
std::mutex mtx;
};

MyLock lock1;
MyLock lock2;
MyLock lock3;  

constexpr size_t iteration_count = 100;

void func_thread1() {
  size_t i = 0;
  while(i++ < iteration_count) {
    {
      std::lock_guard lg{log_mutex};
      std::cout << std::endl << std::this_thread::get_id() << " Start acquiring thread1" << std::endl << std::endl;
    }
    std::scoped_lock scl{lock1, lock2, lock3};
    std::lock_guard lg{log_mutex};
    std::cout << std::endl << std::this_thread::get_id() << " End acquiring thread1" << std::endl << std::endl;
  }
}

void func_thread2() {
  size_t i = 0;
  while(i++ < iteration_count) {  
    {
      std::lock_guard lg{log_mutex};
      std::cout << std::endl << std::this_thread::get_id() << " Start acquiring thread2" << std::endl << std::endl;
    }  
    std::scoped_lock scl{lock3, lock2, lock1};
    std::lock_guard lg{log_mutex};
    std::cout << std::endl << std::this_thread::get_id() << " End acquiring thread2" << std::endl << std::endl;
  }
}

int main() {
  std::jthread thr1{func_thread1};
  std::jthread thr2{func_thread2};
}
Все довольно просто. Определяем класс-обертку вокруг std::mutex, который позволит нам логировать все операции с ним, указывая идентификатор потока. Определяем все методы, включая try_lock, чтобы MyLock можно было использовать с std::scoped_lock. Также определяем 2 функции, которые будут запускаться в разных потоках и пытаться локнуть сразу 3 замка. И блокируют они их в разных порядках. Все это в циклах, чтобы какую-то статистику иметь. С потоками сложно детерминировано общаться. Запускаем это дело и смотрим на вывод консоли. Там будет огромное полотно текста, но вы сможете заметить в нем "несостыковочку" с теорией про блокировку по адресам. Возможный кусочек вывода:
129777453237824 Start acquiring thread1

129777453237824 Lock at address 0x595886d571e0 is acquired.
129777453237824 Try lock at address 0x595886d57220
129777453237824 Try lock at address 0x595886d57260
...
129777442752064 Start acquiring thread2

129777442752064 Lock at address 0x595886d57260 is acquired.
129777442752064 Try lock at address 0x595886d57220
129777442752064 Try lock at address 0x595886d571e0
Тут наглядно показано, что мьютексы лочатся в противоположном порядке в разных потоках. И в коде мьютексы передаются в скоупд лок в противоположном порядке. А значит дело тут не в адресах, а в чем-то другом. О этом в следующий раз. Don't get fooled. Stay cool. #concurrency #cpp17

Цензура ЛЮБОЙ нейронки разлетается в щепки только от ОДНОГО промпта. После ввода — никаких запретов. Генерим хоть самый жестк
Цензура ЛЮБОЙ нейронки разлетается в щепки только от ОДНОГО промпта. После ввода — никаких запретов. Генерим хоть самый жесткий контент. Мегапромпт лежит здесь.

Спасибо всем, кто поделился своими лав-стори в комментах. Такие разные истории, но в то же время одинаковые. И во всех них сквозит одна мысль: "Двигайся по зову своего внутреннего огня. Он приведет тебя к цели" Если что-то не нравится, никогда не поздно это поменять. Если что-то нравится, делай это и жизнь будет радостней. И это касается не только программирования естественно. Рад, что у нас здесь собрались такие разные люди под одной двойнопозитивной крышей. Просто решил поделиться с вами своими эмоциями. Всем продуктивного дня! Be passionate. Stay cool.

А как вы пришли к программированию на плюсах? Как это изменило вашу жизнь?
А как вы пришли к программированию на плюсах? Как это изменило вашу жизнь?

std::lock #опытным Сейчас уже более менее опытные разрабы знают про std::scoped_lock и как он решает проблему блокировки множества мьютексов. Однако и в более старом стандарте С++11 есть средство, позволяющее решать ту же самую проблему. Более того std::scoped_lock - это всего лишь более удобная обертка над этим средством. Итак, std::lock. Эта функция блокирует 2 и больше объектов, чьи типы удовлетворяют требованию Locable. То есть для них определены методы lock(), try_lock() и unlock() с соответствующей семантикой. Причем порядок, в котором блокируются объекты - не определен. В стандарте сказано, что объекты блокируются с помощью неопределенной серии вызовов методов lock(), try_lock и unlock(). Однако гарантируется, что эта серия вызовов не может привести к дедлоку. Собстна, для этого все и затевалось. Штука эта полезная, но не очень удобная. Сами посудите. Эта функция просто блокирует объекты, но не отпускает их. И это в эпоху RAII. Ай-ай-ай. Поэтому ее безопасное использование выглядит несколько вычурно:
struct SomeSharedResource {
  void swap(SomeSharedResource& obj) {
    {
      // !!!
          std::unique_lock<std::mutex> lk_c1(mtx, std::defer_lock);
          std::unique_lock<std::mutex> lk_c2(obj.mtx, std::defer_lock);
          std::lock(mtx, obj.mtx);
      // handle swap
    }
  }
  std::mutex mtx;
};

int main() {
  SomeSharedResource resource1;
  SomeSharedResource resource2;  
  std::mutex m2;
  std::thread t1([&resource1, &resource2] {
    resource1.swap(resource2);
    std::cout << "1 Do some work" << std::endl;
  });
  std::thread t2([&resource1, &resource2] {
    resource2.swap(resource1);
    std::cout << "2 Do some work" << std::endl;
  });

  t1.join();  
  t2.join();
}
Раз мы все-таки за безопасность и полезные практики, то нам приходится использовать std::unique_lock'и на мьютексах. Только нужно передать туда параметр std::defer_lock, который говорит, что не нужно локать замки при создании unique_lock'а, его залочит кто-то другой. Тем самым мы убиваем 2-х зайцев: и RAII используем для автоматического освобождения мьютексов, и перекладываем ответственность за блокировку замков на std::lock. Можно использовать и более простую обертку, типа std::lock_guard:
struct SomeSharedResource {
  void swap(SomeSharedResource& obj) {
    {
      // !!!
          std::lock(mtx, obj.mtx);
          std::lock_guard<std::mutex> lk_c1(mtx, std::adopt_lock);
          std::lock_guard<std::mutex> lk_c2(obj.mtx, std::adopt_lock);
      // handle swap
    }
  }
  std::mutex mtx;
};
Здесь мы тоже используем непопулярный конструктор std::lock_guard: передаем в него параметр std::adopt_lock, который говорит о том, что мьютекс уже захвачен и его не нужно локать в конструкторе lock_guard. Можно и ручками вызвать .unlock() у каждого замка, но это не по-православному. Использование unique_lock может быть оправдано соседством с условной переменной, но если вам доступен C++17, то естественно лучше использовать std::scoped_lock. Use modern things. Stay cool. #cpp11 #cpp17 #concurrency

​​Локаем много мьютексов #опытным Cтандартное решение этой проблемы дедлока из постов выше - лочить замки в одном и том же порядке во всех потоках. Но как это сделать? Они не же на физре, "по порядку рассчитайсьььь" не делали. Можно конечно на ифах городить свой порядок на основе, например, адресов мьютексов. Но это какие-то костыли и так делать не надо. Так как проблема довольно стандартная, то и решение мы скорее всего найдем в том же стандарте. std::scoped_lock был введен в С++17 и представляет собой RAII обертку над локом множества мьютексов. Можно сказать, что это std::lock_guard на максималках. То есть буквально, это обертка, которая лочит любое количество мьютексов от 0 до "сами проверьте верхнюю границу". Но есть один важный нюанс. Никак не гарантируется порядок, в котором будут блокироваться замки. Гарантируется лишь то, что выбранный порядок не будет приводить к dead-lock'у. Пример из прошлого поста может выглядеть теперь вот так:
struct SomeSharedResource {
  void swap(SomeSharedResource& obj) {
    {
      std::scoped_lock lg{mtx, obj.mtx};
      // handle swap
    }
  }
  std::mutex mtx;
};

int main() {
  SomeSharedResource resource1;
  SomeSharedResource resource2;  
  std::mutex m2;
  std::thread t1([&resource1, &resource2] {
    resource1.swap(resource2);
    std::cout << "1 Do some work" << std::endl;
  });
  std::thread t2([&resource1, &resource2] {
    resource2.swap(resource1);
    std::cout << "2 Do some work" << std::endl;
  });

  t1.join();  
  t2.join();
}
И все. И никакого дедлока. Однако немногое лишь знают, что std::scoped_lock - это не только RAII-обертка. Это еще и более удобная обертка над "старой" функцией из С++11 std::lock. О ней мы поговорим в следующий раз. Ведь не всем доступны самые современные стандарты. Да и легаси код всегда есть. Be comfortable to work with. Stay cool. #cpp17 #cpp11 #concurrency

​​Зачем локать мьютексы в разном порядке #новичкам Можете подумать, что раз всем так известно, что мьютексы опасно лочить в разном порядке, то почему вообще кому-то в голову может прийти наступить на грабли и накалякать такое своими программисткими пальчиками снова? Просто не пишите хреновый код и будет вам счастье. Дело в том, что иногда это не совсем очевидно. Точнее почти всегда это не очевидно. Программисты хоть и разные бывают, но ревью никто не отменял и такую броскую ошибку бы явно заметили. Поэтому не все так просто, как на первый взгляд кажется. Разберем чуть более сложный пример:
struct SomeSharedResource {
  void swap(SomeSharedResource& obj) {
    {
      std::lock_guard lg{mtx};
      // just for results reproducing
      std::this_thread::sleep_for(std::chrono::milliseconds(10));
      std::lock_guard lg1{obj.mtx};
      // handle swap
    }
  }
  std::mutex mtx;
};

int main() {
  SomeSharedResource resource1;
  SomeSharedResource resource2;  
  std::mutex m2;
  std::thread t1([&resource1, &resource2] {
    resource1.swap(resource2);
    std::cout << "1 Do some work" << std::endl;
  });
  std::thread t2([&resource1, &resource2] {
    resource2.swap(resource1);
    std::cout << "2 Do some work" << std::endl;
  });

  t1.join();  
  t2.join();
}
Все просто. У нас есть пара объектов, которые используются в качестве разделяемых ресурсов (их могут изменять более 1 потока). Объекты могут обмениваться между собой данными. Ну и по всем канонам нам же надо защитить оба объекта при свопе. Поэтому после захода в функцию обмена сначала лочим свой мьютекс, а потом мьютекс объекта с которым собираемся свопаться. И вроде снаружи кажется, что мы всегда лочим в одном порядке. Пока мы не будем обменивать данные одних и тех же объектов в разных потоках в разном порядке. Тогда первый поток может залочить мьютекс первого объекта и, допустим, заснуть. А второй поток первым залочит мьютекс второго объекта. И все. Приплыли. И это все еще может показаться детским садом и очевидушкой. Только вот сам момент взятия замка уже не выглядит так подозрительно. А сами объекты могут лежать в каких-то страшных структурах данных и могут быть разбросаны по коду. В таком случае все не так очевидно становится. В следующий раз поговорим, как же не допускать такие ошибки. Don't be obvious. Stay cool. #concurrency

🚀 Приглашаем на бесплатный вебинар по C++! 🚀 Дата: 19 ноября 2024 года Время: 20:00 Тема: Как протестировать C++ код и оцен
🚀 Приглашаем на бесплатный вебинар по C++! 🚀 Дата: 19 ноября 2024 года Время: 20:00 Тема: Как протестировать C++ код и оценить степень собственной лени На вебинаре поговорим о том, зачем разработчикам писать юнит-тесты, и какую пользу они несут. Посмотрим популярные фреймворки тестирования, такие как GTest и Boost, разберем несколько практических примеров. Затем попробуем оценить, достаточно ли тестов мы написали для своего кода. На занятии: 1. Научимся подключать фреймворки тестирования к своему проекту при помощи CMake. 2. Напишем готовые к запуску тесты. 3. Поговорим о том, как измерить покрытие тестами кода, какие инструменты для этого существуют. В результате: 1. Придем к тому, что вместе с кодом стоит сразу писать и юнит-тесты к нему. 2. В будущем с легкостью интегрируем любой фреймворк тестирования в свой проект. 3. Научимся не только писать тесты, но и мерить их покрытие. 👉🏻О курсе "Специализация C++ Developer" на Otus: Длительность курса: 10 месяцев. Формат: Онлайн. Программа курса: · Введение в язык C++: основы синтаксиса, структура программ, базовые конструкции. · Классы и структуры: ООП, наследование, полиморфизм, шаблоны. · Основы unit-тестирования: подключение фреймворков, написание тестов, измерение покрытия. · Стандартная библиотека и полезные алгоритмы: контейнеры, ввод-вывод, алгоритмы. 📌Скидка 15%: действует до 17 ноября! Не упустите шанс стать профессионалом в C++! Присоединяйтесь к вебинару и узнайте больше о курсе. 🔗 Регистрация на вебинар Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

Дедлокаем код #новичкам Частый вопрос на собесах или даже на скринингах - сколько минимум нужно мьютексов, чтобы гарантировано скрафтить дедлок. Прежде, чем приступим к разбору, небольшое введение. Deadlock - это ситуация, когда два или более потока оказываются заблокированными и не могут продолжить свою работу, так как каждый из них ожидает ресурс, который удерживает другой заблокированный поток. В результате, ни один из потоков не может завершиться, а система оказывается в застойном состоянии. Многопоточка - это мир магии и чудес. Ошибки, которые появляются по причине плохой организации доступа потоков к ресурсам - самые трудновоспроизводимые и сложные для отладки. И никогда не знаешь откуда выстрелит. Да и сами ошибки принимают причудливые облики. Поэтому дедлоки могут по разному быть проявлены в коде, но поведение программы всегда при этом стабильно убогое - потоки не могут дальше продолжать производить работу. В рамках текущего поста будем обсуждать так называемую циклическую блокировку. Ща поймете, о чем речь. Стандартный ответ на вопрос из начала - 2. В одном потоке блочим в начале первый мьютекс, потом второй, а в другом потоке наоборот. Таким образом может произойти ситуация, когда оба потока захватили в заложники каждый по одному мьютексу и бесконечно висят в ожидании освобождения второго. Этого никогда не произойдет, потому что каждому нужен замок, который уже захватил другой поток и он его отдавать не собирается, пока пройдет всю критическую секцию. А он ее никогда не пройдет. В общем, собака пытается укусить себя за чресла. Самый простой пример, который иллюстирует эту ситуацию:
std::mutex m1;
std::mutex m2;

std::thread t1([&m1, &m2] {
  std::cout << "Thread 1. Acquiring m1." << std::endl;
  m1.lock();
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  std::cout << "Thread 1. Acquiring m2." << std::endl;
  m2.lock();
  std::cout << "Thread 1 perform some work" << std::endl;
});

std::thread t2([&m1, &m2] {
  std::cout << "Thread 2. Acquiring m2." << std::endl;
  m2.lock();
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  std::cout << "Thread 2. Acquiring m1." << std::endl;
  m1.lock();
  std::cout << "Thread 2 perform some work" << std::endl;
});

t1.join();
t2.join();
Все канонично. В двух разных потоках пытаемся залочить замки в разном порядке. В итоге вывод будет такой:
Thread 1. Acquiring m1.
Thread 2. Acquiring m2.
Thread 2. Acquiring m1.
Thread 1. Acquiring m2.
И дальше программа будет бесконечно простаивать без дела, как тостер на вашей кухне. Do useful work. Stay cool. #concurrency

Получите оффер Software Engineer всего за 3 дня! 🚀 Вендор и производитель IT-инфраструктуры YADRO приглашает Software Engine
Получите оффер Software Engineer всего за 3 дня! 🚀 Вендор и производитель IT-инфраструктуры YADRO приглашает Software Engineer на SPRINT OFFER. Команда KVADRA OS ждёт кандидатов сразу на два направления: Linux-based (от уровня junior) и Android (уровня middle и senior). В YADRO под брендом KVADRA ребята создают персональные устройства, а также разрабатывают собственную операционную систему kvadraOS, которая поддерживает облачное хранилище и включает множество компонентов собственной разработки — от системного ядра до приложений и сервисов. 🔵 Присоединившись к YADRO, вы станете частью большого инженерного сообщества и получите возможность влиять на продукт, работая в уникальной команде специалистов. Читайте подробности на сайте, оставляйте заявку до 24 ноября и присоединяйтесь к команде KVADRA OSпо ссылке.

Возвращаем ссылку в std::optional #новичкам В прошлом посте я упоминал, что методы front и back последовательных контейнеров играют в ящик, если их пытаться вызвать на пустом контейнере. Это приводит к UB. Один из довольно известных приемов для обработки таких ситуаций - возвращать не ссылку на объект, а std::optional. Это такая фича С++17 и класс, который может хранить или не хранить объект. Теперь, если контейнер пустой - можно возвращать std::nullopt, который создает std::optional без объекта внутри. Если в нем есть элементы, то возвращать ссылку. Только вот проблема: std::optional нельзя создавать с ссылочным типом. А копировать объект ну вот никак не хочется. А если он очень тяжелый? Мы программисты и тяжести поднимать не любим. И вроде бы ситуация безвыходная. Но нет! Решение есть! Можно возвращать std::optional<std::reference_wrapper<T>>. std::reference_wrapper - это такая обертка над ссылками, чтобы они вели себя как кошерные объекты со своими блэкджеком, конструкторами, деструкторами и прочими прелестями. Это абсолютно легально и теперь у вас никакого копирования нет!
std::optional<std::reference_wrapper<T>> Container::front() {
  if (data_.empty()) {
    return std::nullopt;
  }
  return std::ref(data_[0]);
}
И в добавок есть нормальная безопасная проверка. Пользуйтесь. Stay safe. Stay cool. #cpp17 #STL #goodpractice

А что вы думаете по поводу вектора и скорости развития С++? И почему у нас до сих пор нет стандартного тредпула??
А что вы думаете по поводу вектора и скорости развития С++? И почему у нас до сих пор нет стандартного тредпула??