https://github.com/user-attachments/files/22373415/Transactional_Locking.pdf
It doesnât explain in sufficient detail what itâs doing behind the scenes.
I need an example that shows the actual benefits,
My understanding is it tracks how data is accessed and schedules threads in a manner that avoids overlapping accesses?
I think this could be done with an implementation of the future std.Io interface, though it might require some API beyond what the interface will provide.
I skimmed over the text and (coming from a DB background) it sounds interesting. However, the still complicated usage, at least in C++, would prevent me from using it, even if I had a use case.
I think it doesnât avoid overlapping access. Instead it detects conflicts and resolves them by rolling back and giving the callers a chance to retry.
Reminds me of optimistic locking in ORMs.
If I understand correctly, it promises better performance than locking when the number of threads is high and the probability of actually conflicting changes is low. This fits to the spirit of Zig somehowâŚ
Answer to the title: yes. I donât think it needs to be in the standard library (although Iâm not opposed either), but Zig-the-language does precisely nothing to tame the complexity of multi-threaded parallel code.
Nor should it, but that means that writing correct parallel code is a matter of discipline. Thatâs not as scary as it sounds, or it shouldnât be: using STM is an example of discipline, and âdo everything through this APIâ, unlike âjust get it all right lol, lmaoâ, is an actually-practical approach to correctness in this context.
So yeah, absolutely a nice STM library would be a great addition to the ecosystem.
Yes, yes, yes, and yes. This is all correct. Having a native âeffectfulâ async system with event-loop characteristics, will go a long way towards making the hard parts (like retries) easier, as @vulpesx points out.
For those who prefer to read, rather than watch video, the STM Wikipedia entry is both approachable, and has links to all the foundational papers on the subject (which are themselves well worth reading).
If a transaction fails to commit, the STM will attempt to rerun it. This requires that the transaction cannot modify any non-STM variables. A transaction is a piece of code that can contain arbitrary function calls. These calls must also ensure that they do not modify any non-STM variables. Furthermore, no transaction can be run within a transaction (this is implemented in Haskell using the STM monad. Running transactions is done within I/O, and STM cannot contain I/O).
From Zigâs perspective, only language-level support for these behavioral checks can ensure the correctness of transaction code; any other approach cannot prevent users from writing incorrect code.
Indeed, thatâs the part where the new monad-esque IO system can help.
Perhaps this could be asserted using a .Debug only mutex within the STM struct? Just brute force it.
Zig isnât really about preventing users from writing incorrect code. It wants to help! But other values are more prominent.
However, if you have a solid idea of what that language-level support would look like, Iâm curious to hear it. Especially if the feature(s) would be minimal, broadly applicable, and compatible with what we already have.
Following the STM monad here, my simple idea is as follows:
-
First, define a native type TVar(a), which represents the variable of STM. TVar has three operations:
newTVar(T: type, val: T) TVar(T)
readTVar(ref: TVar(T)) T
writerTVar(ref: TVar(T), T) void -
STM code blocks are allowed. TVar(a) operations can only occur within STM blocks. Non-TVar variables cannot be modified within STM blocks. STM blocks may be retried and allowed to execute multiple times until successful. Function calls can be made within STM blocks, but both the called and the function must meet the above requirements.
-
There is an atomically function that runs the STM block. The STM block cannot be executed by itself and can only be executed by the atomically function.
// TVar()
// STMBlock
// atomically(STMBlock) STMBlock return type{}
fn foo() void {
tv1: TVar(i32) = atomically({//STM block
newTVar(i32, i);
});
tv2: TVar(i32) = atomically({//STM block
newTVar(i32, i);
});
k1 = atomically({//STM block
v1 = readTVar(tv1);
writerTVar(tv2, v1 + 1);
});
atomically(bar(tv1, tv2));
}
fn bar(tv1: TVar(i32), tv2: TVar(i32)) i32 {//STM block
v1 = readTVar(tv1);
v2 = readTVar(tv2);
const res = v1 + v2
//The retry operation can only be performed in the STM block.
//It will block the thread until changes occur in tv1 and tv2.
//The thread is awakened and the transaction is retried.
if(res > 100) retry();
writerTVar(tv1, res);
return res;
}
Seems plausible.
Zig doesnât have blocks, of course, but it does allow the construction of context objects. Could that be made to suffice?
Could you provide some concrete example code?
In my example, STMBlock == STM monad. Iâm not sure how context objects achieve this.
Context object like:
pub const StmBlock = struct {
ctx: *anyopaque,
atom_fn: *const fn(*StmBlock) StmBlock,
pub fn atomically(self: StmBlock) StmBlock {
return self.atom_fn(self.ctx);
}
};
Thatâs just the barest possible sketch, but itâs what I meant. atomically would have to handle everything except the provided atomic function, which gets an opaque pointer to any state it needs, and whatever else you decide that the interface requires.