How it works...

As of the time of writing, there are four atomic types in the standard library under the std::sync::atomic module: AtomicBool, AtomicIsize, AtomicUsize, and AtomicPtr. Each one of them represents a primitive type, namely bool, isize, usize, and *mut. We are not going to look at the last, which, being a pointer, you will probably only have to deal with when interfacing with programs written in other languages anyways.

In case you haven't encountered isize and usize before, they are representations of the smallest amount of bytes needed to address any part of the memory of your machine. On 32-bit targets this is 4 bytes, while 64-bit systems will need 8 bytes. isize uses those bytes to represent a signed number, as in an integer that can be negative. usize instead represents an unsigned number, which can only be positive but has a lot more capacity for huge numbers in that direction. They are usually used when dealing with collections capacities. For example, Vec returns a usize when calling its .len() method. Additionally, on the nightly toolchain, there are atomic variants of all other concrete integer types like u8 or i32.

The atomic versions of our primitives work the same way as their cousins, with one important distinction: they have well-defined behavior when used in parallel environments. All their methods take a parameter of the type atomic::Ordering, which stands for which low-level concurrent strategy to use. In this example, we are only going to use Ordering::SeqCst, which stands for sequentially consistent. This, in turn, means that the behavior is quite intuitive. If some data is stored or modified using this ordering, another thread can see its content after the write as if the two threads ran one after another. Or, in other words, the behavior is consistent with that of a sequential series of events. This strategy will always work with all parallel algorithms. All other orderings merely relax the constraints on the data involved in order to get some kind of performance benefit.

With this knowledge in hand, you should be able to understand most things done in main up to the usage of NaiveMutex[72]. Note how some of the atomic methods are just different ways of doing the same as with our normal primitives, with the added twist of specifying an ordering and most of them returning the old value. For instance, some_number.fetch_add(12, Ordering::SeqCst), apart from returning the old value of some_number, is essentially nothing but some_number += 12.

A real use case for atomics comes up in the second part of the example code, where we implement our very own Mutex. A mutex, prominently featured in all modern programming languages, is a kind of lock that does not allow any two threads to access a resource at the same time. After reading the last recipe, you know that you can imagine a Mutex as a kind of RwLock that always locks everything in write mode.

Let's jump a few lines forward in our code to [102]:

pub struct NaiveMutex<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}

As you can see, we are going to base our NaiveMutex on a simple atomic flag, locked, which is going to track whether our mutex is available or not. The other member, data, holds the underlying resource we are interested in locking. Its type, UnsafeCell, is the underlying type of every struct that implements some kind of interior mutability (see Chapter 5Advanced Data Structures; Working with interior mutability).

The next struct is going to look familiar to you if you've read Chapter 6, Handling Errors; Understanding RAII, as it's an RAII guard with a reference to its parent [110]:

pub struct NaiveMutexGuard<'a, T: 'a> {
naive_mutex: &'a NaiveMutex<T>,
}

Let's take a look at how we lock a thread:

pub fn lock(&self) -> NaiveMutexGuard<T> {
while self.locked.compare_and_swap(false, true, Ordering::SeqCst) {}
NaiveMutexGuard { naive_mutex: self }
}

Looks a bit weird at first glance, doesn't it? compare_and_swap is one of the more complex atomic operations. It works as follows:

  1. It compares the value of the atomic with the first parameter
  2. If they are the same, it stores the second parameter in the atomic
  3. Lastly, it returns the value of the atomic from before the function call

Let's apply that to our call:

  • compare_and_swap checks if self.locked contains false
  • If so, it sets self.locked to true
  • In any case, it will return the old value

If the returned value is true, it means our mutex is currently locked. What should our thread do then? Absolutely nothing: { }. Because we call this in a while loop, we will continue doing nothing (this is called spinning) until the situation changes. This algorithm is called spinlock.

When our mutex is finally available, we set its locked flag to true and return an RAII guard with a reference to our NaiveMutex.

This is not how the real std::sync::Mutex is implemented. Because exclusively locking a resource is a very basic concurrent task, operating systems natively support it. The Mutex implemented in the Rust standard library is still built by the RAII pattern as well, but uses the OS's mutex handles instead of our custom logic. Fun fact—the Windows implementation uses SRWLocks (https://msdn.microsoft.com/en-us/library/windows/desktop/aa904937(v=vs.85).aspx), which are Windows's native version of RwLock, as they proved to be faster than a native Mutex. So, on Windows at least, the two types really are very similar.

The implementation of NaiveMutexGuard provides the counterpart of lock during its dropping [138]:

fn drop(&mut self) {
self.naive_mutex.locked.store(false, Ordering::SeqCst);
}

We simply store the value false in self.locked whenever our guard goes out of scope (see Chapter 6Handling Errors; Implementing the Drop trait). The next two trait NaiveMutexGuard implements are Deref and DerefMut, which let us call methods of type T directly on NaiveMutexGuard<T>. They both share nearly the same implementation [146]:

unsafe { &*self.naive_mutex.data.get() }

Remember when we said that you'll have to deal with pointers on rare occasions? Well, this is one of those times.

UnsafeCell doesn't guarantee any borrowing safety, hence the name and the unsafe block. It relies on you to make sure all calls to it are actually safe. Because of this, it gives you a raw mutable pointer, which you can manipulate in any way you want. What we do here is dereference it with *, so *mut T becomes only T. Then we return a normal reference to that with &[146]. The only thing different in the implementation of deref_mut is that we instead return a mutable reference with &mut [152]. All of our unsafe calls are guaranteed to follow Rust's ownership principles, as we only allow one scope to borrow our resource anyway.

The last thing required for our Mutex implementation is the following line, which we skipped before:

unsafe impl<T: Send> Sync for NaiveMutex<T> {}

The Sync trait has a pretty small implementation, right? That's because it is a marker. It belongs to a family of traits that don't actually do anything themselves but only exist to tell the compiler something about the types that implement them. Another trait in the std::marker module is Send, which we also use here.

If a type T implements Send, it tells the world that it is safe to be moved (sent) between threads by passing it around as a value instead of a reference. Nearly all types of Rust implement Send.

If T is Sync, it tells the compiler that it is safe to be shared between threads (it behaves in a synchronized way) by passing it around per reference, &T. This is harder to accomplish than Send, but our NaiveMutex guarantees that types in it can be shared around, as we only allow one access to its inner type at a time. This is why we implement the Sync trait for every Send in our mutex. If it's safe to pass it around, it's automatically also safe to share it within NaiveMutex.

Back in main you can now find some usage examples of our Mutex[75 and 84], similar to the examples in the previous recipe.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.181.146