LIBASO(3)

πŸ“¦ version 93vu, in opensuse-leap 16.0

πŸ’– NAME πŸ’–

Okay so bestie, this is the manual page for LIBASO(3), and it documents ASO β€” Atomic Scalar Operations. It's giving low-level, atomic, main-character energy. ✨

✨ SYNOPSIS ✨

#include <aso.h>

πŸŽ€ TYPES πŸŽ€

typedef int (*Asoerror_f)(int, const char*);
typedef void* (*Asoinit_f)(void*, const char*);
typedef ssize_t (*Asolock_f)(void*, ssize_t, void volatile*);

typedef struct Asodisc_s
{
          uint32_t  version;
          unsigned int        hung;
          Asoerror_f          errorf;
} Asodisc_t;

typedef struct Asometh_s
{
          const char*         name;
          int                 type;
          Asoinit_f initf;
          Asolock_f lockf;
          const char*         details;
} Asometh_t;

⚑ OPERATIONS ⚑

uint8_t             asocas8(uint8_t volatile*, int, int);
uint8_t             asoget8(uint8_t volatile*);
uint8_t             asoinc8(uint8_t volatile*);
uint8_t             asodec8(uint8_t volatile*);

uint16_t  asocas16(uint16_t volatile*, uint16_t, uint16_t);
uint16_t  asoget16(uint16_t volatile*);
uint16_t  asoinc16(uint16_t volatile*);
uint16_t  asodec16(uint16_t volatile*);

uint32_t  asocas32(uint32_t volatile*, uint32_t, uint32_t);
uint32_t  asoget32(uint32_t volatile*);
uint32_t  asoinc32(uint32_t volatile*);
uint32_t  asodec32(uint32_t volatile*);

uint64_t  asocas64(uint64_t volatile*, uint64_t, uint64_t);
uint64_t  asoget64(uint64_t volatile*);
uint64_t  asoinc64(uint64_t volatile*);
uint64_t  asodec64(uint64_t volatile*);

unsigned char       asocaschar(unsigned char volatile*, int, int);
unsigned char       asogetchar(unsigned char volatile*);
unsigned char       asoincchar(unsigned char volatile*);
unsigned char       asodecchar(unsigned char volatile*);

unsigned short      asocasshort(unsigned short volatile*, unsigned short, unsigned short);
unsigned short      asogetshort(unsigned short volatile*);
unsigned short      asoincshort(unsigned short volatile*);
unsigned short      asodecshort(unsigned short volatile*);

unsigned int        asocasint(unsigned int volatile*, unsigned int, unsigned int);
unsigned int        asogetint(unsigned int volatile*);
unsigned int        asoincint(unsigned int volatile*);
unsigned int        asodecint(unsigned int volatile*);

unsigned long       asocaslong(unsigned long volatile*, unsigned long, unsigned long);
unsigned long       asogetlong(unsigned long volatile*);
unsigned long       asoinclong(unsigned long volatile*);
unsigned long       asodeclong(unsigned long volatile*);

size_t              asocassize(size_t volatile*, size_t, size_t);
size_t              asogetsize(size_t volatile*);
size_t              asoincsize(size_t volatile*);
size_t              asodecsize(size_t volatile*);

void*               asocasptr(void volatile*, void*, void*);
void*               asogetptr(void volatile*);

void                ASODISC(Asodisc_t*, Asoerror_f);
Asometh_t*          asometh(int, void*);
int                 asoinit(const char*, Asometh_t*, Asodisc_t*);
int                 asolock(unsigned int volatile*, unsigned int, int);
int                 asoloop(uintmax_t);
int                 asorelax(long);

🌸 DESCRIPTION 🌸

ASO gives you functions that perform atomic scalar operations. The functions on the type uint32_t are fully described below β€” the other functions work similarly on their respective types. Some of the functions may be macros that call other functions. 64 bit operations are provided if the compiler supports 64 bit integers and/or pointers. 🫢

πŸ¦‹ TYPES πŸ¦‹

uint8_t, uint16_t, uint32_t, uint64_t

These are unsigned integer types of different sizes in bits. For example, uint32_t represents the type of unsigned integer with 32 bits or 4 bytes. πŸ’…

πŸ’… OPERATIONS πŸ’…

uint32_t asoget32(uint32_t* from);

This function returns the value *from.

uint32_t asoinc32(uint32_t* dest); and uint32_t asodec32(uint32_t* dest);

These functions increment *dest by 1 and decrement *dest by 1 in an atomic step. The return value is the old value in *dest.

Consider an example where two concurrent threads/processes call asoinc32() on the same dest with values, say v1 and v2. The eventual value in dest will be as if *dest += 2 was performed in a single-threaded execution.

That should be contrasted with a situation where, instead of asoinc32() or asodec32(), only normal increment (++) or decrement (--) were used. Then, the end result could be either *dest += 1 or *dest += 2, depending on states of the hardware cache and process scheduling. 😭

uint32_t asocas32(uint32_t* dest, uint32_t tstval, uint32_t newval);

