"엔지니어는 기술로 인정 받을 수는 있지만 존중되는 이유는 다른 데에 있다."

1. 비동기(Asynchronous)  I/O

* 비동기 I/O의 이해
: 프로그램의 CPU 활용도가 심한 기복이 있다는 것은 활용 방식에 문제가 있다는 것이며 성능저하로 이어진다.
: 데이터 수신과 플레이가 반복되는 동영상 플레이어의 경우, 중간중간 플레이 할때 끊길 수 밖에 없다.
: 한번 호출되면 완료될 떄까지 블로킹 되는 함수들을 가리켜 블로킹(Blocking)함수라 하고, 이 함수들을 활용한 입,출력 연산을 가리켜 동기(Synchronous) I/O 라 표현한다.
: 데이터 수신과 플레이를 동시에 병행한다면 위의 사례보다 비교도 안되는 성능향상을 이룰수 있다.
: 데이터 수신은 CPU할당을 크게 요구하지 않는 작업이기 때문이다.
: 이러한 구조의 I/O를 가리켜 비동기(Asynchronous) I/O라 한다.
* 중첩(Overlapped) I/O
: Windows에서 제공하는 비동기 I/O 방식 중에서 가장 대표적인 것이 중첩 I/O 이다.
: 데이터도 읽고 플레이도 동시에 가능하려면 Non-Blocking 함수이어야 한다.
: Non-Blocking함수는 작업의 완료에 상관없이 바로 반환해 버리는 특성을 지닌다. 물론, 반환후에도 작업은 계속된다.
: 이 경우 함수를 호출한 다음 다른 함수를 호출하여 동시에 여러 작업을 진행할수 있게 된다 이것이 중첩 I/O이다.
: 비동기 I/O는 파일보다 네트워크 통신에서 더 큰 의미를 지닌다.
* 중첩(Overlapped) I/O 예제
: 이벤트를 통해 항상 I/O가 완료 되었는지 확인해야 한다.
: 동기 방식 I/O연산에서는 데이터가 클라이언트에 전송되어야 완료되는것이 아니라, 전송을 위해 할당된 내부 메모리 버퍼에 복사가 완료되면 함수는 반환된다.
* 완료루틴 기반 확장 I/O
: 확장 I/O 제공 기능 = 중첩 I/O 제공기능 + a(루틴 컨트롤을 자동으로 해준다는 것)
: 따라서 중첩 I/O와는 달리 이벤트 오브젝트를 생성하지 않는다. I/O가 완료되면 완료루틴(함수)가 자동으로 호출되기 때문이다.
* 알림가능 상태(Alertable State)
: Windows는 완료루틴 실행 타이밍을 우리들이 결정할수있도록 한다.
: SleepEx 함수를 통해 미룰 수 있다.
* OVERLAPPED 구조체의 파일 위치 정보
: 하나의 파일에 중복 I/O로써 접근할 때 커널 오브젝트에 존재하는 파일의 위치정보는 아무 의미가 없다.
: 즉, 파일에 문자가 같은 위치에 덮여져 쓰일 오류가 있다는 것이다.
: 그러므로, OVERLAPPED구조체 맴버 중 데이터 I/O의 시작 위치를 지정하는 Offset, OffsetHigh 을 활용하여 각 함수의 데이터 I/O 시작 위치를 개발자가 직접 계산해야만한다.
* 타이머에서의 완료루틴
: 14장의 타이머와 SleepEx함수를 통해 완료 루틴을 만들수 있다.

* 지금까지의 내용 정리
-키워드 : 비동기 I/O, 동기 I/O, 블로킹 함수, 넌 블로킹 함수, 중첩(Overlapped), 완료루틴
: Windows는 기보넞ㄱ으로 두가지 방식의 비동기 I/O를 지원한다. 중첩 I/O방식과 완료루틴 확장I/O방식이다.
: 블로킹 함수는 호출된 함수가 일을 다 끝낸 다음에 반환하는 함수이다
: 넌 블로킹 함수는 호출되자마자 바로 반환을 하기 때문에 일이 완료되는 시점과 반환하는 시점이 서로 다르다. 그러므로 비동기 I/O는 넌블로킹 방식을 사용한다.
: 완료루틴의 경우 I/O가 완료되었을 때 지정된 함수가 호출된다는 특징을 가진다.
: 하지만 중첩 I/O의 경우 그러한 장치가 없기 때문에 중간에 I/O가 완료되었음을 확인하는 과정이 필요하다.

* APC(Asynchronous Procedure Call)
: 비동기 함수 호출 매커니즘을 의미한다.
* APC의 구조
: 두가지 종류로 나뉜다. User-mode APC 와 Kernel-mode APC 이다.
: Kernel-mode APC는 Normal Kernel-mode APC 와 Special Kernel-mode APC 로 나뉜다.
: 모든 쓰레드는 자신만의 APC Queue 라는 것을 가지고 있다. 

: APC Queue가 쓰레드마다 독립적이라는 것을 생각하면, 완료루틴이 쓰레드 마다 독립적인 메커니즘이라는 것을 알 수 있다.

* APC Queue의 접근
: APC Queue에 함수 정보를 전달 할 수 있는 방법은 소개한 내용 기준으로 세개 이다. WriteFileEX, ReadFileEx, SetWaitableTimer 함수이다.
: QueueUserAPC 함수를 이용하면 직접 전달할 수 있다.
: 나중에 완료루틴이 실행되었을 때 APC Queue에 있는 함수들이 순차적으로 실행되는 것이다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기





18장의 경우 함수 중심의 설명이기 때문에, 생략합니다.


1. 기본적인 파일 처리 함수들

* 파일 열기 닫기

: CreateFile, CloseHandle

* 파일 읽기 & 쓰기와 포인터

: ReadFile & WriteFile

* 파일의 시간 정보 얻어오기

: GetFileTime

* 파일 사이즈 얻어오기

: GetFileSize

