The removal of std.Thread.mutex and std.Thread.futex from the synchronization primitives saddens me

I am developing a cross-platform queue library and have conducted some basic performance tests. However, the removal of std.Thread.mutex and std.Thread.futex from the synchronization primitives saddens me. I believe these should have been retained. Currently, using io.futexwait seems to spawn a new thread, which is not what I want.

benchLockBasedBoundedQueue:
        OneLockPowSizeQueue(tryPush/tryPop): 6279151.944017593  159
        OneLockPowSizeQueue(Push/Pop): 9547423.867405195        104
        OneLockAnySizeQueue(tryPush/tryPop): 5537588.517355684  180
        OneLockAnySizeQueue(Push/Pop): 6378378.14717079 156
        TwoLockPowSizeQueue(tryPush/tryPop): 12600422.738890711 79
        TwoLockPowSizeQueue(Push/Pop): 13421764.868127806       74
        TwoLockAnySizeQueue(tryPush/tryPop): 14703671.396470163 68
        TwoLockAnySizeQueue(Push/Pop): 13729383.316163778       72
benchLockFreeSpScBounded: 64026829.80276055     15
benchLockFreeWaitSpScBoundedCached:
        PowSizeQueue(tryPush/tryPop): 38124799.99253364 26
        PowSizeQueue(Push/Pop): 37687974.66237464       26
        AnySizeeQueue(tryPush/tryPop): 45759234.356490746       21
        AnySizeeQueue(Push/Pop): 40499493.86567531      24
benchLockFreeWaitSpScBoundedClassic:
        PowSizeQueue(tryPush/tryPop):51233948.38475832  19
        PowSizeQueue(push/pop):32200851.808488872       31
        AnySizeQueue(tryPush/tryPop):46189696.46440968  21
        AnySizeQueue(push/pop):15823171.82688307        63

All queues have a capacity of 256, with a test volume of 10,000,000 operations, running on macOS.

1 Like

It doesn’t do that. This is what it does:

Atomically checks if the value at ptr equals expected, and if so, blocks until either:

  • a matching (same ptr argument) futexWake call occurs, or
  • a spurious (“random”) wakeup occurs.

Have you tried updating std.Thread.Mutex to Io.Mutex (as well as thread spawning to io.concurrent)?

2 Likes

Oh, I see I misunderstood.

I’ve completed the migration to Zig 0.16 and ran some basic benchmarks. The lockfree wait queue (using futex for waiting) has a noticeable performance drop.

Use mutex.lockUncancelable instead of mutex.lock.

Use cond.waitUncancelable instead of cond.wait.

Use io.futexWaitUncancelable instead of futexWait.

# zig-0.15.2/windows
benchLockBasedBoundedQueue:
        OneLockPowSizeQueue(tryPush/tryPop): 9186408.48816795   108
        OneLockPowSizeQueue(Push/Pop): 14675926.197702337       68
        OneLockAnySizeQueue(tryPush/tryPop): 8005801.96479993   124
        OneLockAnySizeQueue(Push/Pop): 13658509.668107776       73
        TwoLockPowSizeQueue(tryPush/tryPop): 14918243.549873628 67
        TwoLockPowSizeQueue(Push/Pop): 13136033.611431923       76
        TwoLockAnySizeQueue(tryPush/tryPop): 14786046.644358465 67
        TwoLockAnySizeQueue(Push/Pop): 14565239.967426298       68
benchLockFreeSpScBounded: 80190860.66364834     12
benchLockFreeWaitSpScBoundedCached:
        PowSizeQueue(tryPush/tryPop): 435210250.0718097 2
        PowSizeQueue(Push/Pop): 48724062.96318313       20
        AnySizeeQueue(tryPush/tryPop): 299514187.987085 3
        AnySizeeQueue(Push/Pop): 58836577.28802211      16
benchLockFreeWaitSpScBoundedClassic:
        PowSizeQueue(tryPush/tryPop):96622558.4687257   10
        PowSizeQueue(push/pop):97109157.49066053        10
        AnySizeQueue(tryPush/tryPop):39917004.5641103   25
        AnySizeQueue(push/pop):85044282.5579279 11
# zig-0.16.0/windows
benchLockBasedBoundedQueue:
        OneLockPowSizeQueue(tryPush/tryPop): 8626058.63304574   115
        OneLockPowSizeQueue(Push/Pop): 18132070.374191534       55
        OneLockAnySizeQueue(tryPush/tryPop): 8492460.8179089    117
        OneLockAnySizeQueue(Push/Pop): 20132471.663546134       49
        TwoLockPowSizeQueue(tryPush/tryPop): 12229035.153584452 81
        TwoLockPowSizeQueue(Push/Pop): 9569094.122566698        104
        TwoLockAnySizeQueue(tryPush/tryPop): 12418225.981909128 80
        TwoLockAnySizeQueue(Push/Pop): 9312920.652984744        107
benchLockFreeSpScBounded: 93753750.150006       10
benchLockFreeWaitSpScBoundedCached:
        PowSizeQueue(tryPush/tryPop): 474608448.030375  2
        PowSizeQueue(Push/Pop): 25940539.0962835        38
        AnySizeeQueue(tryPush/tryPop): 368704372.8338618        2
        AnySizeeQueue(Push/Pop): 26952797.565623324     37
benchLockFreeWaitSpScBoundedClassic:
        PowSizeQueue(tryPush/tryPop):122144863.80847685 8
        PowSizeQueue(push/pop):27936160.286513258       35
        AnySizeQueue(tryPush/tryPop):42133647.931237884 23
        AnySizeQueue(push/pop):30268267.656237233       33

The new std.Io is interesting, but I still wish there was an option for users. I’d really like to see std.Thread.Mutex, std.Thread.Futex, and std.Thread.Condition added back. It’s great, but sometimes I don’t need std.Io, and this leaves me with no choice. On the other hand, I don’t mind providing an interface with std.Io for libraries, but I’d prefer to keep a set of interfaces without std.Io as well—that’s really useful in certain situations.

At least bring futex back.

Do I really have to implement futex, mutex, and condition manually?

This is getting bit off-topic for this thread. The reason i think std doesn’t have Io-independent synchronization primitives or threads is because they would not mix with Io, the Io has to be aware of the synchronization.

Now zig being systems programming language, whether only putting all balls into the std.Io basket and not having lower level abstractions like in zig 0.15 and before is a design decision for sure. But you could also say that if you write system specific code, you could probably also write better code than those portable abstractions.

One thing is sure, that if zig 0.16 program does Io without going through given Io instance, the program may block for uncertain amount of time, or in worst case infinitely if mixing Io synchronization primitives with OS ones.

What counts as staying on-topic? Can’t I give feedback here? Besides, it was the OP who asked me about my specific situation.

Do you know what this means?

Does a Zig program have to use only the std.Io abstraction? Can’t it use an event loop like in C?

1 Like

std.posix had code that did not work properly on some platforms, or was suboptimal because usually using the posix api as the model that every implementation had to fit to. Also freestanding had no way to implement std.posix.

No, but those can’t interact with each other seamlessly. I think one of goals with ziglibc is that it can integrate into std.Io meaning it is transparent to C code that they are actually going through a zig Io instance.

Who’s talking about those std.posix interfaces? I’m talking about the most fundamental synchronization primitives of the language. As a library author, one should avoid having to implement these basic language features themselves, because that fragments the ecosystem—when every library that needs them goes off and implements its own version, it effectively creates fragmentation. You have no idea what that means… and you’re off there answering about std.posix, completely missing the point.

I never said I’m against std.Io. But it leaves me with no choice—do you understand what that means? I can provide interfaces that take an Io parameter, and those interfaces would integrate seamlessly, just as you want. Users could also use interfaces without an Io parameter—that would be up to them.

On the other hand, for a performance-oriented library, this design forces me to implement things at literally twice the performance cost. Do you think that’s acceptable?

The std.Thread synchronization primitives weren’t that different in spirit of std.posix. I already also told you that if you use OS native synchronization primitives and threads, they will not work in tandem with std.Io. It is possible to make them cooperate, but you basically will have to mix 2 different kind of synchronization primitives and know much about the implementation itself.

Avoiding this indeed is the idea of std.Io interface. Think how you have to normally write both blocking and non-blocking version of code. Or in rust, support every async runtime seperately.

If you don’t care about these aspects, then yeah you could start by copying the old 0.15.2 code and maintaining your own portable OS abstraction layer. That will have issues and caveats when mixed with std.Io that you and any consumer of the project would have to be aware of.

If you are “performance-oriented” library, that wants to break through every abstraction level, then I don’t see why this is such a huge issue for you.

1 Like

Of course it’s gonna split the ecosystem. Because you are giving the way to split the ecosystem … If you don’t want to split the ecosystem use std.Io, that’s the point.

1 Like

Did I not provide an interface based on std.Io? How can that be called splitting the ecosystem?

If you are implementing Io providing the lower level APIs is up to you indeed. If user is gonna use those lower-level APIs instead of the std.Io implementation you provide, they make the decision that it might not be compatible with other things out there. This is similar situation to rust async runtimes, or deciding on libuv, libxev, zig-aio, zio.

you are not alone

i am asking similar questions

1 Like

That has nothing to do with me. That is the user splitting the ecosystem.

According to your logic, providing an interface that is not based on std.Io is an act of splitting the ecosystem. So does the Zig world only run one programming paradigm? Is only std.Io allowed to exist?

2 Likes

I think it’s very unlikely that std will bring back these primitives. I’m also unhappy with the situation, because Io is a very lacking interface. I think splitting the ecosystem is unavoidable at some point, much like C++ has a standard library, but applications don’t use it. I don’t see how e.g. TigerBeatle would want to use std.Io instead of it’s own abstraction layer.

I joined the discussion in the middle of the road

now i re-read the title more carefully

it looks like the situation with Io direction is more problematic.

I thought removing std.posix was the only “improvement.”

I still think the trial-and-error process with Zig changes is fine.

But with this kind of framework-enforced style, we’re moving toward a C++ mindset.

I also would like to see those primitives back, yes std.Io have those but there are still situations where users of the code does not need to choose an io implementation or provide one. I get that it might interfere but other things in code can do that as well. Whats stopping a developer to implement these on their own?

former std.Thread.mutex is zig abstraction - for example Windows mutex is reqursive but zig one is not (there is ReqursiveMutex iirc), windows supports named mutexes - zig does not

These os independed abstractions were already implemented , it was huge work - I don’t want repeat it

the same “IO” oriented os primitives exist in Windows

WSAEVENT WSAAPI WSACreateEvent();

This api creates WinSocket event, but IT’S THE SAME AS REGULAR WINDOWS EVENT:

application can call the CreateEvent function directly

I understand your question to be that “these were already created, why give them up, I need them?”

I also understood the Zig team’s motivations to be that “maintaining code takes resources, we want to spend those resources maintaining this new abstraction and retire the old abstraction”.

If the old Zig standard library 15.2 code really is what you want, then why can’t you copy-paste it into a new library? Seems like there are several of you here thinking about this, could you team up and support something like this?

1 Like

I don’t hurry - I’m programming for fun, not for profit.

I hope I have time to wait for the results of Io revolution…