Conversation
This comment has been minimized.
This comment has been minimized.
|
Just want to say that I really like this approach! ❤️ I especially like the design of making Regarding async logic implemented in Base, what is the intended policy for whether to cancel just the waiter, or waiter+waitee? Should we expect that all async resources from Base will cancel on/within any async call, for predictability? That is to say, if we have a producer-consumer setup on a Regarding more than just ^C, can we expect that Regarding timeouts and other structured cancellation, will it be possible to target a cancellation request at a particular task? I can imagine that this will avoid having to implement APIs like Aside: I do think it's worth thinking more on whether we want users to have a way to target the cancellation at a library/task/arbitrary machinery, but as mentioned, this is mostly an orthogonal concern. |
Kind of. The canceler checks
Yes
Policy decision by the async library, so I'm not really expressing a preference. For now,
I was at this point not expecting cancellation to propagate through channels and conditions - rather I was expecting that it would cancel the wait on those objects and then the thrown exception might potentially cancel the expected producer in its cleanup scope - however, that's a bit of an orthogonal API design question that I don't have a strong opinion on.
Maybe - I could imagine SIGTERM trying to cancel all tasks in the system simultaneously with this mechanism - I don't know if the tree-based cancellation makes sense there, but it could be useful for graceful shutdown.
PR provides a |
|
I guess I should have said that I want the cancellation point to be a preemption point rather than a yield point. We don't currently have that concept, so a bit of an open question whether those are different, but I wanted to be precise. |
|
Capturing some slack discussion with @vtjnash. This reflects my best understanding, but @vtjnash was trying to make a larger point that I don't quite understand.
Probably not. The correctness of these functions depends on being paired with a unique @vtjnash provided the example
I think we need to have both versions, with the user selecting the appropriate one.
I think some variant of the following works: Cancelling thread: Waiting thread:
I think this is a cancellation point and the thread dies. This is different from
I think it can be weakref.
Probably should be fixed, ignore for now.
Not attached to the term. The naming was due to the possibility of introducing more unsafe cancellation variants (to be used on timeout or repeated ^C) that, while being more likely to succeed, could leave the system in an inconsistent state. Useful for looking around for debugging, but not semantically sound. |
|
Now with compiler and reset_ctx support, courtesy of claude (note the absence of explicit cancellation points inside the inner loop): |
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
The `preserve_none` calling convention is a new calling convention in clang (>= 19) and gcc that preserves a more minimal set of registers (rsp, rbp on x86_64; lr, fp on aarch64). As a result, if this calling convention is used with setjmp, those registers do not need to be stored in the setjmp buffer, allowing us to reduce the size of this buffer and use fewer instructions to save the buffer. The tradeoff of course is that these registers may need to be saved anyway, in which case both the stack usage and the instructions just move to the caller (which is strictly worse). It is not clear that this is useful for exceptions (which already have a fair bit of state anyway, so even in the happy path the savings are not necessarily that big), but I am thinking about using it for #60281, which has different characteristics, so this is an easy way to try out whether there are any unexpected challenges. Note that preserve_none is a very recent compiler feature, so most compilers out there do not have it yet. For compatibility, this PR supports using different jump buffer formats in the runtime and the generated code.
| return FunctionType::get(getInt32Ty(C), {}, false); | ||
| }, | ||
| [](LLVMContext &C) { return AttributeList::get(C, | ||
| Attributes(C, {Attribute::ReturnsTwice}), |
There was a problem hiding this comment.
All stores in the function after this call but before a matching longjmp need to changed to use volatile=true if they can be observed after the longjmp
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
The ability to request cancellation of a pending or in-progress write has been requested several times in libuv (at least joyent/libuv#1393 and libuv#2051), but was not yet implemented. I am currently on a mission to make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281). Of course, this would require the ability to cancel in-progress writes for sane semantics, so I have a renewed interest in this feature, which this PR attempts to implement. One primary problem with the API is that the existing callback does not provide support for passing in the number of written bytes (since it is expected to always complete). I see two options: 1. Create a new uv_write3 that takes a new cb type that does take this argument, or, 2. Create a new public `uv_write_t` field that this information can be read from. This PR takes the first approach, but as it turns out the number of bytes written needs to be stored anyway, so maybe there is not much point to this choice. As mentioned, we do need to store two extra bits of state (the total number of bytes written and which kind of callback we have). To maintain ABI compatibility we steal two pointers from the generic req_t reserve pool. This preserves ABI compatibility, but is a bit awkward, because it puts these new private fields into a different place than they would otherwise be. I don't see that we have much choice though, other than creating a completely new req_t subtype (which does not seem worth it). The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the request was submitted using the `uv_write`/`uv_write2` API that does not take the new callback signature (unless no bytes have been written, in which case cancellation succeeds). It also returns `0` if the request is already done. The thought here was that it would be too racy to return an error code here, but I think it would be fine to return EALREADY (although I don't know what the caller would do with that information. `uv_write3` also takes a flags parameter reserved for future use (just in case). The windows side is relatively straightforward in that the only thing we really need to do is call `CancelIoEx` on the overlapped structure that's already inside of our req. We do of course need the appropriate accounting for the number of bytes written. Discloure: Claude Code was used in the creation of this PR, although dumb design decisions are probably by me. I have done some minimal integrated testing of this on Linux. On windows, I have run the included test, but have not tested in-situ. I consider this WIP until I've had a chance to run our full test suite on all platforms, but I wanted to make sure to open this early for API feedback/concerns.
The ability to request cancellation of a pending or in-progress write has been requested several times in libuv (at least joyent/libuv#1393 and libuv#2051), but was not yet implemented. I am currently on a mission to make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281). Of course, this would require the ability to cancel in-progress writes for sane semantics, so I have a renewed interest in this feature, which this PR attempts to implement. One primary problem with the API is that the existing callback does not provide support for passing in the number of written bytes (since it is expected to always complete). I see two options: 1. Create a new uv_write3 that takes a new cb type that does take this argument, or, 2. Create a new public `uv_write_t` field that this information can be read from. This PR takes the first approach, but as it turns out the number of bytes written needs to be stored anyway, so maybe there is not much point to this choice. As mentioned, we do need to store two extra bits of state (the total number of bytes written and which kind of callback we have). To maintain ABI compatibility we steal two pointers from the generic req_t reserve pool. This preserves ABI compatibility, but is a bit awkward, because it puts these new private fields into a different place than they would otherwise be. I don't see that we have much choice though, other than creating a completely new req_t subtype (which does not seem worth it). The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the request was submitted using the `uv_write`/`uv_write2` API that does not take the new callback signature (unless no bytes have been written, in which case cancellation succeeds). It also returns `0` if the request is already done. The thought here was that it would be too racy to return an error code here, but I think it would be fine to return EALREADY (although I don't know what the caller would do with that information. `uv_write3` also takes a flags parameter reserved for future use (just in case). The windows side is relatively straightforward in that the only thing we really need to do is call `CancelIoEx` on the overlapped structure that's already inside of our req. We do of course need the appropriate accounting for the number of bytes written. Discloure: Claude Code was used in the creation of this PR, although dumb design decisions are probably by me. I have done some minimal integrated testing of this on Linux. On windows, I have run the included test, but have not tested in-situ. I consider this WIP until I've had a chance to run our full test suite on all platforms, but I wanted to make sure to open this early for API feedback/concerns.
The ability to request cancellation of a pending or in-progress write has been requested several times in libuv (at least joyent/libuv#1393 and libuv#2051), but was not yet implemented. I am currently on a mission to make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281). Of course, this would require the ability to cancel in-progress writes for sane semantics, so I have a renewed interest in this feature, which this PR attempts to implement. One primary problem with the API is that the existing callback does not provide support for passing in the number of written bytes (since it is expected to always complete). I see two options: 1. Create a new uv_write3 that takes a new cb type that does take this argument, or, 2. Create a new public `uv_write_t` field that this information can be read from. This PR takes the first approach, but as it turns out the number of bytes written needs to be stored anyway, so maybe there is not much point to this choice. As mentioned, we do need to store two extra bits of state (the total number of bytes written and which kind of callback we have). To maintain ABI compatibility we steal two pointers from the generic req_t reserve pool. This preserves ABI compatibility, but is a bit awkward, because it puts these new private fields into a different place than they would otherwise be. I don't see that we have much choice though, other than creating a completely new req_t subtype (which does not seem worth it). The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the request was submitted using the `uv_write`/`uv_write2` API that does not take the new callback signature (unless no bytes have been written, in which case cancellation succeeds). It also returns `0` if the request is already done. The thought here was that it would be too racy to return an error code here, but I think it would be fine to return EALREADY (although I don't know what the caller would do with that information. `uv_write3` also takes a flags parameter reserved for future use (just in case). The windows side is relatively straightforward in that the only thing we really need to do is call `CancelIoEx` on the overlapped structure that's already inside of our req. We do of course need the appropriate accounting for the number of bytes written. Discloure: Claude Code was used in the creation of this PR, although dumb design decisions are probably by me. I have done some minimal integrated testing of this on Linux. On windows, I have run the included test, but have not tested in-situ. I consider this WIP until I've had a chance to run our full test suite on all platforms, but I wanted to make sure to open this early for API feedback/concerns.
The ability to request cancellation of a pending or in-progress write has been requested several times in libuv (at least joyent/libuv#1393 and libuv#2051), but was not yet implemented. I am currently on a mission to make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281). Of course, this would require the ability to cancel in-progress writes for sane semantics, so I have a renewed interest in this feature, which this PR attempts to implement. One primary problem with the API is that the existing callback does not provide support for passing in the number of written bytes (since it is expected to always complete). I see two options: 1. Create a new uv_write3 that takes a new cb type that does take this argument, or, 2. Create a new public `uv_write_t` field that this information can be read from. This PR takes the first approach, but as it turns out the number of bytes written needs to be stored anyway, so maybe there is not much point to this choice. As mentioned, we do need to store two extra bits of state (the total number of bytes written and which kind of callback we have). To maintain ABI compatibility we steal two pointers from the generic req_t reserve pool. This preserves ABI compatibility, but is a bit awkward, because it puts these new private fields into a different place than they would otherwise be. I don't see that we have much choice though, other than creating a completely new req_t subtype (which does not seem worth it). The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the request was submitted using the `uv_write`/`uv_write2` API that does not take the new callback signature (unless no bytes have been written, in which case cancellation succeeds). It also returns `0` if the request is already done. The thought here was that it would be too racy to return an error code here, but I think it would be fine to return EALREADY (although I don't know what the caller would do with that information. `uv_write3` also takes a flags parameter reserved for future use (just in case). The windows side is relatively straightforward in that the only thing we really need to do is call `CancelIoEx` on the overlapped structure that's already inside of our req. We do of course need the appropriate accounting for the number of bytes written. Discloure: Claude Code was used in the creation of this PR, although dumb design decisions are probably by me. I have done some minimal integrated testing of this on Linux. On windows, I have run the included test, but have not tested in-situ. I consider this WIP until I've had a chance to run our full test suite on all platforms, but I wanted to make sure to open this early for API feedback/concerns.
The ability to request cancellation of a pending or in-progress write has been requested several times in libuv (at least joyent/libuv#1393 and libuv#2051), but was not yet implemented. I am currently on a mission to make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281). Of course, this would require the ability to cancel in-progress writes for sane semantics, so I have a renewed interest in this feature, which this PR attempts to implement. One primary problem with the API is that the existing callback does not provide support for passing in the number of written bytes (since it is expected to always complete). I see two options: 1. Create a new uv_write3 that takes a new cb type that does take this argument, or, 2. Create a new public `uv_write_t` field that this information can be read from. This PR takes the first approach, but as it turns out the number of bytes written needs to be stored anyway, so maybe there is not much point to this choice. As mentioned, we do need to store two extra bits of state (the total number of bytes written and which kind of callback we have). To maintain ABI compatibility we steal two pointers from the generic req_t reserve pool. This preserves ABI compatibility, but is a bit awkward, because it puts these new private fields into a different place than they would otherwise be. I don't see that we have much choice though, other than creating a completely new req_t subtype (which does not seem worth it). The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the request was submitted using the `uv_write`/`uv_write2` API that does not take the new callback signature (unless no bytes have been written, in which case cancellation succeeds). It also returns `0` if the request is already done. The thought here was that it would be too racy to return an error code here, but I think it would be fine to return EALREADY (although I don't know what the caller would do with that information. `uv_write3` also takes a flags parameter reserved for future use (just in case). The windows side is relatively straightforward in that the only thing we really need to do is call `CancelIoEx` on the overlapped structure that's already inside of our req. We do of course need the appropriate accounting for the number of bytes written. Discloure: Claude Code was used in the creation of this PR, although dumb design decisions are probably by me. I have done some minimal integrated testing of this on Linux. On windows, I have run the included test, but have not tested in-situ. I consider this WIP until I've had a chance to run our full test suite on all platforms, but I wanted to make sure to open this early for API feedback/concerns.
Asymmetric atomic fences are a performance optimization of regular atomic fences (the seq_cst version of which we expose as `Base.Threads.atomic_fence`). The problem with these regular fences is that they require a CPU fence instruction, which can be very expensive and is thus unsuitable for code in the hot path. Asymmetric fences on the other hand split an ordinary fence into two: A `light` side where the fence is extremely cheap (only a compiler reordering barrier) and a `heavy` side where the fence is very expensive. Basically the way it works is that the heavy side does a system call that issues an inter-processor-interrupt (IPI) which then issues the appropriate barrier instruction on the other CPU (i.e. both CPUs will have issues a barrier instruction, one of them just does it asynchronously due to interrupt). The `light` and `heavy` naming here is taken from C++ PR1202R5 [1], which is the proposal for the same feature in the C++ standard library (to appear in the next iteration of the C++ concurrency spec). On the julia side, these functions are exposed as `Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic atomic_fence is adjusted appropriately to faciliate this). The heavy side has OS-specifc implementations, where: 1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to `mprotect` for systems that don't have it. 2. Windows uses the `FlushProcessWriteBuffers` syscall. 3. macOS uses an implementation from the dotnet runtime (dotnet/runtime#44670), which the dotnet folks have checked with Apple does the right thing by happenstance (i.e. an IPI/memory barrier is needed to execute the syscall), but looks a little nonsensical by itself. However, since it's what Apple recommended to dotnet, I don't see much risk here, though I wouldn't be surprised if Apple added a proper syscall for this in the future (since freebsd has it now). Note that unlike the C++ spec, I have specified that `atomic_fence_heavy` does synchronize with `atomic_fence`. This matches the underlying system call. I suspect C++ chose to omit this for a hypothetical future architecture that has instruction support for doing this from userspace that would then not synchronize with ordinary barriers, but I think I would rather cross that bridge when we get there. I intend to use this in #60281, but it's an independently useful feature. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf --------- Co-authored-by: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Co-authored-by: Keno Fischer <Keno@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The vector-write override that carries the buffer's owner for detached write rooting is a LibuvStream method, but BufferStream - a LibuvStream subtype with in-memory internals and its own unsafe_write - was caught by it and failed on the missing send-buffer field. Writes to a BufferStream complete synchronously, so no uv request can outlive the caller and the detached-owner bookkeeping does not apply: give it a vector-write method that forwards to its own unsafe_write. Exposed by the process-stdio forwarder writing chunks into a BufferStream stdio target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The published GMP_jll now carries gmp-mpz_realloc.patch (via JuliaPackaging/Yggdrasil#14191), closing the double-free window that task cancellation could open through the mpz reallocation paths, so the temporary source-build force is no longer needed. Validated the JLL artifact against the cancellation double-free reproducer and the gmp testset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The C side of ^C delivery marks the episode source directly, but a task running under a scoped child source (e.g. inside @sync) polls its own source at cancellation points - carrying the episode's cancellation down to it required the julia-side sigint listener's tree walk. In a single-threaded process (julia's default) whose only thread runs a compute-bound task, the listener never gets scheduled, so the first ^C never landed and only the escalation ladder could get through. The per-thread cancellation-delivery paths - which every ^C already triggers on every thread - now carry the pending episode into the interrupted task's bound source themselves: if the source is governed by the episode source (parent-chain membership), the episode's state byte is CAS-maxed into it, a single async-signal-safe write, and the task's next cancellation point observes it with no julia-side scheduling at all. The listener's eventual walk redoes the remaining bookkeeping level-triggered. Three gates had to open for this to work: the dispatch-pending flag is set before the per-thread sends (the handlers race it otherwise), and the senders no longer require a published interruptible-region context when a ^C dispatch is pending and the task carries a binding - a purely polling victim between cancellation points never has one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
…nowledged When the C-side fast path delivers a ^C entirely on its own - the episode source marked from the signal thread and the cancellation acknowledged by a polling task - no tree walk has run, so tasks parked under the episode source (a sleeping @sync sibling, for example) have not been woken. The listener's acknowledged-fast-path then skipped the walk too, and the parked task waited forever. A wakeup that carries a real press now redelivers even when the active severity is already acknowledged: the walk is level-triggered and idempotent, waking whoever is still parked; only spurious wakeups without a pending press short-circuit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
CI test workers run with JULIA_NUM_THREADS=1, where three testsets could never finish: their spin-loop victims only terminate through a cross-thread action (an abandon commit or a cancel! issued by the canceller task) that a single thread can never perform, wedging the whole test file. Move them into cancellation_exec.jl, the -t2 subprocess whose charter is exactly the thread-dependent cancellation tests, and give the abandon-refusal victim a defensive shutdown so a platform that never commits fails cleanly instead of spinning. The remaining single-threaded behaviors are legitimate and now asserted as such: the escalation ladder's listener rungs cannot run while the victim monopolizes the only thread, so a repeat press re-offers the first rung and the third press reaches the C-side direct abandonment - the pty ladder test now branches its expectations on the child's thread count (with an order-tolerant expectation for the endgame's interleaved messages), and the catch-all swallow test accepts the direct path's announcement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
CI exposed three timing holes in the direct (C-side) abandonment rung of the ^C escalation, all reproducible locally by pinning the process to a single CPU: - The "Abandoned the current task" announcement was printed after performing the abandonment. The moment the victim thread switches to the rescue task, session cleanup can conclude - in a script it exits the process - and on a busy machine that exit reliably won the race against the message, leaving no trace of why the process died. Announce before abandoning instead. - A press that found the victim transiently inside the allocator or the GC (gc_state != 0 or runtime locks held) consumed the rescue-timer expiry and fell through to a plain re-request, silently discarding the escalation rung. Wait up to 100ms for the victim to return to an abandonable state before deciding. - With more than one worker thread, an unclaimed dispatch notification only means the sigint listener has not run *yet*, not that it cannot run - the direct rung could fire on a loaded machine and rip away whatever thread 0 happened to be running, bypassing the graded escalation; and since an unbound current task passed the "governed" check, that could even be runtime infrastructure such as the listener itself, wedging the session. Gate the delivered-episode arm on the session having exactly one worker thread, and require the abandoned task to be bound under the ^C episode source. Fixes the cancellation test failures on the aarch64, mmtk, and rr jobs of https://buildkite.com/julialang/julia-pr/builds/351. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The owner-carrying write path called `uv_write(s, p, n; owner)` with a `@nospecialize`d owner, producing a keyword call whose NamedTuple type is abstract - an unresolvable dynamic call that fails `--trim` verification (seen as the JuliaC test failures on all platforms of https://buildkite.com/julialang/julia-pr/builds/351). Route the keyword method and the internal caller through a positional core instead. Also add `unsafe_write(::TTY, ::Ptr{UInt8}, ::UInt)` to the hardcoded sysimage precompiles: it fell out of the image when it gained the `cancel` keyword, and REPL startup now compiled it fresh, tripping the REPL precompilation test's zero-fresh-precompiles budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
Address the remaining test-side failures from https://buildkite.com/julialang/julia-pr/builds/351: - core.jl: add Task's new atomic fields (wait_state, bound_cancel_token, preempt_request) to the atomic-field allowlist. - threads.jl: lock parking now links through the dedicated `lock_queue` Task field, not `wait_queue` - check both in `parked_on`, and give the park wait a CI-sized timeout. - cancellation.jl: gate the "^C" testset on unix (uv_kill(SIGINT) on Windows terminates the child outright), skip fd polling of a pipe on Windows (ENOTSOCK), synchronize run_with_sigint on a readiness marker so a slow-starting child is not killed by the first SIGINT before its handlers are armed, raise the exec-subprocess watchdog to 600s, and match the direct abandonment's announce-before-abandon wording. - cancellation_exec.jl: size the BLAS cancellation baseline adaptively instead of three fixed 12000^2 gemms, which alone could outlast the parent's watchdog on an oversubscribed machine. - misc.jl: a repeat SIGINT can land while the first one's error report is being displayed, cancelling the report itself - accept the fallback note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The previous trim fix routed the owner-carrying write path through a positional core, but the call into `uv_write_noncancel` still compiled as a generic (trim-unresolvable) call: a default value on a `@nospecialize` parameter drops the `@nospecialize` from the generated full-arity method, so a caller whose `owner` is deliberately untyped cannot devirtualize the call. Write the optional-argument forwarders out by hand so the full-arity methods keep their bare `@nospecialize`, and split the token branch at the call site so every path stays concretely resolvable. Verified locally against the JuliaC TrimmabilityProject: the trim verify is clean and the trimmed executable builds and runs. Fixes the JuliaC test failures on all platforms of https://buildkite.com/julialang/julia-pr/builds/389. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
SuspendThread is asynchronous: the victim keeps executing until a GetThreadContext forces the suspension to complete. Both Windows delivery senders validated - and the cancellation sender consumed - the published region context between SuspendThread and GetThreadContext, i.e. while the victim could still run. With concurrent senders (cancel!, preemption, the sigint dispatch all send best-effort signals), a second sender could pass validation on a stale reset context after the first delivery had already made the victim throw and unwind, then SetThreadContext the thread into a dead frame's garbage - observed as the cancellation_exec.jl -t2 child dying with EXCEPTION_ACCESS_VIOLATION at a garbage ip (0xa5a3) in the reset_ctx testset on both Windows arches. Freeze the thread first (SuspendThread + GetThreadContext), then load, validate, and consume the region context. Overlapping suspensions nest, so a concurrent sender now either observes the consumed (NULL) context and skips, or redirects to the same still-frozen, still-valid reset point. The abandon sender gets the same ordering: jl_abandon_try_commit now really does validate against frozen state, as its contract always claimed. CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-6692-42d9-8ea5-5d1430478856 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
…very Two delivery-robustness fixes found chasing the cancellation test failures on the slower CI platforms: FreeBSD selects the POSIX-timer rescue mechanism, whose SIGINT is told apart from a user press by sigwaitinfo's SI_TIMER si_code - but every sigwaitinfo use was guarded by _POSIX_C_SOURCE >= 199309L, which glibc defines through _GNU_SOURCE and FreeBSD does not define at all. The signal listener there fell back to plain sigwait, so a rescue-timer expiry was indistinguishable from a user ^C: no escalation warning could ever print, the expiry was never recorded (so the direct-abandonment gate never opened), and each phantom "press" re-armed the timer. That left the REPL pty ladder on FreeBSD completely dead after the first press. Gate these paths on a new HAVE_SIGWAITINFO that includes FreeBSD, where sigwaitinfo is available regardless of the feature macro. jl_abandon_task's delivery-retry loop was bounded by iteration count (200 sends x 10 x uv_sleep(1)), nominally ~2s - but every uv_sleep is a reschedule, and on an oversubscribed or serialized machine (CI under load, rr) the loop stretched to minutes per call. The unsafe_abandon! testsets spam that call in a 20s wall-clock loop, blowing through the parent's subprocess watchdog on the rr job. Bound the loop by wall clock instead. Relatedly, jl_thread_suspend_and_get_state asserted the per-thread signal request slot held no request, but a parked best-effort cancellation (5) or abandon (6) whose victim never consumes signals (e.g. SIGUSR2 blocked) can occupy it indefinitely; both senders tolerate a displaced delivery. Seen as an abort in assert builds when a termination signal arrived while an abandon request was pending. CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-6691-42e6-aa26-0fe946612248 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
Asynchronous delivery of foreign-call cancellation handlers exists only where JL_HAVE_CANCEL_HANDLER_DELIVERY is defined (linux/darwin x86_64/aarch64, windows x86_64). On i686-linux, freebsd, and win32 the handler-stopped foreign spins in the cancellation_exec.jl testsets can never terminate: the spin waits for a handler that is never delivered, the unstoppable victims pin both worker threads, and the -t2 child wedges until the parent watchdog SIGKILLs it. Expose the capability as jl_have_cancel_handler_delivery() and skip the handler-delivery subtests and the BLAS cancellation testset where it is absent (the pre-call pending-cancellation check works everywhere and stays). Also make both REPL pty testsets kill their session if it fails to exit - success(p) on a wedged session previously hung the whole test file until the outer driver timeout (2h on the freebsd job). CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-668e-4feb-a5cb-69e7bcf86fc5 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
…weep
The all-methods return_types(isequal, Tuple{Any,Any}) sweep from #49800
asserts over every isequal method in the session - including ones added
by OTHER test files sharing the worker process. test/arrayops.jl defines
isequal(::totally_not_five26034, ::Number) (and its mirror), which infer
Bool in isolation but degrade to Any once the LinearAlgebra tests have
inflated the ==/Number method tables earlier in the same process. The
failure therefore appears whenever the scheduler happens to run arrayops
(after the LinearAlgebra tests) before missing on one worker -
deterministically reproducible with
JULIA_CPU_THREADS=1 julia test/runtests.jl LinearAlgebra/triangular2 \
LinearAlgebra/structuredbroadcast triplequote intrinsics iobuffer \
staged arrayops combinatorics euler client terminfo errorshow \
goto llvmcall some docs interpreter floatfuncs missing
Restrict the assertion to methods owned by Base and the stdlibs (module
root not Main); in a fresh process this still covers all 27 shipped
methods.
CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-668b-4c95-81ed-e80a15899a30
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The cancellation-lowering pass skipped every atomic store when collecting unsafe points, as a proxy for recognizing its own reset_ctx bookkeeping stores, and never considered cmpxchg/atomicrmw at all. A reset region thus stayed published across user atomic operations, so a SAFE cancellation delivered around e.g. a spin-lock acquire (cmpxchg) or release (atomic store) would longjmp away and leave the lock owned forever. Mark the pass's own stores with the julia.reset_safe metadata instead, and treat unmarked atomic stores and read-modify-write operations as unsafe points that clear the published reset region. Reported in #60281 (comment): #60281 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
A cancelled `@sync` awaits its children in sync_cancel!, parking each teardown wait with min_severity one above the acknowledged request so only an escalation can interrupt it. That interruption unwound straight out of the `@sync`, so a SAFE request escalated to ABANDON_EXTERNAL released the block while internal compute-bound children were still running, contradicting the documented contract that ABANDON_EXTERNAL keeps awaiting internal tasks. Adopt the escalated request instead: re-enter the teardown wait under the new severity's policy, and report the strongest request when the teardown completes. ABANDON_ALL still ceases waiting (the children are frozen). Reported in #60281 (comment): #60281 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
An abandon request could commit while the victim sat between the scheduler's sleep_check_state transition and the actual sleep (e.g. inside jl_task_get_next's trypoptask/checkempty callbacks). Abandonment bypasses the JL_CATCH that restores the sleep state and running count on unwind, so the rescue task re-entered the scheduler with the sleep bookkeeping still claimed and tripped the not_sleeping assertion in jl_task_get_next. Add sleep_check_state to the abandon refusal set: like held runtime locks, a claimed sleep slot is runtime state that the discarded context cannot be allowed to leak. Reported in #60281 (comment): #60281 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
Pick up JuliaLang/libuv#46, which carries libuv/libuv#4966: uv_cancel() support for in-flight write requests plus the uv_write_nwritten() query, needed by the upcoming Julia cancellation work (JuliaLang/julia#60281). On Windows the library now uses WaitOnAddress, linked via libsynchronization (already present in the checked-in configure). Co-authored-by: Keno Fischer <Keno@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bump the libuv pin to the current julia-uv2-1.48.0 head, picking up JuliaLang/libuv#46, which carries libuv/libuv#4966: in-flight write requests can now be cancelled through uv_cancel, and the new uv_write_nwritten accessor reports how much of a (possibly cancelled) write was actually submitted. This is the libuv-side half of cancellable stream writes for the task cancellation work (JuliaLang#60281). On Windows the reworked pipe writer thread waits with WaitOnAddress/WakeByAddressSingle from libsynchronization; libuv's own build links it, but everything in this repository that links the static libuv.a (flisp, libjulia, the test libraries) takes its Windows system libraries from WIN_SYSTEM_LIBS, so add it there. It is resolved to a full path via $(CC) -print-file-name (like the pre-existing libatomic handling) because those raw-ld links do not search the toolchain's own library directories, where some environments (e.g. msys2 mingw32) keep the import library. The source build is temporarily forced until a LibUV_jll built from the pinned commit is published. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
When a parked task is interrupted (`schedule(t, exc, error=true)`), the waking party must both win the right to reschedule the task against any concurrent `notify` and remove the task's wait-queue registration, which requires the waitee's lock - and reacquiring that lock may itself need to park. Master handles this with an unsynchronized `list_deletefirst!` on a foreign task's queue, which is only safe under an ownership discipline that cancellation precisely cannot satisfy. This replaces the task-intrusive wait queues with per-wait heap registrations (`WaitEntry`, one cached per task so the common park does not allocate) and a single-word wake-claim protocol on the new atomic `Task.waiting_on` field: `notify` claims a popped entry with an expected-entry CAS (making stale entries of interrupted waits harmless to skip), interrupters claim with an unconditional swap and leave the entry for lazy unlinking by the waiter's own cleanup or a later notify, and wake sources aimed at one specific wait (`wait_with_timeout`'s timer task) use a fresh single-use entry so their CAS cannot claim a later, unrelated wait. A task whose interrupted wait left a stale registration behind can immediately park elsewhere (e.g. on a lock during its cleanup) with a fresh entry. Compared to the previous approach on this branch, this removes the per-task `wait_state` byte (and its ABA workarounds), the second and third intrusive link sets, and the duplicated list implementations; sticky workqueue elements now also record the synchronized wrapper as their queue identity, so asynchronous deletions take the wrapper's lock instead of mutating the inner list unsynchronized. The claim-failed path of `schedule(t, exc, error=true)` intentionally retains master's blind-fire semantics (legal only for owned tasks); making interruption safe in that window as well is deferred to the `cancellation_request` mechanism in JuliaLang#60281. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a parked task is interrupted (`schedule(t, exc, error=true)`), the waking party must both win the right to reschedule the task against any concurrent `notify` and remove the task's wait-queue registration, which requires the waitee's lock - and reacquiring that lock may itself need to park. Master handles this with an unsynchronized `list_deletefirst!` on a foreign task's queue, which is only safe under an ownership discipline that cancellation precisely cannot satisfy. This replaces the task-intrusive wait queues with per-wait heap registrations (`WaitEntry`, one cached per task so the common park does not allocate) and a single-word wake-claim protocol on the new atomic `Task.waiting_on` field: `notify` claims a popped entry with an expected-entry CAS (making stale entries of interrupted waits harmless to skip), interrupters claim with an unconditional swap and leave the entry for lazy unlinking by the waiter's own cleanup or a later notify, and wake sources aimed at one specific wait (`wait_with_timeout`'s timer task) use a fresh single-use entry so their CAS cannot claim a later, unrelated wait. A task whose interrupted wait left a stale registration behind can immediately park elsewhere (e.g. on a lock during its cleanup) with a fresh entry. Compared to the previous approach on this branch, this removes the per-task `wait_state` byte (and its ABA workarounds), the second and third intrusive link sets, and the duplicated list implementations; sticky workqueue elements now also record the synchronized wrapper as their queue identity, so asynchronous deletions take the wrapper's lock instead of mutating the inner list unsynchronized. The claim-failed path of `schedule(t, exc, error=true)` intentionally retains master's blind-fire semantics (legal only for owned tasks); making interruption safe in that window as well is deferred to the `cancellation_request` mechanism in JuliaLang#60281. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a parked task is interrupted (`schedule(t, exc, error=true)`), the waking party must both win the right to reschedule the task against any concurrent `notify` and remove the task's wait-queue registration, which requires the waitee's lock - and reacquiring that lock may itself need to park. Master handles this with an unsynchronized `list_deletefirst!` on a foreign task's queue, which is only safe under an ownership discipline that cancellation precisely cannot satisfy. This replaces the task-intrusive wait queues with per-wait heap registrations (`WaitEntry`, one cached per task so the common park does not allocate) and a single-word wake-claim protocol on the new atomic `Task.waiting_on` field: `notify` claims a popped entry with an expected-entry CAS (making stale entries of interrupted waits harmless to skip), interrupters claim with an unconditional swap and leave the entry for lazy unlinking by the waiter's own cleanup or a later notify, and wake sources aimed at one specific wait (`wait_with_timeout`'s timer task) use a fresh single-use entry so their CAS cannot claim a later, unrelated wait. A task whose interrupted wait left a stale registration behind can immediately park elsewhere (e.g. on a lock during its cleanup) with a fresh entry. Compared to the previous approach on this branch, this removes the per-task `wait_state` byte (and its ABA workarounds), the second and third intrusive link sets, and the duplicated list implementations; sticky workqueue elements now also record the synchronized wrapper as their queue identity, so asynchronous deletions take the wrapper's lock instead of mutating the inner list unsynchronized. The claim-failed path of `schedule(t, exc, error=true)` intentionally retains master's blind-fire semantics (legal only for owned tasks); making interruption safe in that window as well is deferred to the `cancellation_request` mechanism in JuliaLang#60281. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduction
This commit is a first sketch for what I would like to do for robust cancellation
(i.e. "Making ^C just work"). At this point it's more of a sketch than a real PR,
but I think I've done enough of the design for a design discussion.
The first thing I should say is that the goals of this PR is very narrowly to
make ^C work well. As part of that, we're taking a bit of a step towards
structured concurrency, but I am not intending this PR to be a full implementation
of that.
Given that some of this has been beaten to death in previous issues, I will also
not do my usual motivation overview, instead jumping straight into the implementation.
As I said, the motivation is just to make ^C work reliably at this point.
Setting the stage
Broadly when we're trying to cancel a task, it'll be in one of two broad categories:
Waiting for some other operation to complete (e.g. an IO operation, another task,
an external event, etc.). Here, the actual cancellation itself is not so difficult
(after all the task is not running, but suspended in a somehwat well-defined place).
However, robust cancellation requires us to potentially propagate the cancellation
signal down the wait tree, since the operation we actually want to cancel may not
be the root task, but may instead be some operation being performed by the task
we're waiting on (and we'd prefer not to leak those operations and have rogue tasks
going around performing potentially side-effecting operations).
Currently running and doing some computation. The core problem is not really one of
propagation (after all the long-running computation is probably what we're wanting
to cancel), but rather how to do the cancellation without state corruption. A lot of
the crashiness of our existing ^C implementation is just that we would simply inject
an exception in places that are not expecting to handle it.
For a full solution to the problem, we need to have an answer for both of these points.
I will begin with the second, since the first builds upon it.
Cancellation points
This PR introduces the concept of a
cancellation requestand acancellation point.Each task has a
cancellation_requestfield that can be set externally (e.g. by ^C).Any task performing computation should regularly check this field and abort its
computation if a cancellation request is pending.
For this purpose, the PR provides the
@cancel_checkmacro. This macro turns a pendingcancellation request into a well-modeled exception. Package authors should insert a
call to the macro into any long-running loops. However, there is of course some overhead
to the check and it is therefor inappropriate for tight inner loops.
We attempt to address this with compiler support. Note that this part is currently
incompletely implemented, so the following describes the design rather than the current
state of the PR. Consider the cancel_check macro:
where
cancellation_point!is a new intrinsic that defines a cancellation point. Thecompiler is semantically permitted to extend the cancellation point across any following
effect_free calls (note for transitivity reasons, the effect is not exactly the same,
but is morally equivalent). Upon passing a
cancellation_point!, the system willset the current task's
reset_ctxto this cancellation point. If a cancellation requestoccurs before the
reset_ctxis cleared, the task's execution will be reset to thenearest cancellation point. I proposed this mechanism in #52291.
Additionally, the
reset_ctxcan in principle be used to establish scoped cancellationhandlers for external C libraries as well although I suspect that there are not many
C libraries that are actually reset safe in the required manner (since allocation is not).
Note that
cancellation_point!is also intended to be a yield point in order to faciliatethe ^C mechanism described below. However, this is not currently implemented.
Structured cancellation
Turning our attention now to the first of the two cases mentioned above, we tweak the task's
existing
queuereference to become a generic (atomic) "waitee" reference. The queue isrequired to be obtainable from with object via the new
waitqueuegeneric function.To cancel a
waiterwaiting for a waitablewaiteeobject, wewaiteeand call a new generic functioncancel_wait!,which shall do whatever synchronization and internal bookkeeping is
required to remove the task from the wait-queue and then resumes the
task.
waiterresumes in the wait code. It may now decide how and whether topropagate the cancellation to the object it was just waiting on. Note that
this may involve re-queing a wait (to wait for the cancellation of
waiteeto complete).
The idea here is that this provides a well-defined context for cancellation-propagation
logic to run. I wanted to avoid having any cancellation propagation logic run in parallel
with actual wait code.
How the cancellation propagates is a bit of a policy question and not one that I fully
intend to address in this PR. My plan is to implement a basic state machine that works
well for ^C (by requesting safe cancellation immediately and then requesting increasingly
unsafe modes of cancellation upon timeout or repeated ^C), but I anticipate that external
libraries will want to create their own cancellation request state machines, which the
system supports. The implementation is incomplete, so I will not describe it here yet.
One may note that there are a significant number of additional fully dynamic dispatches
in this scheme (at least
waitqueueandcancel_wait!and possibly in the future).However, note that these dynamic dispatches are confined to the cancellation path, which
is not throughput-sensitive (but is latency sensitive).
^C handling
The handling of ^C is delegated to a dedicated task that then gets notified from the
signal handler when a SIGINT is received (similar to the existing profile listener)
task. There is a little bit of an additional wrinkle in that we need some logic to
kick out a computational-task to its nearset cancellation point if we do not have
any idle threads. This logic is not yet implemented.
Examples to try
As noted above, the
@Base.cancel_checkis not intended to be required in the inner loop.Rather, the compiler is expected to extend the cancellation point from the start of the loop
to the entire function. However, this is not yet implemented.
I hope this will fix (more of a checklist for me at this point) #4037 #6283 #25790 #29369 #36379 #42072 #43451 #43451 #45055 #47839 #50045 #56462 #56545 #58105 #58849 #58689
Closes #49541
Part of #33248 #52291
Refs, but yet to be decided #58259 #35026 #39699
TODO
punted