Документация Engee

Многопоточность

Threads.@threads [schedule] for ... end

Макрос, выполняющий цикл for в параллельном режиме. Пространство итерации распределяется среди крупномодульных задач. Эту политику можно задать аргументом schedule. Выполнение цикла ожидает оценки всех итераций.

См. также описание @spawn и pmap в Distributed.

Расширенная справка

Семантика

За исключением случаев, когда более твердые гарантии определены параметром настройки расписания, цикл, выполняемый макросом @threads, имеет следующую семантику.

Макрос @threads выполняет тело цикла в неустановленном порядке и, возможно, в многопоточном режиме. Он не задает точные присваивания заданий и потоки рабочей роли. Для каждого выполнения присваивания могут отличаться. Код тела цикла (включая весь код, транзитивно вызываемый из него) не должен делать допущения относительно распределения итераций по заданиям или рабочим потокам, в котором они выполняются. Тело цикла для каждой итерации должно иметь возможность развиваться прогрессивно независимо от остальных итераций, и гонка по данным должна быть исключена. Таким образом, недопустимые синхронизации в различных итерациях могут взаимоблокироваться, а несинхронизированные сеансы доступа к памяти — привести к неопределенному поведению.

Например, приведенные выше условия предполагают следующее:

  • Блокировка, установленная в итерации, должна быть снята в пределах той же итерации.

  • Взаимодействие между итерациями с использованием примитивов, таких как Channel, является некорректным.

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

  • Если не используется расписание :static, значение threadid() может изменяться даже в пределах одной итерации. См. раздел Task Migration.

Планировщики

Без аргумента планировщика точное создание расписания настроить нельзя, и оно может варьироваться в различных выпусках Julia. В настоящее время используется :dynamic, если планировщик не задан.

Совместимость: Julia 1.5

Аргумент schedule впервые реализован в Julia 1.5.

:dynamic (по умолчанию)

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

Этот параметр планирования представляет собой просто подсказку для базового механизма выполнения. Однако при этом можно ожидать ряда свойства. Количество Task, используемых планировщиком :dynamic, ограничено небольшой постоянной величиной, равной количеству доступных рабочих потоков (Threads.threadpoolsize()). Каждое задание обрабатывает сплошные области пространства итерации. Таким образом, @threads :dynamic for x in xs; f(x); end, как правило, более эффективен, чем @sync for x in xs; @spawn f(x); end, если length(xs) значительно превышает количество рабочих потоков и время выполнения f(x) относительно меньше стоимости порождения и синхронизации задания (как правило, менее 10 миллисекунд).

Совместимость: Julia 1.8

Доступен параметр :dynamic аргумента schedule, который используется по умолчанию начиная с версии Julia 1.8.

:greedy

Планировщик :greedy порождает до Threads.threadpoolsize() задач, каждая из которых обрабатывает как можно больше заданных итерируемых значений по мере их создания. Как только задача завершает работу, она берет следующее значение из итератора. Любая отдельная задача не обязательно выполняет работу со смежными значениями итератора. Итератор может выдавать значения вечно; требуется только интерфейс итератора (без индексации).

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

Совместимость: Julia 1.11

Значение :greedy аргумента schedule впервые реализовано в Julia 1.11.

:static

Планировщик :static создает одну задачу на поток и разделяет итерации поровну между ними, присваивая каждую конкретную задачу отдельно каждому потоку. В частности, значение threadid() гарантированно будет константой в пределах одной итерации. Указание :static является ошибочным, если используется из другого цикла @threads или потока, отличного от 1.

Планирование :static предусматривается для поддержки переноса кода, написанного в версиях до Julia 1.3. В новых функциях библиотеки планирование :static не рекомендуется, поскольку функции, использующие этот параметр, нельзя вызывать из произвольных рабочих потоков.

Примеры

Чтобы продемонстрировать различные стратегии планирования, можно рассмотреть следующую функцию busywait, которая содержит нерезультативный цикл заданной продолжительности, выполняемый в течение определенного количества секунд.