* 파일 특성 정보 얻어오기

: GetFileAttributes, SetFileAttributes

* 파일의 특성 정보 핸들로부터 얻어오기 + a

: GetFileInformationByHandle

* 파일의 경로(path) 정보 얻어오기

: GetFullPathName

* 파일 포인터의 이동

: SetFilePointer

2. 디렉터리 관련 함수 및 그 밖의 함수들

* 디렉터리의 생성과 소멸

: CreateDirectory, RemoveDirectory

* 현재 디렉터리, 시스템 디렉터리 그리고 Windows 디렉터리

- 현재 디렉터리

: 초기에는 프로그램이 로드된 디렉터리로 설정되며 그 이후 변경 가능하다.

: GetCurrentDirectory

- 시스템 디렉터리(System Directory) & Windows 디렉터리

: 변경하면 안되는 디렉터리 이기 때문에 변경이 제한된다. 중요 라이브러리들이 저장되어 있다.

: GetSystemDirectory

* 디렉터리에서 파일 찾기

: SearchPath


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. SEH(Stuctured Exception Handling)

* 예외처리의 필요성

: 프로그램이 동작하는 부분과 흐름에 대한 예외처리 하는 부분을 구분하여 볼 수 있기 때문에 편리하다.

* 예외와 에러의 차이점

: 프로그램 실행 시 발생하는 문제점 대부분을 예외라고 인식해야 한다

: 예외 처리 가능하도록 프로그램을 구현해야 한다.

* 하드웨어 예외와 소프트웨어 예외

: 하드웨어 예외란 하드웨어에서 인식하고 알려주는 예외
    ex) 10 / 0 을 연산할 때 CPU가 운영체제에게 예외가 발생핬다는 것을 알린다.

: 소프트웨어 예외는 하드웨어에서 감지하는 예외이다.

: 하드웨어의 예외는 늘릴 수 없지만, 소프트웨어의 예외사항은 늘릴 수 있다.

2. 종료 핸들러(Termination Handler)

: SEH(Structured Exception Handling) 구조적 예외처리 메커니즘은 성능을 약간 저하 시키기 때문에, 서버프로그래밍에서는 사용하지 않는다.

: 종료 핸들러(Termination Handler)와 예외 핸들러(Exception Handler)로 나뉜다.

* 종료 핸들러의 기본 구성과 동작 원리

: _try 블록을 한줄이라도 실행하게 되면, 반드시 _finally 블록을 실행해야 한다.

: _try에서 return을 하더라도, 프로그램이 종료되기 전에 _finally 블록은 실행된다.

: _try 구문을 빠져나오게 하는 대표적 상황이다. {return, continue, break, goto, "예외"}

: 프로세스나 쓰레드의 강제종료일 떄에는 실행되지 않는다.

* 종료 핸들러 활용사례 연구 1

: 파일을 개방했을 때 무조건 닫아줘야 손실을 막을 수 있다.

: 메모리 동적할당시 해제 해주어야 한다.

* 종료 핸들러 활용 사레 연구 2

: 뮤텍스를 반환해야 할때.

3. 예외 핸들러(Exception Handler)

: 예외상황 발생 시 선별적으로 실행한다

* 예외 핸들러와 필터 (Exception Handler & Filters)

: _try 블록에서 예외가 발생했을 때, _except(예외필터) 블록에서 이 상황을 처리하게 된다. 

: 이 때 예외 필터를 통해 예외처리 메커니즘을 어떻게 동작시킬지 결정할 수 있다. 

: 예외상황 이후의 라인을 실행 시키지 않을 수도있고 실행 시킬 수도있다. (EXCEPTION_EXECUTE_HANDLER)

: 예외 핸들러를 사용하면 프로그램이 강제종료 되지 않는다.

* 예외 핸들러의 활용 사례 연구

: 예외 처리 이후의 실행위치를 결정할 수 있다.

* 처리되지 않은 예외의 이동

: 예외 처리가 되지 않은 블록일 경우 상위 블록에 예외처리가 있는지 스택 구조상 아래로 이동하고 해당 스택 프레임은 반환된다.

: main 함수내에서도 예외 처리할 수 없다면 프로그램이 종료된다.

* 핸들러의 중복

: 예외 핸들러는 예외 핸들러끼리 중복이 가능하고 종료 핸들러와도 중복이 가능하다.

* 정의되어 있는 예외의 종류와 예외를 구분하는 방법

: _except 블록 안에서 GetExceptionCode함수를 통해 예외의 종류를 확인할 수 있다.

* EXCEPTION_CONTINUE_EXECUTION & EXCEPTION_CONTINUE_SEARCH

: EXCEPTION_CONTINUE_EXECUTION 은 예외 발생시 다시 그 곳으로 가서 실행을 이어나가는 방식이다.

: EXCEPTION_CONTINUE_SEARCH 은 예외 발생시 다른 곳의 예외처리를 따르게 하는 방식이다.

4. 소프트웨어 기반의 개발자 정의 예외

: 개발자는 소트프웨어 예외에 해당하는 예외상황을 정의 및 추가할 수 있다.

* 소프트웨어 예외(Software Exceptions) 발생

: RaiseException 함수를 통해 예외알림을 추가할 수 있다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 메모리 계층(Memory Hierarchy)

: 컴퓨터를 구입할 때 CPU가 가장 중요시 되고 성능의 기준처럼 느껴지곤한다.

: 하지만, 개발자의 관점에서는 메모리가 가장 중요한 요소이다.

: 제한된 환경에서(CPU속도는 이미 결정되었다고 가정한다) 가장 높은 성능을 낼 수 있도록 하기 위해서는 메모리의 특성을 잘 파악하고 있어야 하기 때문이다.

* 메모리의 범위와 종류

: 컴퓨터를 구성하는 요소 중 임시적이든, 영구적이든 조금이라도 저장기능을 가지고 있으면 메모리 범위에 포함된다.

- 메인 메모리

