惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 【当耐特】
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 最新话题
O
OpenAI News
S
Secure Thoughts
Cisco Talos Blog
Cisco Talos Blog
Forbes - Security
Forbes - Security
V
Visual Studio Blog
有赞技术团队
有赞技术团队
Jina AI
Jina AI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Microsoft Azure Blog
Microsoft Azure Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Troy Hunt's Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Schneier on Security
雷峰网
雷峰网
The Cloudflare Blog
量子位
Last Week in AI
Last Week in AI
T
Tor Project blog
V
Vulnerabilities – Threatpost
C
Cisco Blogs
B
Blog RSS Feed
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
G
GRAHAM CLULEY
MyScale Blog
MyScale Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Schneier on Security
Schneier on Security
Martin Fowler
Martin Fowler
Help Net Security
Help Net Security
J
Java Code Geeks
人人都是产品经理
人人都是产品经理
G
Google Developers Blog
S
Security Affairs
A
Arctic Wolf
T
Tenable Blog
PCI Perspectives
PCI Perspectives
Spread Privacy
Spread Privacy
AI
AI
L
LangChain Blog
Latest news
Latest news
博客园 - 叶小钗
博客园 - Franky
T
Threatpost
MongoDB | Blog
MongoDB | Blog
Hugging Face - Blog
Hugging Face - Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Структуры данных на практике. Глава 13: Структуры данных без блокировок
PatientZero · 2026-04-27 · via Все публикации подряд на Хабре

Время на прочтение9 мин

Охват и читатели369

Перевод

«Блокировки — это goto конкурентного программирования», — Морис Херлихи

Оглавление

Глава 1: Разрыв в производительности

Глава 2: Иерархия памяти

Глава 3: Бенчмаркинг и профилирование

Глава 4: Массивы и локальность кэша

Глава 5: Связанные списки — убийцы кэша

Глава 6: Стеки и очереди

Глава 7: Хэш-таблицы и конфликты кэша

Глава 8: Динамические массивы и управление памятью

Глава 9: Двоичные деревья поиска

Глава 10: B-деревья и деревья, эффективно использующие кэш

Глава 11: Префиксные деревья и базисные деревья

Глава 12: Кучи и очереди с приоритетом

Глава 13: Структуры данных без блокировок

Проблема 60%

Наша система логгинга тратила 60% своего времени на ожидание снятия блокировок. Не на выполнение полезной работы, только на одно ожидание.

Восемь ядер, пытавшихся записывать сообщения логов, имели общий кольцевой буфер. Реализация была простой: буфер защищался мьютексом. При высокой нагрузке, когда все ядра записывали логи одновременно, профилировщик демонстрировал ужасный паттерн: 60% тактов CPU тратилось на операции с мьютексом.

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

«Можно ли улучшить ситуацию, отказавшись от блокировок?», — спросил меня мой менеджер во время ревью производительности.

Этот вопрос привёл к полной смене архитектуры. Простое решение на основе мьютекса выглядело так:

typedef struct {
    char buffer[LOG_SIZE];
    int head;
    int tail;
    pthread_mutex_t lock;
} log_buffer_t;

void log_write(log_buffer_t *log, const char *msg) {
    pthread_mutex_lock(&log->lock);
    // Запись сообщения в буфер
    int next = (log->tail + 1) % LOG_SIZE;
    strcpy(&log->buffer[log->tail], msg);
    log->tail = next;
    pthread_mutex_unlock(&log->lock);
}

Простое, корректное... и медленное.

При большой нагрузке (одновременном логгинге всех восьми ядер) система тратила 60% своего времени на ожидание снятия блокировок:

$ perf stat -e cycles,instructions,cache-misses ./logger_mutex
  Performance counter stats:
    8,500,000,000 cycles
    2,100,000,000 instructions
       45,000,000 cache-misses
       
Lock contention: 60% of cycles spent in mutex operations
Throughput: 850,000 messages/second

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

$ perf stat -e cycles,instructions,cache-misses ./logger_lockfree
  Performance counter stats:
    3,200,000,000 cycles
    2,400,000,000 instructions
       12,000,000 cache-misses
       
Lock contention: 0%
Throughput: 2,400,000 messages/second

В 2,8 раза быстрее с нулевыми конфликтами блокировок. Однако код стал гораздо сложнее.

