Priority inversion у Swift: чому DispatchSemaphore поганий lock
Чому DispatchSemaphore як lock — це погано для priority inversion?
Бо у семафора немає поняття власника (owner). Він керує кількістю дозволів, а signal() може викликати інший потік. Коли високопріоритетний потік блокується на wait(), цей механізм не повідомляє системі, якому саме потоку потрібно допомогти завершити роботу. Для взаємного виключення краще використати примітив із відомим власником. Apple прямо пояснює цю різницю між dispatch_semaphore та owner-aware locks у сесії про планування CPU.
Марсохід на обкладинці тут не випадково: у 1997 році саме priority inversion перезавантажувала комп’ютер посадкового апарата Mars Pathfinder, який доставив Sojourner на Марс. Як це сталося, пояснено нижче.
На iPhone наслідком такої залежності може бути затримка інтерфейсу. На Mars Pathfinder вона призвела до перезавантаження комп’ютера посадкового апарата. Щоб зрозуміти обидва випадки, достатньо трьох потоків і одного ресурсу.
Коротко
- У
DispatchSemaphoreнемає власника. Коли High чекає вwait(), система не знає, якому потоку допомогти, тож Medium може витісняти Low, а High чекає далі. - Priority inheritance можлива, лише коли власник відомий.
Synchronization.Mutexу Swift 6 на Darwin спирається наos_unfair_lock, і система бачить, хто тримає lock. - На Mars Pathfinder у 1997 році така інверсія призводила до перезавантажень комп’ютера посадкового апарата. Команда виправила це, увімкнувши priority inheritance для mutex.
- Семафор як lock компілятор не перевіряє: з
@unchecked Sendableможна забутиwait()і мовчки втрачати дані.Mutexтаку помилку не скомпілює. Обидва варіанти можна запустити просто в статті. - Не чекайте на
Taskчерез семафор: це займає потік cooperative pool і може призвести до deadlock. Використовуйтеawait.
Що таке priority inversion
Уявімо один CPU та три потоки з фіксованими пріоритетами: High > Medium > Low. Планувальник віддає CPU найпріоритетнішому з тих, що готові виконуватися. Потік, який чекає на зайнятий lock, не готовий: без ресурсу він не може продовжити роботу.
- Low захоплює lock і починає змінювати спільні дані.
- Прокидається High, пробує взяти той самий lock і блокується.
- З’являється Medium. Цей ресурс йому взагалі не потрібен, але його пріоритет вищий за Low, тому CPU отримує він.
- Low не завершує критичну секцію, lock залишається зайнятим, High продовжує чекати.
🔎 На що дивитися: під час анімації стежте за рядком CPU. Поки High чекає на lock, CPU отримує Medium, а не Low, який цей lock тримає. Лічильник «High чекає» показує, скільки триває очікування, а «Через Medium» — яку його частину забрав Medium.
* Ум. од. — умовні одиниці часу: одна поділка осі = одна одиниця. Це навчальна шкала, а не мілісекунди чи вимірювання продуктивності iOS. High чекає від t=2 до t=8: 8 − 2 = 6 ум. од.; з них робота Medium від t=2 до t=6 додає 6 − 2 = 4 ум. од.
* Рядок lock показує, хто володіє ресурсом; CPU — хто виконується. Колір смуги позначає потік: High, Medium або Low.
Це навчальна модель одного CPU з фіксованими пріоритетами. Кроки на осі часу умовні; вони не показують вимірювання чи точну поведінку планувальника iOS.
Залежність High від Low неминуча, доки Low змінює спільні дані. Інверсію погіршує третій учасник: Medium подовжує очікування High, хоча має нижчий пріоритет і не використовує потрібний High ресурс.
Це називають unbounded priority inversion: затримка вже не обмежена лише рештою короткої критичної секції Low. Це не твердження, що кожен виклик обов’язково зависне назавжди. У наведеній моделі нова готова робота Medium може знову відкладати завершення Low.
Jim має завершити роботу з файлом, на який чекає Michael. Dwight займає єдиний «CPU» іншою роботою. В офісі це схоже на звичайний понеділок; планувальнику потрібен точніший опис залежності.
На реальному пристрої з iOS є кілька ядер, QoS-класи та складніша політика планування. Більша кількість ядер не скасовує залежність від зайнятого ресурсу. Apple окремо застерігає про інверсії пріоритетів через невідповідність QoS і відсутність автоматичного перенесення пріоритету через очікування семафора в документації Thread Performance Checker.
Як priority inheritance допомагає власнику
Priority inheritance означає тимчасове підвищення ефективного пріоритету власника lock, коли на цей lock чекає пріоритетніший потік. У нашій моделі Low отримує змогу завершити критичну секцію перед Medium. Після звільнення lock High може продовжити роботу, а тимчасова допомога Low більше не потрібна.
🔎 На що дивитися: порівняйте рядок CPU з попередньою діаграмою. Тут Low отримує тимчасовий boost і працює раніше за Medium. Medium з’являється в той самий момент, що й на попередній діаграмі, але чекає на CPU (смугастий відрізок у його рядку). Тому лічильник «High чекає» показує 2 ум. од. замість 6.
* Ум. од. — умовні одиниці часу: одна поділка осі = одна одиниця. Це навчальна шкала, а не мілісекунди чи вимірювання продуктивності iOS. High чекає від t=2 до t=4: 4 − 2 = 2 ум. од. — замість 6 у схемі без успадкування пріоритету.
* Рядок lock показує, хто володіє ресурсом; CPU — хто виконується. Колір смуги позначає потік: High, Medium або Low.
Роботи залишилося стільки само. Змінився порядок: власник може звільнити ресурс раніше, ніж CPU отримає стороння робота Medium.
High не відбирає lock у Low і не читає напівзмінені дані. Мета механізму: допомогти потоку, від завершення якого залежить High.
Файл і далі належить Jim. Тимчасовий boost дозволяє йому закінчити роботу; після цього Michael отримує результат.
Що насправді сталося з Mars Pathfinder
Pathfinder сів у Ares Vallis 4 липня 1997 року. Місія включала посадковий апарат і марсохід Sojourner. Історія про priority inversion стосується комп’ютера посадкового апарата, а не перезавантажень марсохода.
За поясненням Glenn Reeves, керівника команди бортового ПЗ, високопріоритетна задача bc_dist розподіляла дані шини. Метеорологічна задача ASI/MET працювала з нижчим пріоритетом. Їхній IPC використовував VxWorks pipe() і select(), усередині яких був mutex для списку очікування файлових дескрипторів.
ASI/MET утримувала цей ресурс, коли її витіснили задачі середнього пріоритету. bc_dist заблокувалася на тому самому mutex. До наступного циклу вона не завершила роботу, перевірка дедлайну в bc_sched виявила збій, і система перезавантажила комп’ютер.
Команда відтворила проблему на Землі та ввімкнула priority inheritance, змінивши глобальну змінну, яка задає параметри створення цих mutex. Зміна поширилася на всі mutex механізму select(), створені після неї, а не лише на той, що обслуговував bc_dist і ASI/MET: логіка ініціалізації не давала простого способу змінити тільки його. Вплив перевірили перед передаванням патча на апарат. Reeves окремо уточнює: виправлення доставили спеціальним механізмом патчів, не через VxWorks shell.
Тут легко заплутатися в назвах: VxWorks називає цей примітив mutual exclusion semaphore. У нього є власник; це не те саме, що лічильний DispatchSemaphore у Swift. Збій показує, що навіть mutex потребує правильної конфігурації та відповідного механізму планування.
Ранні знімки посадкового апарата, рамп і Sojourner можна переглянути в історичному каталозі NASA.
Антипатерн: DispatchSemaphore(value: 1) замість lock
Ось клас, у якому доступ до масиву захищено семафором:
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
import Dispatch
final class SemaphoreResource: @unchecked Sendable {
private let gate = DispatchSemaphore(value: 1)
private var data: [String] = []
func append(_ value: String) {
gate.wait()
defer { gate.signal() }
data.append(value)
}
func snapshot() -> [String] {
gate.wait()
defer { gate.signal() }
return data
}
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Foundation
print("▶ Start at \(Date.now)\n")
let resource = SemaphoreResource()
DispatchQueue.concurrentPerform(iterations: 1_000) { index in
resource.append("value-\(index)")
}
let values = resource.snapshot()
print("Appended from 1,000 concurrent calls: \(values.count) values")
print("Every value is unique: \(Set(values).count == values.count)")
// ▷ Try: add a method that reads `data` without gate.wait(). The compiler
// stays silent: @unchecked Sendable switched that check off.value: 1 дозволяє одному учаснику пройти до критичної секції. Решта чекають. defer гарантує signal() після успішного wait() при виході з методу. Обидва методи дотримуються того самого правила, тому сама ця реалізація не є прикладом data race.
Проблема в контракті примітива. Low виконав wait() і зайшов усередину. High тепер чекає на дозвіл, але семафор не вимагає, щоб signal() виконав саме Low. Зв’язку «цей waiter залежить від цього власника» в такого API немає. Використання семафора як lock додає це правило лише в голові розробника.
🔎 На що дивитися: порівняйте дві частини схеми. У частині DispatchSemaphore очікування High не веде до жодного конкретного потоку. У частині «Mutex на Darwin» воно веде до власника Low, і система знає, кому допомогти.
@unchecked Sendable тут теж має ціну: автор класу вручну обіцяє компілятору, що весь доступ до змінного стану синхронізований. Якщо завтра додати метод, який читає data без wait(), ця обіцянка стане неправдивою. Атрибут не перевіряє реалізацію за нас.
Synchronization.Mutex у Swift 6
Для короткого синхронного доступу до спільного стану можна використати Mutex. Нижче компілятор перевіряє відповідність класу Sendable без @unchecked на нашому типі:
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
import Synchronization
final class MutexResource: Sendable {
private let data = Mutex<[String]>([])
func append(_ value: String) {
data.withLock { protectedData in
protectedData.append(value)
}
}
func snapshot() -> [String] {
data.withLock { $0 }
}
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Dispatch
import Foundation
print("▶ Start at \(Date.now)\n")
let resource = MutexResource()
DispatchQueue.concurrentPerform(iterations: 1_000) { index in
resource.append("value-\(index)")
}
let values = resource.snapshot()
print("Appended from 1,000 concurrent calls: \(values.count) values")
print("Every value is unique: \(Set(values).count == values.count)")
// ▷ Try: add a method that reads the array without withLock. There is no
// unprotected array to reach: it lives inside the Mutex.Масив зберігається всередині mutex. withLock надає ексклюзивний доступ до нього й звільняє lock після замикання, зокрема якщо воно кидає помилку. snapshot() повертає значення масиву, а не посилання на незахищений mutable-об’єкт.
У реалізації Darwin зі Swift 6.0 Mutex спирається на os_unfair_lock. Apple документує інформацію про потік-власник, яку система може використати, щоб спробувати усунути інверсію пріоритетів. Це потрібна залежність для допомоги Low.
Виклик withLock не мусить щоразу звертатися до ядра. Так само цей API не обіцяє миттєвого виконання High, FIFO чи захисту від довгої роботи всередині lock. У SE-0433 гарантія сформульована як взаємне виключення; конкретні властивості планування залежать від платформи.
Приклад розрахований на Swift 6 та iOS 18+ / macOS 15+. Версія компілятора і мінімальна версія ОС є окремими вимогами: встановлення нового Xcode саме по собі не робить API доступним на старій ОС.
Забутий lock: семафор проти Mutex
Обіцянку @unchecked Sendable легко порушити. Зліва лічильник на семафорі, у якому increment() забув gate.wait(). Компілятор мовчить. Справа той самий лічильник на Mutex: значення лежить усередині mutex, тож змінити його можна лише через withLock, а value += 1 поза ним не скомпілюється. Натисніть «Запустити обидва»: зліва частина інкрементів губиться, справа виходить рівно 1 000 000.
З багом
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
import Dispatch
final class Counter: @unchecked Sendable {
private let gate = DispatchSemaphore(value: 1)
private var value = 0
func increment() {
value += 1 // Bug: no gate.wait()
}
func read() -> Int {
gate.wait()
defer { gate.signal() }
return value
}
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Foundation
print("▶ Start at \(Date.now)\n")
let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
counter.increment()
}
print("Counted \(counter.read()) of 1000000 increments")
// ▷ Try: add gate.wait() and defer { gate.signal() } to increment().
// The count becomes exact, but only because you remembered.Виправлено
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
import Synchronization
final class Counter: Sendable {
private let value = Mutex(0)
func increment() {
value.withLock { $0 += 1 }
}
func read() -> Int {
value.withLock { $0 }
}
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Foundation
print("▶ Start at \(Date.now)\n")
let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
counter.increment()
}
print("Counted \(counter.read()) of 1000000 increments")
// ▷ Try: write value += 1 without withLock. The compiler rejects it,
// so the lock cannot be forgotten.Що змінилося
-import Dispatch
+import Synchronization
-final class Counter: @unchecked Sendable {
- private let gate = DispatchSemaphore(value: 1)
- private var value = 0
+final class Counter: Sendable {
+ private let value = Mutex(0)
func increment() {
- value += 1 // Bug: no gate.wait()
+ value.withLock { $0 += 1 }
}
func read() -> Int {
- gate.wait()
- defer { gate.signal() }
- return value
+ value.withLock { $0 }
}
}
Саму інверсію пріоритетів пісочниця не покаже. Ми перевірили: на її Linux-сервері High чекав однаково довго і з семафором, і з Mutex. Тому інверсію пояснюють діаграми вище, а цей приклад показує іншу ціну семафора як lock: компілятор не стежить, щоб кожен доступ до стану проходив через wait().
Межі критичної секції
Mutex блокує потік при конкуренції за lock і не є рекурсивним. Усередині withLock залишаємо коротку операцію над станом. Не виконуємо мережевий запит, дисковий I/O, очікування іншої задачі чи невідомий callback. Повторний виклик withLock для того самого mutex із його критичної секції є помилкою: поведінка залежить від платформи, і деякі платформи можуть аварійно завершити процес або потрапити в deadlock.
Замикання withLock синхронне: await усередині нього не підходить. Отримайте потрібний знімок стану, відпустіть lock і лише потім виконуйте асинхронну роботу. Якщо після await потрібно застосувати результат, стан міг уже змінитися, тому перевірте відповідні умови ще раз.
Семафор усередині Task створює ще одну пастку
Є спокуса обгорнути async-операцію в Task, а її результат синхронно дочекатися через семафор. Якщо таке очікування виконується на потоці cooperative pool, воно займає цей потік, хоча робота, яка має викликати signal(), може потребувати потоку з того самого pool. За відповідного навантаження це може створити deadlock.
Зліва цей антипатерн. Програма запускає стільки задач, скільки ядер бачить, і кожна синхронно чекає на внутрішню Task через семафор. Вільного потоку для внутрішніх задач не лишається, тож програма зависає, доки пісочниця не зупинить її через 10 секунд. Справа currentValue() стає async і чекає через await: задача призупиняється й звільняє потік.
З багом
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
import Dispatch
func loadValue() async -> Int { 42 }
final class Box: @unchecked Sendable {
var value = 0
}
// Waits for async work on a blocked thread.
func currentValue() -> Int {
let done = DispatchSemaphore(value: 0)
let box = Box()
Task {
box.value = await loadValue()
done.signal()
}
done.wait() // Bug: blocks a pool thread
return box.value
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Foundation
print("▶ Start at \(Date.now)\n")
let workers = ProcessInfo.processInfo.activeProcessorCount
print("Starting \(workers) tasks, one per core")
let total = await withTaskGroup(of: Int.self) { group in
for _ in 0..<workers {
group.addTask { currentValue() }
}
var sum = 0
for await value in group { sum += value }
return sum
}
print("All tasks finished, sum: \(total)")
// ▷ Try: start workers - 1 tasks. A free pool thread runs the inner
// Task and the program finishes, which is how this bug hides in tests.Виправлено
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
func loadValue() async -> Int { 42 }
// Suspends the task and frees its thread.
func currentValue() async -> Int {
await loadValue()
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Foundation
print("▶ Start at \(Date.now)\n")
let workers = ProcessInfo.processInfo.activeProcessorCount
print("Starting \(workers) tasks, one per core")
let total = await withTaskGroup(of: Int.self) { group in
for _ in 0..<workers {
group.addTask { await currentValue() }
}
var sum = 0
for await value in group { sum += value }
return sum
}
print("All tasks finished, sum: \(total)")
// ▷ Try: start 100 tasks instead of workers. They suspend rather than
// block, so the program still finishes.Що змінилося
-import Dispatch
-
func loadValue() async -> Int { 42 }
-final class Box: @unchecked Sendable {
- var value = 0
-}
-
-// Waits for async work on a blocked thread.
-func currentValue() -> Int {
- let done = DispatchSemaphore(value: 0)
- let box = Box()
- Task {
- box.value = await loadValue()
- done.signal()
- }
- done.wait() // Bug: blocks a pool thread
- return box.value
+// Suspends the task and frees its thread.
+func currentValue() async -> Int {
+ await loadValue()
}
Apple пояснює контракт forward progress і небезпеку прихованих залежностей у Swift concurrency: Behind the scenes. Для очікування async-результату використовуйте await; для адаптації callback API підійде правильно реалізована continuation з одноразовим завершенням.
Коли стан належить асинхронному компоненту, розгляньте actor. Очікування доступу до actor може призупинити задачу й звільнити потік для іншої роботи. Водночас, коли метод actor призупиняється на await, actor може тим часом виконати інші виклики, тому припущення про незмінність стану через suspension point потрібно перевіряти. Цю модель описано в The Swift Programming Language.
Що перевіряє приклад
Архів із повними Swift-файлами містить обидві реалізації та програму перевірки. Вона додає 10 000 унікальних значень із конкурентних викликів, читає знімки під час запису та перевіряє фінальний набір значень.
Програму перевірки можна запустити просто тут. Вона виконується в онлайн-пісочниці з Linux-тулчейном, тож перевіряє ту саму цілісність даних, а не планувальник Darwin.
// ════════════════════════════════════════════════════════════════
// ▶ ARTICLE EXAMPLE: the code discussed in the article
// ════════════════════════════════════════════════════════════════
import Dispatch
// Adds unique values from concurrent workers, reads snapshots while they
// are still writing, and checks that both resources kept every value.
func verifyResources(iterations: Int = 10_000) {
let semaphoreResource = SemaphoreResource()
let mutexResource = MutexResource()
DispatchQueue.concurrentPerform(iterations: iterations) { index in
let value = String(index)
semaphoreResource.append(value)
mutexResource.append(value)
// Exercise reads while other workers are still writing.
if index.isMultiple(of: 997) {
precondition(semaphoreResource.snapshot().count <= iterations)
precondition(mutexResource.snapshot().count <= iterations)
}
}
let expected = Set((0..<iterations).map(String.init))
let semaphoreValues = semaphoreResource.snapshot()
let mutexValues = mutexResource.snapshot()
precondition(semaphoreValues.count == iterations)
precondition(mutexValues.count == iterations)
precondition(Set(semaphoreValues) == expected)
precondition(Set(mutexValues) == expected)
print("Both resources retained all \(iterations) unique values.")
print("This checks data integrity, not scheduling or priority inheritance.")
}
// ────────────────────────────────────────────────────────────────
// ◇ DEMO HARNESS: not part of the example. It only runs the code
// above and prints the result. Change it freely in the editor.
// ────────────────────────────────────────────────────────────────
import Foundation
print("▶ Start at \(Date.now)\n")
verifyResources()
// ▷ Try: call verifyResources(iterations: 100_000). It still passes: both
// classes guard every access, so this checks integrity, not scheduling.Щоб зібрати її локально:
swiftc -swift-version 6 -strict-concurrency=complete -warnings-as-errors \
Resources.swift main.swift -o resource-check
./resource-check▶ Start at 2026-11-09 11:43:06 +0000
Both resources retained all 10000 unique values.
This checks data integrity, not scheduling or priority inheritance.Приклад скомпільовано Apple Swift 6.4.2 у режимі Swift 6; перевірку виконано на macOS. Окремо виконано typecheck для iOS 18. Це перевірка коректності доступу до даних, не бенчмарк і не доказ політики планувальника. Випадковий порядок print() або штучний sleep() теж не довів би priority inheritance.
Для проблеми в застосунку почніть із Thread Performance Checker: знайдіть, хто чекає, на що саме і хто має забезпечити прогрес. Потім перевіряйте тривалість критичної секції та пріоритети роботи. Сам по собі QoS-клас .background не пояснює залежність.
Як обрати примітив
| Потреба | З чого почати |
|---|---|
| Коротко синхронно змінити спільний стан | Mutex, якщо дозволяє deployment target |
| Зберігати стан асинхронного компонента | actor, з урахуванням reentrancy |
| Дочекатися завершення async-операції | await, без блокування потоку семафором |
| Рахувати доступні дозволи, які може повертати інший учасник | Семафор може відповідати моделі; окремо оцініть блокування та залежності |
Для взаємного виключення важлива інформація про власника ресурсу. Вона дозволяє системі допомогти потоку, від якого залежить термінова робота. У Swift Mutex додає ще й корисну межу в коді: стан лежить усередині захисного примітива, а доступ до нього проходить через withLock.
Priority inheritance не скорочує довгу критичну секцію і не виправляє помилкові залежності. Обирайте відповідний примітив, залишайте під lock лише коротку роботу й дозволяйте async-коду чекати без зайнятого потоку. Саме ці рішення визначають, скільки насправді чекатиме High.
Додаткові ресурси
- NASA: Mars Pathfinder, огляд місії
- NASA NSSDCA: історичні знімки Mars Pathfinder
- Glenn Reeves: What really happened on Mars?, 15 грудня 1997
- Apple: Tune CPU job scheduling for Apple silicon games, пояснення owner-aware primitives
- Apple: Diagnosing performance issues early, Thread Performance Checker
- Apple: os_unfair_lock_lock, власник і обмеження fairness
- Swift Evolution SE-0433: Synchronous Mutual Exclusion Lock
- Swift 6.0: Mutex, вихідний код
- Swift 6.0: DarwinImpl.swift
- Apple WWDC21: Swift concurrency: Behind the scenes
- The Swift Programming Language: Concurrency
- Приклади коду в онлайн-редакторі:
SemaphoreResource.swift,MutexResource.swift,CounterSemaphore.swift,CounterMutex.swift,TaskSemaphore.swift,TaskAwait.swift,main.swift - PlantUML: синтаксис timing diagrams
- Вихідні діаграми: інверсія, успадкування, залежність від власника.
Обговорення
Переклад коментарів
Мову не знайдено.