148
@hive_tool_hal ·

Mutex or a channel for a counter hit about 50k times a second Concurrency

Single process, twelve goroutines all incrementing one int64, and a metrics endpoint that reads it once a second. I keep going back and forth between a sync.Mutex around the field and a dedicated goroutine owning the value with an increment channel. The channel version feels more like the Go I've been told to write, but it also feels like a lot of ceremony for n++.

7 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @quietmargin · 4w ago · 3 replies

    Neither. Use atomic.Int64 and call Add(1). Rough per-op costs on a normal x86 box: atomic add around 5-10ns uncontended, mutex lock/unlock around 20-25ns uncontended and much worse under contention, channel send-and-receive north of 100ns because it involves the scheduler. At 50k/s you're doing one op every 20 microseconds so honestly all three work, but atomic is both the fastest and the least code.

    The typed atomic.Int64 from Go 1.19 is nicer than the old atomic.AddInt64(&n, 1) because you can't accidentally read the field non-atomically somewhere else.

    268
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @hive_tool_hal · 4w ago

      Didn't know about the typed wrappers, that solves my actual worry which was somebody reading the field directly in a log line six months from now.

      74
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @parquet_pile · 4w ago

      If you ever get to millions per second on many cores, the single cache line becomes the bottleneck and you shard the counter per-P and sum on read. That is a real technique but it is absolutely not a 50k/s problem.

      41
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @carbon_steel_kit · 4w ago · 2 replies

    Mutex is fine and I'd take it over a channel every time here. It's three lines, everyone reading the code understands it instantly, and you can extend it to two related fields later without redesigning anything. Reach for atomic when profiling tells you to.

    121
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @easy_pace_ellis · 4w ago

      This is the underrated point. The moment your counter becomes "count and last-updated timestamp", the atomic version needs a rethink and the mutex version needs one more line.

      39
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @flat_rate_finn · 4w ago

    The "share memory by communicating" line gets read as "channels always", which was never the intent. Channels are for transferring ownership of a thing between goroutines. Protecting one integer that everyone keeps touching is the textbook case for a lock or an atomic.

    64
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @two_week_notice · 4w ago

    Channel version, no contest. A goroutine owning the state is the idiomatic answer and the performance difference is theoretical at your volume.

    14
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report