Конкурентность
Конкурентность возникает всякий раз, когда разные части программы могут выполняться в разное время или не по порядку. Во встраиваемом контексте сюда относятся:
- обработчики прерываний, которые запускаются каждый раз, когда происходит связанное с ними прерывание;
- различные формы многопоточности, когда ваш микропроцессор регулярно переключается между частями программы;
- а в некоторых системах — многоядерные микропроцессоры, где каждое ядро может независимо выполнять другую часть программы одновременно.
Поскольку многим встраиваемым программам приходится работать с прерываниями, конкурентность так или иначе обычно возникает, и именно здесь могут появляться тонкие и трудные для поиска ошибки. К счастью, Rust предоставляет ряд абстракций и гарантий безопасности, которые помогают писать корректный код.
Без конкурентности
Самая простая конкурентность для встраиваемой программы — её отсутствие: ваше ПО состоит из одного главного цикла, который просто продолжает работать, и никаких прерываний вообще нет. Иногда это прекрасно подходит для решаемой задачи! Обычно ваш цикл считывает входные данные, выполняет обработку и записывает выходные данные.
#[entry]
fn main() {
let peripherals = setup_peripherals();
loop {
let inputs = read_inputs(&peripherals);
let outputs = process(inputs);
write_outputs(&peripherals, outputs);
}
}
Поскольку конкурентности нет, не нужно беспокоиться о совместном использовании данных между частями программы или о синхронизации доступа к периферии. Если вы можете обойтись таким простым подходом, это может стать отличным решением.
Глобальные изменяемые данные
В отличие от не встраиваемого Rust, обычно у нас нет роскоши выделять память в куче и передавать ссылки на эти данные во вновь созданный поток. Вместо этого наши обработчики прерываний могут быть вызваны в любой момент и должны знать, как обращаться к любой общей памяти, которую мы используем. На самом низком уровне это означает, что нам нужна статически выделенная изменяемая память, на которую могут ссылаться и обработчик прерывания, и основной код.
В Rust такие переменные static mut всегда небезопасно читать или записывать, потому что без особых мер предосторожности вы можете спровоцировать состояние гонки, при котором ваш доступ к переменной прерывается на полпути другим прерыванием, которое также обращается к этой переменной.
Чтобы увидеть, как такое поведение может привести к тонким ошибкам в коде, рассмотрим встраиваемую программу, которая считает фронты входного сигнала в каждом односекундном периоде (частотомер):
static mut COUNTER: u32 = 0;
#[entry]
fn main() -> ! {
set_timer_1hz();
let mut last_state = false;
loop {
let state = read_signal_level();
if state && !last_state {
// ОПАСНО — на самом деле это небезопасно! Может вызвать гонки данных.
unsafe { COUNTER += 1 };
}
last_state = state;
}
}
#[interrupt]
fn timer() {
unsafe { COUNTER = 0; }
}
Каждую секунду прерывание таймера сбрасывает счётчик в 0. Тем временем основной цикл непрерывно измеряет сигнал и увеличивает счётчик, когда видит переход от низкого уровня к высокому. Нам пришлось использовать unsafe, чтобы обратиться к COUNTER, так как это static mut, а значит, мы обещаем компилятору, что не вызовем неопределённого поведения. Можете ли вы заметить состояние гонки? Инкремент COUNTER не гарантированно атомарен: на большинстве встраиваемых платформ он будет разбит на загрузку, затем инкремент, затем сохранение. Если прерывание сработает после загрузки, но до сохранения, сброс в 0 будет проигнорирован после выхода из прерывания — и за этот период мы посчитаем вдвое больше переходов.
Критические секции
Так что же мы можем сделать с гонками данных? Простой подход — использовать критические секции, контекст, в котором прерывания запрещены. Оборачивая доступ к COUNTER в main в критическую секцию, мы можем быть уверены, что прерывание таймера не сработает, пока мы не закончим инкремент COUNTER:
static mut COUNTER: u32 = 0;
#[entry]
fn main() -> ! {
set_timer_1hz();
let mut last_state = false;
loop {
let state = read_signal_level();
if state && !last_state {
// Новая критическая секция обеспечивает синхронизированный доступ к COUNTER
cortex_m::interrupt::free(|_| {
unsafe { COUNTER += 1 };
});
}
last_state = state;
}
}
#[interrupt]
fn timer() {
unsafe { COUNTER = 0; }
}
В этом примере мы используем cortex_m::interrupt::free, но на других платформах будут похожие механизмы для выполнения кода в критической секции. Это то же самое, что запретить прерывания, выполнить некоторый код, а затем снова разрешить прерывания.
Обратите внимание, что нам не нужно было помещать критическую секцию внутрь обработчика прерывания, по двум причинам:
- запись 0 в
COUNTERне может пострадать от гонки, так как мы не читаем его; - обработчик и так никогда не будет прерван потоком
main.
Если бы COUNTER разделялся между несколькими обработчиками прерываний, которые могут вытеснять друг друга, то каждому из них тоже могла бы понадобиться критическая секция.
Это решает нашу непосредственную проблему, но мы всё ещё пишем много небезопасного кода, который нужно тщательно обосновывать, и можем использовать критические секции без необходимости. Поскольку каждая критическая секция временно приостанавливает обработку прерываний, есть связанная с этим цена: немного больший размер кода, большая задержка прерываний и джиттер (прерывания могут обрабатываться дольше, а время до их обработки будет более изменчивым). Является ли это проблемой, зависит от вашей системы, но в общем случае мы хотели бы этого избегать.
Стоит отметить, что хотя критическая секция гарантирует, что прерывания не сработают, она не даёт гарантии эксклюзивности в многоядерных системах! Другое ядро может спокойно обращаться к той же памяти, что и ваше ядро, даже без прерываний. Если вы используете несколько ядер, вам понадобятся более строгие примитивы синхронизации.
Атомарный доступ
На некоторых платформах доступны специальные атомарные инструкции, которые гарантируют атомарность операций чтения-модификации-записи. Для Cortex-M это выглядит так: thumbv6 (Cortex-M0, Cortex-M0+) предоставляет только атомарные инструкции загрузки и сохранения, тогда как thumbv7 (Cortex-M3 и выше) предоставляет полные инструкции сравнения и обмена (Compare and Swap, CAS). Эти инструкции CAS дают альтернативу грубому запрету всех прерываний: мы можем попытаться выполнить инкремент, он в большинстве случаев будет успешным, а если его прервали, он автоматически повторит всю операцию инкремента. Эти атомарные операции безопасны даже при нескольких ядрах.
use core::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
#[entry]
fn main() -> ! {
set_timer_1hz();
let mut last_state = false;
loop {
let state = read_signal_level();
if state && !last_state {
// Используем `fetch_add`, чтобы атомарно прибавить 1 к COUNTER
COUNTER.fetch_add(1, Ordering::Relaxed);
}
last_state = state;
}
}
#[interrupt]
fn timer() {
// Используем `store`, чтобы записать 0 непосредственно в COUNTER
COUNTER.store(0, Ordering::Relaxed)
}
На этот раз COUNTER — безопасная переменная static. Благодаря типу AtomicUsize COUNTER можно безопасно изменять и из обработчика прерывания, и из основного потока без запрета прерываний. Когда это возможно, это лучшее решение, но оно может не поддерживаться на вашей платформе.
Замечание о Ordering: этот параметр влияет на то, как компилятор и аппаратура могут переупорядочивать инструкции, а также на видимость в кэше. Если предположить, что целевая платформа однопроцессорная, Relaxed достаточно и является наиболее эффективным выбором в данном конкретном случае. Более строгий порядок заставит компилятор вставлять барьеры памяти вокруг атомарных операций; в зависимости от того, для чего вы используете атомарные операции, это может быть нужно или нет! Точные детали модели атомарности сложны и лучше описаны в другом месте.
Подробнее про атомарные операции и порядок см. в nomicon.
Абстракции, Send и Sync
Ни одно из описанных выше решений не является особенно удовлетворительным. Они требуют блоков unsafe, которые нужно очень тщательно проверять, и они неудобны в использовании. Конечно, в Rust мы можем сделать лучше!
Мы можем обернуть наш счётчик в безопасный интерфейс, который можно безопасно использовать в любом другом месте нашего кода. В этом примере мы используем счётчик на критической секции, но нечто очень похожее можно сделать и с атомарными операциями.
use core::cell::UnsafeCell;
use cortex_m::interrupt;
// Наш счётчик — просто обёртка вокруг UnsafeCell<u32>, которая лежит в основе внутренней
// изменяемости (interior mutability) в Rust. Используя внутреннюю изменяемость, мы можем
// сделать COUNTER `static`, а не `static mut`, и при этом по-прежнему менять его значение.
struct CSCounter(UnsafeCell<u32>);
const CS_COUNTER_INIT: CSCounter = CSCounter(UnsafeCell::new(0));
impl CSCounter {
pub fn reset(&self, _cs: &interrupt::CriticalSection) {
// Требуя передачи CriticalSection, мы знаем, что работаем внутри критической секции,
// и потому можем уверенно использовать этот блок unsafe (он нужен для вызова UnsafeCell::get).
unsafe { *self.0.get() = 0 };
}
pub fn increment(&self, _cs: &interrupt::CriticalSection) {
unsafe { *self.0.get() += 1 };
}
}
// Необходимо, чтобы разрешить static CSCounter. См. объяснение ниже.
unsafe impl Sync for CSCounter {}
// COUNTER больше не `mut`, так как использует внутреннюю изменяемость;
// поэтому для доступа к нему больше не нужны блоки unsafe.
static COUNTER: CSCounter = CS_COUNTER_INIT;
#[entry]
fn main() -> ! {
set_timer_1hz();
let mut last_state = false;
loop {
let state = read_signal_level();
if state && !last_state {
// Тут без unsafe!
interrupt::free(|cs| COUNTER.increment(cs));
}
last_state = state;
}
}
#[interrupt]
fn timer() {
// Нам всё же нужно войти в критическую секцию здесь, чтобы получить корректный токен cs,
// даже несмотря на то, что мы знаем: никакое другое прерывание не может вытеснить это.
interrupt::free(|cs| COUNTER.reset(cs));
// Если бы мы действительно хотели, могли бы использовать unsafe-код, чтобы создать
// поддельный CriticalSection и избежать накладных расходов:
// let cs = unsafe { interrupt::CriticalSection::new() };
}
Мы перенесли наш код unsafe внутрь тщательно спланированной абстракции, и теперь код приложения не содержит никаких блоков unsafe.
Этот дизайн требует, чтобы приложение передавало токен CriticalSection: такие токены безопасно создаёт только interrupt::free, поэтому, требуя передать один, мы гарантируем, что работаем внутри критической секции, не выполняя блокировку самостоятельно. Эту гарантию компилятор предоставляет статически: с cs не связано никаких накладных расходов во время выполнения. Если бы у нас было несколько счётчиков, им всем можно было бы передать один и тот же cs, не требуя нескольких вложенных критических секций.
Это также подводит нас к важной теме конкурентности в Rust: трейтам Send и Sync. Если подытожить книгу по Rust, тип является Send, когда его можно безопасно переместить в другой поток, а Sync — когда им можно безопасно делиться между несколькими потоками. Во встраиваемом контексте мы считаем, что прерывания выполняются в отдельном потоке от кода приложения, поэтому переменные, к которым обращаются и прерывание, и основной код, должны быть Sync.
Для большинства типов в Rust оба этих трейта автоматически выводятся компилятором. Однако поскольку CSCounter содержит UnsafeCell, он не является Sync, и поэтому мы не могли бы создать static CSCounter: переменные static обязаны быть Sync, так как к ним могут обращаться несколько потоков.
Чтобы сообщить компилятору, что мы позаботились о том, что CSCounter действительно безопасно разделять между потоками, мы явно реализуем трейт Sync. Как и в случае предыдущего использования критических секций, это безопасно только на однопроцессорных платформах: при нескольких ядрах потребовались бы дополнительные меры, чтобы обеспечить безопасность.
Мьютексы
Мы создали полезную абстракцию, специфичную для нашей задачи со счётчиком, но для конкурентности существует много распространённых абстракций.
Один из таких примитивов синхронизации — мьютекс, сокращение от mutual exclusion (взаимное исключение). Эти конструкции обеспечивают эксклюзивный доступ к переменной, например к нашему счётчику. Поток может попытаться заблокировать (или захватить) мьютекс, и либо сразу успешно захватывает его, либо блокируется в ожидании захвата, либо получает ошибку о том, что мьютекс заблокировать не удалось. Пока поток удерживает блокировку, ему предоставляется доступ к защищённым данным. Когда поток закончил работу, он разблокирует (или освобождает) мьютекс, позволяя другому потоку его заблокировать. В Rust разблокировку обычно реализуют с помощью трейта Drop, чтобы гарантировать, что она всегда выполняется, когда мьютекс выходит из области видимости.
Использовать мьютекс с обработчиками прерываний может быть непросто: обычно обработчику прерывания нельзя блокироваться, и было бы особенно катастрофично, если бы он заблокировался в ожидании, пока основной поток освободит блокировку, так как тогда возник бы дедлок (основной поток никогда не освободит блокировку, потому что выполнение остаётся в обработчике прерывания). Дедлок не считается небезопасным: он возможен даже в безопасном Rust.
Чтобы полностью избежать такого поведения, мы могли бы реализовать мьютекс, для блокировки которого требуется критическая секция, точно так же, как в примере со счётчиком. Пока критическая секция длится столько же, сколько блокировка, мы можем быть уверены в эксклюзивном доступе к обёрнутой переменной, даже без отслеживания состояния блокировки и разблокировки мьютекса.
На самом деле это уже сделано за нас в крейте cortex_m! Мы могли бы написать наш счётчик с его помощью:
use core::cell::Cell;
use cortex_m::interrupt::Mutex;
static COUNTER: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
#[entry]
fn main() -> ! {
set_timer_1hz();
let mut last_state = false;
loop {
let state = read_signal_level();
if state && !last_state {
interrupt::free(|cs|
COUNTER.borrow(cs).set(COUNTER.borrow(cs).get() + 1));
}
last_state = state;
}
}
#[interrupt]
fn timer() {
// Нам всё равно нужно войти в критическую секцию здесь, чтобы удовлетворить Mutex.
interrupt::free(|cs| COUNTER.borrow(cs).set(0));
}
Теперь мы используем Cell, который вместе со своим собратом RefCell служит для безопасной внутренней изменяемости. Мы уже видели UnsafeCell — нижний слой внутренней изменяемости в Rust: он позволяет получить несколько изменяемых ссылок на своё значение, но только с помощью небезопасного кода. Cell похож на UnsafeCell, но предоставляет безопасный интерфейс: он позволяет только получить копию текущего значения или заменить его, но не получить ссылку, а поскольку он не является Sync, его нельзя разделять между потоками. Эти ограничения делают его безопасным в использовании, но мы не могли бы использовать его напрямую в переменной static, так как static должен быть Sync.
Так почему же приведённый выше пример работает? Mutex<T> реализует Sync для любого T, который является Send, — например, для Cell. Он может делать это безопасно, потому что предоставляет доступ к своему содержимому только внутри критической секции. Поэтому мы получаем безопасный счётчик вообще без какого-либо небезопасного кода!
Это отлично подходит для простых типов, таких как u32 нашего счётчика, но как насчёт более сложных типов, которые не являются Copy? Чрезвычайно распространённый пример во встраиваемом контексте — структура периферии, которая обычно не является Copy. Для этого мы можем обратиться к RefCell.
Совместное использование периферии
Крейты устройств, сгенерированные с помощью svd2rust и подобных инструментов, обеспечивают безопасный доступ к периферии, гарантируя, что одновременно может существовать только один экземпляр структуры периферии. Это обеспечивает безопасность, но затрудняет доступ к периферии одновременно из основного потока и из обработчика прерывания.
Чтобы безопасно разделить доступ к периферии, мы можем использовать Mutex, который мы видели раньше. Нам также понадобится RefCell, который использует проверку во время выполнения, чтобы выдавать одну ссылку на периферию за раз. Это создаёт больше накладных расходов, чем простой Cell, но поскольку мы выдаём ссылки, а не копии, мы должны быть уверены, что одновременно существует только одна из них.
Наконец, нам также нужно как-то учесть перемещение периферии в разделяемую переменную после её инициализации в основном коде. Для этого мы можем использовать тип Option, инициализированный значением None и позже установленный в экземпляр периферии.
use core::cell::RefCell;
use cortex_m::interrupt::{self, Mutex};
use stm32f4::stm32f405;
static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> =
Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
// Получаем синглтоны периферии и настраиваем её.
// Этот пример взят из крейта, сгенерированного svd2rust, но
// большинство крейтов устройств будут похожими.
let dp = stm32f405::Peripherals::take().unwrap();
let gpioa = &dp.GPIOA;
// Какая-то функция конфигурации.
// Предположим, что она настраивает PA0 как вход, а PA1 как выход.
configure_gpio(gpioa);
// Помещаем GPIOA в мьютекс, перемещая его.
interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
// Теперь мы не можем использовать `gpioa` или `dp.GPIOA`, и вместо этого должны
// обращаться к нему через мьютекс.
// Обязательно включайте прерывание только после установки MY_GPIO:
// иначе прерывание может сработать, пока там всё ещё None,
// и в написанном виде (с `unwrap()`) это вызовет панику.
set_timer_1hz();
let mut last_state = false;
loop {
// Теперь читаем состояние как цифровой вход через мьютекс
let state = interrupt::free(|cs| {
let gpioa = MY_GPIO.borrow(cs).borrow();
gpioa.as_ref().unwrap().idr.read().idr0().bit_is_set()
});
if state && !last_state {
// Устанавливаем PA1 в высокий уровень, если мы увидели восходящий фронт на PA0.
interrupt::free(|cs| {
let gpioa = MY_GPIO.borrow(cs).borrow();
gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
});
}
last_state = state;
}
}
#[interrupt]
fn timer() {
// В этот раз в прерывании мы просто сбросим PA0.
interrupt::free(|cs| {
// Здесь можно использовать `unwrap()`, потому что мы знаем, что прерывание не было включено
// до того, как был установлен MY_GPIO; иначе нам нужно было бы обработать возможность значения None.
let gpioa = MY_GPIO.borrow(cs).borrow();
gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().clear_bit());
});
}
Это довольно много для восприятия, поэтому разберём важные строки.
static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> =
Mutex::new(RefCell::new(None));
Наша разделяемая переменная теперь — Mutex вокруг RefCell, который содержит Option. Mutex гарантирует, что доступ есть только во время критической секции, и поэтому делает переменную Sync, хотя простой RefCell не был бы Sync. RefCell даёт нам внутреннюю изменяемость через ссылки, которая понадобится для работы с нашим GPIOA. Option позволяет инициализировать эту переменную пустым значением и только позже действительно переместить в неё нужное значение. Мы не можем обращаться к синглтону периферии статически, только во время выполнения, поэтому это необходимо.
interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
Внутри критической секции мы можем вызвать borrow() у мьютекса, который даёт нам ссылку на RefCell. Затем мы вызываем replace(), чтобы переместить наше новое значение в RefCell.
interrupt::free(|cs| {
let gpioa = MY_GPIO.borrow(cs).borrow();
gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
});
Наконец, мы используем MY_GPIO безопасным и параллельным образом. Критическая секция не даёт прерыванию сработать, как обычно, и позволяет заимствовать мьютекс. RefCell затем даёт нам &Option<GPIOA> и отслеживает, как долго он остаётся заимствованным: как только эта ссылка выходит из области видимости, RefCell обновляется, показывая, что он больше не заимствован.
Поскольку мы не можем вынуть GPIOA из &Option, нам нужно преобразовать его в &Option<&GPIOA> с помощью as_ref(), а затем вызвать unwrap(), чтобы получить &GPIOA, который позволяет изменять периферию.
Если нам нужна изменяемая ссылка на разделяемый ресурс, вместо этого следует использовать borrow_mut и deref_mut. Следующий код показывает пример с таймером TIM2.
use core::cell::RefCell;
use core::ops::DerefMut;
use cortex_m::interrupt::{self, Mutex};
use cortex_m::asm::wfi;
use stm32f4::stm32f405;
static G_TIM: Mutex<RefCell<Option<Timer<stm32::TIM2>>>> =
Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
let mut cp = cm::Peripherals::take().unwrap();
let dp = stm32f405::Peripherals::take().unwrap();
// Какая-то функция настройки таймера.
// Предположим, что она настраивает таймер TIM2, его прерывание NVIC,
// и наконец запускает таймер.
let tim = configure_timer_interrupt(&mut cp, dp);
interrupt::free(|cs| {
G_TIM.borrow(cs).replace(Some(tim));
});
loop {
wfi();
}
}
#[interrupt]
fn timer() {
interrupt::free(|cs| {
if let Some(ref mut tim)) = G_TIM.borrow(cs).borrow_mut().deref_mut() {
tim.start(1.hz());
}
});
}
Уф! Это безопасно, но немного громоздко. Есть ли что-то ещё, что мы можем сделать?
RTIC
Одна из альтернатив — фреймворк RTIC, сокращение от Real Time Interrupt-driven Concurrency (конкурентность реального времени, управляемая прерываниями). Он обеспечивает статические приоритеты и отслеживает доступ к переменным static mut («ресурсам»), чтобы статически гарантировать, что разделяемые ресурсы всегда используются безопасно, без накладных расходов на постоянный вход в критические секции и подсчёт ссылок (как в RefCell). Это даёт ряд преимуществ, например гарантию отсутствия дедлоков и чрезвычайно малые накладные расходы по времени и памяти.
RTIC поставляется с асинхронным исполнителем, поэтому ваши программные задачи — это функции async, в которых можно использовать асинхронные API наряду с обычными синхронными API.
Фреймворк также включает другие возможности, например передачу сообщений, которая уменьшает потребность в явном разделяемом состоянии, и возможность планировать запуск задач на заданное время, что можно использовать для реализации периодических задач. Подробнее смотрите в документации!
Embassy
Embassy — это экосистема библиотек, которая фокусируется на использовании синтаксиса async / await, встроенного в Rust, для конкурентности. Ядро embassy — её асинхронный исполнитель, который поддерживает большинство распространённых архитектур MCU.
Embassy также придерживается подхода «всё в комплекте» и предлагает множество других компонентов, например:
- Библиотека времени
- Различные библиотеки HAL, которые также обеспечивают поддержку библиотеки времени.
- embassy-sync для примитивов синхронизации
Подробнее см. на сайте и в книге.
Операционные системы реального времени
Другая распространённая модель конкурентности во встраиваемых системах — операционная система реального времени (RTOS). Хотя в Rust их изучено пока меньше, в традиционной встраиваемой разработке они широко используются. Примеры с открытым исходным кодом — FreeRTOS и ChibiOS. Эти RTOS обеспечивают поддержку запуска нескольких потоков приложения, между которыми переключается ЦПУ: либо когда потоки добровольно передают управление (это называется кооперативной многозадачностью), либо на основе регулярного таймера или прерываний (вытесняющая многозадачность). RTOS обычно предоставляют мьютексы и другие примитивы синхронизации и часто взаимодействуют с аппаратными возможностями, такими как движки DMA.
На момент написания мало примеров RTOS на Rust, на которые можно было бы указать, но это интересная область, так что следите за обновлениями!
Несколько ядер
Всё чаще во встраиваемых процессорах бывают два или более ядер, что добавляет ещё один уровень сложности в конкурентность. Все примеры с критической секцией (включая cortex_m::interrupt::Mutex) предполагают, что единственный другой поток выполнения — это поток прерываний, но в многоядерной системе это уже не так. Вместо этого нам понадобятся примитивы синхронизации, разработанные для нескольких ядер (также называемые SMP, от symmetric multi-processing — симметричная многопроцессорная обработка).
Обычно они используют атомарные инструкции, которые мы видели ранее, так как система обработки гарантирует сохранение атомарности на всех ядрах.
Подробно рассматривать эти темы в настоящее время выходит за рамки этой книги, но общие шаблоны такие же, как и для однопроцессорного случая.