julia> function busywait(seconds)
            tstart = time_ns()
            while (time_ns() - tstart) / 1e9 < seconds
            end
        end

julia> @time begin
            Threads.@spawn busywait(5)
            Threads.@threads :static for i in 1:Threads.threadpoolsize()
                busywait(1)
            end
        end
6.003001 seconds (16.33 k allocations: 899.255 KiB, 0.25% compilation time)

julia> @time begin
            Threads.@spawn busywait(5)
            Threads.@threads :dynamic for i in 1:Threads.threadpoolsize()
                busywait(1)
            end
        end
2.012056 seconds (16.05 k allocations: 883.919 KiB, 0.66% compilation time)

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

Threads.foreach(f, channel::Channel;
                schedule::Threads.AbstractSchedule=Threads.FairSchedule(),
                ntasks=Threads.threadpoolsize())

Аналогично foreach(f, channel), но итерация по channel и вызовы f разделяются по ntasks задачам, порождаемым Threads.@spawn. Эта функция будет ожидать завершения внутренне порожденных задач, прежде чем вернуть управление.

Если schedule isa FairSchedule, то Threads.foreach попытается породить задачи таким способом, который позволит планировщику Julia более свободно балансировать нагрузку для рабочих операций в различных потоках. Такой подход в целом предполагает более высокие накладные расходы на операцию, но обеспечивает более высокую эффективность по сравнению с StaticSchedule в параллельном режиме при наличии других многопотоковых рабочих нагрузок.

Если schedule isa StaticSchedule, то Threads.foreach порождает задания таким способом, который предполагает более низкие накладные расходы на операцию, чем FairSchedule, но менее пригоден для балансировки нагрузки. Следовательно, такой подход может лучше подходить для более точных, однородных рабочих нагрузок, но при этом будет менее эффективным, чем FairSchedule, в параллельном режиме при наличии других многопотоковых рабочих нагрузок.

Примеры

julia> n = 20

julia> c = Channel{Int}(ch -> foreach(i -> put!(ch, i), 1:n), 1)

julia> d = Channel{Int}(n) do ch
           f = i -> put!(ch, i^2)
           Threads.foreach(f, c)
       end

julia> collect(d)
collect(d) = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400]
Совместимость: Julia 1.6

Для этой функции требуется версия Julia не ниже 1.6.

Threads.@spawn [:default|:interactive] expr

Создает Task и применяет к нему schedule для выполнения в любом доступном потоке в указанном пуле потоков (:default, если не указано). Задача выделяется в поток после того, как станет доступной. Чтобы дождаться завершения задачи, вызовите wait для результата этого макроса или вызовите fetch, чтобы дождаться завершения и затем получить возвращаемое значение.

Значения можно интерполировать в @spawn с помощью $, который копирует значение непосредственно в сконструированное базовое замыкание. Это позволяет вставлять значение переменной, изолируя асинхронный код от изменений значения переменной в текущей задаче.

Поток, в котором выполняется задача, может измениться, если задача завершится, поэтому threadid() не следует рассматривать как константу для задачи. Дополнительные важные предостережения см. в разделе Task Migration и в более обширном руководстве по многопоточности. См. также главу о пулах потоков.

Совместимость: Julia 1.3

Этот макрос впервые реализован в Julia 1.3.

Совместимость: Julia 1.4

Интерполяция значений с помощью $ доступна начиная с версии Julia 1.4.

Совместимость: Julia 1.9

Пул потоков можно указывать начиная с версии Julia 1.9.

Примеры

julia> t() = println("Hello from ", Threads.threadid());

julia> tasks = fetch.([Threads.@spawn t() for i in 1:4]);
Hello from 1
Hello from 1
Hello from 3
Hello from 4
Threads.threadid() -> Int

Получает идентификационный номер текущего потока выполнения. Главный поток имеет идентификатор 1.

Примеры

julia> Threads.threadid()
1

