14 comments

  • lifis9 hours ago
    I can&#x27;t understand why address instability is a problem: if a Mutex is moved, then it can&#x27;t be locked (because you need to hold a borrow while locked, which impedes moving), so using addresses is perfectly fine and there is absolutely no need to use IDs.<p>Also the fact that it doesn&#x27;t detect locking the same mutex twice makes no sense: a static order obviously detects that and when locking multiple mutexes at the same level all you need to do is check for equal consecutive addresses after sorting, which is trivial.<p>Overall it seems like the authors are weirdly both quite competent and very incompetent. This is typical of LLMs, but it doesn&#x27;t seem ZlLM-made.
    • Guvante9 hours ago
      Don&#x27;t address introduce ambiguous locking order across attempts?<p>While not obviously problematic, that seems weird enough you would need to validate that it is explicitly safe.
      • Seattle35032 hours ago
        If I need to grab 100 locks, they are all moving around a lot, but I&#x27;ve got the first 10, will the order be the same for someome trying to get the same 100? Eg maybe someone swaps two that neither of us has grabbed yet.
    • drzaiusx117 hours ago
      Doesn&#x27;t multiple lock support then not make it a mutex anymore? I thought that becomes a monitor lock instead? I forget how standardized the terminology is though, there may be leeway in the mutex definition already.
    • noodletheworld59 minutes ago
      &gt; Overall it seems like the authors are weirdly both quite competent and very incompetent<p>This is an unusually hostile take.<p>The authors comment about address instability is only a minor point in the article:<p>&gt; happylock also sorts locks by memory address, which is not stable across Vec reallocations or moves.<p>…specifically with regard to happylock, which has a bunch of commentary on it (1) around the design.<p>You&#x27;re asserting this is a problem that doesn&#x27;t exist <i>in general</i>, or specifically saying the author doesn&#x27;t know what they&#x27;re talking about with regard to happylock and vecs?<p>Anyway, saying they&#x27;re not competent feels like a childish slap.<p>This is a well written article about a well written library.<p>Its easy to make a comment like this without doing any research or actually understanding whats been done, responding to the title instead of the article.<p><i>Specifically</i> in this regard, why do you believe the approach taken here to overcome the limitations of happylock has not been done correctly?<p>(1) - <a href="https:&#x2F;&#x2F;github.com&#x2F;botahamec&#x2F;happylock" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;botahamec&#x2F;happylock</a>
    • gsliepen7 hours ago
      What about mutexes living in shared memory, and each process having a different address mapping?
      • loeg7 hours ago
        All bets go out the window with adversarial multi-process shared memory mutexes. The other process may not even be running the same locking code.
  • jcalvinowens11 hours ago
    The Level&lt;&gt; abstraction is a really neat way to have your cake and eat it too: you only need a consistent arbitrary order to avoid deadlocks, but the order can have performance consequences when some locks are more coarse than others.<p>But the example seems backwards to me: unless every callsite that locks any item always locks the big global lock first (probably not true, because if you serialize all item access on a global lock then a per-item lock serves no purpose...), aren&#x27;t you begging for priority inversions by acquiring the big global lock before you acquire the item lock?<p>My only gripe is missing the obvious opportunity for Ferengi memes (&quot;rules of acquisition&quot;) :D :D
    • gpm6 hours ago
      &gt; unless every callsite that locks any item always locks the big global lock first (probably not true, because if you serialize all item access on a global lock then a per-item lock serves no purpose...)<p>A pattern I&#x27;ve definitely both seen and used is<p><pre><code> let guard1 = datastructure_containing_the_whole_world.lock(); let guard2 = guard1.subset_of_that_datastructure.lock(); guard1.unlock(); &#x2F;&#x2F; Do expensive work guard2.unlock(); </code></pre> Which works to parallelize work so long as guard2 isn&#x27;t contended... and at least ensures correctness and forward progress the rest of the time.
    • vlovich12310 hours ago
      There’s no global lock. There’s a linear MutexKey&lt;N&gt; that a lock of Level &gt;= N has to be acquired with. Aquiring it consumes MutexKey&lt;N&gt; and hands you back MutexKey&lt;Level+1&gt; where Level is the N of the level you’re locking.<p>There’s no priority inversion possible because locks can only ever be held in decreasing orders of priority - you can’t acquire a low priority lock and then a high priority lock since your remaining MutexKey won’t have the right level.
      • jcalvinowens10 hours ago
        In the example it seems pretty clear to me that:<p><pre><code> Mutex::new(AppConfig::default()); </code></pre> ...is meant to be acquiring a mutex protecting some global config object, yes? That&#x27;s what I&#x27;m calling a &quot;global lock&quot;.<p>&gt; There’s no priority inversion possible because locks can only ever be held in decreasing orders of priority<p><pre><code> T1 T2 -- -- small_lock(); big_lock(); small_lock(); &lt;--- Spins waiting for T1 </code></pre> ...and now any other thread that needs big_lock() spins waiting for T2 to release it, but T2 is spinning waiting for T1 to release the (presumably less critical) small lock.<p>If small_lock is never ever acquired without acquiring big_lock first, small_lock serves no purpose and should be deleted from the program.
        • cryptonector58 minutes ago
          &gt; In the example it seems pretty clear to me that:<p>&gt; Mutex::new(AppConfig::default());<p>&gt; ...is meant to be acquiring a mutex protecting some global config object, yes? That&#x27;s what I&#x27;m calling a &quot;global lock&quot;.<p>You could certainly have a global lock at the top-most level, but you&#x27;re not required to. The example is just an example.
        • vlovich1236 hours ago
          Mutex::new creates a lock, it doesn’t acquire one.<p>Look at the API - if big_lock and small_lock are at the same level, you would need to acquire the lock simultaneously for both locks which is accomplished within the library by sorting* the locks and then acquiring. If you fail to acquire small_lock, big lock isn’t held (it’s an all or nothing situation). This exact scenario is explained in the link by the way. You can’t bypass the “acquire simultaneously” api because you only have a key for one level<p>Your terminology is also off. A lock around a configuration is typically called a fine grained lock unless you’re holding that lock for large swathes of program. Global as it refers to locking doesn’t refer to visibility of the lock or that it does mutual exclusion. For example, a lock on a database that only allows one thread into a hot path operation at a time is a global lock.<p>* sorting is done based on global construction order grabbed at construction - there’s a singleton atomic that hands out IDs for each mutex.
          • jcalvinowens5 hours ago
            No, the entire point of what I was saying is that big_lock and little_lock are at two different levels.
            • vlovich1233 hours ago
              If big lock and little lock are at different levels you won’t have a key at the appropriate level to create an inversion by trying to acquire in the first place.<p>T2 might “spin” waiting for small lock but assuming small lock is released at some point you’ve not got a deadlock (and by construction it’s impossible for small lock to have it’s release blocked on the acquisition of a lock that depends on big_lock).<p>That’s the whole point of having a level to the locks and to the key that you have to give up to acquire that lock.<p>Your terminology is also off. Mutexes are not implemented through spin locks. It’s an atomic operation and when lock acquisition fails you call futex_lock (or whatever your OS api is) to have the thread be put to sleep until the lock is acquired.
        • bonzini9 hours ago
          Usually a global lock is a lock that is taken outside all others and is taken for large parts of the runtime (or even, everywhere the thread isn&#x27;t waiting on a condition variable, file descriptor and the like).<p>Mutex::new(AppConfig::default()) might very well be a small, leaf mutex.
  • vlovich12310 hours ago
    I feel like Fuschia’s DAG approach can still be made compile time lock free by either disallowing holding locks from different branches or requiring an ordering when that does happen to prevent cycles (ie you can’t acquire them independently, you have to acquire all independent branches as a single group.
  • EffCompute9 hours ago
    I really agree with jandrewrogers&#x27; point about the insularity of the database domain. While working on a custom C++ engine to handle 10M vectors in minimal RAM, I’ve noticed that many &#x27;mainstream&#x27; concurrency patterns simply don&#x27;t scale when cache-locality is your primary bottleneck.<p>In the DB world, we often trade complex locking for deterministic ordering or latch-free structures, but translating those to general-purpose app code (like what this Rust crate tries to do) is where the friction happens. It’s great to see more &#x27;DB-style&#x27; rigour (like total ordering for locks) making its way into library design.
  • cptroot11 hours ago
    I appreciate that this appears to be an incremental improvement on Fuschia&#x27;s tree_lock, with the sharp edges sanded off. Good work! I hope I won&#x27;t have to use it :p
  • accelbred7 hours ago
    Most of the deadlocks I&#x27;ve faced are with different proccesses&#x2F;devices both waiting on reads from each end of a socket&#x2F;uart&#x2F;etc. I&#x27;ve taken to putting timeouts on read calls, though then you have to deal with legitimate long request cycles timing out.
  • Groxx10 hours ago
    &gt;<i>Why a Total Order, Not a DAG?</i><p>&gt;<i>This is a deliberate design decision. lock_tree uses a DAG, which lets you declare that branches A and B are independent — neither needs to come before the other. Sounds great, but it has a subtle problem: if thread 1 acquires A then B, and thread 2 acquires B then A, and both orderings are valid in the DAG, you have a deadlock that the compiler happily approved.</i><p>Would it be possible to build one at compile time? Static levels seem like they won&#x27;t let you share code without level-collaboration, so that might be kinda important for larger-scale use.<p>I don&#x27;t know enough about Rust&#x27;s type system to know if that&#x27;s possible though. Feels like it&#x27;s pushing into &quot;maybe&quot; territory, like maybe not with just linear types but what about proc macros?<p>I can definitely see why it&#x27;s easier to build this way though, and for some contexts that limitation seems entirely fine. Neat library, and nice post :)
    • expede6 hours ago
      (Author here). Early in development I did exactly this with a macro. It was confusing when you wanted to refactor the code to change lock orders, harder to make clear error messages, and so on. Forcing the user to assign in a level means that it&#x27;s clear(er?) to users what&#x27;s happening, we don&#x27;t need fancy (and difficult to debug) macro magic, and users can still do the linearisation themselves. That&#x27;s the HOPE at least.<p>IMO compile time locking levels should be preferred whenever possible... but the biggest problem with compile time levels is that they, well, check at compile time. If you need to make mutexes at runtime (eg mange exclusive access to documents uploaded to a server by users) then you need to be able to safely acquire those too (provided in surelock with LockSet).
  • electromech10 hours ago
    I&#x27;m intrigued! I was fighting deadlocks in some Java code this week, and I&#x27;m working on a Rust project to maybe replace some of that.<p>One thing I didn&#x27;t see in the post or the repo: does this work with async code?<p>I couldn&#x27;t find the &quot;search&quot; button on Codeberg, and tests&#x2F;integration.rs didn&#x27;t have any async.<p>For embedded, I have had my eye on <a href="https:&#x2F;&#x2F;github.com&#x2F;embassy-rs&#x2F;embassy" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;embassy-rs&#x2F;embassy</a> (which has an async runtime for embedded) and would love a nice locking crate to go with it.
    • cbarrick9 hours ago
      IIUC, this crate has similar restrictions to the std Mutex. So it depends on what you mean by &quot;work with async code.&quot;<p>First, lock acquisition seems to be a blocking method. And I don&#x27;t see a `try_lock` method, so the naive pattern of spinning on `try_lock` and yielding on failure won&#x27;t work. It&#x27;ll still work in an async function, you&#x27;ll just block the executor if the lock is contested and be sad.<p>Second, the key and guard types are not Send, otherwise it would be possible to send a key of a lower level to a thread that has already acquired a lock of a higher level, allowing deadlocks. (Or to pass a mutex guard of a higher level to a thread that has a key of a lower level.)<p>Therefore, holding a lock or a key across an await point makes your Future not Send.<p>Technically, this is fine. Nothing about Rust async in general requires that your Futures are Send. But in practice, most of the popular async runtimes require this. So if you want to use this with Tokio, for example, then you have to design your system to not hold locks or keys across await points.<p>This first restriction seems like it could be improved with the addition of an `AsyncLockable` trait. But the second restriction seems to me to be fundamental to the design.
      • mplanchard8 hours ago
        Just wanted to add to your great summary a link to tokio’s docs on which kind of mutex to use, which seem applicable to the mutex in TFA as well: <a href="https:&#x2F;&#x2F;docs.rs&#x2F;tokio&#x2F;latest&#x2F;tokio&#x2F;sync&#x2F;struct.Mutex.html#which-kind-of-mutex-should-you-use" rel="nofollow">https:&#x2F;&#x2F;docs.rs&#x2F;tokio&#x2F;latest&#x2F;tokio&#x2F;sync&#x2F;struct.Mutex.html#wh...</a><p>Also to note, regarding “future not send,” that, in tokio codebases where the general expectation is that futures will be Send, enabling the clippy lint “future_not_send” is extremely helpful in avoiding these kinds of issues and also in keeping the error localized to the offending function, rather than it being miles away somewhere it happens to be getting indirectly spawned or whatever: <a href="https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rust-clippy&#x2F;stable&#x2F;index.html?search=future+not+send#future_not_send" rel="nofollow">https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rust-clippy&#x2F;stable&#x2F;index.html?se...</a>
  • eru11 hours ago
    I agree with the author: it&#x27;s a shame that TVars aren&#x27;t catching on in more languages. They are a great idea from the database world, that we could use in the rest of computing, too.
    • embedding-shape11 hours ago
      The entire programming (or even computing) ecosystem suffers from this issue where very useful ideas don&#x27;t always propagate across domains even though they just make a whole lot of sense. I&#x27;m not sure if it&#x27;s because they truly wouldn&#x27;t work out in practice, or if it&#x27;s just a discovery&#x2F;communication thing.<p>One thing that I think do affect things, is that language design discussions tend to be concentrated into their own communities based on the programming language itself, rather than one &quot;programming language discussions&quot; place where everyone can easier cross-pollinate ideas across languages. Luckily, there are some individuals who move between communities without effort, which does lead to a bit of ideas making it across, but it feels like we&#x27;re missing out on so much evolution and ideas from various languages across the ecosystem.
      • eru11 hours ago
        &gt; Luckily, there are some individuals who move between communities without effort, [...]<p>Oh, many of these travelers spend a lot of effort!
      • 01HNNWZ0MV43FF10 hours ago
        It&#x27;s discovery and communication. Public education for adults is way under-appreciated in many many scopes.
    • jandrewrogers10 hours ago
      The cross-fertilization of ideas across computer science domains is more limited than I think people assume. Databases are just one area that contains a lot of good ideas that never seem to leak into other parts of the software world.<p>Supercomputing is another domain that has deep insights into scalable systems that is famously so insular that ideas rarely cross over into mainstream scalable systems. My detour through supercomputing probably added as much to my database design knowledge as anything I actually did in databases.
    • twoodfin10 hours ago
      The canonical industrial explanation “why not” is probably this 2010 piece from Joe Duffy @ Microsoft:<p><a href="http:&#x2F;&#x2F;joeduffyblog.com&#x2F;2010&#x2F;01&#x2F;03&#x2F;a-brief-retrospective-on-transactional-memory&#x2F;" rel="nofollow">http:&#x2F;&#x2F;joeduffyblog.com&#x2F;2010&#x2F;01&#x2F;03&#x2F;a-brief-retrospective-on-...</a>
      • vlovich12310 hours ago
        I don’t think we read the same thing.<p>&gt; Models can be pulled along other axes, however, such as whether memory locations must be tagged in order to be used in a transaction or not, etc. Haskell requires this tagging (via TVars) so that side-effects are evident in the type system as with any other kind of monad. We quickly settled on unbounded transactions.<p>Snip<p>&gt; In hindsight, this was a critical decision that had far-reaching implications. And to be honest, I now frequently doubt that it was the right call. We had our hearts in the right places, and the entire industry was trekking down the same path at the same time (with the notable exception of Haskell)<p>So basically not that TM isn’t workable, but unbounded TM is likely a fool’s errand but Haskell’s is bounded TM that requires explicit annotation of memory that will participate in atomicity.
        • senderista4 hours ago
          Having worked a bit on a hobby STM in C++ (spun out of a DB startup) I would have to agree. Fully transparent STM that depends on a &quot;sufficiently smart compiler&quot; for an imperative language with unrestricted side effects is hopeless. But I do think that a much humbler version of STM is feasible for C++ or Rust, requiring much more explicit cooperation from the programmer. I haven&#x27;t worked on this for 3 years but hope to revisit it someday.
          • vlovich1231 hour ago
            Haskell still needs TVar and it’s not an imperative language with unrestricted side effects. I think it’s bounded vs unbounded. Side effects make it more complicated perhaps but it sounds like even in a JIT language you could have done it.
    • senderista4 hours ago
      Intel, MSFT, IBM spent billions from about 2005-2015 trying to make this happen and failed miserably.<p><a href="https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;10.1145&#x2F;1400214.1400228" rel="nofollow">https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;10.1145&#x2F;1400214.1400228</a>
    • hackingonempty9 hours ago
      It is a big reason why I picked Scala3&#x2F;Zio over Rust for my most recent project.
    • mamcx8 hours ago
      Well, what means to support, truly, TVars?<p>Is easy, or hard?<p>Demand a new paradigm at large, or is only a inconvenience in the few places is used?<p>Because if the answer is &quot;turns the language into Haskell&quot; then is a big NOPE!
  • FpUser1 hour ago
    I must&#x27;ve been a lucky one. I develop software since 80s. Went from directly entering machine codes and up to enterprise middleware, backends and various device control and multimedia game like systems. In all my life I&#x27;ve only had a single case of deadlock. But it cost me more than 24 hours no sleep marathon trying to nail it down. It was related to communication between my custom Directshow filters and threads in a main software.
  • 0x1ceb00da10 hours ago
    What is the &quot;graph&quot; view on the right side?
  • forrestthewoods8 hours ago
    Hrm. I&#x27;m not immediately impressed by the &quot;Level&lt;&gt;&quot; construct. That feels like a lot of new cognitive burden. It&#x27;s also not at all obvious to me that multiple levels of mutex is a common pattern? I&#x27;m not sure I&#x27;ve ever encountered a situation where locking Account also and always requires locking Config? Heaven help you if you have 3 or more levels.<p>I dunno. I appreciate the opposition to &quot;just be careful&quot;. But this feels to me like it&#x27;s inducing bad design patterns. So it feels like it&#x27;s wandering down the wrong path.
    • wrs7 hours ago
      Lock ordering is indeed a common pattern to avoid deadlocks. I learned it in school in the 80&#x27;s and MIT teaches it today. [0]<p>[0] <a href="https:&#x2F;&#x2F;web.mit.edu&#x2F;6.005&#x2F;www&#x2F;fa15&#x2F;classes&#x2F;23-locks&#x2F;#deadlock_rears_its_ugly_head" rel="nofollow">https:&#x2F;&#x2F;web.mit.edu&#x2F;6.005&#x2F;www&#x2F;fa15&#x2F;classes&#x2F;23-locks&#x2F;#deadloc...</a>
      • forrestthewoods6 hours ago
        I&#x27;m aware.<p>I&#x27;d be curious to hear the authors reason to not prefer a LockSet everywhere.
        • expede2 hours ago
          (Author here) it depends on your use case. If you need to incrementally acquire locks, then levels are helpful -- you can&#x27;t do that with LockSets on their own. A place where this comes up is if you need to read a value out of one lock, and pick what to lock next based on that without releasing the first one, and then modify both. Of course you should think twice when doing this but when you need it, you REALLY need it.<p>Opting out of lock levels was a design goal. By default all locks are are Level1, so the level can be omitted thanks to the default type parameter filling it in for you. Levels have no runtime cost, so sidestepping them is free. This lets you live in an atomic-locks only world if you want, and if you later find that you need incremental locks, you can add more levels at that time :)<p>[EDIT: fixing autocorrect typos when I got back to my laptop]
  • rowanG07710 hours ago
    That&#x27;s pretty awesome. Dead locks are extremely tough to debug. There are even cases where I saw behavior in code that might have been a dead lock. I never found out though.
  • airstrike10 hours ago
    I&#x27;d read this, but I can&#x27;t stomach this ChatGPT voice. It&#x27;s absolutely grating.
    • expede6 hours ago
      Author here! This post was human written, LLM proofread, and edited a couple times as folks pointed out broken links and minor errors when it was posted to r&#x2F;rust a few days ago. As someone mentioned lower in the thread, there&#x27;s a form of what is sometimes called Bay Area Standard that both very online humans and LLMs have absorbed. I find it FASCINATING that we&#x27;re in an era where we have to prove our humanity, and the downstream behaviours of things like killing em-dash use in response are interesting to watch in real time. I&#x27;ve made the same mistake, so it&#x27;s honestly difficult to tell!
      • airstrike1 hour ago
        I use em dashes a lot and I&#x27;m chronically online, so that defense doesn&#x27;t apply here.<p>It&#x27;s things like &quot;perfectly invisible in code review, happy to pass CI a thousand times, then lock your system up at 3am under a request pattern that no one anticipated.&quot; which are a dead tell it was written by ChatGPT<p>I&#x27;ll bet you 2 beers the LLM you used to proofread the post was indeed ChatGPT.
        • expede1 hour ago
          Then you owe me two beers; I use Opus
    • Groxx10 hours ago
      tbh I&#x27;m not getting GPT-voice from this
      • ericb9 hours ago
        I&#x27;m not either. If this was GPT-voice, I&#x27;d be happy. It&#x27;s concise, technical, with good emphasis but no drama or AI tropes.
      • IshKebab9 hours ago
        It&#x27;s there in places (&quot;The honest answer is...&quot;) but I think most of this is human written. They probably started with an AI draft I&#x27;d guess.
      • iknowstuff8 hours ago
        [dead]
      • LtdJorge9 hours ago
        [dead]
    • PaulDavisThe1st10 hours ago
      So tired of this sort of comment. LLMs are trained using (primarily, generally) online material. It sounds like online humans, in aggregate, plus or minus a bit of policy on the part of the model builders.
      • altairprime8 hours ago
        &gt; <i>So tired of this sort of comment.</i><p>Email the mods about it rather than replying, subject “Accusation of AI in FP comment” or whatever. It’s a guidelines violation to make the accusation in a comment rather than to them by email, and they have tools to deal with it!
        • slopinthebag8 hours ago
          Nobody is making an accusation of an AI comment - people are pointing out that the article is at least partially AI generate, which does not go against any HN guidelines, and neither does complaining about those comments.
      • IshKebab9 hours ago
        &gt; It sounds like online humans, in aggregate<p>That&#x27;s exactly the problem. It sounds like <i>one</i> aggregate person. It&#x27;s quite unpleasant to read the same turns of phrase again and again and again, especially when it means that the author copped out of writing it themselves.<p>In fairness I think in this case they mostly did write it themselves.
      • CyberDildonics10 hours ago
        They write like the worst possible person. It&#x27;s terrible and obnoxious, there is no reason to put up with it.
      • slopinthebag8 hours ago
        Except <i>nobody</i> writes like the aggregate, hence why it&#x27;s so jarring.<p>The closest actually human style to LLM writing is obnoxious marketing speak. So that also sucks.<p>So many people who are not great writers lean on LLMs to write, but aren&#x27;t good enough to see how bad it is. They should be criticised for this. Either use them and be good enough to make it read as human, or just don&#x27;t use them. No free lunch.
    • macintux8 hours ago
      &gt; Please don&#x27;t post shallow dismissals, especially of other people&#x27;s work. A good critical comment teaches us something.