Frontoil.ru

Авто Масла
14 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

4 совета по работе с потоками и мьютексами в C

4 совета по работе с потоками и мьютексами в C++

Книга 4 совета по работе с потоками и мьютексами в C++

C++ представляет собой отличный язык программирования, который принято ассоциировать с высокой производительностью, а также доступным управлением памятью и указателями. Тем не менее в нем также есть очень важные, но менее обсуждаемые компоненты, такие как потоки и мьютексы. Одна из наиболее значимых характеристик С++ — это широкие возможности контроля и определения точности при выполнении нескольких параллельных потоков наряду с безопасным распределением ресурсов между ними. К примеру, браузер, в котором вы сейчас это читаете, как раз задействует несколько потоков, облегчая одновременное выполнение действий и демонстрацию представлений.

В данной статье я опишу четыре рекомендации по использованию потоков и мьютексов в С++. Интерфейсы, о которых мы будем здесь говорить, относятся к версиям стандарта С++11 и выше. Ранее этой версии потоки формально в нем не определялись, и использовать можно было только интерфейсы потоков ОС, такие как POSIX.

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

Любая переменная сначала создается в основном хранилище и лишь затем копируется в рабочую память потоков, которые будут ее применять.

Если переменная объявлена, как volatile, то ее чтение и запись будет производиться изв основное хранилище.

Чтение volatile переменных синхронизировано и запись в volatile переменные синхронизирована, а неатомарные операции – нет.

Алгоритм исследования функции двух переменных на условный экстремум

  1. Составить функцию Лагранжа $F(x,y)=f(x,y)+lambdavarphi(x,y)$
  2. Решить систему $ left < begin& frac=0;\ & frac=0;\ & varphi (x,y)=0. end right.$
  3. Определить характер экстремума в каждой из найденных в предыдущем пункте стационарных точек. Для этого применить любой из указанных способов:
    • Составить определитель $H$ и выяснить его знак
    • С учетом уравнения связи вычислить знак $d^2F$

Многопоточное программирование в Java 8. Часть вторая. Синхронизация доступа к изменяемым объектам

Обложка: Многопоточное программирование в Java 8. Часть вторая. Синхронизация доступа к изменяемым объектам

Добро пожаловать во вторую часть руководства по параллельному программированию в Java 8. В предыдущей части мы рассматривали, как выполнять код параллельно с помощью потоков, задач и сервисов исполнителей. Сегодня мы разберём, как синхронизировать доступ к изменяемым объектам с помощью ключевого слова synchronized , блокировок и семафоров.

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

Для простоты примеров я использую в них два метода-помощника: sleep(секунды) и stop(сервис-исполнитель) . Их реализации я выложил на GitHub, если кому-то интересно.

Синхронизация

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

Мы создаём поле count и метод increment() , который увеличивает count на единицу:

Если мы будем вызывать этот метод одновременно из двух потоков, у нас возникнут серьёзные проблемы:

Вместо ожидаемого постоянного результата 10000 мы будем каждый раз получать разные числа. Причина этого — использование изменяемой переменной несколькими потоками без синхронизации, что вызывает состояние гонки (race condition).

Revolut , Санкт-Петербург, Москва, можно удалённо , По итогам собеседования

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

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

Тогда, после выполнения метода 10000 раз, мы всегда будем получать значение 10000, и никакой гонки состояний возникать не будет:

Это ключевое слово можно применять не только к методам, но и к отдельным их блокам:

Под капотом Java использует так называемый монитор (monitor lock, intrinsic lock) для обеспечения синхронизации. Этот монитор привязан к объекту, поэтому синхронизированные методы используют один и тот же монитор соответствующего объекта. Все неявные мониторы устроены реентерабельно (reentrant), т.е. таким образом, что поток может без проблем вызывать блокировку одного и того же объекта, исключая взаимную блокировку (например, когда синхронизированный метод вызывает другой синхронизированный метод на том же объекте).

