GW LABS

멀티스레드 프로그래밍 (1) - 스레드 사용법 본문

Programming

멀티스레드 프로그래밍 (1) - 스레드 사용법

GeonWoo Kim 2021. 3. 5. 11:29

멀티스레드 프로그래밍

웹 개발 업무에서 통상적인 비즈니스 로직만 다루다보면 사실상 멀티스레드, 멀티프로세스를 통한 성능향상을 경험해볼 기회가 많지 않다. 그러나 웹 개발의 근간이 되는 정적 웹 서버 혹은 웹 어플리케이션 서버들만 해도 멀티 프로세스 및 멀티 스레드를 통해서 사용자의 요청을 효율적으로 처리하고 있다. 다뤄야 할 데이터의 크기가 GB, PB 급의 크기라면 싱글 스레드 혹은 싱글 프로세스로는 고객을 만족시킬 수 없다! 이번 멀티스레드, 멀티프로세스 포스팅 시리즈를 통해서 이론을 실제로 코드로 구현해보고 컴퓨터한테 어떻게 멀티태스킹을 효율적으로 시킬 수 있을지 알아보려고 한다. 


 

프로세스 vs 스레드

먼저 프로세스와 스레드에 대해 간단히 복습해보자. 프로세스는 실행을 위해 시스템에 등록된 작업을 의미한다. 따라서 각각의 프로세스들은 독립적인 메모리 공간을 갖게 되고 서로 공유할 수 없다. 스레드는 프로세스 내에서 실행되는 여러 흐름 단위이다. 프로세스 내부에서의 최소 제어단위이기 때문에 각 스레드들은 프로세스 내부에서 힙영역의 메모리를 공유한다. 

프로세스와 스레드에는 각각의 장단점이 있다. 프로세스의 경우 하나의 프로세스가 이상이 생겨도 다른 프로세스에는 영향이 가지 않는다는 장점이 있는 반면에, 운영체제에서 프로세스의 실생순서를 조정할 때 컨텍스트 스위칭이 발생하며 이 과정에서 성능손실이 발생한다. 스레드의 경우에는 컨텍스트 스위칭이 프로세스보다 적다는 장점이 있으나 공유자원에 대한 처리를 적절히 하지 않으면 프로그램에 이상이 생길 수 있다는 단점이 있다.

 

 

기초적인 스레드 사용법

1. C++ 스레드

#include <iostream>
#include <thread>

using namespace std;

void counting1() {
    for (int idx = 0; idx < 3; ++idx) {
        cout << "쓰레드 1 ==> " << idx << endl;
    }
}

void counting2() {
    for (int idx = 0; idx < 3; ++idx) {
        cout << "쓰레드 2 ==> " << idx << endl;
    }
}

void counting3() {
    for (int idx = 0; idx < 3; ++idx) {
        cout << "쓰레드 3 ==> " << idx << endl;
    }
}

int main() {

    thread t1(counting1);
    thread t2(counting2);
    thread t3(counting3);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

C++의 경우에는 Thread 헤더를 임포트하여 쉽게 스레드를 사용할 수 있다. 스레드 객체에 실행하고자 하는 함수를 인자로 넘겨주고 객체를 초기화한 후에 join 메소드를 통해서 생성한 스레드에 작업을 할당해줄 수 있다. 리눅스에서 컴파일 시에 옵션으로 -lpthread 옵션을 줘야한다. 1~3까지 반복으로 세는 함수를 쓰레드 3개에서 실행하면 아래와 같은 출력을 볼 수 있다.

 

C++ 출력예제

 

2. Java 스레드

package parallel;

class Counting implements Runnable{

	private int number;
	
	Counting(int number) {
		this.number = number;
	}
	
	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println("쓰레드 " + Integer.toString(this.number) + " ==> " + Integer.toString(i));
		}
	}
}

public class Multithread {
	
	public static void main(String[] args) throws InterruptedException {
		Counting c1 = new Counting(1);
		Counting c2 = new Counting(2);
		Counting c3 = new Counting(3);
		
		Thread t1 = new Thread(c1, "1");
		Thread t2 = new Thread(c2, "2");
		Thread t3 = new Thread(c3, "3");

		t1.start();
		t2.start();
		t3.start();
		
		t1.join();
		t2.join();
		t3.join();
	}
}

 

