#android #ui
{1/4} Что такое Looper, как работает и что делает?
🙌 Представьте себе бесконечный цикл, допустим
Это и есть вся суть Looper. Просто бесконечный цикл который получает из очереди сообщения и их выполняет.
Чтобы создать Looper нужно вызвать метод Looper.prepare(). После этого метод Looper.prepare() сохраняет созданный объект в статическое поле типа ThreadLocal.
Реализация инициализации лупера довольна простая, и при этом позволяет в любом месте программы и из любого треда получить лупер, связанный с текущим тредом. Статический метод Looper.myLooper() просто достает лупер из переменной ThreadLocal.
Далее мы запускаем Looper при помощи метода Looper.loop() он уходит в бесконечный цикл, который мы обсудили выше. В следующих постах обсудим что за сообщения, и кто их посылает.
{1/4} Что такое Looper, как работает и что делает?
🙌 Представьте себе бесконечный цикл, допустим
for(;;){}
. Далее представим, что в этом цикле мы читаем из некоторой очереди Queue<Runnable> значения и выполняем их, получается что-то вроде:
Queue<Runnable> queue;
for(;;){
final Runnable runnable = queue.take();
runnable.run();
}
Это и есть вся суть Looper. Просто бесконечный цикл который получает из очереди сообщения и их выполняет.
Чтобы создать Looper нужно вызвать метод Looper.prepare(). После этого метод Looper.prepare() сохраняет созданный объект в статическое поле типа ThreadLocal.
Реализация инициализации лупера довольна простая, и при этом позволяет в любом месте программы и из любого треда получить лупер, связанный с текущим тредом. Статический метод Looper.myLooper() просто достает лупер из переменной ThreadLocal.
Далее мы запускаем Looper при помощи метода Looper.loop() он уходит в бесконечный цикл, который мы обсудили выше. В следующих постах обсудим что за сообщения, и кто их посылает.
👍23
#android #ui
{2/4} В прошлом посте мы поговорили про Looper, там упоминалась некоторая очередь Queue<Runnable>. Давай-те подробнее о ней поговорим.
В реальности есть два отличия:
☝️- это не просто очередь из Collection, это отдельный класс, который так и называется MessageQueue
✌️- внутри очереди не просто Runnable, а специальные объекты, которые называются Message
Начнем с класса Message. В классе есть много полей, но нас сейчас интересует только 3️⃣ это callback, when и next.
callback – тот самый Runnable, который будет исполнен Looper'ом
next - ссылка на следующее сообщение
when - просто поле типа long, которое является 🕑временем, когда это сообщение должно быть выполнено
MessageQueue – простой односвязный список. Если заглянуть в MessageQueue то увидим, что там просто одно поле mMessages типа Message. У каждого Message есть ссылка на следующее сообщение Message.next. Другими словами, MessageQueue хранит только ссылку на первое сообщение.
Сообщения в MessageQueue отсортированы по возрастанию значения поля Message.when. Looper вызывает метод MessageQueue.next() в цикле, и получает отсортированное сообщение, которое нужно выполнить, если же очередь пуста, метод MessageQueue.next() блокирует цикл до тех пор, пока сообщение не появится.
Чтобы положить сообщение в очередь нужно вызвать метод MessageQueue.enqueueMessage(). Метод MessageQueue.enqueueMessage() проходит по очереди, проверяя значение Message.when каждого из сообщений и вставляет новое сообщение в положенное место очереди.
Как создается сообщение? Вручную сообщение лучше не создавать, для создания лучше использовать метод Message.obtain(). Message.obtain() возвращает объект message из пула, который представляет собой связный список максимальным размером 5️⃣0️⃣ сообщений. Если все сообщения пула используются, то Message.obtain() создает и возвращает новый объект Message.
{2/4} В прошлом посте мы поговорили про Looper, там упоминалась некоторая очередь Queue<Runnable>. Давай-те подробнее о ней поговорим.
В реальности есть два отличия:
☝️- это не просто очередь из Collection, это отдельный класс, который так и называется MessageQueue
✌️- внутри очереди не просто Runnable, а специальные объекты, которые называются Message
Начнем с класса Message. В классе есть много полей, но нас сейчас интересует только 3️⃣ это callback, when и next.
callback – тот самый Runnable, который будет исполнен Looper'ом
next - ссылка на следующее сообщение
when - просто поле типа long, которое является 🕑временем, когда это сообщение должно быть выполнено
MessageQueue – простой односвязный список. Если заглянуть в MessageQueue то увидим, что там просто одно поле mMessages типа Message. У каждого Message есть ссылка на следующее сообщение Message.next. Другими словами, MessageQueue хранит только ссылку на первое сообщение.
Сообщения в MessageQueue отсортированы по возрастанию значения поля Message.when. Looper вызывает метод MessageQueue.next() в цикле, и получает отсортированное сообщение, которое нужно выполнить, если же очередь пуста, метод MessageQueue.next() блокирует цикл до тех пор, пока сообщение не появится.
Чтобы положить сообщение в очередь нужно вызвать метод MessageQueue.enqueueMessage(). Метод MessageQueue.enqueueMessage() проходит по очереди, проверяя значение Message.when каждого из сообщений и вставляет новое сообщение в положенное место очереди.
Как создается сообщение? Вручную сообщение лучше не создавать, для создания лучше использовать метод Message.obtain(). Message.obtain() возвращает объект message из пула, который представляет собой связный список максимальным размером 5️⃣0️⃣ сообщений. Если все сообщения пула используются, то Message.obtain() создает и возвращает новый объект Message.
👍4🔥3
{3/4} В этом посте используется код на Kotlin, просьба Java дИдов не пугаться!
Понемногу у нас с вами складывается картина того, каким образом работает UI в android. Мы разобрали что такое Looper, что такое Message и MessageQueue. Узнали, что в Looper посылаются задачи через MessageQueue. Возникает вопрос❓, как именно послать задачу в Looper, так как прямого доступа к MessageQueue у нас нет. Тут на сцену выходит Handler.
Прежде чем начнем разбирать Handler введем 2️⃣ понятия:
☝️поток consumer - поток, который ждет сообщения, тот, который вызывал Looper.loop().
✌️поток producer - поток, который создает сообщения и посылает их потоку consumer через Handler.
Поток consumer и поток producer могут быть одним потоком, как это может быть разберем далее.
Каждый Android разработчик хотя бы раз в своих проектах использовал Handler. Как уже сказано выше, нужен он для того, чтобы посылать задачи из потока producer в поток consumer.
У Handler есть несколько конструкторов, интересные пожалуй только 2️⃣ это:
☝️дефолтый коструктор без аргументов Handler()
✌️конструктор с аргументом типа Looper Handler(looper: Looper)
Когда используют конструктор без Looper, Handler пытается найти его через Looper.myLooper() и если его не находит, то падает❌.
Допустим в Activity.onCreate вы вызовете
Конструктор с входным параметром в виде Looper предпочтительнее, так как тогда вы явно задаете Looper🔁, в который будут post’иться сообщения. С ним все довольно очевидно, на вход нужно подать Looper в который будут посылаться задачи.
Теперь посмотрим на слудующий кусок кода:
Для начала попробуйте ответить сами, что будет выведено в лог❓ В логах мы увидим следующую последовательность:
- "hello from onCreate thread main"
- "hello from handler thread main"
Когда вызываем метод post, переданный Runnable оборачивается в Message и через MessageQueue подается на Looper consumer потока. Однако система все методы жизненного цикла активности тоже вызывает через Looper потока Main🧶, даже сам метод onCreate() вызывался примерно так:
Следовательно, когда мы вызываем
Этот пример очень сильное упрощение того, что происходит на самом деле, но суть та же. Это тот случай, когда поток consumer и поток producer это один и тот же поток. Именно этим фактом, обуславливается асинхронность в UI. Когда мы создаем транзакцию для показа фрагмента, после метода commit эта транзакция также кладется в MessageQueue через Handler, и выполняется позже Looper’ом Main Thread🧶, также происходит и с показом новой🆕 Activity и многими другими вещами вроде анимаций и т.п.
Стоит упомянуть еще одну интересную особенность, так уж вышло, что огромное количество багов связаных с View на Android можно решить просто отложив задачу
Помните мы разбирали, что у Message есть специальное поле when, так вот, когда вызываем post у Handler, там используется SystemClock.uptimeMillis()), наподобие System.getCurrentTimeMillis(), а когда вызываем postDelayed, то входной аргумент delay прибавляется к SystemClock.uptimeMillis()) и записывается в поле when, а дальше магия сортировки, которая обсуждалась туть.
Понемногу у нас с вами складывается картина того, каким образом работает UI в android. Мы разобрали что такое Looper, что такое Message и MessageQueue. Узнали, что в Looper посылаются задачи через MessageQueue. Возникает вопрос❓, как именно послать задачу в Looper, так как прямого доступа к MessageQueue у нас нет. Тут на сцену выходит Handler.
Прежде чем начнем разбирать Handler введем 2️⃣ понятия:
☝️поток consumer - поток, который ждет сообщения, тот, который вызывал Looper.loop().
✌️поток producer - поток, который создает сообщения и посылает их потоку consumer через Handler.
Поток consumer и поток producer могут быть одним потоком, как это может быть разберем далее.
Каждый Android разработчик хотя бы раз в своих проектах использовал Handler. Как уже сказано выше, нужен он для того, чтобы посылать задачи из потока producer в поток consumer.
У Handler есть несколько конструкторов, интересные пожалуй только 2️⃣ это:
☝️дефолтый коструктор без аргументов Handler()
✌️конструктор с аргументом типа Looper Handler(looper: Looper)
Когда используют конструктор без Looper, Handler пытается найти его через Looper.myLooper() и если его не находит, то падает❌.
Допустим в Activity.onCreate вы вызовете
Handler().post{ doSmth() }
- тут создается Handler, который через метод Looper.myLooper() получает Looper который привязан к Main Thread🧶, что аналогично записи Handler(Looper.getMainLooper()).post{ doSmth() }
. Но если попытаемся тоже самое сделать на потоке без Looper, конструктор упадет💣:
Thread {
val handler = Handler() // - тут мы упадем
❌, так как у этого потока нет Looper
🔁, подробнее смотри в посте про Looper
handler.post { doSmth() }
}.start()
Конструктор с входным параметром в виде Looper предпочтительнее, так как тогда вы явно задаете Looper🔁, в который будут post’иться сообщения. С ним все довольно очевидно, на вход нужно подать Looper в который будут посылаться задачи.
Теперь посмотрим на слудующий кусок кода:
fun onCreate() { // этот метод вызывает система
Handler().post {
Log.d("hello from handler thread ${Thread.currentThread().name}")
}
Log.d("hello from onCreate thread ${Thread.currentThread().name}")
}
Для начала попробуйте ответить сами, что будет выведено в лог❓ В логах мы увидим следующую последовательность:
- "hello from onCreate thread main"
- "hello from handler thread main"
Когда вызываем метод post, переданный Runnable оборачивается в Message и через MessageQueue подается на Looper consumer потока. Однако система все методы жизненного цикла активности тоже вызывает через Looper потока Main🧶, даже сам метод onCreate() вызывался примерно так:
val activity = getCurrentActivity()
val handler = Handler()
handler.post {
activity.onCreate()
}
Следовательно, когда мы вызываем
Handler().post{ Log.d("hello from handler thread ${Thread.currentThread().name}") }
эта задача кладется в очередь и выполнится после того, как завершится метод onCreate().Этот пример очень сильное упрощение того, что происходит на самом деле, но суть та же. Это тот случай, когда поток consumer и поток producer это один и тот же поток. Именно этим фактом, обуславливается асинхронность в UI. Когда мы создаем транзакцию для показа фрагмента, после метода commit эта транзакция также кладется в MessageQueue через Handler, и выполняется позже Looper’ом Main Thread🧶, также происходит и с показом новой🆕 Activity и многими другими вещами вроде анимаций и т.п.
Стоит упомянуть еще одну интересную особенность, так уж вышло, что огромное количество багов связаных с View на Android можно решить просто отложив задачу
Handler().postDelayed(100) { //doSmth }
.Помните мы разбирали, что у Message есть специальное поле when, так вот, когда вызываем post у Handler, там используется SystemClock.uptimeMillis()), наподобие System.getCurrentTimeMillis(), а когда вызываем postDelayed, то входной аргумент delay прибавляется к SystemClock.uptimeMillis()) и записывается в поле when, а дальше магия сортировки, которая обсуждалась туть.
👍24❤1🔥1
{4/4} Разобрав тему Handler стоит упомянуть одну❕интересную особенность работы с Handler через View.
У каждой View в Android также есть методы
Если AttachInfo не равен null, тогда Message просто кладется в Handler который есть у этого самого AttachInfo, т.е в Handler главного потока, тот который
Если же AttachInfo в данный момент равен null, т.е View еще не приатачена к компоненту, то Message кладется в специальную очередь, которая уникальна для каждой View. Затем, когда View приатачится к компоненту, система пробежится по этой очереди и запустит все Messages, которые были в очереди в Handler.
Почему это важно знать ❗️.
Когда работаем с Handler через View нужно вручную удалить все задачи, которые еще не выполнены, иначе компонент может утечь💧 или просто упасть💣, так как View уже не будет. Это актуально для длительных задач, если запускаем задачу с delay меньше секунды, то можно забить.
Однако, если мы делаем что-то вроде
Поэтому для длительных задач 🕰 (которые бывают очень редко) сохраняем Runnable(тот который пихаем в метод
У каждой View в Android также есть методы
post()
, postAtTime()
, postDelayed()
, аналогичные тем, что есть у Handler, но работают немного прикольнее. Они сперва проверяют есть ли в данный момент AttachInfo, или по-другому, приатачена ли View к компоненту, например к Activity. Если AttachInfo не равен null, тогда Message просто кладется в Handler который есть у этого самого AttachInfo, т.е в Handler главного потока, тот который
Handler(Looper.getMainLooper())
Если же AttachInfo в данный момент равен null, т.е View еще не приатачена к компоненту, то Message кладется в специальную очередь, которая уникальна для каждой View. Затем, когда View приатачится к компоненту, система пробежится по этой очереди и запустит все Messages, которые были в очереди в Handler.
Почему это важно знать ❗️.
Когда работаем с Handler через View нужно вручную удалить все задачи, которые еще не выполнены, иначе компонент может утечь💧 или просто упасть💣, так как View уже не будет. Это актуально для длительных задач, если запускаем задачу с delay меньше секунды, то можно забить.
Однако, если мы делаем что-то вроде
refreshlayout.postDelayed(4000) { refreshlayout.isEnabled = false }
, и при этом сами не очищаем очередь View вызвав refreshlayout.removeCallbacks(runnable)
то можем упасть 💣, так как задача может быть вызвана даже когда уйдем с этого экрана. Поэтому для длительных задач 🕰 (которые бывают очень редко) сохраняем Runnable(тот который пихаем в метод
post()
)в поле Fragment/Activity и удаляем его ручками на onDestroyView/onDestroy через метод removeCallbacks(runnable).👍22❤1
#ui #android
Большой респект тем, кто прочитал первую серию постов. Серию можно перечитывать перед собесами, и тогда вам не будет равных по вопросу Handler, Looper и MessageQueue.
Здесь что-то вроде оглавления:
- Пост про Looper
- Пост про MessageQueue
- Пост про Handler
- Дополнение к посту про Handler
Большой респект тем, кто прочитал первую серию постов. Серию можно перечитывать перед собесами, и тогда вам не будет равных по вопросу Handler, Looper и MessageQueue.
Здесь что-то вроде оглавления:
- Пост про Looper
- Пост про MessageQueue
- Пост про Handler
- Дополнение к посту про Handler
👍32
Итак инвариант, что это и зачем это знать? В подкасте подлодка, на одном из выпусков про обязательные знания программиста, пришедший эксперт сказал интересную вещь. Он сказал, что каждый сеньор должен знать, что такое инвариант и знать как ответить на такой вопрос на собеседовании. Чтож давай-те разберем, что это, чтобы не ударить в грязь лицом🤦♂️.
Начнем с истории, само понятие инвариант, пришло к нам из математики. Инвариант в математике - это выражение которое сохраняет свое значение, аля если у нас есть функция
💻 Вернемся в программирование. В программировании инвариантом называют предикат (читай некоторое условие) который всегда истинный. Другими словами если функция инвариантна, значит некоторое условие сохраняется до вызова функции и после вызова функции. Если класс инвариантен, значит его состояние всегда удовлетворяет какому-то условию. Для еще большого понимания, есть языки программирования, в которых понятие инварианта вшито в синтаксис языка, вот пример язык D
Думаю из кода все очевидно, в блоке
🦾 Мы используем для разработки такие языки как java или kotlin, в которых нет такой фичи как
Делая это, мы даем гарантии для других разработчиков по использованию наших классов. Если кто-то начнет творить фигню код просто упадет и сразу можно будет найти ошибку.
Начнем с истории, само понятие инвариант, пришло к нам из математики. Инвариант в математике - это выражение которое сохраняет свое значение, аля если у нас есть функция
y = x + 2
, то при x = 3
, y
всегда будет равен 5
. Не будет такого, что сейчас он 5
, а завтра 6
, не нифига, условие непоколебимое как моя преподша по матану на экзамене 👩🏫. 💻 Вернемся в программирование. В программировании инвариантом называют предикат (читай некоторое условие) который всегда истинный. Другими словами если функция инвариантна, значит некоторое условие сохраняется до вызова функции и после вызова функции. Если класс инвариантен, значит его состояние всегда удовлетворяет какому-то условию. Для еще большого понимания, есть языки программирования, в которых понятие инварианта вшито в синтаксис языка, вот пример язык D
class Date {
int day;
int hour;
invariant() {
assert(1 <= day && day <= 31);
assert(0 <= hour && hour < 24);
}
}
Думаю из кода все очевидно, в блоке
invariant
, задаем условия, которые всегда должны быть истины. Если попытаемся присвоить полю day
значение 32
, код просто упадет с ошибкой. Это дает нам гарантии того, что используя данный класс в его полях всегда будет корректное значение. 🦾 Мы используем для разработки такие языки как java или kotlin, в которых нет такой фичи как
invariant
, следовательно нам с вами это нужно делать руками. Чаще всего это реализуется так, что мы делаем проверку значений поля класса, которые хотим поменять в функции перед выполнением кода функции и после. Если проверка прошла то ок, если нет, то падаем. Делая это, мы даем гарантии для других разработчиков по использованию наших классов. Если кто-то начнет творить фигню код просто упадет и сразу можно будет найти ошибку.
👍11❤1
Я долго думал, чтобы такого не сложного рассказать по архитектуре и не придумал ничего умнее.
Композия или наследование?
Представим, что у нас есть такой класс Developer:
И мы хотим сделать класс Devops, который умеет запускать докер и помимо этого, чтобы он еще и умел пить кофе как Developer. Очевидный путь это создать класс Devops написать функцию по запуску докера, а функцию пить кофе просто скопировать из Developer.
Однако получится дублирование кода, что довольно скверно и несет кучу проблем в будущем. Как решить это дерьмо? Есть два варианта: Наследованиe и Композиция, разберем каждый.
👨👩👧 Наследованиe. Если класс
Вроде бы все круто, но возникает сложность. Допустим мы не хотим, чтобы Devops умел писал код (ведь
🪆Композиция, это когда одно из полей класса
мы избавились от дублирования кода и при этом в классе
Возникает вопрос зачем нам тогда нужно наследование, ведь все вокруг трубят, что лучше использовать композицию а не наследование?
Самым правильным ответом на этот вопрос будет: это зависит от вашего случая.
Наследование стоит выбирать тогда, когда у двух классов есть отношение
Композицию стоит выбирать когда есть отношение
Некоторые практических советов как выбрать и итоги:
👉При использовании стороних библиотек, стоит унаследоваться только от абстрактных классов или интерфейсов. Во всех других случаях лучше использовать композицию. Это связано с тем, что простые классы могуть меняться, и эти изменения могут сильно стрельнуть.
👉Если вы сами делаете либу, то делайте ваши классы закрытыми для наследования. Давайте возможность клиентам наследоваться только от ваших абстактных классов и интерфейсов.
👉Если класс
👉Если класс
Композия или наследование?
Представим, что у нас есть такой класс Developer:
Developer{
fun writeCode()
fun drinkCoffee()
}
И мы хотим сделать класс Devops, который умеет запускать докер и помимо этого, чтобы он еще и умел пить кофе как Developer. Очевидный путь это создать класс Devops написать функцию по запуску докера, а функцию пить кофе просто скопировать из Developer.
Однако получится дублирование кода, что довольно скверно и несет кучу проблем в будущем. Как решить это дерьмо? Есть два варианта: Наследованиe и Композиция, разберем каждый.
👨👩👧 Наследованиe. Если класс
Devops
наследует класс Developer
, значит все открыте методы и поля Developer окажутся в объекте Devops
:class Devops : Developer{
fun launchDocker()
}
val devops = Devops()
devops.writeCode()
devops.launchDocker()
Вроде бы все круто, но возникает сложность. Допустим мы не хотим, чтобы Devops умел писал код (ведь
Devops
это не человек а идеалогия, но кофе пить можно!). В таком случае лучше использовать композицию.🪆Композиция, это когда одно из полей класса
Devops
является классом Developer
. Эначит мы сначала конструируем объект Developer
, а потом устанавливаем этот объект в поле объекта Devops
. А Devops
уже будет обращаться к методам из класса Developer
:class Devops(
private val developer:Developer
){
fun launchDocker()
fun drinkCoffee(){
developer.drinkCoffee()
}
}
val devops = Devops()
devops.drinkCoffee()
devops.launchDocker()
мы избавились от дублирования кода и при этом в классе
Devops
нет метода drinkCoffee()
.Возникает вопрос зачем нам тогда нужно наследование, ведь все вокруг трубят, что лучше использовать композицию а не наследование?
Самым правильным ответом на этот вопрос будет: это зависит от вашего случая.
Наследование стоит выбирать тогда, когда у двух классов есть отношение
является
. Например есть класс Promotion (Акция) и есть класс NewYearPromotion (Новогодняя акция) очевидно, что у них есть отношение является
, так как NewYearPromotion это просто другая разновидность Promotion и тут нужно наследование.Композицию стоит выбирать когда есть отношение
использует
. Допустим есть класс Car и класс Wheel, явно Car использует
Wheel, а не является Wheel значит тут нужна композиция.Некоторые практических советов как выбрать и итоги:
👉При использовании стороних библиотек, стоит унаследоваться только от абстрактных классов или интерфейсов. Во всех других случаях лучше использовать композицию. Это связано с тем, что простые классы могуть меняться, и эти изменения могут сильно стрельнуть.
👉Если вы сами делаете либу, то делайте ваши классы закрытыми для наследования. Давайте возможность клиентам наследоваться только от ваших абстактных классов и интерфейсов.
👉Если класс
B
расширяет и является классом A
то наследование.👉Если класс
B
только использует часть функционала класса A
то композиция.👍14❤3
Начинаем цикл про некоторые проблемы многопоточности.
Как таковых проблем существует куча но мы разберем самые основные:
👉 Visibility
👉 Atomicity
👉 Reordering
👉 Happens-before
👉 Deadlock
Как таковых проблем существует куча но мы разберем самые основные:
👉 Visibility
👉 Atomicity
👉 Reordering
👉 Happens-before
👉 Deadlock
👍4😁3
🙈 Сегодня поговорим про проблему Visibility. Чтобы понять суть проблемы разберем один синтетический пример.
Все программы, которые мы пишем, используют как минимум несколько потоков. Один отвечает за отображение другие за походы в сеть, в файловую систему и много чего еще. Даже когда вы пишете код на js и кажется будто поток всегда один, на самом деле их несколько просто за вас эту работу делает браузер.
Вернемся к проблеме, у нас несколько потоков и есть переменная с которой эти потоки работают. Эта переменная может изменятся из нескольких потоков. Небольшой кусок кода на kotlin:
В коде мы запускаем 100 потоков, каждый из потоков увеличивает значение переменной number на 1. Затем мы ждем завершения всех потоков и выводим значение переменной.
❓Вопрос что будет в поле number? Правильный ответ вообще хз. Может быть 100, а может и 98 и 101 можете попробовать сами🙃
Почему так проиходит, почему иногда программа глючит? Чтобы это понять придется погрузится в то, как устроенны процессоры. 💻
Почти все процессоры, даже мобильные имеют несколько ядер, это нужно потому как повышать герцы мы больше не можем и приходится их ускорять путем паррелизации задач.
Пока одно ядро показывает вам видос на YouTube другое ядро в это время ищет вирусы, третье что-то качает с сети и т.п.
У каждого ядра есть кеши, L1..L4. Каждый кеш имеет свой размер и свою скорость записи/чтения. L1 супер быстрое чтение/запись и очень маленький размер ~ 32 Кб. L4 более медленная чтение/запись, но размер уже по больше ~ 16 МБ байт. Естественно размеры зависят он конкретного процессора.
Эти кеши нужны чтобы процессор не ходил в оперативу каждый раз. Это еще одна оптимизация для ускорения, так как ходить в кеш гораздо быстрее, ведь он находится в самом процессоре.
Теперь возвращаемся к нашему примеру, у нас создается 100 потоков, и предположим у системы 4 ядра, значит скорее всего параллельно будут работать 4 потока.
🙌И теперь следите за руками, когда процессор работает с переменной number, это значение он сначала кладет в кеш L1, затем спустя какое-то время это значение из кеша попадает в оперативную память. Увеличение значения переменной просиходит в 3 шага, получить переменную, увеличить на один и записать переменную.
Допустим поток X взял переменную пока её значение было 1, затем он увеличил её до 2 и записал это значение. Это значение сначало записалось в кеш, и только спустя какое-то время оно попадет в оперативную память.
В это время другой поток Y тоже хочет проделать аналогичную операцию. Он также идет в оперативную память, получает значение 1, так как поток X работает на другом ядре и еще не записал значение в оперативную память. Поток Y получает получает значение 1 увеличивает его на 1 и записывает 2, аналогично потоку X.
В итоге должно было получиться 3, но получилось 2 из-за того, что потоки не видят того, что делают другие потоки, или видят значение но с опозданием. 🕰
Это сильное упращение того, что проиcходит в реальности, однако суть таже. Это фундаментальная проблема Visibility в многопоточности. О том как решается эта проблема поговорим в отдельном посте.
Все программы, которые мы пишем, используют как минимум несколько потоков. Один отвечает за отображение другие за походы в сеть, в файловую систему и много чего еще. Даже когда вы пишете код на js и кажется будто поток всегда один, на самом деле их несколько просто за вас эту работу делает браузер.
Вернемся к проблеме, у нас несколько потоков и есть переменная с которой эти потоки работают. Эта переменная может изменятся из нескольких потоков. Небольшой кусок кода на kotlin:
fun main(){
val threadList = mutableList<Thread>()
var number = 0
repeat(100){
threadList+=thread {
number++
}
}
thread.forEach { it.join()}
print(number)
}
В коде мы запускаем 100 потоков, каждый из потоков увеличивает значение переменной number на 1. Затем мы ждем завершения всех потоков и выводим значение переменной.
❓Вопрос что будет в поле number? Правильный ответ вообще хз. Может быть 100, а может и 98 и 101 можете попробовать сами🙃
Почему так проиходит, почему иногда программа глючит? Чтобы это понять придется погрузится в то, как устроенны процессоры. 💻
Почти все процессоры, даже мобильные имеют несколько ядер, это нужно потому как повышать герцы мы больше не можем и приходится их ускорять путем паррелизации задач.
Пока одно ядро показывает вам видос на YouTube другое ядро в это время ищет вирусы, третье что-то качает с сети и т.п.
У каждого ядра есть кеши, L1..L4. Каждый кеш имеет свой размер и свою скорость записи/чтения. L1 супер быстрое чтение/запись и очень маленький размер ~ 32 Кб. L4 более медленная чтение/запись, но размер уже по больше ~ 16 МБ байт. Естественно размеры зависят он конкретного процессора.
Эти кеши нужны чтобы процессор не ходил в оперативу каждый раз. Это еще одна оптимизация для ускорения, так как ходить в кеш гораздо быстрее, ведь он находится в самом процессоре.
Теперь возвращаемся к нашему примеру, у нас создается 100 потоков, и предположим у системы 4 ядра, значит скорее всего параллельно будут работать 4 потока.
🙌И теперь следите за руками, когда процессор работает с переменной number, это значение он сначала кладет в кеш L1, затем спустя какое-то время это значение из кеша попадает в оперативную память. Увеличение значения переменной просиходит в 3 шага, получить переменную, увеличить на один и записать переменную.
Допустим поток X взял переменную пока её значение было 1, затем он увеличил её до 2 и записал это значение. Это значение сначало записалось в кеш, и только спустя какое-то время оно попадет в оперативную память.
В это время другой поток Y тоже хочет проделать аналогичную операцию. Он также идет в оперативную память, получает значение 1, так как поток X работает на другом ядре и еще не записал значение в оперативную память. Поток Y получает получает значение 1 увеличивает его на 1 и записывает 2, аналогично потоку X.
В итоге должно было получиться 3, но получилось 2 из-за того, что потоки не видят того, что делают другие потоки, или видят значение но с опозданием. 🕰
Это сильное упращение того, что проиcходит в реальности, однако суть таже. Это фундаментальная проблема Visibility в многопоточности. О том как решается эта проблема поговорим в отдельном посте.
❤5👍3🔥1
Очень давно наткнулся на интересную вещь. Один чувак сделал матрицу компетенций для разработчика. В ней представлены многие области в которых должен разбираться человек, который претендует на звание инженера. Матрица разбита по уровням, т.е насколько глубоко должен шарить джун, мидл и сеньор (такой, как Манифесто).
От себя могу добавить, что к матрице стоит подходить очень cкeптически, много спорных вещей, как например, что крепкий мидл должен писать используя TDD и знать минимум 4-5 платформ. Однако её можно рассматривать как некоторый идеал к которому можно хотя немного стремиться.
От себя могу добавить, что к матрице стоит подходить очень cкeптически, много спорных вещей, как например, что крепкий мидл должен писать используя TDD и знать минимум 4-5 платформ. Однако её можно рассматривать как некоторый идеал к которому можно хотя немного стремиться.
👍3
Следующая проблема многопоточности Atomicity.
Суть в том, что в платформе, с которой вы работаете, те операции, которые как вам кажется выполняются за одну операцию, на самом деле могут выполняться в несколько операций.
Потому как большинство тут джависты, будем рассуждать на примере платформы JVM (я тупо в других не шарю...)
Начнем с простого примера:
Взглянем подробнее на операцию
📖 считать значение переменной number;
1️⃣ увеличить значение на 1;
✒️ записать новое значение в переменную number.
Все это довольно очевидно и наверняка в головах у вас звучит "Спасибо Кэп 👨✈️", но не спешите меня осуждать. Перейдем к примеру прикольнее:
Смотрим на операцию
"Что за хуефокус❓" – Вопрос, который мог возникнуть в вашей голове и чтобы на него ответить давайте разбираться.
Начнем с того, что это зависит от конкретной JVM и окружения, в которой она выполняется. Как мы знаем программы написанные на языке java можно запускать везде, где есть JVM. Мы пишем на java, а JVM уже умеет работать с конкретной платформой.
И вот тут начинается веселье, в примере мы используем Long. В большинстве языков – Long это целочисленное значение, под которое выделяется 64 бита (для Double кстати тоже).
💻 Процессоры у нас бывают 32х и 64х разрядные. Представим, что JVM работает в системе с 32х разрядным процессором. Вот незадача у нас переменная с размером в 64 бита, а процессор 32х разрядный, как он тогда вообще может записать значение в переменную типа Long? 🤔
Правильно, в 2 этапа, сначала первые 32 бита, затем вторые 32 бита. Смекаете к чему я веду и почему это является проблемой многопоточности? Потому как может быть ситуация, где один поток запишет первые 32 бита, а другой вторые 32 бита. В итоге получим такой баг, который без знания этих основ можно искать очень долго 🔎.
Вот так, как всегда проблема обозначена, а решение я пока попридержу.
Суть в том, что в платформе, с которой вы работаете, те операции, которые как вам кажется выполняются за одну операцию, на самом деле могут выполняться в несколько операций.
Потому как большинство тут джависты, будем рассуждать на примере платформы JVM (я тупо в других не шарю...)
Начнем с простого примера:
fun main(){
var number: Int = 0
number++
print(number)
}
Взглянем подробнее на операцию
number++
– за сколько шагов она делается? В прошлом посте мы уже разобрали, что тут происходит 3️⃣ операции: 📖 считать значение переменной number;
1️⃣ увеличить значение на 1;
✒️ записать новое значение в переменную number.
Все это довольно очевидно и наверняка в головах у вас звучит "Спасибо Кэп 👨✈️", но не спешите меня осуждать. Перейдем к примеру прикольнее:
fun main(){
val number: Long = 0
number = 42
print(number)
}
Смотрим на операцию
number = 42
сколько шагов делается тут? Иииии наш любимый ответ хз 🤷♂️, может быть в одну операцию, но может и в две) "Что за хуефокус❓" – Вопрос, который мог возникнуть в вашей голове и чтобы на него ответить давайте разбираться.
Начнем с того, что это зависит от конкретной JVM и окружения, в которой она выполняется. Как мы знаем программы написанные на языке java можно запускать везде, где есть JVM. Мы пишем на java, а JVM уже умеет работать с конкретной платформой.
И вот тут начинается веселье, в примере мы используем Long. В большинстве языков – Long это целочисленное значение, под которое выделяется 64 бита (для Double кстати тоже).
💻 Процессоры у нас бывают 32х и 64х разрядные. Представим, что JVM работает в системе с 32х разрядным процессором. Вот незадача у нас переменная с размером в 64 бита, а процессор 32х разрядный, как он тогда вообще может записать значение в переменную типа Long? 🤔
Правильно, в 2 этапа, сначала первые 32 бита, затем вторые 32 бита. Смекаете к чему я веду и почему это является проблемой многопоточности? Потому как может быть ситуация, где один поток запишет первые 32 бита, а другой вторые 32 бита. В итоге получим такой баг, который без знания этих основ можно искать очень долго 🔎.
Вот так, как всегда проблема обозначена, а решение я пока попридержу.
👍6
Новый пост, новая проблема, сегодня поговорим о Reordering. 👩🏫 Начнем с определения: в многопоточной среде результаты операций, произведённых другими потоками, могут наблюдаться не в том порядке, в котором мы ожидаем. Лааадно, вы же не думали, что я тут буду душнить. 🙌 Как всегда, разберем на пальцах пример:
Этот код может показаться немного запутанным, но это самый показательный пример который я смог придумать. Итак, вопрос, что будет выведено в консоль?
Кто читал прошлые посты уже догадываются какой ответ 🙃. Варианты которые тут могут быть: 0,0; 0,1; 1,0; 1,1. Разберем каждый из кейсов.
👉 Кейс первый 0 и 1. С этим кейсом все просто, представляем, что потоки у нас стартуют одновременно без задержек, тогда в переменную
👉 Кейс второй 0 и 0. В данном случае второй поток стартует с некоторой задержкой, и первый поток успевает изменить переменную
👉 Кейс третий 1 и 1. В этом кейсе наоборот первый поток стартует с задержкой, и второй поток успевает затереть переменную
👉 Кейс четвертый 1 и 0. Самый загадочный из всех кейсов, воспроизвести его безумно сложно, практически нереально, но в теории возможно. Как такое может произойти? Вкратце, это еще одна оптимизация которую может сделать компилятор, процессор или окружение.
☝️Для начала договоримся, что действие это либо запись, либо чтение с переменной. Как вы все знаете компиляторы и процессоры очень сложные штуки 🧐. Компилятор может переставить действия местами если посчитает, что так будет быстрее. 💻 Процессор и JVM могут выполнять действия не в том порядке как они расположены в коде, и могут выполнять их как им это покажется нужным.
Еще раз наглядно на коде:
Нет никакой гарантии того, что операции присваивания в
Следовательно, в последнем кейсе это и произошло, инструкции с присваиванием поменялись местами. JVM спокойно могла их выполнить не в том, порядке в котором они расположены, так как это никак не повлияет на логику. Другими словами было:
Стало:
Избежать этих перестановок можно, а как и когда это нужно поговорим позже)
var x = 0
var y = 1
thread {
var a = x
y = 0
print(a)
}
thread {
var b = y
x = 1
print(b)
}
Этот код может показаться немного запутанным, но это самый показательный пример который я смог придумать. Итак, вопрос, что будет выведено в консоль?
Кто читал прошлые посты уже догадываются какой ответ 🙃. Варианты которые тут могут быть: 0,0; 0,1; 1,0; 1,1. Разберем каждый из кейсов.
👉 Кейс первый 0 и 1. С этим кейсом все просто, представляем, что потоки у нас стартуют одновременно без задержек, тогда в переменную
a
у нас сохранится значение 0, а в переменную b
значение 1, все просто.👉 Кейс второй 0 и 0. В данном случае второй поток стартует с некоторой задержкой, и первый поток успевает изменить переменную
y
. В этом случае в переменную a
у нас сохраняется значение 0 и в переменную b
тоже сохраняется значение 0.👉 Кейс третий 1 и 1. В этом кейсе наоборот первый поток стартует с задержкой, и второй поток успевает затереть переменную
x
. Тогда в переменную a
у нас сохранится значение 1, и в переменную b
значение 1, в целом все очевидно .👉 Кейс четвертый 1 и 0. Самый загадочный из всех кейсов, воспроизвести его безумно сложно, практически нереально, но в теории возможно. Как такое может произойти? Вкратце, это еще одна оптимизация которую может сделать компилятор, процессор или окружение.
☝️Для начала договоримся, что действие это либо запись, либо чтение с переменной. Как вы все знаете компиляторы и процессоры очень сложные штуки 🧐. Компилятор может переставить действия местами если посчитает, что так будет быстрее. 💻 Процессор и JVM могут выполнять действия не в том порядке как они расположены в коде, и могут выполнять их как им это покажется нужным.
Еще раз наглядно на коде:
x = 3
y = 5
Нет никакой гарантии того, что операции присваивания в
x
будет выполнена первой. Если у вас однопоточная программа, то эти перестановки вообще никак не влияют на ее выполнение, и можно вообще на это забить. 🧵Однако если у программы несколько потоков, и они еще и обращаются к одному и тому же месту, то всегда помните что JVM, компилятор или процессор могут переупорядочить действия.Следовательно, в последнем кейсе это и произошло, инструкции с присваиванием поменялись местами. JVM спокойно могла их выполнить не в том, порядке в котором они расположены, так как это никак не повлияет на логику. Другими словами было:
var b = y
x = 1
Стало:
x = 1
var b = y
Избежать этих перестановок можно, а как и когда это нужно поговорим позже)
👍3❤1
Happens-before. На каждом собесе где меня спрашивали про многопоточность, задавали вопрос про Happens-before. В целом это не сложная концепция, но порой сложно конкретно ответить на этот вопрос, давай те разберем и эту тему.
Начнем с того, что это не проблема многопоточности, а скорее некоторая абстракция, или даже набор правил. Для наглядности начнем с кода. Представим, что у нас есть две функции
Далее у нас есть два потока, поток first и поток second, функция
Теперь следим за руками, если сказано, что
Помните мы разбирали, проблему с Visibility, так вот если сказано, что гарантируется
Как использовать это на практике? Самый простой способ просто использовать синхронизацию:
Представим, что поток first точно запуститься первым. В примере получается, что
Для примера, несколько вещей в java в которых гарантируется happens-before:
👉Правило запуска потока. Вызов Thread.start на потоки происходит перед каждым действием в запущенном потоке.
👉Правило мониторного замка. Операция unlock на мониторном замке происходит перед каждой последующей операцией lock на том же самом мониторном замке.
👉Правило финализатора. Завершение конструктора объекта происходит перед началом финализатора этого объекта.
🧐Немного запутанная штука, но по сути достаточно понять что показано в картинке и уметь это как-то объяснить.
Начнем с того, что это не проблема многопоточности, а скорее некоторая абстракция, или даже набор правил. Для наглядности начнем с кода. Представим, что у нас есть две функции
operationFirst()
и operationSecond()
, которые что-то делают, как-то изменяют состояние объекта:
class Some {
private var x = 0
private var y = 0
fun operationFirst(){
x++
}
fun operationSecond(){
y++
}
}
Далее у нас есть два потока, поток first и поток second, функция
operationFirst()
вызывается в потоке first, функция operationSecond()
вызывается в потоке Second.
val some = Some()
val first = thread { some.operationFirst() }
val second = thread { some.operationSecond() }
Теперь следим за руками, если сказано, что
operationFirst() happens-before operationSecond()
это означает что все изменения, которые сделал поток first до момента вызова функции operationFirst()
и изменения, которые произошли в самой функции operationFirst()
будут видны потоку second в момент вызова функции operationSecond()
. Помните мы разбирали, проблему с Visibility, так вот если сказано, что гарантируется
operationFirst() happens-before operationSecond()
, то это значит, что проблемой с Visibility точно не будет, поток second точно увидит актуальное значение переменных. Мы также затрагивали проблему Reordering? Если гарантируется happens-before, то переупорядочивание нам тоже не страшно.Как использовать это на практике? Самый простой способ просто использовать синхронизацию:
val lock = Lock()
val first = thread { lock.withLock { some.operationFirst() } }
val second = thread { lock.withLock { some.operationSecond() } }
first.join()
second.join()
Представим, что поток first точно запуститься первым. В примере получается, что
operationFirst() happens-before operationSecond()
, следовательно, все что будет сделано в потоке first, увидит поток second в момент исполнения функции operationSecond()
.Для примера, несколько вещей в java в которых гарантируется happens-before:
👉Правило запуска потока. Вызов Thread.start на потоки происходит перед каждым действием в запущенном потоке.
👉Правило мониторного замка. Операция unlock на мониторном замке происходит перед каждой последующей операцией lock на том же самом мониторном замке.
👉Правило финализатора. Завершение конструктора объекта происходит перед началом финализатора этого объекта.
🧐Немного запутанная штука, но по сути достаточно понять что показано в картинке и уметь это как-то объяснить.
👍4❤1
Последняя проблема в списке, но не по значению – DeadLock. DeadLock один из сбоев жизнеспособности. Эта самая популярная проблема многопоточности на практике, которая приносит больше всего проблем. Суть проблемы очень проста, её можно описать небольшим снипетом кода:
☝️Значит есть два потока, которые ждут друг друга, и итоге программа зависает и никуда не продвигается. Пример кода объясняющий суть немного синтетический, на практике DeadLock возникает немного по другой причине. Еще один пример с кодом, погнали:
🧵 Поток A вызывает метод
Решил сильно не растягивать пост, этого достаточно для базового понимания того, что такое DeadLock. Помимо причин приведённых в посте есть еще множество способов вызывать DeadLock. Также есть еще различные виды сбоев жизнеспособности которые мы возможно обсудим отдельно.
И как всегда проблему обозначили, а как её диагностировать и решать обсуждаем отдельно.
val treadList = mutableListOf<Thread>()
treadList += Thread { thread[1].join() }
treadList += Thread { thread[0].join() }
treadList.forEach { it.start() }
☝️Значит есть два потока, которые ждут друг друга, и итоге программа зависает и никуда не продвигается. Пример кода объясняющий суть немного синтетический, на практике DeadLock возникает немного по другой причине. Еще один пример с кодом, погнали:
class LeftRightDeadlock {
private val leftLock = ReentrantLock()
private val rightLock = ReentrantLock()
fun leftRight() {
leftLock.withLock {
rightLock.withLock {
doSomething()
}
}
}
fun rightLeft() {
rightLock.withLock {
leftLock.withLock {
doSomething()
}
}
}
}
🧵 Поток A вызывает метод
leftRight()
, в этот момент поток B вызывает метод rightLeft()
, так как они захватывают замки в разных порядках, оба потока ждут освобождения ресурса, которые не будут освобождены, как на рисунке. Решил сильно не растягивать пост, этого достаточно для базового понимания того, что такое DeadLock. Помимо причин приведённых в посте есть еще множество способов вызывать DeadLock. Также есть еще различные виды сбоев жизнеспособности которые мы возможно обсудим отдельно.
И как всегда проблему обозначили, а как её диагностировать и решать обсуждаем отдельно.
👍6❤1
Начнем с решением проблемы Visibility.
Итак, два потока🧵, меняют переменную, которая сначала записывается в кеши, а не в оперативную память. Есть 2️⃣ варианта решения проблемы.
👉Первый это модификатор volatile. В языках java, c++, c# для этого существует специальный модификатор volatile (в языке kotlin это делается при помощи аннотации @Volatile). Когда вы ставите этот модификатор над переменной, это говорит компилятору и окружению о том, что когда мы работаем с этой переменной, сразу записывать данное значение в оперативную память минуя кеши.
👉Второй вариант использовать такую штуку, как монитор. В java монитор реализован при помощи замков, которые мы упоминали в прошлом посте. Если не углубляться в подробности, при помощи этой штуки мы говорим, что допустим вот эту функцию одновременно может вызывать лишь один поток. Остальные просто будут ожидать. Если мы с захватом монитора модифицируем переменную, у нас автоматически решается проблема Visibility. Однако, чтение тоже должно быть с захватом монитора.
Вот так просто мы решаем проблему Visibility.
Итак, два потока🧵, меняют переменную, которая сначала записывается в кеши, а не в оперативную память. Есть 2️⃣ варианта решения проблемы.
👉Первый это модификатор volatile. В языках java, c++, c# для этого существует специальный модификатор volatile (в языке kotlin это делается при помощи аннотации @Volatile). Когда вы ставите этот модификатор над переменной, это говорит компилятору и окружению о том, что когда мы работаем с этой переменной, сразу записывать данное значение в оперативную память минуя кеши.
👉Второй вариант использовать такую штуку, как монитор. В java монитор реализован при помощи замков, которые мы упоминали в прошлом посте. Если не углубляться в подробности, при помощи этой штуки мы говорим, что допустим вот эту функцию одновременно может вызывать лишь один поток. Остальные просто будут ожидать. Если мы с захватом монитора модифицируем переменную, у нас автоматически решается проблема Visibility. Однако, чтение тоже должно быть с захватом монитора.
Вот так просто мы решаем проблему Visibility.
❤3👍1
Была небольшая пауза, но мы идем дальше. Следующая проблема Atomicity.
Значит у нас есть Long/Double которые 64 бита, и при записи записываются первые 32 бита, затем вторые, когда один поток 👍, когда больше 👎.
Решение у этой проблемы аналогично предыдущей. Если у полей класса, типа Long/Double поставить модификатор volatile, это прикажет окружению записывать данные атомарно. Другими словами теперь операция записи/чтения в Long/Double будут происходить в одну операцию, и теперь не паримся если несколько потоков. ☝️Важно запомнить что если модифицируем переменную из нескольких потоков, и переменная Long/Double обязательно ставим volatile.
И конечно второй вариант использовать монитор. Если чтение/запись в поле Long/Double будет происходить через использование монитора, то в этом случае у нас тоже гарантируется атомарность. Это очевидно ведь мы используем монитор, а это означает что другие потоки в этот момент ждут. В таком случае, даже если операция записи будет в 5 операций это ни на что не повлияет. ☝️Однако помните, что в этом случае и чтение и запись, должны быть из монитора иначе вся магия пропадает.
Значит у нас есть Long/Double которые 64 бита, и при записи записываются первые 32 бита, затем вторые, когда один поток 👍, когда больше 👎.
Решение у этой проблемы аналогично предыдущей. Если у полей класса, типа Long/Double поставить модификатор volatile, это прикажет окружению записывать данные атомарно. Другими словами теперь операция записи/чтения в Long/Double будут происходить в одну операцию, и теперь не паримся если несколько потоков. ☝️Важно запомнить что если модифицируем переменную из нескольких потоков, и переменная Long/Double обязательно ставим volatile.
И конечно второй вариант использовать монитор. Если чтение/запись в поле Long/Double будет происходить через использование монитора, то в этом случае у нас тоже гарантируется атомарность. Это очевидно ведь мы используем монитор, а это означает что другие потоки в этот момент ждут. В таком случае, даже если операция записи будет в 5 операций это ни на что не повлияет. ☝️Однако помните, что в этом случае и чтение и запись, должны быть из монитора иначе вся магия пропадает.
👍3