Добавить в корзинуПозвонить
Найти в Дзене
Цифровая Переплавка

⚠️ Почему без потоковой безопасности не бывает безопасности памяти: неожиданные выводы на примере Go

Современные разработчики привыкли делить безопасность программ на несколько категорий: безопасность памяти (memory safety) и потоковая безопасность (thread safety). Однако на деле эти понятия куда теснее переплетены, чем кажется. Свежая статья эксперта по языкам программирования Ральфа Юнга заставляет по-новому взглянуть на то, как эти концепции влияют друг на друга, и почему даже популярные и «безопасные» языки, такие как Go, могут скрывать серьёзные риски. Под безопасностью памяти обычно понимается отсутствие проблем вроде: При этом традиционно безопасность памяти отделяют от потоковой безопасности, которая подразумевает защиту программы от ошибок многопоточности и гонок данных (data races). Но эта грань оказалась тоньше, чем ожидалось. В статье автор приводит яркий пример кода на Go, языке, который широко считается безопасным с точки зрения управления памятью. Вот упрощённый пересказ проблемы, вскрывающей неожиданные последствия гонки данных: 🔹 Как это происходит на практике: На эт
Оглавление
Разломанный микрочип: слева зелёный сегмент с замком символизирует безопасность памяти, справа красный сегмент трескается и вспыхивает от гонки потоков. Перепутанные цветные «нити» потоков пронизывают схему, а в фоне полупрозрачный силуэт Go‑грофера напоминает, что проблема спрятана внутри языка
Разломанный микрочип: слева зелёный сегмент с замком символизирует безопасность памяти, справа красный сегмент трескается и вспыхивает от гонки потоков. Перепутанные цветные «нити» потоков пронизывают схему, а в фоне полупрозрачный силуэт Go‑грофера напоминает, что проблема спрятана внутри языка

Современные разработчики привыкли делить безопасность программ на несколько категорий: безопасность памяти (memory safety) и потоковая безопасность (thread safety). Однако на деле эти понятия куда теснее переплетены, чем кажется. Свежая статья эксперта по языкам программирования Ральфа Юнга заставляет по-новому взглянуть на то, как эти концепции влияют друг на друга, и почему даже популярные и «безопасные» языки, такие как Go, могут скрывать серьёзные риски.

🤔 Что такое безопасность памяти?

Под безопасностью памяти обычно понимается отсутствие проблем вроде:

  • 🗑️ Использования уже освобождённой памяти (use-after-free)
  • 📏 Выхода за пределы выделенного диапазона (out-of-bounds access)

При этом традиционно безопасность памяти отделяют от потоковой безопасности, которая подразумевает защиту программы от ошибок многопоточности и гонок данных (data races). Но эта грань оказалась тоньше, чем ожидалось.

🐞 Скрытая угроза в Go

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

🔹 Как это происходит на практике:

  • Создаётся глобальная переменная-интерфейс, которая постоянно переключается между двумя разными типами.
  • В другом потоке метод интерфейса вызывается постоянно без какой-либо синхронизации.
  • Возникает гонка данных, из-за которой интерфейс одновременно содержит указатель на объект одного типа, а методы от другого.
  • Программа пытается обратиться к некорректному адресу памяти (0x2a, т.е. числу 42), что вызывает крах программы (segfault).

На этом примере ясно видно, что простая гонка данных способна нарушить даже базовые гарантии памяти.

📚 А как у других?

Сравним поведение Go с другими популярными языками:

  • Java: Позволяет гонки данных, но поведение программы чётко определено, и подобные гонки не могут привести к критическому сбою.
  • 🦀 Rust и 🕊️ Swift: Предотвращают гонки данных статически, благодаря строгой типовой системе и ограничениям компилятора.

Go же оказывается в подвешенном состоянии: он допускает гонки, но не гарантирует определённого поведения, тем самым открывая двери неопределённому поведению (Undefined Behavior).

⚙️ Технические детали: почему это происходит?

Внутренне интерфейсы в Go представлены парой указателей (на данные и таблицу виртуальных функций — vtable). Когда гонка данных приводит к одновременному обновлению этих указателей, возникает ситуация, при которой метод одного типа пытается интерпретировать данные другого типа. Это приводит к обращению по неверному адресу, что в лучшем случае заканчивается крахом приложения, а в худшем — уязвимостью безопасности.

Подобные проблемы возникают и со срезами (slices), так как их структура также хранится в нескольких отдельных словах памяти, что опять-таки создаёт почву для гонок и некорректного чтения памяти.

🧐 Мнение автора: что такое истинная безопасность?

На мой взгляд, автор статьи абсолютно прав в своих рассуждениях: разделять понятия безопасности памяти и потоков не имеет смысла, если программа допускает неопределённое поведение. Настоящая безопасность — это отсутствие неопределённого поведения вообще. Любая брешь в этом принципе ставит под вопрос безопасность всей программы.

🛠️ Go пытается смягчить проблему встроенными инструментами (например, детектором гонок данных), но полагаться на них полностью рискованно: инструмент способен найти лишь те гонки, которые возникли при выполнении тестов, и не может гарантировать отсутствие проблем в продакшене.

📢 Личное мнение и выводы

Go — прекрасный язык для быстрой и простой разработки, но его подход к параллелизму несёт в себе скрытую опасность. Язык, позиционируемый как «безопасный», не должен допускать ситуации, в которых базовые инварианты памяти оказываются под угрозой из-за гонок данных.

🔸 Что можно было бы улучшить?

  • Введение строгой статической проверки гонок данных на уровне компиляции.
  • Более прозрачное описание таких рисков в официальной документации.

🔸 Что стоит помнить разработчикам:

  • Используйте средства синхронизации даже там, где это кажется избыточным.
  • Не полагайтесь исключительно на встроенные детекторы гонок — покрытие тестами никогда не бывает полным.

В конечном счёте, будущее за языками, которые сочетают простоту и строгость гарантий безопасности. Rust и Swift уже показывают пример такого пути — возможно, и Go пора переосмыслить свой подход.

🌐 Источник новости и дополнительная информация:

📖 Полезные ссылки из оригинала статьи: