#Any libraries/wrappers that implement Optional atomics?

76 messages · Page 1 of 1 (latest)

wispy palm
#

I would like to denote an "uninitialized" state in a static atomic value like Option<AtomicU64> mainly for the reason that I don't want to use 0 or u64::MAX to denote that this atomic hasnt been written to, since AtomicU64 is not Copy i find it really hard to work around this

gray cape
#

what does being Copy or not have to do with it

#

if your implementation needs it to be Copy then it would be incorrect

honest agate
#

If it helps you could use any other value as a niche value, e.g. u64::MAX / 3

wispy palm
#

dont want to use any other values

ember dust
# wispy palm dont want to use any other values

Many CPUs support atomic operations on at most 64 bits. The operation you're asking for requires an atomic operation on slightly more than 64 bits of data. So unless you plan to run your program only on CPUs that support 128-bit atomics, you will have to make a compromise of some kind.

wispy palm
#

nice

#

no cant block the thread

ember dust
#

Actually no, that doesn't work because it consumes the value into None each time you read it...

#

Could you explain what your use case is?

wispy palm
#

the idea here is to denote an unitialized state, that means the reader would see that the atomic hasnt been written to yet and can write an appropriate value as per the case of the uninitialized state

#

does that make sense?

modest glacier
#

Are 0 and u64::MAX appropriate values?

ember dust
wispy palm
ember dust
#

Once it's initialized, do you need to modify it later?

wispy palm
#

yes

ember dust
#

Is it fine if two threads both initialize the value and overwrite each other?

wispy palm
ember dust
#

Like, what's your usage pattern?

wispy palm
#

basically building a wrapper around a sdk function that returns a stream that returns a number when polled, and i am tracking the number as a static atomic

ember dust
#

Is it fine if there's blocking while the value is being initialized?

wispy palm
#

tbh the whole context is hard to explain

wispy palm
#

Relaxed ordering is ok

ember dust
#

Is the following scenario fine?

  1. Thread A makes a request to the API. Gets a response.
  2. Thread B makes a request to the API. Gets a newer response.
  3. Thread B writes the newer response.
  4. Some reader thread observes the value.
  5. Thread A overwrites the older value with a newer value.
wispy palm
#

either reader should be able to see that it is the first writer

#

and from read -> write should be atomic

ember dust
wispy palm
#

hmm

ember dust
#

Actually... is there only one writer thread in your use case?

wispy palm
#

the thread that reads also writes

#

and can be many

#

rw should be atomic like compare and swap

ember dust
#

So... how does each thread know that the value is stale and therefore an API call is needed?

wispy palm
#

it doesnt need to know about staleness

#

actually

#

remove the comparison part

#

its just swap

#

continuously swapping as the stream gets polled

ember dust
#

When does polling happen?

#

Periodically on a timer?

wispy palm
#

depends on the client i think

#

in short, i just want either thread to know that it is the first writer

ember dust
#

Hmmm

ember dust
#

Or should two threads never poll the API "at the same time"?

wispy palm
#
    static CACHE: AtomicU64 = AtomicU64::new(0);
    static INITIALIZED: AtomicBool = AtomicBool::new(false);

    if INITIALIZED.load(Ordering::Relaxed) {
        println!("Using cached value");
        return CACHE.load(Ordering::Relaxed);
    }

    println!("Computing value for the first time");
    let result = expensive_computation();
    CACHE.store(result, Ordering::Relaxed);
    INITIALIZED.store(true, Ordering::Relaxed);
    result
#

something like this

#

except i dont want this extra atomic static INITIALIZED: AtomicBool = AtomicBool::new(false);

wispy palm
#

idk trying to think of better practices

#

having two atomics seems like an overkill?

ember dust
#

So... should expensive_computation() ever be called more than once?

wispy palm
#

yeah

#

poll_next

ember dust
#

Your use case sounds similar to OnceLock except for the part where you need to later update the value...

wispy palm
#

yup

#

looked into oncelock

ember dust
#

If one thread is currently retrieving the value to initialize with, should other threads block on waiting for the initialization to finish?

#

And what should happen if the value is already initialized, but two threads want to poll for an updated value at the same time?

modest glacier
ember dust
wispy palm
ember dust
#

This type wouldn't solve the "two threads trying to poll for an update at the same time" issue though

wispy palm
#

whats the performance difference between atomics and oncelock?

ember dust
#

After a OnceLock is initialized, it's basically the "two atomics" solution wrapped into a nicer package.

#

But before it's initialized, OnceLock will internally have a mutex that needs to be locked for initialization.

wispy palm
#

i see

ember dust
#

Idea: Two atomics plus a mutex.

#
static WRITE_LOCK: Mutex<()> = Mutex::new(());
static CACHE: AtomicU64 = AtomicU64::new(0);
static INITIALIZED: AtomicBool = AtomicBool::new(false);

fn initialize() {
    if !INITIALIZED.load(Acquire) {
        let _lock = WRITE_LOCK.lock().unwrap();
        if !INITIALIZED.load(Acquire) {
            let val = expensive_computation();
            CACHE.store(val, Release);
            INITIALIZED.store(true, Release);
        }
    }
}

fn update() {
    assert!(INITIALIZED.load(Acquire));
    let _lock = WRITE_LOCK.lock().unwrap();
    let val = expensive_computation();
    CACHE.store(val, Release);
}

fn get() -> Option<u64> {
    if INITIALIZED.load(Acquire) {
        Some(CACHE.load(Acquire));
    } else {
        None
    }
}
wispy palm
#

that makes sense!

#

hey @ember dust thanks for taking the time to talk to me regarding this, i really appreciate it!

i am very sleepy rn, would it be ok if i ping you tmrw?