A purely hypothetical question, just trying to wrap my head around things. So, let’s say I have
threadlocal var buf: [16_384]u8 = undefined;
pub fn doSomethingImportantWithBuf(arg1, ...) !void {
// prepare buf
@memcpy(buf[ .. ], argX);
...
moduleX.call_function_f1(); // There are some functions invoked here.
...
// finish working with buf.
writeToFile(..., buf);
}
How do I know whether function moduleX.call_function_f1 is fully synchronous, and it won’t fetch the global io instance (or io kept in closure/anonymous struct in a callback), then fill the buffer or directly .flush(io), which would invite some other code to kick in concurrently in this thread in the interim, possibly clobbering content in my localthread variables?
In TypeScript, functions are marked as returning Async<T>, and ESLint forces you to insert await, explicitly indicating that something might concurrently get scheduled for execution (see “microtask” and “macrotask” in JavaScript V8) in the current thread. Reading Zig code, how do I know whether a function (or some of the functions that this function calls, recursively) switches the context?
You know it’s synchronous as you dont call it via io.async or io.concurrent.
Based on its name, I assume moduleX is not an instance of a type, but some import instead. If that’s the case, then it can’t be passed implicitly, and you don’t pass anything explicitly, so there is no chance of a non-global Io being used.
The only way to be sure a global Io is not used is to read the source, but it should be well known that it would be nonconducive to portable code and therefore is not idiomatic zig. I think it’s reasonable to assume the author of that code is aware of that, and that if they need to do otherwise they should have documented that, either on the function or for their whole library/module.
Your thread local is private, and the code you are calling is presumably from an import. There is no way for that code to access your thread local variable without doing some incredibly unsafe memory manipulation. It is reasonable to assume that won’t happen.
zig is much the same, asynchronous or concurrent tasks return a Future(T), you can’t access the return value without calling await or cancel.
There is the detail that zig does not require the task to be run on the current thread.
Zig also has the benefit of (in idiomatic code) passing the runtime explicitly, so if a function/type does not take a runtime you can reasonably assume it doesn’t do async/concurrent things internally either.
Lastly, there is the caveat that the function being defined does not decide if itself is called asynchronously, concurrently or synchronously. That is instead decided by the caller, if they do foo(arg1) then it’s synchronous, if they io.async(foo, .{arg1})/io.concurrent(foo, .{arg1}) then it is asynchronous/concurrent, and they get a future, they can propagate that future up if they want/need but in idiomatic zig this is not common.
the function can ofc require that it be called concurrently to get the desired behaviour, that should be documented.
Question is based on an invalid premise. This is not a justified use of threadlocal var. This function should accept a pointer to the buffer. As written, the function has an unnecessary and problematic dependency on which thread it executes on, as well as on thread local storage in general, which relies on linker features which may have portability issues.
/// Here's an exact function that is invoked from Node.js, which loads Zig's dynamic library:
pub fn logBuffer(e: napi.napi_env, cb_info: napi.napi_callback_info) callconv(.c) napi.napi_value {
// I want to use Zig's IO_Uring I/O here!
}
Sure. It doesn’t have to be. I could be massaging a globally accessible pointer to mmap()ed memory, with a mutex currently obtained for exclusive access. Now, I call a function, it calls another function, which calls another function, which decides to log something using Zig’s IO_Uring I/O. The log Writer’s buffer gets filled, the current fiber gets unscheduled pending the completion of I/O, another microtask kicks in, it also locks the mutex (which happens to be a Recursive mutex), gets through because the mutex’ thread ID matches, and happily continue clobbering the mmap’ed memory because it thinks that it obtained exclusive access.
But that’s besides the point. So, you’re saying, my current task can’t get unscheduled because I must explicitly await in order for it to happen, and that doesn’t automatically happen in parallel? I definitely need to understand this API better.