CS - Process, Thread

Marble·2026년 4월 12일

스레드와 프로세스에 대해 간략하게 작성한 적이 있는데 이번엔 조금 자세히 알아볼게요

Process와 Thread

저번에 프로세스란 운영체제로부터 자원을 할당받은 작업의 단위, 스레드는 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위라고 했었어요.
앱을 실행하면 프로세스가 만들어지고, 그 안에서 실제 일을 하는 단위가 스레드인 셈이죠
iOS에선 보통 다음 처럼 UI관련은 메인 스레드, 그 외의 작업들은 백그라운드 스레드에서 진행됩니다.

  • 메인 스레드: UI
  • 백그라운드 스레드: 네트워크, 파싱, 디코딩

Race Condition

스레드는 프로세스 안에 있어서 메모리를 공유한다 했었잖아요? 그래서 빠르지만 경합도 발생하죠. 그 중에서 공유 mutable 상태를 동기화 없이 동시에 접근해서 실행 순서에 따라 결과가 달라지는 버그를 Race Condition이라고 해요. 예를 들면 다음과 같은 상황에 발생해요

  • 같은 배열/딕셔너리를 여러 Task가 동시에 수정
  • 토큰 갱신 로직을 동시에 타서 마지막 write가 덮어씀

Context Switching

CPU는 여러 작업을 동시에 하는 것처럼 보이게 하는데 이를 멀티태스킹이라 해요. 그리고 이 멀티 태스킹을 위해 실행 중인 프로세스/스레드를 멈추고, 다른 프로세스/스레드로 바꾸는데 이를 컨텍스트 스위칭이라 하는거죠. 컨텍스트 스위칭을 할 때 비용이 발생하는데 이 비용을 오버헤드라고 해요. 실제 업무 로직을 수행한 시간이 아니라 작업 전환을 관리하는 데 쓰는 필수불가결하게 발생하는 부가 비용이기 때문에 오버헤드라고 불린다 합니다.
이 오버헤드가 발생하는 이유들로는 다음과 같이 있어요

  1. 상태 저장/복원 비용
    현재 스레드의 레지스터, 스택 포인터, 프로그램 카운터 등을 저장하고 다음 스레드의 상태를 다시 불러오는데 CPU가 바로 계산하지 못하고 준비 작업을 해야해요
  2. 스케줄러 실행 비용
    어떤 태스크를 다음에 돌릴지 커널이 판단하기 위해 우선순위, 타임슬라이스, 런큐(run queue) 등을 계산하는데 이 판단 자체가 CPU 시간을 사용해요
  3. 커널 모드 전환 비용
    컨텍스트 스위칭 때는 보통 커널이 개입해서 스케줄링을 하므로 사용자 모드→커널 모드로 들어갑니다. 이때 사용자 모드에서 어떠한 작업들을 하고 있었는지 기억하기 위한 비용이 발생하죠. 또 커널에서 작업을 처리하기 위해 비용이 발생하고 작업이 끝난 후에 다시 사용자 모드로 돌아오기 위해 비용이 발생하죠
  4. 캐시/파이프라인/분기예측 손실
    새 스레드(또는 프로세스)로 바꾸면 CPU 내부 학습 상태가 현재 작업과 안 맞을 수 있어요. 그래서 CPU 캐시 미스 증가하죠. 그리고 예측기는 과거 패턴을 학습하는데, 작업이 바뀌면 패턴이 달라져 오예측이 잠깐 늘 수 있어서 스위칭 직후 성능이 잠깐 예열 구간을 겪는데 이때 비용이 발생하는거죠

비동기인데 한 코어에서만 도는 것처럼 보이는 이유

개발을 하다가 생긴 궁금점인데 이 글을 쓰게 된 이유기도 해요

Instruments를 통해서 CPU를 확인하고 있었는데 비동기 작업을 특정 코어에서만 계속 작업을 하고 쉬고 있는 코어도 있더라고요? 그 이유에 대해 알아볼게요

핵심부터 말하면, async/await는 자동 병렬화가 아니라 구조화된 동시성이라 일을 멈추지 않고 이어서 처리하게 해주지만, 코어를 골고루 다 쓰게 보장하진 않아요. 그럼 왜 한 곳에 몰려하냐면

  1. Task는 스레드가 아님
    Swift Task는 작업 단위고, 실제 스레드/코어 배치는 런타임+OS가 결정하기 때문에 여러 Task가 있어도 같은 스레드에서 순차 재개될 수 있어요
  2. 코어 이동 자체가 비용
    방금 컨텍스트 스위칭에 대해 말했었죠? 코어를 이동하면 컨텍스트 스위칭/캐시 미스 비용이 많이 발생해요. 그래서 효율 좋은 스케줄링 알고리즘을 짜지 않는 이상 짧은 작업은 분산보다 같은 코어에서 처리하는 게 더 효율적일 때가 많아서 위처럼 하나의 코어에서 작업하는거에요

iOS에서 자주 만나는 실제 현상

스케줄링 + 경합 + 대기열이 겹쳐서 ios 개발중에 생길 수 있는 일들은 다음과 같아요

  1. AsyncImage 다량 로딩 시 지연/실패처럼 보임
    예전에 AsyncImage 이미지 로딩 실패 때문에 을 썼었잖아요? 그 원인도 여기 있었어요. 이미지 요청이 한 번에 몰리면 URLSession 연결/스트림 제한에 걸려 일부는 대기, 응답 후 디코딩/캐시/메인 반영까지 몰리는데 그 결과 어떤 이미지는 늦게 뜨고, 셀 재사용/스크롤 타이밍과 겹치면 실패처럼 체감되는 거였어요
  2. 스크롤 끊김
    메인 스레드가 레이아웃/렌더링/이벤트 처리 중인데, 여기에 무거운 작업(JSON 가공, 이미지 변환, 동기 I/O)이 끼어들면 프레임 예산(60Hz면 약 16.7ms)이 초과하여 끊길 수가 있어요. iOS에서 UI는 메인 스레드에서 책임 지잖아요? 그래서 메인 스레드는 UI 전용으로 최대한 비워둬야 해요
    Combine을 사용하면 대부분 네트워크/파싱/가공은 백그라운드 스레드에서, UI 상태 반영만 메인 스레드에서 진행되게 하는 것도 이런 이유 같아요.
urlSession.dataTaskPublisher(for: request)
    .subscribe(on: DispatchQueue.global(qos: .userInitiated)) // 업스트림 작업 시작 컨텍스트
    .map(\.data)
    .decode(type: Response.self, decoder: JSONDecoder())      // 무거운 처리
    .receive(on: DispatchQueue.main)                          // UI 반영 직전 메인으로
    .sink(...)
profile
개발자가 되고 싶은 공돌이

0개의 댓글