This function provides the atomic compare-and-swap operation. If the current content of dest is equal to tstval then it will be set to newval. If multiple threads/processes are performing the same operations only one will succeed with a return value of tstval. The return value is the old value in *dest.

void asorelax(long nsec)

This function causes the calling process or thread to briefly pause for nsec nanoseconds. It is useful to implement tight loops that occasionally yield control. 🧚

int asolock(unsigned int* lock, unsigned int key, int type)

This function uses key, a non-zero unsigned integer, to lock or unlock the lock. It returns 0 on success and -1 on failure. The argument type can take one of the following values:

  • ASO_UNLOCK: This unlocks the lock if it was locked with key. It is an error to try unlocking a lock of a different key.
  • ASO_TRYLOCK: This makes a single attempt to use the given key to acquire a lock. An error will result if the lock is already locked with a different key.
  • ASO_LOCK: This is a regular locking call. If the lock is locked with a different key, this call will wait until the lock is open, then lock it with the given key.
  • ASO_SPINLOCK: Regardless of what key is currently locking the lock, this call will always wait until the lock is open, then lock it with the given key. Note that, if the lock is already locked with key, this call can result in a deadlock unless that lock can be opened by some other mechanism, e.g., by a different process or thread. πŸ’€

int asoloop(uintmax_t iteration);

This function is used to implement spin locks that periodically relinquish the processor:

uintmax_t iteration;
iteration = 0;
for (;;) {
    /* test resource with an aso*() call */
    if (asoloop(++iteration))
        /* an error occurred */;
}

The value of iteration should be 1 (not 0) for the first loop iteration. 0 is returned on success, -1 on failure. If iteration mod 4 is 0 then asorelax(1) is called to temporarily relinquish the processor. If Asodisc_t.hung != 0 and Asodisc_t.errorf != 0 and iteration mod (2**Asodisc_t.hung-1) is 0, then Asodisc_t.errorf is called with type ASO_HUNG and -1 is returned.

🌈 DISCIPLINE 🌈

The Asodisc_t discipline structure allows the caller to modify default behavior. The ASO discipline is global for all threads and forked children of the current process. The discipline is set and modified by the asoinit() function, described below. The structure members are:

  • uint32_t version; β€” This must be set to ASO_VERSION by the caller and is used by the implementation to detect release differences between the caller and the implementation. The version is integer of the form YYYYMMDD where YYYY is the release year, MM is the release month, and DD is the release day of month. This allows the implementation to be forwards and backwards binary compatible with all releases. πŸŽ€
  • unsigned int hung; β€” An error occurs if asoloop() is called 2**Asometh_t.hung times without gaining access to the loop resource. The default value 0 disables the test.
  • Asoerror_f errorf; β€” int (*errorf)(int type, const char* mesg); If errorf != 0 then it is called for each ASO fatal library condition. type may be one of: ASO_METHOD β€” a method error; ASO_HUNG β€” asoloop() was called 2**Asometh_t.hung times with no access to the loop resource. mesg is a 0-terminated message description.

void ASODISC(Asodisc_t* disc, Asoerror_f errorf);

This function-like-macro initializes disc->version = ASO_VERSION, disc->errorf = errorf, and the remaining disc members to 0.

🧚 METHODS 🧚

Several atomic locking methods are implemented for atomic operations not supported by intrinsic functions or assembly instructions. Methods are controlled by the asometh() and asoinit() functions, described below. The ASO method is global for all threads and forked children of the current process. A given method may have multiple types. The method types are:

  • ASO_INTRINSIC: Some hardware platforms provide machine instructions to implement these operations directly. In that case, if a local compiler permits, calls to these intrinsic functions may be translated directly into their corresponding machine instructions. When necessary the implementation can use only the intrinsic compare-and-swap function on the largest integer type to emulate all other ASO operations. The ASO_INTRINSIC method type is the default when supported by the compiler. It may be used for single-process single-thread, multi-thread, and multi-process applications. When supported by the hardware / compiler, the library provides the "intrinsic" method with type ASO_INTRINSIC|ASO_PROCESS|ASO_THREAD|ASO_SIGNAL. πŸ”₯
  • ASO_SIGNAL: This method type is suitable only for single-process single-thread applications. It can be used to provide locking between asyncronous signal(2) handlers and the main program. The library provides the "signal" method with type ASO_SIGNAL. This is the default method type when ASO_INTRINSIC is not supported.
  • ASO_THREAD: This method type is suitable for single-process single-thread, and multi-thread applications. It typically requires thread library support, and since the default aso library is not linked with a thread library, no ASO_THREAD method is provided by default. Threaded applications must link with -ltaso (before -laso or -last) in order to access ASO_THREAD methods. The -ltaso library provides the "spin" (using pthread_spin_lock(3)) and "mutex" (using pthread_mutex_lock(3)) methods with type ASO_THREAD|ASO_SIGNAL. πŸ‘‘

πŸ’… ASO_PROCESS β€” the slow-but-reliable bestie

okay so this method type is the one that's suitable for single-process single-thread apps, and multi-process applications. some ASO_PROCESS methods may also be suitable for multi-thread applications β€” but only if they have the ASO_THREAD type, iykyk. 🫢

