스레드(Thread), 멀티스레드(Multi-Thread)!

H_dev·2022년 4월 27일
1

😁 오늘은 스레드(Thread)에 대해 정리한다.

스레드와 멀티스레드에 대해 알아보고 코드까지 작성해보도록 하자

프로세스

프로세스?
스레드를 알려면 먼저 프로세스의 개념부터 알아야한다.
프로세스는 현재 실행되고 있는 프로그램을 의미한다.
더 붙여 설명하면 메모리에 올라가서 실행되는 상태이다.
운영체제 상 프로세스에는 4가지 메모리 영역이 존재한다.

  • 명령어, 실제 코드가 할당되는 code 영역
  • 정적(static), 전역변수가 할당되는 data 영역
  • 런타임에 데이터가 동적으로 할당되는 heap 영역
  • 함수 호출정보, 지역변수, 매개변수가 할당되는 stack 영역
    프로세스마다 4가지 영역을 할당받기 때문에 서로 공유하지 않는다.

    참고 블로그

스레드

그렇다면 스레드는 무엇일까!
스레드는 위와 같은 프로세스 안에서 독립적으로 실행되는 흐름 단위이다.
이런 단위가 여러개 동시에 실행된다면?? 그것이 바로 멀티스레드이다. 이 다음에 마저 알아보도록 하자
스레드에도 메모리영역이 존재한다.
하지만 프로세스와는 달리 각 스레드별로 함수정보가 할당되는 stack 영역을 따로 할당받고 나머지 code, data, heap 영역은 프로세스의 자원을 스레드끼리 공유한다.

참고 블로그


멀티스레드

이제 멀티스레드를 살펴볼텐데, 당연히 여러개 실행해서 연산하게 된다면 빠르고 효율적이라고 생각된다. 하지만!! 꼭 그런것만은 아니라는 점을 유의하자.

일반적으로 하나의 프로세스는 하나의 스레드를 가지고 작업을 수행하게 되는데, 멀티스레드란 하나의 프로세스 내에서 여러개의 스레드가 동시에 작성을 수행한다는 것을 의미한다. 프로세스의 메모리를 공유하기 때문에 자원의 낭비가 적고 각각 다른 작업을 진행시킬 수 있어서 이점이 된다.

일반 스레드가 가지고 있는 특징도 멀티스레드의 관점에서 기록한다.

특징

  • 문맥 교환 (context switching)
    문맥교환이란 현재 작업상태나 다음에 필요한 데이터를 저장하고 읽어오는 것이다.
    이 문맥교환이 많아지면 시간소요가 많아지고 당연히 멀티스레드의 효율은 저하된다.
    ex) cpu 코어 수보다 많은 스레드 실행 시
    위에 말했던 것처럼 많은 수의 스레드가 무조건적으로 좋은게 아니다.
    오히려 싱글스레드로 처리하는게 빠른 경우도 있다.

즉! 싱글스레드와 멀티스레드의 큰 차이점이라고 할 수 있는 순차실행병렬실행, 둘 중 어떻게 처리하는게 좋을지 비용을 따져봐야 한다.


장점

  • 응답성
    싱글스레드는 프로그램 일부분이 중단되거나 에러가 발생하면 프로그램이 멈추지만,
    멀티스레드는 수행이 계속돼서 사용자에 대한 응답성이 증가한다.
    ex) 유튜브 영상 시청 도중에 좋아요 구독 눌러도 각각 상호작용 가능

  • 경제성
    위에서 설명했듯이 프로세스 내 메모리를 공유하기 때문에 자원, 공간이 절약된다.

단점

  • 싱글코어나, 적은 수의 cpu 코어에서의 멀티스레드는 문맥교환, 동기화 때문에 오히려 싱글스레드보다 느릴 수 있다.

  • 자원을 공유하는데, 다른 스레드에서 사용중인 자원에 동시에 접근해 수정하거나 읽어와서 잘못된 값을 얻을수 있다. -> 동기화(synchronized) 사용 이유


예제코드

멀티스레드 환경에서 한 파일(자원)에 동시 접근해 기록하는 예제를 작성해봤다.
멀티스레드의 특징과 장단점을 보여줄 수 있을거라 생각해 은행에 동시 다발적으로 입출금하는 예제를 응용했다.

Bank.java

public class Bank {

	long defaultMoney = 100;
	
	public void deposit(long money) { // 계산 후 잔액반환
		defaultMoney += money;
	}
	
	public void withDraw(long money) { // 계산 후 잔액반환
		defaultMoney -= money;
	}
	
