RE-Heat 개발자 일지

[운영체제] Sleep 함수와 우연 그리고 CPU로 랜덤뽑기 본문

CS/운영체제

[운영체제] Sleep 함수와 우연 그리고 CPU로 랜덤뽑기

RE-Heat 2023. 8. 1. 21:02

널널한 개발자님의 강의를 듣고 작성한 글입니다.

 

https://www.youtube.com/watch?v=Js1HSwUurpw&list=PLXvgR_grOs1DGFOeD792kHlRml0PhCe9l&index=2 

 

■ Sleep()이란 무엇인가?

개념 : 프로그램의 실행을 일정 시간 동안 멈추도록 하는 함수.

 

쓰레드가 sleep() 함수를 쓰는 이유

1. 자원 점유를 피하기 위해

    한 쓰레드가 무한반복된다면 다른 쓰레드가 실행될 기회를 거의 받지 못하게 된다.

    해결책 : sleep()함수를 사용해 무한 반복된 쓰레드를 일시적 대기 상태로 만들면 다른 쓰레드가 실행될 수 있다.

2. 실행 시간 제어

    한 쓰레드가 너무 빠르거나 느리게 실행되는 걸 방지하기 위해 사용

3. 동기화 이슈 처리

    여러 쓰레드가 공유 중인 자원에 동시에 접근하면 데이터 일관성을 유지하기 어렵다.

    이런 경합 상태를 해결해야 할 필요가 있는데, 이때 사용할 수 있는 게 sleep()함수다.

     => 특정 쓰레드가 자원 사용 후 일시대기(sleep) => 다른 쓰레드가 이 자원 사용   

    참고] 동기화 : 한 쓰레드가 공유자원을 사용할 때 다른 쓰레드가 접근하지 못하도록 막는 것.

4. 타이머나 딜레이 기능

    특정 작업을 정기적으로 수행할 때 sleep()함수를 사용해 시간 간격을 설정할 수 있다.

    단, sleep()의 매개변수에 넣는 시간은 관리목록에서 제외하는 시간이지 실제 쉬는 시간이 아니므로 오차가 존재.

5. 임시적인 작업 중지

 

 

▶ 쓰레드의 상태 변환

쓰레드는 OS가 관리하기 편하게 상태라는 개념(연산: Running, 중단 : Suspended)을 달고 다닌다.

① Running → Suspended

    쓰레드 => sleep() 사용해 일시정지 요청 => OS CPU 관리 목록에서 해당 쓰레드 제외

② Suspended → Running

    쓰레드 다시 살리기 => OS CPU 관리목록에 등록

 

쓰레드는 일시 정지해야 할 땐 sleep()함수를 써 OS에 CPU 관리 목록에서 잠시 제외해 달라고 요청한다. 

예컨대 sleep(1)이면 1ms동안 CPU 관리목록에서 제외해 달라는 뜻이다.

 

그런데 여기서 문제가 발생하는데 이 1ms가 실제로 쓰레드가 정지된 시간은 아니라는 것이다. 

 

■ 우연 - sleep()의 위험성

우선 실제로 sleep()함수의 수행시간이 어떤지 코드로 확인해 보자. 

 

#include <iostream>
#include <windows.h>

int main()
{
        LARGE_INTEGER freq;
        LARGE_INTEGER begin;
        LARGE_INTEGER end;
        __int64 elapsed;
        double during;

        // CPU 타이머 주파수 확인
        ::QueryPerformanceFrequency(&freq);
        std::cout << "초당 주파수: " << freq.QuadPart << std::endl;

        for (int i = 0; i < 10; i++)
        {
        		// 시작할 때 클럭(Clock) 수 저장
                ::QueryPerformanceCounter(&begin);
                //////////////////////////////////////////////////
                // 1 ms 중단
                ::Sleep(1);
                /////////////////////////////////////////////////
                // 끝났을 때 클럭 수 저장
                ::QueryPerformanceCounter(&end);

                elapsed = end.QuadPart - begin.QuadPart;
                during = (double) elapsed / freq.QuadPart;
                std::cout << "실제로 흘러간 시간(micro): " << during * 100 * 1000 << std::endl;
        }
        return 0;
}

① QueryPerformanceCounter() : 타이머의 현재 클록. tick 수를 얻는다. 수행시간이 얼마나 걸릴지 확인하는 데 쓴다는 뜻.

② Sleep() : 매개변수 기본값은 ms(1초/1000)

 

실제 멈추길 원한 시간인 1ms보다 더 많은 시간을 소요

 

사실 1ms보다 시간이 더 많이 걸리는 건 당연한 일이다. Running<>Suspended로 상태전환한 이후 다시 CPU를 점유하기 위해선 추가적으로 대기해야 되기 때문이다.

더보기

실제 쉬는 시간 = 1ms + &

또한, 운영체제(OS)·CPU의 상태에 따라 추가 지연 시간이 달라질 수 있으며 이로 인해 '우연'이라는 불확실성이 발생할 수 있다. 

 

그리고 이러한 우연은 불필요한 위험을 야기할 수 있다.

 

 

1991년 패트리어트 미사일 요격실패 사고

https://gall.dcinside.com/mgallery/board/view/?id=war&no=2601049

 

요격에 실패한 원인은 컴퓨터에서 실수를 표기할 때 발생하는 필연적으로 발생한 오차 때문이다. 당시 시점의 패트리어트는 24비트 부동소수점 계산으로 0.1초 주기로 시간을 측정하고 있었는데, 컴퓨터 연산에서 사용하는 이진법으로는 0.1을 깔끔하게 표기할 수 없어서 일정 자릿수 이하를 잘라버리고 반올림하여 계산하기에 오차가 필연적으로 발생할 수밖에 없었다. 문제는 장비를 너무 오래 기동하면 발생한 오차가 누적될 수밖에 없다는 것인데, 당시의 사고 상황에서는 100시간 연속으로 가동하여 0.3초 가량의 오차가 발생했고, 누적된 오차로 거리측정이 잘못되어 요격이 불가능한 상황이 발생해 버린 것. (출처 : 나무위키)