Итак инвариант, что это и зачем это знать? В подкасте подлодка, на одном из выпусков про обязательные знания программиста, пришедший эксперт сказал интересную вещь. Он сказал, что каждый сеньор должен знать, что такое инвариант и знать как ответить на такой вопрос на собеседовании. Чтож давай-те разберем, что это, чтобы не ударить в грязь лицом🤦♂️.
Начнем с истории, само понятие инвариант, пришло к нам из математики. Инвариант в математике - это выражение которое сохраняет свое значение, аля если у нас есть функция
💻 Вернемся в программирование. В программировании инвариантом называют предикат (читай некоторое условие) который всегда истинный. Другими словами если функция инвариантна, значит некоторое условие сохраняется до вызова функции и после вызова функции. Если класс инвариантен, значит его состояние всегда удовлетворяет какому-то условию. Для еще большого понимания, есть языки программирования, в которых понятие инварианта вшито в синтаксис языка, вот пример язык 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
Решение проблемы Reordering.
😅Это довольно забавно, но для решения этой проблемы инструменты не меняются. Снова 2️⃣ варианта.
👉 Модификатор volatile, запрещает окружению переставлять действия местами. Это значит, чтобы, избежать четвертого кейса который мы разбирали тут, достаточно поставить volatile перед полями класса.
👉 И конечно же вариант использовать монитор. Если производить операции будет только один поток, а второй будет ждать очереди, проблема Reordering просто отпадает.
😅Это довольно забавно, но для решения этой проблемы инструменты не меняются. Снова 2️⃣ варианта.
👉 Модификатор volatile, запрещает окружению переставлять действия местами. Это значит, чтобы, избежать четвертого кейса который мы разбирали тут, достаточно поставить volatile перед полями класса.
👉 И конечно же вариант использовать монитор. Если производить операции будет только один поток, а второй будет ждать очереди, проблема Reordering просто отпадает.
👍3
Последняя проблема из списка, Deadlock. 🔐
Вероятность того, что вы столкнетесь с такой проблемой в продакшен коде крайне мала, однако стоит хотя бы примерно понимать как искать проблему 🔎, если все таки стрельнет.
☝️Для начала стоит запомнить фразу: "Программа, которая никогда не приобретает более одного замка за один раз, не может столкнуться с взаимной блокировкой, которая инициируется порядком блокировки." Это значит, что когда пишете код, старайтесь делать так, чтобы поток не захватывал более двух замков сразу. Если по другому никак, то проверяйте, чтобы все потоки захватывали замки в одном и том же порядке.
Есть три основных инструмент которые вы можете использовать для того, чтобы найти причину deadlock:
👉 Поточный дамп
👉 API явных замков
👉 Статические анализаторы кода и ревью кода
Вкратце пройдемся по каждому инструменту.
В JVM есть механизм, по которому мы можем получить информацию о том, что делает каждый конкретный поток. Используя этот инструмент можно найти где именно у нас происходит deadlock. Для этого запускаем программу, затем приказываем JVM собрать поточный дамп, обычно для этого есть спец кнопка📌 в IDE. Затем мы получаем некоторый листинг. В листинге нужно найти потоки состояние которых: "waiting to lock monitor". Это знак того, что скорее всего поток ждет ресурс который другой поток не может освободить. В иллюстрации с посту есть пример того, как выглядит этот листинг.
Второй инструмент это использовать явные замки 🔐, т.е объекты типа Lock. В API этих замков есть метод tryLock с timeout ⏲ который мы можем задать. Суть этого метода сводится к тому, что мы просто заменяем обычные методы lock на tryLock с каким-нибудь временем, например 3 секунды. Затем запускаем программу и начинаем тестить, если программа упала 💥, мы явно увидим где конкретно, а дальше уже отталкиваясь от этого можно найти конкретную причину.
Последний инструмент, наверное самый лучший из всех, это наши глаза 👀 и статические анализаторы кода 🤖. На данный момент на рынке очень много линтеров, которые умеют подсказывать о возможных ошибках в многопоточности из коробки. Стоит погуглить их и затащить в свой проект. Ну и конечно никто не отменяет код ревью, очень внимательно проверять места, где используются примитивы многопоточности 🧵. Если мы говорим про Android разработку, то таких мест будет крайне мало, так как Rx и корутины покрывают 90% кейсов. Во всех остальных случаях супер внимательно смотрим не наговнокодил ли коллега👨💻 и очень внимательно с максимальной критикой просматриваем свой код.
Этого должно хватить если вы вдруг столкнулись с такой проблемой.
Вероятность того, что вы столкнетесь с такой проблемой в продакшен коде крайне мала, однако стоит хотя бы примерно понимать как искать проблему 🔎, если все таки стрельнет.
☝️Для начала стоит запомнить фразу: "Программа, которая никогда не приобретает более одного замка за один раз, не может столкнуться с взаимной блокировкой, которая инициируется порядком блокировки." Это значит, что когда пишете код, старайтесь делать так, чтобы поток не захватывал более двух замков сразу. Если по другому никак, то проверяйте, чтобы все потоки захватывали замки в одном и том же порядке.
Есть три основных инструмент которые вы можете использовать для того, чтобы найти причину deadlock:
👉 Поточный дамп
👉 API явных замков
👉 Статические анализаторы кода и ревью кода
Вкратце пройдемся по каждому инструменту.
В JVM есть механизм, по которому мы можем получить информацию о том, что делает каждый конкретный поток. Используя этот инструмент можно найти где именно у нас происходит deadlock. Для этого запускаем программу, затем приказываем JVM собрать поточный дамп, обычно для этого есть спец кнопка📌 в IDE. Затем мы получаем некоторый листинг. В листинге нужно найти потоки состояние которых: "waiting to lock monitor". Это знак того, что скорее всего поток ждет ресурс который другой поток не может освободить. В иллюстрации с посту есть пример того, как выглядит этот листинг.
Второй инструмент это использовать явные замки 🔐, т.е объекты типа Lock. В API этих замков есть метод tryLock с timeout ⏲ который мы можем задать. Суть этого метода сводится к тому, что мы просто заменяем обычные методы lock на tryLock с каким-нибудь временем, например 3 секунды. Затем запускаем программу и начинаем тестить, если программа упала 💥, мы явно увидим где конкретно, а дальше уже отталкиваясь от этого можно найти конкретную причину.
Последний инструмент, наверное самый лучший из всех, это наши глаза 👀 и статические анализаторы кода 🤖. На данный момент на рынке очень много линтеров, которые умеют подсказывать о возможных ошибках в многопоточности из коробки. Стоит погуглить их и затащить в свой проект. Ну и конечно никто не отменяет код ревью, очень внимательно проверять места, где используются примитивы многопоточности 🧵. Если мы говорим про Android разработку, то таких мест будет крайне мало, так как Rx и корутины покрывают 90% кейсов. Во всех остальных случаях супер внимательно смотрим не наговнокодил ли коллега👨💻 и очень внимательно с максимальной критикой просматриваем свой код.
Этого должно хватить если вы вдруг столкнулись с такой проблемой.
👍6❤1
Большая часть проголосовала за IPC (я кстати сам его тоже выбрал), поэтому похардкодим немножечко🦾...
Для начала немного вспомним CS, а конкретнее разницу м/у потоком и процессом. Нужно понимать разницу, чтобы понять что такое вообще IPC, поэтому пройдемся поверхностно.
📝 Процесс это некоторый объект в системе, которому ОС выделяет время процессора, память, доступ к сети, доступ к железкам и т.п. Запускаете программу, ОС для этой программы создает процесс, который получает время процессора и память.
👣Идем дальше, каждый процесс в системе как бы интроверт, он не может обратиться к участку памяти который выделен другому процессу. И это понятно почему, было бы крайне не приятно если бы левая прога начала читать участок памяти где крутится банковское приложение.
У каждого процесса есть состояние. Упростим до 2-х, он может быть либо активен, либо быть приостановлен. Одно из неудобств программирования под Android, что если пользователь свернет наше приложение, система может грохнуть 🧨 наш процесс. Под "грохнуть процесс" подразумевается, что ОС не видит смысла больше выделять оперативную память для этого процесса (нашего приложения) и просто выгружает его, или по другому забирает память 👨🦳.
В таком случае, все наши данные которые мы сохраняли в static поля или во ViewModel просто убьются. Однако данные которые успели записаться в Bundle потом восстановятся, не забываем про это.
🧵 Далее идет поток. Поток в некотором грубом смысле это более легкая версия процесса. Мы открываем приложение, система создает для него процесс. Уже в этом процессе мы можем создавать потоки, которые по сути являются просто участками кода которые выполняются паралельно.
🧵 Потоки в отличие от процессов уже имеют доступ к памяти друг друга, потому как находятся в общем участке памяти процесса. Значит создав переменную мы спокойно можем менять её значение из нескольких потоков. Конечно есть свои сложности, но это можно легко сделать.
📵С процессом такое не проканает, нельзя сделать переменную которую можно поменять их нескольких процессов. По сути процессы мало что знаю друг о друге.
Бывают случаи когда нам нужно или получить данные с другого процесса, или наоборот что-то сообщить процессу. Что это за случай разберем в след постах.
Для начала немного вспомним CS, а конкретнее разницу м/у потоком и процессом. Нужно понимать разницу, чтобы понять что такое вообще IPC, поэтому пройдемся поверхностно.
📝 Процесс это некоторый объект в системе, которому ОС выделяет время процессора, память, доступ к сети, доступ к железкам и т.п. Запускаете программу, ОС для этой программы создает процесс, который получает время процессора и память.
👣Идем дальше, каждый процесс в системе как бы интроверт, он не может обратиться к участку памяти который выделен другому процессу. И это понятно почему, было бы крайне не приятно если бы левая прога начала читать участок памяти где крутится банковское приложение.
У каждого процесса есть состояние. Упростим до 2-х, он может быть либо активен, либо быть приостановлен. Одно из неудобств программирования под Android, что если пользователь свернет наше приложение, система может грохнуть 🧨 наш процесс. Под "грохнуть процесс" подразумевается, что ОС не видит смысла больше выделять оперативную память для этого процесса (нашего приложения) и просто выгружает его, или по другому забирает память 👨🦳.
В таком случае, все наши данные которые мы сохраняли в static поля или во ViewModel просто убьются. Однако данные которые успели записаться в Bundle потом восстановятся, не забываем про это.
🧵 Далее идет поток. Поток в некотором грубом смысле это более легкая версия процесса. Мы открываем приложение, система создает для него процесс. Уже в этом процессе мы можем создавать потоки, которые по сути являются просто участками кода которые выполняются паралельно.
🧵 Потоки в отличие от процессов уже имеют доступ к памяти друг друга, потому как находятся в общем участке памяти процесса. Значит создав переменную мы спокойно можем менять её значение из нескольких потоков. Конечно есть свои сложности, но это можно легко сделать.
📵С процессом такое не проканает, нельзя сделать переменную которую можно поменять их нескольких процессов. По сути процессы мало что знаю друг о друге.
Бывают случаи когда нам нужно или получить данные с другого процесса, или наоборот что-то сообщить процессу. Что это за случай разберем в след постах.
👍4
После того как мы осознали разницу между потоком и процессом разберем задачу. Есть два (🪑) приложения, допустим одно приложение это калькулятор💻, а второе банковское приложение💰. Банковское приложение не умеет считать, следовательно, для расчета оно должно обратиться к приложению калькулятор, который как мы знаем находится в другом процессе. Как это сделать?
💡Ответом будет IPC. IPC расшифровывается как inter-process communication, или по-русски межпроцессная (не путать с межпроцессорным) коммуникация. Это механизм, который позволяет общаться процессам друг с другом.
У IPC есть куча методов, учитывая, что мы говорим в контексте системы Android, разберем основные которые можно использовать в этой системе:
🌐 По сети. Приложение калькулятор делает локальный сервер, а банковское приложение просто стучится к этому серверу. Два приложения на одном устройстве, значит даже интернет не нужен.
📁Через файл. Банковское приложение оставляет заявку на расчет в некотором файле. Калькулятор постоянно следит за этим файлом. Как только в нем оказывается заявка, калькулятор делает расчет и пишет результат в другой файл, который уже подхватывает банковское приложение.
👉Content provider. Эта штука специфична для платформы Android. Content provider один из основных компонентов Android приложения, который позволяет делиться данными нашего приложения с другими. Его например использует Яндекс . Когда вы залогинились в одном приложении, вы автоматически логинитесь в других приложениях Яндекса.
🌚Фреймворк Binder❓
У каждого варианта есть свои недостатки и свои плюсы. Рассмотрим недостатки каждого:
👉Вариант с сетью. Основная проблема в том, что система может неожиданно прибить 🧨 приложение сервер. Учитывая, что Android последних версий вообще ужесточает правила для приложений работающих в фоне, этот вариант почти нереален.
👉С файлом еще больше проблем: может закончиться место, другой процесс прибьет файл, приложение калькулятор может упасть и испортить файл, или система просто прибьет приложение калькулятор.
👉 С Content provider в этом плане все намного лучше, однако он очень ограничен . По своей сути работа с Content provider похожа на работу с обычной базой данных 🗄. Мы можем получить данные, или сохранить, но мы не можем вызывать функцию у другого процесса и попросить дернуть наше приложение позже с ответом.
И остается Binder, темная лошадка 🐎, о которой знают даже не все Android разработчики. Что это и как использовать разберем в следующих постах.
💡Ответом будет IPC. IPC расшифровывается как inter-process communication, или по-русски межпроцессная (не путать с межпроцессорным) коммуникация. Это механизм, который позволяет общаться процессам друг с другом.
У IPC есть куча методов, учитывая, что мы говорим в контексте системы Android, разберем основные которые можно использовать в этой системе:
🌐 По сети. Приложение калькулятор делает локальный сервер, а банковское приложение просто стучится к этому серверу. Два приложения на одном устройстве, значит даже интернет не нужен.
📁Через файл. Банковское приложение оставляет заявку на расчет в некотором файле. Калькулятор постоянно следит за этим файлом. Как только в нем оказывается заявка, калькулятор делает расчет и пишет результат в другой файл, который уже подхватывает банковское приложение.
👉Content provider. Эта штука специфична для платформы Android. Content provider один из основных компонентов Android приложения, который позволяет делиться данными нашего приложения с другими. Его например использует Яндекс . Когда вы залогинились в одном приложении, вы автоматически логинитесь в других приложениях Яндекса.
🌚Фреймворк Binder❓
У каждого варианта есть свои недостатки и свои плюсы. Рассмотрим недостатки каждого:
👉Вариант с сетью. Основная проблема в том, что система может неожиданно прибить 🧨 приложение сервер. Учитывая, что Android последних версий вообще ужесточает правила для приложений работающих в фоне, этот вариант почти нереален.
👉С файлом еще больше проблем: может закончиться место, другой процесс прибьет файл, приложение калькулятор может упасть и испортить файл, или система просто прибьет приложение калькулятор.
👉 С Content provider в этом плане все намного лучше, однако он очень ограничен . По своей сути работа с Content provider похожа на работу с обычной базой данных 🗄. Мы можем получить данные, или сохранить, но мы не можем вызывать функцию у другого процесса и попросить дернуть наше приложение позже с ответом.
И остается Binder, темная лошадка 🐎, о которой знают даже не все Android разработчики. Что это и как использовать разберем в следующих постах.
👍6
Тему я назвал хардкорной ⛓ не просто так, с каждым постом сложность будет возрастать, однако я постараюсь объяснить на пальцах, погнали 👉
☝️Мы разбираем тему отталкиваясь от проблемы. В прошлом посте мы разбирали пример с банковским приложением и калькулятором. Этот пример довольно синтетический, он далек от реальности. Разберем реальную проблему.
У нас на устройствах есть📍GPS и приложения, которые хотят использовать его: такси, карты, навигатор и т.д. Туева хуча приложений и давать прямой доступ довольно рискованно, потому как вдруг левое приложение захочет в фоне отслеживать нашу позицию🥷.
🤔Значит доступ к датчику GPS должен быть только у некоторого системного сервиса (по сути приложение, только без UI). Сервиса, который поставляется самой системой и которому мы можем доверять (не можем и гугл знает о вас все 🤷♂️).
👉 Теперь приложения не ходят на прямую к датчику GPS, а просят данные у системного сервиса, который в свою очередь решает кому эти данные отдавать, а кому нет, как часто и с какой точностью. Как реализовать такой механизм, в чем отличие этого сервиса от нашего приложения?
💻Каждое приложение в Android работает в своем процессе и со своим экземпляром JVM это называется "Песочница". Механизм песочницы основан на том, что каждому приложению назначается уникальный user ID (UID) и group ID (GID). Значит, у каждого приложения в Android есть свой непривилегированный пользователь 👨.
🤴 В системе также есть привилегированные пользователи, имена и идентификаторы которых жестко зашиты в систему⚙️. Они нужны для сервисов которые имеют доступ к критичным секциям ресурсов (GPS, контакты, ваши любовные смс-ки). У обычных приложений нет доступа к критичным секциям, так как у них непривилегированный пользователь 🤕.
🤗Все круто, безопасно, но как теперь данные то получать с сервиса, ведь наше приложение и сервис работают в разных процессах? Как вы уже догадались ни один из способов описанных в предыдущем посте не подойдет. И тут на сцену выходит Binder.
👉 Binder IPC это фреймворк межпроцессной коммуникации, который пришел на замену System V IPCs. Кто не в теме, System V IPCs по сути это набор низкоуровневых функций, которые реализованы на уровне ядра, позволяющие обмениваться данными между процессами так, будто у них есть общая память (надеюсь меня не читают лютые линуксойды, иначе закидают ссанымы тряпками за такое упрощение 😅).
👉Ну так вот, этот Binder IPC невероятно сложная и крутая штука, которая решает нашу проблему. Фреймворк позволяет синхронно и асинхронно вызывать методы удаленных объектов (значит методы приложений/сервисов, которые работают в другом процессе) так, будто они локальные.
🎛 Все остальные способы взаимодействия между процессами, включая Intents и Content Provider, реализованы используя Binder IPC. При помощи фреймворка мы можем попросить системный сервис дергать функции нашего приложения когда, допустим меняется локация пользователя 📍.
Что это такое мы разобрали, а как он работает разберем в следующем посте 🤚
☝️Мы разбираем тему отталкиваясь от проблемы. В прошлом посте мы разбирали пример с банковским приложением и калькулятором. Этот пример довольно синтетический, он далек от реальности. Разберем реальную проблему.
У нас на устройствах есть📍GPS и приложения, которые хотят использовать его: такси, карты, навигатор и т.д. Туева хуча приложений и давать прямой доступ довольно рискованно, потому как вдруг левое приложение захочет в фоне отслеживать нашу позицию🥷.
🤔Значит доступ к датчику GPS должен быть только у некоторого системного сервиса (по сути приложение, только без UI). Сервиса, который поставляется самой системой и которому мы можем доверять (не можем и гугл знает о вас все 🤷♂️).
👉 Теперь приложения не ходят на прямую к датчику GPS, а просят данные у системного сервиса, который в свою очередь решает кому эти данные отдавать, а кому нет, как часто и с какой точностью. Как реализовать такой механизм, в чем отличие этого сервиса от нашего приложения?
💻Каждое приложение в Android работает в своем процессе и со своим экземпляром JVM это называется "Песочница". Механизм песочницы основан на том, что каждому приложению назначается уникальный user ID (UID) и group ID (GID). Значит, у каждого приложения в Android есть свой непривилегированный пользователь 👨.
🤴 В системе также есть привилегированные пользователи, имена и идентификаторы которых жестко зашиты в систему⚙️. Они нужны для сервисов которые имеют доступ к критичным секциям ресурсов (GPS, контакты, ваши любовные смс-ки). У обычных приложений нет доступа к критичным секциям, так как у них непривилегированный пользователь 🤕.
🤗Все круто, безопасно, но как теперь данные то получать с сервиса, ведь наше приложение и сервис работают в разных процессах? Как вы уже догадались ни один из способов описанных в предыдущем посте не подойдет. И тут на сцену выходит Binder.
👉 Binder IPC это фреймворк межпроцессной коммуникации, который пришел на замену System V IPCs. Кто не в теме, System V IPCs по сути это набор низкоуровневых функций, которые реализованы на уровне ядра, позволяющие обмениваться данными между процессами так, будто у них есть общая память (надеюсь меня не читают лютые линуксойды, иначе закидают ссанымы тряпками за такое упрощение 😅).
👉Ну так вот, этот Binder IPC невероятно сложная и крутая штука, которая решает нашу проблему. Фреймворк позволяет синхронно и асинхронно вызывать методы удаленных объектов (значит методы приложений/сервисов, которые работают в другом процессе) так, будто они локальные.
🎛 Все остальные способы взаимодействия между процессами, включая Intents и Content Provider, реализованы используя Binder IPC. При помощи фреймворка мы можем попросить системный сервис дергать функции нашего приложения когда, допустим меняется локация пользователя 📍.
Что это такое мы разобрали, а как он работает разберем в следующем посте 🤚
👍11❤1