[HN Gopher] Ditch your mutex, you deserve better
___________________________________________________________________
Ditch your mutex, you deserve better
Author : commandersaki
Score : 123 points
Date : 2025-11-11 22:11 UTC (7 days ago)
(HTM) web link (chrispenner.ca)
(TXT) w3m dump (chrispenner.ca)
| scottmas wrote:
| So cool! Any languages support STM first class besides Haskell?
| cosmic_quanta wrote:
| I think a decade ago or so, people started trying to integrate
| STM in Pypy
| vijaysharma12 wrote:
| I believe Clojure has first class support for STM.
| CGamesPlay wrote:
| Looks like somebody made a Rust experiment back when Rust was
| new: https://docs.rs/stm/latest/stm/
| lmm wrote:
| Scala has great STM in the same way (monad-based).
| LelouBil wrote:
| Not "first class" but pretty good in Kotlin
|
| https://arrow-kt.io/learn/coroutines/stm/
| spencerflem wrote:
| The new Verse lang by Epic Games & a core Haskell contributor
| has a lot of transaction features. I don't know if it's exactly
| the same as STM though.
| andersa wrote:
| Verse only supports single-threaded transactional memory.
| Epic hasn't yet demonstrated that their approach can actually
| scale to be used from multiple threads in a useful manner,
| though they claim that it will.
| stackghost wrote:
| There are c++ libraries that offer it.
| hackingonempty wrote:
| Scala supports it with for-comprehensions which are equivalent
| to Haskell's do-notation but STM is not part of the Scala
| standard library. Zio and Cats Effect are two popular Scala
| effects systems with STM.
| vinkelhake wrote:
| This felt like a blast from the past. At a few times reading this
| article, I had to go back and check that, yes, it's actually a
| _new_ article from the year 2025 on STM in Haskell and it 's even
| using the old bank account example.
|
| I remember 15 or 20 years (has it been that long?) when the
| Haskell people like dons were banging on about: 1) Moore's law
| being dead, 2) future CPUs will have tons of cores, and 3) good
| luck wrangling them in your stone age language! Check out the
| cool stuff we've got going on over in Haskell!
| VBprogrammer wrote:
| Yeah, I wrote an essay on STM in Haskell for a class back in
| 2005 I think.
| lmm wrote:
| Yeah, remember when we used to care about making better
| programming languages that would perform faster and avoid
| errors, instead of just slapping blockchains or AI on
| everything to get VC money. Good times.
| doliveira wrote:
| Only half-joking: maybe Java was a mistake. I feel like so
| much was lost in programming language development because of
| OOP...
| exasperaited wrote:
| Java is most of why we have a proliferation of VM-based
| languages and a big part of why WASM looks the way it does
| (though as I understand it, WASM is the shape it is in some
| measure because it _rejects_ JVM design-for-the-language
| quirks).
|
| I would also not blame Java for the worst of OO, all of
| that would have happened without it. There were so many OO
| culture languages pretending to that throne. Java got there
| first because of the aforementioned VM advantage, but the
| core concepts are things academia was offering and the
| industry wanted in non-Ada languages.
|
| I would say Java also had a materially strong impact on the
| desire for server, database and client hardware
| agnosticism.
|
| Some of this is positive reinforcement: Java demonstrates
| that it's better if you don't have to worry about what
| brand your server is, and JDBC arguably perfected ODBC.
|
| Some of it is negative: a lot of the move to richer client
| experiences in the browser has to do with trying to remove
| client-side Java as a dependency, because it failed. It's
| not the only bridged dependency we removed, of course:
| Flash is equally important as a negative.
| bluGill wrote:
| OOP is very useful/powerful, don't throw the good parts
| out. Java messed up by deciding everything must be an
| object when there are many other useful way to program.
| (you can also argue that smalltalk had a better object
| model, but even then all objects isn't a good thing).
| Functional programing is very powerful and a good solution
| to some problems. Procedural programing is very powerful
| and a good solution to some problems. You can do both in
| Java - but you have to wrap everything in an object anyway
| despite the object not adding any value.
| kragen wrote:
| Java was derived from C++, Smalltalk, and arguably Cedar,
| and one of its biggest differences from C++ and Smalltalk
| is that in Java things like integers, characters, and
| booleans _aren 't_ objects, as they are in C++ and
| Smalltalk. (Cedar didn't have objects.)
| bluGill wrote:
| Right. Everything a user can do is object, but there are
| a few non-object built ins. (they are not objects in C++
| either, but C++ doesn't make everything you write be an
| object)
| kragen wrote:
| In C++ integers and characters _are_ objects. See
| https://en.cppreference.com/w/cpp/language/objects.html,
| for example, which explicitly mentions "unsigned char
| objects", "a bit-field object", "objects of type char",
| etc.
| dpark wrote:
| I feel this is a case of using the same word to mean
| something different. C++ "object" here seems to mean
| something more akin to "can be allocated and stuffed into
| an array" than a Smalltalk-type object.
|
| i.e. C++ primitive types are defined to be objects but do
| not fit into a traditional object-oriented definition of
| "object".
| kragen wrote:
| Yes, _many_ people believe that C++ isn 't really
| "object-oriented", including famously Alan Kay, the
| inventor of the term. Nevertheless, that _is_ the
| definition of "object" in C++, and Java _is_ based on
| C++, Smalltalk, and Cedar, and makes an
| "object"/"primitive" distinction that C++, Smalltalk, and
| Cedar do not, so "Java [did something] by deciding
| everything must be an object" is exactly backwards.
| dpark wrote:
| To be clear, I'm not trying to pick at whether or not C++
| is "really object oriented".
|
| What I'm saying is that the discrepancy between
| primitives in C++ and Java is entirely one of
| _definition_. Java didn't actually change this. Java just
| admitted that "objects" that don't behave like objects
| aren't.
| kragen wrote:
| On the contrary, Java objects are very different from C++
| objects, precisely because they lack a lot of the
| "primitive-like" features of C++ objects such as copying,
| embedding as fields, and embedding in arrays. (I'm
| tempted to mention operator overloading, but that's just
| syntactic sugar.)
| dpark wrote:
| Java differs from C++ in an endless number of ways.
|
| What I'm saying is that in _both_ C++ and Java, there are
| a set of primitive types that do not participate in the
| "object-orientedness". C++ primitives do not have class
| definitions and cannot be the base of any class. This is
| very much like Java where primitives exist outside the
| object system.
|
| If the C++ standard used the term "entities" instead of
| "objects" I don't think this would even be a point of
| discussion.
| kragen wrote:
| It's not some minor point of terminology.
|
| The entire design of C++ is built around eliminating all
| distinctions between primitive "entities" and user-
| defined "entities" in a way that Java just isn't. It's
| true that you can't inherit from integers, but that's one
| of very few differences. User-defined "entities" don't
| (necessarily) have vtables, don't have to be heap-
| allocated, can overload operators, can prevent
| subclassing, don't necessarily inherit from a common base
| class, etc.
|
| C++'s strange definition of "object" is a natural result
| of this pervasive design objective, but changing the
| terminology to "entity" wouldn't change it.
| dpark wrote:
| > The entire design of C++ is built around eliminating
| all distinctions between primitive "entities" and user-
| defined "entities"
|
| If the intent was to erase all distinction between built-
| in and user-defined entities then making the primitive
| types unable to participate in object hierarchies was a
| pretty big oversight.
|
| But at this point I think we're talking past each other.
| Yes, in Java objects are more distinct from primitives
| than in C++. But also yes, in C++ there _is_ a special
| group of "objects" that are special and are notably
| distinct from the rest of the object system, very much
| like Java.
| kragen wrote:
| You can read Stroustrup's books and interviews, if the
| language design itself doesn't convey that message
| clearly enough; you don't have to guess what his
| intentions and motivations were. And, while I strongly
| disagree with you on how "special and notably distinct"
| primitive types are in C++, neither of us is claiming
| that C++ is _less_ adherent to the principle that
| "everything is an object" than Java. You think it's a
| _little_ more, and I think it 's a lot more.
|
| But we agree on the direction, and that direction is not
| "Java [did something] by deciding everything must be an
| object," but its opposite.
| dpark wrote:
| I don't actually think it's any more adherent to that
| notion. This is exactly why I tried to point out the
| discrepancies in definitions. You have to define what an
| "object" is or the discussion is meaningless.
|
| If the definition of object is something like "an
| instance of a class that has state, operations, and
| identity" then C++ primitives are fundamentally not
| objects. They have no identity and they are not defined
| by a class. If "participates in a class hierarchy" is
| part of the definition, then C++ is _way_ less OO than
| Java.
|
| I don't quite understand what your definition is, but you
| seem to be arguing that user-defined entities are more
| like primitives in C++, so it's more object-oriented. So
| maybe "consistency across types == object orientedness"?
| Except C++ isn't really more consistent. Yes, you can
| create a user-defined type without a vtable, but this is
| really a statement that user defined types a far more
| flexible than primitives. But also if "consistency across
| types" is what makes a language OO then C seems to be
| more OO than C++.
| kragen wrote:
| I don't think C++ is object-oriented, and it is certainly
| way less OO than Java in most ways. Its "classes" aren't
| the same kind of thing as classes in OO languages and its
| "objects" aren't OO objects.
|
| In part this is because by default even C++ class
| instances _don 't_ have identity, or anyway they only
| have identity in the sense that ints do, that every (non-
| const) int has an address and mutable state. You have to
| define a destructor, a copy constructor, and an
| assignment operator to give identity to the instances of
| a class in C++.
|
| With respect to "participates in a class hierarchy", that
| has not been part of the definition of OO since the
| Treaty of Orlando. But, in Java, all objects do
| participate in a class hierarchy, while no primitives do,
| while, in C++, you can also create class instances (or
| "class" "instances") that do not participate in a class
| hierarchy (without ever using inheritance, even
| implicitly). So, regardless of how OO or non-OO it may
| be, it's another distinction that Java draws between
| primitives and class instances ("objects" in Java) that
| C++ doesn't.
| dpark wrote:
| I think we are in agreement.
|
| In C++, everything is an object as defined by the C++
| spec, but a lot of things are not objects in an OO sense.
|
| In Java, almost everything is an object in an OO sense,
| but some stuff is definitely not.
| bluGill wrote:
| I'm not sure who invented "object oriented", but objects
| were invented by Simula in 1967 (or before, but first
| released then?) and that is where C++ takes the term
| from. Smalltalk-80 did some interesting things on top of
| objects that allow for object oriented programming.
|
| In any case, Alan Kay is constantly clear that object
| oriented programming is about messages, which you can do
| in C++ in a number of ways. (I'm not sure exactly what
| Alan Kay means here, but it appears to exclude function
| calls, but would allow QT signal/slots)
| kragen wrote:
| The specific thing you can do in Smalltalk (or Ruby,
| Python, Objective-C, Erights E, or JS) that you can't do
| in C++ (even Qt C++, and not Simula either) is define a
| proxy class you can call arbitrary methods on, so that it
| can, for example, forward the method call to another
| object across the network, or deserialize an object
| stored on disk, or simply log all the methods called on a
| given object.
|
| This is because, conceptually, the object has total
| freedom to handle the message it was sent however it sees
| fit. Even if it's never heard of the method name before.
| bluGill wrote:
| You can do that in C++ too - it is just a lot of manual
| work. Those other languages just hide (or make easy) all
| the work needed to do that. There are trade offs though -
| just because you can in C++ doesn't mean you should: C++
| is best where the performance cost of that is
| unacceptable.
| kragen wrote:
| No, in C++ it's literally impossible. The language
| provides no way to define a proxy class you can call
| arbitrary methods on. You have to generate a fresh proxy
| class every time you have a new abstract base class you
| want to interpose, either by hand, with a macro
| processor, or with run-time code generation. There's no
| language mechanism to compile code that calls
| .fhqwhgads() successfully on a class that doesn't have a
| .fhqwhgads() method declared.
| bluGill wrote:
| you don't call fhqwhgads() on your proxy class though.
| You call runFunction("fhqwhgads") and it all compiles -
| the proxy class then string matches on the arguments. Of
| course depending on what you want to do it can be a lot
| more complex. That is do manually what other languages do
| for you automatically under the hood.
|
| Again, this is not something you should do, but you can.
| kragen wrote:
| That doesn't provide the same functionality, because it
| requires a global transformation of your program,
| changing every caller of .fhqwhgads(). By contrast, in OO
| languages, you typically just define a single method
| that's a few lines of code. You're sinking into the
| Turing Tarpit where everything is possible but nothing of
| interest is easy. Before morning you'll be programming in
| Brainfuck.
| bluGill wrote:
| Just like Java, you cannot inherit from integers or
| characters. Depending on what you want to do with them
| that might or might not matter.
| kragen wrote:
| That's true, and in Smalltalk it's not true. In Cedar
| there is no inheritance. At any rate it's not a case of
| Java making _more_ things objects than its forebears.
| igouy wrote:
| > everything must be an object when there are many other
| useful way to program.
|
| Perhaps you would prefer a multi-paradigm programming
| language?
|
| http://mozart2.org/mozart-v1/doc-1.4.0/tutorial/index.htm
| l
| exasperaited wrote:
| To be maximally fair to the Haskell people, they have been
| enormously influential. Haskell is like Canada: you grow up
| nicely there and then travel the world to bring your energy to
| it.
| dist-epoch wrote:
| I think Intel x86 had some hardware support for STM at some
| point. Not sure what's the status of that.
| PhilipRoman wrote:
| Disabled on most CPUs, plagued by security issues. I haven't
| used it but I assume debugging would be extremely painful,
| since any debug event would abort the transaction.
| kragen wrote:
| That's not _software_ transactional memory, it 's _hardware_
| transactional memory, and their design was not a good one.
| gpderetta wrote:
| Well, HTM was not useful per se, except accelerating an STM
| implementation.
| kragen wrote:
| It isn't very useful for that, but you can use it to
| implement other higher-level concurrency primitives like
| multi-word compare and swap efficiently.
| gpderetta wrote:
| True. At some point in the now distant past, AMD had a
| proposal for a very restricted form of HTM that allowed
| CAS up to 7 memory locations as they had some very
| specific linked list algorithms that they wanted optimize
| and the 7 location restrictions worked well with the
| number of ways of their memory.
|
| Nothing came out of it unfortunately.
| kragen wrote:
| I'd like to see what kind of hardware acceleration
| _would_ help STMs without imposing severe limitations on
| their generality.
|
| To me, the appealing things about STMs are the
| possibility of separating concerns of worst-case
| execution time and error handling, which are normally
| pervasive concerns that defeat modularity, from the
| majority of the system's code. I know this is not the
| mainstream view, which is mostly about manycore
| performance.
| gpderetta wrote:
| Not an expert, but my understanding is that HTM basically
| implements the fast path: you still need a fully fledged
| STM implementation as a fallback in case of interference,
| or even in the uncontended case if the working set
| doesn't fit in L1 (because of way collision for example)
| and the HTM always fails.
| kragen wrote:
| I'm no expert either, but maybe some other hardware
| feature would be more helpful to STMs than hardware TM
| is.
| mrkeen wrote:
| And it's all still true, although I would offer the usual
| argument that concurrency!=parallelism, and if you reach for
| threads&STM to try to speed something up, you'll probably have
| a bad time. With the overhead of GC, STM-retries, false-
| sharing, pointer-chasing, etc you might have a better time
| rewriting it single-threaded in C/Rust.
|
| STM shines in a concurrent setting, where you know you'll
| multiple threads accessing your system and you want to keep
| everything correct. And nothing else comes close.
| qprofyeh wrote:
| @OP perhaps there's a comparison bug in withdraw(): if (a.balance
| <= amount)
| kubanczyk wrote:
| I've caught unbalanced parens: forkIO
| (atomically (transfer req.from req.to req.amount)
| DeathArrow wrote:
| When working with high throughput concurrent code like consumer
| producer pipelines, it's better to avoid shared mutable state
| entirely. Something actor like fits better and C# or Go channels
| works wonders.
| IshKebab wrote:
| Sure, but sometimes shared mutable state _is_ better,
| especially from a performance point of view. For example
| blurring an image.
| janetpacker wrote:
| Isn't that a typical case where you don't have to share
| anything? Divide the image into N chunks, let N threads
| handle each one, no sharing, just need a single sync point at
| the end to wait on completion.
| kreetx wrote:
| Account balance is necessarily a shared mutable state.
| tlb wrote:
| It's not necessarily shared. You could assign a single thread
| to own the account balances, reading requests from a message
| queue. That probably scales better than locking. A single
| thread can do several million transactions per second, more
| than the entire global financial system.
| xmcqdpt2 wrote:
| And in a green threading system like Go or Scala Cats, the
| balances thread isn't a thread at all, and it will run in
| the same thread as the transfer caller when the call isn't
| contended, so you don't even have a context switch.
| kreetx wrote:
| What if you want to compose an action on a balance with
| something else? (That is what the OP is about)
|
| Also, with a queue, you've moved the shared state
| elsewhere, namely, into the queue.
| tlb wrote:
| The account-owning thread has to accept messages for
| every atomic action you need it to do.
|
| There are lots of standard shared queues you can pick
| from that have been fully regression tested. That's
| almost always better than mixing concurrency in with your
| business logic.
| kreetx wrote:
| Sure, but what I meant is when there is some other thing
| that needs to happen atomically together with the balance
| handled by that one thread (i.e, both balance and other
| thing change or neither do). You'll need another thread
| for that other thing, then a method to synchronize the
| two and.. you're back at the mutex.
| internet_points wrote:
| TFA has a whole section praising actors for certain tasks and
| explaining why it doesn't fit here.
| mrkeen wrote:
| > it's better to avoid shared mutable state entirely.
|
| Yes! I would even go so far as to have the type system separate
| the mutable from the non-mutable for this reason!
|
| > Something actor like fits better and C# or Go channels works
| wonders.
|
| Or even STM channels/queues:
| https://hackage.haskell.org/package/stm/docs/Control-
| Concurrent-STM-TChan.html
| https://hackage.haskell.org/package/stm/docs/Control-
| Concurrent-STM-TQueue.html
| Nican wrote:
| This is a nice article, and I appreciate the examples. The next
| problem to solve is how to persist state on disk across two
| different accounts after a transfer has been done.
| juliangmp wrote:
| I found that a lot of the problems I had been having with
| mutexes, stem from the fact that traditionally the mutex and the
| data it protects are separate. Bolting them together, like Rust's
| Mutex<T> does, solves a lot these problems. It let's you write
| normal, synchronous code and leave the locking up to the caller,
| but without making it a nightmare. You can't even access the data
| without locking the mutex.
|
| This isn't an attack on the (very well written) article though.
| Just wanted to add my two cents.
| mgaunard wrote:
| I find it better to model that as an Actor than a mutex, but I
| guess it's inherently the same thing, except the actor also
| allows asynchronous operations.
| gpderetta wrote:
| You can go full circle and also make operations on a mutex
| asynchronous. Hence the realization that message passing and
| shared memory are truly dual.
| mgaunard wrote:
| The very idea of a mutex is that it is synchronous. You
| wait until you can acquire the mutex.
|
| If it's asynchronous, it's not a mutex anymore, or it's
| just used to synchronously setup some other asynchronous
| mechanism.
| gpderetta wrote:
| A mutex is a way to guarantee mutual exclusion nothing
| more nothing less; You can recover synchronous behaviour
| if you really want:
| synchronized<Something> something; ...
| co_await something.async_visit([&](Something& x) {
| /* critical section here */ });
| mgaunard wrote:
| that isn't a mutex, that's delegating work asynchronously
| and delegating something else to run when it is complete
| (the implicitly defined continuation through coroutines).
|
| In systems programming parlance, a mutex is a resource
| which can be acquired and released, acquired exactly
| once, and blocks on acquire if already acquired.
| gpderetta wrote:
| Do a CPS transform of your typical std::mutex critical
| section and you'll find they are exactly the same.
| mgaunard wrote:
| They're not, the interactions with the memory model are
| different, as are the guarantees.
|
| CPS shouldn't be able to deadlock for example?
| gpderetta wrote:
| CPS can trivially deadlock for all meaningful definitions
| of deadlock.
|
| Would you consider this a mutex?
| async_mutex mux; co_await mux.lock();
| /* critical section */ co_await mux.unlock();
|
| What about: my_mutex mux; {
| std::lock_guard _{mux}; /* critical section */
| }
|
| where the code runs in a user space fiber.
|
| Would you consider boost synchronized a mutex?
|
| Don't confuse the semantics with the implementation
| details (yes async/await leaks implementation details).
| dist-epoch wrote:
| Sounds like the Java synchronized class.
| the_gipsy wrote:
| Not at all. With rust you cannot accidentally leak a
| reference, and here's the killer: it guarantees these
| properties at compile time.
| masklinn wrote:
| No. It's not a property of the type so you can have multiple
| items under a mutex and you're not at the mercy of whoever
| wrote it, it works fine with POD types, it does not force a
| lock / unlock on each method call (instead the compiler
| essentially ensures you hold the lock before you can access
| the data), and the borrow checker is there to ensure you can
| not leak any sort of sub-states, even though you can call all
| sorts of helpers which have no requirement to be aware of the
| locking.
|
| It's what synchronized classes wish they had been, maybe.
| Nauxuron wrote:
| > You can't even access the data without locking the mutex.
|
| It's even nicer than that: you can actually access data without
| locking the mutex, because while you hold a mutable borrow to
| the mutex, Rust statically guarantees that no one else can
| acquire locks on the mutex.
|
| https://doc.rust-lang.org/std/sync/struct.Mutex.html#method....
| jstimpfle wrote:
| Given a data item of non-thread safe type (i.e. not Mutex<T>
| etc), the borrow checker checks that there's only ever one
| mutable reference to it. This doesn't solve concurrency as it
| prevents multiple threads from even having the ability to
| access that data.
|
| Mutex is for where you have that ability, and ensures at
| runtime that accesses get serialized.
| dwattttt wrote:
| The maybe unexpected point is that if you know you're the
| only one who has a reference to a Mutex (i.e. you have a
| &mut), you don't need to bother lock it; if no one else
| knows about the Mutex, there's no one else who could lock
| it. It comes up when you're setting things up and haven't
| shared the Mutex yet.
|
| This means no atomic operations or syscalls or what have
| you.
| jstimpfle wrote:
| Do you have an example? I don't program in Rust, but I
| imagine I'd rarely get into that situation. Either my
| variable is a local (in a function) in which case I can
| tell pretty easily whether I'm the only one accessing it.
| Or, the data is linked globally in a data structure and
| the only way to access it safely is by knowing exactly
| what you're doing and what the other threads are doing.
| How is Rust going to help here? I imagine it's only
| making the optimal thing harder to achieve.
|
| I can see that there are some cases where you have heap-
| data that is only visible in the current thread, and the
| borrow checker might be able to see that. But I can
| imagine that there are at least as many cases where it
| would only get in the way and probably nudge me towards
| unnecessary ceremony, including run-time overhead.
| adwn wrote:
| When you construct an object containing a mutex, you have
| exclusive access to it, so you can initialize it without
| locking the mutex. When you're done, you publish/share
| the object, thereby losing exclusive access.
| struct Entry { msg: Mutex<String>, }
| ... // Construct a new object on the stack:
| let mut object = Entry { msg: Mutex::new(String::new())
| }; // Exclusive access, so no locking needed
| here: let mutable_msg = object.msg.get_mut();
| format_message(mutable_msg, ...); ... //
| Publish the object by moving it somewhere else, possibly
| on the heap: global_data.add_entry(object);
| // From now on, accessing the msg field would require
| locking the mutex
| jstimpfle wrote:
| Initialization is always special. A mutex can't protect
| that which doesn't exist yet. The right way to initialize
| your object would be to construct the message first, then
| construct the composite type that combines the message
| with a mutex. This doesn't require locking a mutex, even
| without any borrow checker or other cleverness.
| adwn wrote:
| Dude, it's a simplified example, of course you can poke
| holes into it. Here, let me help you fill in the gaps:
| let mut object = prepare_generic_entry(general_settings);
| let mutable_msg = object.msg.get_mut();
| do_specific_message_modification(mutable_msg,
| special_settings);
|
| The point is, that there are situations where you have
| exclusive access to a mutex, and in those situations you
| can safely access the protected data without having to
| lock the mutex.
| jstimpfle wrote:
| Sorry, I don't find that convincing but rather construed.
| This still seems like "constructor" type code, so the
| final object is not ready and locking should not happen
| before all the protected fields are constructed.
|
| There may be other situations where you have an object in
| a specific state that makes it effectively owned by a
| thread, which might make it possible to forgo locking it.
| These are all very ad-hoc situations, most of them would
| surely be very hard to model using the borrow checker,
| and avoiding a lock would most likely not be worth the
| hassle anyway.
|
| Not sure how this can help me reduce complexity or
| improve performance of my software.
| imtringued wrote:
| >I don't program in Rust, but I imagine I'd rarely get
| into that situation.
|
| Are you sure? Isn't having data be local to a thread the
| most common situation, with data sharing being the
| exception?
|
| >Or, the data is linked globally in a data structure and
| the only way to access it safely is by knowing exactly
| what you're doing and what the other threads are doing.
|
| That's exactly what the borrow checker does. It tracks
| how many mutable references you have to your data
| structure at compile time. This means you can be sure
| what is local and what is shared.
|
| Meanwhile without the borrow checker you always have to
| assume there is a remote probability that your mental
| model is wrong and that everything goes wrong anyways.
| That's mentally exhausting. If something goes wrong, it
| is better to only have to check the places where you know
| things can go wrong, rather than the entire code base.
| jstimpfle wrote:
| I use lots of locals but only to make my code very
| "local", i.e. fine-grained, editable and clear, using
| lots of temporary variable. No complicated expressions.
| That's all immutable data (after initialization). I
| rarely take the address of such data but make lots of
| copies. If I take its address, then as an immutable
| pointer, maybe not in the type system but at least in
| spirit.
|
| I keep very little state on the stack -- mostly implicit
| stuff like mutex lock / mutex unlock. By "state" I mean
| object type things that get mutated or that need cleanup.
| I always have a "database schema" of my global state in
| mind. I define lots of explicit struct types instead of
| hiding state as locals in functions. I've found this
| approach of minimizing local state to be the right
| pattern because it enables composability. I'm now free to
| factor functionality into separate functions. I can much
| more freely change and improve control flow. With this
| approach it's quite rare that I produce bugs while
| refactoring.
|
| So yes, I have lots of locals but I share basically none
| of them with other threads. Also, I avoid writing any
| code that blocks on other threads (other than maybe
| locking a mutex), so there's another reason why I would
| not intentionally share a local with another thread.
| Anything that will be shared with another thread should
| be allocated on the heap just for the reason that we want
| to avoid blocking on other threads.
|
| In that sense, the borrow checker is a tool that would
| allow me to write code more easily that I never wanted
| written in the first place.
| dwattttt wrote:
| It's relevant when you have more complex objects, such as
| ones that contain independent mutexes that lock different
| sections of data.
|
| You want the object to present its valid operations, but
| the object could also be constructed in single or
| multithreaded situations.
|
| So you'd offer two APIs; one which requires a shared
| reference, and internally locks, and a second which
| requires a mutable reference, but does no locking.
|
| Internally the shared reference API would just lock the
| required mutexes, then forward to the mutable reference
| API.
| indigo945 wrote:
| This doesn't solve the deadlock problem, however.
| torginus wrote:
| Mutexes suffer from a host of problems, and imo are not a very
| good concurrency primitive - they were designed to turn single-
| threaded code into multi-threaded. With todays 8+ cores in most
| systems, usually a single point of contention quickly becomes a
| problem.
|
| They're liable to deadlocks/livelocks, and sometimes not only
| with other explicitly Mutex-like things (it might happen some
| library you use has a lock hidden deep inside).
|
| They're also often backed byOS primitives (with big overheads)
| with inconsistent behaviors between platforms (spinlocks,
| waiting etc). We've run into an issue with .NET, that their
| version of Mutex didn't wake up the blocked thread on Linux as
| fast as on Windows, meaning we needed about 100x the time to
| serve a request as the thread was sleeping too long.
|
| There are questions like when to use spinlocks and when to go
| to wait sleep, which unfortunately the developer has to answer.
|
| Not assigning blame here, just pointing out that threading
| primitives and behaviors don't translate perfectly between
| OSes.
|
| Multi-threading is hard, other solutions like queues suffer
| from issues like backpressure.
|
| That's why I'm skeptical about Rust's fearless concurrency
| promise - none of these bugs are solved by just figuring out
| data races - which are a huge issue, but not the only one.
| adwn wrote:
| Your view on mutex performance and overhead is outdated, at
| least for the major platforms: The Rust standard library
| mutex only requires 5 bytes, doesn't allocate, and only does
| a syscall on contention. The mutex implementation in the
| _parking_lot_ library requires just 1 byte per mutex (and
| doesn 't allocate and only does a syscall on contention).
| This enables very fine-grained, efficient locking and low
| contention.
| magicalhippo wrote:
| How does it avoid cache contention with just a few bytes
| per mutex? That is, multiple mutex instances sharing a
| cache line. Say I have a structure with multiple int32
| counters protected by their own mutex.
| adwn wrote:
| Cache contention is (mostly) orthogonal to your locking
| strategy. If anything, fine-grained locking has the
| potential to _improve_ cache contention, because
|
| 1) the mutex byte/word is more likely to be in the same
| cache line as the data you want to access anyway, and
|
| 2) different threads are more likely to write to mutex
| bytes/words in different cache lines, whereas in coarse-
| grained locking, different threads will fight for
| exclusive access over the cache line containing that one,
| global mutex.
|
| @magicalhippo: Since I'm comment-rate-throttled, here's
| my answer to your question:
|
| Typically, you'd artificially increase the size and
| alignment of the structure:
| #[repr(align(64))] struct Status {
| counter: Mutex<u32>, }
|
| This struct now has an alignment of 64, and is also 64
| bytes in size (instead of just the 4+1 required for
| _Mutex <u32>_), which guarantees that it's alone in the
| cache line. This is wasteful from a memory perspective,
| but can be worth it from a performance perspective. As
| often when it comes to optimization, it very heavily
| depends on the specific case whether this makes your
| program faster or slower.
| magicalhippo wrote:
| > different threads are more likely to write to mutex
| bytes/words in different cache lines
|
| If you got small objects and sequential allocation,
| that's not a given in my experience.
|
| Like in my example, the ints could be allocated one per
| thread to indicate some per thread status, and the main
| UI thread wants to read them every now and then hence
| they're protected by a mutex.
|
| If they're allocated sequentially, the mutexes end up
| sharing cache lines and hence lead to effective
| contention, even though there's almost no "actual"
| contention.
|
| Yes yes, for a single int you might want to use an atomic
| variable but this is just for demonstration purposes.
| I've seen this play out in real code several times, where
| instead of ints it was a couple of pointers say.
|
| I don't know Rust though, so just curious.
| gpderetta wrote:
| The issue might be allocating the int contiguously in the
| first place. No language magic is going to help you avoid
| thinking about mechanical sympathy.
|
| And allocating the int contiguously might actually be the
| right solution is the cost of sporadic false sharing is
| less than the cost of wasting memory.
|
| There's no silver bullet.
| magicalhippo wrote:
| But the mutex encapsulates the int, so if the mutex
| ensured it occupied a multiple of cache lines, there
| would be no contention. At the very small cost of a few
| bytes of memory.
| gpderetta wrote:
| the mutex forcing alignment would be extremely wasteful.
| FWIW, I have used 1-bit spin locks.
| torginus wrote:
| By not avoiding it. And a year later you get to write a
| blog post about how you discovered and fixed this
| phenomenon hitherto unknown to computer science.
| torginus wrote:
| These are OS primitives I'm talking about - I haven't
| checked out the standard library version but the
| parking_lot version uses a spinlock with thread sleep when
| the wait times get too high - it has no way of getting
| notified when the mutex gets unblocked nor does it support
| priority inversion.
|
| It seems it's optimized for scenarios with high performance
| compute heavy code, and short critical sections.
|
| These assumptions may let it win benchmarks, but don't
| cover the use cases of all users. To illustrate why this is
| bad, imagine if you have a Mutex protected resource that
| becomes available after 10us on average. This locks spins
| 10 times checking if it has become available )(likely <1us)
| then yields the thread. The OS (lets assume Linux) wont
| wake it up the thread until the next scheduler tick, and
| its under no obligation to do so even then (and has no idea
| it should). But even best-case, you're left waiting 10ms,
| which is a typical scheduler tick.
|
| In contrast OS based solutions are expensive but not that
| expensive, let's say that add 1us to the wait. Then you
| would wait 11us for the resource.
|
| A method call taking 10ms and one taking 15 us is a factor
| of 60x, which can potentially kill your performance.
|
| You as the user of the library are implicitly buying into
| these assumptions which may not hold for your case.
|
| There's also nothing in Rust that protects you from
| deadlocks with 100% certainty. You can fuzz them out, and
| use helpers, but you can do that in any language.
|
| So you do need to be mindful of how your mutex works, if
| you want to build a system as good as the one it replaces.
| galangalalgol wrote:
| The best practices I adopt for rust avoid the use of
| mutex whenever possible precisely because of how easy a
| deadlock is. It turns out it is always possible. There
| are entire languages the disallow any mutable state, much
| less shared mutable state. The question becomes how much
| performance are you willing to sacrifice to avoid the
| mutex. By starting with no shared mutable state and
| adding it when something is too slow, you end up with
| very few mutexes.
| adwn wrote:
| > _avoid the use of mutex [...] It turns out it is always
| possible_
|
| How would you handle the archetypical example of a money
| transfer between two bank accounts, in which 100 units of
| money need to be subtracted from one account and
| atomically added to another account, after checking that
| the first account contains at least 100 units?
| galangalalgol wrote:
| The simplest pure functional way would be to copy the
| whole database instantiating a new copy with the desired
| change if the condition was met. That obviously doesn't
| scale, which is where the performance thing comes in. A
| still pure way would be to use a persistent tree or hash
| mapped trie that allows efficient reuse of the original
| db. There are times a purely functional approach doesn't
| perform well enough, but even with large scale entity
| component type systems in both rust and c++, the number
| of times I've had to use a mutex to be performant is
| small. Atomic is much more common, but still not common.
| Persistent data structures alleviate most of the need.
| pas wrote:
| pure or not eventually this comes down to durability, no?
|
| and the way to do it is to either have some kind single-
| point-of-control (designated actor or single-threaded
| executor) or mark the data (ie. use some concurrency
| control primitive either wrapping the data or in some
| dedicated place where the executors check [like JVM's
| safepoints])
|
| using consistent hashing these hypothetical accounts
| could be allocated to actors and then each transaction is
| managed by the actor of the source (ie. where the money
| is sent from, where the check needs to happen), with
| their own durable WAL, and periodically these are
| aggregated
|
| (or course then the locking is hidden in the maintenance
| of the hashring as eating philosophers are added/removed)
| kragen wrote:
| Eliminating the durability constraint doesn't make it any
| easier to program, just easier to get good performance
| on.
|
| Distributing accounts among different actors, without
| two-phase commit or its moral equivalent, enables check
| kiting.
| vrmiguel wrote:
| Since the thread mentions Rust: in Rust, you often
| replace Mutexes with channels.
|
| In your case, you could have a channel where the Receiver
| is the only part of the code that transfers anything.
| It'd receive a message Transfer { from: Account, to:
| Account, amount: Amount } and do the required work. Any
| other threads would therefore only have copies of the
| Sender handle. Concurrent sends would be serialized
| through the queue's buffering.
|
| I'm not suggesting this is an ideal way of doing it
| adwn wrote:
| > _[...] but don 't cover the use cases of all users._
|
| No single concurrency primitive covers _all_ use cases. I
| was addressing your misconceptions about mutex
| performance and overhead, not whether mutexes are the
| best solution to your particular problem.
|
| > _[...] it has no way of getting notified when the mutex
| gets unblocked [...] The OS (lets assume Linux) wont wake
| it up the thread until the next scheduler tick, and its
| under no obligation to do so even then (and has no idea
| it should)._
|
| You've misunderstood the parking_lot implementation. When
| thread B tries to lock a mutex that's currently locked by
| thread A, then, after spinning a few cycles, thread B _"
| parks" itself_, i.e., it asks the kernel to remove it
| from the Runnable task queue. On Linux, this is done
| using the futex syscall. When thread A unlocks the mutex,
| it detects that another thread is waiting on that mutex.
| Thread A takes one thread from the queue of waiting
| threads and " _unparks_ " it, i.e., it asks the kernel to
| move it into the Runnable task queue. The kernel is
| notified _immediately_ , and if there's a free CPU core
| available, will tend to dispatch the thread to that core.
| On a non-realtime OS, there's no guarantee how long it
| takes for an unblocked thread to be scheduled again, but
| that's the case for _all_ concurrency primitives.
| Someone wrote:
| > A method call taking 10ms and one taking 15 us is a
| factor of 60x
|
| 667 (a thousand 15ms calls take 15ms)
| ahoka wrote:
| It's called a futex and supported by both Linux and Windows
| since ages.
| adwn wrote:
| The 1-byte-per-mutex parking_lot implementation works
| even on systems that don't provide a futex syscall or
| equivalent.
| kiitos wrote:
| this is an overly simplistic and somewhat reductive
| perspective on a pretty fundamental concept/primitive
|
| > they were designed to turn single-threaded code into multi-
| threaded
|
| not really
|
| > usually a single point of contention quickly becomes a
| problem.
|
| not generally, no
|
| > They're liable to deadlocks/livelocks,
|
| deadlocks/livelocks are orthogonal to any specific primitive
|
| > They're also often backed byOS primitives (with big
| overheads) with inconsistent behaviors between platforms
| (spinlocks, waiting etc).
|
| the mutex as a primitive is orthogonal to any specific
| implementation...
|
| etc. etc.
| kragen wrote:
| _Traditionally_ traditionally, monitors were declared together
| with the data they contained, and the compiler enforced that
| the data was not accessed outside the monitor. Per Brinch
| Hansen wrote a rather bitter broadside against Java 's
| concurrency model when it came out.
| csb6 wrote:
| Was this the article?
|
| http://brinch-hansen.net/papers/1999b.pdf
| kragen wrote:
| This is a toned-down, but still scathing, version of what I
| remember reading.
| stackghost wrote:
| >Ugh, a correct transfer function should conceptually just be the
| composition of our well encapsulated withdraw and a deposit
| functions, but defining it correctly has forced us to remove the
| locking from both withdraw and deposit, making both of them less
| safe to use.
|
| I know OOP isn't cool any more, but the above is what OOP solves.
|
| TFA's transfer() and withdraw() functions aren't compliant with
| double-entry accounting anyways, so you'd mark them private and
| only expose Transfer to callers. Let the class handle its own
| details.
| mrkeen wrote:
| > I know OOP isn't cool any more, but the above is what OOP
| solves.
|
| The article was a lengthy explanation of why the problem occurs
| in _non-STM_ settings. In OO settings.
|
| What do you propose as an OO solution?
| kragen wrote:
| OOP does not provide any help in solving the problem in
| question, and indeed encapsulation is a major obstacle to
| solving it. I think you haven't understood what problem is
| being discussed.
| mgaunard wrote:
| tl;dr mutexes are evil because they don't compose, STM is the
| solution because it does compose, otherwise just avoid shared
| state, or even state entirely.
|
| Not anything that's not already covered in any undergraduate CS
| course.
| kreetx wrote:
| Which CS course did you go to?
| mgaunard wrote:
| You made me check the programme of many university courses.
|
| Most parallel programming courses are at Masters level and
| require specialization, but those are much more advanced
| (sorting networks, distributed computing on supercomputers
| and GPU, consensus algorithms, parallelization of linear
| solvers...)
|
| There are undergraduate courses that cover simple things like
| multithreading patterns in mainstream languages, but it seems
| they are only available if you go to an education institution
| with both a practical mindset and a firm grip of the
| fundamentals, which is unfortunately quite rare, as most
| institutions tend to specialize in one or the other.
| pessimizer wrote:
| During my undergrad this stuff was heavily covered in my
| Intro to Operating Systems class. But that was 20 years
| ago, may be different now.
| niek_pas wrote:
| I've never taken an undergraduate CS course so I'm happy to
| have read this!
| foota wrote:
| I've found it interesting to think about trying to adopt data
| structures like CRDT designed for distributed systems to the
| problem of local consistency between CPU-local data structures
| spread across cores for parallelism.
| sebstefan wrote:
| >STM is an optimistic concurrency system. This means that threads
| never block waiting for locks. Instead, each concurrent operation
| proceeds, possibly in parallel, on their own independent
| transaction log. Each transaction tracks which pieces of data it
| has accessed or mutated and if at commit time it is detected that
| some other transaction has been committed and altered data which
| this transaction also accessed, then the latter transaction is
| rolled back and is simply retried.
|
| I already foresaw (and it gets mentioned later), the problem that
| if you have many small, frequent operations, they will prevent a
| big, long operation from happening because they will always
| change the state and cause conflicts before the big one can
| finish. You can easily code yourself an app that will softlock
| forever.
|
| The post doesn't offer a good solution (it talks about one where
| there's a tradeoff, but you don't need tradeoffs for this)
|
| The way this gets fixed it is to make the lock acquisition (or in
| this case, the priority of the merge?) preemptive (Wound-Wait)
|
| All transactions have a global, ever incrementing number attached
| to them, their ID, which we call "seniority", if you try to get a
| a lock, and the lock is held by a transaction with a lower
| seniority (=> a higher ID), you kill the transaction, take the
| lock, and once you're done the transaction you killed is allowed
| to retry
|
| In the meantime, if a transaction with the lower seniority tries
| to get the lock, it gets blocked
|
| This insures that your program will always finish.
|
| In the case of "lots of frequent small transactions" + "one big,
| long report", the report will get killed a few times, until your
| report inevitably becomes the most senior transaction to ask for
| this resource, and is allowed to complete.
| internet_points wrote:
| https://old.reddit.com/r/haskell/comments/1oujfmi/mutexes_su...
| apparently "optimistic" glosses over some details
| benmmurphy wrote:
| databases also solve this problem by allowing the read only
| transaction to see a consistent snapshot of the data at some
| point in time. so the reader can do its work without needing a
| logical mutex.
| kragen wrote:
| Yeah, STMs can use MVCC in the same way, but that doesn't
| solve the long-transaction problem for bulk update
| transactions. The first example in Gray and Reuter is adding
| monthly interest to every savings account, exactly once.
| pas wrote:
| long-running transactions are a really fundamentally bad
| idea if the length of the transaction scales with the
| number of accounts/users. these things need to be batched
| up in a way that code with at-least-once semantics
| correctly solves the problem. (in one TX handle just a
| bunch of accounts, calculating the interest, recording that
| these accounts already got the interest, and moving to the
| next batch.)
|
| the problem with STM (or any other concurrency control
| mechanism) is that it's not their job to solve these issues
| (as usually they require conscious product design), so they
| are not going to solved by "just use STM".
| kragen wrote:
| There are some different approaches, but that's
| definitely one of them. However, it's not without its
| tradeoffs; you're sacrificing atomicity, in the sense
| that if updating some account throws an error, the
| already-updated accounts stay updated.
| mrkeen wrote:
| > databases also solve this problem
|
| They could, but are typically configured not to.
|
| With default settings they let other processes' commits cut
| into the middle of your read. (See READ_COMMITTED.). I was
| flabbergasted when I read this.
|
| When you turn MSSQL up to a higher consistency level you can
| get it to grind to a halt and throw Deadlock exceptions on
| toy code. (Moving money between accounts will do it).
|
| I used to advertise STM as like ACID-databases for shared-
| memory systems, but I guess it's even more consistent than
| that.
| kragen wrote:
| Postgres's `set transaction isolation level serializable`
| doesn't result in deadlocks (unless you explicitly grab
| locks) but does require your application to retry
| transactions that fail due to a conflict.
| timeon wrote:
| > If the programmer forgets to lock the mutex the system won't
| stop them from accessing the data anyways, and even then there's
| no actual link between the data being locked and the lock itself,
| we need to trust the programmers to both understand and respect
| the agreement. A risky prospect on both counts.
|
| Not every language is like that. I would not use language (that
| allows this) for parallel programming. You can use other ways but
| how can you guarantee everyone who will edit the code-base will
| not use unenforced mutex?
| philippta wrote:
| The fundamental problem here is shared memory / shared ownership.
|
| If you assign exclusive ownership of all accounting data to a
| single thread and use CSP to communicate transfers, all of these
| made up problems go away.
| torginus wrote:
| CSP suffers from backpressure issues (which is not to say its
| bad, but it's not a panacea either)
| mrkeen wrote:
| Yes, multithreaded problems go away on a single thread.
|
| Is there any way for an external thread to ask (via CSP) for
| the state, think about the state, then write back the new state
| (via CSP)?
|
| If so, you're back to race conditions - with the additional
| constraints of a master thread and CSP.
| philippta wrote:
| That would be shared ownership again.
| mrkeen wrote:
| So then I would sell STM to you from the "other end".
|
| Everyone else has multiple threads, and should replace
| their locks with STM for ease and safety.
|
| You've got safe single-thread and CSP, you should try STM
| to gain multithreading and get/set.
| icar wrote:
| I would've enjoyed if the solution was proposed in Go as well.
| WhyNotHugo wrote:
| Indeed, "Software Transactional Memory" sounds like a magic
| black box. It feels a bit like reading "just use an sql
| database and transactions". It's not really telling me how the
| problem is solved, just to use someone else's solution without
| understating how it's implemented.
| internet_points wrote:
| https://www.microsoft.com/en-us/research/wp-
| content/uploads/...
| garethrowlands wrote:
| STM isn't really used in Go like it is in Haskell.
|
| Here's the example from a Go STM package that's based on
| Haskell STM. It has gotchas that you won't encounter in Haskell
| though, due to the nature of these languages.
|
| https://github.com/anacrolix/stm/blob/master/cmd/santa-examp...
|
| For the equivalent Haskell, check out the link at the top of
| the file.
| torginus wrote:
| I wonder what happened to hardware transactional memory. Your CPU
| caches already keep track of which core is keeping which line in
| its cache and whether they have modified it via the MESI
| protocol:
|
| https://en.wikipedia.org/wiki/MESI_protocol
|
| So it has most of the hardware to support transactional memory,
| only it's not exposed to the user.
|
| Intel had their own version, but it turned out it was buggy, so
| they disabled it and never put it in any subsequent CPU so that
| was that.
| agalunar wrote:
| > A data race occurs any time two threads access the same memory
| location concurrently and non-deterministically when at least one
| of the accesses is a write.
|
| From what I understand of the C++ memory model (shared by C and
| Rust), this is not the definition of _data race_ - a data race
| occurs when two or more threads access memory concurrently where
| at least one access is a write and the accesses are
| _unsynchronized._ However, synchronized accesses may not have a
| deterministic ordering, in which case a _race condition_ occurs.
|
| (Confusing as it may be, I believe this is standard terminology.)
| Surac wrote:
| c# offers a very convinient way to pack data access and locking
| into one thing. the "lock" instruction. it does hover not let you
| lock more that one "resource" at a time.
| mrkeen wrote:
| All of the "my non-Haskell language does this" comments in the
| thread are the same (with maybe a Rust exception).
|
| The "lock" instruction is what the article is telling you to
| ditch.
|
| > If the programmer forgets to lock the mutex the system won't
| stop them from accessing the data anyways
|
| If the programmer forgets to "lock"
|
| > and even then there's no actual link between the data being
| locked and the lock itself
|
| lock (thing) { return thing.contents // shared, mutable array
| given out freely to the world }
|
| 'contents' has no notion that it has anything to do with this
| "lock"ing thing.
| Surac wrote:
| OK, seems my english was not enough to see the point. after
| reading this clear explantation is have to agree with you
| 100%
| nerdralph wrote:
| It is rather long-winded, and ends with a donation request. I
| don't like that style of writing.
| gpderetta wrote:
| In C++ (with a bit of help from boost). bool
| transfer(boost::synchronized<Account>& sync_from,
| boost::synchronized<Account>& sync_to, int amount) {
| auto [from, to] = synchronize(sync_from, sync_to);
| if (from->withdraw(amount)) { to->deposit(amount)
| return true; } else { return false
| } }
|
| Hopefully synchronized will make it into the standard library at
| some point, in the meantime it is not terribly hard to write it
| yourself if you do not want a boost dependency.
| xmcqdpt2 wrote:
| Does this avoid the dining philosopher deadlock?
| gpderetta wrote:
| yes, 'synchronize' uses a try_lock/backoff algorithm, same as
| std::scoped_lock.
|
| edit: it could theoretically livelock, but I believe most if
| not all STM implementations also do not guarantee forward
| progress.
| kragen wrote:
| Purely optimistic STM implementations that abort
| transactions early and don't permit other transactions to
| read uncommitted data _can_ guarantee forward progress, and
| I believe that both Haskell 's STM and Fraser and Harris's
| STM _do_ , though I could easily be mistaken about that.
| gpderetta wrote:
| Probably you are right. I vaguely remembered the "Why
| Transactional Memory Should Not Be Obstruction-Free"
| paper, but I might have misunderstood or forgotten what
| it meant (the implementation can be non obstruction-free,
| but it doesn't mean it can live-lock).
| kragen wrote:
| You avoid livelock, as I understand the term in an STM,
| if the only thing that can prevent a transaction from
| committing when it tries to commit is some other
| transaction having committed. That way, forward progress
| is guaranteed; as long as _some_ transaction commits, you
| 're not livelocked, are you?
|
| I'm not familiar with "obstruction-free"ness; should I
| be?
| psychoslave wrote:
| >Moore's law is dying, beefy single cores are no longer keeping
| up.
|
| On the other hand, there are many other things that could be done
| to avoid wasting all the extra power gained over the years which
| don't even require any parallelism boost.
| kragen wrote:
| Maybe, but on a 64-core machine, a single-threaded task can't
| even use 2% of the computer. Even interpreted Python wastes
| only 97% of the computer.
| kragen wrote:
| I've been excited about STMs since I read "Composable Memory
| Transactions" back in 02005, shortly before it was published, and
| I still believe in the glorious transactional future, but it's
| difficult to adopt an STM piecemeal; it kind of wants to own your
| entire program, the same way that garbage collection or
| nonblocking I/O do, and more so than multithreading with locks.
| You kind of have to commit entirely to an STM. The efforts in C#
| to adopt an STM ending around 02010 were a disaster as a result.
|
| The article says a couple of things about STMs that are not true
| of STMs in general, just true of the Haskell STM the author is
| familiar with, like a small Brazilian child confidently telling
| you that almost everyone speaks Portuguese.
|
| One of these is, "STM is an optimistic concurrency system." The
| possibility of making your concurrency 100% lock-free is one of
| the most appealing things about STMs, and I think it could be a
| key to solving the UI latency problem, which just keeps getting
| worse and worse. Actors and CSP don't normally help here; an
| Actor is just as "blocking" as a lock. But you can implement an
| STM with partly pessimistic concurrency, or purely pessimistic,
| and it might even be a good idea.
|
| Another is, "One last benefit of STM which we haven't yet
| discussed is that it supports intelligent transaction retries
| based on conditions of the synchronized data itself." This was an
| innovation introduced by "Composable Memory Transactions", and
| many STMs do not support it, including Keir Fraser's awesomely
| fast version. I am even less certain that it is the correct
| tradeoff for all uses than I am about purely optimistic
| synchronization.
|
| But all of this is why I'm rereading Gray and Reuter's
| _Transaction Processing_ right now after 25 years. With the
| benefit of 35 years of hindsight, it 's a frustrating mix of
| inspiring long-range vision and myopic boneheadedness. But it
| shares a lot of hard-won wisdom about problems like long
| transactions that pop up in a new guise in STMs.
| delichon wrote:
| > 02005, 02010
|
| Are you planning for this comment to be relevant for long
| enough that the leading zeros will be helpful for
| disambiguation?
| kragen wrote:
| I try not to post comments that won't be relevant 8000 years
| in the future.
| lann wrote:
| Clearly they are referring to the years 1029 and 1032
| (decimal). I just want to know what calendar system they're
| using...
| kragen wrote:
| Since the reunification of China under the most glorious of
| all the dynasties, perhaps? Or the founding of Chichen
| Itza?
| hazn wrote:
| interesting, the notation also implies 2005 and 2010 A.D and
| not B.C, or maybe the notation is about exactly A.D? either
| way, interesting choice if it was intentional. we say "year
| 515" without disambiguation right
| 201984 wrote:
| He does it to get somebody to comment, and it worked this
| time.
| kragen wrote:
| I _wish_ people would comment about transactions,
| optimistic synchronization, CSP, actors, priority
| inversion, Fraser 's astounding code (https://www.cl.cam.ac
| .uk/research/srg/netos/projects/archive...) etc., but I
| guess we each do what we can, and you have to meet people
| where they are. I probably couldn't have posted a comment
| any better than yours when I was 12, and maybe you're 12,
| so maybe you can't do any better. Hopefully, eventually,
| you will.
|
| Remember! Brawndo: It's What Plants Crave.
| vacuity wrote:
| I think writing concurent programs will always be a hard
| problem, relative to the difficulty of writing non-concurrent
| programs, and the only "solution" is to isolate, minimize, and
| regulate contention. The implementation details of TM, locks,
| monitors, semaphores, actors, message queues, transactions,
| etc., are _at best_ "distractions", at worst hindrances. I
| think a good model of a concurrent program, one that lends
| itself to writing the program simply, will be applicable across
| many different implementations. Anything that obscures the
| development of such a model is harmful. Worst of all is the
| sheer prevalence of shared resources (especially shared
| memory). Sharing brings contention, so control sharing.
| kragen wrote:
| I don't agree that whether you're using TM, shared-memory
| monitors, or actors with message queues is an implementation
| detail or that there is a better programming model that hides
| the differences between them. You can implement any of them
| on top of any of the others, but you're still programming to
| whatever model you put on top.
| vacuity wrote:
| In the implementation that runs, yes, you have to choose
| something. However, I think the fundamental design is
| independent of those options, and probably should be
| developed independently.
| kragen wrote:
| On the problems with STM in C#, see
| https://joeduffyblog.com/2010/01/03/a-brief-retrospective-on...
| (I can't believe nobody else has posted this link yet). As with
| the Chris Penner article, there are a lot of things described
| as features of STMs in general which are actually just
| properties of the STM he worked on, which explains some of the
| things that sound like nonsense if you've only worked with
| Haskell's STM or Clojure's. (Duffy is much better about
| delineating the boundaries of the systems he's talking about,
| though, because he knows there are alternatives.)
|
| See also https://www.infoq.com/news/2010/05/STM-Dropped/.
| isuckatcoding wrote:
| It kind of threw me off that we went from golang to Haskell so
| would love to see they bank example actually comeback full circle
| to golang
| nlitened wrote:
| I think STM looks pretty cool in toy examples, but in practice
| it's pretty bad. Very difficult for me to make strong logical
| argument about this, just based on how it feels.
|
| In Clojure, there are first-class STM primitives with retriable
| transactions in the standard lib, but every time I tried to use
| them, the code was slow, difficult to reason about, full of
| "colored" functions, and non-composable.
|
| Also, unintuitively, the code starts immediately smelling
| slightly "wrong", as if I tried to put into code my first
| childish thoughts instead of actually thinking about how the
| system _should_ work. Like the notorious bank accounts example:
| yeah, cool, two accounts' balances are TVars -- but what if I
| have 10M accounts? Is the list of accounts also a TVar since
| sometimes new accounts are created? Are indexes on the list also
| TVars? How do you persist the lists, how do you synchronize slow
| persistence and in-memory transactions? How do you synchronize
| all the deposits with a backup/failover machine? As you continue
| trying to untangle all this with STM, you start drowning in
| complexity, and encountering huge contention among transactions
| touching many parts of your systems, and spewing countless retry
| logs.
|
| It's not only my own experience -- I believe it's widely accepted
| in Clojure community that nobody actually uses STM, and instead
| uses simple atomic updates or queues.
| blandflakes wrote:
| I have a similar experience, though it's not necessarily
| related to shortcomings in the STM implementation (which I
| always felt was quite competent in the space). To expand, I
| think a few things are true:
|
| 1. I've come to feel that actually managing concurrency is a
| "below the fold" competency for most engineers. I'm not saying
| it _should_ be, and people need to understand what a data race
| is. 2. As you pointed out, most state in services that I work
| on is not in need of coordinated updates, so atoms are where
| you want to be, or single-writer queues. 100% agreed 3.
| Structured concurrency approaches, fibers, and other abstracted
| runtimes seem to be the best way to get concurrent and parallel
| work, given points 1 and 2 about how coordinated state really
| isn 't that common for most work on my team
|
| A lot of times when we're updating state we're more having to
| think about it in the database, employing either a last-write-
| wins strategy or when coordination is in need, some form of
| pessimistic or optimistic locking.
| kragen wrote:
| Thanks, this is very valuable! Have you tried other
| implementations of STM such as Haskell's?
| nlitened wrote:
| I don't have any experience with STMs in other languages, but
| as far as I can see, the ideas are very similar in Haskell
| and Kotlin implementations, for example, so I guess the
| downsides are the same as well
| kragen wrote:
| What I've heard from Haskell programmers suggests that the
| downsides are very different, even though the ideas are
| very similar.
| brabel wrote:
| Bummer, I thought for a second that we had found a magic bullet
| for all our concurrency problems!
| dwohnitmok wrote:
| I chalk this up to problems with Clojure's STM API specifically
| rather than STM generally, e.g. Haskell's STM implementation is
| considerably more useful.
|
| I go into more detail here:
| https://news.ycombinator.com/item?id=35805138 and
| https://news.ycombinator.com/item?id=32759886
| tonymet wrote:
| A nice trick in go is embedding sync.mutex into any type eg
| account.Lock() / Unlock()
| bluGill wrote:
| Shared data is hard no matter what you do. Parallelism and shared
| data do not mix. STM makes some things easier, but you still will
| run into problems if you have a lot of it. You must design your
| code such that you spend a lot of CPU cycles doing single thread
| no shared data calculations between every place where you need to
| share data. If you can't do that you can't do parallelism.
|
| When there are only a few places where data needs to be shared a
| mutexs works since you put your best programmer on maintaining
| just that code and with only a few places they can figure out it.
| You can also make a variable atomic, which sometimes works better
| than a mutex and sometimes worse. You can use STM. However no
| matter what you use the reality of synchronizing between cores
| means you can't do any of the above "very often".
| taeric wrote:
| Optimistic locking works great when what is excluded is
| effectively a calculation. The annoyance, though, is you have
| basically acknowledged that you can use a compare-and-swap at the
| end of your calculation to know that things worked.
|
| This is not at all always the case, though. Sometimes, what you
| need to use mutual exclusion for is actively working with
| something that is, itself, active work. That is, sometimes you
| have to stop something even starting.
|
| Think about how you would model a laundry mat. It isn't enough to
| say you could use optimistic locks for access to the machines.
| You can't use the machine until you can use the machine.
|
| This is not unheard of in computing, either. Sometimes, you can't
| process a buffer until you know it has been finished by the code
| before you.
___________________________________________________________________
(page generated 2025-11-18 23:01 UTC)