Search

멀티스레드의 경쟁상태

날짜
2025/02/01
태그
공부
스터디
프로젝트
SSAFY
공개여부

프로세스

실행중인 프로그램
다른 프로세스와 메모리공간을 공유하지 않는다.
내부에 최소 1개의 스레드를 가지고 있다.

멀티 프로세스

하나의 프로그램을 여러 개의 프로세스로 구성하여 각 프로세스가 병렬적으로 작업을 처리
하나의 프로세스가 정지되어도 다른 프로세스에 영향이 없는 장점
작업량이 많을 수록 성능이 저하된다.(context switching)

스레드

프로세스 안에서 실행되는 여러 흐름 단위
프로세스 내에서 메모리(Stack)영역만 할당받고 나머지 메모리(Heap, Code, Data)영역은 다른 스레드와 공유하여 사용한다.

멀티 스레드

하나의 프로그램을 여러 개의 스레드로 구성하여 각 스레드가 하나의 작업을 처리하도록 하는 것
공유 메모리를 사용하여 시간과 자원 손실 감소되는 장점
하나의 스레드가 공유 메모리의 데이터를 손상시키면 전체로 영향이 가는 단점

멀티 프로세스와 멀티 스레드 선택

일반적으로 프로그램을 돌릴 때 멀티 스레드로 프로그램을 돌리는 것이 유리하다.
스레드는 프로세스보다 가볍고 빠르다. 스레드는 프로세스와 달리 code, data, stack영역을 제외한 나머지 자원을 서로 공유한다. 그리고 프로세스 간 통신(IPC)를 사용하지 않고도 데이터를 공유할 수 있기 때문에 자원을 효율적으로 사용할 수 있는 장점이 있다.
하지만 멀티 프로세스의 장점이 더 중요한 상황에서는 멀티 프로세싱을 사용하는 경우가 좋다. 프로그램 내부에 여러 작업들을 실행 할 때 각 작업들이 다른 작업들에 영향을 미치지 않아야 할 경우 멀티 프로세스를 사용한다. 또한 분산 서비스를 위해 독립적인 프로세스를 여러개 두어 다른 프로세스에 영향 없이 수정, 확장하는 경우도 있다.
멀티 프로세스와 멀티 스레드 둘다 문맥교환으로 인한 자원 손실이 발생하게 된다. 멀티 스레드가 공유하는 자원을 제외한 스레드 정보만 교체하면 되므로 멀티 프로세스보다 더 효율적인 것은 맞다. 하지만 스레드를 많이 쓰는 것은 좋은 프로그래밍은 아니다. 스레드가 많아지면 문맥교환에 사용되는 자원의 양이 늘어나 항상 좋지는 않다.

멀티 스레드 경쟁상태 예시

ICoffeeService

public interface ICoffeeService { public int sellCoffee(); public int getAccount(); }
Java
복사

CoffeeService

@Service public class CoffeeService implements ICoffeeService { private int account = 0; public int sellCoffee() { // 의도적으로 지연 추가 try { Thread.sleep(1); // 지연 시간 추가 (경쟁 상태 유도) } catch (InterruptedException e) { e.printStackTrace(); } account += 5_000; return account; } public int getAccount(){ return account; } }
Java
복사

CoffeeServiceTest > 멀티스레드_경쟁_테스트

customer 손님(테스트 개수) 100개
cashier 계산원(스레드 개수) 10개
latch 여러개의 스레드가 전부 종료 될 때까지 기다려주는 역할
@SpringBootTest class CoffeeServiceTest { @Autowired private ICoffeeService coffeeService; @RepeatedTest(10) public void 멀티스레드_경쟁_테스트() throws Exception { //given int customer = 100; ExecutorService cashier = Executors.newFixedThreadPool(10); CountDownLatch latch = new CountDownLatch(customer); coffeeService = new CoffeeService(); // when for (int i = 0; i < customer; i++) { cashier.execute(() -> { coffeeService.sellCoffee(); latch.countDown(); }); } latch.await(); cashier.shutdown(); // then int expectedAccount = customer * 5_000; int actualAccount = coffeeService.getAccount(); System.out.println("Expected Account: " + expectedAccount); System.out.println("Actual Account: " + actualAccount); assertNotEquals(expectedAccount, actualAccount); } }
Java
복사

경쟁상태 테스트 결과

계좌에 500,000이 들어가야 하는데 실제로는 더 적은 값이 들어갔음을 확인
assertNotEquals 으로 서로 값이 다른 경우 테스트 통과
일정 확률로 예상 계좌 값과 실제 계좌 값이 같은 경우도 나올 수 있음(운좋게 경쟁상태 발생하지 않음)

AtomicCoffeeService

CAS(compare and swap) 방식으로 동시성
Lock 없이 비동기적 연산 가능
public class AtomicCoffeeService implements ICoffeeService{ private final AtomicInteger account = new AtomicInteger(0); @Override public int sellCoffee() { return account.addAndGet(5_000); } @Override public int getAccount() { return account.get(); }
Java
복사
CAS(compare and swap)
스레드 2개가 공유자원의 값이 200,000 일 때 값을 수정 시도한 경우이다.
값을 변경할 때 기존 값을 복사하고 계산을 한 다음 기존 값이랑 바꾸는 작업을 진행한다.
스레드가 동시에 공유자원의 값을 읽고 바꾸려고 했을 때 CAS 방식에 의해 동시성이 보장되지 않는 경우 작업을 실패처리한다.
.addAndGet() 방식을 사용하면 실패한 작업을 재시도 하여 성공할 때까지 반복하게 된다.