맨위로가기

바쁜 대기

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

바쁜 대기(Busy waiting)는 공유 자원 접근 권한 획득이 짧은 시간에 필요한 경우나, 동기화 객체 사용의 오버헤드가 큰 상황에서 사용될 수 있는 기술이다. 이는 CPU 자원을 소모하며, 멀티 프로세서 환경에서 주로 사용된다. 바쁜 대기는 스핀락 구현과 같이 특정 상황에서 유용하지만, CPU 시간을 낭비한다는 단점이 있다. C 언어 코드 예제와 같이 플래그 변수를 공유하는 여러 스레드에서 바쁜 대기가 사용될 수 있으며, C11의 원자 연산 또는 조건 변수와 같은 대안 기술이 존재한다. 바쁜 대기는 CPU 로드 평균을 높여 시스템 성능 저하를 유발할 수 있으며, 저수준 하드웨어 드라이버 프로그래밍과 같은 특정 상황에서 적절하게 사용될 수 있다.

2. 장단점

바쁜 대기는 공유 자원에 대한 권한 획득이 매우 빠르게 이루어질 수 있거나, 뮤텍스나 세마포어 등의 동기화 객체를 이용하기에는 오버헤드가 큰 상황에서 간단하게 사용할 수 있다는 장점이 있다.[1] 그러나 단일 프로세서 시스템에서는 시스템이 멈추는(hang) 현상이 발생할 수 있으며, 멀티 프로세서 환경에서 사용해야 하고, 공유 자원 이용 시간이 짧아야 한다는 제약이 있다.[1]

대부분의 운영 체제와 스레딩 라이브러리는 락 획득, 타이머 변경, I/O 가능 여부 또는 신호와 같은 이벤트에 대해 프로세스를 차단하는 다양한 시스템 호출을 제공한다. 이러한 호출을 사용하면 일반적으로 가장 간단하고 효율적이며 공정하고 경쟁 상태가 없는 결과를 얻을 수 있다.[2] 호출자가 차단된 동안 다른 프로세스가 CPU를 사용할 수 있으며, 스케줄러는 우선순위 상속 또는 자원 기아를 방지하기 위한 메커니즘을 구현하는 데 필요한 정보를 제공받는다.[2]

바쁜 대기 자체는 `sleep()`와 같은 지연 함수를 사용하여 덜 낭비적으로 만들 수 있다.[3] 루프가 간단한 것을 확인하는 경우 대부분의 시간을 절전 상태로 보내고 CPU 시간을 거의 낭비하지 않는다.[3] 하지만, 일반적으로 바쁜 대기는 CPU 시간을 낭비한다는 관점에서 권장되지 않는다.[4]

2. 1. 우선순위 스케줄링 문제

단순한 우선순위 스케줄링 또는 FCFS(First-come First-served) 스케줄링 기반의 단일 프로세서 시스템에서는 바쁜 대기를 적용할 때 시스템 행(hang)이 발생할 수 있다. 이는 특히 실시간 시스템이나 빠른 응답 속도가 중요한 환경에서 치명적인 문제를 야기할 수 있다.

3. C 언어 코드 예제

다음은 pthread 라이브러리를 사용한 C 언어 코드 예시이다. 여러 스레드가 전역 변수를 통해 플래그를 공유하며, 첫 번째 스레드는 바쁜 대기로 플래그 값의 변경을 기다린다.

```c

#include

#include

#include

#include

/* 모든 함수에서 보이는 전역 플래그 변수 */

atomic_int g_flag;

/* 스레드 #1은 플래그가 거짓 (0)이 될 때까지 스핀하며 대기한다. */

static void *thread_func1(void*) {

while (g_flag) {

/* 아무것도 하지 않음 - 단순히 체크하면서 계속 돈다. */

}

printf("Thread#1 received notification: value of flag has been changed to %d.\n", g_flag);

return NULL;

}

/* 스레드 #2는 일정 시간 대기한 후에 플래그를 거짓 (0)으로 설정한다. */

static void *thread_func2(void*) {

sleep(60); /* 60초 동안 슬립 */

g_flag = 0;

printf("Thread#2 changed the value of flag to %d.\n", g_flag);

return NULL;

}

int main(void) {

int ret_code;

pthread_t t1, t2;

g_flag = 1; /* 플래그를 참으로 설정 */

ret_code = pthread_create(&t1, NULL, thread_func1, NULL);

if (ret_code != 0) {

printf("Failed to create pthread #1.\n");

return -1;

}

ret_code = pthread_create(&t2, NULL, thread_func2, NULL);

if (ret_code != 0) {

printf("Failed to create pthread #2.\n");

return -1;

}

pthread_join(t1, NULL);

pthread_join(t2, NULL);

printf("All pthreads finished.\n");

return 0;

}

```

