일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 코드업
- 인천여행
- 백트래킹
- VPC
- 쿠버네티스
- 로드밸런서
- Kafka
- 알고리즘
- Spring
- 월미도
- DFS
- springboot
- 클라우드 컴퓨팅
- gcp
- 스프링부트
- 클라우드
- Docker
- Apache Kafka
- 자료구조
- 카프카
- aws
- Spring Boot
- Elasticsearch
- 백준
- Spring Data JPA
- 프로그래밍문제
- JPA
- 스프링
- 오일러프로젝트
- 스프링 부트
- Today
- Total
GW LABS
멀티스레드 프로그래밍 (1) - 스레드 사용법 본문
웹 개발 업무에서 통상적인 비즈니스 로직만 다루다보면 사실상 멀티스레드, 멀티프로세스를 통한 성능향상을 경험해볼 기회가 많지 않다. 그러나 웹 개발의 근간이 되는 정적 웹 서버 혹은 웹 어플리케이션 서버들만 해도 멀티 프로세스 및 멀티 스레드를 통해서 사용자의 요청을 효율적으로 처리하고 있다. 다뤄야 할 데이터의 크기가 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개에서 실행하면 아래와 같은 출력을 볼 수 있다.
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++의 코드와 큰 차이는 없다.
공유자원에 대한 문제
스레드는 힙 영역의 메모리를 공유하기 때문에 동시에 공유자원에 접근했을 경우 의도치 않은 결과를 얻게 될 수 있다. 이런 문제를 이해하기 위해 상황을 설정해서 예제 코드를 만들어보려고 한다.
여러분이 은행에 갔다고 생각해보자. 그런데 이 은행은 매우 작은 은행이여서 은행원이 한 명밖에 없다! 번호표도 없는 상황에서 여러사람이 은행에 있는 현금을 인출하려고 한다. 그러나 은행에 있는 현금은 제한되어 있기 때문에 모두 인출하고 나면 현금을 인출할 수 없는 사람도 생길 것이다. 이런 상황을 스레드를 이용해서 구현해 볼 것이다.
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 |
---|