Heim  >  Artikel  >  Java  >  Java-Sperren-Parallelität, sperrenfreie Parallelität und CAS-Beispielanalyse

Java-Sperren-Parallelität, sperrenfreie Parallelität und CAS-Beispielanalyse

WBOY
WBOYnach vorne
2023-05-23 13:34:341405Durchsuche

Gesperrte Parallelität

Für die meisten Programmierer (natürlich bin ich im Grunde einer von ihnen) ist gleichzeitiges Programmieren fast gleichbedeutend mit dem Hinzufügen einer Sperre (Mutex) zur relevanten Datenstruktur. Wenn wir beispielsweise einen Stapel benötigen, der Parallelität unterstützt, ist es am einfachsten, einem Single-Threaded-Stack eine Sperre hinzuzufügen std::sync::Mutex . (Arc wird hinzugefügt, um mehreren Threads den Besitz des Stapels zu ermöglichen) std::sync::Mutex  。(加上Arc是为了能让多个线程都拥有栈的所有权)

<code>
   
   
  <p>use std::sync::{Mutex, Arc};<br><br>#[derive(Clone)]<br>struct ConcurrentStack<T> {<br>    inner: Arc<Mutex<Vec<T>>>,<br>}<br><br>impl<T> ConcurrentStack<T> {<br>    pub fn new() -> Self {<br>        ConcurrentStack {<br>            inner: Arc::new(Mutex::new(Vec::new())),<br>        }<br>    }<br><br>    pub fn push(&self, data: T) {<br>        let mut inner = self.inner.lock().unwrap();<br>        (*inner).push(data);<br>    }<br><br>    pub fn pop(&self) -> Option<T> {<br>        let mut inner = self.inner.lock().unwrap();<br>        (*inner).pop()<br>    }<br><br>}<br>
  
    

   
   
  </p></code>

代码写起来十分方便,因为它几乎与单线程版本相同,这很明显是一个好处。只需要按照单线程的版本写完,然后给数据结构加上锁,然后在必要的时候获取和释放(在Rust中基本上是自动的)锁即可。

那么问题是什么呢?首先不谈你可能会忘记获取和释放锁(这一点要感谢Rust,在Rust中几乎不可能发生),你可能会面临死锁的问题(哲学家就餐问题)。然后也不谈一些低优先级的任务可能会长期抢占高优先级任务的资源(因为锁是第一位的),当线程数量比较大的时候,大部分的时间都被用在了同步上(等待锁能被获取),性能就会变得非常差。考虑一个大量读取而偶尔写入的并发数据库,如果用锁去处理,即使数据库没有任何更新,每两个读之间也需要做一次同步,代价太大!

无锁并发

于是,大量计算机科学家和程序员就把目光转向了无锁(lock-free)并发。无锁对象:如果一个共享对象保证了无论其他线程做何种操作,总有一些线程会在有限的系统操作步骤后完成一个对其的操作  Her91  。也就是说,至少有一个线程对其操作会取得成效。使用锁的并发明显就不属于这一范畴:如果获取了锁的线程被延迟,那么这段时间里没有任何线程能够完成任何操作。极端情况下如果出现了死锁,那么没有任何线程能够完成任何操作。

CAS(compare and swap)原语

那大家可能会好奇,无锁并发要怎么实现呢?有没有例子呢?在此之前让我们先看一下一个公认的在无锁并发中非常重要的原子原语:CAS。CAS的过程是用指定值去比较一储存值,只有当他们相同时,才会修改储存值为新的指定值。CAS是个原子操作(由处理器支持,比如x86的compare and exchange (CMPXCHG)),该原子性保证了如果其他线程已经改变了储存值,那么写入就会失败。Rust标准库中的  std::sync::atomic  中的类型就提供了CAS操作,比如原子指针  std::sync::atomic::AtomicPtr