: RAM 이다. 정확히는 D-RAM 계열의 메모리이다. 메인메모리가 반드시 램이어야할 이유는 없다. 보통 RAM을 사용하기 때문에 일반적인 의미에서 메인메모리가 RAM을 뜻한다

- 레지스터(Register)

: CPU안에 내장되어 연산을 위한 저장소를 제공한다.

- 캐쉬(Cache)

: 캐쉬는 D-RAM 보다 빠른 S-RAM으로 구성하는데, RAM과 구별하기 위해 보통 Cashe라고 표현한다. 

: CPU와 RAM사이에서 중간 저장소 역할을 하는 메모리이다.

: 요즘 캐쉬가 CPU에 내장되어있다고 표현하기도 하는데, 캐쉬 메모리는 원래 CPU의 일부로 존재하는 메모리 개념이 아니다. CPU에 근접해 있는 메모리 개념이다.

: CPU와 같이 올려져 디자인되는 것 뿐이다.

- 하드디스크(Hard Disk)와 이외의 저장 장치들

: 하드 디스크와 SD카드, CD-ROM과 같은 I/O 장치들도 메모리에 해당한다.

: 프로그래머가 개발하는 데 있어서 Register, Cashe, main memory, hard disk 뿐만아니라 I/O장치들과의 입출력 타이밍 및 대기시간 들을 가장 중요한 요소로 생각하고 항상 고민해야 한다.

* 메모리 계층 구조(Memory Hierarchy)

: 프로그램이 실행되는 동안 메모리가 하는 역할은 데이터의 입력 및 출력이다. 기본적인 역할은 모든 메모리가 동일하다.

: 가장 큰 차이점은 CPU기준으로 얼마나 떨어져 있느냐이다. 레지스터는 CPU와 가깝다 못해 안에 있는 메모리고, 그다음으로 캐쉬, 메인메모리, 하드디스크 순서가 된다.

: CPU와 가까이 있는 것이 가장 빠르고 멀리 있을 수록 속도가 느리다. Register에는 별다른 절차 없이 접근할 수 있지만, 메인 메모리에 접근하기 위해서는 복잡한 과정을 거쳐야 한다. Bus Interface Control 을 지나는 등의 과정이다.

: 메모리의 계층이 나뉘는 것은 기술과 비용때문이다. 

: 결국 모든 메모리의 역할은 자신 아래 계층에 대한 캐쉬(자주 사용되는 메모리의 일부를 저장해서 속도를 향상시키는 것)의 의미에서 존재하는 것으로 이해해야 한다.

: 이것이 비 효율적이게 보이겠지만, 실제로는 L1캐쉬와 L2 캐쉬에 연산에 필요한 데이터가 존재할 확률이 90% 이상 된다고 한다. 캐쉬가 속도를 아주 많이 향상시키는 셈이다.


Tip Level 1 캐쉬와 Level2 캐쉬

: 시스템의 성능을 좌우하는 클럭속도는 항상 느린쪽에 맞춰지게 된다. CPU가 아무리 좋아져도 Main Memory의 속도는 이를 따라가지 못하였고, CPU의 연산처리 속도가 두배 빨라졌다고 해도 주변장치의 속도가 그대로라면, 기대하는 만큼 속도 향상을 이룰 수 없다.

: CPU가 연산을 하기 위해서는 Main Memory 에서 피 연산자에 해당하는 데이터를 가져와야 하는데, 그 연산 결과를 메모리에 저장해야 다음 작업을 할 수 있다. I/O에 굉장한 시간이 소모되게 되고, 메모리가 중요하다는 이유가 여기 있다.

: 따라서 CPU가 Main Memory 의 병목현상 때문에 쉬고 있는 것을 줄이기 위해, L1 캐쉬를 두게 되었고 L1캐쉬의 용량적 한계때문에 하나 더 두어 L2 캐쉬가 있게 되었다. 


2. 캐쉬(cache)와 캐쉬 알고리즘

* 컴퓨터 프로그램의 일반적인 특성

: Temporal Locality(템퍼럴 로컬리티)란, 프로그램 실행 시 한번 접근이 이뤄진 주소의 메모리 영역은 자주 접근하게 된다는 프로그램 특성을 표현할 때 사용한다.

: Spatial Locality(스페이셜 로컬리티)란, 프로그램 실행 시 접근하는 메모리 영역은 이미 접근이 이루어진 영역이 근처일 확률이 높다는 프로그램 성격을 표현할 때 사용한다.

: 파워 개발자는 이런 식으로 캐쉬의 도움을 받을 수 있도록 프로그램을 구현하며 이를 캐쉬 프랜들리 코드(Cache Friendly Code)라고 한다.

* 캐쉬 알고리즘

: ALU 연산 과정에서 필요한 데이터가 있다면 Register로 가져와야하는데 이때 L1캐쉬에 데이터가 존재할 경우 이를 가리켜 캐쉬 힛(Cache Hit)라고 하며, 존재하지 않을 경우 캐쉬 미스(Cache Miss)라고 한다. 

: 캐쉬 미스가 발생했을 경우 L2 캐쉬 혹은 메인 메모리에서 데이터를 가져오게 되는데, 이때 해당하는 데이터 뿐만아니라, 데이터가 속해있는 하나의 블록을 모두 가져온다. 이는 블록 단위로 전송을 해서 스페이셜 로컬리티(Spatial Locality)의 특성을 성능 향상에 십분 활용하는 것이다.

: 아래 단계에 있는 메모리 일 수록, 블록 단위를 크게 잡아서 접근 횟수를 줄여 느린 속도에 대한 성능 향상을 도모한다.

