수달이네 기술 블로그

17. 스레드 본문

언어/Python

17. 스레드

슬픈 수달이 2025. 9. 4. 15:16

🔍스레드 관련 용어 간단 정리

프로세스: 하나의 응용프로그램이 메모리에 로딩되어 cpu에 의해 실행된 상태.

스레드: 작업단위(프로세스의 실행 단위)를 말함

싱글스레드: 스레드 하나를 운영(메인스레드가 종료되면 프로세스도 종료)

멀티스레드: 여러개의 스레드를 동시에 운영하는 것. (실행중인 스레드가 하나라도 있으면 프로세스 종료되지 않음, 메인이 종료되더라도)

메인스레드: 파이썬 인터프리터가 제일 먼저 시작하는 부분

서브스레드: 메인스레드 혹은 다른 스레드에서 스레드를 만들어서 실행시킨 스레드

  • 서브스레드와 메인스레드는 병렬로 코드를 실행할 수 있다.
  • 동시에 실행되는것처럼 보인다.(멀티 태스킹을 수행하는 것)

🔍파이썬에서의 구현

thread 모듈, threading모듈이 존재한다.

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print("sub thread start", threading.current_thread().getName())
        time.sleep(3)
        print("sub thread end", threading.current_thread().getName())

print("main thread start")
for i in range(5):
    name = "thread {}".format(i)
    t = Worker(name)
    t.start()

print("main thread end")

# sub thread start thread 0
# sub thread start thread 1
# sub thread start thread 2
# sub thread start thread 3
# sub thread start thread 4
# main thread end
# sub thread endsub thread end thread 1sub thread end thread 3
# 
#  thread 4sub thread end
# sub thread end thread 0
#  thread 2

위의 코드의 결과를 보면 스레드가 0~4 순서대로 시작하였다.

그리고 서브스레드들은 3초 기다리도록 설정되어있으나, 동시에 실행되어 3초 후 결과가 한번에 다 나오는 것을 볼 수 있다.

따라서 순서대로 15초를 기다리는 것이 아닌 3초만 기다린다.

또한, 종료될 때 순서가 정해져 있지 않았다.

스레드는 cpu스케줄러에 의해서 작업을 진행하고 끝낸다. 

  • 따라서 cpu의 선택에 따라 종료 순서가 달라진다.(cpu의 선택 순서에 따라 결정됨)
  • 라운드로빈 방식으로 결정된다.

🔍fork와 join

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print("sub thread start", threading.current_thread().getName())
        time.sleep(3)
        print("sub thread end", threading.current_thread().getName())

print("main thread start")
for i in range(5):
    name = "thread {}".format(i)
    t = Worker(name)
    t.start()
    t.join()

print("main thread end")

# sub thread start thread 0
# sub thread end thread 0
# sub thread start thread 1
# sub thread end thread 1
# sub thread start thread 2
# sub thread end thread 2
# sub thread start thread 3
# sub thread end thread 3
# sub thread start thread 4
# sub thread end thread 4
# main thread end

fork는 메인 스레드가 서브스레드를 생성하는 것을 지칭하며

join()은 스레드가 작업을 마칠때까지 기다리도록 하는 메서드이다.(

위 결과에서 보면 join()을 스레드에 설정할 경우 서브스레드가 모두 완료될 경우 main스레드가 종료된다.

🔍데몬스레드(종속성 스레드)

메인스레드가 종료될 때 자신의 실행상태와 상관없이 종료되는 서브스레드이다.(즉, 메인스레드에 종속된 스레드)

ex) 네이버 메일 자동저장의 경우 네이버 메일이 종료되면 자동으로 저장되어야 할 것이다.

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print("sub thread start", threading.current_thread().getName())
        time.sleep(3)
        print("sub thread end", threading.current_thread().getName())

print("main thread start")
for i in range(5):
    name = "thread {}".format(i)
    t = Worker(name)
    t.daemon = True
    t.start()

print("main thread end")

# main thread start
# sub thread start thread 0
# sub thread start thread 1
# sub thread start thread 2
# sub thread start thread 3
# sub thread start thread 4
# main thread end

daemon스레드로 바꿔주었더니 메인스레드가 종료되자 다른 서브스레드들도 종료된 것을 볼 수 있다. 그렇기에 아래 sub thread end구문이 출력되지 않는다.

🔍동시성과 병렬성

프로그램을 구현했는데 성능이 부족한 경우 알고리즘의 개선이 어려운 경우.

동시성, 병렬성을 통해 성능을 향상 시킬 수 있다.

여기서 동시성은 멀티태스킹이다.

  • 일을 잘게 분할한 후 조금씩 번갈아가며 실행하는 것을 말한다. 컴퓨터는 매우 빠르기 때문에 마치 동시에 처리하는 것처럼 보이게 된다.
  • 프로세스나 스레드를 실행할 그 순간에 하나만 처리하지만 너무 빠르므로 계속 실행되는 것 처럼 보인다.

병렬성은 진짜로 동시에 여러개를 처리하는 것이다.

  • 2000년대에 들어 멀티코어 프로세서가 보급되었고, 코어가 여러개이므로 일을 처리하는 일꾼도 여러명이라는 뜻이다. 따라서 하나씩 쓰레드를 맡아 독립적으로 진행하여 일을 병렬적으로 처리하게 된다.

🔍스레드의 문제점(동기화)

운영체제 과목에서 배웠던 임계구역에 관련한 문제는 스레드에 관한 문제이다. 임계구역에 두 스레드가 한번에 진입할 경우 데이터의 신뢰성이 떨어질 가능성이 있다.

import threading

totalCount = 0
class Counter(threading.Thread):
    def __init__(self):
        super().__init__()

    def run(self):
        global totalCount
        for _ in range(25000000):
            totalCount += 1
        print("25000000번 카운팅 끝")

if __name__ == '__main__':
    for _ in range(4):
        cnt = Counter()
        cnt.start()

    print("스레드 진행 중...")
    mainThread = threading.currentThread()
    for thread in threading.enumerate():
        #메인스레드 빼고 나머지 스레드가 끝날때 까지 대기
        if thread is not mainThread:
            thread.join()
    print("총 %s 번 카운팅" %totalCount)
    
# 스레드 진행 중...
# 25000000번 카운팅 끝
# 25000000번 카운팅 끝
# 25000000번 카운팅 끝
# 25000000번 카운팅 끝
# 총 44483007 번 카운팅

원래는 100000000번이 나와야 정상이지만 개수차이가 크다. 그 이유는 변수에 동시에 접근했기 때문이다.

해당 문제를 해결하기 위해 파이썬은 Lock기능을 지원한다.

Lock

Lock: 특정 스레드에서 변수를 사용하기 시작했으면 다른 스레드가 사용하지 못하도록 막는 역할(객체 잠금)

Lock.aquire(): 다른 스레드가 접근 못하도록 막음

이 두 함수 사이에

Lock.release(): 잠금 해제

import threading
totalCount = None

class ThreadVariable():
    def __init__(self):
        self.lock = threading.Lock()
        self.lockedValue = 0
    def plus(self, value):
        self.lock.acquire()
        self.lockedValue += value
        self.lock.release()

class Counter(threading.Thread):
    def __init__(self):
        super().__init__()
    def run(self):
        global totalCount
        for _ in range(2500000):
            totalCount.plus(1)
        print("카운팅 끝")

if __name__ == '__main__':
    totalCount = ThreadVariable()
    for _ in range(4):
        lockThread = Counter()
        lockThread.start()
    print("스레드 진행 중...")
    mainThread = threading.currentThread()
    for thread in threading.enumerate():
        # 메인스레드 빼고 나머지 스레드가 끝날때 까지 대기
        if thread is not mainThread:
            thread.join()
    total = format(totalCount.lockedValue, ',')
    print(total)
    
# 스레드 진행 중...
# 카운팅 끝
# 카운팅 끝
# 카운팅 끝
# 카운팅 끝
# 10,000,000

해당 기능 사용시 연산속도는 느려진다. 그러나 데이터의 신뢰성을 보장할 수 있다.