# Is the main task always sticky?

**URL:** <https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892>\
**Category:** Internals & Design\
**Created:** [November 14, 2025, 9:59pm UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892 "2025-11-14T21:59:09Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)\
**Post date:** [November 14, 2025, 9:59pm UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/1 "2025-11-14T21:59:09Z")

</div>

```julia-auto
$ julia -q
julia> current_task().sticky
true

```

It appears to currently be true, but is it always true that the main task is sticky, and will it stay true in future Julia versions?

Edit: I’m not asking because I view this as a limitation, I’m asking because I’d like to have a guarantee that it remains sticky. I need to know if I can run something on the main task and guarantee it won’t change thread.

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [November 15, 2025, 4:48am UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/2 "2025-11-15T04:48:41Z")

</div>

I’m not aware of any real-world ramifications of this design. Seems that we can spawn tasks to other threads if they are heavy.

The REPL thread can be reserved for, e.g., printing progress info, querying states (taking keyboard input) etc.

---

<div class="post-metadata">

**Author:** ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)\
**Post date:** [November 15, 2025, 7:33am UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/3 "2025-11-15T07:33:58Z")

</div>

Ah thanks but you misinferred the point of my question, have edited to clarify.

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [November 15, 2025, 9:20am UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/4 "2025-11-15T09:20:36Z")

</div>

I don’t think this is actually guaranteed. I’d be curious to see how many things implicitly rely on this though 🤔

---

<div class="post-metadata">

**Author:** ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)\
**Post date:** [November 15, 2025, 7:34pm UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/5 "2025-11-15T19:34:20Z")

</div>

I was considering explicitly relying on it 🙂 but only if it were guaranteed.

---

<div class="post-metadata">

**Author:** ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)\
**Post date:** [November 18, 2025, 5:47am UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/6 "2025-11-18T05:47:36Z")

</div>

There are several questions that you should explore before we can get to the answer.