В этой главе мы поговорим о том, когда структуры данных без блокировок оправдывают свою сложность.


Что нам рассказывают учебники

Нам обещают, что структуры данных без блокировок:

  • Не имеют блокировок: потокам не нужно ждать снятия блокировок

  • Лучше масштабируются: производительность растёт с увеличением количества ядер

  • Не имеют взаимных блокировок: без блокировок не получится и взаимных блокировок

  • Гарантируют прогресс: по крайней мере, один поток всегда имеет прогресс

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


Проверка реальностью

Программирование без блокировок — это сложная задача. Вот, на что не делают упор учебники:

1. Сложности порядка работы с памятью

В современных CPU порядок операций с памятью можно менять. Рассмотрим этот «простой» флаг без блокировки:

// Поток 1
data = 42;
ready = 1;  // Сигнализирует о том, что данные готовы

// Поток 2
if (ready) {
    use(data);  // Возможно, видит старое значение данных!
}

Проблема: CPU может изменить порядок записи в Потоке 1, поэтому Поток 2 увидит ready = 1 до data = 42.

Решение: барьеры памяти (fence):

// Поток 1
data = 42;
__atomic_store_n(&ready, 1, __ATOMIC_RELEASE);  // Снимаем барьер

// Поток 2
if (__atomic_load_n(&ready, __ATOMIC_ACQUIRE)) {  // Ставим барьер
    use(data);  // Теперь поток гарантированно увидит data = 42
}

2. Проблема ABA

У классического стека без блокировок есть малозаметный баг:

typedef struct node {
    int value;
    struct node *next;
} node_t;

node_t *top;  // Вершина стека

void push(int value) {
    node_t *new_node = malloc(sizeof(node_t));
    new_node->value = value;
    do {
        new_node->next = top;
    } while (!__atomic_compare_exchange_n(&top, &new_node->next, new_node,
                                          0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST));
}

Проблема ABA:

  1. Поток 1 считывает top = A

  2. Поток 2 извлекает A, извлекает B, снова записывает A (на вершине снова A)

  3. Операция сравнения с обменом (CAS) Потока 1 завершается успешно (на вершине по-прежнему A), но стек повреждён!

Решение: использовать тегированные указатели или указатели опасности (сложно!).

3. Пинг-понг линий кэша

Отсутствие блокировок не означает удобство для кэша. Рассмотрим счётчик без блокировок:

atomic_int counter = 0;

// Все восемь потоков выполняют инкремент
__atomic_fetch_add(&counter, 1, __ATOMIC_SEQ_CST);

Каждый инкремент заставляет линию кэша перескакивать между ядрами:

Ядро 0: считывание счётчика (промах кэша)
Ядро 0: инкремент, запись обратно
Ядро 1: считывание счётчика (промах кэша - инвалидирован Ядром 0)
Ядро 1: инкремент, запись обратно
Ядро 2: считывание счётчика (промах кэша - инвалидирован Ядром 1)
...

Результат: при сильных конфликтах хуже, чем мьютекс!

Я измерил это в нашей системе логгинга:

8 ядер, атомарный счётчик:
  Промахи кэша: 45 миллионов (столько же, сколько и с мьютексом!)
  Пропускная способность: 900 тысяч операций/с (чуть лучше, чем с мьютексом)

8 ядер, счётчики для каждого ядра (не общий):
  Промахи кэша: 2 миллиона
  Пропускная способность: 6,5 миллиона операций/с

Вывод: по возможности следует избегать общих атомарных переменных между ядрами.


Кольцевой буфер без блокировок: практический пример

Позвольте показать, как я реализовал кольцевой буфер для нашей системы логгинга.

Архитектура

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

typedef struct {
    char buffer[LOG_SIZE];
    atomic_int head;  // Индекс чтения
    atomic_int tail;  // Индекс записи
} lockfree_log_t;

bool log_write(lockfree_log_t *log, const char *msg, int len) {
    int current_tail, next_tail, current_head;

    do {
        current_tail = __atomic_load_n(&log->tail, __ATOMIC_ACQUIRE);
        next_tail = (current_tail + len) % LOG_SIZE;
        current_head = __atomic_load_n(&log->head, __ATOMIC_ACQUIRE);

        // Проверка буфера на заполненность
        if (next_tail == current_head) {
            return false;  // Буфер заполнен
        }

    } while (!__atomic_compare_exchange_n(&log->tail, &current_tail, next_tail,
                                          0, __ATOMIC_RELEASE, __ATOMIC_ACQUIRE));

    // Теперь мы владеем диапазоном [current_tail, next_tail)
    memcpy(&log->buffer[current_tail], msg, len);

    return true;
}