gcc 또는 clang을 사용할 수 있는 UNIX 계열 시스템에서는 위 코드를 다음과 같이 컴파일할 수 있다.

```bash

$ cc -std=c11 spinlock.c -pthread

```

이러한 코드에서는 플래그 변수 조작을 뮤텍스를 사용한 임계 구역에서 보호하여 명시적으로 동기화하거나, 조건 변수를 사용하거나, C11 또는 C++11 표준 이후의 아토믹 변수를 사용하는 것이 좋다.[3]

3. 1. C 코드 예제 (pthread 및 atomic 사용)

c

#include

#include

#include

#include

#include

/* i는 전역 변수이므로 모든 함수에서 볼 수 있다. 원자 메모리 접근을 허용하는 특수

  • atomic_int 형식을 사용한다.
  • /

atomic_int i = 0;

/* f1은 i가 0에서 변경될 때까지 기다리기 위해 스핀락을 사용한다. */

static void *f1(void *p)

{

int local_i;

/* i의 현재 값을 원자적으로 local_i에 로드하고 해당 값이

0인지 확인한다. */

while ((local_i = atomic_load(&i)) == 0) {

/* 아무것도 하지 않음 - 계속해서 확인만 반복 */

}

printf("i의 값이 %d로 변경되었습니다.\n", local_i);

return NULL;

}

static void *f2(void *p)

{

int local_i = 99;

sleep(10); /* 10초 동안 대기 */

atomic_store(&i, local_i);

printf("t2가 i의 값을 %d로 변경했습니다.\n", local_i);

return NULL;

}

int main()

{

int rc;

pthread_t t1, t2;

rc = pthread_create(&t1, NULL, f1, NULL);

if (rc != 0) {

fprintf(stderr, "pthread f1 실패\n");

return EXIT_FAILURE;

}

rc = pthread_create(&t2, NULL, f2, NULL);

if (rc != 0) {

fprintf(stderr, "pthread f2 실패\n");

return EXIT_FAILURE;

}

pthread_join(t1, NULL);

pthread_join(t2, NULL);

puts("모든 pthread가 완료되었습니다.");

return 0;

}