Блокировки

Кроме использования блокировок неявно (с помощью ключевого слова synchronized ), Concrurrency API предлагает много способов их явного использования, определённых интерфейсом Lock . С помощью явных блокировок можно настроить работу программы гораздо тоньше и тем самым сделать её эффективнее.

Стандартный JDK предоставляет множество реализаций Lock , которые мы сейчас и рассмотрим.

ReentrantLock

Класс ReentrantLock реализует то же поведение, что и обычные неявные блокировки. Давайте попробуем переписать наш пример с увеличением на единицу с помощью него:

Блокировка осуществляется с помощью метода lock() , а освобождаются ресурсы помощью метода unlock() . Очень важно оборачивать код в try<>finally<> , чтобы ресурсы освободились даже в случае выброса исключения. Код, представленный выше, так же потокобезопасен, как и его аналог с synchronized . Если один поток вызвал lock() , и другой поток пытается получить доступ к методу до вызова unlock() , то второй поток будет простаивать до тех пор, пока метод не освободится. Только один поток может удерживать блокировку в каждый момент времени.

Для большего контроля явные блокировки поддерживают множество специальных методов:

Пока первый поток удерживает блокировку, второй выведет следующую информацию:

Locked: true
Held by me: false
Lock acquired: false

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

ReadWriteLock

Интерфейс ReadWriteLock предлагает другой тип блокировок — отдельную для чтения, и отдельную для записи. Этот интерфейс был добавлен из соображения, что считывать данные (любому количеству потоков) безопасно до тех пор, пока ни один из них не изменяет переменную. Таким образом, блокировку для чтения (read-lock) может удерживать любое количество потоков до тех пор, пока не удерживает блокировка для записи (write-lock). Такой подход может увеличить производительность в случае, когда чтение используется гораздо чаще, чем запись.

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

Если вы попробуете запустить этот пример, то заметите, что оба потока, созданные для чтения, будут простаивать секунду, ожидая завершения работы потока для записи. После снятия блокировки они выполнятся параллельно, и одновременно запишут результат в консоль. Им не нужно ждать завершения работы друг друга, потому что выполнять одновременное чтение вполне безопасно (до тех пор, пока ни один поток не работает параллельно на запись).

StampedLock

В Java 8 появился новый тип блокировок — StampedLock . Так же, как и в предыдущих примерах, он поддерживает разделение на readLock() и writeLock() . Однако, в отличие от ReadWriteLock , метод блокировки StampedLock возвращает «штамп» — значение типа long . Этот штамп может использоваться в дальнейшем как для высвобождения ресурсов, так и для проверки состояния блокировки. Вдобавок, у этого класса есть методы, для реализации «оптимистичной» блокировки. Но обо всём по порядку.

Вот таким образом следовало бы переписать наш предыдущий пример под использование StampedLock :

Работать этот код будет точно так же, как и его брат-близнец с ReadWriteLock . Тут, правда, стоит упомянуть, что в StampedLock не реализована реентерантность. Поэтому особое внимание нужно уделять тому, чтобы не попасть в ситуацию взаимной блокировки (deadlock).

В следующем примере демонстрируется «оптимистичная блокировка»:

Оптимистичная блокировка для чтения, вызываемая с помощью метода tryOptimisticRead() , отличается тем, что она всегда будет возвращать «штамп» не блокируя текущий поток, вне зависимости от того, занят ли ресурс, к которому она обратилась. В случае, если ресурс был заблокирован блокировкой для записи, возвращённый штамп будет равняться нулю. В любой момент можно проверить, является ли блокировка валидной с помощью lock.validate(stamp) . Для приведённого выше кода результат будет таким:

Optimistic Lock Valid: true
Write Lock acquired
Optimistic Lock Valid: false
Write done
Optimistic Lock Valid: false

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

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

Иногда может быть полезным преобразовать блокировку для чтения в блокировку для записи не высвобождая ресурсы. В StampedLock это можно сделать с помощью метода tryConvertToWriteLock() , как в этом примере:

В этом примере мы хотим прочитать значение переменной count и вывести его в консоль. Однако, если значение равно нулю, мы хотим изменить его на 23. Для этого нужно выполнить преобразования из readLock во writeLock, чтобы не помешать другим потокам обрабатывать переменную. В случае, если вы вызвали tryConvertToWriteLock() в тот момент, когда ресурс занят для записи другим потоком, текущий поток остановлен не будет, однако метод вернёт нулевое значение. В таком случае можно вызвать writeLock() вручную.

Семафоры

Семафоры — отличный способ ограничить количество потоков, которые одновременно работают над одним и тем же ресурсом:

В этом примере сервис-исполнитель может потенциально запустить все 10 вызываемых потоков, однако мы создали семафор, который ограничивает количество одновременно выполняемых потоков до пяти. Снова напомню, что важно освобождать ресурсы именно в блоке finally<> на случай выброса исключений. Для приведённого выше кода вывод будет следующим:

Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore

Это была вторая часть серии статей про многопоточное программирование. Настоятельно рекомендую разобрать вышеприведенные примеры самостоятельно. Все они, как обычно, доступны на GitHub. Можете смело форкать репозиторий и добавлять его в избранное.

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

Задачи

Вам даны две переменные x и y, менять их не нужно. При помощи тернарного оператора сравните их и если x больше, чем y, то выведите фразу: «x больше, чем y», иначе выведите фразу: «x не больше, чем y».

Ответ: «x больше, чем y».

Четное или нечетное число

Пользователь вводит какое-то число (num). Используя конструкцию if..else, напишите код, который делает запрос: «Введите число».
Если посетитель вводит четное число, то выводить «»Число » + num + » четное»», если нечетное: «Число » + num + » нечетное».

Суть решения заключается в том, что проверяется остаток от деления числа на 2. Если остаток равен , значит число чётное, иначе – нечётное. Получить остаток от деления в можно с помощью оператора %.

Сколько цифр в числе и его знак

Напишите код, который предлагает пользователю ввести целое число. Нужно вывести на экран сколько в этом числе цифр, а также положительное оно или отрицательное. Например, «Число » + num + » однозначное положительное». Достаточно будет определить, является ли число однозначным, двухзначным или трехзначным и более.

  1. Проверяем, не является ли число нулем. Ноль не является ни положительным, ни отрицательным.
  2. После этого проверяем количество знаков в в положительном числе и выводим на эран соответствующее сообщение.
  3. Проверяем количество знаков в отрицательном числе, выводим сообщение.

Принадлежность точки окружности

Даны координаты точки A(x = 4, y = 9) и радиус окружности (R = 10) с центром в начале координат. Используя тернарный оператор напишите код, который будет выводить сообщение о том, лежит ли данная точка внутри окружности или за её пределами. Для извлечения квадратного корня из числа z вам понадобится метод Math.sqrt(z) .

Принадлежность точки окружности

Следует рассмотреть прямоугольный треугольник, один катет которого лежит на любой оси, а другой является перпендикуляром к этой оси из заданной точки A. В этом случае длины катетов треугольника равны значениям x и y, а гипотенуза L является отрезком, соединяющим начало координат O с точкой A. Если этот отрезок L больше радиуса круга R, то значит точка лежит вне окружности.

Длина гипотенузы находится по теореме Пифагора: L = Math.sqrt(x*x + y*y).

Определить существование треугольника по трем сторонам

У треугольника сумма любых двух сторон должна быть больше третьей. Иначе две стороны просто <лягут> на третью и треугольника не получится.
Пользователь вводит поочерёдно через prompt длины трех сторон. Используя конструкцию if..else, напишите код, который должен определять, может ли существовать треугольник при таких длинах. Т. е. нужно сравнить суммы двух любых строн с оставшейся третьей стороной. Чтобы треугольник существовал, сумма всегда должна быть больше отдельной стороны.

Треугольник

Поскольку у треугольника три стороны, то можно составить три варианта сложения двух сторон: a + b, b + c, a + c. Первую сумму сравниваем с оставшейся стороной c, вторую — с a и третью — с b. Если хотя бы в одном случае сумма окажется не больше третьей стороны, то можно сделать вывод, что треугольник не существует.

Оператор if . else

Давайте глянем на наиболее распространённый тип условного оператора, который вы будете использовать в JavaScript — if . else оператор.

Базовый if . else синтаксис

Базовый if. else синтаксис выглядит как pseudocode:

  1. Ключевое слово if расположено перед круглыми скобками.
  2. Условие для проверки (condition), расположено внутри круглых скобок (например «это значение больше другого значения?», или «это значение существует?»). Это условие использует операторы сравнения (comparison operators), которые мы изучим позже, и возвратит нам true или false .
  3. Внутри скобок < >расположен код, который будет выполняться только в том случае, если условие (condition) верно ( true) .
  4. Ключевое слово else (иначе) .
  5. Ещё скобки < >, код внутри которых выполнится, только если условие не верно (не true) .

Этот код довольно читабелен — он говорит «if (если) condition (условие) возвращает true (истина) , запусти код A, else (иначе) запусти B»

Стоит заметить, что else и второй блок скобок < >не обязателен — следующий код так же будет работать:

Тем не менее, следует быть осторожным — в случае, если код внутри вторых скобок < >не контролируется условием, то этот код будет выполняться всегда. Это не плохо, просто вы должны помнить об этом, чаще вы хотите запустить один кусок кода или другой, но не оба.

И, наконец, иногда вы можете встретить код if. else без фигурных скобок в сокращённой форме:

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

Реальный пример

Чтобы лучше понять синтаксис, давайте рассмотрим реальный пример. Представьте, что мать или отец попросили помочь с работой по дому своего ребёнка. Родитель может сказать: «Если ты поможешь мне с покупками, то я дам тебе дополнительные деньги на карманные расходы, которые ты сможешь потратить на игрушку, какую захочешь». В JavaScript, мы можем представить это так:

В этом коде, как показано, всегда будет shoppingDone равный false , что означает разочарование для нашего бедного ребёнка. Мы должны предоставить механизм для родителя, чтобы установить для переменной shoppingDone значение true , если ребёнок помог с покупками.

Примечание: вы можете увидеть больше в полной версии этого примера на GitHub (также посмотреть как он работает вживую.)

else if

В предыдущем примере предоставлено два выбора, или результата — но что, если мы хотим больше, чем два?

Существует способ привязать дополнительные варианты/результаты к вашему if. else — использовать else if . Для каждого дополнительного выбора требуется дополнительный блок, который нужно расположить между if() < . >и else < . >— проверьте следующий более сложный пример, который может быть частью простого приложения прогноза погоды:

  1. Здесь у нас есть элемент HTML <select> который позволяет нам выбирать разные варианты погоды и простой абзац.
  2. В JavaScript мы создаём ссылки на элементы <select> и <p> и добавляем обработчик события элемента <select> , чтобы при его изменении значения запускалась функция setWeather() .
  3. Когда функция будет запущена, первоначально мы определим значение переменной choice , которая равна выбранному значению в элементе <select> . Затем мы используем условный оператор для отображения текста внутри абзаца в зависимости от того, какое значение у переменной choice . Обратите внимание, как все условия проверяются в else if() <. >блоках, за исключением первого, который использует if() <. >блок.
  4. Последний выбор, внутри else <. >блока, в основном является «последним средством» — код внутри него будет запущен, если ни одно из условий не будет true . В этом случае он служит для удаления текста из абзаца, если ничего не выбрано, например, если пользователь решает повторно выбрать опцию «—Сделайте выбор—» которая указана в начале.

Примечание об операторах сравнения