Почему это работает

  1. Операция CAS с хвостом: затребовать диапазон может только один поток

  2. Отсутствие блокировок данных: после получения диапазона запись выполняется без конфликтов

  3. Порядок операций с памятью: ACQUIRE/RELEASE обеспечивает видимость

Бенчмарк

Я сравнил три реализации:

Тест: 8 ядер, 10 миллионов сообщений логов

На основе мьютекса:
  Такты: 8,5 миллиарда
  Промахи кэша: 45 миллионов
  Пропускная способность: 850 тысяч сообщений/с
  Конфликты блокировок: 60%

На основе спин-блокировки:
  Такты: 7,2 миллиарда
  Промахи кэша: 52 миллиона (хуже!)
  Пропускная способность: 1,1 миллиона сообщений/с
  Конфликты блокировок: 45%

Без блокировок (атомарные CAS):
  Такты: 3,2 миллиарда
  Промахи кэша: 12 миллиона
  Пропускная способность: 2,4 миллиона сообщений/с
  Конфликты блокировок: 0%

Версия без блокировок оказалась в 2,8 раз быстрее, чем версия с мьютексом, и в 2,2 раза быстрее, чем со спин-блокировкой.

Почему:

  • Отсутствие системных вызовов (мьютекс использует фьютекс)

  • Отсутствие циклов (спин-блокировка тратит такты)

  • Меньше трафика когерентности кэша (атомарны только индексы)


Когда разумно не пользоваться блокировками

Реализовав множество структур данных без блокировок, я понял, когда они оправдывают свою сложность.

1. Высокая конфликтность, короткие критические части

Если потоки постоянно конфликтуют за один ресурс, а работа внутри него выполняется быстро (всего несколько команд), то отсутствие блокировок может быть выгодным.

Пример: наша система логгинга

  • Высокая конфликтность: 8 ядер выполняют логгинг одновременно

  • Короткие критические части: просто обновление индекса

  • Результат: ускорение в 2,8 раза

2. Системы реального времени

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

Структуры без блокировок обеспечивают гарантии отсутствия ожидания и отсутствия блокировок.

Пример: обработчики прерываний

  • Невозможность блокировки контекста прерывания

  • Запросы без блокировок обеспечивают возможность связи прерывание → поток

  • Используется в кольцевых буферах ядра Linux

3. Нагрузки с большой долей операций чтения

Если количество операций чтения сильно превышает количество операций записи, то RCU (Read-Copy-Update) может полностью устранить блокировки на стороне чтения.

Пример: RCU ядра Linux

  • Читатели: без блокировок, без атомарных операций, только барьеры памяти

  • Писатели: редки, используют синхронизацию

  • Результат: миллионы операций чтения в секунду с нулевыми конфликтами

4. Когда НЕ использовать структуры данных без блокировки

Низкая конфликтность: если потоки редко конфликтуют, мьютекс проще и имеет такую же скорость.

Тест: 2 ядра, низкая конфликтность
  Мьютекс: 1,2 миллиона операций/с
  Без блокировки: 1,3 миллиона операций/с (всего на 8% быстрее, не оправдывает повышения сложности)

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

Отладка: баги с отсутствием блокировок крайне сложно воссоздавать и отлаживать. Если доказанной проблемы с производительностью нет, используйте блокировки.


Пример из реального мира: переменные ядра Linux для каждого CPU

Чтобы не использовать атомарные операции, в ядре Linux применяется хитрый трюк: переменные каждого CPU.

Вместо одного общего счётчика:

atomic_int global_counter;  // Общий счётчик, приводящий к пинг-понгу кэша

применяется по одному счётчику на каждый CPU:

DEFINE_PER_CPU(int, local_counter);  // По одному на CPU, без общего счётчика

void increment_counter(void) {
    int cpu = smp_processor_id();
    per_cpu(local_counter, cpu)++;  // Атомарность не нужна!
}

