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

Механизм «возраста мира»

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

Совместимость

Julia 1.12 До версии Julia 1.12 механизм «возраста мира» не применялся к изменениям в глобальной таблице привязок. Информация в этой главе относится к версии Julia 1.12 и более поздним.

Warning В этой главе руководства для анализа «возраста мира» и структур данных времени выполнения в качестве средства, повышающего наглядность, используются внутренние функции. В целом, если не указано иное, механизм «возраста мира» не является стабильным интерфейсом, и с ним следует взаимодействовать в пакетах только через стабильные API (например, invokelatest). В частности, не следует предполагать, что «возраст мира» всегда является целым числом или что значения «возраста мира» следуют линейно.

Обзор «возраста мира»

«Счетчик возраста мира» — это монотонно возрастающий счетчик, который увеличивается при каждом изменении глобальной таблицы методов или глобальной таблицы привязок (например, при определении метода, определении типа, объявлении import или using, создании глобальных переменных, возможно типизированных, или определении констант).

Текущее значение глобального счетчика «возраста мира» можно получить с помощью (внутренней) функции Base.get_world_counter.

julia> Base.get_world_counter()
0x0000000000009632

julia> const x = 1

julia> Base.get_world_counter()
0x0000000000009633

Кроме того, в каждом объекте Task хранится локальный «возраст мира», определяющий то, какие изменения в глобальных таблицах привязок и методов в данный момент видны выполняющейся задаче. «Возраст мира» выполняемой задачи никогда не превышает глобальный счетчик «возраста мира», но может отставать от него на любую величину. В общем случае термин «текущий возраст мира» относится к локальному «возрасту мира» текущей выполняемой задачи. Текущий «возраст мира» можно получить с помощью (внутренней) функции Base.tls_world_age.

julia> function f end
f (generic function with 0 methods)

julia> begin
           @show (Int(Base.get_world_counter()), Int(Base.tls_world_age()))
           Core.eval(@__MODULE__, :(f() = 1))
           @show (Int(Base.get_world_counter()), Int(Base.tls_world_age()))
           f()
       end
(Int(Base.get_world_counter()), Int(Base.tls_world_age())) = (38452, 38452)
(Int(Base.get_world_counter()), Int(Base.tls_world_age())) = (38453, 38452)
ERROR: MethodError: no method matching f()
The applicable method may be too new: running in current world age 38452, while global world is 38453.

Closest candidates are:
  f() (method too new to be called from this world context.)
   @ Main REPL[2]:3

Stacktrace:
 [1] top-level scope
   @ REPL[2]:5

julia> (f(), Int(Base.tls_world_age()))
(1, 38453)

Здесь определение метода f привело к увеличению глобального счетчика, но текущий «возраст мира» не изменился. В результате определение f не было доступно в выполняемой в данный момент задаче, и возникла ошибка MethodError.

Note В выходных данных об ошибке метода содержится дополнительная информация о том, что функция f() доступна в более позднем «возрасте мира». Эта информация добавляется при отображении ошибки, а не задачей, выдавшей MethodError. Выдаваемая ошибка MethodError не зависит от того, существует ли соответствующее определение f() в более позднем «возрасте мира».

Однако следует отметить, что определение f() стало доступно при следующем приглашении к вводу данных в REPL, поскольку «возраст мира» текущей задачи увеличился. В целом некоторые синтаксические конструкции (в частности, большинство определений) повышают «возраст мира» текущей задачи до последнего глобального «возраста мира», тем самым делая видимыми все изменения (как из текущей задачи, так и из любых одновременно выполняющихся задач). Текущий «возраст мира» повышают следующие операторы:

  1. Явный вызов Core.@latestworld

  2. Начало каждого оператора верхнего уровня

  3. Начало каждой команды REPL

  4. Любое определение типа или структуры

  5. Любое определение метода

  6. Любое объявление константы

  7. Любое объявление глобальной переменной (но не присваивание значения глобальной переменной)

  8. Любой оператор using, import, export или public

  9. Некоторые другие макросы, например @eval (зависит от реализации макроса)

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

julia> f() = Core.@latestworld
ERROR: syntax: World age increment not at top level
Stacktrace:
 [1] top-level scope
   @ REPL[5]:1

Если это не так (например, в случае с @eval), побочный эффект «возраста мира» игнорируется.

Вследствие этих правил Julia может предполагать, что «возраст мира» не изменяется в ходе выполнения обычной функции.

function my_function()
    before = Base.tls_world_age()
    # Любой произвольный код
    after = Base.tls_world_age()
    @assert before === after # всегда true
end

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

Временное повышение «возраста мира» с помощью invokelatest

Как описывалось выше, «возраст мира» невозможно окончательно увеличить на оставшуюся часть выполнения объекта Task, если только задача не выполняет операторы верхнего уровня. Однако с помощью invokelatest можно временно изменить «возраст мира» в ограниченной области:

julia> function f end
f (generic function with 0 methods)

julia> begin
           Core.eval(@__MODULE__, :(f() = 1))
           invokelatest(f)
       end
1

invokelatest временно повысит «возраст мира» текущей задачи до последнего глобального «возраста мира» (на момент входа в invokelatest) и выполнит предоставленную функцию. Обратите внимание, что после выхода из invokelatest «возраст мира» вернется к предыдущему значению.

«Возраст мира» и переопределения константных структур

Описанная выше семантика переопределения методов также применима к переопределению констант:

