Toward a Better Use of C11 Atomics - Part 1

GNU C library

Introduction

Following the lead of C++, along with a memory model describing the requirements and semantics of multithreaded programs, the C11 standard adopted a proposal for a set of atomic types and operations into the language. This change has made it possible to write portable multi-threaded software that efficiently manipulates objects indivisibly and without data races. The atomic types are fully interoperable between the two languages so that programs can be developed that share objects of atomic types across the language boundary. This paper examines some of the trade-offs of the design, points out some of its shortcomings, and outlines solutions that simplify the use of atomic objects in both languages.

The need for software to operate on objects of basic types in an atomic way goes back to the first multiprocessor systems. Many solutions were developed over the decades, each with its own unique characteristics that made writing portable code that took advantage of those features difficult. The C11 and C++11 standards codify an approach that allows software to make use of the hardware support for atomics on the broadest spectrum of processors. The authors of the proposals for C and C++ atomics envisioned efficient, lock-free implementations of these interfaces on architectures that provide robust support for such operations. The authors, however, didn't want to preclude lock-based emulations on older or less capable hardware. Although emulated implementations were expected to store the lock associated with each atomic object separately from the object itself, this aspect too wasn't mandated by the proposed specification. The proposers didn't want to preclude stateful implementations of atomics.

The original proposal to add support for the feature into C (WG14 N1284 ) called for the addition of a fixed number of otherwise unspecified structs corresponding to each of the basic integer, character and address (pointer) types, along with a small set of generic operations on objects of these types. In efficient implementations the structs were anticipated to contain just a single member of the corresponding type (called the direct type), perhaps padded to the required alignment boundary for the hardware. In emulated implementations, the structs could also store a mutex to provide the necessary locking. The specification of the atomic types was carefully crafted to allow for full interoperability between C and C++. To achieve it, the atomic types must have the same alignment, size, and representation in both languages.

Limitations of the Proposed Approach

It was recognized early on that the approach initially proposed for C had several limitations. This section briefly examines those limitations and describes the solutions ultimately adopted to overcome them.

No Support For User-Defined Types

All C++ atomic types are specializations of a primary class template that C++ programmers can instantiate on arbitrary user-defined types. Objects of specializations on the user-defined types can be operated on using the same intuitive syntax as for scalar types. While atomic operations on many user-defined types must be emulated in software, operations on user-defined types that fit in a single CPU register can commonly take advantage of hardware support to achieve the same efficiency as those on ordinary scalars. However, since the set of C atomic types was finite, C programmers would have had no way to define atomic types of their own. For example, defining an atomic form of a struct like the C++ one below would not have been possible in C:

Inconvenient Syntax For Operations On Atomic Scalars

Thanks to operator overloading, C++ provides an intuitive syntax for manipulating its atomic types with the conservative sequentially consistent semantics. However, since C doesn't allow overloading, it provided only a functional interface to manipulate atomic objects. This interface was considerably less convenient to use than the C++ API. For example, when the guarantees of sequentially consistent semantics are required, an atomic_int variable ai can be incremented using the same compound assignment expression in C++ as an ordinary int can:

In C, the same variable would have had to have been incremented solely using the atomic_fetch_add() or atomic_fetch_add_explicit() generic functions:

For situations when sequential consistency isn't necessary, the more general atomic_fetch_add_explicit() interface lets both C and C++ callers specify the desired memory order using the third argument. The following call is equivalent with the call to atomic_fetch_add() above:

No Way To Initialize Atomics

C++ has constructors that make initialization of non-trivial objects transparent to users: an object with a constructor is initialized automatically when it's defined. Since C has no equivalent feature it must rely on the programmer to explicitly initialize struct members before using them. However, since the members of the C atomic structs were deliberately unspecified, there was no portable way to initialize objects of such types. This wasn't an issue for the efficient lock-free implementations which stored just a single data member in each atomic type — the value of the corresponding direct type. However, it posed a problem for the (hypothetical) emulated implementations that stored additional state such as a mutex along with the value in the same object. Such types were thought to require a similar approach as that used to initialize POSIX threads mutex objects.

In addition, for full interoperability between the languages, both need to make it possible to create an object of an atomic type that is in an uninitialized state and that can be initialized by a construct in the other language. Such a construct wasn't available.

Solutions Adopted In C11

User-defined atomic types.

To provide a comparable degree of extensibility and syntactic convenience as C++, in response to the Atomic Proposal (WG14 N1485 ) C adopted a new _Atomic keyword. The keyword, which takes the form of either a type specifier or type qualifier, provides a general mechanism that enables declaring any type atomic, including user-defined structs. For example, the declarations below define the AtomicInt96 type whose objects can be manipulated atomically:

Intuitive Syntax For Operations On Atomic Types

In addition to the ability to define user-defined atomic types, the _Atomic keyword makes it possible to use objects of all atomic types in nearly all the same expressions and contexts as their direct (non-atomic) counterparts. Owing to the C feature called generic selection, it is also possible to use the same small set of functions (called generic functions) to operate on objects of all atomic types. This way C achieved nearly the same degree of convenience for the uses of atomic types as C++. For example, the following expressions are valid in both C and C++ and have the same effect and the same sequentially consistent memory order guarantees in both languages:

Initialization

The initialization problem was recognized by the C++ study group working on interoperability of atomics between the two languages. To solve it, the paper on Explicit Initializers for Atomics (WG14 N1445 ) introduced a pair of macros for C and C++ programmers to initialize atomic objects with. The ATOMIC_VAR_INIT() macro can be used to initialize atomic objects with static storage duration to a non-zero value, while the atomic_init() macro is used to initialize automatic or dynamically allocated objects. For example, the following statically initializes the global atomic variable index to -1:

and this function initializes the object pointed to by the argument a to the specified values at runtime:

Outstanding And New Problems

