멀티 스레딩 환경에서 데이터 일관성 유지하기 (feat. 동기화, 경쟁조건, 임계 영역)

이강용·2024년 2월 15일
0

CS

목록 보기
18/109

멀티 스레딩과 공유 자원의 중요성

멀티 스레딩은 현대 소프트웨어 개발에서 성능을 향상시키는 핵심 기술이다. 특히, 공유 자원에 대한 접근을 적절히 제어하는 것은 매우 중요한데, 잘못 관리될 경우 데이터 무결성 문제나 예상치 못한 결과가 발생할 수 있다. 귤 포장 공장에서 두 명의 작업자가 동시에 하나의 귤 박스에서 상한 귤을 선별하는 상황을 예로 들면, 두 작업자가 동시에 같은 귤을 선택할 때 누가 우선권을 가질지 결정하는 메커니즘이 필요하다. 이는 소프트웨어 개발에서도 공유 자원에 대한 접근을 제어하는 메커니즘의 필요성을 강조한다.

자바 코드 예시

package programmers;

import java.util.ArrayList;
import java.util.List;

class Orange{
	private boolean isRotten;
	
	public Orange(boolean isRotten) {
		this.isRotten = isRotten;
	}
	
	public boolean isRotten() {
		return isRotten;
	}
}

class OrangePicker implements Runnable{
	
	private List<Orange> oranges;
	
	public OrangePicker(List<Orange> oranges) {
		this.oranges = oranges;
	}
	
	
	@Override
	public void run() {
		synchronized (oranges) {
			oranges.removeIf(Orange::isRotten);
		}
		
	}
}

public class OrangeSortingProgram {
	public static void main(String[] args) {
		List<Orange> oranges = new ArrayList<>();
		
		for(int i = 0; i < 10; i++) {
			oranges.add(new Orange(Math.random() < 0.5));
		}
		
		Thread picker1 = new Thread(new OrangePicker(oranges));
		Thread picker2 = new Thread(new OrangePicker(oranges));
		
		picker1.start();
		picker2.start();
		
		try {
			picker1.join();
			picker2.join();
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("남은 귤의 수 : " + oranges.size());
	}

}

동기화, 경쟁 조건, 임계 영역의 이해

동기화(Synchronization) : 동기화는 공유 자원에 대한 접근을 안전하게 제어하는 방법. 자바에서는 synchronized 키워드를 사용하여 특정 코드 블록의 접근을 동시에 하나의 스레드로 제한한다.

@Override
public void run() {
    synchronized (oranges) {
        oranges.removeIf(Orange::isRotten);
    }
}

경쟁 조건(Race Condition) : 두 개 이상의 스레드가 동시에 공유 자원을 수정하려고 할 때 발생하는 상황이다. 이 상황에서는 스레드 간의 실행 순서에 따라 결과가 달라질 수 있다.

Thread picker1 = new Thread(new OrangePicker(oranges));
Thread picker2 = new Thread(new OrangePicker(oranges));

picker1.start();
picker2.start();

임계 영역(Critical Section) : 공유 자원에 대한 접근을 포함하는 코드 섹션으로, 동시에 여러 스레드에 의해 실행되면 안 되는 부분이다. 임계 영역은 적절한 동기화 메커니즘을 사용하여 보호되어야 한다.

synchronized (oranges) {
    oranges.removeIf(Orange::isRotten);
}

critical section problem의 해결책이 되기 위한 조건

