Тайное знание: синхронизация, многопоточность, очереди

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Может ли быть так, что в большинстве популярных языков отсутствует самый эффективный механизм синхронизации? Что инженеры Microsoft, Oracle и мн. др., не говоря уже об остальных, вплоть до 2024 года так и не догадались, как же эффективнее всего синхронизировать доступ к данным? А все что знает абсолютное большинство программистов, в том числе топовых IT компаний (за исключением редких разработчиков платформ Apple) о синхронизации - ошибочно? Сегодня попробуем разобраться.

Эта статья подразумевает, что вы уже имеете базовое представление о механизмах синхронизации. Код написан на C#, но конкретный язык особого значения не имеет.

Вступление

На C# я уже давно не пишу, но около 12 лет назад я начал заниматься кроссплатформенной мобильной разработкой на Xamarin. В задачах часто требовалось реализовывать всевозможные синхронизации доступа к состоянию из главного и фоновых потоков, а также синхронизацию работы с базой SQLite. На тот момент в Xamarin были доступны как инструменты из нативной разработки - например Grand Central Dispatch с параллельными и последовательными очередями и NSOperationQueue на их основе, так и из языка C# - Monitor, SpinLock, Mutex, Semaphore, TPL Dataflow, Thread, потокобезопасные коллекции, RxUI и многое другое. Имея такое огромное разнообразие, разработчики использовали “то, что могли” и что быстрее находилось в поисковике и на StackOverflow: блокировка примитивами всех вызовов из главного потока или из пула потоков, создание отдельных потоков, синхронизация главным потоком, да и весь остальной зоопарк, что я перечислил ранее. Все это периодически сопровождалось ошибками типа заморозки пользовательского интерфейса, состояния гонки, взаимной блокировки.

Но все таки я любил докапываться до сути и находить лучшее решение, и довольно быстро пришел к выводу, что:

  • Любые длительные блокировки потоков - плохо, так как либо расходуют процессорное время на ожидание очереди, либо приводят к переключениям контекста и дополнительному созданию потоков. А блокировка главного потока приложения, отвечающего за рендеринг и обработку нажатий пользователя - это очень плохо, и ведет к “заморозке” отрисовки интерфейса и обработки нажатий.

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

Но с блокирующими операциями такое не пройдет - во время полноценных блокировок (Mutex) пул потоков вынужден создавать дополнительные потоки, чтобы ядра процессора не простаивали, а во время циклических блокировок (SpinLock) процессор попусту гоняет циклы. И если для операций ввода-вывода решение очевидно - использовать неблокирующие альтернативы, то все стандартные механизмы синхронизации популярных языков типа C# блокируют ожидающий поток на время ожидания очереди выполнения операции, которое может длится довольно долго - часто даже дольше чем сама операция.

Знакомство с GCD

Разбираясь в возможностях библиотеки GCD от Apple, и разрабатывая на Xamarin, параллельные очереди не показались чем то необходимым для использования - по сути это тот же ThreadPool либо TaskPool на C#. Но вот последовательные очереди меня заинтересовали - они легковесны, следуют принципу «первым пришёл — первым ушёл» (FIFO), не блокируют поток вызова на время ожидания выполнения операции. Выполняют операцию также в другом потоке, при этом не создают потоки на каждую операцию, а используют либо имеющийся пул, либо свой собственный поток в случае очереди главной очереди.

Логично сделать вывод, что на практике в большинстве случаев лучшим решением для синхронизации являются именно последовательные очереди, так как следуют ранее перечисленным требованиям "лучшего решения". Исключения могут составить лишь самые короткие операции, но даже для них, в абсолютном большинстве случаев, при разработке с уже имеющейся очередью главного потока (например под iOS), не имеет никакого смысла использовать более низкоуровневые, и соответственно более сложные и багоемкие конструкции, о которых могут не знать другие разработчики в команде - операцию в наносекунду лучше кинуть в эту самую очередь главного потока, и это никак не отразится на кадрах в секунду. Причем практика показывает, что даже те, кто думает что знают о примитивах синхронизации, часто заблуждаются и выдают некачественный, багоемкий и медленный код. А баги в “мнотопоточке” часто очень нестабильны и тяжелы в воспроизведении и исправлении. Также это хорошо сочетается с общепринятым правилом всегда вызывать методы обратного вызова (callback) в главном потоке, что позволяет не гадать в каком потоке ты сейчас находишься, и не думать лишний раз о синхронизациях. Кстати, в Xamarin для async/await при вызове из главного потока это работало по умолчанию, что довольно удобно.

Реальные примеры проблем с блокировками потоков и синхронизацией

Несколько примеров ошибки блокировки потоков, отразившихся на качество продукта, из реальной жизни:

  • Одно из самых популярных приложений каршеринга в России - годами главный поток постоянно блокировался, и только относительно недавно они наконец смогли решить проблему. Приложение работало настолько отвратительно, что пользователей пришлось удерживать самой низкой ценой на рынке, за что, впрочем, хочу высказать коллегам “руки-не-из-того-места” благодарность

Источник: https://habr.com/ru/articles/803273/


Интересные статьи

Интересные статьи

Почти все самые важные и интересные финансовые новости в России и мире за неделю: банки хотят повышать ипотечные ставки по невыгодным договорам, из белорусского БНБ-банка выгоняют нерезидентов, Тинько...
В этой статье хочу поделиться нашим опытом работы с обновлениями RabbitMQ Live. Здесь вы узнаете некоторые подробности о нашей архитектуре и вариантах ее использования. Давайте начнем с самого простог...
Всем привет! В новой версии KeyClusterer нами была проведена работа над оптимизацией импорта данных, добавлена многопоточность с возможностью сбора данных в поисковой системе Google, доба...
Я переводчик из ижевской компании CG Tribe, и я продолжаю выкладывать перевод руководства к Vulkan API. Ссылка на источник — vulkan-tutorial.com. В этой публикации представлен перевод...
Есть мнение, что Яндекс, занимая лидирующее положение на рынке интернет-поиска в России, не просто продвигает свои сервисы общедоступными способами. И что он с помощью «колдунщиков» з...