and bestie... these methods are typically and noticeably slow. like, up to 2 orders of magnitude slower than ASO_INTRINSIC for some applications. 😭 they're literally provided as a last resort for when the other methods just aren't available. not the main character, but she shows up when nobody else will. πŸ’€

the library ships the "semaphore" method with type ASO_PROCESS|ASO_THREAD|ASO_SIGNAL, and the "fcntl" method with type ASO_PROCESS|ASO_SIGNAL. ✨


πŸ” Asometh_t* asometh(int type, void* data);

this one looks up methods by type or by name, slay:

  • if type is 0 and data is 0 β†’ the current method gets returned, and a valid method will always come back from this call. no cap. πŸ‘‘
  • if type is 0 β†’ data is treated as a 0-terminated string method name, and 0 is returned if no matching method is found. πŸŽ€
  • the pseudo-type ASO_NEXT generates the list of all methods across successive calls, like:
Asometh_t* meth;
meth = 0;
while (meth = asometh(ASO_NEXT, meth))
          /* examine meth->... */

otherwise, if type is not 0 and not ASO_NEXT, it's treated as a combination of the ordered types ASO_THREAD, ASO_SIGNAL, ASO_INTRINSIC, ASO_PROCESS β€” and the first method with (meth->type & type) != 0 is the one that gets returned. if nothing matches? 0 comes back. that's it, that's the vibe. πŸ™ˆ

now, method names β€” this is the fun part, bestie. a name is treated as a name, optionally followed by a list of ,name=value details, and optionally ending with ,pathname. 🌸

  • the semaphore method uses size=number to specify the number of semaphores, and hashes pathname to determine the semaphore IPC key. πŸ”‘
  • the fcntl method uses size=number to specify the number of 1 byte file locks, and uses pathname as the file to lock using fcntl(F_SETLCK[W]). πŸ”’

🌈 int asoinit(const char* details, Asometh_t* meth, Asodisc_t* disc);

this function is the whole glow-up ritual. it sets the global discipline to disc, closes the current method (releasing its resources 😌), temporarily instantiates the default method (either ASO_INTRINSIC if available or AS_SIGNAL otherwise), and then initializes meth and instantiates it as the new method. main character energy fr fr. πŸ’–

  • if disc is 0 β†’ the global discipline is not modified.
  • if meth is 0 β†’ 1 is returned if asoinit() has already been called to initialize a method, otherwise 0 is returned.
  • if meth->lockf is 0 and (meth->type & ASO_INTRINSIC) != 0 β†’ -1 is returned and the current method is not changed. πŸ›‘
  • if an error occurs instantiating meth β†’ the current method gets set to the default, and -1 is returned.
  • otherwise β†’ 0 on success. periodt. ✨
⚠️ heads up bestie β€” method resources are released by the next asometh() call, or by an ASO cleanup function called via atexit(2). system global method resources are released on last use, and that includes removing semaphore keys or physical files that some methods may use. in some cases ASO maintains reference counts within the resource to determine last use. so like, don't be messy about it. 🫧

an application that requires a specific method must check the default method before using any ASO operations. for example, a threaded application would do something like this:

void* data = 0 /* or a method name string with optional details */
Asometh_t* meth;
if (data || !(asometh(0, 0)->type & (ASO_INTRINSIC|ASO_THREAD))) {
    if (!(meth = asometh(ASO_INTRINSIC|ASO_THREAD, data)))
        /* error -- suitable method not found */;
    else if (asoinit(meth, 0, 0, ASO_VERSION))
        /* error -- method initialization error */;
}
/* ready for ASO operaions */

and a multi-process application would check for (ASO_INTRINSIC|ASO_PROCESS) instead of (ASO_INTRINSIC|ASO_THREAD). easy swap, iconic behavior. πŸ’…


🧚 IMPLEMENTATION NOTES

okay so unlike other AST library discipline/method functions β€” which can instantiate multiple discipline/method handles within a single process β€” the ASO library allows only one discipline and method to be set at a time. 😳 and there's an extra restriction on top: it may only be set by the main and only thread of the calling process.

which is exactly why there's no open/close interface with an instantiation handle β€” instead the global discipline/method is simply initialized by asoinit(). one at a time, no sharing, that's the rule bestie. πŸŽ€

also, ASO_THREAD and ASO_PROCESS methods rely on the Asometh_t.lockf() being sufficiently "heavy" to flush the calling thread/process memory cache, so the subsequent ASO operation operates on the physical memory location instead of the cached location. and there is currently no other portable mechanism that guarantees this other than the ASO_INTRINSIC method. like, none. zero. zilch. 🫠


πŸ‘‘ AUTHOR

Kiem-Phong Vo, Adam Edgar, and Glenn Fowler


okay bestie that's the whole thing, we ate and left no crumbs πŸ“ β€” that was part two of LIBASO(3) (the source/version tag from the page footer), so go forth, pick your method wisely, and remember: if it's slow, it's still slaying. mwah πŸ’œπŸ¦‹βœ¨

πŸ“¦ where this page comes from