Отношения родитель/потомок в Rust без загрязнения на всю жизнь

У меня есть достаточно сложное приложение, которое разделено на небольшое количество достаточно сложных, сильно взаимозависимых структур. Сейчас у нас просто Arc и Weak разбросаны довольно хаотично. Я бы хотел удалить их и предположить, что есть лучший способ выполнить то, что я пытаюсь сделать, но пока не нашел хорошего решения.

Все рассматриваемые подразделы имеют возможность изменения внутренней части, поэтому не нужно беспокоиться о mut здесь. Однако каждая из частей взаимодействует друг с другом через центральный App, экземпляр которого создается при запуске и уничтожается при завершении работы. Единственное исключение из этого правила — интеграционные тесты, где одновременно может существовать несколько App и быть уничтожены до завершения среды выполнения.

Изначально это был просто гигантский статический/глобальный синглтон, который упрощал задачу, но не подходил для тестов.

Текущая структура выглядит (примерно) следующим образом:

struct App {
  things: ThingManager,
  stuff: StuffManager,
  actions: ActionManager,
}
impl App {
  fn new() -> Arc<App> {
    // App instantiates each of the Thing/Stuff/Actions, and gives them a weak reference to itself
    let app = Arc::new(App { ThingManager::new(), StuffManager::new, ActionManager::new() })
    app.things.set_app(&app)
    app.stuff.set_app(&app)
    app.actions.set_app(&app)
  }
}

// Each of the Manager's look like:
struct ThingManager {
  app: OnceCell<Weak<App>>,
  // Other fields
}
impl ThingManager {
  fn set_app(&self, app: &Arc<App>) {
    self.app.set(Arc::downgrade(app))
  }
  fn get_app(&self) -> Arc<App> {
    self.app.get().upgrade()
  }

  // And unfortunately touch other parts of the app
  fn complex_logic(&self) {
    let app = self.get_app();
    let some_stuff = app.stuff.find_specific_stuff();
    let action = app.action.do_something(some_stuff);
    // etc
  }
}

Я рассматривал возможность использования таких вещей, как распределители арен (bumpalo, Generational-arena), но каждый раз, когда я начинаю реализацию, я в конечном итоге загрязняю свои структуры сроками жизни <'app>, и они очень быстро распространяются по всему коду (многие функции занимают App , и необходимо указать время жизни и т. д.).

Я знаю, что мой ThingManager будет жить ровно столько же, сколько и мое приложение, и когда приложение будет удалено, я также хотел бы удалить менеджеры. Как мне это четко представить? Я не в восторге от использования небезопасных блоков, но если бы они были ограничены по объему (ручная установка/удаление) и были «безопасными», при условии, что мои ограничения сохраняются (менеджеры не переживут приложение и т. д.), я бы не стал быть слишком против...

Что именно не так с кодом, который вы показываете? Хотя на самом деле это не Rust-y, мне кажется, что он работает нормально и работает.

Finomnis 07.06.2024 15:32

При профилировании огромное количество времени тратится на атомарные операции, начиная с (я полагаю) обновления/удаления Arc. Учитывая степень взаимозависимости между частями, вызов чего-либо на ThingManager делает get_app() с ним что-то маленькое, вызывает StuffManager, который делает свое собственное get_app(), и связывает большое количество раз (а затем отбрасывает большое количество Arc<App> в конце цепь). Теоретически у нас может быть просто &App или (что-то вроде) один Arc<App> в каждом Manager, чтобы нам не приходилось обновлять/удалять каждый вызов функции...

theflash 07.06.2024 15:36

Ах я вижу. Я чувствую, что ту архитектуру, которую вы здесь пытаетесь создать, действительно трудно реализовать на Rust. Я не уверен, что ваш способ разделения является самым разумным, честно говоря, для меня слова «подразделенный» и «сильно взаимозависимый» противоречат друг другу. Если вы что-то разделите, оно не должно быть сильно взаимозависимым, потому что в конечном итоге вы ничего не получите. Похоже, вы структурировали код, но если он действительно настолько взаимозависим, вы не можете обновить одну часть, не затрагивая другую, поэтому вы также можете объединить их.

