Counter, the right way

Okay, at this point we know that instead of a plain integer we need to use an atomic int and we should use Ordering::Relaxed to fetch_add and load it.

Starting from this section, I'll omit the part with C functions, instead there will be only comments like "Exposed as XXX in Ruby"

use std::sync::atomic::{AtomicU64, Ordering};

#[derive(Debug)]
pub struct AtomicCounter {
    value: AtomicU64,
}

impl AtomicCounter {
    // Exposed as `AtomicCounter.new` in Ruby
    pub fn new(n: u64) -> Self {
        Self {
            value: AtomicU64::new(n),
        }
    }

    // Exposed as `AtomicCounter#increment` in Ruby
    pub fn increment(&self) {
        self.value.fetch_add(1, Ordering::Relaxed);
    }

    pub fn read(&self) -> u64 {
        self.value.load(Ordering::Relaxed)
    }
}

The main question is "does it actually work?". First, single-threaded code

require 'c_atomics'

counter = CAtomics::AtomicCounter.new
1_000.times do
  counter.increment
end
p counter.read
# => 1000

Great, it works. Time for multi-threaded code:

require 'c_atomics'

COUNTER = CAtomics::AtomicCounter.new
ractors = 5.times.map do
  Ractor.new do
    1_000.times { COUNTER.increment }
    Ractor.yield :completed
  end
end
p ractors.map(&:take)
# => [:completed, :completed, :completed, :completed, :completed]
p COUNTER.read
# => 5000

Isn't it great?