A Task’s stickiness is not public API. There is not a public setter or getter to indicate stickiness. So an initial answer is “No” because stickiness is not even guaranteed to exist itself. It is an implementation detail. That said the [documentation](https://docs.julialang.org/en/v1/base/parallel/) does refer to it a lot more than before.

The next question is what is the public API that allows one to manipulate stickiness. It is the apparent difference between `@async` and `Threads.@spawn`.

```julia-auto
julia> @macroexpand @async println("Hello World")
quote
    #= task.jl:539 =#
    let
        #= task.jl:540 =#
        local var"#73#task" = Base.Task((()->begin
                            #= REPL[30]:1 =#
                            println("Hello World")
                        end))
        #= task.jl:541 =#
        if $(Expr(:islocal, Symbol("##sync#41")))
            #= task.jl:542 =#
            Base.put!(var"##sync#41", var"#73#task")
        end
        #= task.jl:544 =#
        Base.schedule(var"#73#task")
        #= task.jl:545 =#
        var"#73#task"
    end
end

julia> @macroexpand Threads.@spawn println("Hello World")
quote
    #= threadingconstructs.jl:484 =#
    let
        #= threadingconstructs.jl:485 =#
        local var"#75#task" = Base.Threads.Task((()->begin
                            #= REPL[31]:1 =#
                            println("Hello World")
                        end))
        #= threadingconstructs.jl:486 =#
        (var"#75#task").sticky = false
        #= threadingconstructs.jl:487 =#
        Base.Threads._spawn_set_thrpool(var"#75#task", :default)
        #= threadingconstructs.jl:488 =#
        if $(Expr(:islocal, Symbol("##sync#41")))
            #= threadingconstructs.jl:489 =#
            Base.Threads.put!(var"##sync#41", var"#75#task")
        end
        #= threadingconstructs.jl:491 =#
        Base.Threads.schedule(var"#75#task")
        #= threadingconstructs.jl:492 =#
        var"#75#task"
    end
end

```

The higher level question is will the REPL backend ever run using `Threads.@spawn`. How is the backend Task created anyways?

It turns out that the default REPL backend `Task` has an even more special property than stickiness. It is the root task! It is not created in Julia.

```julia-auto
$ ./julia -q
julia> Base.roottask == Base.current_task() true

```

However, this was not always the case. I know this because my very first pull request to core Julia was to make this true.

> <https://github.com/JuliaLang/julia/pull/35048>
>
> This change allows the REPL backend to run on the root \`Task\` rather than runnin…g using \`@async\` on a new \`Task\` as done in \`REPL.start\_repl\_backend\`. When the change is activated by setting the environmental variable \`JULIA\_ROOT\_REPL=1\`, the statement \`Base.roottask === Base.current\_task()\` will evaluate to \`true\` in the REPL.
> 
> Use of the environmental variable \`JULIA\_ROOT\_REPL\` is mainly meant to permit evaluation and testing in the near term. Movement of the backend REPL loop to \`REPL.repl\_backend\_loop\` and the addition of \`backend\_on\_current\_task\` keyword argument to \`REPL.run\_repl\` also facilitates running the REPL backend on the root Task by an outside package.
> 
> JavaCall is a package for interoperability with the Java Virtual Machine that could benefit from a REPL backend running on the root Task. Currently \`JULIA\_COPY\_STACKS=1\` is required for JavaCall.jl to function properly on Mac and Linux. On the Windows operating system, \`JULIA\_COPY\_STACKS=1\` is not needed for JavaCall and may create more issues. This approach avoids the performance degradation involved in enabling \`JULIA\_COPY\_STACKS=1\`. If the REPL backend were run on the root \`Task\`, then \`JULIA\_COPY\_STACKS=1\` would not be required to run JavaCall on the REPL.
> 
> The status quo results in a segmentation fault on Linux run trying to run JavaCall:
> \`\`\`julia
> $ ./julia --banner=no
> julia\> versioninfo()
> Julia Version 1.5.0-DEV.422
> Commit de4f8211b9\* (2020-03-08 21:42 UTC)
> Platform Info:
> OS: Linux (x86\_64-linux-gnu)
> CPU: AMD FX(tm)-8350 Eight-Core Processor
> WORD\_SIZE: 64
> LIBM: libopenlibm
> LLVM: libLLVM-9.0.1 (ORCJIT, bdver1)
> 
> julia\> import Pkg; Pkg.status()
> Status \`~/.julia/environments/v1.5/Project.toml\`
> 494afd89 JavaCall v0.7.3
> 
> julia\> using JavaCall; JavaCall.init()
> ┌ Warning: JavaCall needs the environment variable \`JULIA\_COPY\_STACKS\` to be \`1\` or \`yes\`.
> │ Calling the JVM may result in undefined behavior.
> └ @ JavaCall ~/.julia/packages/JavaCall/0wJQZ/src/JavaCall.jl:38
> Segmentation fault (core dumped)
> \`\`\`
> 
> With \`JULIA\_ROOT\_REPL = 1\` and the REPL backend running on the root \`Task\`, a segmentation fault does not occur and JavaCall functions perfectly. \`JULIA\_COPY\_STACKS\` does not need to be enabled here for JavaCall to function.
> \`\`\`julia
> $ JULIA\_ROOT\_REPL=1 ./julia --banner=no
> julia\> versioninfo()
> Julia Version 1.5.0-DEV.422
> Commit de4f8211b9\* (2020-03-08 21:42 UTC)
> Platform Info:
> OS: Linux (x86\_64-linux-gnu)
> CPU: AMD FX(tm)-8350 Eight-Core Processor
> WORD\_SIZE: 64
> LIBM: libopenlibm
> LLVM: libLLVM-9.0.1 (ORCJIT, bdver1)
> Environment:
> JULIA\_ROOT\_REPL = 1
> 
> julia\> import Pkg; Pkg.status()
> Status \`~/.julia/environments/v1.5/Project.toml\`
> 494afd89 JavaCall v0.7.3
> 
> julia\> using JavaCall; JavaCall.init()
> ┌ Warning: JavaCall needs the environment variable \`JULIA\_COPY\_STACKS\` to be \`1\` or \`yes\`.
> │ Calling the JVM may result in undefined behavior.
> └ @ JavaCall ~/.julia/packages/JavaCall/0wJQZ/src/JavaCall.jl:38
> 
> julia\> jlm = @jimport java.lang.Math
> JavaObject{Symbol("java.lang.Math")}
> 
> julia\> jcall(jlm, "sin", jdouble, (jdouble,), pi/2)
> 1.0
> 
> julia\> jcall(jlm, "sin", jdouble, (jdouble,), pi/4)
> 0.7071067811865475
> \`\`\`
> 
> Further thoughts:
> 1. Perhaps running the REPL backend on the root \`Task\` should be the default behavior. I am not aware of a situation that actually requires the backend to run under \`@async\` on a non-root \`Task\`
> 2. This approach may allow Julia to drop the need for \`JULIA\_COPY\_STACKS\`, simplifying the code base.
> 3. Packages like JavaCall could enable functions that need to be executed from the root \`Task\` to be called on non-root \`Tasks\` through a similar \`Channel\` based system. Java is fully capable of asynchronous multithreaded behavior on its own.

This is again another implementation detail, but a very fundamental one. The root task has access to the original stack without any of the libuv virtual threading. It it thus uniquely positioned for interop with other runtimes who know nothing about Julia’s Task implementation.

I became interested in this when working with JavaCall.jl. Generally, for JavaCall.jl to work you need to set the environment variable `JULIA_COPY_STACKS=1`. This changes how Julia’s task implementation works.

However, if you are running on the root task, you do not have this requirement. JavaCall.jl will work on the root task without that environment variable.

Something that did change recently is that the root task is now on the interactive thread pool.

At the end of the day, I think `Base.roottask == Base.current_task()` will be around because some interop will be impossible without it. Furthermore, this task will likely not migrate threads. That said Julia does evolve, and I’m not sure if the concept of `sticky` and “root task” will always be relevant.

What is the actual property that you require?

---

<div class="post-metadata">

**Author:** ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)\
**Post date:** [November 18, 2025, 8:22am UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/7 "2025-11-18T08:22:26Z")

</div>

The context is this PythonCall issue and related discussion:

- [Intermittent CI failures: Fatal Python error: \_Py\_CheckRecursiveCall: Unrecoverable stack overflow · Issue #694 · JuliaPy/PythonCall.jl · GitHub](https://github.com/JuliaPy/PythonCall.jl/issues/694)
- [Python 3.14.0 is incompatible with stack-switching systems. What do we do? - Core Development - Discussions on Python.org](https://discuss.python.org/t/python-3-14-0-is-incompatible-with-stack-switching-systems-what-do-we-do/104880)

Basically, the way PythonCall embeds Python into Julia violates some assumptions that CPython makes about the execution environment - in particular that the Python code is executed in a single ordinary stack on each thread. This is violated because each Julia task has its own stack, and also because tasks can migrate between threads.

This hasn’t been an issue so far but Python 3.14 has made their stack overflow detection mechanism more precise, but heavily relies on this one-stack assumption being true. The upshot is that PythonCall crashes very quickly with Python 3.14 on many platforms.

So I’m thinking about things like:

- When can we assume that the thread or stack doesn’t change?
- Do I just need to assume it can always change?
- Is it sufficient to reset/recreate the Python thread state every time I call into Python?
- Will `OncePerTask` and `OncePerThread` help?
- Do I need to make dedicated sticky tasks where the actual CPython calls occur, so the stack definitely doesn’t move?
- How slow are any of these solutions going to be?

(And to answer your question) This led me to wonder if at the very least I can rely on the main task always staying on whichever thread it started on and never having its stack moved somewhere else. It certainly sounds like this is currently true, based on your description (being the root task that is on the root stack where the Julia runtime was initialised).

I’m happy to discuss those other points somewhere else but let’s keep this thread to the original question about stickiness.

Edit to add: I have for a while been considering changing PythonCall to always acquire the GIL before every call into CPython anyway, which would mean you can safely use PythonCall from any task on any thread. This is really just a workaround for the same incompatibility issue - it’s just that things are suddenly a lot lot worse with Python 3.14.

---

<div class="post-metadata">

**Author:** ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)\
**Post date:** [November 18, 2025, 11:06pm UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/8 "2025-11-18T23:06:17Z")

</div>

I think we might be in the same situation as JavaCall.jl and QML.jl then. The requirement here is not stickiness but rather that we are actually on the root task. In near term, I think we can rely on the root task existing. Pragmatically, we really have no other option.

To help guarantee the existence of the root task as concept, please consider commenting on the following pull request to add a root task API.

> <https://github.com/JuliaLang/julia/pull/35726>
>
> Currently, the constant \`Base.roottask\` is not documented as part of the public …API per #35715. The closest API method is \`Base.current\_task()\`.
> 
> Rather than documenting \`Base.roottask\` I suggest creating \`Base.root\_task()\` whose implementation could change further down the road if needed. The name is consistent with the existing \`Base.current\_task()\`.
> 
> Currently the implementation just returns \`Base.roottask\`. Alternatively, this could call \`jl\_get\_root\_task\`, but more work might be needed to expose it.
> 
> Root \`Task\` detection is currently used by \[JavaCall.jl\](https://github.com/JuliaInterop/JavaCall.jl/blob/8d03209ab2c5e2e63709a8f4ef0cd3d4ca8c9a2d/src/jvm.jl#L256) to detect if initialization is occurring on the root \`Task\` since otherwise a segmentation fault would occur if \`JULIA\_COPY\_STACKS\` were not set to 1 or "yes".
> 
> Root \`Task\` detection may be needed to implement a work around for using \`JavaCall\` from \`@async\` per @vtjnash and @c42f : https://github.com/JuliaLang/julia/pull/35048#issuecomment-599823900 https://github.com/JuliaLang/julia/pull/35048#issuecomment-599861736

My current thought is that we should only ever call the Python C API from the Julia root task, which should have a normal looking stack as far as Python is concerned. To facilitate this, PythonCall would need to acquire the main task and then start running a task worker there to service other tasks that want to call Python.

I prototyped this concept with [GitHub - mkitti/TaskWorkers.jl: Create a worker loop that will execute arbitrary functions](https://github.com/mkitti/TaskWorkers.jl) . It will probably need an update. The idea is that we can start running the worker on the root task. If the user still needs a REPL, we boot up a new REPL whose backend is not running on the root task.

> [@cjdoris](#):
>
> - Will `OncePerTask` and `OncePerThread` help?

You may recall that I started work on an enhanced GIL-aware lock for PythonCall.jl. @jameson intervened leaving a comment about the use of `OncePerThread`. I had to pause the work at the time because I had discovered a bug in Julia 1.12’s `OncePerThread` implementation causing instability. It might be time to revisit that now that the [bug](https://github.com/JuliaLang/julia/issues/58993) has been fixed and Julia 1.12 has been released.

> <https://github.com/JuliaPy/PythonCall.jl/pull/637/files#r2189826111>
>
> The Python GIL should only be able obtained by a single Julia thread / task.
> To… enforce this, we use a ReentrantLock on the Julia side that must be obtained
> before trying to acquire the Python GIL.
> 
> Implements the fix suggested by https://github.com/JuliaPy/PythonCall.jl/issues/627 by @nhdaly and @kpamnany
> 
> Also inspired by https://stackoverflow.com/questions/47167251/pygilstate-ensure-causing-deadlock/49202868#49202868

---

<div class="post-metadata">

**Author:** ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)\
**Post date:** [November 20, 2025, 4:39pm UTC](https://discourse.julialang.org/t/is-the-main-task-always-sticky/133892/9 "2025-11-20T16:39:42Z")

</div>

Thanks @mkitti that’s all helpful stuff and food for thought.
