Sobes.tech
Middle+

Jak rozwiązać problem wyścigu (race condition)?

sobes.tech AI

Odpowiedź od AI

Używanie synchronizacji wątków do dostępu do zasobów współdzielonych.

Główne podejścia:

  • Mutexy (Mutual Exclusion): Blokują dostęp do zasobu dla innych wątków, gdy jeden z nich go używa.
    • NSLock
    • os_unfair_lock
    • pthread_mutex_t
import Foundation

let lock = NSLock()
var sharedResource = 0

func modifyResource() {
    lock.lock() // Zdobycie mutexa
    // Sekcja krytyczna - bezpieczny dostęp do sharedResource
    sharedResource += 1
    print("Wartość zasobu: \(sharedResource)")
    lock.unlock() // Zwolnienie mutexa
}

// Przykład wywołania z różnych wątków
// DispatchQueue.global().async { modifyResource() }
// DispatchQueue.global().async { modifyResource() }
  • Kolejki seryjne: Wykonują zadania jedno po drugim, gwarantując, że tylko jeden blok kodu ma dostęp do zasobu w danym czasie.
    • DispatchQueue.main
    • DispatchQueue.global() z atrybutem .serial
import Foundation

let serialQueue = DispatchQueue(label: "com.example.serialQueue")
var sharedResource = 0

func modifyResourceAsync() {
    serialQueue.async {
        // Kod wykonywany kolejno w tej kolejce
        sharedResource += 1
        print("Wartość zasobu: \(sharedResource)")
    }
}

// Przykład wywołań z różnych wątków/kolejek
// modifyResourceAsync()
// modifyResourceAsync()
  • Blokady czytelnik-pisarz: Pozwalają wielu wątkom czytać zasób jednocześnie, ale tylko jednemu pisać.
    • DispatchQueue z barierami (.barrier) dla zapisu i synchronicznym/asynchronicznym dostępem do odczytu.
import Foundation

let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
var sharedResource = 0

func readResource() {
    concurrentQueue.async { // Odczyt - wielu czytelników jednocześnie
        print("Odczyt zasobu: \(sharedResource)")
    }
}

func writeResource(newValue: Int) {
    concurrentQueue.sync(flags: .barrier) { // Zapis - tylko jeden pisarz i nikt nie czyta/nie pisze
        sharedResource = newValue
        print("Zapis zasobu: \(sharedResource)")
    }
}

// Przykład wywołań
// readResource()
// readResource()
// writeResource(newValue: 10)
// readResource()
  • Operacje atomowe: Operacje gwarantujące wykonanie w całości bez przerwania przez inny wątek. Używane dla prostych typów danych (liczniki, flagi).
    • OSAtomicIncrement, OSAtomicDecrement (Przestarzałe, ale koncepcja nadal aktualna)
    • <atomic> w C++11
#include <atomic>
#include <iostream>

std::atomic<int> atomicCounter(0);

void incrementCounter() {
    atomicCounter++; // Atomowe zwiększenie
    std::cout << "Licznik: " << atomicCounter << std::endl;
}

// Przykład w kontekście iOS z C++ (użycie z Objective-C++)
// Wywołać z różnych wątków
  • Typy wartości: Przekazywanie kopii struktur/enumeracji zamiast referencji do klas może pomóc uniknąć problemów z wyścigiem danych, jeśli dane nie są bezpośrednio współdzielone między wątkami w zmiennym stanie.

Wybór metody zależy od specyfiki zadania i wymaganego poziomu równoległości. Dla większości zadań w iOS preferowane są DispatchQueue (seryjne i równoległe z barierami) ze względu na łatwość użycia i integrację z Grand Central Dispatch (GCD).