Операторы сравнения используют для проверки условий внутри наших условных операторов. Сначала мы посмотрели на операторы сравнения в нашей статье Базовая математика в JavaScript — цифры и операторы . Наш выбор это:

  • === и !== — проверяет одно значение идентично или не идентично другому.
  • < и > — проверяет одно значение меньше или больше, чем другое.
  • <= и >= — проверяет одно значение меньше или равно, либо больше или равно другому.

Примечание: Просмотрите материал по предыдущей ссылке, если вы хотите освежить свою память.

Мы хотели бы особо обратить внимание на проверку булевых значений ( true / false ), и общий шаблон, который вы будете встречать снова и снова. Любое значение, которое не есть false , undefined , null , 0 , NaN , или пустая строка ( » ) фактически возвращает true при тестировании как условного оператора. Поэтому вы можете просто использовать имя собственной переменной, чтобы проверить, равна ли она true , или существует (т. е. переменная не равна undefined). Например:

И, возвращаясь к нашему предыдущему примеру о ребёнке, выполняющем поручение своего родителя, вы можете это записать так:

Вложенность if . else

Вполне нормально использовать один условный оператор if. else внутри другого — вложить их. Например, мы могли бы обновить наше приложение прогноза погоды, чтобы показать ещё один набор вариантов в зависимости от температуры:

Несмотря на то, что весь код работает вместе, каждый условный оператор if. else работает полностью отдельно от другого.

Логические операторы: И, ИЛИ и НЕ

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

  • && — И; позволяет объединить два или более выражения так, что каждое из них отдельно должно иметь значение true , чтобы в итоге общее выражение имело значение true .
  • || — ИЛИ; позволяет объединить два или более выражения так, что одно или несколько из них должно иметь значение true , чтобы в итоге общее выражение имело значение true .

Чтобы дать вам пример оператора И, предыдущий фрагмент кода можно переписать так:

Так, для примера, первый блок кода выполнится только в том случае, если choice === ‘sunny’ и temperature < 86 вернут значение true .

Давайте посмотрим на быстрый пример оператора ИЛИ:

Последний тип логического оператора НЕ, выраженный ! оператором, можно использовать для отрицания выражения. Давайте объединим его с ИЛИ в приведённом выше примере:

В этом фрагменте, если условие ИЛИ возвращает true , оператор НЕ будет отрицать это и выражение вернёт false .

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

Распространённой ошибкой при использовании логического оператора ИЛИ в условном выражении является указание переменной, значение которой нужно проверить со списком возможных значений этой переменной, разделённых операторами || (ИЛИ). Например.

В данном примере условие в if(. ) всегда будет оцениваться как true, поскольку 7 (или любое другое ненулевое значение) всегда будет оцениваться как true. Фактически, это условие гласит «если х равен 5, или 7 является true». Но нам требуется совсем не это. Чтобы достичь нужной цели, придётся выполнять полноценную проверку после каждого оператора ИЛИ:

Домашнее задание

1. Представьте, что вы просите пользователя ответить на вопрос: «Сколько ног у паука?». Правильный ответ – 8. Пропишите несколько возможных сообщений для пользователя, в зависимости от введённых им значений, используя конструкцию else if:

— если пользователь ввёл 8, то он получит сообщение «Правильно!»;

— если меньше 8, то «Нет! Это слишком мало.»;

— если любое другое число, то «Нет! Это очень много.».

2. Теперь попросим пользователя вычислить результат простого арифметического выражения 3+2. Пропишите условия, используя конструкцию switch case:

— если пользователь ввёл 5, то он получит сообщение «Верно! Это 5.»;

— если 6, то «Нет, необходимо сложить, а не умножить!»;

— если 1, то «Нет, не вычитай, а складывай!»;

— во всех других случаях «Кажется, у тебя плохо с математикой!».

голоса
Рейтинг статьи
Читайте так же:
Как правильно отрегулировать ближний свет на машине
Ссылка на основную публикацию
Adblock
detector