24 Şubat 2020 Pazartesi

std::memory_order

Giriş
std::memory_order bir enum ve değerleri şöyle
enum memory_order {
  memory_order_relaxed,
  memory_order_consume,
  memory_order_acquire,
  memory_order_release,
  memory_order_acq_rel,
  memory_order_seq_cst
};
std::memory_order Neden Lazım*
Açıklaması şöyle.
In fact, atomics are always thread safe, regardless of the memory order! The memory order is not for the atomics -> it's for non atomic data.
Yani std::memory_order eğer sadece std::atomic kullanıyorsak gerekli değil. Ancak elimizde şöyle bir kod olsun
Node:
   Atomic<Node*> next_node;
   T non_atomic_data
Açıklaması şöyle. Yani kodun sıra değiştirebilmesi (Out of Order Execution) yüzünden std::memory_order gerekiyor.
Now, let's say I push a new node into the list. next_node is always thread safe, another thread will always see the latest atomic value. But who grantees that other threads see the correct value of non_atomic_data?

No-one.

Here is a perfect example of the usage of memory orders - we "piggyback" atomic stores and loads to next_node by also adding memory orders that synchronize the value of non_atomic_data.

So when we store a new node to the list, we use memory_order_release to "push" the non atomic data to the main memory. when we read the new node by reading next_node, we use memory_order_acquire and then we "pull" the non atomic data from the main memory. This way we assure that both next_node and non_atomic_data are always synchronized across threads.
std::memory_order Acquire ve Release yazısına bakabilirsiniz
std::memory_order Consume ve Release yazısına bakabilirsiniz

std::memory_order_relaxed
Açıklaması şöyle. Aynı thread içindeki satırlar yer değiştirebilir gibi düşünülebilir.
Atomic operations tagged memory_order_relaxed are not synchronization operations; they do not impose an order among concurrent memory accesses. They only guarantee atomicity and modification order consistency.
threadler arasında öncelik farkı yok, sadece işlemin atomik olması garanti edilir. Örneğin tüm thread'ler aynı değişkeni sadece artırıyorsa sıralama yapmaya gerek yoktur.

Örnek

Elimizde şöyle bir kod olsun
a.store(1, std::memory_order_relaxed);
b.store(2, std::memory_order_relaxed);
Açıklaması şöyle
In reality, a.store can happen after b.store. The CPU does this all the times, it's called Out of Order Execution and its one of the optimizations techniques CPUs use to speed up execution. a and b are still thread-safe, even though the thread-safe stores might happen in a reverse order.
Örnek
Şöyle yaparız. İlk thread y'yi okur x'e yaz işlemini yapar. İkinci thread x'i okur y'e 42 değerini yaz işlemini yapar.
// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
 y.store(42, std::memory_order_relaxed); // D
Bu kodun açıklaması şöyle. Çünkü ikinci thread içinde D ve C yer değiştirebilir. Bu durumda önce D işletilir y 42 olur. Daha sonra A ve işletilir ve r1 42 olur. Daha sonra B işletilir ve x 42 olur. En son C işletilir ve r2 42 olur.
is allowed to produce r1 == r2 == 42 because, although A is sequenced-before B within thread 1 and C is sequenced before D within thread 2, nothing prevents D from appearing before A in the modification order of y, and B from appearing before C in the modification order of x. The side-effect of D on y could be visible to the load A in thread 1 while the side effect of B on x could be visible to the load C in thread 2. In particular, this may occur if D is completed before C in thread 2, either due to compiler reordering or at runtime.
Açıklaması şöyle.
C is sequenced before D. But when viewed from this thread in isolation, nothing that C does affects the outcome of D. And nothing that D does would change the result of C. The only way one could affect the other would be as an indirect consequence of something happening in another thread. However, by specifying std::memory_order_relaxed, you explicitly stated that the order in which the load and store are observed by another thread is irrelevant. Since no other thread can observe the load and store in any particular order, there is nothing another thread could do to make C and D affect each other in a consistent manner. Thus, the order in which the load and store are actually performed is irrelevant. Thus, the compiler is free to reorder them. And, as mentioned in the explanation underneath that example, if the store from D is performed before the load from C, then r1 == r2 == 42 can indeed come about…
std::memory_order_consume - Okuma İçindir
Açıklaması şöyle
First of all, memory_order_consume is temporarily discouraged by the ISO C++ committee until they come up with something compilers can actually implement. For a few years now, compilers have treated consume as a synonym for acquire
std::memory_order_acquire - Okuma İçindir
Açıklaması şöyle. Kodda std::memory_order_acquire'dan sonra gelen bir satırın benim önüme geçmesini engeller. load işleminin gerçekleşmesi için kullanılır.
memory_order_acquire tells the compiler and CPU not to execute operations that happen after it code-wise, before it.
Açıklaması şöyle. std::memory_order_relaxed'e göre daha sıkıdır.
All atomic operations on the same object will always have a single order, and the order within a single thread is included in that. That is, relaxed cannot reorder atomic operations on the same object within the same thread.

The relaxed memory order is the order relative to other memory operations besides that specific atomic one. So relaxed is fine for checking for the wait itself, but when you want to actually read data which written by the releasing thread that isn't contained in the atomic value, you'll need a stronger memory order in order to ensure their visibility. So your outer loop should use a more appropriate condition for acquiring visibility, should it be necessary.
Örneğin okuma işleminde kullanılır. Açıklaması şöyle.
So when we store a new node to the list, we use memory_order_release to "push" the non atomic data to the main memory. when we read the new node by reading next_node, we use memory_order_acquire and then we "pull" the non atomic data from the main memory. This way we assure that both next_node and non_atomic_data are always synchronized across threads.
Örnek
Şöyle yaparız.
std::atomic<bool> x;

while (x.load(std::memory_order_acquire) == false)
{
  x.wait(false, std::memory_order_acquire);
}
std::memory_order_release - Yazma İçindir
Açıklaması şöyle. Kodda std::memory_order_release'den önce gelen bir satırın benim arkama geçmesini engeller. Böylece arkamdaki kod her zaman arkamda çalışır. store işleminin gerçekleşmesi için kullanılır. 
memory_order_release tells the compiler and CPU not to execute operations that before it code-wise, after it.
Örneğin yazma işleminde kullanılır. Açıklaması şöyle.
So when we store a new node to the list, we use memory_order_release to "push" the non atomic data to the main memory. when we read the new node by reading next_node, we use memory_order_acquire and then we "pull" the non atomic data from the main memory. This way we assure that both next_node and non_atomic_data are always synchronized across threads.
std:.memory_order_acq_rel 
Açıklama yaz

std::memory_order_seq_cst
Sequentially consisten anlamına gelir. İki thread aynı anda bir std::atomic_thread_fence<std::memory_order_cst>' e erişmeye çalışsa, birini diğerinin önüne geçirir. yani sıraya sokar. Böylece thread1 thread2'nin yaptığı işlemi görür.

Hiç yorum yok:

Yorum Gönder