자바에서는 스레드를 구현하는 방법이 두 가지 있다. 하나는 Thread 클래스를 상속하는 방법이고 다른 하나는 Runnable 인터페이스를 구현하는 방법이다. 위에서는 좀 더 범용적으로 사용되는 Runnable 인터페이스를 상속해서 구현했다. 위의 C++의 코드와 큰 차이는 없다.

 

Java 출력예제

 

공유자원에 대한 문제

스레드는 힙 영역의 메모리를 공유하기 때문에 동시에 공유자원에 접근했을 경우 의도치 않은 결과를 얻게 될 수 있다. 이런 문제를 이해하기 위해 상황을 설정해서 예제 코드를 만들어보려고 한다.

여러분이 은행에 갔다고 생각해보자. 그런데 이 은행은 매우 작은 은행이여서 은행원이 한 명밖에 없다! 번호표도 없는 상황에서 여러사람이 은행에 있는 현금을 인출하려고 한다. 그러나 은행에 있는 현금은 제한되어 있기 때문에 모두 인출하고 나면 현금을 인출할 수 없는 사람도 생길 것이다. 이런 상황을 스레드를 이용해서 구현해 볼 것이다.

 

package parallel;

class Bank {
	
	private Long deposit;
	
	Bank (Long deposit) {
		this.deposit = deposit;
	}
	
	public void withdraw(Person p, Long money) {
		if (this.deposit > 0) {
			this.deposit -= money;
			System.out.println(p.getName() + "이 인출하고 난 현재 잔고 : " + this.deposit);
		}
		else {
			System.out.println(p.getName() + " 잔고가 부족합니다!!");
		}
	}
}

class Person implements Runnable {
	
	private Bank bank;
	private Long withdrawMoney;
	private String name;
	
	Person(Bank bank, Long withdrawMoney, String name) {
		this.bank = bank;
		this.withdrawMoney = withdrawMoney;
		this.name = name;
	}
	
	@Override
	public void run() {
		for (int i = 0; i < 51; i++) {
			this.bank.withdraw(this, this.withdrawMoney);
		}
	}
	
	public String getName() {
		return this.name;
	}
	
}

public class Parallel {
	
	public static void main(String[] args) throws InterruptedException {
		Bank bank = new Bank(100L);
		
		Person p[] = new Person[5];
		for (int i = 0; i < 5; i++) {
			p[i] = new Person(bank, 1L, Integer.toString(i));
		}
		
		Thread t[] = new Thread[5];
		for (int i = 0; i < 5; i++) {
			t[i] = new Thread(p[i], Integer.toString(i));
		}
		
		long beforeTime = System.currentTimeMillis();
		
		for (int i = 0; i < 5; i++) {
			t[i].start();
		}
		
		for (int i = 0; i < 5; i++) {
			t[i].join();
		}
		
		long afterTime = System.currentTimeMillis(); 
		long secDiffTime = (afterTime - beforeTime);
		System.out.println("시간차이(m) : "+secDiffTime);
	}
}

 

위의 코드에서는 bank 인스턴스가 공유자원이 된다. 100원이 들어있는 은행에 사람들 5명이 서로 경쟁하면서 인출하는 상황이 만들어졌다. 출력을 통해서 결과를 보면 구현하고자 한 결과를 볼 수 없다. 아래는 출력 예제이다.

이미 인출이 다 되었어도 인출이 된다고!?

공유자원에 대한 동기화 처리가 되어 있지 않으면 위와 같은 상황이 발생할 수 있다. 은행에서 인출이 다 되어 돈이 없는 상황인데 누군가 인출을 또 받는 상황이 발생한다면 끔찍한 장애가 아닐 수 없다.

 

 

이렇게 기본적인 스레드의 내용을 알아봄과 함께 멀티 스레드 프로그래밍에서 발생하는 필연적인 문제인 공유자원에 대한 처리 문제를 소개했다. 다음 포스팅에서는 공유자원을 동기화하는 방법을 알아보자.

'Programming' 카테고리의 다른 글

멀티스레드 프로그래밍 (2) - 동기화 이론  (0) 2021.05.15
Comments