<code>
   
   
  <p>pub fn compare_and_swap(<br>    &self,<br>    current: *mut T,<br>    new: *mut T,<br>    order: Ordering<br>) -> *mut T<br>
  
    

   
   
  </p></code>

(在这里,不用纠结ordering是什么,也就是说请尽管忽略忽略  Acquire  ,   Release  ,   Relaxed  )

无锁栈(天真版)

<code>
   
   
  <p>#![feature(box_raw)]<br><br>use std::ptr::{self, null_mut};<br>use std::sync::atomic::AtomicPtr;<br>use std::sync::atomic::Ordering::{Relaxed, Release, Acquire};<br><br>pub struct Stack<T> {<br>    head: AtomicPtr<Node<T>>,<br>}<br><br>struct Node<T> {<br>    data: T,<br>    next: *mut Node<T>,<br>}<br><br>impl<T> Stack<T> {<br>    pub fn new() -> Stack<T> {<br>        Stack {<br>            head: AtomicPtr::new(null_mut()),<br>        }<br>    }<br><br>    pub fn pop(&self) -> Option<T> {<br>        loop {<br>            // 快照<br>            let head = self.head.load(Acquire);<br><br>            // 如果栈为空<br>            if head == null_mut() {<br>                return None<br>            } else {<br>                let next = unsafe { (*head).next };<br><br>                // 如果现状较快照并没有发生改变<br>                if self.head.compare_and_swap(head, next, Release) == head {<br><br>                    // 读取内容并返回<br>                    return Some(unsafe { ptr::read(&(*head).data) })<br>                }<br>            }<br>        }<br>    }<br><br>    pub fn push(&self, t: T) {<br>        // 创建node并转化为*mut指针<br>        let n = Box::into_raw(Box::new(Node {<br>            data: t,<br>            next: null_mut(),<br>        }));<br>        loop {<br>            // 快照<br>            let head = self.head.load(Relaxed);<br><br>            // 基于快照更新node<br>            unsafe { (*n).next = head; }<br><br>            // 如果在此期间,快照仍然没有过时<br>            if self.head.compare_and_swap(head, n, Release) == head {<br>                break<br>            }<br>        }<br>    }<br>}<br>
  
    

   
   
  </p></code>

我们可以看到,无论是pop还是push思路都是一样的:先在快照上pop或者是push,然后尝试用CAS替换原来的数据。如果快照和数据相等,就意味着在此期间没有执行写操作,因此更新就能成功。如果数据不一致,则表明在此期间有其他线程对其进行了修改,需要重新开始。这就是一个无锁的栈。似乎一切都已经大功告成了!

内存释放

如果你正在使用Java或其他带有GC的编程语言,你可能已经完成了大量的工作。现在的问题在于,在Rust这种没有GC的语言中,pop中

return Some(unsafe { ptr::read(&(*head).data) })

并没有人去释放  headrrreee