: 캐쉬 미스가 발생했을 경우 데이터를 가져오게 되는데, 보통 캐쉬는 메모리를 꽉 채워 놓아 데이터 소유 확률을 높인다. 그렇기 때문에 캐쉬는 블록 교체 알고리즘에 대해 캐쉬 교체 정책(Cache's Replacement Policy)에 따르게 된다. 대학의 운영체제 수업에서 언급되는게 LRU(Least-Recently Used)알고리즘이며 가장 오래전에 참조된 블록을 밀어내는 알고리즘인데 실제로는 캐쉬 정책에 따라 조금씩 차이를 보인다.

* 캐쉬 프렌들리 코드(Cache Friendly Code) 작성 기법

: 가능한 템퍼럴 로컬리티와 스페이셜 로컬리티의 특성을 만족시키는 방향으로 가야 한다.

3. 가상 메모리 (Virtual Memory)

* 물리 주소(Physical Address)

: 실제 물리적인 메인 메모리의 주소 범위에 주소를 할당하는 것을 물리적 주소 지정(Physical Addressing)이라고 하고, 메인 메모리 크기에 따라서 지정 가능한 주소의 범위가 결정된다. 

: 주소범위에 제한이 생긴다는 것은 프로그램 개발에 있어서 엄청난 제약사항이다.

* 가상 주소 (Virtual Address) 시스템 1

: 실제 물리적인 공간은 할당할 수 있는 메모리 크기에 비해 턱없이 부족하다. 따라서 실제 존재하지 않는 가상의 주소를 지정하는 것을 가상 주소 지정(Virtual Addressing)이라 하며, 이러한 메모리 공간을 가상 메모리 공간(Virtual Address Space)라고 한다.

: 하드디스크가 메인 메모리의 역할을 못하라는 법은 없다. 더군다나 조금 느리지만, 용량이 많은 상황에서 메모리 공간을 할당해 준다고해서 문제될 것은 없다.

: 이렇게 되면 둘이상의 프로세스들에게도 4G이상의 메모리 공간 할당이 가능하다.

: 두가지 문제점만 고려하자

1. 선 할당으로 인한 부담

: 실제로 사용할지도 안할지도 모르는 프로세스에게 미리 많은 메모리를 할당한다는 것은 메모리와 시간 낭비이다.

2. 느린 속도의 개선 필요성

: 하드디스크는 너무 느리다

: 대부부분의 시스템에서 Paging(페이징) 이라는 기법을 사용한다.

: 아래 상황은 16bit 시스템에 16K 메인메모리, 프로세스별 64K메모리 할당을 가정한다.

: MMU(Memory Management Unit)은 16K밖에 존재하지 않는 메모리를 64K바이트가 존재하는 것처럼 CPU가 느끼도록 컨트롤 한다.

: 실제로 CPU와 함께 하나로 패키징 되는 장치이고 CPU는 메모리에 직접 접근하지 않고, MMU를 통해 요청한다.

: CPU가 20바이트 할당만 요청해도 MMU는 메모리 할당 단위에 따라 예를들어 4K단위로 할당하여 연산을 줄이고 속도를 빠르게 한다.

: 이러한 메모리 할당 단위, 즉 블럭을 페이지 프레임(Page Frame) 혹은 페이지(Page) 라고 하고 스페이셜 로컬리티의 특성을 위해 이용한다.

: 가상의 메모리에서 물리 메모리로 한 페이지씩 가져온다고 생각한다.

: 페이지 크기를 4K 바이트로 정하는 경우 64K 바이트 메모리 공간에서 얻는 페이지는 16개이다. 페이지 테이블을 통해 해당 페이지의 물리 주소를 찾아낼 수 있다. 예를 들어 상위 4비트는 몇번째 테이블인지, 하위 12비트는 해당 테이블의 어느 주소인지를 나타낸다.

* 가상주소(Virtual Address) 시스템 2

: 물리 메모리의 한계를 하드디스크를 통해 해결할 수 있다.

: 스왑 파일 (Swap File)이라는 개념을 도입해서 램(RAM)에 해당하는 메인 메모리를 하드디스크로까지 확장 한다.

: 가상 메모리에서 메모리의 할당과 주소 변환에 대한 내용들만 MMU가 처리한다.



뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 쓰레드 풀에 대한 이해

: 쓰레드의 생성과 소멸은 컴퓨터의 성능을 저하 시킨다.

: 쓰레드 풀이 기본 원리는 쓰레드의 재활용이다. 

: 쓰레드 풀에 존재하는 쓰레드 보다 일이 많으면 일이 순서대로 처리하게 할 수 도 있고 쓰레드 갯수를 유동적으로 변화시킬 수 있다.

2. 쓰레드 풀의 구현

* 쓰레드 풀 구현의 모듈별 해석

- 쓰레드 풀 자료구조

-쓰레드 풀의 함수 관계

3. 명령 프롬프트 프로젝트 기능 추가

*입력과 출력을 연결하는 파이프

: 서로 다른 프로세스의 입력과 출력을 연결하는 경우, 파이프를 구성한다고 표현한다

ex) type Random.txt | sort


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 실행순서에 있어서의 동기화

: 메모리에 접근하는 쓰레드의 실행순서를 동기화 한다.

* 생산자/소비자 모델

: 입력하는 쓰레드와 출력하는 쓰레드를 독립시켜야 한다.

* 이벤트(Event) 기반 동기화

: 이벤트 오브젝트를 사용한다. 이벤트 오브젝트는 두가지 모드가 있다.

: 이벤트 오브젝트는 프로그래머 요청에 의해서 Signaled 상태가 된다.

: 함수가 Blocked 상태를 빠져나올 때 자동 리셋 모드 이벤트 오브젝트라면, Non-Signaled 상태로의 변화가 자동으로 이루어진다.

* 수동 리셋(Manual-Reset)모드 이벤트(Event)의 활용 예

: 수동 리셋 모드 이벤트는 원하는 타이밍에 둘 이상의 쓰레드를 동시에 깨워서 실행해야 할 때 아주 좋은 도구가 될 수 있다.

: 하지만 둘 이상의 쓰레드의 순서는 뮤텍스를 함께 사용해야 해결 할 수 있다.