julia> Threads.@threads for i in 1:4
          println(Threads.threadid())
       end
4
2
5
4

Поток, в котором выполняется задача, может измениться, если задача завершится, что называется Task Migration. По этой причине в большинстве случаев небезопасно использовать threadid() для индексации, скажем, вектора буферных объектов или объектов с сохранением состояния.

Threads.maxthreadid() -> Int

Получает нижнюю границу числа доступных потоков (во всех пулах потоков), доступных процессу Julia, с семантикой atomic-acquire. Результат всегда будет больше или равен threadid(), а также threadid(task) для любой задачи, которую вы могли наблюдать до вызова maxthreadid.

Threads.nthreads(:default | :interactive) -> Int

Получает текущее количество потоков в указанном пуле потоков. Потоки в :interactive имеют идентификационные номера 1:nthreads(:interactive), а потоки в :default — в диапазоне nthreads(:interactive) .+ (1:nthreads(:default)).

См. также описание BLAS.get_num_threads и BLAS.set_num_threads в стандартной библиотеке LinearAlgebra и nprocs() в стандартной библиотеке Distributed и Threads.maxthreadid().

Threads.threadpool(tid = threadid()) -> Symbol

Возвращает пул потоков указанного потока: :default, :interactive или :foreign.

Threads.nthreadpools() -> Int

Возвращает количество текущих настроенных пулов потоков.

Threads.threadpoolsize(pool::Symbol = :default) -> Int

Получает количество потоков, доступных пулу потоков по умолчанию (или указанному пулу потоков).

См. также описание BLAS.get_num_threads и BLAS.set_num_threads в стандартной библиотеке LinearAlgebra и nprocs() в стандартной библиотеке Distributed.

Threads.ngcthreads() -> Int

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

См. также раздел Многопоточность.

Атомарные операции

Небезопасные операции с указателями совместимы с загрузкой и сохранением указателей, объявленных с типом _Atomic и std::atomic в C11 и C++23 соответственно. Если атомарная загрузка типа Julia T не поддерживается, может возникнуть ошибка.

См. также описание unsafe_load, unsafe_modify!, unsafe_replace!, unsafe_store!, unsafe_swap!

@atomic var
@atomic order ex

Помечает var или ex как выполняемый атомарно, если ex представляет собой поддерживаемое выражение. Если значение order не указано, по умолчанию используется :sequentially_consistent.

@atomic a.b.x = new @atomic a.b.x += addend @atomic :release a.b.x = new @atomic :acquire_release a.b.x += addend

Выполняет операцию хранения, атомарно выраженную в правой части, и возвращает новое значение.

При использовании = эта операция преобразуется в вызов setproperty!(a.b, :x, new). При использовании любого оператора эта операция преобразуется в вызов modifyproperty!(a.b, :x, +, addend)[2].

@atomic a.b.x max arg2 @atomic a.b.x + arg2 @atomic max(a.b.x, arg2) @atomic :acquire_release max(a.b.x, arg2) @atomic :acquire_release a.b.x + arg2 @atomic :acquire_release a.b.x max arg2

Выполняет двоичную операцию, атомарно выраженную в правой части. Сохраняет результат в поле в первом аргументе и возвращает значения (old, new).

Эта операция преобразуется в вызов modifyproperty!(a.b, :x, func, arg2).

Дополнительные сведения см. в разделе Атомарные операции в каждом поле руководства.

Примеры

julia> mutable struct Atomic{T}; @atomic x::T; end

julia> a = Atomic(1)
Atomic{Int64}(1)

julia> @atomic a.x # получает поле х объекта а с последовательной согласованностью
1

julia> @atomic :sequentially_consistent a.x = 2 # задает поле х объекта а с последовательной согласованностью
2

julia> @atomic a.x += 1 # пошагово увеличивает поле х объекта а с последовательной согласованностью
3

julia> @atomic a.x + 1 # пошагово увеличивает поле х объекта а с последовательной согласованностью
3 => 4