Unfortunately, not all problems with the specification of atomics were recognized before the C11 and C++11 standards were published. A number of issues have been discovered since then. Some of them have been brought to the attention of the language committees. A subset of those have been acknowledged as defects or are being actively discussed. Some others have only recently been uncovered and are waiting to be raised as potential defect reports. This section briefly summarizes the first set.  In Part 2 of the article we then spend a bit more time discussing the second set of remaining problems, specifically initialization.

DR 431 - atomic_compare_exchange: What does it mean to say two structs compare equal?

WG14 Defect Report 431 ( DR 431) raises a question about the intended semantics of the atomic_compare_exchange() generic function. The function is specified as follows:

atomic_compare_exchange_strong (volatile A *object, C *expected, C desired); Atomically, compares the value pointed to by object for equality with that in expected, and if true, replaces the value pointed to by object with desired, and if false, updates the value in expected with the value pointed to by object.

The problem with the specification is that the concept of equality is not defined for objects of aggregate types such as structs. The informative note that follows the description suggests that implementations are expected to use the memcmp() function to perform the comparison, as if like so:

However, using memcmp() to compare objects containing padding is not guaranteed to yield consistent results for objects with distinct representations of the same value. This includes not only structs with padding, but also objects of type _Bool (and in C++ the type bool) that may have many representations of each of the two values, true and false. It also includes any integer or pointer types in the rare implementations where objects of such types have padding bits. Furthermore, in another defect report, DR 474 , Blaine Garst, the author of the C _Atomic keyword proposal argues that the memcmp() semantics are not appropriate for structs.

To better understand the problem, consider the following function that atomically increments an AtomicInt96 value:

The function first copies the value of the atomic variable to the ordinary, non-atomic local variables expected and desired. It then starts a loop in which it non-atomically increments desired and attempts to atomically assign its value to object . The assignment is successful if the value of object is equal to expected, otherwise it fails. On success atomic_compare_exchange_strong() returns true, otherwise it returns false and stores the value of *object in expected. The function loops until the assignment is successful.

The problem pointed out by DR 431 is that calling memcmp() on a pair of structs with padding has unspecified results. This should be obvious when the destination object is initialized by assigning to its individual members as done in the inc96() function, but it is so even if the two structs are copies of one another obtained by assignment, because the assignment need not copy the padding bits. To make matters worse, the problem cannot be solved by zeroing out the storage occupied by the struct by calling memset(&desired, 0, sizeof desired) before assigning to its members because C allows each assignment to modify the padding bits in an unspecified way. This is permitted to let implementations to use faster instructions when assigning values to members that are smaller than the size of the register used in the more efficient instructions.

Fortunately, while this problem is real, as Hans Boehm, one of the authors of the atomic proposal points out, it doesn't affect the correctness of the loop. At worst, it adds one iteration to it. This is because on failure, the atomic_compare_exchange_strong() call stores a bitwise copy of the atomic variable in expected, obtained as if by a call to memcpy(). Because the bitwise copy is obtained by accessing the object via an lvalue of unsigned char (that is how memcpy() is specified to copy bytes) it is guaranteed by the C standard to have the exact same bit representation as the original.

DR 485 — Problem with the specification of ATOMIC_VAR_INIT

WG14 Defect Report 485 ( DR 485 ) points out that the ATOMIC_VAR_INIT() macro as specified by the C standard and as defined by most implementations cannot be used with the "{ a, b }" form of initializers to initialize user-defined structs. Typically, the macro is defined like so:

Using this macro, an AtomicInt96 object can have one of its members initialized this way:

However, the macro cannot be used to also initialize the other members of the struct using the same intuitive aggregate syntax:

That's because the macro takes a single argument and the comma separating the initializers is interpreted by the preprocessor as separating arguments to the macro. To get around the problem, clever users might resort to the following awkward form instead:

Notice how the whole initializer is enclosed in a pair parentheses to foil the preprocessor and have it treat the expression as a single argument to the ATOMIC_VAR_INIT() macro. This is not only unintuitive but also more difficult to read than the first form.

To solve the problem, the technical corrigendum proposed in DR 485 recommends that the standard be changed to essentially require ATOMIC_VAR_INIT() to be a macro accepting a variable number of arguments. With the macro defined like so any number of arguments can be provided:

In the next section we show that the macro actually isn't necessary at all and that atomic aggregate objects can be simply initialized the same way as ordinary, non-atomic aggregates.

  • Red Hat Enterprise Linux
  • Red Hat OpenShift
  • Red Hat Ansible Automation Platform
  • See all products
  • See all technologies
  • Developer Sandbox
  • Developer Tools
  • Interactive Tutorials
  • API Catalog
  • Operators Marketplace
  • Learning Resources
  • Cheat Sheets

Communicate

  • Contact sales
  • Find a partner

Report a website issue

  • Site Status Dashboard
  • Report a security problem

RED HAT DEVELOPER

Build here. Go anywhere.

We serve the builders. The problem solvers who create careers with code.

Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

Red Hat legal and privacy links

  • About Red Hat
  • Contact Red Hat
  • Red Hat Blog
  • Diversity, equity, and inclusion
  • Cool Stuff Store
  • Red Hat Summit
  • Privacy statement
  • Terms of use
  • All policies and guidelines
  • Digital accessibility

DEV Community

DEV Community

AbdulKarim

Posted on Oct 29, 2023

How C-Pointers Works: A Step-by-Step Beginner's Tutorial

In this comprehensive C Pointers tutorial, my primary goal is to guide you through the fundamentals of C pointers from the ground up. By the end of this tutorial, you will have gained an in-depth understanding of the following fundamental topics:

  • What is a Pointer?
  • How Data is Stored in Memory?
  • Storing Memory Addresses using Pointers