2. 이벤트(Event) 더하기 뮤텍스(Mutex)

* 이벤트와 뮤텍스 오브젝트 적용 예제

3. 타이머(TImer) 기반 동기화

: Windows에서 Signaled 상태라는 개념이 중요하다. 어떤 커널 오브젝트는 자동으로 되기도 하고 함수호출을 통해 변하기도한다.

: 타이머 오브젝트는 정해진 시간이 지나면 자동으로 Signaled 상태가 되는 특성을 지닌다.

: 여기서 말하는 동기화는 쓰레드의 실행시간 및 실행주기를 결정하겠다는 의미이다.

: Non-Signal 로 생성된다.

- 수동리셋 타이머

: 가장 일반적인 타이머, 설정한 시간에 이벤트가 발생한다.

- 주기적 타이머

: 설정한 시간에 주기적으로 이벤트가 발생하거나, 설정한 시간 간격 주기로 이벤트가 발생한다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


: 동기화는 컴파일의 문제가 아니라 런타임에서 발생하는 오류다. 이러한 오류는 눈에 띄지 않고 디버깅하기 힘든 오류이다.

: 쓰레드에 관련된 런타임 오류를 미리 예측하고 막는것이 더 쉽다.


1. 쓰레드 동기화란 무엇인가?

* 두가지 관점에서의 쓰레드 동기화

: 순서에 있어서 질서가 지켜지고 있음을 의미하는 동기화 이다.

- 실행순서의 동기화

: 쓰레드 간의 실행 순서를 정의하고 이 순서에 따르도록 하는 것이 쓰레드의 동기화이다.

- 메모리 접근에 대한 동기화

: 메모리 접근에 있어서 동시 접근을 막는 것 또한 쓰레드의 동기화이다.

* 쓰레드 동기화에 있어서의 두가지 방법

- 유저 모드 동기화

: 동기화 시 커널의 힘을 빌리지 않는다. 커널모드로 전환되지 않기 때문에 성능상 이점이 있지만, 기능상 제한도 있다.

- 커널 모드 동기화

: 커널에서 제공하는 동기화 기능을 활용하는 방법이다.

2. 임계영역(Critical Section) 접근 동기화

* 임계영역(Critical Section)에 대한 이해

: 쓰레드가 동시에 접근하여 문제의 원인이 될 수 있는 코드의 블록을 가리켜 임계 영역이라고 한다.

: 임계 영역이란 배타적 접근(한 순간에 하나의 쓰레드만 접근)이 요구되는 공유 리소스(전역변수와 같은)에 접근하는 코드 블록을 의미한다.

- 유저모드 동기화

1. 크리티컬 섹션 (Critical Section) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

2. 인터락 함수 (Interlocked Family Of Function) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

- 커널모드 동기화

3. 뮤텍스(Mutex) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

4. 세마포어(Semaphore) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

5. 이름있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화 : 프로세스 간 동기화에 사용할 예정

6. 이벤트(Event) 기반의 동기화 : 실행순서 동기화에 사용할 예정

3. 유저모드의 동기화 (Synchronization In User Mode)

: 유저모드는 커널 모드에 비해 성능상 이점이 있고 활용 방법도 단순하다.

* 크리티컬 섹션(Critical Section) 기반의 동기화

: 크리티컬 섹션 오브젝트를 만든다.

: 초기화 한다. -> 쓰레드가 크리티컬 섹션 오브젝트를 가져간다 -> 그 이외 쓰레드는 Blocked 된다.

: 쓰레드가 크리티컬 섹션 오브젝트를 반환한다. -> blocked된 쓰레드 중 하나가 가져간다. -> 반복 후 -> 오브젝트를 제거한다.

* 인터락 함수 ( Interlocked Family Of Function ) 기반의 동기화

: 변수 하나에 대한 동기화를 할 때 유용하다.

: volatile 을 사용하면, 컴파일러가 최적화를 하지 않고 캐쉬되지 않고 메모리로 바로 저장한다.

4. 커널 모드 동기화 (Synchronization In Kernel Mode)

: 유저모드 보다 느리지만, 다양한 기능이 사용 가능하다.

* 뮤텍스(Mutex) 기반의 동기화

: 크리티컬 섹션과 비슷

: 함수를 통해 생성되는 뮤텍스는 커널 오브젝트이다.

: 뮤텍스는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓인다.

: 사용 중일 때에는 Non-Signaled 상태이다.

* 세마포어(Semaphore) 기반의 동기화

: 뮤텍스와 비슷하지만, 임계영역에 접근가능한 쓰레드 개수를 조절하는 기능이 있다.

: 키를 반환할 때, 카운트가 하나씩 줄어들어 0이 되었을 경우 더이상 임계영역에 접근이 불가능하다.

* 이름있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화

: 뮤텍스가 커널 오브젝트 기반이라면, 서로 다른 프로세스에 있는 쓰레드 끼리의 동기화도 가능할 것이다.

: 하지만, 핸들 테이블의 유효성 때문에 한계에 부딫힌다.

: 이를 해결하기 위해 뮤텍스에게 이름을 붙여준 것이다.

* 뮤텍스의 소유와 WAIT_ABANDONED

: 뮤텍스의 소유 쓰레드가 예상치 못하게 종료되었을 때 커널은 이를 반환해주고 다른 쓰레드에게 WAIT_ABANDONED를 반환한다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기




1. Windows에서의 쓰레드 생성과 소멸

* 쓰레드의 생성

: 쓰레드를 생성할 수 있는 최대 개수는 메모리가 허용하는 만큼이다.

: 쓰레드의 흐름은 예측할 수 없다. 누가 먼저 실행될 것인지 예측하는 것은 의미가 없다.

: Sleep 함수 -> 자신에게 현재 할당된 타임슬라이스를 포기하고 해당 시간동안 우선순위가 같은 다른 쓰레드에게 실행의 기회를 양보한다.

* 쓰레드의 소멸