julia> @atomic a.x # получает поле х объекта а с последовательной согласованностью
4

julia> @atomic max(a.x, 10) # изменяет поле х объекта а на максимальное значение с последовательной согласованностью
4 => 10

julia> @atomic a.x max 5 # снова изменяет поле х объекта а на максимальное значение с последовательной согласованностью
10 => 10
Совместимость: Julia 1.7

Для этой функции требуется версия Julia не ниже 1.7.

@atomicswap a.b.x = new
@atomicswap :sequentially_consistent a.b.x = new

Сохраняет new в a.b.x и возвращает прежнее значение a.b.x.

Эта операция преобразуется в вызов swapproperty!(a.b, :x, new).

Дополнительные сведения см. в разделе Атомарные операции в каждом поле руководства.

Примеры

julia> mutable struct Atomic{T}; @atomic x::T; end

julia> a = Atomic(1)
Atomic{Int64}(1)

julia> @atomicswap a.x = 2+2 # заменяет поле х объекта а на значение 4 с последовательной согласованностью
1

julia> @atomic a.x # получает поле х объекта а с последовательной согласованностью
4
Совместимость: Julia 1.7

Для этой функции требуется версия Julia не ниже 1.7.

@atomicreplace a.b.x expected => desired
@atomicreplace :sequentially_consistent a.b.x expected => desired
@atomicreplace :sequentially_consistent :monotonic a.b.x expected => desired

Выполняет условную замену, атомарно выраженную парой, возвращая значения (old, success::Bool). Где success указывает, выполнялась ли замена.

Эта операция преобразуется в вызов replaceproperty!(a.b, :x, expected, desired).

Дополнительные сведения см. в разделе Атомарные операции в каждом поле руководства.

Примеры

julia> mutable struct Atomic{T}; @atomic x::T; end

julia> a = Atomic(1)
Atomic{Int64}(1)

julia> @atomicreplace a.x 1 => 2 # заменяет поле х объекта а на значение 2, если оно было равно 1, с последовательной согласованностью
(old = 1, success = true)

julia> @atomic a.x # получает поле х объекта а с последовательной согласованностью
2

julia> @atomicreplace a.x 1 => 2 # заменяет поле х объекта а на значение 2, если оно было равно 1, с последовательной согласованностью
(old = 2, success = false)

julia> xchg = 2 => 0; # заменяет поле х объекта а на значение 0, если оно было равно 2, с последовательной согласованностью

julia> @atomicreplace a.x xchg
(old = 2, success = true)

julia> @atomic a.x # получает поле х объекта а с последовательной согласованностью
0
Совместимость: Julia 1.7

Для этой функции требуется версия Julia не ниже 1.7.

@atomiconce a.b.x = value
@atomiconce :sequentially_consistent a.b.x = value
@atomiconce :sequentially_consistent :monotonic a.b.x = value

Выполняет условное присваивание значения атомарно, если оно ранее не было установлено, возвращая значение success::Bool. success указывает, было ли присваивание выполнено.

Эта операция преобразуется в вызов setpropertyonce!(a.b, :x, value).

Дополнительные сведения см. в разделе Атомарные операции в каждом поле руководства.

Примеры

julia> mutable struct AtomicOnce
           @atomic x
           AtomicOnce() = new()
       end

