DEV Community

Emil Ossola
Emil Ossola

Posted on

Understanding the AtomicUsize in std::sync::atomic (Rust Tutorial)

The std::sync::atomic module in Rust provides a set of types and functions for performing synchronized atomic operations on shared memory locations. This module is particularly useful for writing concurrent and parallel programs, as it ensures that multiple threads can safely access and modify shared data without causing race conditions or data corruption.

The AtomicUsize type is one of the key components of this module. It represents an unsigned integer that can be atomically modified and read by multiple threads. Rust provides a wide range of atomic operations for AtomicUsize, such as compare-and-swap, load, store, fetch-and-add, and more. These operations ensure that modifications to AtomicUsize are performed atomically, without the need for explicit locks or mutexes.

By using std::sync::atomic and AtomicUsize, Rust programmers can build high-performance and thread-safe code that takes full advantage of modern multi-core processors. The module's well-defined and carefully designed APIs make it easier to reason about concurrent code and avoid common pitfalls associated with shared memory access.

In this guide, we will explore the features and usage of AtomicUsize in std::sync::atomic, providing a comprehensive understanding of how to leverage synchronized atomic operations in Rust. We will cover various atomic operations, memory ordering, and best practices for writing concurrent code using AtomicUsize.

Image description

What is AtomicUsize?

The AtomicUsize type, found in the std::sync::atomic module of the Rust programming language, provides a way to perform synchronized atomic operations on usize values. This type guarantees that the operations performed on it are atomic, meaning they cannot be interrupted by concurrent threads, ensuring correctness and preventing data races.

With AtomicUsize, developers can safely perform synchronization tasks and implement lock-free data structures in Rust. Understanding how to use AtomicUsize effectively is crucial for writing concurrent and thread-safe Rust code. This guide will provide a comprehensive explanation of AtomicUsize and its various methods and operations.

Atomic Operations with AtomicUsize

The AtomicUsize type in the std::sync::atomic module of Rust provides a way to perform synchronized atomic operations on a usize value. Here are some of the common atomic operations that can be performed on an AtomicUsize:

  • Load: The load() method retrieves the current value of the atomic variable. It returns the value without modifying it.
  • Store: The store(value) method sets the value of the atomic variable to the given value. It overwrites the current value without returning anything.
  • Fetch-And-Add: The fetch_add(value, ordering) method adds the given value to the current value of the atomic variable and returns the previous value. The ordering parameter specifies the memory ordering constraints for the operation.
  • Fetch-And-Sub: The fetch_sub(value, ordering) method subtracts the given value from the current value of the atomic variable and returns the previous value. The ordering parameter specifies the memory ordering constraints for the operation.
  • Fetch-And-And: The fetch_and(value, ordering) method performs a bitwise AND operation between the given value and the current value of the atomic variable. It stores the result in the atomic variable and returns the previous value. The ordering parameter specifies the memory ordering constraints for the operation.
  • Fetch-And-Or: The fetch_or(value, ordering) method performs a bitwise OR operation between the given value and the current value of the atomic variable. It stores the result in the atomic variable and returns the previous value. The ordering parameter specifies the memory ordering constraints for the operation.
  • Fetch-And-Xor: The fetch_xor(value, ordering) method performs a bitwise XOR operation between the given value and the current value of the atomic variable. It stores the result in the atomic variable and returns the previous value. The ordering parameter specifies the memory ordering constraints for the operation.

We'll look a little bit deeper into these operations and explain them with examples:

Load in Rust AtomicUsize

In Rust, the AtomicUsize type from the std::sync::atomic module provides synchronized atomic operations for manipulating unsigned integers. The load method is used to atomically read the value of an AtomicUsize variable.

It ensures that the value read is always the most up-to-date value, even in the presence of concurrent modifications by other threads. The load method takes no arguments and returns the current value of the AtomicUsize. This method is useful when you need to access the value of the atomic variable without modifying it.

Here's an example:

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

fn main() {
    let atomic_value = AtomicUsize::new(42);

    let loaded_value = atomic_value.load(Ordering::SeqCst);
    println!("Loaded value: {}", loaded_value);
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create an AtomicUsize named atomic_value and initialize it with a value of 42. We then call the load method on atomic_value, passing an Ordering parameter to specify the memory ordering.

The load method retrieves the current value stored in atomic_value atomically and returns it. In this case, we use Ordering::SeqCst to indicate sequential consistency, ensuring that the load operation is synchronized and provides a consistent view of the shared data.

The loaded value is then printed, resulting in the output:

Loaded value: 42
Enter fullscreen mode Exit fullscreen mode

The load method is useful when you need to retrieve the current value of an atomic variable in a thread-safe and synchronized manner. It allows you to safely read the shared data without causing data races or undefined behavior in concurrent scenarios.

Store in Rust AtomicUsize

The Store operation in AtomicUsize is used to atomically store a value into the atomic variable. It guarantees that the value is written to memory in a synchronized manner, making it visible to all threads. The Store operation takes in two arguments: the desired value to be stored and the ordering constraint.

The ordering constraint determines the synchronization requirements for the operation, allowing fine-grained control over how other threads perceive the operation's relationship with other memory accesses. By using Store, developers can ensure that the stored value is safely shared among concurrent threads.

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

fn main() {
    let atomic_value = AtomicUsize::new(0);

    atomic_value.store(42, Ordering::SeqCst);
    let stored_value = atomic_value.load(Ordering::SeqCst);
    println!("Stored value: {}", stored_value);
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create an AtomicUsize named atomic_value and initialize it with a value of 0. We then call the store method on atomic_value, passing the value 42 to be stored and an Ordering parameter to specify the memory ordering.

The store method sets the value of atomic_value atomically, ensuring that the store operation is synchronized and provides a consistent update to the shared data. In this case, we use Ordering::SeqCst to indicate sequential consistency.

After storing the value, we call the load method on atomic_value to retrieve the updated value. The loaded value is then printed, resulting in the output:

Stored value: 42
Enter fullscreen mode Exit fullscreen mode

The store method is useful when you need to atomically set the value of an atomic variable in a thread-safe and synchronized manner. It allows you to safely update the shared data without causing data races or undefined behavior in concurrent scenarios.

Fetch-And-Add in Rust AtomicUsize

The fetch_add method provided by AtomicUsize in std::sync::atomic is a powerful operation that allows synchronized atomic incrementations or decrements of a value. This method atomically adds a given value to the current value of the atomic variable and returns the previous value.

By using fetch_add, concurrent threads can safely modify the variable's value without causing data races or inconsistencies. This operation is useful in scenarios where multiple threads need to perform incremental updates to a shared counter or maintain a consistent state in a concurrent environment.

In Rust, the fetch_add method in AtomicUsize is used to atomically fetch the current value of the atomic variable and add a given value to it. It ensures that the operation is synchronized and provides a consistent update to the shared data. Here's an example:

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

fn main() {
    let atomic_value = AtomicUsize::new(10);

    let original_value = atomic_value.fetch_add(5, Ordering::SeqCst);
    println!("Original value: {}", original_value);

    let updated_value = atomic_value.load(Ordering::SeqCst);
    println!("Updated value: {}", updated_value);
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create an AtomicUsize named atomic_value and initialize it with a value of 10. We then call the fetch_add method on atomic_value, passing the value 5 to be added and an Ordering parameter to specify the memory ordering.

The fetch_add method atomically fetches the current value of atomic_value, adds the given value (5 in this case), and returns the original value before the addition. In this example, the original value is printed.

After the fetch and addition operation, we call the load method on atomic_value to retrieve the updated value. The updated value is then printed.

The output will be:

Original value: 10
Updated value: 15
Enter fullscreen mode Exit fullscreen mode

The fetch_add method is useful when you need to atomically perform an addition operation on an atomic variable in a thread-safe and synchronized manner. It allows you to safely modify the shared data without causing data races or undefined behavior in concurrent scenarios.

Fetch-And-Sub in Rust AtomicUsize

The fetch_sub method is a synchronized atomic operation provided by the AtomicUsize type in Rust's std::sync::atomic module.

This method atomically subtracts a value from the current value of the AtomicUsize variable and returns the previous value. It ensures that no other threads can access or modify the variable during the operation, guaranteeing synchronization.

This is useful in scenarios where multiple threads need to perform subtractive operations on a shared variable without interference, maintaining data integrity and consistency.

Here's an example:

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

fn main() {
    let atomic_value = AtomicUsize::new(20);

    let original_value = atomic_value.fetch_sub(7, Ordering::SeqCst);
    println!("Original value: {}", original_value);

    let updated_value = atomic_value.load(Ordering::SeqCst);
    println!("Updated value: {}", updated_value);
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create an AtomicUsize named atomic_value and initialize it with a value of 20. We then call the fetch_sub method on atomic_value, passing the value 7 to be subtracted and an Ordering parameter to specify the memory ordering.

The fetch_sub method atomically fetches the current value of atomic_value, subtracts the given value (7 in this case), and returns the original value before the subtraction. In this example, the original value is printed.

After the fetch and subtraction operation, we call the load method on atomic_value to retrieve the updated value. The updated value is then printed.

The output will be:

Original value: 20
Updated value: 13
Enter fullscreen mode Exit fullscreen mode

The fetch_sub method is useful when you need to atomically perform a subtraction operation on an atomic variable in a thread-safe and synchronized manner. It allows you to safely modify the shared data without causing data races or undefined behavior in concurrent scenarios.

Fetch-And-And in Rust AtomicUsize

In the context of synchronized atomic operations in Rust, the fetch_and method, available on AtomicUsize, allows for the atomic bitwise AND operation between the current value and a given value. This operation updates the value in an atomic and synchronized manner, ensuring that no other thread can interfere during the operation.

The fetch_and method returns the previous value, allowing for a variety of useful applications such as bitmasking, clearing specific bits, or implementing complex synchronization protocols.

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

fn main() {
    let atomic_value = AtomicUsize::new(0b1111_1111);

    let result = atomic_value.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current_value| {
        current_value & 0b1111_0000
    });

    println!("Previous value: {:08b}", result);
    println!("Updated value:  {:08b}", atomic_value.load(Ordering::SeqCst));
}
Enter fullscreen mode Exit fullscreen mode

In this example, we start with an AtomicUsize named atomic_value initialized with the binary value 1111_1111 (255 in decimal).

We use the fetch_update method on atomic_value and provide two Ordering parameters: the first one specifies the ordering for the load operation, and the second one specifies the ordering for the store operation.

Within the closure passed to fetch_update, we perform the bitwise AND operation (current_value & 0b1111_0000) on the current value of the atomic variable.

The fetch_update method atomically updates the value of atomic_value by applying the closure to the current value, and it returns the previous value before the update.

Finally, we print the previous value (result) and the updated value after the bitwise AND operation.

The output will be:

Previous value: 11111111
Updated value:  11110000
Enter fullscreen mode Exit fullscreen mode

Using the fetch_update method with a closure that performs the bitwise AND operation allows you to achieve similar functionality to fetch_and. It performs the bitwise AND operation between the given value and the current value of the atomic variable, stores the result, and returns the previous value.

Fetch-And-Or in Rust AtomicUsize

In Rust's std::sync::atomic module, the AtomicUsize type provides a set of synchronized atomic operations. One such operation is the fetch_and_or method, which atomically performs a bitwise OR operation between the current value of the atomic usize and the provided value, returning the previous value of the atomic usize.

This method is useful in scenarios where multiple threads need to update a shared value while ensuring that no data races occur. By using the fetch_and_or method, developers can safely perform atomic bitwise OR operations and retrieve the previous value in a synchronized manner.

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

fn main() {
    let atomic_value = AtomicUsize::new(0b0000_1111);

    let result = atomic_value.fetch_or(0b1111_0000, Ordering::SeqCst);

    println!("Previous value: {:08b}", result);
    println!("Updated value:  {:08b}", atomic_value.load(Ordering::SeqCst));
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an AtomicUsize named atomic_value initialized with the binary value 0000_1111 (15 in decimal).

We use the fetch_or method on atomic_value and provide the value 0b1111_0000 to perform the bitwise OR operation.

The fetch_or method atomically performs the bitwise OR operation between the given value and the current value of atomic_value, stores the result in atomic_value, and returns the previous value before the update.

Finally, we print the previous value (result) and the updated value of atomic_value after the bitwise OR operation.

The output will be:

Previous value: 00001111
Updated value:  11111111
Enter fullscreen mode Exit fullscreen mode

Using the fetch_or method, you can perform a bitwise OR operation atomically between the given value and the current value of the atomic variable. It allows you to update the atomic value while obtaining the previous value for further processing or evaluation.

Fetch-And-Xor in Rust AtomicUsize

The fetch_xor operation in AtomicUsize is a synchronized atomic operation in Rust's std::sync::atomic module. It performs an exclusive OR operation between the current value of the atomic usize and the given value, and stores the result back in the atomic usize. This operation guarantees atomicity, ensuring that multiple threads can safely perform the operation concurrently without any data races.

In Rust, the fetch_xor method is used to perform a bitwise XOR operation between the given value and the current value of the atomic variable. It stores the result in the atomic variable and returns the previous value. The ordering parameter specifies the memory ordering constraints for the operation. Here's an example:

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

fn main() {
    let atomic_value = AtomicUsize::new(0b1010_1010);

    let result = atomic_value.fetch_xor(0b1100_0011, Ordering::SeqCst);

    println!("Previous value: {:08b}", result);
    println!("Updated value:  {:08b}", atomic_value.load(Ordering::SeqCst));
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an AtomicUsize named atomic_value initialized with the binary value 1010_1010 (170 in decimal).

We use the fetch_xor method on atomic_value and provide the value 0b1100_0011 to perform the bitwise XOR operation.

The fetch_xor method atomically performs the bitwise XOR operation between the given value and the current value of atomic_value, stores the result in atomic_value, and returns the previous value before the update.

Finally, we print the previous value (result) and the updated value of atomic_value after the bitwise XOR operation.

The output will be:

Previous value: 10101010
Updated value:  01101101
Enter fullscreen mode Exit fullscreen mode

Using the fetch_xor method, you can perform a bitwise XOR operation atomically between the given value and the current value of the atomic variable. It allows you to update the atomic value while obtaining the previous value for further processing or evaluation.

What is an Atomic Ordering?

In concurrent programming, atomic ordering refers to the order in which operations on shared variables are observed by different threads. When multiple threads are accessing and modifying shared data concurrently, it is crucial to ensure that their operations are executed in a predictable and consistent manner.

The concept of atomic ordering provides a way to control and synchronize these operations, ensuring that they are performed atomically and in a defined order. The Rust programming language provides the AtomicUsize type in the std::sync::atomic module, which offers a set of synchronized atomic operations that can be used to implement concurrent algorithms and ensure correct behavior in multi-threaded programs.

In Rust, the AtomicUsize type in the std::sync::atomic module provides synchronized atomic operations. When performing atomic operations, you can specify the ordering for how these operations interact with other concurrent operations. The following are the different ordering options available:

Relaxed Ordering in Rust

This is the least restrictive ordering option. It allows reordering and allows operations to appear out of order when viewed by other threads. It provides no synchronization or guarantees about the visibility of other concurrent operations.

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

fn main() {
    let atomic_value = AtomicUsize::new(0);

    atomic_value.store(1, Ordering::Relaxed);
    let result = atomic_value.load(Ordering::Relaxed);

    println!("Result: {}", result);
}
Enter fullscreen mode Exit fullscreen mode

In this example, the store and load operations are performed with Ordering::Relaxed. This provides no synchronization guarantees or ordering constraints, allowing reordering and potential out-of-order visibility of operations.

Release Ordering in Rust

This ordering option ensures that all previous operations on the atomic variable are completed before the release operation. It provides synchronization with other threads by preventing the reordering of subsequent operations.

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

fn main() {
    let atomic_value = AtomicUsize::new(0);

    atomic_value.store(1, Ordering::Release);
    let result = atomic_value.load(Ordering::Relaxed);

    println!("Result: {}", result);
}
Enter fullscreen mode Exit fullscreen mode

In this example, the store operation is performed with Ordering::Release, ensuring that all previous operations on atomic_value are completed before the release. The subsequent load operation can have a relaxed ordering because it does not require synchronization with other threads.

Acquire Ordering in Rust

This ordering option ensures that the acquire operation completes before any subsequent operations on the atomic variable. It guarantees synchronization with other threads by preventing the reordering of previous operations.

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

fn main() {
    let atomic_value = AtomicUsize::new(0);

    atomic_value.store(1, Ordering::Relaxed);
    let result = atomic_value.load(Ordering::Acquire);

    println!("Result: {}", result);
}
Enter fullscreen mode Exit fullscreen mode

In this example, the store operation is performed with Ordering::Relaxed, allowing reordering. The subsequent load operation is performed with Ordering::Acquire, ensuring that the acquire operation completes before any subsequent operations on atomic_value.

AcqRel (Acquire-Release) Ordering in Rust

This ordering option combines the properties of both acquire and release. It ensures that the acquire operation completes before any subsequent operations and that all previous operations complete before the release operation.

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

fn main() {
    let atomic_value = AtomicUsize::new(0);

    atomic_value.store(1, Ordering::AcqRel);
    let result = atomic_value.load(Ordering::AcqRel);

    println!("Result: {}", result);
}
Enter fullscreen mode Exit fullscreen mode

In this example, both the store and load operations are performed with Ordering::AcqRel. The acquire operation guarantees synchronization with other threads by preventing reordering of previous operations, and the release operation ensures that all previous operations complete before the subsequent operations.

SeqCst (Sequentially Consistent) Ordering in Rust

This is the most restrictive ordering option. It ensures that all operations on the atomic variable are seen in a total order by all threads. It provides synchronization and guarantees that operations will behave as if they occurred in a sequential order, regardless of the actual order of execution.

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

fn main() {
    let atomic_value = AtomicUsize::new(0);

    atomic_value.store(1, Ordering::SeqCst);
    let result = atomic_value.load(Ordering::SeqCst);

    println!("Result: {}", result);
}
Enter fullscreen mode Exit fullscreen mode

In this example, both the store and load operations are performed with Ordering::SeqCst. This provides the most restrictive ordering and synchronization guarantees. The operations on atomic_value are seen in a total order by all threads, behaving as if they occurred in a sequential order.

Tips for optimizing the usage of AtomicUsize in performance-critical scenarios

When working with the AtomicUsize type in Rust's std::sync::atomic module for synchronized atomic operations, there are several tips to consider for optimizing performance in critical scenarios:

  1. Minimize unnecessary atomic operations: Atomic operations are relatively expensive compared to regular variable access. Therefore, it is crucial to minimize the number of atomic operations in your code. Consider using atomic operations only when necessary to synchronize threads.
  2. Use non-atomic operations for non-shared data: If a variable is not accessed by multiple threads simultaneously, it can be declared as a regular variable rather than an AtomicUsize. This avoids the overhead of atomic operations.
  3. Batch atomic operations when possible: If you need to perform multiple atomic operations in succession, consider grouping them together using a single lock or atomic operation. This reduces the overhead of acquiring and releasing locks or performing atomic operations multiple times.
  4. Avoid unnecessary synchronization: If synchronization is not required for a particular operation or section of code, consider using unsynchronized operations instead. This can greatly improve performance in scenarios where synchronization is not necessary.
  5. Consider using other synchronization primitives: Depending on the specific requirements of your application, you may find that other synchronization primitives, such as mutexes or RwLocks, provide better performance than atomic operations. Evaluate different options to find the most efficient solution for your use case.

By following these tips, you can optimize the usage of AtomicUsize and improve the performance of your code in performance-critical scenarios.

Learn Rust Programming with Online Rust IDE

Rust is a powerful systems programming language known for its safety, speed, and concurrency features.

To start learning Rust, an online Rust IDE (Integrated Development Environment) can be a valuable tool. Lightly IDE provides a web-based environment where you can write, compile, and execute Rust code without the need for local installations.

Image description

It offer features like syntax highlighting, code completion, error checking, and the ability to run code snippets. With an online Rust IDE, beginners can dive into Rust programming quickly, experiment with code, and get instant feedback.

Say hello to an accessible and convenient way to explore Rust's syntax, concepts, and build small projects, and enhance your learning experience to become proficient in Rust programming.

Read more: Understanding the AtomicUsize in std::sync::atomic (Rust Tutorial)

Top comments (0)