: 쓰레드 함수 내에서 return으로 소멸시키는 것이 가장 이상적이다.

case 1 : 쓰레드 종료시 return을 이용하면 좋은 경우(거의 대부분의 경우)

: 1~10까지 더하는 상황을 가정했을 때 (I/O 작업이 발생한다고 가정)

: 이를 세개의 쓰레드에서 나눠서 진행한다면, 정해진 시간 동안에 CPU에게 보다 많은 일 시킬 수 있고, Blocked 상태에 놓이는 경우도 나눠서 감당하기 때문에 속도가 높아질 확률이 높다.

: 구현은 쓰레드에 나누어 실행 시킨뒤에 쓰레드의 커널 오브젝트를 관찰하여 종료되었을 때 메인 쓰레드에서 감지하고 종료한다.

case 2 : 쓰레드 종료 시 ExitThread 함수 호출이 유용한 경우(특정 위치에서 쓰레드의 실행을 종료시키고자 하는 경우)

: 언제 어디서나 쓰레드를 종료시킬 수 있지만, 메모리 누수현상이 일어날 수 있다.

: 하지만, 특정 함수가 호출되었을 때 종료시키고 싶다면, 유용하다.

case 3 : 쓰레드 종료 시 TerminateThread 함수 호출이 유용한 경우(외부에서 쓰레드를 종료시키고자 하는 경우)

: 강제 종료이기 때문에 사용하면 좋지 않다.

2. 쓰레드의 성격과 특성

* 힙, 데이터 영역, 그리고 코드 영역의 공유에 대한 검증

: 쓰레드 끼리 공유하고 있는 total 이라는 전역변수에 직접 결과값을 더함으로써 처리해야하는 코드 양이 줄었다,

* 동시접근에 있어서의 문제점

:  하지만 실제로는 total 값을 불러와서 레지스터에 저장한 뒤, 연산하고 다시 total에 저장하려고 할때 블럭킹되어 다른 쓰레드가 실행된 뒤 total에 값을 저장하게 되어, 연산 값이 제대로 반영되지 않는 경우가 다반사 이다.

: 실제로 Context Switching은 빈번하게 나타나며, 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 매우 높다.

* 프로세스로부터의 쓰레드 분리

: 쓰레드도 프로세스와 마찬가지로 생성될 때 Usage Count가 2가 된다. 따라서 CloseHandle함수를 곧바로 호출 함으로써 프로세스로부터 쓰레드를 분리할 수 있다.

: 그래야 쓰레드의 종료시점이 쓰레드의 소멸시점이 된다.

* ANSI 표준 C라이브러리와 쓰레드

: 마이크로소프트에서는 멀티 스레드에 안전한 ANSI 표준 라이브러리를 제공하고 있다. 

: CreateThread라는 함수 대신에 _beginthreadex 함수를 사용한다. 독립적인 메모리 블록을 할당한다는 차이점이 있다.

: 라이브러리에서 제공하는 함수를 사용하는 것이 안전하다.

3. 쓰레드의 상태 컨트롤

: 쓰레드의 상태는 운영체제가 관리하나, 필요에 따라서 프로그래머가 변경하는 경우도 발생한다.

* 쓰레드의 상태 변화

: 프로세스와 동일하게 이해하면된다.

: Running, Ready, Blocked

* Suspend & Resume

: SuspendThread는 Blocked 상태에 두는 함수이고 ResumeThread는 Ready 상태에 두기 위한 함수이다.

: SuspendThread 함수가 호출되면 해당 쓰레드의 커널 오브젝트에 SuspendCount(디폴트는 0)이 증가하고 Blocked상태가 된다. 반면 ResumeThread는 SuspendCount를 감소시키는 함수인데, 만약 SuspendThread 함수가 두번 호출되었다면, ResumeThread가 두번 호출되어 SuspendCount를 0으로 만들어야 쓰레드가 Ready 상태가 된다.

: 쓰레드 생성시 SuspendCount를 1로 두어 Blocked상태로 생성할 수 있다.

4. 쓰레드 우선순위 컨트롤    

: 사실 우선순위는 프로세스가 가지는 것이아니라, 쓰레드가 가진다.

: 프로세스는 기준 우선순위를 가지며, 쓰레드가 가지는 상대 우선순위와 결합하여 쓰레드의 우선순위가 결정된다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기



1. 쓰레드란 무엇인가?

* 멀티 프로세스 기반 프로그램

: 서로 다른 프로그램을 실행시키기 위해서 다른 프로세스를 생성하는 것 처럼, 하나의 프로그램이 둘 이상의 일을 동시에 처리하기 위해서 둘이 상의 프로세스를 필요로 한다.

* 멀티 프로세스 운영체제 기반 프로그램의 문제점과 새로운 제안

: 많은 수의 프로세스 생성은 빈번한 컨텍스트 스위칭(Context Switching : 레지스터에 프로세스의 상태 정보를 저장하고 복원하는 일련의 과정)으로 이어져 성능에 영향을 미치기 때문에 부담스러운 작업이다.

: 저장하고 복원하는 컨텍스트 정보의 개수를 줄여준다면, 그 부담을 줄어들 것이다. 컨텍스트 정보란 프로세스 상태정보를 뜻한다.

: A프로세스와 B프로세스가 부모 자식관계라도 일단 생성되고 나면 완전히 별개의 프로세스가 된다.

: 그런데 A프로세스와 B프로세스가 완전히 별개가 아닌 50% 정도만 별개이고 나머지는 공유하는 구조라면 컨텍스트 스위칭의 부담도 반으로 줄지 않을까? 이것이 쓰레드의 탄생한 배경이다.

* 해결책, 쓰레드

: 하나의 프로그램 내에서 둘 이상의 프로그램 흐름을 만들어내기 위해 디자인되었다. 쓰레드간에는 공유하는 상태 정보들이 있기 때문에 이것이 쓰레드의 컨텍스트 스위칭을 빠르게 하는 요인이 된다.