Accessing Data through Pointers

  • Pointer Arithmetic
  • Pointer to Pointer (Double Pointers)
  • Passing Pointers as Function Arguments

Arrays of Pointers

Null pointers, prerequisite:.

To grasp pointers effectively, you should be comfortable with basic C programming concepts, including variables, data types, functions, loops, and conditional statements. This familiarity with C programming forms the foundation for understanding how pointers work within the language. Once you have a solid grasp of these fundamental concepts, you can confidently delve into the intricacies of C pointers.

What is a pointer?

A pointer serves as a reference that holds the memory location of another variable. This memory address allows us to access the value stored at that location in the memory. You can think of a pointer as a way to reference or point to the location where data is stored in your computer's memory

Pointers can be a challenging concept for beginners to grasp, but in this tutorial, I'll explain them using real-life analogies to make the concept clearer. However, Before delving into pointers and their workings, it's important to understand the concept of a memory address.

A memory address is a unique identifier that points to a specific location in a computer's memory. Think of it like a street address for data stored in your computer's RAM (Random Access Memory). Just as a street address tells you where a particular house is located in the physical world, a memory address tells the computer where a specific piece of information or data is stored in its memory.

Take a look at the image below for a better understanding:

Block of memory

In this illustration, each block represents one byte of memory. It's important to note that every byte of memory has a unique address. To make it easier to understand, I've represented the addresses in decimal notation, but computers actually store these addresses using hexadecimal values. Hexadecimal is a base-16 numbering system commonly used in computing to represent memory addresses and other low-level data. It's essential to be aware of this representation when working with memory-related concepts in computer programming

How data is stored in the memory:

Every piece of data in your computer, whether it's a number, a character, or a program instruction, is stored at a specific memory address. The amount of space reserved for each data type can vary, and it is typically measured in bytes (where 1 byte equals 8 bits, with each bit representing either 0 or 1). The specific sizes of data types also depend on the computer architecture you are using. For instance, on most 64-bit Linux machines, you'll find the following typical sizes for common data types: char = 1 byte int = 4 bytes float = 4 bytes double = 8 bytes These sizes define how much memory each data type occupies and are crucial for memory management and efficient data representation in computer systems.

You can use the sizeof operator to determine the size of data types on your computer. example:

In this example: sizeof(char) returns the size of the char data type in bytes. sizeof(int) returns the size of the int data type in bytes. sizeof(float) returns the size of the float data type in bytes. sizeof(double) returns the size of the double data type in bytes. When you run this code, it will print the sizes of these data types on your specific computer, allowing you to see the actual sizes used by your system.

When you declare a variable, the computer allocates a specific amount of memory space corresponding to the chosen data type. For instance, when you declare a variable of type char, the computer reserves 1 byte of memory because the size of the 'char' data type is conventionally 1 byte.

address of char n

In this example, we declare a variable n of type char without assigning it a specific value. The memory address allocated for the n variable is 106 . This address, 106 , is where the computer will store the char variable n, but since we haven't assigned it a value yet, the content of this memory location may initially contain an unpredictable or uninitialized value.

When we assign the value 'C' to the variable n, the character 'C' is stored in the memory location associated with the variable n. When we assign the value 'C' to the variable n, the character 'C' is stored in the memory location associated with the variable n.

address of cahr n = c

As mentioned earlier, a byte can only store numerical values. When we store the letter 'C' in a byte, the byte actually holds the ASCII code for 'C,' which is 67. In computer memory, characters are represented using their corresponding ASCII codes. So, in memory, the character 'C' is stored as the numerical value 67. Here's how it looks in memory

Ascii code of c

Since integers are typically stored within four bytes of memory, let's consider the same example with an int variable. In this scenario, the memory structure would appear as follows:

add. of int t

In this example, the memory address where the variable t is stored is 121. An int variable like “t” typically uses four consecutive memory addresses, such as 121, 122, 123, and 124. The starting address, in this case, 121, represents the location of the first byte of the int, and the subsequent addresses sequentially represent the following bytes that collectively store the complete int value.

If you want to know the memory address of a variable in a program, you can use the 'address of' unary operator, often denoted as the '&' operator. This operator allows you to access the specific memory location where a variable is stored.

When you run the following program on your computer: It will provide you with specific memory addresses for the variables c and n. However, each time you rerun the program, it might allocate new memory addresses for these variables. It's important to understand that while you can determine the memory address of a variable using the & operator, the exact memory location where a variable is stored is typically managed by the system and the compiler. As a programmer, you cannot directly control or assign a specific memory location for a variable. Instead, memory allocation and management are tasks handled by the system and the compiler.

Storing memory address using pointers

As mentioned earlier, a pointer is a variable that stores the memory address of another variable. This memory address allows us to access the value stored at that location in memory. You can think of a pointer as a way to reference or point to the location where data is stored in your computer's memory.

Now, let's begin by declaring and initializing pointers. This step is essential because it sets up the pointer to hold a specific memory address, enabling us to interact with the data stored at that location.

Declaring Pointers: To declare a pointer, you specify the data type it points to, followed by an asterisk (*), and then the pointer's name. For example:

Here, we've declared a pointer named ptr that can point to integers.

Memory of Declaring an integer pointer

The size of pointers on 64-bit systems is usually 8 bytes (64 bits). To determine the pointer size on your system, you can use the sizeof operator:

Initializing Pointers: Once you've declared a pointer, you typically initialize it with the memory address it should point to. Once again, To obtain the memory address of a variable, you can employ the address-of operator (&). For instance:

In this program:

We declare an integer variable x and initialize it with the value 10. This line creates a variable x in memory and assigns the value 10 to it.

ptr

We declare an integer pointer ptr using the int *ptr syntax. This line tells the compiler that ptr will be used to store the memory address of an integer variable.

pointrt to ptr