julia> const x = 1
1

julia> get_const() = x
get_const (generic function with 1 method)

julia> begin
           @show get_const()
           Core.eval(@__MODULE__, :(const x = 2))
           @show get_const()
           Core.@latestworld
           @show get_const()
       end
get_const() = 1
get_const() = 1
get_const() = 2
2

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

julia> global y = 1
1

julia> get_global() = y
get_global (generic function with 1 method)

julia> begin
           @show get_global()
           Core.eval(@__MODULE__, :(y = 2))
           @show get_global()
       end
get_global() = 1
get_global() = 2
2

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

julia> struct MyStruct
           x::Int
       end

julia> const one_field = MyStruct(1)
MyStruct(1)

julia> struct MyStruct
           x::Int
           y::Float64
       end

julia> const two_field = MyStruct(1, 2.0)
MyStruct(1, 2.0)

julia> one_field
@world(MyStruct, 38452:38455)(1)

julia> two_field
MyStruct(1, 2.0)

На внутреннем уровне два определения MyStruct представляют собой совершенно разные типы. Однако после определения нового типа MyStruct для исходного определения MyStruct больше не существует привязки по умолчанию. Тем не менее для облегчения доступа к этим типам можно использовать специальный макрос @world, позволяющий получить доступ к значению имени в предыдущем «мире». Однако этот инструмент предназначен исключительно для внутреннего анализа. В частности, следует отметить, что значения «возраста мира» нестабильны во время предварительной компиляции и, как правило, должны рассматриваться непрозрачно.

Интроспекция разделения привязок

В некоторых случаях может быть полезно проанализировать интерпретацию системой того, что означает привязка в том или ином «возрасте мира». При выводе данных по умолчанию о Core.Binding отображается полезная сводка (например, о MyStruct из приведенного выше примера):

julia> convert(Core.Binding, GlobalRef(@__MODULE__, :MyStruct))
Binding Main.MyStruct
   38456:∞ - constant binding to MyStruct
   38452:38455 - constant binding to @world(MyStruct, 38452:38455)
   38451:38451 - backdated constant binding to @world(MyStruct, 38452:38455)
   0:38450 - backdated constant binding to @world(MyStruct, 38452:38455)

«Возраст мира» и using/import

Привязки, предоставляемые посредством using и import, также работают через механизм «возраста мира». Разрешение привязок — это функция без сохранения состояния определений import и using, видимых в текущем «возрасте мира». Например:

julia> module M1; const x = 1; export x; end

julia> module M2; const x = 2; export x; end

julia> using .M1

julia> x
1

julia> using .M2

julia> x
ERROR: UndefVarError: `x` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.

julia> convert(Core.Binding, GlobalRef(@__MODULE__, :x))
Binding Main.x
   38458:∞ - ambiguous binding - guard entry
   38457:38457 - implicit `using` resolved to constant 1

Получение «возраста мира»

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

julia> const x = 1

julia> t = @task (wait(); println("Running now"); x);

julia> const x = 2

julia> schedule(t);
Running now

julia> x
2

julia> fetch(t)
1

Помимо задач, непрозрачные замыкания также получают «возраст мира» при создании. См. описание Base.Experimental.@opaque.

@world(sym, world)

Resolve the binding sym in world world. See invoke_in_world for running arbitrary code in fixed worlds. world may be UnitRange, in which case the macro will error unless the binding is valid and has the same value across the entire world range.

As a special case, the world always refers to the latest world, even if that world is newer than the world currently running.

The @world macro is primarily used in the printing of bindings that are no longer available in the current world.

Example

julia> struct Foo; a::Int; end
Foo

julia> fold = Foo(1)

julia> Int(Base.get_world_counter())
26866

julia> struct Foo; a::Int; b::Int end
Foo

julia> fold
@world(Foo, 26866)(1)
Совместимость

Julia 1.12 This functionality requires at least Julia 1.12.

get_world_counter()

Returns the current maximum world-age counter. This counter is global and monotonically increasing.

tls_world_age()

Returns the world the current_task() is executing within.

invoke_in_world(world, f, args...; kwargs...)

Call f(args...; kwargs...) in a fixed world age, world.

This is useful for infrastructure running in the user’s Julia session which is not part of the user’s program. For example, things related to the REPL, editor support libraries, etc. In these cases it can be useful to prevent unwanted method invalidation and recompilation latency, and to prevent the user from breaking supporting infrastructure by mistake.

The global world age can be queried using Base.get_world_counter() and stored for later use within the lifetime of the current Julia session, or when serializing and reloading the system image.

Technically, invoke_in_world will prevent any function called by f from being extended by the user during their Julia session. That is, generic function method tables seen by f (and any functions it calls) will be frozen as they existed at the given world age. In a sense, this is like the opposite of invokelatest.

Note It is not valid to store world ages obtained in precompilation for later use. This is because precompilation generates a "parallel universe" where the world age refers to system state unrelated to the main Julia session.

@opaque ([type, ]args...) -> body

Marks a given closure as "opaque". Opaque closures capture the world age of their creation (as opposed to their invocation). This allows for more aggressive optimization of the capture list, but trades off against the ability to inline opaque closures at the call site, if their creation is not statically visible.

An argument tuple type (type) may optionally be specified, to specify allowed argument types in a more flexible way. In particular, the argument type may be fixed length even if the function is variadic.

Warning This interface is experimental and subject to change or removal without notice.