Finomnis 07.06.2024 15:44

Я конечно понимаю, что ваш пример сильно минимизирован, поэтому не уверен, насколько ценен этот совет для вашего реального проекта.

Finomnis 07.06.2024 15:45

Другое решение, также часто используемое в C++, — это так называемое внедрение зависимостей. Хитрость в том, что вы не храните зависимые объекты в классе, а передаете их в функции при вызове функций с использованием интерфейсов. Это не только решает эту проблему, но и делает интерфейсы между вашими частями макетируемыми, поэтому их можно тестировать самостоятельно. Это очень важно; модуль должен быть тестируемым сам по себе, иначе нет смысла делать его модулем. Недостатком является то, что это немного усложняет API ваших менеджеров.

Finomnis 07.06.2024 15:47

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

Finomnis 07.06.2024 15:48

Да, полностью согласен с противоречием по поводу разделенности/взаимозависимости, это определенно корень проблемы. Раньше я использовал внедрение зависимостей, но не был знаком с этим в Rust. Но вы как бы заметили интересную деталь: мой App, по сути, просто действует как инструмент внедрения зависимостей (хотя и уродливый)...

theflash 07.06.2024 15:52

Некоторая путаница в посте может быть связана с вызовом вещей Manager, они, вероятно, ближе к обычной зависимости. Представьте, что один из них — это DbConnection, другой — Cache, один — WorkerThread и т. д. WorkerThread может нуждаться в DbConnection, DbConnection может кэшировать данные, WorkerThread может также просматривать (тот же) кеш и т. д. Мой непосредственный предполагаю (без данных...), что инструмент глубокой инъекции будет использовать ту же самую Arc структуру под капотом, но я обязательно осмотрюсь и немного протестирую.

theflash 07.06.2024 15:56

Если вы используете многопоточность, вам действительно нужна эта сложность, потому что потоки могут жить дольше, чем main, а ржавчина предотвращает что-либо подобное, чтобы избежать неопределенного поведения (Rust гарантирует отсутствие неопределенного поведения как одну из своих основных целей). Если у вас есть только один поток, который выполняет upgrade, вместо этого вы можете использовать Rc/Weak, что требует гораздо меньше накладных расходов.

Finomnis 07.06.2024 16:06

Да, некоторая сложность определенно связана с многопоточностью (и тем, как изначально туда попали Arc). И полностью понять природу потоков/данных, живущих за пределами основного. Однако в этом конкретном случае я не уверен, как объяснить ржавчине: «Я понимаю последствия, я позабочусь о том, чтобы это просуществовало столько, сколько вам нужно», и принять на себя связанный с этим риск UB. Может быть, я просто смирюсь с этим и разбросаю вокруг несколько небезопасных/необработанных указателей.

theflash 07.06.2024 16:17

Еще одно замечание по поводу упомянутого вами разделения/взаимозависимости: в конце концов, все эти менеджеры в приложении на самом деле являются частью приложения, все они представляют собой один и тот же объект. Разделение проводилось только в целях чистоты кода (общий/похожий код, .get() в базе данных отличался от .get() в кэше, но с тем же именем функции). Не знаю, как это согласовать здесь, но по сути я хочу, чтобы все эти части были небольшой частью более крупного объекта (и, следовательно, времени жизни), я просто не знаю, как это сделать в ржавчине чисто...

theflash 07.06.2024 16:30

(Разделение из-за длины) Обычно я бы сказал: «О, вместо подструктур каждая часть может быть просто признаком приложения (DbTrait, CacheTrait и т. д.), но в CacheTrait есть хорошая инкапсуляция, есть свои собственные поля (карты , списки и т. д.) вместо того, чтобы размещать их все на верхнем уровне в App. Возможно, у App есть Cache (тупая структура) и CacheTrait, который обращается только к этой одной структуре?

theflash 07.06.2024 16:32

«и принять на себя связанный с этим риск UB» — нет. Это сведет на нет всю цель использования Rust. Даже unsafe не предназначен для сценариев «просто сделай это», а вместо этого означает «Я, программист, гарантирую, что соблюдаю правила надежности вручную». «Риск» означает лишь то, что Rust как язык программирования может оказаться неподходящим инструментом для этой работы :/

Finomnis 07.06.2024 16:35

Тем не менее, не всем многопоточным программам нужен Arc. std::thread::scope отлично работает с обычными ссылками без каких-либо накладных расходов. std::thread::scope сообщает компилятору Rust, что данный поток завершится до того, как охватит его область действия (например, main), ослабляя часть защитного поведения компилятора. Боюсь, вам просто нужно соответствующим образом реструктурировать свою программу. Хотя я все еще считаю, что наличие двунаправленных ссылок — это красный флаг, и должны быть лучшие способы структурировать ваш код.

Finomnis 07.06.2024 16:38

Полностью согласен, что нельзя просто разбрасывать небезопасные вещи. Я думаю, что 99,9% кодовой базы — это правильный выбор (из соображений безопасности), однако возникают проблемы, по существу, в верхней/нижней части приложения (запуск/снятие). Вместо «взять на себя риск UB» мне, вероятно, следовало сказать: «Я позабочусь о том, чтобы эти две вещи прожили достаточно долго друг для друга».

theflash 07.06.2024 16:45

К сожалению, я не знаю, как вам помочь, не зная больше о вашем проекте, который выходит за рамки StackOverflow. Может быть, спросите на форуме Rust или в субреддите Rust, чтобы получить дополнительную информацию по конкретному проекту?

Finomnis 07.06.2024 17:13

Я ценю всю помощь и позволяю мне поделиться с вами некоторыми идеями. Я поиграю с несколькими идеями и посмотрю, что у меня получится. Спасибо!

theflash 07.06.2024 17:15

Если несколько App используются только в тестах, возможно, утечка допустима? В этом случае вы можете просто поставить Arc<App> вместо Weak<App>.

Chayim Friedman 09.06.2024 00:06
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
18
76
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не знаю вашего варианта использования в деталях, но похоже, что следующий дизайн может быть разумным:

struct App {
  things: ThingManagerInner,
  stuff: StuffManagerInner,
  actions: ActionManagerInner,
}
impl App {
  fn new() -> App {
    App {
        things: ...,
        stuff: ...,
        actions: ...,
    }
  }
  fn things(&self) -> ThingManager<'_> {
    ThingManager { app: self }
  }
}

// Each of the Manager's look like:
struct ThingManagerInner {
  // Other fields
  other_field: usize,
}
struct ThingManager<'a> {
    app: &'a App,
    // optionally add other references here as necessary if they are conditional based on app, deeply nested, inside a Vec...
}
impl Deref for ThingManager<'_> {
    type Target = ThingManagerInner;
    fn deref(&self) -> Self::Target {
        &self.app.things
    }
}
impl ThingManager<'_> {
  // And unfortunately touch other parts of the app
  fn complex_logic(&self) {
    let some_stuff = self.app.stuff().find_specific_stuff(self.other_field);
    let action = self.app.action().do_something(some_stuff);
    // etc
  }
}

В Rust вы обычно хотите ориентировать построение структуры на данные, а не на поведение. Затем можно построить слои поведения сверху.

Как правило, вам не нужен unsafe ни для чего (кроме FFI), и, как правило, это скорее пустяк, чем что-либо еще, учитывая дополнительные гарантии, которые приходится предоставлять при написании небезопасного кода на Rust по сравнению с C (алиасинг из-за оптимизация noalias, ковариация/инвариантность, контрвариантность...) и тот факт, что всегда есть лучший дизайн, который сохраняет максимальную производительность, оставаясь при этом полной безопасностью.

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

theflash 10.06.2024 17:13

Другие вопросы по теме