We initialize the pointer ptr with the memory address of the variable x . This is achieved with the line ptr = &x; . The & operator retrieves the memory address of x, and this address is stored in the pointer ptr .

address of variable x

Dereferencing Pointers: To access the data that a pointer is pointing to, you need to dereference the pointer. Dereferencing a pointer means accessing the value stored at the memory address that the pointer points to. In C, you can think of pointers as variables that store memory addresses rather than actual values. To get the actual value (data) stored at that memory address, you need to dereference the pointer.

Dereferencing is done using the asterisk (*) operator. Here's an example:

It looks like this in the memory: int x = 10; variable 'x' stores the value 10:

var X

int *ptr = &x; Now, the pointer 'ptr' point to the address of 'x':

Pointer to X

int value = *ptr; Dereference 'ptr' to get the value stored at the address it points to:

pointer value is 10

Reading and Modifying Data: Pointers allow you to not only read but also modify data indirectly:

Note: The asterisk is a versatile symbol with different meanings depending on where it's used in your C program, for example: Declaration: When used during variable declaration, the asterisk (*) indicates that a variable is a pointer to a specific data type. For example: int *ptr; declares 'ptr' as a pointer to an integer.

Dereferencing: Inside your code, the asterisk (*) in front of a pointer variable is used to access the value stored at the memory address pointed to by the pointer. For example: int value = *ptr; retrieves the value at the address 'ptr' points to.

Pointer Arithmetic:

Pointer arithmetic is the practice of performing mathematical operations on pointers in C. This allows you to navigate through arrays, structures, and dynamically allocated memory. You can increment or decrement pointers, add or subtract integers from them, and compare them. It's a powerful tool for efficient data manipulation, but it should be used carefully to avoid memory-related issues.

Incrementing a Pointer:

Now, this program is how it looks in the memory: int arr[4] = {10, 20, 30, 40};

int arr

This behavior is a key aspect of pointer arithmetic. When you add an integer to a pointer, it moves to the memory location of the element at the specified index, allowing you to efficiently access and manipulate elements within the array. It's worth noting that you can use pointer arithmetic to access elements in any position within the array, making it a powerful technique for working with arrays of data. Now, let's print the memory addresses of the elements in the array from our previous program.

If you observe the last two digits of the first address is 40, and the second one is 44. You might be wondering why it's not 40 and 41. This is because we're working with an integer array, and in most systems, the size of an int data type is 4 bytes. Therefore, the addresses are incremented in steps of 4. The first address shows 40, the second 44, and the third one 48

Decrementing a Pointer Decrement (--) a pointer variable, which makes it point to the previous element in an array. For example, ptr-- moves it to the previous one. For example:

Explanation:

We have an integer array arr with 5 elements, and we initialize a pointer ptr to point to the fourth element (value 40) using &arr[3].

Then, we decrement the pointer ptr by one with the statement ptr--. This moves the pointer to the previous memory location, which now points to the third element (value 30).

Finally, we print the value pointed to by the decremented pointer using *ptr, which gives us the value 30.

In this program, we demonstrate how decrementing a pointer moves it to the previous memory location in the array, allowing you to access and manipulate the previous element.

Pointer to pointer

Pointers to pointers, or double pointers, are variables that store the address of another pointer. In essence, they add another level of indirection. These are commonly used when you need to modify the pointer itself or work with multi-dimensional arrays.

To declare and initialize a pointer to a pointer, you need to add an extra asterisk (*) compared to a regular pointer. Let's go through an example:

In this example, ptr2 is a pointer to a pointer. It points to the memory location where the address of x is stored (which is ptr1 ).

pointer to poiter

The below program will show you how to print the value of x through pointer to pointer

In this program, we first explain that it prints the value of x using a regular variable, a pointer, and a pointer to a pointer. We then print the memory addresses of x , ptr1 , and ptr2 .

Passing Pointers as Function Arguments:

In C, you can pass pointers as function arguments. This allows you to manipulate the original data directly, as opposed to working with a copy of the data, as you would with regular variables. Here's how it works:

How to Declare and Define Functions that Take Pointer Arguments: In your function declaration and definition, you specify that you're passing a pointer by using the * operator after the data type. For example:

In the above function, we declare ptr as a pointer to an integer. This means it can store the memory address of an integer variable.

Why Would You Pass Pointers to Functions?

Passing pointers to functions allows you to:

  • Modify the original data directly within the function.
  • Avoid making a copy of the data, which can be more memory-efficient.
  • Share data between different parts of your program efficiently.

This concept is especially important when working with large data structures or when you need to return multiple values from a function.

Call by Value vs. Call by Reference:

Understanding how data is passed to functions is crucial when working with pointers. there are two common ways that data can be passed to functions: call by value and call by reference.

Call by Value:

When you pass data by value, a copy of the original data is created inside the function. Any modifications to this copy do not affect the original data outside of the function. This is the default behavior for most data types when you don't use pointers.

Call by Reference (Using Pointers):

When you pass data by reference, you're actually passing a pointer to the original data's memory location. This means any changes made within the function will directly affect the original data outside the function. This is achieved by passing pointers as function arguments, making it call by reference. Using pointers as function arguments allows you to achieve call by reference behavior, which is particularly useful when you want to modify the original data inside a function and have those changes reflected outside the function.

Let's dive into some code examples to illustrate how pointers work as function arguments. We'll start with a simple example to demonstrate passing a pointer to a function and modifying the original data.

Consider this example:

In this code, we define a function modifyValue that takes a pointer to an integer. We pass the address of the variable num to this function, and it doubles the value stored in num directly.

This is a simple demonstration of passing a pointer to modify a variable's value. Pointers allow you to work with the original data efficiently.

An array of pointers is essentially an array where each element is a pointer. These pointers can point to different data types (int, char, etc.), providing flexibility and efficiency in managing memory.

How to Declare an Array of Pointers? To declare an array of pointers, you specify the type of data the pointers will point to, followed by square brackets to indicate it's an array, and then the variable name. For example:

Initializing an Array of Pointers You can initialize an array of pointers to each element to point to a specific value, For example:

How to Access Elements Through an Array of Pointers? To access elements through an array of pointers, you can use the pointer notation. For example:

This program demonstrates how to access and print the values pointed to by the pointers in the array.

A NULL pointer is a pointer that lacks a reference to a valid memory location. It's typically used to indicate that a pointer doesn't have a specific memory address assigned, often serving as a placeholder or default value for pointers.

Here's a code example that demonstrates the use of a NULL pointer:

In this example, we declare a pointer ptr and explicitly initialize it with the value NULL. We then use an if statement to check if the pointer is NULL. Since it is, the program will print "The pointer is NULL." This illustrates how NULL pointers are commonly used to check if a pointer has been initialized or assigned a valid memory address.

conclusion:

You've embarked on a comprehensive journey through the intricacies of C pointers. You've learned how pointers store memory addresses, enable data access, facilitate pointer arithmetic, and how they can be used with arrays and functions. Additionally, you've explored the significance of NULL pointers.

By completing this tutorial, you've equipped yourself with a robust understanding of pointers in C. You can now confidently navigate memory, manipulate data efficiently, and harness the power of pointers in your programming projects. These skills will be invaluable as you advance in your coding endeavors. Congratulations on your accomplishment, and keep coding with confidence!

Reference: C - Pointers - Tutorials Point

Pointers in C: A One-Stop Solution for Using C Pointers - simplilearn

Top comments (3)

pic

Templates let you quickly answer FAQs or store snippets for re-use.

imperiald profile image

  • Joined Jan 7, 2024

Love your way to write articles, could you add an article for, .o files, .h files, lists and makefile? Thank you in advance!

cocomelonjuice profile image

  • Joined Nov 4, 2023

Great post. Thank you so much for this.

koderkareem profile image

Thank you for your kind words! I'm thrilled to hear that you enjoyed the article. Your feedback means a lot to me. If you have any questions or if there's a specific topic you'd like to see in future posts, feel free to let me know. Thanks again for your support

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink .

Hide child comments as well

For further actions, you may consider blocking this person and/or reporting abuse

thelamina profile image

How to Implement Face Detection in React Native Using React Native Vision Camera

Itunu Lamina - Apr 19

amyroy profile image

‍How much does it cost to start a business online

amyroy - Apr 18

mikeyoung44 profile image

Long-form music generation with latent diffusion

Mike Young - Apr 18

ananyadasgupta profile image

Deploy a web application with AWS Lambda

AnanyaDasgupta - Apr 17

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Dot Net Tutorials

Atomic Operations in C

Back to: C Tutorials For Beginners and Professionals

Atomic Operations in C Language with Examples

In this article, I will discuss Atomic Operations in C Language with Examples. Please read our previous article discussing Bounds Checking Interfaces in C with Examples. 

What are Atomic Operations in C?

Atomic operations in C programming are a set of instructions that are executed as a single, indivisible step. This means other threads or processes cannot interrupt an atomic operation once started. This is crucial in multi-threading environments to prevent race conditions, where multiple threads access and modify the same variable concurrently, leading to unpredictable results.

Key Concepts

  • Atomicity: Ensures that an operation completes without any interference from other threads.
  • Visibility: Changes made by one thread are visible to other threads.
  • Ordering: Ensures a sequence of operations is followed as expected.

Atomic Functions in C11 Standard: With the C11 standard, a new header file <stdatomic.h> was introduced. This provides a set of atomic operations and types that can be used to perform atomic operations on variables.

Types of Atomic Operations:

  • Load Operations : Read a value atomically.
  • Store Operations: Write a value atomically.
  • Read-Modify-Write Operations: Operations like atomic_fetch_add or atomic_compare_exchange_strong combine reading, modifying, and writing back a value atomically.

Memory Order: Atomic operations in C can specify memory order, which dictates the visibility of the operation to other threads. For example, memory_order_relaxed, memory_order_acquire, memory_order_release, etc.

Use in Multithreading: Atomic operations are essential in multithreaded applications to prevent race conditions, where two or more threads access shared data concurrently and cause unpredictable results.

Lock-Free Programming: Atomics enables lock-free programming, which can lead to more efficient multithreaded applications since they avoid the overhead of locking mechanisms.

Hardware Support: Atomic operations are often directly supported by hardware instructions, which makes them very efficient.

Examples of Atomic Operations:

  • atomic_load: Atomically loads and returns the value of the atomic variable.
  • atomic_store: Atomically stores a value in the atomic variable.
  • atomic_fetch_add: Atomically adds a number to the atomic variable and returns its old value.

Atomic Operations Examples in C

Below are examples illustrating the use of atomic operations in C. These examples assume the inclusion of the <stdatomic.h> header, which is part of the C11 standard.

Example: Atomic Load and Store

This example demonstrates simple atomic load and store operations.

Output: The atomic variable’s value is: 20

In this example, an atomic_int variable is initialized, a value is stored atomically using atomic_store, and then the value is read using atomic_load.

Example: Atomic Fetch Add

This example shows how to add to a variable atomically.

Output: Counter value after atomic addition: 5

Here, atomic_fetch_add is used to atomically add a value to the counter. The operation returns the old value of the counter, but this return value is ignored in the example.

Example: Atomic Compare and Exchange

This example uses atomic_compare_exchange_strong to update a value conditionally.

Output: Value successfully changed to 200

In this example, atomic_compare_exchange_strong checks if controlVar is equal to expected (100). If it is, controlVar is set to desired (200). If not, expected is set to the current value of controlVar.

When to Use Atomic Operations in C?

Atomic operations in C are powerful for managing concurrent access to shared data in multithreaded programming. They are crucial in ensuring data integrity and preventing race conditions. Here are scenarios and guidelines on when to use atomic operations:

  • Managing Shared Variables in Multithreading: Use atomic operations when multiple threads need to read, modify, and update shared variables. Atomics ensures that these operations on shared data are performed as a single, indivisible step, thus preventing inconsistencies and race conditions.
  • Implementing Lock-Free Data Structures: In scenarios where you need efficient multithreading without the overhead of locks, atomic operations can be used to implement lock-free data structures. These structures are more scalable under high contention as they avoid the costs associated with lock-based synchronization.
  • Counter and Flag Updates: Atomic operations are ideal for safely incrementing counters or updating flags in a multithreaded environment. For example, maintaining a count of active threads or updating a status flag that multiple threads need to check.
  • Signal Handling: In situations where a signal handler modifies a variable that is also accessed by other parts of the program, atomic operations can be used to ensure the integrity of these variable modifications.
  • Implementing Thread-Safe Singleton Patterns: When implementing a singleton pattern in a multithreaded environment, atomic operations can be used to ensure that the singleton instance is initialized only once.
  • Ordering Memory Operations: Atomic operations with specific memory orders (like memory_order_acquire and memory_order_release) can be used to enforce ordering constraints on memory operations, which is critical in multithreaded programs for maintaining consistency and avoiding memory reordering issues.
  • Synchronization Between Threads: Atomics can be used for simple synchronization tasks between threads, such as signaling a thread to proceed or waiting for a condition without involving heavy synchronization primitives like mutexes or condition variables.
  • Performance-Critical Sections: In performance-critical sections of code, where the overhead of locking mechanisms is too costly, atomic operations provide an efficient alternative for ensuring thread safety.
  • Inter-Thread Communication: For simple inter-thread communication, such as passing a single piece of data between threads, atomics can be a lightweight solution compared to more complex synchronization mechanisms.

Advantages and Disadvantages of Atomic Operations in C

Atomic operations in C, as in other programming languages, offer specific advantages and disadvantages, particularly in the context of multi-threaded programming. Understanding these can help you decide when and how to use them effectively.

Advantages of Atomic Operations in C:

  • Thread Safety: Atomic operations ensure that a variable is safely read or modified by one thread at a time, preventing race conditions.
  • Performance: They can be more efficient than synchronization mechanisms like mutexes, especially for simple operations, as they often translate to single-machine instructions and do not require context switching.
  • Simplicity: For simple operations like incrementing a counter or toggling a flag, atomic operations can be simpler to use and understand than other concurrency control mechanisms.
  • Lock-Free Programming: They enable lock-free algorithms, which can improve performance by reducing contention and deadlock risks.
  • Ordering Guarantees: Atomic operations provide guarantees about ordering operations, which is crucial in multi-threading, where the order of execution can be non-deterministic.
  • Consistency and Predictability: In multi-threaded applications, they provide a consistent view of memory across different threads, leading to predictable behavior.

Disadvantages of Atomic Operations in C:

  • Complexity: Properly understanding and using atomic operations requires a good grasp of memory models and concurrency, which can be complex and error-prone.
  • Limited Scope: Atomic operations are generally limited to simple operations. Complex operations or critical sections still require traditional locking mechanisms.
  • Performance Overhead: While faster than locks for simple operations, atomic operations can still have significant performance overhead, especially on weakly ordered memory models requiring memory fences.
  • Portability Issues: The behavior and performance of atomic operations can vary between hardware architectures and compilers, potentially leading to portability issues.
  • Overuse Pitfalls: Overusing atomic operations, especially in cases where higher-level abstractions are more appropriate, can lead to maintainability issues and performance bottlenecks.
  • Scalability Limits: In highly contended scenarios, the performance of atomic operations can degrade, limiting scalability.

In the next article, I will discuss Advanced C Concepts . In this article, I explain Atomic Operations in C Language with Examples. I hope you enjoy this Atomic Operations in C Language with Examples article. I would like to have your feedback. Please post your feedback, questions, or comments about this article.

dotnettutorials 1280x720

About the Author: Pranaya Rout

Pranaya Rout has published more than 3,000 articles in his 11-year career. Pranaya Rout has very good experience with Microsoft Technologies, Including C#, VB, ASP.NET MVC, ASP.NET Web API, EF, EF Core, ADO.NET, LINQ, SQL Server, MYSQL, Oracle, ASP.NET Core, Cloud Computing, Microservices, Design Patterns and still learning new technologies.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • Create Account

Home Posts Topics Members FAQ

Join Bytes to post your question to a community of 473,388 software developers and data experts.

By using Bytes.com and it's services, you agree to our Privacy Policy and Terms of Use .

To disable or enable advertisements and analytics tracking please visit the manage ads & tracking page.

The atomic types (C++11)

Specializations and instantiations of the atomic template must have a deleted copy constructor, a deleted copy assignment operator, and a constexpr value constructor.

The standard library provides full specializations of the atomic template for integral types, one full specialization for the bool type, and partial specializations for all pointer types. Each of these specializations has standard layout, a trivial default constructor, and a trivial destructor. They all support aggregate initialization syntax.

  • Member functions (C++11)
  • Specialized member functions (C++11)
  • <cassert> (assert.h)
  • <cctype> (ctype.h)
  • <cerrno> (errno.h)
  • C++11 <cfenv> (fenv.h)
  • <cfloat> (float.h)
  • C++11 <cinttypes> (inttypes.h)
  • <ciso646> (iso646.h)
  • <climits> (limits.h)
  • <clocale> (locale.h)
  • <cmath> (math.h)
  • <csetjmp> (setjmp.h)
  • <csignal> (signal.h)
  • <cstdarg> (stdarg.h)
  • C++11 <cstdbool> (stdbool.h)
  • <cstddef> (stddef.h)
  • C++11 <cstdint> (stdint.h)
  • <cstdio> (stdio.h)
  • <cstdlib> (stdlib.h)
  • <cstring> (string.h)
  • C++11 <ctgmath> (tgmath.h)
  • <ctime> (time.h)
  • C++11 <cuchar> (uchar.h)
  • <cwchar> (wchar.h)
  • <cwctype> (wctype.h)

Containers:

  • C++11 <array>
  • <deque>
  • C++11 <forward_list>
  • <list>
  • <map>
  • <queue>
  • <set>
  • <stack>
  • C++11 <unordered_map>
  • C++11 <unordered_set>
  • <vector>

Input/Output:

  • <fstream>
  • <iomanip>
  • <ios>
  • <iosfwd>
  • <iostream>
  • <istream>
  • <ostream>
  • <sstream>
  • <streambuf>

Multi-threading:

  • C++11 <atomic>
  • C++11 <condition_variable>
  • C++11 <future>
  • C++11 <mutex>
  • C++11 <thread>
  • <algorithm>
  • <bitset>
  • C++11 <chrono>
  • C++11 <codecvt>
  • <complex>
  • <exception>
  • <functional>
  • C++11 <initializer_list>
  • <iterator>
  • <limits>
  • <locale>
  • <memory>
  • <new>
  • <numeric>
  • C++11 <random>
  • C++11 <ratio>
  • C++11 <regex>
  • <stdexcept>
  • <string>
  • C++11 <system_error>
  • C++11 <tuple>
  • C++11 <type_traits>
  • C++11 <typeindex>
  • <typeinfo>
  • <utility>
  • <valarray>
  • <atomic>
  • C++11 atomic
  • C++11 atomic_flag
  • C++11 memory_order
  • C++11 atomic_signal_fence
  • C++11 atomic_thread_fence
  • C++11 kill_dependency

initialization macros

  • C++11 ATOMIC_FLAG_INIT
  • C++11 ATOMIC_VAR_INIT

functions (C-style atomics)

  • C++11 atomic_compare_exchange_strong
  • C++11 atomic_compare_exchange_strong_explicit
  • C++11 atomic_compare_exchange_weak
  • C++11 atomic_compare_exchange_weak_explicit
  • C++11 atomic_exchange
  • C++11 atomic_exchange_explicit
  • C++11 atomic_fetch_add
  • C++11 atomic_fetch_add_explicit
  • C++11 atomic_fetch_and
  • C++11 atomic_fetch_and_explicit
  • C++11 atomic_fetch_or
  • C++11 atomic_fetch_or_explicit
  • C++11 atomic_fetch_sub
  • C++11 atomic_fetch_sub_explicit
  • C++11 atomic_fetch_xor
  • C++11 atomic_fetch_xor_explicit
  • C++11 atomic_flag_clear
  • C++11 atomic_flag_clear_explicit
  • C++11 atomic_flag_test_and_set
  • C++11 atomic_flag_test_and_set_explicit
  • C++11 atomic_init
  • C++11 atomic_is_lock_free
  • C++11 atomic_load
  • C++11 atomic_load_explicit
  • C++11 atomic_store
  • C++11 atomic_store_explicit
  • C++11 atomic::atomic

member functions

  • C++11 atomic::compare_exchange_strong
  • C++11 atomic::compare_exchange_weak
  • C++11 atomic::exchange
  • C++11 atomic::is_lock_free
  • C++11 atomic::load
  • C++11 atomic::operator T
  • C++11 atomic::operator=
  • C++11 atomic::store

member functions (spec.)

  • C++11 atomic::fetch_add
  • C++11 atomic::fetch_and
  • C++11 atomic::fetch_or
  • C++11 atomic::fetch_sub
  • C++11 atomic::fetch_xor
  • C++11 atomic::operator--
  • C++11 atomic::operator (comp. assign.)
  • C++11 atomic::operator++

std:: atomic ::operator (comp. assign.)

Return value, exception safety.

cppreference.com

Std::atomic<t>:: operator=, [ edit ] parameters, [ edit ] return value, [ edit ] notes.

Unlike most assignment operators, the assignment operators for atomic types do not return a reference to their left-hand arguments. They return a copy of the stored value instead.

[ edit ] See also

  • Recent changes
  • Offline version
  • What links here
  • Related changes
  • Upload file
  • Special pages
  • Printable version
  • Permanent link
  • Page information
  • In other languages
  • This page was last modified on 18 September 2023, at 05:27.
  • This page has been accessed 132,755 times.
  • Privacy policy
  • About cppreference.com
  • Disclaimers

Powered by MediaWiki

IMAGES

  1. C++ : Is pointer assignment atomic in C++?

    c pointer assignment atomic

  2. Pointer Expressions in C with Examples

    c pointer assignment atomic

  3. C Pointers

    c pointer assignment atomic

  4. Pointer Expressions in C with Examples

    c pointer assignment atomic

  5. Unlock the Mysteries of Pointers in C

    c pointer assignment atomic

  6. C Pointer to Pointer (Theory & Example)

    c pointer assignment atomic

VIDEO

  1. C++ From Scratch: std::atomic

  2. C Language || Pointers in C || Part-4: Pointer Assignment in C || Telugu Scit Tutorial

  3. What are Pointers? How to Use Them? and How they can Improve your C++ Programming Skills

  4. BARC Assignment Atomic & Molecular Physics @physicsgalaxy1537

  5. DAY_1_ADVANCED_C_POINTER_WORKSHOP_4

  6. What are Pointers? How to Use Them? and How they can Improve your C++ Programming Skills

COMMENTS

  1. Is changing a pointer considered an atomic action in C?

    4. The cop-out answer is that the C spec does not require a pointer assignment to be atomic, so you can't count on it being atomic. The actual answer would be that it probably depends on your platform, compiler, and possibly the alignment of the stars on the day you wrote the program. answered May 18, 2009 at 18:25.

  2. std::atomic<std::shared_ptr>

    The partial template specialization of std::atomic for std:: shared_ptr < T > allows users to manipulate shared_ptr objects atomically.. If multiple threads of execution access the same std::shared_ptr object without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur unless all such access is performed through an instance of std ...

  3. std::atomic_...<std::shared_ptr>

    The Concurrency TS offers atomic smart pointer classes atomic_shared_ptr and atomic_weak_ptr as a replacement for the use of these functions. These functions were deprecated in favor of the specializations of the std::atomic template: std::atomic<std::shared_ptr> and std::atomic<std::weak_ptr> . (since C++20)

  4. Toward a Better Use of C11 Atomics

    Introduction. Following the lead of C++, along with a memory model describing the requirements and semantics of multithreaded programs, the C11 standard adopted a proposal for a set of atomic types and operations into the language. This change has made it possible to write portable multi-threaded software that efficiently manipulates objects ...

  5. (Almost) Everything You Need To Know About Pointers in C

    char *pa = &a; // pa now contains the address of a. printf("%p", pa); // %p is the format specifier to print a pointer. If you run this program, you will see something like 0x7ffc2fc4ff27. That is the value of the pointer, which is the address of the variable a (this is in hexadecimal). This value is not fixed.

  6. Pointer declaration

    Pointers to functions. A pointer to function can be initialized with an address of a function. Because of the function-to-pointer conversion, the address-of operator is optional: void f (int);void(* pf1 )(int)=& f;void(* pf2 )(int)= f;// same as &f. Unlike functions, pointers to functions are objects and thus can be stored in arrays, copied ...

  7. How C-Pointers Works: A Step-by-Step Beginner's Tutorial

    To declare and initialize a pointer to a pointer, you need to add an extra asterisk (*) compared to a regular pointer. Let's go through an example: int x = 10; int *ptr1 = &x; // Pointer to an integer int **ptr2 = &ptr1; // Pointer to a pointer to an integer. In this example, ptr2 is a pointer to a pointer.

  8. Atomic Operations in C Language with Examples

    Example: Atomic Fetch Add. This example shows how to add to a variable atomically. Output: Counter value after atomic addition: 5. Here, atomic_fetch_add is used to atomically add a value to the counter. The operation returns the old value of the counter, but this return value is ignored in the example.

  9. Implementation of std::atomic<std::shared_ptr<T>> for C++20

    6. As you may know, C++20 has added std::atomic<std::shared_ptr<T>> specialization to the standard, but sadly, most compilers have not implemented it yet. So I decided to implement it myself. I want to know if I can improve this code or not. In addition, I'm not sure if my implementations of wait(), notify_one() and notify_all() are correct ...

  10. std::atomic

    Notes. There are non-member function template equivalents for all member functions of std::atomic.Those non-member functions may be additionally overloaded for types that are not specializations of std::atomic, but are able to guarantee atomicity.The only such type in the standard library is std:: shared_ptr < U >.. _Atomic is a keyword and used to provide atomic types in C.

  11. Is assignment to C++ int and pointer variables atomic?

    While int *could* be atomic on a given platform, it is certainly not required. Consider, for example, an implementation of C on a typical 8-bit CPU - all ints will have to be accessed with a pair of loads or stores. Pardon if this is the wrong newsgroup for this question, and/or if this question is naive.

  12. atomic

    Objects of atomic types contain a value of a particular type (T).The main characteristic of atomic objects is that access to this contained value from different threads cannot cause data races (i.e., doing that is well-defined behavior, with accesses properly sequenced).Generally, for all other objects, the possibility of causing a data race for accessing the same object concurrently qualifies ...

  13. The atomic types (C++11)

    Specializations and instantiations of the atomic template must have a deleted copy constructor, a deleted copy assignment operator, and a constexpr value constructor.. The standard library provides full specializations of the atomic template for integral types, one full specialization for the bool type, and partial specializations for all pointer types.

  14. Assignment operators

    Assignment performs implicit conversion from the value of rhs to the type of lhs and then replaces the value in the object designated by lhs with the converted value of rhs . Assignment also returns the same value as what was stored in lhs (so that expressions such as a = b = c are possible). The value category of the assignment operator is non ...

  15. Is struct assignment atomic in C/C++?

    No, both C and C++ standard don't guarantee assignment operations to be atomic. You need some implementation-specific stuff for that - either something in the compiler or in the operating system. Or you need C++11 std::atomic<A> shared_struct; or C11 _Atomic struct A shared_struct; See my answer.

  16. A simple guide to atomics in C++

    The compare exchange also called compare and swap (CAS) is the most powerful atomic operation available in C++. In most cases, it allows an atomic comparison of the current value of an atomic variable, and if that comparison is true, it then attempts to store the desired value. Despite being an atomic operation, a compare exchange can certainly ...

  17. C++

    atomic specializations for integral (1) and pointer (2) types support compound assignments: Each of these functions accesses the contained value, apply the proper operator and return the value the contained value had immediately before the operation; all in a single atomic operation that cannot be affected by other threads. These functions behave as if the proper fetch_* member function was ...

  18. std::atomic<T>::operator=

    make_hazard_pointer (C++26) Atomic types: atomic (C++11) atomic_ref (C++20) atomic_flag (C++11) ... Unlike most assignment operators, the assignment operators for atomic types do not return a reference to their left-hand arguments. They return a copy of the stored value instead.

  19. c++

    As long as the memory bus is at least as wide as the type being read or written, the CPU reads and writes these types in a single bus transaction, making it impossible for other threads to see them in a half-completed state. On x86 and x64 there, is no guarantee that reads and writes larger than eight bytes are atomic.