프로세스
•
실행중인 프로그램
•
다른 프로세스와 메모리공간을 공유하지 않는다.
•
내부에 최소 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() 방식을 사용하면 실패한 작업을 재시도 하여 성공할 때까지 반복하게 된다.