  1. mutual exclusion (상호 배제)
    • 동시에 두 개 이상의 프로세스(또는 스레드)가 임계 영역에 들어갈 수 없다는 원칙
      👉🏻 이를 통해 공유 자원에 대한 동시 접근을 방지하여 데이터의 일관성을 유지한다.
  2. progress (진행)
    • 임계 영역에 들어가고자 하는 프로세스가 있을 때, 해당 프로세스가 무한히 대기하지 않고 결국에는 임계 영역에 들어갈 수 있어야 한다.
      👉🏻 즉, 임계 영역 접근 권한을 얻기 위한 결정은 무기한으로 지연되어서는 안된다.
  3. bounded waiting(한정된 대기)
    • 프로세스가 임계 영역에 들어가려고 요청한 후, 다른 프로세스들이 임계 영역에 들어가는 횟수에 한계가 있어야 한다.
      👉🏻 이는 모든 프로세스가 공정하게 임계 영역에 접근할 수 있도록 보장한다.

어떻게 동기화를 시킬 것인가?

동기화 문제를 해결하기 위해서는 공유 자원에 대한 접근을 엄격히 제어해야 한다. 이를 위해 자바에서는 synchronized 또는 ReentrantLock과 같은 동기화 메커니즘을 제공한다. 동기화를 통해 각 스레드가 공유 자원을 사용할 때 안전하게 접근하도록 하며, 경쟁 조건을 방지하고 임계 영역을 안전하게 관리 할 수 있다.

동기화 전략 (Spinlock, Mutex, Semaphore)

스핀락(Spinlock)

스핀락은 임계 영역에 접근할 수 있을 때까지, 프로세스나 스레드가 반복적으로 잠금 상태를 확인하는 방식
CPU 자원을 사용하면서 대기하기 때문에, 잠금을 얻기 위한 대기 시간이 매우 짧은 경우에 효율적이다.

import java.util.concurrent.atomic.AtomicBoolean;

class SpinLock {
    private final AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
        while (!lock.compareAndSet(false, true)) {
            // 현재 스레드는 CPU 자원을 사용하면서 잠금이 해제될 때까지 대기
        }
    }

    public void unlock() {
        lock.set(false);
    }
}

뮤텍스(Mutex)

뮤텍스는 상호 배제를 보장하기 위해 사용되며, 한 번에 하나의 스레드만 임계 영역에 접근할 수 있게 한다.
스레드가 임계 영역에 들어가기 위해서는 뮤텍스 잠금을 획득해야 하며, 작업이 끝나면 잠금을 해제해야 한다.

import java.util.concurrent.locks.ReentrantLock;

class MutexExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void criticalSection() {
        lock.lock();
        try {
            // 임계 영역 코드
        } finally {
            lock.unlock();
        }
    }
}

세마포어(Semaphore)

세마포어는 뮤텍스와 유사하지만, 동시에 여러 스레드가 임계 영역에 접근할 수 있도록 허용하는 카운트 기반 메커니즘이다.
세마포어는 임계 영역에 동시 접근할 수 있는 스레드의 최대 수를 제어할 수 있으며, 이를 통해 리소스의 동시 사용을 관리한다.

import java.util.concurrent.Semaphore;

class SemaphoreExample {
    private final Semaphore semaphore = new Semaphore(2); // 동시에 2개의 스레드만 접근 허용

    public void criticalSection() {
        try {
            semaphore.acquire();
            // 임계 영역 코드
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
        }
    }
}

멀티 스레딩 환경에서의 동기화 메커니즘 비교

특성스핀락뮤텍스세마포어
정의CPU 시간을 사용하면서 잠금 획득을 위해 반복적으로 상태를 확인한 번에 하나의 스레드만 임계 영역에 접근하도록 하는 잠금 메커니즘동시에 여러 스레드가 임계 영역에 접근할 수 있도록 하는 카운트 기반의 메커니즘
CPU 사용대기하는 동안 계속 CPU를 사용대기하는 스레드는 CPU 시간을 소비하지 않음대기하는 스레드는 CPU 시간을 소비하지 않음
접근 허용단일 스레드단일 스레드여러 스레드 (설정된 카운트에 따라)
적합한 상황대기 시간이 매우 짧은 경우장시간 대기가 필요할 경우동시에 여러 리소스 접근이 필요할 경우

Mutex와 2진(Binary) Semaphore는 같은 것인가?

Mutex(뮤텍스)와 2진(바이너리) 세마포어는 상호 배제를 보장하는 동기화 메커니즘으로서 비슷한 기능을 제공하지만, 둘 사이에는 중요한 차이점이 있다.

Mutex와 2진(Binary) Semaphore의 주요 특성과 차이점

특성Mutex2진(바이너리) 세마포어
정의한 번에 하나의 스레드만이 임계 영역에 접근할 수 있도록 하는 동기화 메커니즘입니다.세마포어의 값이 0 또는 1인 경우로, 기본적으로 Mutex와 유사하게 작동할 수 있습니다.
소유권소유권 개념을 가지고 있어, 잠금을 획득한 스레드만이 잠금을 해제할 수 있습니다.소유권 개념이 없어, 어떤 스레드든 세마포어를 획득하거나 해제할 수 있습니다.
Priority Inheritance우선순위 상속 기능을 지원할 수 있어, 우선순위 역전 문제를 해결하는 데 도움을 줍니다.우선순위 상속과 같은 고급 기능을 직접 지원하지 않습니다.

profile
HW + SW = 1

0개의 댓글