```

이 코드는 `atomic_int` 형식을 사용하여 변수 `i`에 대한 원자적 접근을 보장한다. `atomic_load` 및 `atomic_store` 함수를 사용하여 `i`의 값을 읽고 쓴다.

pthread 라이브러리를 사용하여 두 개의 스레드를 생성하고, `f1` 함수는 `i`가 0이 아닐 때까지 바쁜 대기를 수행하고, `f2` 함수는 10초 대기 후 `i`의 값을 99로 변경한다.

이와 같은 사용 사례에서는 C11의 조건 변수를 사용하는 것을 고려할 수 있다.[3]

3. 2. C 코드 예제 (volatile 사용, 권장되지 않음)

다음은 C 코드 예제이다. 이 예제는 전역 정수 '''i'''를 공유하는 두 개의 스레드를 보여준다. 첫 번째 스레드는 '''i''' 값의 변경을 검사하기 위해 바쁜 대기를 사용한다.



#include

#include

#include

#include

volatile int i = 0; /* i는 전역 변수이므로 모든 함수에서 볼 수 있다.

또한 컴파일러가 예측할 수 없는 방식(여기서는 다른 스레드)으로 변경될 수 있으므로 volatile로 표시된다.

이는 컴파일러가 값을 캐싱하는 것을 방지한다.*/

/* f1은 i가 0에서 바뀔 때까지 스핀락을 사용하여 기다린다. */

static void *f1(void *p)

{

while (i==0) {

/* 아무것도 하지 않음 - 계속해서 확인만 반복 */

}

printf("i의 값이 %d로 변경되었습니다.\n", i);

return NULL;

}

static void *f2(void *p)

{

sleep(60); /* 60초 동안 대기 */

i = 99;

printf("t2가 i의 값을 %d로 변경했습니다.\n", i);

return NULL;

}

int main()

{

int rc;

pthread_t t1, t2;

rc = pthread_create(&t1, NULL, f1, NULL);

if (rc != 0) {

fprintf(stderr,"pthread f1 실패\n");

return EXIT_FAILURE;

}

rc = pthread_create(&t2, NULL, f2, NULL);

if (rc != 0) {

fprintf(stderr,"pthread f2 실패\n");

return EXIT_FAILURE;

}

pthread_join(t1, NULL);

pthread_join(t2, NULL);

puts("모든 pthread가 종료되었습니다.");

return 0;

}



C 언어 및 C++에서 `volatile` 키워드는 컴파일러 최적화를 억제하기 위한 수식자이며, 그 효과는 처리계에 따라 달라진다. C/C++ 표준에서 스레드가 표준화되지 않았던 시절에는, 스레드 간에 공유되는 플래그 변수에 `volatile`을 지정하여 레지스터에 캐싱하지 않고 반드시 메모리에서 값을 읽어오도록 최적화를 억제하는 효과를 기대할 수 있었다. 이러한 이유로 관습적으로 사용되었지만, 원래 스레드 동기화 목적으로는 사용해서는 안 된다.[3]

4. 대안

대부분의 운영 체제와 스레딩 라이브러리는 락 획득, 타이머, I/O, 신호와 같은 이벤트에 대해 프로세스를 차단하는 시스템 호출을 제공한다. 이러한 시스템 호출은 효율적이고, 공정하며, 경쟁 상태를 방지하는 결과를 가져온다.[1] 대한민국에서는 리눅스 기반 시스템이 널리 사용되므로, 이러한 시스템 호출을 활용한 프로그래밍이 일반적이다.

4. 1. 지연 함수 활용

바쁜 대기는 대부분의 운영 체제에서 제공하는 지연 함수(예: `sleep()`)를 사용하여 개선할 수 있다. 지연 함수는 스레드를 지정된 시간 동안 절전 상태로 만들어 CPU 시간 낭비를 줄인다. 이 방식은 주기적인 작업이나 폴링(polling) 방식의 입출력(I/O) 처리에 유용하다. 루프가 간단한 확인 작업만 수행한다면, 대부분의 시간을 절전 상태로 보내 CPU 시간을 거의 낭비하지 않는다.

4. 2. 이벤트 기반 프로그래밍

대부분의 운영 체제와 스레딩 라이브러리는 시스템 호출을 통해 락 획득, 타이머 변경, I/O 가능 여부, 신호와 같은 이벤트에 대해 프로세스를 차단하는 기능을 제공한다. 이러한 호출은 경쟁 상태 없이 효율적이고 공정한 결과를 얻을 수 있게 한다. 호출자가 차단된 동안 다른 프로세스가 CPU를 사용할 수 있으며, 스케줄러는 우선순위 상속이나 자원 기아를 방지하는 메커니즘을 구현할 수 있다.[1]

시그널을 이용하는 대체 기법을 이벤트 구동 프로그래밍이라고 부른다. 이는 첫 번째 스레드를 슬립 상태로 유지하고, 두 번째 스레드가 플래그 값을 변경했을 때 시그널로 이를 알리는 방식이다. 이 기법은 CPU 시간을 소모하지 않기 때문에 효율적이다.[1]

5. CPU 이용 상황

UNIX 계열 운영 체제에서는 `top`이나 `uptime`과 같은 유틸리티를 사용하여 바쁜 대기 프로그램의 CPU 이용 상황을 확인할 수 있다. 다음은 프로그램 실행 예시이다.

```bash

$ uptime; ./a.out ; uptime

13:25:47 up 53 days, 23:50, 4 users, load average: 0.00, 0.00, 0.00

Thread#2 changed the value of flag to 0.

Thread#1 received notification: value of flag has been changed to 0.

All pthreads finished.

13:26:47 up 53 days, 23:51, 4 users, load average: 0.75, 0.21, 0.07

```

시스템에 따라 표시되는 값은 다를 수 있지만, 프로그램 실행 전 로드 애버리지(시스템 부하 평균 값)가 0.00에서 프로그램 실행 후 최근 1분간의 로드 애버리지가 0.75까지 상승한 것을 볼 수 있다. (로드 애버리지의 계산 방법은 다양하지만, 적어도 1.00이 되면 프로세서 1개가 100% 동작하고 있음을 나타낸다.) 이는 바쁜 대기가 CPU 로드 평균을 높여 시스템 성능 저하를 유발할 수 있음을 보여준다. 대한민국에서는 시스템 자원 사용량을 모니터링하고 최적화하는 것이 중요하게 여겨진다.

6. 적절한 사용 상황

공유 자원에 대한 접근 권한 획득이 매우 빠르게 이루어질 수 있다고 확신하거나, 뮤텍스나 세마포어 같은 동기화 객체를 사용하기에 오버헤드가 큰 상황에서는 바쁜 대기를 사용할 수 있다. 그러나 단순한 우선순위 스케줄링이나 FCFS(선착순 처리 방식) 스케줄링 기반의 단일 프로세서 시스템에서는 바쁜 대기를 사용하면 시스템이 멈출(행) 수 있다. 따라서 멀티 프로세서 환경에서 사용하는 것이 바람직하며, 바쁜 대기로 권한을 얻어 공유 자원을 사용할 때는 매우 짧은 시간 동안만 연산을 수행하고 즉시 권한을 해제해야 한다.

가장 단순한 구현 방법은 "아무것도 하지 않음(nop)" 처리를 반복 실행하여 일정 시간을 지연시키는 것이다. CPU는 nop 명령을 만나면 일정 시간 동안 아무것도 하지 않으므로, 실행하는 nop 명령 횟수를 조정하여 원하는 시간을 지연시킬 수 있다. 보통 nop 명령 반복 실행을 위해 루프 구조를 사용하며, 루프 반복 횟수에 따라 nop 명령 실행 횟수를 지정한다.

하지만 이 방법은 CPU 처리 속도에 따라 nop 명령 하나가 소비하는 시간이 달라질 수 있으므로, 정해진 횟수의 루프로 지연시키는 시간은 변할 수 있다. 특히 장치와의 입출력 처리에서는 장치와의 명령 송수신 및 데이터 처리 타이밍이 중요하며, 단순한 바쁜 대기를 타이밍 조정에 사용하면 동작이 불안정해질 수 있다. 속도(클럭 주파수)가 다른 호환 CPU를 지원하려면, 프로그램 시작 시 nop를 사용한 루프의 실효 속도를 측정하고, 바쁜 대기 부분에서 사용하는 적절한 루프 횟수를 계산하여 설정해야 한다. 그러나 이는 프로그램 실행 중에 클럭 주파수가 항상 고정되어 있다는 전제하에 가능하며, 부하에 따라 동적으로 클럭 주파수가 변동하는 CPU에는 대응할 수 없다.

SMP 시스템을 위한 운영 체제 내의 스핀락 구현과 같은 특정 상황에서 바쁜 대기는 유효하고 실용적이다. 그러나 일반적으로는 CPU 시간을 아무것도 하지 않고 낭비하므로 권장되지 않는다. CPU 시간을 들여 기다릴 시간이라면 다른 스레드를 동작시키는 것이 효율적이다.

6. 1. 실시간 시스템

실시간 시스템에서는 예측 가능한 응답 시간을 보장하는 것이 중요하다. 따라서 바쁜 대기는 제한적으로 사용될 수 있다. 낮은 수준의 프로그래밍에서는 바쁜 대기가 실제로 바람직할 수 있는데, 모든 하드웨어 장치에 대해 인터럽트 구동 처리를 구현하는 것은 바람직하지 않거나 실용적이지 않을 수 있으며, 특히 액세스가 드문 장치의 경우 더욱 그렇다.[1] 때때로 하드웨어에 일종의 제어 데이터를 쓰고 쓰기 작업의 결과로 장치 상태를 가져와야 하는데, 이 상태는 쓰기 작업 후 여러 머신 사이클이 경과해야 유효해질 수 있다.[1] 프로그래머는 운영 체제 지연 함수를 호출할 수 있지만, 그렇게 하면 장치가 상태를 반환할 때까지 몇 클럭 사이클 동안 스핀하는 데 소비되는 시간보다 더 많은 시간을 소모할 수 있다.[1]

저수준 하드웨어 드라이버 프로그래밍에서 바쁜 대기는 실제로 바람직할 수 있다. 모든 장치에 인터럽트에 의한 통지 기능을 구현하는 것은 현실적이지 않다(특히 접근이 거의 발생하지 않는 장치).[2] 경우에 따라 제어 데이터를 장치에 쓰고, 어떠한 데이터를 해당 장치에서 읽어내는 방식의 장치도 있으며, 이 경우 읽기는 수십 클럭 사이클을 기다려야 할 수도 있다(예: 실시간 시계).[2] 프로그래머는 운영 체제의 지연 함수를 호출할 수도 있지만, 함수 호출만으로 기다려야 할 사이클 수를 초과할 가능성이 높다.[2] 이러한 경우 바쁜 대기 방식으로 장치의 상태 변화를 계속 확인하는 것이 일반적이다.[2] 지연 함수를 호출하는 것은 함수 호출의 오버헤드와 스레드 전환 때문에 CPU 시간을 낭비할 뿐이다.[2]

7. 구현 방식 (어셈블리)

구현 방식은 CPU 처리 속도에 따라 달라진다. 특정 명령을 반복 수행하는 방식으로 시간을 지연시킬 수 있는데, 예를 들어 어셈블리 언어에서는 nop (no operation) 명령을 사용한다. 이 명령은 아무런 동작도 하지 않으면서 CPU 시간을 소비한다.

7. 1. 클럭 주파수 변동 문제

CPU의 처리 속도에 따라 nop 명령 하나의 소비 시간이 달라지므로, 정해진 횟수의 루프로 경과시키는 시간은 변한다. 특히 장치와의 입출력 처리에는 장치와의 명령 송수신 및 데이터 처리 타이밍이 중요하며, 단순한 바쁜 대기를 타이밍 조정에 사용하면 동작이 불안정해질 수 있다.[1] 속도(클럭 주파수)가 다른 호환 CPU를 지원하려는 경우, 프로그램 시작 시 nop를 사용한 루프의 실효 속도를 측정하고, 바쁜 대기 부분에서 사용하는 적절한 루프 횟수를 계산하여 설정할 수 있다.[1] 그러나 이는 프로그램 실행 중에 클럭 주파수가 항상 같다는 전제하에 성립하며, 부하에 따라 동적으로 클럭 주파수가 변동하는 CPU에는 대응할 수 없다.[1] 대한민국에서는 전력 효율성과 성능을 위해 CPU 클럭 주파수를 동적으로 조절하는 기술이 널리 사용되므로, 이 문제는 더욱 중요하게 고려되어야 한다.

8. 결론

바쁜 대기는 공유 자원에 대한 접근 권한을 매우 빠르게 얻을 수 있거나, 뮤텍스나 세마포어 같은 동기화 기능을 사용하기에는 부담이 큰 상황에서 간단하게 사용할 수 있다. 그러나 단순한 우선순위 스케줄링이나 선착순 스케줄링 기반의 단일 프로세서 시스템에서는 바쁜 대기를 사용하면 시스템이 멈출(행) 수 있다. 따라서 바쁜 대기는 멀티 프로세서 환경에서 사용하는 것이 좋으며, 바쁜 대기로 권한을 얻어 공유 자원을 사용할 때는 매우 짧은 시간 동안만 연산을 수행하고 바로 권한을 해제해야 한다는 제약이 있다.[1]

참조

[1] 웹사이트 Intel Turbo Boost Technology https://www.intel.co[...]
[2] 웹사이트 Why the 'volatile' type class should not be used https://www.kernel.o[...] 2013-06-10
[3] 웹사이트 POS03-C. volatile を同期用プリミティブとして使用しない https://www.jpcert.o[...]
[4] Internet Archive Javaの理論と実践: volatile を扱う | IBM https://web.archive.[...]
[5] Microsoft Docs volatile (C# リファレンス) | Microsoft Docs https://docs.microso[...]



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com