-
cpu바운드 프로그램 멀티쓰레딩에 최적의 스레드 개수 결정 방법 및 멀티쓰레딩 개발에 주의할점컴퓨터과학 2024. 5. 22. 14:01728x90반응형
기본 원칙
- 최적의 스레드 수 = CPU 코어 수: 일반적으로, CPU 바운드 작업에서는 CPU 코어 수만큼의 스레드를 사용하는 것이 가장 효율적입니다. 이는 각 스레드가 동시에 실행될 수 있어 최대의 병렬성을 달성할 수 있기 때문입니다.
- 과도한 스레드 사용 피하기: 스레드 수가 CPU 코어 수를 초과하면, 스레드 간의 문맥 전환 오버헤드가 발생하여 성능 저하를 초래할 수 있습니다.
쓰레드 갯수 선택 이유
- 병렬 처리 최적화: 각 스레드가 하나의 CPU 코어에서 실행될 수 있어 최대한의 병렬 처리가 가능해집니다.
- 컨텍스트 스위칭 감소: 불필요한 스레드 간의 컨텍스트 스위칭을 최소화하여 CPU 오버헤드를 줄입니다.
예제 코드
아래 예제는 CPU 바운드 작업을 수행하는 파이썬 프로그램입니다. 이 프로그램은 다중 스레드를 사용하여 계산 집약적인 작업을 수행합니다.
import threading import time import multiprocessing # CPU 바운드 작업 예제: 큰 수의 팩토리얼 계산 def cpu_bound_task(n): result = 1 for i in range(2, n + 1): result *= i return result def worker(): print(f"Thread {threading.current_thread().name} starting computation.") cpu_bound_task(50000) # 예제: 큰 수의 팩토리얼 계산 print(f"Thread {threading.current_thread().name} finished computation.") if __name__ == "__main__": num_cores = multiprocessing.cpu_count() # 시스템의 CPU 코어 수 확인 print(f"Number of CPU cores available: {num_cores}") # 최적의 스레드 수 설정: CPU 코어 수와 동일하게 설정 threads = [] for i in range(num_cores): thread = threading.Thread(target=worker, name=f"Thread-{i+1}") threads.append(thread) thread.start() # 모든 스레드가 완료될 때까지 기다림 for thread in threads: thread.join() print("All threads have finished execution.")
CPU 바운드 프로그램에서는 CPU 코어 수만큼의 스레드를 사용하는 것이 최적의 성능을 발휘합니다. 이는 각 스레드가 동시에 실행될 수 있는 환경을 제공하여 최대의 병렬성을 달성하고, 불필요한 컨텍스트 스위칭을 피할 수 있기 때문입니다. 위 예제 코드는 이러한 원칙을 기반으로 작성되었습니다.
멀티쓰레딩 프로그램 개발에 따른 주의점 정리
동기화 이슈 (Synchronization Issues)
레이스 컨디션 (Race Condition)
- 문제점: 두 개 이상의 스레드가 동시에 동일한 자원에 접근하고 수정할 때 발생할 수 있습니다. 결과는 스레드 실행 순서에 따라 달라질 수 있습니다.
- 해결책: 락, 뮤텍스, 세마포어와 같은 동기화 메커니즘을 사용하여 자원에 대한 동시 접근을 제어합니다.
예제
에러 상황:
여러 스레드가 동시에 counter를 증가시키려 하면, 각 스레드의 작업이 중첩되어 예상치 못한 최종 결과가 나올 수 있습니다. synchronized 블록 또는 ReentrantLock을 사용하지 않으면 이러한 문제가 발생할 수 있습니다.
보완 사항:
락을 사용하여 counter 접근을 동기화합니다.
에러가 발생할수 있는 코드:
public class RaceConditionExample { private static int counter = 0; public static void incrementCounter() { for (int i = 0; i < 1000000; i++) { counter++; } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(RaceConditionExample::incrementCounter); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println("Final counter value: " + counter); } }
에러를 보완한 코드
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class RaceConditionExample { private static int counter = 0; private static Lock lock = new ReentrantLock(); public static void incrementCounter() { for (int i = 0; i < 1000000; i++) { lock.lock(); try { counter++; } finally { lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(RaceConditionExample::incrementCounter); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println("Final counter value: " + counter); } }
데드락 (Deadlock)
- 문제점: 두 개 이상의 스레드가 서로가 소유한 락을 기다릴 때 발생하여 영원히 대기 상태에 빠집니다.
- 해결책: 락 획득 순서를 고정하거나, 타임아웃을 설정하여 데드락을 피합니다.
에러 상황:
task1과 task2가 서로가 소유한 락을 기다리면서 데드락이 발생할 수 있습니다. 예를 들어, task1이 lock1을 획득하고 lock2를 기다리는 동안, task2가 lock2를 획득하고 lock1을 기다리면 두 스레드가 영원히 대기 상태에 빠집니다.
보완 사항:
락을 획득하는 순서를 동일하게 하거나 타임아웃을 설정하여 데드락을 피합니다.
에러가 발생할수있는 코드
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class DeadlockExample { private static Lock lock1 = new ReentrantLock(); private static Lock lock2 = new ReentrantLock(); public static void task1() { lock1.lock(); try { Thread.sleep(50); // Simulate some work lock2.lock(); try { System.out.println("Task 1 is running"); } finally { lock2.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock1.unlock(); } } public static void task2() { lock2.lock(); try { Thread.sleep(50); // Simulate some work lock1.lock(); try { System.out.println("Task 2 is running"); } finally { lock1.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock2.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(DeadlockExample::task1); Thread thread2 = new Thread(DeadlockExample::task2); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
에러를 보완한 코드
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class DeadlockExample { private static Lock lock1 = new ReentrantLock(); private static Lock lock2 = new ReentrantLock(); public static void task1() { try { if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) { try { Thread.sleep(50); // Simulate some work if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) { try { System.out.println("Task 1 is running"); } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } } public static void task2() { try { if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) { try { Thread.sleep(50); // Simulate some work if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) { try { System.out.println("Task 2 is running"); } finally { lock1.unlock(); } } } finally { lock2.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(DeadlockExample::task1); Thread thread2 = new Thread(DeadlockExample::task2); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
리소스 관리 (Resource Management)
메모리 누수 (Memory Leak)
- 문제점: 스레드가 종료되지 않고 계속해서 메모리를 점유하는 경우 발생할 수 있습니다.
- 해결책: 스레드가 종료될 수 있도록 하고, 종료된 스레드를 적절히 정리합니다.
에러 상황:
스레드가 종료되지 않거나 적절히 정리되지 않으면 메모리 누수가 발생할 수 있습니다.
보완 사항:
스레드가 정상적으로 종료되도록 하고, 종료된 스레드를 적절히 정리합니다.
public class MemoryLeakExample { public static void worker() { // Simulate work } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(MemoryLeakExample::worker); threads[i].start(); } // If we forget to join the threads, they may still be running, // causing potential memory leaks. } }
에러를 보완한 코드
public class MemoryLeakExample { public static void worker() { // Simulate work } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(MemoryLeakExample::worker); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println("All threads have finished."); } }
성능 이슈 (Performance Issues)
과도한 컨텍스트 스위칭 (Excessive Context Switching)
- 문제점: 스레드 수가 너무 많으면 컨텍스트 스위칭 오버헤드가 발생하여 성능이 저하됩니다.
- 해결책: CPU 코어 수에 맞는 적절한 스레드 수를 유지합니다.
스레드 풀 (Thread Pool) 사용
- 문제점: 매번 새로운 스레드를 생성하고 종료하는 것은 비용이 많이 듭니다.
- 해결책: 스레드 풀을 사용하여 스레드를 재사용합니다.
에러 상황:
스레드를 직접 관리하면 스레드 생성과 종료에 많은 오버헤드가 발생할 수 있습니다.
보완 사항:
스레드 풀을 사용하여 스레드를 재사용합니다.
에러가 발생할수 있는 코드
public class ThreadPoolExample { public static void worker() { System.out.println(Thread.currentThread().getName() + " is running"); } public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(ThreadPoolExample::worker).start(); } } }
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void worker() { System.out.println(Thread.currentThread().getName() + " is running"); } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 20; i++) { executor.submit(ThreadPoolExample::worker); } executor.shutdown(); } }
728x90반응형'컴퓨터과학' 카테고리의 다른 글
JSON 마셜링/언마셜링이란? (0) 2024.05.23 컨슈머 스레드(Consumer Thread)와 프로듀서 스레드(Producer Thread) (0) 2024.05.23 프로세스 컨텍스트 스위칭과 스레드 컨텍스트 스위칭 정의 및 차이 with 예제 python (0) 2024.05.22 단일프로세스,멀티프로그래밍,멀티태스킹,멀티프로세싱,컨텍스트 스위칭,스레드,멀티쓰레드란 무엇인가? (0) 2024.05.17 동기(sync)/비동기(async) , 블로킹(blocking)/논블로킹(nonblocking) 쉽게 이해하고 완전 정복 하자 with 예제코드 with java (0) 2024.05.08