- 쓰레드는 하나의 프로그램 내에서 여러 개의 실행 흐름을 두기 위한 모델이다.

- 쓰레드는 프로세스처럼 완벽히 독립적인 구조가 아니다. 쓰레드들 사이에는 공유하는 요소들이 있다.

- 쓰레드는 공유하는 요소가 잇는 관계로 컨텍스트 스위칭에 걸리는 시간이 프로세스 보다 짧다.

* 메모리 구조 관점에서 본 프로세스와 쓰레드

: 자식 프로세스 생성후 메모리 구조는 서로 아무리 관계가 없게 된다.

: 쓰레드 생성후 메모리 구조는 해당 쓰레드만을 위한 스택을 생성할 뿐 그 이외의 영역은 프로세스영역을 공유하고 있다.

- 쓰레드의 특성 1 : 쓰레드마다 스택을 독립적으로 할당해 준다.

: 스택은 함수 호출 시 전달되는 인자, 되돌아갈 주소값 및 함수내에서 선언하는 변수들을 저장하는 메모리 공간이다.

: 실행흐름의 추가를 위한 최소 조건이다.

- 쓰레드의 특성 2 : 코드 영역을 공유한다.

: 프로세스의 main 함수 이외에도 쓰레드의 main 함수가 따로 존재한다.

- 쓰레드의 특성 3 : 데이터 영역과 힙을 공유한다.

: 쓰레드 간에 힙과 데이터 영역을 공유하기 때문에 IPC가 필요 없어졌다. 전역변수와 malloc 함수를 통해서 동적할당된 메모리 공간은 공유가 가능하다.

: 메모리 영역을 공유할 때 문제가 발생할 수 있기 때문에 프로그래밍시 주의가 필요하다

* Windows에서의 프로세스와 쓰레드

: Windows 입장에서는 프로세스는 단순히 쓰레드를 담는 상자에 지나지 않는다.

: 사실 Windows운영체제에서 프로세스는 상태를(Running, Ready, Blocked)를 지니지 않고 쓰레드가 가진다.

: 스케줄러가 실행단위로 잡는 것도 쓰레드이다.

: 즉, Windows에서 실행의 중심은 프로세스가 아니라 쓰레드이다.

: main Thread라고 부르며 일반적으로 프로그래머에 의해서 직접적으로 생성되는 쓰레드와 구분지어 말한다.

2. 쓰레드 구현 모델에 따른 구분

* 커널레벨(Kernel Level) 쓰레드와 유저레벨(User Level) 쓰레드

-. 커널레벨 쓰레드 모델

: 프로그래머 요청에 따라 쓰레드를 생성 및 스케줄링하는 주체가 커널인 경우, 커널 레벨 쓰레드라고 한다.

: 위의 유저영역은 코드, 데이터, 스팁 및 힙을 가리킨다.

: 커널 영역은 운영체제라는 하나의 소프트웨어를 실행시키기 우해서 필요한 메모리 공간이다.

: 오늘 날의 대부분의 운영체제는 커널 레벨 쓰레드를 기반으로 쓰레드 모델을 지원한다.

-. 유저레벨 쓰레드 모델

: 커널에서 쓰레드 기능을 지원하지 않을 때 생각해 볼 수 있는 모델.

: 커널에 의존적이지 않은 형태로 쓰레드의 기능을 제공하는 라이브러리를 활용할 수 있다.

: 쓰레드를 지원하지 않기 때문에 스케줄링의 대상은 프로세스이다.

: 쓰레드를 스케줄링하는 스케줄러는 유저영역에서 실행된다.

Tip 커널(Kernel) 영역

: 운영체제도 일반적 프로세스과 마찬가지로 함수, 스택, 코드, 전역함수 선언 등등이 동일하게 존재한다.

: 영역이 다른 프로그램일 뿐이고 유저가 사용하는 프로세스와 혼선을 피하기 위해 유저영역, 커널영역으로 나누어 둔것이다.

Tip 컨텍스트 스위칭이 빨라진 쓰레드

- pc : 쓰레드마다 별개의 main함수가 돌아가므로 Context switching이 발생한다.

- fp, sp : 스텍은 별도이므로 Context switching이 발생한다.

- Register : 공유될수는 있으나 디자인에 따라 달라지므로 일반적으로 언급하기 힘들다

- 성능의 차이는 Cash에서 찾자(한번 읽어들인 메인 메모리의 데이터를 저장하고 있다가 CPU가 다시 그 메모리에 저장된 데이터를 요구할 때, 바로 전달해주는 역할)

- 쓰레드는 Cash에 있는 정보를 같이 사용하기 때문에 Cash가 다시 메인 메모리를 읽어들일 필요가 사라진다.


* 커널 모드(Kernel Mode)와 유저모드(User Mode)

: 유저 모드에서는 커널 영역으로의 접근이 금지된다. 

: 커널 모드에서는 모든 영역의 접근이 혀용된다.

: 모드의 전환(커널 모드 유저모드)은 시스템에 부담을 주는 일이다.

: 커널 모드와 유저 모드를 제공하는 것은 프로세서(Processor), 즉 CPU에서 제공하는 일이다.

* 커널 레벨 쓰레드와 유저 레벨 쓰레드의 장점 및 단점

- 커널 레벨 쓰레드

장점 : 안전성, 기능의 다양성

단점 : 커널에서 기능을 제공하기 때문에 성능 저하

- 유저레벨 쓰레드

장점 : 전환 필요없기때문에 성능 좋음

단점 : 프로세스 내에 쓰레드가 하나만 블로킹 되어도 나머지 쓰레드가 작동하기 어려움

: 리눅스의 경우 유저 레벨 쓰레드를 활용하기도 한다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


이번 Part3의 큰 주제는 쓰레드이다. 쓰레드가 무엇인지 설명하고 쓰레드 프로그래밍에서 가장 중요한 동기화를 공부한다.