	public String getMsg() {
		return "현재잔액 : " + defaultMoney;
	}
	
}
  • 예제에서 사용할 은행 클래스이다. 입출금, 잔액조회 -> defaultMoney 변수를 스레드가 공유할 것이다.
  • 기본 잔액은 100원이다.
  • 입금액을 받아 입금하는 deposit()
  • 출금액을 받아 출금하는 withDraw()
  • 잔액을 조회하는 getMsg()

BankDepositThread.java

public class BankDepositThread extends Thread{
	
	Bank bank;
	FileOutputStream output;
	
	
	public BankDepositThread(Bank bank, FileOutputStream output) {
		this.bank = bank;
		this.output = output;
	}

	@Override
	public void run() {
		for (int i = 0; i < 500; i++) {
			bank.deposit(1000);
		}
		try {
			output.write((bank.getMsg()+'\n').getBytes());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
  • Thread 클래스를 상속받은 입금쓰레드 클래스이다.
  • 은행정보(잔액 등)와 출력할 파일 객체는 Main에서 생성자로 전달받는다.
  • run()을 오버라이딩해서 for문을 통해 1000원을 500번 입금한다.
  • 그 후 출력파일(output)에 잔액조회를 하여 기록하는 간단한 스레드이다.

BankWithDrawThread.java

public class BankWithDrawThread extends Thread{
	
	Bank bank;
	FileOutputStream output;
	
	
	public BankWithDrawThread(Bank bank, FileOutputStream output) {
		this.bank = bank;
		this.output = output;
	}	
	
	
	@Override
	public void run() {
		for (int i = 0; i < 500; i++) {
			bank.withDraw(1000);
		}
		try {
			output.write((bank.getMsg()+'\n').getBytes());
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
  • 위의 입금스레드와 동일한 흐름 -> 차이점은 출금을 한다는 점

Main.java

public class Main {
	
	public static void main(String[] args) throws IOException, InterruptedException {
		Bank bank = new Bank(); // 은행정보 생성  기본금 100원 존재
		FileOutputStream output = null;
		Thread deposit = null;
		Thread withDraw = null;
		
		try {
			output = new FileOutputStream("C:/workspace/output.txt");
			
			for (int i = 0; i < 10; i++) {
				deposit = new BankDepositThread(bank, output); // 은행정보와 출력할 파일객체 스레드에 전달
				withDraw = new BankWithDrawThread(bank, output); // 은행정보와 출력할 파일객체 스레드에 전달
				deposit.start();
				withDraw.start();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}	
	}
}
  • 마지막으로 Main 문이다
  • 은행정보를 생성한다. (디폴트 잔액 100원)
  • 출력파일을 생성한다.
  • 각각의 입금, 출금 스레드에서는 1000원씩 500번을 입금하고 출금한다.
  • 더 많은 연산을 위해 메인에서 10번정도 스레드를 더 돌려준다.

💻 결과

결과는 과연 어떨까? 원래대로라면 똑같은 금액을 500번씩 입금하고 출금했기 때문에 디폴트 잔액인 100원이 나와야 정상이다.
결과를 바로 확인해보자.

결과가 이상하다;; 마지막 연산을 진행하고 100원이 찍혀야하는데 엉뚱한 금액이 찍혀있다.

바로 여기서! 멀티스레드의 단점이 보인다. 연산을 마구잡이로 하고 기록해버려서 이상한 값이 찍히는 점이다.

스레드가 엉뚱한 값에 연산을하지않고 겹쳐지지않게 기록하려면 동기화 메서드를 사용해야한다.
스레드 클래스들의 run()을 다음과 같이 수정하자

	@Override
	public void run() {

		try {
			synchronized (bank) {
				for (int i = 0; i < 500; i++) {
					bank.deposit(1000);
				}
				output.write((bank.getMsg()+'\n').getBytes());
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
  • synchronaized 블록을 넣어줬다. bank에 대해 동기화하여 사용한다는 의미이다.
  • 이렇게 하면 한 스레드가 연산을 하고 기록을 완료하는 동안 다른 스레드가 접근할 수 없다.

💻 결과 2

  • 스레드가 순서없이 연산을 하고 기록한 것이 보이지만 결과로 기대값인 100원이 정상적으로 출력됐다.

📝 마침

그리고 자바에서 스레드를 사용할 때 다른 한가지 방법이 더 있다.
이번에는 Thread를 상속받아 사용했지만, 다른 클래스를 이미 상속받고 있는 상태라면
Runnable 인터페이스를 implements 해서 run()을 구현하면 된다.
상황에 맞게 입맛대로 쓸수 있기를 바라면서 더 공부해야겠다.

profile
성장 개발일지

0개의 댓글