КОДЕКС

C ++ Многопоточность, простой способ

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

Многопоточность часто имеет репутацию сложной. По сравнению с другими концепциями разработки программного обеспечения, это, безусловно, можно было бы убедить. Однако многопоточность на самом деле не так уж отличается от общего программирования. Это просто потенциально более опасно. Однако умение защищаться от опасности может позволить реализовать гораздо более мощные алгоритмы и программы, чем вы могли бы в однопоточном режиме.

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

Параллелизм

Возможно, наименее опасная форма многопоточности - это параллелизм. Под параллелизмом обычно подразумевается одновременное выполнение нескольких потоков, но без совместного использования каких-либо ресурсов. Это означает, что никакие структуры данных, память или что-либо другое не разделяется между потоками. Параллелизм обычно используется для задач, которые можно разделить между потоками и работать над ними независимо.

Чтобы проиллюстрировать это, давайте рассмотрим пример, когда каждый поток получает указатель на целое число, и поток увеличивает это целое число, а затем останавливается. Каждый поток запускается, пока не увеличит число в несколько сотен раз. Затем эти потоки, часто называемые «рабочими» потоками, присоединяются к основному потоку. Все потоки работают одновременно.

Если вы новичок в многопоточности, некоторые части этого кода могут не иметь смысла. Метод join(), вероятно, один из них. Важная деталь для понимания запуска новых потоков заключается в том, что они работают и функционируют полностью отдельно от основного потока, потока, который начинается в main(). Поскольку они полностью отделены друг от друга, мы должны решить, в какой момент мы хотим дождаться, пока они завершат свою работу.

Подумайте о join() аналогично тому, как два человека могут разделиться, чтобы выполнять свои собственные отдельные задачи, а затем снова «объединиться» вместе. Если вы путешествуете или собираетесь куда-то с другом, вы не хотите, чтобы он просто бросил их! В идеале вам следует подождать, пока они снова наверстают упущенное. Та же логика применима к потокам. Каждый раз, когда создаются дополнительные потоки, необходимо указать, как вы хотите, чтобы центральный, основной поток действовал в соответствии с ними.

Отсоединение

Вам всегда нужно присоединяться к темам? Нет. На самом деле есть еще один вариант. Как и в случае с друзьями, возможно, что друг захочет вернуться домой своим собственным путем, а не встречать вас снова. В случае нитей это называется отсоединением. Отсоединение потока означает разрешение ему работать и завершать свою работу независимо от основного потока. Но это может быть опасно. Возьмем следующий пример, очень похожий на пример для join().

Первый риск здесь заключается в использовании кучи, выделенной после ее удаления. В отличие от join(), detach() не останавливает вызывающий поток и не ожидает чего-либо. Это означает, что как только закончится третий вызов detach(), вызывающий поток удалит массив numbers. Если созданные потоки не завершили свою работу, они будут записывать в удаленный массив, что повредит память.

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

Общие ресурсы

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

В отличие от параллелизма, общий ресурс используется, когда желательно, чтобы потоки выполняли работу с одними и теми же данными или объектом. Это означает, что объекты не размещены в собственном стеке потока и видны другим потокам только один. Что может сделать это сложным для понимания, так это то, что, хотя оба потока могут получить доступ к некоторому ресурсу, они никогда не смогут увидеть другие потоки, обращающиеся к этому ресурсу.

Прекрасным примером общего ресурса в реальной жизни является взлетно-посадочная полоса в ночное время. Взлетно-посадочная полоса имеет мигающие огни, чтобы помочь самолетам выровняться с ней при подготовке к посадке. Но другим самолетам очень трудно, если не невозможно, видеть друг друга ночью из-за темноты и скорости, с которой они летают. Если самолет попытается приземлиться на взлетно-посадочной полосе одновременно с другим самолетом, это будет катастрофой. Единственный способ избежать таких столкновений - это согласовать действия авиадиспетчерской службы.

Потоки функционируют одинаково в том смысле, что они зависят от механизмов синхронизации для координации доступа к ресурсам, например, не записывают в них в одно и то же время. Механизм, который мы здесь обсудим, возможно, самый распространенный, - это мьютекс. Мьютекс, известный под типом std::mutex, позволяет потокам получать блокировки . Блокировки - это форма управления, которая позволяет только одному потоку проходить через часть кода за раз. Давайте посмотрим на этот пример.

В приведенном выше примере методы push() и pop() класса выполняются, пока вызывающий поток создает блокировку мьютекса, связанного с очередью. Эту блокировку лучше всего использовать в качестве объекта стиля RAII, где она активна только в определенном объеме кода. Как только программа завершает эту область видимости, объект защиты блокировки уничтожается, позволяя другому потоку построить и получить блокировку мьютекса. Этот шаблон продолжает удовлетворять условию, что только один поток может изменять очередь за раз.

Мьютексы по-прежнему потенциально опасны

Несмотря на то, что они действительно звучат аккуратно и прямо, мьютексы все же могут быть опасными. Когда поток получает блокировку мьютекса, он отвечает за освобождение или разрушение этой блокировки, чтобы другие потоки также могли получить доступ к защищенной области кода. Что произойдет, если поток так и не освободит полученную блокировку? Что ж, что-то действительно плохое.

утечка блокировки - это когда поток блокирует мьютекс, но затем эту блокировку по какой-то причине невозможно разблокировать. Если это произойдет, все потоки будут заблокированы и будут ждать мьютекса неопределенное время, не выполняя никаких действий или выполняя какую-либо работу.

Практическое правило для мьютексов - тщательно и критически относиться к тому, что делает поток, когда блокирует мьютекс. Жизненно важно, чтобы поток блокировался только тогда, когда ему абсолютно необходим доступ к одному потоку, и при этом он работал как можно быстрее. Хотя мьютексы предоставляют средства для безопасного доступа к одному и тому же ресурсу, они делают это за счет снижения производительности.

Есть ли другие способы предотвратить незаконный двойной доступ к ресурсам между несколькими потоками? да. Но это тема для другого поста.