catomic

C parameter pass by value when parameter is a structure. Can we ensure at least the single parameter is atomic?


Have a builder function (think poor man's constructor) that takes some structure values and returns another structure by value.

typedef int32_t grid_unit_t;
typedef struct grid_point_t
{
    grid_units_t  x;  // 0 - 8000
    grid_units_t  y;  // 0 - 8000
} grid_point_t;

typedef struct grid_slope_t
{
    grid_units_t  dx;  // -8000 - 8000, but never 0
    grid_units_t  dy;  // -8000 - 8000
} grid_slope_t;

typedef struct grid_line_t
{
    grid_point_t pnt[2]; // two points that define the line
    grid_units_t length; // used in pnt to line distance
    grid_slope_t m;      // stored as dy and dx m = (dy/dx)
    grid_units_t b;      // dx is never 0.
} grid_line_t;

grid_line_t build_line(grid_point_t p0, grid_point_t p1)
{
    grid_units_t dx = p1.x - p0.x == 0? 1: p1.x - p0.x;
    grid_units_t dy = p1.y - p0.y;
    return (grid_line_t){
        .pnt[0] = p0, .pnt[1] = p1,
        .m.dx = dx,
        .m.dy = dy,
        .length = sqrt(dx*dx + dy*dy),
        .b = p0.y - (p0.x * dy) / dx
    };
}

extern grid_point_t my_p0;
extern grid_point_t my_p1;
void process_line(grid_line_t *line);

void main(void)
{
    // build a line based on some external points
    grid_line_t my_line = build_line(my_p0, my_p1);
    // some random processing of line
    process_line(&my_line);
}

I know that the parameters are copied then passed into the function. I am pretty sure the two parameters are not copied atomically with respect to each other, but is there any way to ensure that each parameter itself is copied atomically?

Some processors will now copy 64bit objects atomically so I could ensure alignment then cast the structure to int64 but was looking for a language supported option.

I am not interested in debating the desirability of doing this, just looking to see if it is possible. Also this is C not C++ so no stdatomic. I also understand there are other ways of solving this with mutex and such.


Solution

  • ISO standard C has _Atomic objects since C11 (current is C23). No version of ISO C makes language- or standard-library-level provision for atomic operations on non-_Atomic objects, though some C implementations provide such as extensions. Since you seem to rule out use of synchronization objects such as mutexes, giving the external variables from which you are reading the structure values _Atomic type is what standard C has available to serve your objective:

    extern _Atomic grid_point_t my_p0;
    extern _Atomic grid_point_t my_p1;
    

    The _Atomic qualifier is part of the data type, so all declarations of these objects must agree about it.

    Note well that there is an immediate consequence on your code: the members of an _Atomic structure cannot be accessed without provoking undefined behavior. You should instead use whole-structure accesses. Individual members should be accessed on a non-atomic copy, either before writing (the whole structure) to the atomic object or after reading from it.

    Ordinary accesses to _Atomic objects are performed atomically, with sequentially consistent memory semantics. That makes it very easy to get atomic accesses (your program wouldn't need anything else to get the guarantees you asked for), but sequentially consistent semantics are pretty strong. You can perform atomic accesses with weaker memory order semantics via functions defined in stdatomic.h. That may provide for better performance, but in your particular case it would require an extra copy of each object, which might outweigh any cost reduction from the weaker semantics. For example,

    #include <stdatomic.h>
    
    extern _Atomic grid_point_t my_p0;
    extern _Atomic grid_point_t my_p1;
    
    int main(void) {
        // build a line based on some external points
    
        grid_point_t p0 = atomic_load_explicit(&my_p0, memory_order_relaxed);
        grid_point_t p1 = atomic_load_explicit(&my_p1, memory_order_relaxed);
    
        grid_line_t my_line = build_line(p0, p1);
    
        // ...
    }
    

    Memory ordering is about the visibility of modifications of other objects to threads that observe the values of atomic objects. You can see surprising results if you get this wrong. If you're unsure what you need then sticking with sequential consistency (ordinary accesses or memory_order_seq_cst) is the safe play.