Find your perfect ski equipment Alpine Skis Touring Skis Nordic Skis Skiboots, Goggles, Helmets ★ Engineered in Austria Find a dealer nearby! Symbol: No Atomic Number: 102 Atomic Mass: (259.0) amu Melting Point: Unknown Boiling Point. Here is a list of the elements sorted by atomic number.Element nameElement symbolAtomic numberHydrogenH1HeliumHe2LithiumLi3.
Much has already been written about atomic operations on the web, usually with a focus on atomic read-modify-write (RMW) operations. However, those aren’t the only kinds of atomic operations. There are also atomic loads and stores, which are equally important. In this post, I’ll compare atomic loads and stores to their non-atomic counterparts at both the processor level and the C/C++ language level. Along the way, we’ll clarify the C++11 concept of a “data race”.
An operation acting on shared memory is atomic if it completes in a single step relative to other threads. When an atomic store is performed on a shared variable, no other thread can observe the modification half-complete. When an atomic load is performed on a shared variable, it reads the entire value as it appeared at a single moment in time. Non-atomic loads and stores do not make those guarantees.
Without those guarantees, lock-free programming would be impossible, since you could never let different threads manipulate a shared variable at the same time. We can formulate it as a rule:
Any time two threads operate on a shared variable concurrently, and one of those operations performs a write, both threads must use atomic operations.
If you violate this rule, and either thread uses a non-atomic operation, you’ll have what the C++11 standard refers to as a data race (not to be confused with Java’s concept of a data race, which is different, or the more general race condition). The C++11 standard doesn’t tell you why data races are bad; only that if you have one, “undefined behavior” will result (§1.10.21). The real reason why such data races are bad is actually quite simple: They result in torn reads and torn writes.
A memory operation can be non-atomic because it uses multiple CPU instructions, non-atomic even when using a single CPU instruction, or non-atomic because you’re writing portable code and you simply can’t make the assumption. Let’s look at a few examples.
Non-Atomic Due to Multiple CPU Instructions
Suppose you have a 64-bit global variable, initially zero.
At some point, you assign a 64-bit value to this variable.
When you compile this function for 32-bit x86 using GCC, it generates the following machine code.
As you can see, the compiler implemented the 64-bit assignment using two separate machine instructions. The first instruction sets the lower 32 bits to 0x00000002
, and the second sets the upper 32 bits to 0x00000001
. Clearly, this assignment operation is not atomic. If sharedValue
is accessed concurrently by different threads, several things can now go wrong:
- If a thread calling
storeValue
is preempted between the two machine instructions, it will leave the value of0x0000000000000002
in memory – a torn write. At this point, if another thread readssharedValue
, it will receive this completely bogus value which nobody intended to store. - Even worse, if a thread is preempted between the two instructions, and another thread modifies
sharedValue
before the first thread resumes, it will result in a permanently torn write: the upper 32 bits from one thread, the lower 32 bits from another. - On multicore devices, it isn’t even necessary to preempt one of the threads to have a torn write. When a thread calls
storeValue
, any thread executing on a different core could readsharedValue
at a moment when only half the change is visible.
Reading concurrently from sharedValue
brings its own set of problems:
Here too, the compiler has implemented the load operation using two machine instructions: The first reads the lower 32 bits into eax
, and the second reads the upper 32 bits into edx
. In this case, if a concurrent store to sharedValue
becomes visible between the two instructions, it will result in a torn read – even if the concurrent store was atomic.
These problems are not just theoretical. Mintomic’s test suite includes a test case called test_load_store_64_fail
, in which one thread stores a bunch of 64-bit values to a single variable using a plain assignment operator, while another thread repeatedly performs a plain load from the same variable, validating each result. On a multicore x86, this test fails consistently, as expected.
Non-Atomic CPU Instructions
A memory operation can be non-atomic even when performed by a single CPU instruction. For example, the ARMv7 instruction set includes the strd
instruction, which stores the contents of two 32-bit source registers to a single 64-bit value in memory.
On some ARMv7 processors, this instruction is not atomic. When the processor sees this instruction, it actually performs two separate 32-bit stores under the hood (§A3.5.3). Once again, another thread running on a separate core has the possibility of observing a torn write. Interestingly, a torn write is even possible on a single-core device: A system interrupt – say, for a scheduled thread context switch – can actually occur between the two internal 32-bit stores! In this case, when the thread resumes from the interrupt, it will restart the strd
instruction all over again.
As another example, it’s well-known that on x86, a 32-bit mov
instruction is atomic if the memory operand is naturally aligned, but non-atomic otherwise. In other words, atomicity is only guaranteed when the 32-bit integer is located at an address which is an exact multiple of 4. Mintomic comes with another test case, test_load_store_32_fail
, which verifies this guarantee. As it’s written, this test always succeeds on x86, but if you modify the test to force sharedInt
to certain unaligned addresses, it will fail. On my Core 2 Quad Q6600, the test fails when sharedInt
crosses a cache line boundary:
Those are enough processor-specific details for now. Let’s look at atomicity at the C/C++ language level.
All C/C++ Operations Are Presumed Non-Atomic
In C and C++, every operation is presumed non-atomic unless otherwise specified by the compiler or hardware vendor – even plain 32-bit integer assignment.
The language standards have nothing to say about atomicity in this case. Maybe integer assignment is atomic, maybe it isn’t. Since non-atomic operations don’t make any guarantees, plain integer assignment in C is non-atomic by definition.
In practice, we usually know more about our target platforms than that. For example, it’s common knowledge that on all modern x86, x64, Itanium, SPARC, ARM and PowerPC processors, plain 32-bit integer assignment is atomic as long as the target variable is naturally aligned. You can verify it by consulting your processor manual and/or compiler documentation. In the games industry, I can tell you that a lot of 32-bit integer assignments rely on this particular guarantee.
Nonetheless, when writing truly portable C and C++, there’s a long-standing tradition of pretending that we don’t know anything more than what the language standards tell us. Portable C and C++ is designed to run on every possible computing device past, present and imaginary. Personally, I like to imagine a machine where memory can only be changed by mixing it up first:
On such a machine, you definitely wouldn’t want to perform a concurrent read at the same time as a plain assignment; you could end up reading a completely random value.
In C++11, there is finally a way to perform truly portable atomic loads and stores: the C++11 atomic library. Atomic loads and stores performed using the C++11 atomic library would even work on the imaginary computer above – even if it means the C++11 atomic library must secretly lock a mutex to make each operation atomic. There’s also the Mintomic library which I released last month, which doesn’t support as many platforms, but works on several older compilers, is hand-optimized and is guaranteed to be lock-free.
Relaxed Atomic Operations
Let’s return to the original sharedValue
example from earlier in this post. We’ll rewrite it using Mintomic so that all operations are performed atomically on every platform Mintomic supports. First, we must declare sharedValue
as one of Mintomic’s atomic data types.
The mint_atomic64_t
type guarantees correct memory alignment for atomic access on each platform. This is important because, for example, the GCC 4.2 compiler for ARM bundled with Xcode 3.2.5 doesn’t guarantee that plain uint64_t
will be 8-byte aligned.
In storeValue
, instead of performing a plain, non-atomic assignment, we must call mint_store_64_relaxed
.
Similarly, in loadValue
, we call mint_load_64_relaxed
.
Using C++11’s terminology, these functions are now data race-free. When executing concurrently, there is absolutely no possibility of a torn read or write, whether the code runs on ARMv6/ARMv7 (Thumb or ARM mode), x86, x64 or PowerPC. If you’re curious how mint_load_64_relaxed
and mint_store_64_relaxed
actually work, both functions expand to an inline cmpxchg8b
instruction on x86; for other platforms, consult Mintomic’s implementation.
Here’s the exact same thing written in C++11 instead:
You’ll notice that both the Mintomic and C++11 examples use relaxed atomics, as evidenced by the _relaxed
suffix on various identifiers. The _relaxed
suffix is a reminder that few guarantees are made about memory ordering.
In particular, it is still legal for the memory effects of a relaxed atomic operation to be reordered with respect to instructions which follow or precede it in program order, either due to compiler reordering or memory reordering on the processor itself. The compiler could even perform optimizations on redundant relaxed atomic operations, just as with non-atomic operations. In all cases, the operation remains atomic.
When manipulating shared memory concurrently, I think it’s good practice to always use Mintomic or C++11 atomic library functions, even in cases where you know that a plain load or store would already be atomic on your target platform. An atomic library function serves as a reminder that elsewhere, the variable is the target of concurrent data access.
Hopefully, it’s now a bit more clear why the World’s Simplest Lock-Free Hash Table uses Mintomic library functions to manipulate shared memory concurrently from different threads.
Learning Outcomes
- Define atomic and mass numbers.
- Determine the number of protons, neutrons, and electrons in an atom.
- Identify the charge and relative mass of subatomic particles.
- Label the location of subatomic particles in the atom.
- Determine the mass of an atom based on its subatomic particles.
- Write A/Z and symbol-mass format for an atom.
Atoms are the fundamental building blocks of all matter and are composed of protons, neutrons, and electrons. Because atoms are electrically neutral, the number of positively charged protons must be equal to the number of negatively charged electrons. Since neutrons do not affect the charge, the number of neutrons is not dependent on the number of protons and will vary even among atoms of the same element.
Atomic Number
The atomic number (represented by the letter Z)of an element is the number of protons in the nucleus of each atom of that element. An atom can be classified as a particular element based solely on its atomic number. For example, any atom with an atomic number of 8 (its nucleus contains 8 protons) is an oxygen atom, and any atom with a different number of protons would be a different element. The periodic table (see figure below) displays all of the known elements and is arranged in order of increasing atomic number. In this table, an element's atomic number is indicated above the elemental symbol. Hydrogen, at the upper left of the table, has an atomic number of 1. Every hydrogen atom has one proton in its nucleus. Next on the table is helium, whose atoms have two protons in the nucleus. Lithium atoms have three protons, beryllium atoms have four, and so on.
Since atoms are neutral, the number of electrons in an atom is equal to the number of protons. Hydrogen atoms all have one electron occupying the space outside of the nucleus. Helium, with two protons, will have two electrons. In the chemical classroom, the proton count will always be equivalent to an atom's atomic number. This value will not change unless the nucleus decays or is bombarded (nuclear physics).
Mass Number
Experimental data showed that the vast majority of the mass of an atom is concentrated in its nucleus, which is composed of protons and neutrons. The mass number (represented by the letter A)is defined as the total number of protons and neutrons in an atom. Consider the table below, which shows data from the first six elements of the periodic table.
Name | Symbol | Atomic Number (Z) | Protons | Neutrons | Electrons | Mass Number (A) (rounded to two decimals) |
---|---|---|---|---|---|---|
hydrogen | (ce{H}) | 1 | 1 | 0 | 1 | 1.01 |
helium | (ce{He}) | 2 | 2 | 2 | 2 | 4.00 |
lithium | (ce{Li}) | 3 | 3 | 4 | 3 | 6.94 |
beryllium | (ce{Be}) | 4 | 4 | 5 | 4 | 9.01 |
boron | (ce{B}) | 5 | 5 | 6 | 5 | 10.18 |
carbon | (ce{C}) | 6 | 6 | 6 | 6 | 12.01 |
Consider the element helium. Its atomic number is 2, so it has two protons in its nucleus. Its nucleus also contains two neutrons. Since (2 + 2 = 4), we know that the mass number of the helium atom is 4. Finally, the helium atom also contains two electrons, since the number of electrons must equal the number of protons. This example may lead you to believe that atoms have the same number of protons and neutrons, but a further examination of the table above will show that this is not the case. Lithium, for example, has three protons and four neutrons, giving it a mass number of 7.
Knowing the mass number and the atomic number of an atom allows you to determine the number of neutrons present in that atom by subtraction.
[text{Number of neutrons} = text{ rounded mass number} - text{atomic number}]
Atoms of the element chromium (left( ce{Cr} right)) have an atomic number of 24 and a mass number of 52. How many neutrons are in the nucleus of a chromium atom? To determine this, you would subtract as shown:
[52 - 24 = 28 : text{neutrons in a chromium atom}]
The composition of any atom can be illustrated with a shorthand notation called A/Z format. Both the atomic number and mass are written to the left of the chemical symbol. The 'A' value is written as a superscript while the 'Z' value is written as a subscript. For an example of this notation, look to the chromium atom shown below:
[ce{^{52}_{24}Cr}]
Another way to refer to a specific atom is to write the mass number of the atom after the name, separated by a hyphen. Symbol-mass format for the above atom would be written as Cr-52. In this notation, the atomic number is not included. You will need to refer to a periodic table for proton values.
Example (PageIndex{1})
Atomic Notation
Calculate each of the three subatomic particles and give specific group or period names for each atom.
- mercury
- platinum
- bromine
Solutions
- Hg (transition metal)- has 80 electrons, 80 protons, and 121 neutrons
- Pt (transition metal)- has 78 electrons, 78 protons, and 117 neutrons
- Br (halogen)- has 35 electrons, 35 protons, and 45 neutrons
Example (PageIndex{2})
Write both A/Z and symbol-mass formats for the atoms in Example (PageIndex{1}).
Solutions
- (ce{^{201}_{80}Hg}) and Hg-201
- (ce{^{195}_{78}Pt}) and Pt-195
- (ce{^{80}_{35}Br}) and Br-80
Example (PageIndex{3})
Identify the elements based on the statements below.
Israel Strikes Syria After Attack Near Secretive Nuclear Site
- Which element has 25 protons?
- Which element has 0 neutrons?
- Which element has 83 electrons?
Solutions
a. manganese
b. hydrogen
c. bismuth
Need More Practice?
- Turn to section 3.E of this OER and answer questions #1-#2, #4, and #8.
Contributors and Attributions
CK-12 Foundation by Sharon Bewick, Richard Parsons, Therese Forsythe, Shonna Robinson, and Jean Dupon.
Allison Soult, Ph.D. (Department of Chemistry, University of Kentucky)