julia> a = AtomicOnce()
AtomicOnce(#undef)

julia> @atomiconce a.x = 1 # присваивает полю х объекта а значение 1, если оно не было задано, с последовательной согласованностью
true

julia> @atomic a.x # получает поле х объекта а с последовательной согласованностью
1

julia> @atomiconce a.x = 1 # присваивает полю х объекта а значение 1, если оно не было задано, с последовательной согласованностью
false
Совместимость: Julia 1.11

Для этой функции требуется версия Julia не ниже 1.11.

AtomicMemory{T} == GenericMemory{:atomic, T, Core.CPU}

Вектор DenseVector{T} фиксированного размера. Доступ к любому его элементу осуществляется атомарно (с упорядочением :monotonic). Задание любого элемента должно осуществляться с помощью макроса @atomic с явным указанием упорядочения.

Каждый элемент является независимо атомарным при доступе и не может быть задан неатомарно. В настоящее время макрос @atomic и интерфейс более высокого уровня еще не полностью готовы, но строительными блоками для будущей реализации являются внутренние встроенные функции Core.memoryrefget, Core.memoryrefset!, Core.memoryref_isassigned, Core.memoryrefswap!, Core.memoryrefmodify! и Core.memoryrefreplace!.

Подробные сведения см. в разделе Атомарные операции.

Совместимость: Julia 1.11

Для этого типа требуется версия Julia не ниже 1.11.

Для небезопасного (unsafe) набора функций также существуют необязательные параметры упорядочения памяти, которые выбирают совместимые с C/C++ версии этих атомарных операций, если для этого параметра задано unsafe_load, unsafe_store!, unsafe_swap!, unsafe_replace! и unsafe_modify!.

Следующие интерфейсы API устарели, хотя, скорее всего, будут поддерживаться еще в нескольких выпусках.

Threads.Atomic{T}

Содержит ссылку на объект типа T, разрешая только атомарный доступ, то есть потокобезопасным способом.

Только определенные «простые» типы можно использовать атомарно, в частности примитивный логический тип, целочисленный тип и тип с плавающей запятой. К ним относятся Bool, Int8…​Int128, UInt8…​UInt128 и Float16…​Float64.

Новые атомарные объекты можно создавать на основе неатомарных значений; если они не заданы, атомарный объект инициализируется с нулевым значением.

Доступ к атомарным объектам можно осуществлять с помощью нотации []:

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> x[] = 1
1

julia> x[]
1

Атомарные операции используют префикс atomic_, такой как atomic_add!, atomic_xchg! и т. д.

Threads.atomic_cas!(x::Atomic{T}, cmp::T, newval::T) where T

Атомарно выполняет операцию сравнения с обменом x.

Атомарно сравнивает значение в x с cmp. Если они равны, выполняется запись newval в x. В противном случае x остается без изменений. Возвращает прежнее значение x. Сравнивая возвращаемое значение с cmp (посредством ===), можно узнать, изменялся ли x и содержит ли он теперь новое значение newval.

Дополнительные сведения см. в инструкции LLVM cmpxchg.

Эту функцию можно использовать для реализации транзакционной семантики. До транзакции выполняется запись значения в x. После выполнения транзакции новое значение сохраняется только в том случае, если x не изменен за этот период.

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_cas!(x, 4, 2);

julia> x
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_cas!(x, 3, 2);

julia> x
Base.Threads.Atomic{Int64}(2)
Threads.atomic_xchg!(x::Atomic{T}, newval::T) where T

Атомарно заменяет значение в x.

Атомарно заменяет значение в x на newval. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw xchg.

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_xchg!(x, 2)
3

julia> x[]
2
Threads.atomic_add!(x::Atomic{T}, val::T) where T <: ArithmeticTypes

Атомарно прибавляет val к x

Выполняет x[] += val атомарным образом. Возвращает прежнее значение. Не определено для Atomic{Bool}.

Дополнительные сведения см. в инструкции LLVM atomicrmw add.

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_add!(x, 2)
3

julia> x[]
5
Threads.atomic_sub!(x::Atomic{T}, val::T) where T <: ArithmeticTypes

Атомарно вычитает val из x

Выполняет x[] -= val атомарным образом. Возвращает прежнее значение. Не определено для Atomic{Bool}.

Дополнительные сведения см. в инструкции LLVM atomicrmw sub.

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_sub!(x, 2)
3

julia> x[]
1
Threads.atomic_and!(x::Atomic{T}, val::T) where T

Атомарно выполняет битовую операцию «и» с x и val.

Выполняет x[] &= val атомарным образом. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw and.

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_and!(x, 2)
3

julia> x[]
2
Threads.atomic_nand!(x::Atomic{T}, val::T) where T

Атомарно выполняет битовую операцию «и не» с x и val

Выполняет x[] = ~(x[] & val) атомарным образом. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw nand.

Примеры

julia> x = Threads.Atomic{Int}(3)
Base.Threads.Atomic{Int64}(3)

julia> Threads.atomic_nand!(x, 2)
3

julia> x[]
-3
Threads.atomic_or!(x::Atomic{T}, val::T) where T

Атомарно выполняет битовую операцию «или» с x и val.

Выполняет x[] |= val атомарным образом. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw or.

Примеры

julia> x = Threads.Atomic{Int}(5)
Base.Threads.Atomic{Int64}(5)

julia> Threads.atomic_or!(x, 7)
5

julia> x[]
7
Threads.atomic_xor!(x::Atomic{T}, val::T) where T

Атомарно выполняет битовую операцию исключающего «или» с x и val

Выполняет x[] $= val атомарным образом. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw xor.

Примеры

julia> x = Threads.Atomic{Int}(5)
Base.Threads.Atomic{Int64}(5)

julia> Threads.atomic_xor!(x, 7)
5

julia> x[]
2
Threads.atomic_max!(x::Atomic{T}, val::T) where T

Атомарно сохраняет максимальное из значений x и val в x

Выполняет x[] = max(x[], val) атомарным образом. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw max.

Примеры

julia> x = Threads.Atomic{Int}(5)
Base.Threads.Atomic{Int64}(5)

julia> Threads.atomic_max!(x, 7)
5

julia> x[]
7
Threads.atomic_min!(x::Atomic{T}, val::T) where T

Атомарно сохраняет минимальное из значений x и val в x

Выполняет x[] = min(x[], val) атомарным образом. Возвращает прежнее значение.

Дополнительные сведения см. в инструкции LLVM atomicrmw min.

Примеры

julia> x = Threads.Atomic{Int}(7)
Base.Threads.Atomic{Int64}(7)

julia> Threads.atomic_min!(x, 5)
7

julia> x[]
5
Threads.atomic_fence()

Вставляет последовательно согласованный барьер памяти.

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

Это, скорее всего, весьма дорогая операция. Учитывая, что все остальные атомарные операции в Julia уже располагают семантикой получения/выпуска, явные барьеры в большинстве случаев не требуются.

Дополнительные сведения см. в инструкции LLVM fence.

Вызов ccall с использованием пула потоков libuv (экспериментальная функция)

@threadcall((cfunc, clib), rettype, (argtypes...), argvals...)

Макрос @threadcall вызывается так же, как и ccall, но выполняет свои задачи в другом потоке. Рекомендуется использовать, если требуется вызвать блокирующую функцию С без блокирования текущего потока julia. Многопоточный режим ограничивается размером пула потоков libuv, который по умолчанию включает 4 потока, но этот лимит можно увеличить, задав переменную среды UV_THREADPOOL_SIZE и перезапустив процесс julia.

Обратите внимание, что вызванная функция не должна выполнять обратный вызов к Julia.

Примитивы низкоуровневой синхронизации

Эти стандартные блоки используются для создания объектов регулярной синхронизации.

SpinLock()

Создает нереентерабельную активную блокировку «проверить — проверить — установить». Рекурсивное использование приводит к взаимоблокировке. Такой тип блокировки можно использовать только для кода, выполнение которого не отнимает много времени и который не блокируется (например, при выполнении ввода-вывода). Как правило, вместо этого можно использовать ReentrantLock.

Каждый lock должен сопровождаться unlock. Если !islocked(lck::SpinLock) удерживается, trylock(lck) выполняется успешно, если только нет других задач, пытающихся удержать блокировку в то же самое время.

Активные блокировки «проверить — проверить — установить» являются самыми быстрыми при наличии примерно 30 конкурирующих потоков. Если конкурирующих потоков больше, следует выбрать другие методы синхронизации.