: 함수가 호출되는 원리와 호출 될때마다 할당되는 메모리 방식에 대해 이야기 할 것이다. 

1. 절차적 함수 호출(Procedure Call)지원 CPU 모델

: 함수 호출이라는 것은 소프트웨어 적인 무언가에 의해 작동하는 기능으로 이해하는 경향이 강하다.

: 하지만, 함수 호출이라는 기능은 하드웨어 종속적인 부분이 상당수 존재한다.

: CPU에 따라서 함수가 호출되는 방식이 다르다.

* 스택 프레임(Stack Frame)구조

: 함수내 선언된 변수는 스택에 할당된다.

: 함수 호출 과정에서 할당되는 메모리 블록을 스택프레임이라고 한다.

: 해당 함수가 호출되면서 변수가 스택에 할당되는데 이를 스택프레임이라고 하고 함수가 반환되었을 때 이 스택 프레임은 반환되어 접근할 수 없다.

* sp 레지스터

: 지역변수의 메모리 공간을 스택이라고 불리는 이유는 메모리의 구조적인 특성인 First In, First Out 때문이다.

: 스택의 위치를 기억해야 데이터를 쌓거나 반환할 수 있기 때문에, 이를 위해 sp(Stack Pointer)라는 이름의 Register가 존재한다.

: 스택은 주소값이 위에서 아래로, 혹은 아래서 위로 올라가는 구조일 수 있다.

: 호출이 완료된 함수에서 빠져나오는 시점에서 메모리 공간을 얼마나 반환해야 하는지(sp를 밑으로 얼만큼 움직여야하는지) 알 수 없다.

* 프레임 포인터(Frame Pointer) 레지스터

: 나중에 되돌아갈 sp의 위치를 저장해 놓는 레지스터이다.

: 함수 호출이 일어날 때 마다 fp 레지스터에 저장되어 있는 값을 스택에 저장한다. 이로써 함수를 중복 호출했을때의 문제를 해결 할 수 있다.

2. 함수 호출 인자의 전달과 PUSH & POP 명령어 디자인

: 함수 호출과 프로시저(Procedure) 호출의 차이는 반환값의 유무로도 볼 수 있다.

함수 호출 시 실행 위치의 이동은 어떻게 이뤄지는가?

함수 호출 시 전달되는 인자들은 어떻게 함수 내부로 전달되는가?

함수 호출이 끝나고 나면 어떻게 이전 실행위치로 복귀하는가?

* 함수 호출 인자의 전달방식

: 지역변수와 마찬가지로 스택에 할당한다.

: 성능향상을 위해 일부 전달 인자들은 레지스터를 할당하도록 제품의 표준을 정의하기도 한다.

* PUSH & POP 명령어 디자인

3. 함수 호출(Procedure Call)에 의한 실행의 이동

* 다시 살펴보는 메모리 구조와 프로그램 카운터

: 프로그램을 실행시키면 위와 같은 메모리 구조가 형성되고 Code 영역에 실행되어야 할 명령어들이 올라가서 순차적인 실행이 이뤄지게 된다.

: 코드영역에 올라간 다음부터 명령어는 Fetch, Decode, Execution된다.

: CPU가 메모리 영역 중 스택을 컨트롤하기 위해서 sp 레지스터를 둔 것처럼, 명령어를 순차적으로 fetch하기 위해서 프로그램 카운터라 불리는 pc 레지스터를 둔다.

: pc 레지스터는 CPU가 자동으로 Fetch, Decode, Execution 과정을 진행하기 때문에 조절하지 않아도 되지만, 경우에 따라서 직접 조절해야하는 경우도 발생한다.

* 함수 호출과 함수 종료

: 함수 호출 시 실행위치의 이동은 어떻게 이뤄지는가?

: 함수호출이 가능하기 위해서는 pc 레지스터가 순차적인 실행만으로는 부족하고 특정 위치로의 이동이 가능토록 해야만 한다.

: IR을 sp의 fp처럼 사용한다. 이때 과거의 IR값은 스택에 저장한다.

4. 함수 호출 규약

* 함수 호출 규약이란?

: 전달 인자의 스택을 쌓는 방법에 두가지 존재하듯이(위에서 아래로, 아래서 위로), 함수 호출 과정에서 할당된 스택 프레임을 반환하는 방법에도 두가지가 존재한다.

: A함수가 B함수를 호출하는 프로그램을 작성한다고 가정할 때, 스택 프레임을 정리하는 코드가 어디에 존재하느냐에 따라서 그 방법이 크게 두가지로 나뉜다.

: 함수 호출 시 인자를 전달하는 방식과 스택 프레임을 반환하는 방식을 약속해 놓은 것을 함수 호출 규약이라 부른다.

* _cdecl, _stdcall + a

: _stdcall 은 함수 호출 규약을 지정하는 것이다. _stdcall 호출규약에 따라서 STDCallFrunction 함수의 호출과 반환을 처리하라는 뜻

: 모든 함수들은 프로젝트 속성창에 선언된 디폴트 함수 호출 규약을 따르게 되고 따로 선언할 수도 있다.

* 호출 규약의 종류와 의미


: _cdecl은 C/C++의 디폴트 호출 규약으로 알려져 있다.

: _stdcall와 _cdecl의 차이점은 스택 프레임을 반환하는 주체이다. _stdcall은 호출된 함수 내에서 스택 프레임을 반환하도록 정의되어 있다. 

: _fastcall 은 함수를 빠르게 처리하기 위한 호출 규약이다.

: Parameters in registers 부분은 전달되는 인자를 저장할 때 레지스터의 사용 유무를 설명한다. 안의 값은 레지스터 이름을 의미한다.

: 레지스터를 사용한다는 점이 함수 호출을 빨라지게 하는 근거가 된다.

: 64비트에서는 인자를 더 많이 활용하며, windows64bit는 4개 Linux는 최대 14개까지 활용한다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


+ Recent posts