int read_total(void) {
    int total = 0;
    for_each_possible_cpu(cpu) {
        total += per_cpu(local_counter, cpu);
    }
    return total;
}

Почему это работает:

  • У каждого CPU есть собственная линия кэша

  • Отсутствие трафика когерентности кэша

  • Операции чтения редки (только когда нужна общая сумма)

Я использовал этот паттерн в нашей системе логгинга для статистики:

По одному счётчику на CPU (сообщения логируются для каждого ядра):
  Промахи кэша: 0 (у каждого ядра есть собственная линия кэша)
  Производительность: 8 миллионов инкрементов/с (8 ядер × 1 миллион на каждое)

Общий атомарный счётчик:
  Промахи кэша: 45 миллионов
  Производительность: 900 тысяч инкрементов/с

Ускорение в 8,9 раза благодаря отсутствию общего счётчика!


Порядок операций с памятью RISC-V

RISC-V имеет relaxed-модель памяти (RVWMO - RISC-V Weak Memory Ordering). Из-за этого ему нужны явно задаваемые fence.

Команды fence

# Полный fence (для всех операций с памятью)
fence rw, rw

# Fence получения (загрузка-загрузка, загрузка-хранение)
fence r, rw

# Fence освобождения (загрузка-хранение, хранение-хранение)
fence rw, w

Атомарные операции C11 на RISC-V

GCC сопоставляет атомарные операции C11 с командами RISC-V:

// __ATOMIC_ACQUIRE
__atomic_load_n(&x, __ATOMIC_ACQUIRE);

Это компилируется в следующий код:

ld   a0, 0(a1)      # Загрузка
fence r, rw         # Fence получения
// __ATOMIC_RELEASE
__atomic_store_n(&x, 42, __ATOMIC_RELEASE);

Компилируется в:

fence rw, w         # Fence освобождения
sd   a0, 0(a1)      # Хранение

Затраты на fence

Fence не бесплатны. Я измерил оверхед:

Тест: 1 миллион атомарных загрузок (RISC-V U74 @ 1.2 GHz)

Relaxed (без fence):
  Такты: 1,2 миллиона (1 такт на загрузку)

Получение (fence r, rw):
  Такты: 8,5 миллиона (8,5 тактов на загрузку)

Последовательная (fence rw, rw):
  Такты: 12,3 миллиона (12,3 такта на загрузку)

Вывод: используйте корректную самый слабый порядок операций с памятью. Не выбирайте по умолчанию __ATOMIC_SEQ_CST.


Подведём итог

Проблема 60 процентов была решена. Кольцевой буфер без блокировок увеличил производительность с 850 тысяч до 2,4 миллиона сообщений в секунду (ускорение в 2,8 раза). Конфликты блокировок снизились с 60% до нуля. Однако код стал существенно сложнее, он требует большого внимания к порядку операций с памятью и проблеме ABA.

Ключевые наблюдения:

  1. Отсутствие блокировок не всегда быстрее. В частях кода с малым количеством конфликтов или сложным критичным кодом мьютексы проще и имеют такую же скорость.

  2. Необходимо внимание к порядку операций с памятью. Для обеспечения видимости между ядрами нужны барьеры ACQUIRE/RELEASE. Из-за relaxed-модели памяти RISC-V они должны быть явными.

  3. Важная когерентность кэша. Атомарные операции приводят к пинг-понгу линий кэша. По возможности избегайте общих атомарных переменных между ядрами.

  4. Проблема ABA. Чтобы избежать повреждений, стекам и очередям без блокировок нужны тегированные указатели или указатели опасности.

  5. Наличие переменных для каждого CPU позволяет устранить конфликты. Если можно разбить данные на CPU, то можно полностью избежать атомарных операций. Часто это оказывается в 5-10 быстрее, чем общие атомарные операции.

Показатели нашей системы логгинга:

  • Мьютекс: 850 тысяч сообщений/с, конфликты блокировок 60%

  • Без блокировок: 2,4 миллиона сообщений/с, 0% конфликтов

  • Статистика для каждого CPU: 8 миллионов инкрементов/с (при общем атомарном счётчике 900 тысяч)

Структуры данных без блокировок — мощный инструмент, но использовать его следует только тогда, когда при профилировании оказывается, что реальным узким местом стали конфликты блокировок. Такая система имеет высокие затраты на сложность.

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