Der Code ist sehr bequem zu schreiben, da er fast mit der Single-Thread-Version identisch ist, was offensichtlich ein Vorteil ist. Schreiben Sie einfach die Single-Threaded-Version, fügen Sie eine Sperre zur Datenstruktur hinzu und erwerben Sie dann die Sperre (im Grunde genommen automatisch in Rust) und geben Sie sie bei Bedarf frei. 🎜🎜Was ist also das Problem? Abgesehen von der Tatsache, dass Sie möglicherweise vergessen, die Sperre zu erwerben und freizugeben (was in Rust dank Rust fast unmöglich ist), können Sie zunächst mit dem Problem eines Deadlocks (dem Dining Philosopher Problem) konfrontiert werden. Ganz zu schweigen davon, dass einige Aufgaben mit niedriger Priorität möglicherweise lange Zeit die Ressourcen von Aufgaben mit hoher Priorität beanspruchen (da Sperren zuerst kommen). Wenn die Anzahl der Threads relativ groß ist, wird die meiste Zeit für die Synchronisierung aufgewendet (Warten auf). die zu erwerbende Sperre), wird die Leistung sehr schlecht. Stellen Sie sich eine gleichzeitige Datenbank mit einer großen Anzahl von Lesevorgängen und gelegentlichen Schreibvorgängen vor. Wenn zur Verarbeitung Sperren verwendet werden, ist eine Synchronisierung zwischen jeweils zwei Lesevorgängen erforderlich, was zu kostspielig ist. 🎜🎜🎜Sperrenfreie Parallelität🎜🎜🎜Daher richteten viele Informatiker und Programmierer ihre Aufmerksamkeit auf die sperrenfreie Parallelität. Sperrfreies Objekt: Wenn ein gemeinsam genutztes Objekt garantiert, dass unabhängig davon, was andere Threads tun, nach einer begrenzten Anzahl von Systemoperationsschritten immer ein Thread eine Operation darauf abschließt. Her91 . Mit anderen Worten: Mindestens ein Thread erzielt Ergebnisse aus seiner Operation. Parallelität mit Sperren fällt offensichtlich nicht in diese Kategorie: Wenn der Thread, der die Sperre erhalten hat, verzögert ist, kann in dieser Zeit kein Thread irgendeinen Vorgang abschließen. Im Extremfall, wenn ein Deadlock auftritt, kann kein Thread einen Vorgang abschließen. 🎜

🎜CAS (vergleichen und austauschen) primitiv🎜

🎜Dann sind Sie vielleicht neugierig, wie man eine sperrenfreie Parallelität erreicht? Gibt es Beispiele? Werfen wir zuvor einen Blick auf ein atomares Grundelement, das für die sperrenfreie Parallelität als sehr wichtig gilt: CAS. Der Prozess von CAS besteht darin, einen gespeicherten Wert mit einem angegebenen Wert zu vergleichen. Nur wenn sie gleich sind, wird der gespeicherte Wert auf den neuen angegebenen Wert geändert. CAS ist eine atomare Operation (die vom Prozessor unterstützt wird, z. B. x86s Compare and Exchange (CMPXCHG)). Diese Atomizität garantiert, dass der Schreibvorgang fehlschlägt, wenn andere Threads den gespeicherten Wert geändert haben. in der Rust-Standardbibliothek std::sync::atomic Die Typen in stellen CAS-Operationen bereit, z. B. atomare Zeiger std::sync::atomic::AtomicPtr 🎜rrreee🎜(Machen Sie sich hier keine Gedanken darüber, was Bestellung ist, das heißt, bitte ignorieren Sie es einfach. Erhalten , Freigabe , Entspannt )🎜

🎜Sperrfreier Stapel (naive Version)🎜

rrreee🎜Wir können sehen, dass die Idee dieselbe ist, egal ob Pop oder Push: Pop oder Push zuerst auf dem Snapshot und dann versuchen, ihn zu ersetzen es mit CAS-Originaldaten. Wenn der Snapshot und die Daten gleich sind, bedeutet dies, dass in diesem Zeitraum keine Schreibvorgänge durchgeführt wurden, sodass die Aktualisierung erfolgreich ist. Wenn die Daten inkonsistent sind, bedeutet dies, dass sie in diesem Zeitraum von anderen Threads geändert wurden und erneut gestartet werden müssen. Dies ist ein sperrenfreier Stapel. Es scheint, dass alles erledigt ist! 🎜

🎜Memory Release🎜

🎜Wenn Sie Java oder andere Programmiersprachen mit GC verwenden, haben Sie möglicherweise viel Arbeit geleistet. Das Problem ist nun, dass in einer Sprache wie Rust ohne GC niemand den Pop 🎜rrreee🎜 veröffentlicht Kopf , das ist ein Speicherleck! Hey, es scheint, dass sperrenfreie Parallelität nicht einfach ist. 🎜

Das obige ist der detaillierte Inhalt vonJava-Sperren-Parallelität, sperrenfreie Parallelität und CAS-Beispielanalyse. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen