스레드 vs 스레드풀 (성능 비교)
📌 서론
테스트를 통해 I/O 작업이 조금 더 스레드풀 설정에 민감하게(성능향상폭) 반응한다는 것을 알게 되었고 CPU작업은 스레드풀 설정에는 민감하지 않지만 제대로 설정하지 않으면 낭비되는 스레드가 많을 것이라는 생각을 하게 되었다.그래서 이번에는 조금 더 상세하게 각 스레드별로 테스트를 진행하고 분석을 해봤다.
이번 내용을 통해 우리가 일반적으로 사용하는 스레드의 성능과 최적화는 어떻게 하는게 좋을지 알아보자.
테스트에 사용되는 코드는 하단의 1,2편에 모두 작성되어 있습니다. (전체 소스코드 깃허브도 이전 포스팅에 포함)
1. 일반 스레드 vs 스레드풀 : I/O 작업
1차 테스트 설정
- 작업수를 10000개로 지정했고 스레드풀은 100개로 설정했다.
// 테스트할 작업의 수
const val TEST_COUNT = 10000
const val THREAD_POOL_SIZE = 100
const val DELAY_MS = 10L // I/O 작업을 시뮬레이션하기 위한 지연 시간
일반 스레드 1차 테스트
- 최대 동시에 활성화된 스레드 수는 453개이다.
🧵 테스트 결과: 일반 스레드 I/O 집중 작업 테스트
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 453
🔢 생성된 스레드 총 수: 10000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 0
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 11.00 ms
🏎️ 최소 작업 시간: 10 ms
🐢 최대 작업 시간: 17 ms
📊 성능 지표:
⏳ 총 실행 시간: 449 ms
🚀 1초당 처리된 작업 수: 22271.71
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
스레드풀 1차 테스트 (100개)
- 스레드풀의 설정대로 최대 동시 활성 스레드 수는 102개다.
🧵 테스트 결과: 스레드 풀 I/O 집중 작업 테스트 (최대 100 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 102
🔢 생성된 스레드 총 수: 100
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 100
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 11.34 ms
🏎️ 최소 작업 시간: 10 ms
🐢 최대 작업 시간: 22 ms
📊 성능 지표:
⏳ 총 실행 시간: 1200 ms
🚀 1초당 처리된 작업 수: 8333.33
📌 추가 정보:
• 스레드 풀 크기: 100
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 100.00
• 스레드당 최소 작업 수: 98
• 스레드당 최대 작업 수: 102
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과:
- 일반 스레드는 최대 동시 스레드가 453개로 스레드풀 100개보다 훨씬 많다. 그래서 동시에 실행되는 작업이 훨씬 많다 보니 총 실행 시간이 449ms로 스레드풀의 1200ms보다 훨씬 빠르다는 것을 알 수 있다.
2차 테스트 설정
- 스레드풀 개수를 300으로 올렸다.
- 일반 스레드 테스트는 '스레드 풀 설정'을 하지 않기 때문에 항상 같은 테스트를 진행하게 되며 거의 동일한 결과를 보인다. 그래서 첫 번째 결과를 기준으로 모든 테스트에 적용하도록 하겠다. (실제로 해봤는데 모든 테스트 결과가 거의 동일하다.)
// 테스트할 작업의 수
const val TEST_COUNT = 10000
const val THREAD_POOL_SIZE = 300
const val DELAY_MS = 10L // I/O 작업을 시뮬레이션하기 위한 지연 시간
스레드풀 2차 테스트 (300개)
🧵 테스트 결과: 스레드 풀 I/O 집중 작업 테스트 (최대 300 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 302
🔢 생성된 스레드 총 수: 300
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 300
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 11.22 ms
🏎️ 최소 작업 시간: 10 ms
🐢 최대 작업 시간: 21 ms
📊 성능 지표:
⏳ 총 실행 시간: 418 ms
🚀 1초당 처리된 작업 수: 23923.44
📌 추가 정보:
• 스레드 풀 크기: 300
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 33.33
• 스레드당 최소 작업 수: 32
• 스레드당 최대 작업 수: 34
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과:
- 총 실행 시간을 따져보면 '일반 스레드'는 449ms였지만 '스레드풀을 300개'로 설정했을 때는 418ms가 나온다. 일반 스레드보다 스레드풀 300개가 성능이 좋은 것을 알 수 있다.
- 또한 스레드 100개의 1200ms는 훨씬 뛰어넘는 성능을 보여준다. 적절한 스레드풀의 설정이 얼마나 중요한지 보여준다.
스레드풀 3차 테스트(1000개)
🧵 테스트 결과: 스레드 풀 I/O 집중 작업 테스트 (최대 1000 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 1002
🔢 생성된 스레드 총 수: 1000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 1000
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 12.30 ms
🏎️ 최소 작업 시간: 10 ms
🐢 최대 작업 시간: 22 ms
📊 성능 지표:
⏳ 총 실행 시간: 193 ms
🚀 1초당 처리된 작업 수: 51813.47
📌 추가 정보:
• 스레드 풀 크기: 1000
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 10.00
• 스레드당 최소 작업 수: 3
• 스레드당 최대 작업 수: 12
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과:
- 일반 스레드: 449ms, 스레드풀 100개:1200ms, 스레드풀 300개: 418ms, 스레드풀 1000개: 193ms
- 이번 테스트에서는 이전(300개)보다 2배는 빠른 성능을 보여준다. 스레드풀을 늘리니 I/O작업의 성능이 계속 좋아진다.
스레드풀 4차 테스트 (2000개)
🧵 테스트 결과: 스레드 풀 I/O 집중 작업 테스트 (최대 2000 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 1660
📈 최대 동시 활성 스레드 수: 2002
🔢 생성된 스레드 총 수: 2000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 2000
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 11.68 ms
🏎️ 최소 작업 시간: 10 ms
🐢 최대 작업 시간: 20 ms
📊 성능 지표:
⏳ 총 실행 시간: 284 ms
🚀 1초당 처리된 작업 수: 35211.27
📌 추가 정보:
• 스레드 풀 크기: 2000
• 스레드 풀 종료 후 활성 스레드 수: 1860
• 스레드당 평균 작업 수: 5.00
• 스레드당 최소 작업 수: 1
• 스레드당 최대 작업 수: 12
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 일반 스레드: 449ms, 스레드풀 100개:1200ms, 스레드풀 300개: 418ms, 스레드풀 1000개: 193ms, 스레드풀 2000개: 284ms
- 이번 테스트에서는 성능이 안 좋아졌다. 오히려 스레드의 적절한 생성을 넘어서다 보니 생성비용이 발생하면서 손해를 보는 것 같다. 그래도 아직 스레드풀 300개보다는 성능이 좋다는 것을 알 수 있다.
마지막 스레드풀 5차 테스트 (3000개)
🧵 테스트 결과: 스레드 풀 I/O 집중 작업 테스트 (최대 3000 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 19
📈 최대 동시 활성 스레드 수: 3002
🔢 생성된 스레드 총 수: 3000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 3000
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 11.85 ms
🏎️ 최소 작업 시간: 10 ms
🐢 최대 작업 시간: 27 ms
📊 성능 지표:
⏳ 총 실행 시간: 351 ms
🚀 1초당 처리된 작업 수: 28490.03
📌 추가 정보:
• 스레드 풀 크기: 3000
• 스레드 풀 종료 후 활성 스레드 수: 215
• 스레드당 평균 작업 수: 3.33
• 스레드당 최소 작업 수: 1
• 스레드당 최대 작업 수: 14
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 일반 스레드: 449ms, 스레드풀 100개:1200ms, 스레드풀 300개: 418ms, 스레드풀 1000개: 193ms, 스레드풀 2000개: 284ms, 스레드풀 3000개: 351ms
- 음... 이번에도 성능이 안 좋아졌다. 확실히 스레드풀 개수를 늘릴수록 스레드의 생성 비용이 커지고 스레드풀의 이점을 활용하지 못하고 비효율적으로 동작시키게 되는 것 때문인 것 같다.
2. 일반 스레드 vs 스레드풀 : I/O 작업 테스트 결과 분석
1. 스레드 풀 크기와 성능의 관계
- 스레드 풀의 크기를 조정하면서 성능이 어떻게 변화하는지를 관찰할 수 있었다. 초기 테스트에서는 스레드 풀을 사용하지 않은 경우보다 스레드 풀을 적절히 사용했을 때 성능이 향상되었다.
- 예를 들어, 스레드 풀 크기를 100에서 300으로 늘리자 실행 시간이 1200ms에서 418ms로 줄어드는 개선이 있었다. 이는 적절한 스레드 풀 크기가 성능을 크게 향상시킬 수 있음을 보여준다.
2. 스레드 풀의 지나친 확장은 성능을 저하시킬 수 있다
- 스레드 풀 크기를 계속해서 늘리는 테스트에서, 스레드 풀이 너무 커지면 오히려 성능이 떨어지는 현상이 관찰되었다. 이것은 이전 2편에서 테스트했을 때와 같은 상황이며 스레드 풀 크기가 1000일 때는 총 실행 시간이 193ms로 매우 좋은 성능을 보였지만, 2000일 때는 284ms로 증가했고, 3000일 때는 351ms로 성능이 더 나빠졌다.
- 이 결과를 통해 알 수 있는 것은 지나치게 많은 스레드를 사용하면 '컨텍스트 스위칭'과 '리소스 관리'로 인한 오버헤드가 증가하여 성능이 저하될 수 있음을 보여준다.
3. 일반 스레드와 스레드 풀 비교
- 일반 스레드를 사용한 경우(스레드 풀 미사용)에는 동시 활성 스레드 수가 453개에 이르렀고, 총 실행 시간은 449ms였다. 반면, 스레드 풀을 사용했을 때는 적은 수의 스레드를 관리하면서도, 적절한 스레드 풀 크기에서는 훨씬 나은 성능을 보여주었다.
- 특히 '스레드 풀' 크기가 1000일 때 193ms로 가장 좋은 성능을 기록했다. 이는 스레드 풀이 적절한 크기에서 효율적으로 작업을 처리할 수 있음을 나타낸다. (이게 제일 중요한 것 같다.)
4. 스레드 관리 효율
- 스레드 풀이 커질수록 스레드당 처리하는 작업 수가 줄어들고, 비활성 스레드 수가 증가하는 경향이 보였다. 이는 스레드가 너무 많으면 오히려 리소스 낭비가 발생할 수 있음을 의미한다.
- 예를 들어, 스레드 풀 크기 3000일 때는 활성 스레드 수가 19에 불과해, 많은 스레드가 비활성 상태로 남아 있었다. 따라서 불필요하게 많은 스레드 풀은 오히려 시스템 리소스에 부담을 줄 수 있다.
5. 최적의 스레드 풀 크기
- 이번 테스트 결과에서 최적의 스레드 풀 크기는 1000개로 나타났다. 이때 성능이 가장 좋았으며(193ms), 이후 크기를 더 늘리면 성능이 오히려 떨어졌다. 이는 '시스템 자원'과 '작업 처리 속도'의 균형을 고려한 '적절한 스레드 풀 크기'가 중요하다는 점을 강조한다.
결론
- 테스트 결과를 종합하면, 스레드 풀의 크기는 적절히 설정해야 하며, 너무 적거나 많을 경우 모두 성능 저하를 가져올 수 있다. 특히 I/O 집중 작업의 경우 적절한 스레드 풀 크기를 유지함으로써 성능을 극대화할 수 있다. 이번 테스트에서는 스레드 풀 크기 1000이 가장 효율적인 크기였고, 스레드 풀이 너무 커지면 오히려 성능이 저하된다는 점을 확인할 수 있었다.
역시 실험을 통해 얻게 되는 결과가 내 궁금증을 해결해 준다.
3. 일반 스레드 vs 스레드풀 : CPU 작업
1차 테스트 설정 (I/O와 동일)
- CPU도 동일하게 10000개의 작업과 100개의 스레드풀로 시작한다.
// 테스트할 작업의 수
const val TEST_COUNT = 10000
const val THREAD_POOL_SIZE = 100
const val DELAY_MS = 10L // I/O 작업을 시뮬레이션하기 위한 지연 시간
일반 스레드 1차 테스트
- 최대 동시 활성 스레드 수가 35개인 것을 확인할 수 있었다.
🧵 테스트 결과: 일반 스레드 CPU 집중 작업 테스트
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 35
🔢 생성된 스레드 총 수: 10000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 0
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 6.89 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 71 ms
📊 성능 지표:
⏳ 총 실행 시간: 6567 ms
🚀 1초당 처리된 작업 수: 1522.77
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
스레드풀 1차 테스트 (100개)
- 스레드풀은 지정한 풀 개수만큼 동시 활성 스레드 수가 측정된다. (2개는 기본 활성화 되어 있던 스레드다.)
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 100 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 102
🔢 생성된 스레드 총 수: 100
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 100
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 62.19 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 361 ms
📊 성능 지표:
⏳ 총 실행 시간: 6506 ms
🚀 1초당 처리된 작업 수: 1537.04
📌 추가 정보:
• 스레드 풀 크기: 100
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 100.00
• 스레드당 최소 작업 수: 89
• 스레드당 최대 작업 수: 109
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과:
- 일반 스레드의 최대 동시 활성 스레드 수는 35개다. 일단 이것만 봐도 I/O의 453개와 비교하면 현저히 적은 것을 알 수 있다.
- 스레드풀을 사용할 때는 당연히 내가 100개의 스레드풀 크기를 지정했기 때문에 100개가 지정되었다. 그러나 100개를 사용한다고 해서 성능의 이점이 있지는 않았다. (스레드 35개나 102개나 성능은 거의 동일하다.)
2차 테스트 설정
- 스레드풀 개수를 300으로 올렸다.
- 일반 스레드 테스트는 '스레드 풀 설정'을 하지 않기 때문에 항상 같은 테스트를 진행하게 되며 거의 동일한 결과를 보인다. 그래서 첫 번째 결과를 기준으로 모든 테스트에 적용하도록 하겠다. (실제로 해봤는데 모든 테스트 결과가 거의 동일하다.)
// 테스트할 작업의 수
const val TEST_COUNT = 10000
const val THREAD_POOL_SIZE = 300
const val DELAY_MS = 10L // I/O 작업을 시뮬레이션하기 위한 지연 시간
스레드풀 2차 테스트 (300개)
- 스레드풀을 300개로 늘렸지만 성능 차이는 보이지 않는다.
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 300 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 302
🔢 생성된 스레드 총 수: 300
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 300
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 170.18 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 873 ms
📊 성능 지표:
⏳ 총 실행 시간: 6499 ms
🚀 1초당 처리된 작업 수: 1538.70
📌 추가 정보:
• 스레드 풀 크기: 300
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 33.33
• 스레드당 최소 작업 수: 26
• 스레드당 최대 작업 수: 44
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과:
- 스레드풀 100개: 6506ms, 스레드풀 300개: 6499ms -> 이걸로 보면 정말 미세한 수준의 성능 개선이 있다.
- 스레드풀의 크기를 늘렸다고 해서 사실상 성능이 좋아지지는 않는다고 보는 게 맞을 것이다.
스레드풀 3차 테스트(1000개)
- 오히려 성능이 안 좋아졌다.
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 1000 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 62
📈 최대 동시 활성 스레드 수: 1002
🔢 생성된 스레드 총 수: 1000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 1000
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 388.65 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 2725 ms
📊 성능 지표:
⏳ 총 실행 시간: 6599 ms
🚀 1초당 처리된 작업 수: 1515.38
📌 추가 정보:
• 스레드 풀 크기: 1000
• 스레드 풀 종료 후 활성 스레드 수: 87
• 스레드당 평균 작업 수: 10.00
• 스레드당 최소 작업 수: 4
• 스레드당 최대 작업 수: 24
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 스레드풀 100개: 6506ms, 스레드풀 300개: 6499ms, 스레드풀 1000개: 6599ms
- ?? 이번에는 스레드풀을 늘렸더니 오히려 성능이 떨어지는 모습을 보여준다. (여러 번 테스트해 봤다.)
스레드풀 4차 테스트 (2000개)
- 스레드를 늘릴수록 점점 성능이 하락되고 있다.
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 2000 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 2002
🔢 생성된 스레드 총 수: 2000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 2000
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 410.60 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 3344 ms
📊 성능 지표:
⏳ 총 실행 시간: 6619 ms
🚀 1초당 처리된 작업 수: 1510.80
📌 추가 정보:
• 스레드 풀 크기: 2000
• 스레드 풀 종료 후 활성 스레드 수: 2
• 스레드당 평균 작업 수: 5.00
• 스레드당 최소 작업 수: 2
• 스레드당 최대 작업 수: 21
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 스레드풀 100개: 6506ms, 스레드풀 300개: 6499ms, 스레드풀 1000개: 6599ms, 스레드풀 2000개: 6619ms
- 이쯤 되니 확신이 든다. CPU작업이든 I/O 작업이든 한계를 넘어서는 스레드풀 개수를 지정하면 오히려 성능이 안 좋아진다.
스레드풀 5차 테스트 (3000개)
- 이 테스트에서 확신했다. 앞으로 스레드를 늘릴수록 성능상 더 손해를 볼 것이다.
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 3000 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 3002
🔢 생성된 스레드 총 수: 3000
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 3000
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 190.07 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 1915 ms
📊 성능 지표:
⏳ 총 실행 시간: 6651 ms
🚀 1초당 처리된 작업 수: 1503.53
📌 추가 정보:
• 스레드 풀 크기: 3000
• 스레드 풀 종료 후 활성 스레드 수: 79
• 스레드당 평균 작업 수: 3.33
• 스레드당 최소 작업 수: 1
• 스레드당 최대 작업 수: 28
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 스레드풀 100개: 6506ms, 스레드풀 300개: 6499ms, 스레드풀 1000개: 6599ms, 스레드풀 2000개: 6619ms, 스레드풀 3000개: 6651ms
- 역시 성능이 나빠지는 것을 확인할 수 있었다. 또한 CPU작업은 스레드풀 100개부터 3000개까지의 성능 차이가 사실상 나타나지 않는다고 생각된다. 스레드풀을 늘릴수록 어느 지점까지는 압도적인 성능향상을 보이던 I/O 작업과는 대조적인 상황이다.
이전 포스팅에서 CPU 작업 테스트를 했을 때도 이런 결과를 얻었다. 그래서 나는 CPU작업은 일반 스레드가 사용 중이던 최대 35개 스레드가 최적의 스레드일 수도 있겠다고 생각했다. 그래서 스레드풀 개수를 줄이는 방식으로 테스트를 한번 더 진행했다.
스레드풀 6차 테스트 (50개)
- 기존 테스트에서 스레드풀 개수를 확 줄여버렸다.
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 50 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 52
🔢 생성된 스레드 총 수: 50
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 50
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 31.14 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 244 ms
📊 성능 지표:
⏳ 총 실행 시간: 6494 ms
🚀 1초당 처리된 작업 수: 1539.88
📌 추가 정보:
• 스레드 풀 크기: 50
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 200.00
• 스레드당 최소 작업 수: 191
• 스레드당 최대 작업 수: 213
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 스레드풀 50개: 6494ms, 스레드풀 100개: 6506ms, 스레드풀 300개: 6499ms, 스레드풀 1000개: 6599ms, 스레드풀 2000개: 6619ms, 스레드풀 3000개: 6651ms
- !?!?! 역시 스레드풀의 개수가 많아질수록 성능이 안 좋아지는 것이 맞았다. 50개로 했더니 가장 빠른 속도로 처리하는 모습을 보여준다.
마지막: 스레드풀 7차 테스트 (30개)
- 최적의 스레드 개수라 생각되는 35개에 가깝도록 30개를 설정해 봤다.
🧵 테스트 결과: 스레드 풀 CPU 집중 작업 테스트 (최대 30 개)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 스레드 정보:
🛫 시작 시 활성 스레드 수: 2
🛬 종료 시 활성 스레드 수: 2
📈 최대 동시 활성 스레드 수: 32
🔢 생성된 스레드 총 수: 30
🔚 (일반, 스레드 풀) 테스트 후 남아있는 스레드 수: 30
📊 작업 처리 정보:
✅ 완료된 작업 수: 10000
⏱️ 평균 작업 시간: 18.42 ms
🏎️ 최소 작업 시간: 5 ms
🐢 최대 작업 시간: 130 ms
📊 성능 지표:
⏳ 총 실행 시간: 6381 ms
🚀 1초당 처리된 작업 수: 1567.15
📌 추가 정보:
• 스레드 풀 크기: 30
• 스레드 풀 종료 후 활성 스레드 수: 0
• 스레드당 평균 작업 수: 333.33
• 스레드당 최소 작업 수: 319
• 스레드당 최대 작업 수: 356
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
결과
- 스레드풀 30개: 6381ms, 스레드풀 50개: 6494ms, 스레드풀 100개: 6506ms, 스레드풀 300개: 6499ms, 스레드풀 1000개: 6599ms, 스레드풀 2000개: 6619ms, 스레드풀 3000개: 6651ms
- 확신을 얻게 되었다. CPU작업은 I/O 작업과는 달리 단순히 스레드 수가 많다고 해서 성능이 좋아진다거나 그런 것이 아니다. 일반 스레드 테스트를 했을 때도 스레드를 400개가 아니라 30개 정도만 생성하길래 왜 그런가 했는데 30개 정도의 스레드가 CPU작업의 최적화된 스레드 개수였나 보다. CPU는 I/O보다 더 세밀하게 스레드를 제어해야 할 것 같다는 생각이 든다.
- 혹시나 해서 20개, 25개, 35개로 설정해서 테스트를 해봤는데 모두 30개와 거의 비슷한 성능을 보여줬다.
이제 마지막 분석을 해보자!
4. 일반 스레드 vs 스레드풀 : CPU 작업 테스트 결과 분석
1. 일반 스레드와 스레드풀의 성능 차이
- CPU 작업에서 일반 스레드와 스레드풀을 비교했을 때, 큰 차이가 없다는 것을 확인할 수 있었다. 일반 스레드는 최대 35개의 동시 스레드만 활성화되었고, 그 결과 성능이 안정적이었다. 반면, 스레드풀을 사용했을 때 스레드 수가 지속적으로 늘어났지만 성능 향상은 거의 없었다.
- 특히 스레드풀 100개, 300개, 1000개로 테스트했을 때, 성능 차이가 거의 없었으며 오히려 하락되는 현상을 보였다. 이것을 통해 CPU 작업의 경우, 스레드 수를 무리하게 늘리는 것은 큰 이점을 주지 않으며, 오히려 오버헤드가 발생할 수 있다는 것을 알 수 있다.
2. 스레드풀의 크기가 너무 크면 성능 저하
- 스레드풀의 크기를 1000개 이상으로 늘렸을 때, 성능이 오히려 저하되는 경향이 나타났다. 스레드풀 크기를 1000개로 설정했을 때 성능이 떨어졌고, 2000개, 3000개로 늘렸을 때는 더욱 나빠졌다. 이는 CPU 작업의 경우 스레드풀이 지나치게 많아지면 컨텍스트 스위칭과 스레드 관리 오버헤드가 증가해 성능이 저하되는 현상이다.
- 즉, CPU 작업을 주로 한다면 너무 많은 스레드를 사용하는 것은 오히려 성능을 저하시킬 수 있으며, 적정 수준에서 스레드 수를 제한하는 것이 성능 최적화에 도움이 된다는 것이다.
3. 스레드풀의 최적 크기
- CPU 작업에서 스레드풀의 최적 크기를 찾기 위한 여러 테스트 결과, 30개의 스레드가 가장 효율적이었다. 스레드풀을 30개로 설정했을 때, 가장 빠른 실행 시간(6381ms)을 기록했다. 이는 CPU 작업에 있어서는 적은 수의 스레드가 더 효율적이라는 것을 보여준다. 스레드풀을 30개로 줄이면, CPU 코어에 적절한 부하를 주면서도 오버헤드를 최소화할 수 있다.
4. CPU 작업의 특성
- CPU 작업은 I/O 작업과 달리, 스레드가 너무 많을 경우 오히려 성능이 떨어질 수 있다. I/O 작업에서는 많은 스레드가 필요할 수 있지만, CPU는 실제 계산 작업을 처리하기 때문에 적절한 수준의 동시성만 유지하면 된다. 이를 넘어서면 스레드 관리와 컨텍스트 스위칭의 오버헤드가 발생하여 성능이 저하된다. 그래서 단순히 많은 스레드를 생성한다고 해서 성능이 향상되지는 않는다.
5. 스레드풀 크기의 적정선
- 스레드풀의 크기를 50개, 30개로 줄였을 때, 성능이 꾸준히 향상되는 것을 볼 수 있었다. 이로부터 CPU 작업에서는 스레드의 개수가 적을수록 더 나은 성능을 보일 수 있다는 점을 알 수 있다. 특히, 스레드풀 크기 30개일 때는 가장 낮은 평균 작업 시간과 전체 실행 시간을 기록하여 최적의 성능을 발휘했다.
최종 결론
- CPU 집중 작업에서 스레드풀의 크기를 늘리는 것은 성능 향상에 도움이 되지 않으며, 오히려 성능 저하를 초래할 수 있다. 최적의 스레드풀 크기는 약 30~50개로 제한하는 것이 가장 효율적이다. 반면, 일반 스레드와 스레드풀 간의 성능 차이는 크지 않지만, 일반 스레드는 동시 활성 스레드 수가 적어 CPU 작업에 적합한 동작을 보여주었다. CPU 작업은 I/O 작업과는 달리 좀 더 신경 써서 스레드 수의 제어가 필요하다.
5. 그 외 테스트 진행 (표를 사용한 정리)
I/O 테스트
- I/O 테스트는 매우 흥미로운 결과를 보여준다. 스레드풀 개수 1000개까지는 늘리는대로 성능이 향상되는 것이 관찰된다.
- 이전과 동일하게 특정 스레드풀 개수를 초과하면 오버헤드가 커져서 그런지 성능이 다시 낮아지는 것을 확인할 수 있다.
테스트 수 | 스레드풀 개수 | 일반 스레드 결과 (ms) | 스레드풀 결과 (ms) |
30000 | 30 | 1175 | 11675 |
30000 | 50 | 1130 | 7028 |
30000 | 100 | 1160 | 3560 |
30000 | 200 | 1150 | 1804 |
30000 | 500 | 1198 | 737 |
30000 | 1000 | 1143 | 493 |
30000 | 2000 | 1121 | 618 |
30000 | 3000 | 1214 | 733 |
CPU테스트
- CPU는 사실상 모든 스레드풀에서 거의 비슷한 성능을 보였다.
- 다만 스레드풀이 적을수록 성능이 좋고 커질수록 성능이 나빠지는것을 볼 수 있다.
- 테스트 결과표에서 18000대의 결과를 보이는것은 스레드풀 개수가 100개일 때뿐이었다.
- 아마 이것은 스레드 100개를 사용하는것이 CPU작업에서의 최적화된 스레드풀 상태라는 것이다.
테스트 수 | 스레드풀 개수 | 일반 스레드 결과 (ms) | 스레드풀 결과 (ms) |
30000 | 30 | 19262 | 19048 |
30000 | 50 | 19293 | 19019 |
30000 | 100 | 19302 | 18939 |
30000 | 200 | 19245 | 19016 |
30000 | 500 | 19285 | 19074 |
30000 | 1000 | 19190 | 19384 |
30000 | 2000 | 19222 | 19387 |
30000 | 3000 | 19291 | 19515 |
내가 내린 결론은 I/O 작업과 CPU작업은 서로 상반되는 최적화 방식을 가졌다는 것이다.
'I/O 작업'은 스레드풀을 늘리며 가장 성능이 좋은 지점을 찾아야 하고 'CPU 작업'은 스레드풀이 많아지면 오히려 성능이 낮아져서 적절한 지점을 찾아야만 한다.
그렇기에 스프링을 사용할때 성능 최적화를 위해서는 어떤 작업을 주로 할지 분석해서 특정 작업에는 직접 설정한 스레드풀을 적용해서 성능 최적화를 해줄 수 있을 것이다.
"코드, 쿼리 뿐만 아니라 적절한 스레드 풀 설정을 해주는것 또한 중요하다."
다음 4편에서는 코루틴에 대한 기본을 다룬다.
'스레드(Thread)' 카테고리의 다른 글
[Thread] 코루틴(Coroutine)의 동시성 제어 (0) | 2024.09.22 |
---|---|
[Thread] 코루틴(Coroutine)의 예외처리 (0) | 2024.09.22 |
[Thread] 4. Kotlin의 코루틴(Coroutine)이란? (1) | 2024.09.19 |
[Thread] 2. 스레드풀의 I/O, CPU 성능 비교 (2) | 2024.09.15 |
[Thread] 1. 일반 스레드의 I/O, CPU 성능 비교 (2) | 2024.09.15 |