반응형
C 프로그래머가 알아야할 것들 - Chapter 4. 프로그램 언어

김성훈 (sunghun84@nate.com)

Chapter 4. 프로그램 언어

(1) 왜 문법을 배워야하는가?
(2) 내가 이걸 배워서 과연 실전에 사용할 수 있을까?
(3) 무엇을 위한 프로그램인가?

(1) 왜 문법을 배워야 하는가?

사실 이건 당연한 이야기 일 수 있는데요, 한국어를 할줄 모르는 독일인과, 독일어를 할줄 모르는 한국인과 대화가 가능할까요? 바디 랭귀지로 하면 되지 않느냐는 분도 계시겠지만 그것도 어느정도 한계가 있기에, 제대로된 의사소통은 불가능할겁니다.

컴퓨터는 0과 1 (2진수)밖에 인식하지 못한다고 앞에서 배웠습니다.
그렇다고 컴퓨터에게 00001110 01010101 이런식으로 모든 명령을 내려야한다면 프로그래머는 정말 수학에 능통한 사람이 아니면 힘들껍니다. (수학에 능통하더라도, 구현할 수 있는 수준은 한계가 있겠죠)

그래서, 기계어에서는 16진수 (2진수를 4개씩 묶어서) 표현하고 있습니다. 2진수일 때보다 조금 나아졌지만, 과연 이걸로 프로그램을 만들 수 있을까요? 이걸로도 뭔가 많이 부족해보입니다.

좀 더 사용자(프로그래머)가 이해하기 쉬운 언어가 필요했습니다.

그래서 나온 어셈블리어는 좀 더 사용자가 알기 편하게 하기 위해, 오퍼랜드, 오퍼레이터, 명령 니모닉등으로 구성되어 있는데, 그렇기에 프로그래머가 기계어보다는 이해하기 쉬웠습니다만, 규모가 큰 프로그램에는 적합하지 못했죠.

결국 프로그래머가 사용하기도 편하고, 기능도(비교적) 우수한 구조형 언어 (파스칼, 코볼,포트란, C언어)등이 나오게되었습니다.

프로그램 언어를 통해 컴퓨터에게 일을 시키기 위해선 어떻게 해야 할까요?

"야 지금부터 시간재라". "야 지금부터 음악 재생좀 해봐."

이렇게 컴퓨터에게 우리가 사용하는 말로 말하면 알까요? (음성인식 프로그램은 예외입니다) 컴퓨터는 컴퓨터가 알 수 있는 말로 해야하기 때문에, 기계어로 말하는건 사실상 불가능에 가까우니, 프로그램 언어의 힘을 빌려야하는데, 그 프로그램 언어가 가진 규칙이 문법입니다. 프로그래머가 컴퓨터와 의사소통을 하기 위해선, 사용하는 프로그램 언어의 문법을 지켜야만 원하는 일을 시키고, 컴퓨터에게 상황을 보고 받을 수 있기 때문에 문법에 대해 알아야 하는건 당연하겠죠?

(2) 내가 이걸 배워서 과연 실전에 사용할 수 있을까?

많은 분들이 하시는 고민. 지금 이 글을 쓰고 있는 저도 마찬가지였던 고민.
바로 그것이 과연 지금 배운 문법들로 프로그램을 만들 수 있겠는가 하는것입니다.
당연한 얘기지만, 정답은 "그렇다"는 것입니다.

그런데, 왜 문법까진 이해했는데, 왜 난 프로그램을 만들 수 없을까하는 생각을 하시는 분들이 많은데요, 그 이유는 프로그래밍에 대한 막연함과, 프로그램 개발 과정에 대한 부족한 이해도등의 이유도 있고, 또 프로그램을 구성하고 있는 기본 법칙이나 내부 원리에 대한 이해가 부족한 이유도 있습니다.

프로그래밍에 대한 막연함이란, 입문서 혹은 문법서에 나온 에제정도만 작성해보았지, 실제 사용할 만한 프로그램 개발 경험이 전무하기 때문에 가지는 부담감이라 할 수 있을겁니다.

프로그램 개발과정에 대한 부족한 이해도란, 프로그래밍이란 단순히 프로그램 언어만 안다고 되는 그런 간단한 것이 아닙니다. 프로그래머는 속도와 메모리의 최적화를 해야할 사명이 있는 것이고, 그것은 하드웨어의 발달과는 무관하게 당연한 것입니다. 프로그램의 개발 과정에는 분석,설계등의 선행 작업이 있고, 분석 과정은 개발할 프로그램이 무엇을 필요로 하고, 무엇이 필요한지, 설계과정에서는 데이터 구조, 사용할 알고리즘등 정해야하죠. 그리고 그것을 문서화하는데, 이러한 과정들이 없이 막연히 어떠한 프로그램을 만든다는 것은 한계가 있고, 그러한 프로그램들은 유지보수, 또는 재사용은 불가능에 가깝다고 봐야합니다. (이 부분은 XP=eXtream Programming과 의견을 달리하는데, 그 방법을 무시하는 것이 아니라, 저는 이 방식이 옳은 방식이라고 보기 때문입니다) 차근 차근 하나 하나 설계해나가고, 작성해나가면 생각보다 쉽게 풀립니다.

한마디로, 소프트웨어 개발 방법론에 대한 이해가 필요하다는 것이죠.
프로그램은 기본적으로 순차적이고, 그 분기는 순환문 또는 조건문을 통해 이루어지고 있고, 처리 과정이 계산을 통해 이루어져있다는 것만 이해한다면, 그다지 어려운 것이 아닙니다.

컴퓨터의 내부 원리에 대한 이해도 기본적으로 되어있어야 어떻게 프로그램을 구성해야 될지 감이 오게됩니다. (추상화=캡슐화를 통해 이런 부분까지 알지 않아도 가능한 시대가 왔다는 사람들도 있지만, 여전히 이 것에 대한 이해는 중요합니다) 우리는 API를 사용함으로써 키보드 장치에 대한 프로그래밍을 하지 않아도, 키보드 입력에 관한 정보를 얻을 수 있고, 어떤식으로 화면이 점을 찍는지 내부원리를 알지 못해도, SetPixel()함수를 통해서 점을 찍을 수 있습니다. 이 것은 매우 유용하고 저를 비롯하여 많은 프로그래머들이 이런 기능을 사용하고 있습니다. 그렇지만, 점을 어떤식으로 화면상이 표시하는지에 대한 원리, 키보드 입력이 어떻게 하여 프로그램으로 메시지로 전달되는지에 대한 이해가 되어있는 사람과, 그렇지 않은 사람과의 실력차이는 분명합니다.

근본 원리를 알고 있는 사람은 좀 더 멀리 내다볼 수 있고, 신 기술에 대한 적응도 빠릅니다. 음악파일의 근본 원리를 아는 사람은, 새로운 포맷을 만들어 낼 수도 있고, 압축과 신장 방법을 알아낼 수 있는등, 그 것의 효율적인 사용법을 알아 낼 수도 있지만, 원리는 모르고 MFC혹은 각종 라이브러리에서 제공하는 함수의 사용법만으로 프로그램을 개발하는 사람은 그 함수가 지원하는 기능으로 프로그램을 구성하는 그 이상은 불가능합니다.

문법을 배워서 실전(프로그래밍)에 사용할 수 있는 것은 가능한 일이고 당연한 일이지만, 더 좋은 프로그램, 더 나은 프로그래밍을 위해서는, 프로그램 언어 이외에도 배워야할 것들이 많고, 그것들을 놓치면 안되는것입니다.

(3) 무엇을 위한 프로그램인가?

프로그램이란 사용자를 위한 것입니다. 우리나라에서는 각종 시스템이 사용자 혹은 고객을 위한 경우가 매우 드뭅니다. (요새는 많이 나아진 편이지만, 그렇지 않은 곳이 여전히 존재하죠.) 기능의 업그레이드나, 버그 수정은 말할거 없이 '당연한'것이고, 부가적으로 사용자 입장을 고려한 프로그램이 개발 되어야하는데, 이 것도 부족한 경우가 상당히 많습니다.

저같은 경우는 자주 사용하는 기기가, 게임기, 컴퓨터, MP3정도인데, 가장 불만인 것이 이 MP3입니다. 해당 개발업체가 아니면 수정할수 없는 펌웨어의 경우 좀 더 신중한 테스트가 필요하고, 사용과정에서 쉽게 눈에 띄는 버그는 존재해선 안됩니다. 자주 발생하지 않는 특정상황이나, 특정 환경에서만 발생하는 버그가 아니라면 말이죠.

어떠한 프로그램이던지, 버그로부터 자유로울 수는 없습니다. 우리가 입문서등에서 가장 처음 접하는 "Hello World!"라는 문장을 출력하는 간단한 프로그램마저도여타 프로그램과 충돌또는 예기치 못할 문제에 이해 오작동할 가능성도 배재할 수 없습니다. 하지만, 눈에 보이는 버그. 테스트 과정에서 쉽게 발견할 수 있는 버그의 경우는 발견즉시 수정을 위한 노력을 기울여야하고, 기업은 자신들이 발매한 프로그램에 대한 문제수정을 위한 인력(프로그래머든, AS직원이든)을 배치해둘 필요가 있습니다. 이런 기본적인 사용자를 위한 배려도 되지 않는다는 것은 상식적으로 이해할 수 없는 일이지만, 이런 일이 빈번하게 벌어지고 있다는 것이 문제입니다.

프로그램 개발 기간에 대한 이야기도 해보자면, 투자자 혹은 기업주의 압박과 독촉에 의하여, 충분한 테스트 기간과, 완성도를 갖추지 못하고 제품이 출시되는 경우가 종종 있습니다. 이런 사례는 게임업계에서 유독 심한걸로 알려져있는데, 요새야 온라인 게임이 대세이다보니 클로즈베타, 오픈베타과정을 거쳐 정식 서비스를 하기에 이런 문제가 적어지고 있는 추세지만, 이전 PC패키지 게임이 주를 이루던 시기에 버그로 인한 사용자의 피해는 말도 못할정도였습니다. (마그나 카르타와, 포가튼 사가는 두고 두고 화자되고 있지요. 창세기전 시리즈, 어스토니시아 스토리,화이트 데이등 국내에서 손꼽히는 명작들도 버그로 인해 그 작품성에 흠을 내곤했으니 말이죠.) 개발자들의 노고와 힘든 상황을 이해 못하는 것은 아니지만, 그것은 자신들의 상황을 핑계로 사용자에게 피해를 입히는 행위로, 이런일이 다신 벌어지지 않아야할 것이고, 그러기 위해서는 개발자들을 위한 환경이 지금보다 많이 개선되어야하겠고, 개발자들도 사용자를 위해 완성도 높은 프로그램을 목표로 꾸준히 노력하는 자세를 가지도록해야한다고 생각합니다.
반응형
C 프로그래머가 알아야할 것들 - Chapter 3. 운영체제와 컴퓨터 원리

김성훈 (sunghun84@nate.com)

Chapter 3. 운영체제와 컴퓨터 원리

(1) 운영체제란?
(2) 이벤트
(3) 프로세스와 쓰레드
(4) 컴퓨터는 계산기다
(5) 디지털과 아날로그
(6) 2D게임이 3D게임보다 빠르다?

(1) 운영체제란?

초기에 컴퓨터는 컴퓨터를 키자마자 애플리케이션 디스크를 삽입해야만 했습니다.
그리고 특별한 경우를 제외하고는 다른 프로그램 사용시에는 재부팅 시켜야만 했습니다.
이 방법은 매우 불편했습니다. (비디오 게임기들은 아직 이 방식을 채용하고 있는 경우도 있습니다.)

그래서 유닉스, MS-DOS등의 운영체제가 나오게 됐습니다. (많은 분들이 오해하시는 것이 있으신데, MDIR은 운영체제가 아닙니다. 인터페이스를 제공해주는 프로그램이죠)
각 운영체제하에 프로그램을 구동시킨후, 프로그램 종료시에는 그 운영체제로 돌아오게끔 하는 방식을 취한것이죠.

또한 프로그래머를 위한 장치 지원 인터페이스를 제공해 주기도 했습니다. (MS-DOS에서는 매우 미약했지만, 윈도우로 넘어와서는 극찬 받는 기능이죠)
이전에는 그래픽 카드나 프린터, 사운드 카드마다 출력을 지원해주는 방식이 달랐습니다. 점 하나 찍거나 소리를 내는방법이 하드웨어에 따라 달랐던것이지요.

그래서 각 하드웨어 장치(여기선 일반적으로 그래픽 카드와 사운드 카드를 의미합니다)를 컨트롤 하기 위한 작업들은 프로그램 개발 업체마다 따로 이루어져야했고, 그렇기에 발매된지 얼마 되지 않은 하드웨어나, 판매량이 적은 비 인기 하드웨어의 경우는 지원되지 않는 경우가 대부분이었습니다.

상황이 이렇다보니 프로그래머들은 프로그래머 나름대로 다수의 하드웨어 장치를 지원하려다 보니 힘들었고, 사용자들은 사용자 나름대로 내 하드웨어가 내가 사려는 소프트웨어의 지원이 되는지 곰곰히 따져봐야하는 불편한 상황이었죠.

물론 DOS시절에도 VESA (Video Electronics Standard Association)등에서 그래픽 카드의 표준화를 시키긴했지만, 호환성 문제는 고질적인 문제였습니다.

그래서 나온 것이 바로 윈도우입니다.

대게 MS-DOS와 비교되는 윈도우의 장점으로 GUI (Graphic User Interface)를 꼽지만, 플러그 앤 플레이나, API (Application Programming Interface)도 빠지면 안될정도로 중요한 요소입니다.

플러그 인 플레이는 자동 하드웨어 장치 인식 기능으로, MS-DOS의 단점을 보완해주기에 충분했습니다.

API는 프로그램 개발용 함수 모음으로, 점찍기, 타이머, 텍스트 출력, 마우스 입력, 키보드 입력 등등 프로그램 개발에 필요한 기본적인 기능을 지원해준 것입니다. 각 프로그램마다 자체적으로 지원하기 위해 엄청난 시간투자를 해왔던 작업들을 운영체제가 지원해줌으로써, 프로그램 개발이 한결 편해진것입니다.

여기서는 주로 윈도우의 예를 들겠지만, 다른 운영체제도 용어만 틀리지 원리는 비슷하니 이해하시는데 무리는 없을겁니다.

(2) 이벤트

MS 윈도우(이하 윈도우)에서 이벤트란 윈도우에서 발생하는 정보들을 말합니다.
즉, 마우스 이동, 마우스왼쪽 버튼 클릭, 마우스오른쪽 버튼 클릭, 키보드 누름, 키보드 뗌, 문자 키 누름,프로그램 시작, 프로그램 종료등 다양한 상황마다 이벤트가 발생하는데, 그렇게 발생되는 이벤트를 메시지로 프로그램에 보내주는 것입니다.

그 메시지를 받아서 필요한 메시지를 이용하여 처리해주는 것이 윈도우 프로그래밍에서의 이벤트 프로그래밍 방식이라고 합니다.

하드웨어 장치를 어느 회사 제품이냐, 어떠한 식으로 제어해야 하는가를 프로그래머가 신경써야한다면, 운영체제가 이벤트를 보낼수 없겠죠? 하드웨어 제어를 프로그래머가 만든 응용 프로그램이 한다면, 운영체제가 그 하드웨어에서 발생한 이벤트를 보낼 수 없을겁니다. (응용 프로그램 내부에서 그 하드웨어를 제어하며 이벤트 발생시 메시지를 보낼 수는 있겠지만요.)

하드웨어의 접근을 운영체제가 직접 관리하기 때문에, 프로그래머들은 그 부분에 신경않아도 되고, 이벤트 발생시 그 이벤트를 메시지로 받아서 처리하기만 하면 되는것이죠

(3) 프로세스와 스레드

간단히 말해서 프로그램이 실행되고 있는 것을 프로세스라 합니다.
이런 프로세스를 (어느정도는 의미가 다르지만) 태스크라 부르기도 하죠.
우리가 흔히 들어온 멀티 태스크란, 다중 프로그램 구동이라고 생각해도 될겁니다.
이 멀티 태스크를 통해 우리는 동시에 프로그램이 실행되고 있구나라고 생각하시는 분도 많을겁니다.
그러나 실상은 눈깜짝할사이에 여러개의 프로그램이 번갈아가면서 실행되고 있기에 우리는 동시에 작동하는걸로 생각되는것이죠.

스레드는 프로세스 내부의 실행 단위를 말합니다. 프로세스내에서 스레드가 여러개 존재하여 처리되는 것을 멀티 스레드라 하는것이죠.
예를 들면 메신져에서 음악 재생하면서 채팅(메시지 입력)을 할 수 있는 것은, 음악 재생과 채팅기능이 스레드 단위로 구동되기 떄문입니다.
만약 이 프로그램이 태스크 단위로 구동되었다면, 그 프로그램을 이용하여 음악을 재생하는중에는 그 프로그램을 이용한 채팅이 불가능하게 됩니다. 음악이 자동으로 멈추거나, 중지 시키지 않는다면 말이죠.

(4) 컴퓨터는 계산기다

컴퓨터는 계산기라고 한다면, 아니? 계산기에서 동영상도 볼 수 있고, 3D게임도 할 수 있고, 그림도 볼 수 있고, 음악도 나온다는 게 말이되냐고 하시는분도있으실겁니다.
하지만 사실입니다. 입력장치(키보드,마우스)나 출력장치(모니터,프린터,스피커)는 주변기기입니다. 컴퓨터는 입력장치로부터 신호를 받아 그 것을 비트로 변환하여 프로그램(혹은 운영체제)에 전달한후 그에 맞는 처리를 하다가, 출력 장치로 출력해줄 필요가 있을 때 출력장치에 신호를 보내서 출력해주게 됩니다. 이 과정들도 비트로 이루어져있습니다. 컴퓨터는 이진수(on, off)밖에 모르기 때문입니다.

초기 컴퓨터(최초의 컴퓨터로는 애니악으로 잘못 알려져있는데 실은 앨런 튜닝이 세계 2차대전에서 독일군 암호 해독을 위해 만들어진 콜로서스입니다)는 연산속도도 느리고, 연산을 위한 저장 장소(덧셈을 해주려면, 최소 두 개의 저장 장소가 필요합니다. 첫 번째값, 그리고 더해줄 두 번째값. 세 번째 값은 결과값이 필요하다고 생각하실지도 모르겠습니다만, 첫 번째 값과 두 번째 값은 연산후에는 필요가 없어지기에 둘중 아무곳에나 결과값을 저장하면 됩니다.)가 작았기 때문에 간단한 처리밖에 못했습니다. 그런 계산들을 하기 위해 컴퓨터가 만들어졌고, 그것이 당연했죠.

그러나, CPU와 RAM의 속도와 용량이 증가함에 따라서, 컴퓨터의 연산속도가 급격히 상승했습니다. 그에 맞춰 소프트웨어와 입출력 장치들도 발전을 거듭하면서, 비트 단위로 이루어진 데이터를 이용하여 동영상을 재생하고, 그림 파일을 보여주고, 3D게임도 가능하게 된 것입니다. 동영상 파일이나, 그림 파일등이 비트를 기반으로 한 정보로 이루어졌었다고 배웠죠? 그런 데이터들을 출력하는 과정도, 출력 장치 혹은 주변 기기(그래픽 카드나 사운드 카드)의 도움을 받기는 하지만, 일반적으로 컴퓨터의 연산기능을 이용합니다.

그렇기에 컴퓨터는 매우 빠른(이런 말로 표현하기엔 터무니없이 부족하지만) 계산기라고도 할 수 있는것입니다.

(5) 디지털과 아날로그

컴퓨터를 이해하기 위해서 우리는 디지털과 아날로그에 대해 이해할 필요가 있습니다. 왜냐하면 컴퓨터는 수치를 기반으로 한 디지털에 기초하고 있기 때문입니다. (그렇기 떄문에 수학과도 큰 연관이 있습니다.)

아날로그란 연속된 정보를 말합니다. 물론 아날로그도 수치로 표현할 수 있지만, 좀 더 세분화된 정보를 다룬다는 것이 차이점입니다.

그에 반해 디지털은 단절된 정보를 다룹니다. 바로 중간값을 취하지 않는다는 얘기입니다. 예를 들면, 컴퓨터에서 작동하는 시계를 직접 만든다고 생각해봅시다. 이 시계는 큰 기능이 필요 없기 때문에 초까지만 다룬고 한다면, 0.1초나, 0.01초등의 정보를 다루지 않게 됩니다. 물론 0.01초까지 다룬기로 했다면, 이야기가 또 틀려지지만요. 결과적으로 중간값을 다루지 않죠.

컴퓨터는 디지털로 이루어져있지만, 아날로그 데이터도 다룰 수 있기 때문에 이 두 방식의 차이점을 기억해두도록 합시다.

(6) 2D게임이 3D게임보다 빠르다?

우리가 흔히 하는 착각은 2D게임이 3D게임보다 빠르다. 혹은 2D게임은 저 사양이다라는 생각입니다.

이것은 지금까지 배운 논리를 적용하면 쉽게 알 수 있는 것인데, 컴퓨터에서 구동되는 모든 과정은 계산에 의한것입니다.

그렇기 때문에, 2D던, 3D던 게임의 속도는 얼마만큼 많은 계산을 필요로 하는지에 달려있는 것이지, 같은 (혹은 비슷한) 기능을 가진 게임이라면 2D와 3D의 기본적인 연산 속도의 차이(3D는 일반적으로 폴리곤=다각형으로 이루어져 있기에 기본적으로 이루어져야할 연산이 많고, 실수 연산이 많이 필요하기 때문에 더욱 2D보다는 확실히 연산할 것이 많습니다.)는 있을 수 있어도, 게임의 규모가 커지다보면 오히려 2D게임이 느려지는 경우가 발생하게 됩니다.

3D게임의 경우는 모션에 따른 변화되는 정보를 가지고 원본 데이터에서 변화시키는 방식인데 반해, 2D의 경우는 캐릭터의 모든 행동 정보를 이미지 파일로 가지고 있어야하기 때문이다.

그리고, 시각적 효과가 거의 없는 게임인 FM시리즈(Football Manager의 약자로, 원래는 Championship Manager 시리즈였던 게임)의 경우 왠만한 3D게임보다도 속도가 느린데, 이 것은 이 게임이 처리해야 될 데이터가 많기 때문입니다. 모든 경기 결과는 랜덤이 아닌, FM시리즈의 규칙(데이터에 기반하되, 그 데이터가 전부가 아닌)에 따른 결과가 나와야 하기 때문에, 모든 경기 결과를 계산해야되는데, 그 계산해야 될 데이터가, 왠만한 3D게임보다 많기 때문에 느린 것입니다.

어때요? 컴퓨터의 속도에 대한 감이 오시나요?
반응형
C 프로그래머가 알아야할 것들 - Chapter 2. 비트의 법칙

김성훈 (sunghun84@nate.com)

Chapter 2. 비트의 법칙

(1) 비트가 뭐지?

(2) 프로그램 작성하는데 비트가 왜 필요한걸까?

(3) 데이터형

(4) 바이트로 구성된 파일

(5) 비트 연산자

(6) 간단히 구현해보는 압축


(1) 비트가 뭐지?

비트란 Binary Digit. 즉 이진수의 약자입니다. 한마디로 이진수를 의미한다고 할 수 있죠. 비트는 컴퓨터에서 제어 가능한 데이터의 최소단위입니다. 하지만, 컴퓨터에서 입출력할때 사용하는 최소 단위는 바이트죠. 둘다 최소단위인데...저게 뭔소린가...하실분도 있을겁니다.

간단히 설명하자면, 비트란 저번 강좌에서 배웠던 이진수 10을 2비트(2진수 2자리 수이기에)로 표현 가능하고 제어 가능하단 의미고, 바이트는 비트 8개가 모여서 구성된 것이 1바이트로, 파일이나 데이터형의 최소단위로 쓰입니다.

(2) 프로그램 작성하는데 비트가 왜 필요한걸까?

많은 분들이 프로그램을 만드는데 왜 비트가 필요한지 의문을 가지실거라고 생각합니다.
그런데, 위에서 설명했듯이 데이터의 최소단위는 비트입니다.

최적화 된 프로그램이란 메모리와 속도 모두 만족 시키는 프로그램을 말하는데, 그것을 만족하기 위해선 비트 단위 연산 또는 처리가 필수이기 때문입니다.

하드웨어의 발전에 따라 빠른 속도보다는 쉬운 사용법과, 유지 보수 또는 코드 재사용이 쉬운 프로그램이 인기를 끌고 있는건 사실입니다.

하드웨어가 발전함에 따라 그 하드웨어의 기능을 활용하기 위한 기술이 적용되다보니 여전히 빠른 프로그램을 작성할 필요성은 존재합니다.
메모리와 비트 단위 연산 권한이라는 강력한 기능을 부여받은 이상 비트에 대해 알아두는 것은 당연하다고 할 수 있습니다.

(3) 데이터형

C언어에서는 다양한 데이터형을 제공합니다.
여기서는 주로 사용되는 몇 개의 데이터형만 가지고, 비트와 관련해 알아보도록 하죠.

흔히 문자형으로 알려진 char (캐릭터형)는 1바이트로 이루어져있습니다.
일반적으로 1바이트=8비트이므로, 0000 0000 이렇게 8자리 2진수만큼 사용할 수 있는데, 2의 7승은 256 (첫 자리가 2의 0승이므로, 8비트의 경우 2의 7승만큼 사용 가능합니다.)
이므로, 부호가 없는 경우는 0~255, 부호가 있는 경우는 -128~127(C표준에서는 -127까지 보장해준다)까지 사용 가능하게 되는겁니다. 부호가 있는 경우는 최상위 비트를 부호 비트로 사용하게 되므로 사용 가능한 수의 범위가 반으로 줄 게 됩니다.

2바이트 데이터형인 short int 도, 2의 15승인 65536만큼의 수를 사용할 수 있는데, 부호 없는 경우 0~65535, 부호가 있는 경우 -32768~32767까지의 수를 사용할 수 있는거죠. 32비트 데이터형도 마찬가지로 2의 31승만큼 사용 가능하다고 알고 계시면 됩니다.

데이터형에 따른 사용범위는 곧 대중화될 예정으로 알려져있는 64비트 운영체제에서 주로 사용될 64비트 데이터형도 마찬가지겠구요.

(4) 바이트로 구성된 파일

비트 8개가 모여서 만든 바이트가 모여서 만들어진 것이 파일입니다.
우리가 흔히 사용하는 이미지 파일들도 알고보면 색상 정보를 담고 있는 바이트의 집합입니다. 여기서 조금 부연 설명을 하자면, BMP(비트맵)파일의 경우는 헤더나 컬러 테이블 정보도 담고 있긴하지만, 일반적으로는 RGB색상값만 가지고 있다고 보시면 됩니다. RGB 색상 값은 Red 1바이트, Green 1바이트, Blue 1바이트씩 저장됩니다.

색상값을 가지고 있다가 프로그램에서 BMP파일을 읽어들였을 때, 색상 정보를 읽어서 RGB색상값을 조합한후 점을 찍어서 (BitBlt함수로도 찍을 수 있는데라고 생각하실분도 있으시겠지만, 그 함수도 결국 내부적으론 점을 찍어서 표현해줍니다.) 그 색상을 모니터에 표현하도록 명령을 내려주기 때문에, 우리 눈에는 컬러 이미지를 볼 수 있는겁니다.

저장되어 있는 RGB색상값을 아무런 압축도 하지 않고 모두 가지고 있는 경우가 위에서 설명한 BMP파일 포맷이고, JPG의 경우는 고도의 압축 기법(압축률도 지정가능합니다)을 통해서 용량을 줄였지만 결과적으로는 색상값을 가지고 있다 신장(압축해제)후 화면에 뿌려주는 원리는 비슷합니다.

벡터 그래픽 파일의 경우에는 점,선,곡선등의 정보를 저장하고 있는 포맷이지요. 그래서 화면에 축소나 확대에서도 같은 이미지를 볼 수 있는것이지요.

동영상 파일도 많은 양의 그림 정보를 담고 있다가, 1초에 몇번 이상(1초에 몇 번 화면이 갱신되는지를 프레임이라고 하는데, 초당 24내지 30프레임은 되어야 깜빡임이 사람 눈에 보이지 않는다고 합니다. 일반적으로 모니터의 경우는 60번 이상 갱신되고 있죠)갱신되는지에 따라 그것을 재생시켜줍니다.

음악파일도 마찬가지로, 소리 정보를 디지털값(수치값)으로 가지고 있다가, 그 정보를 재생시간에 맞춰 재생하는 방식을 취하고 있죠.

스타크래프트 리플레이 파일의 경우에도 비슷하게 첫 위치값을 저장한후에 거기서 변화한 값과 내린 명령,시간값들을 저장했다가, 리플레이 메뉴에서 재생시 그 정보를 바탕으로 게임을 재생시키는 것이지요. 그래서 리플레이 파일의 용량이 그다지 크지 않은것입니다.

게임 세이브 파일의 경우에도, 그 캐릭터의 레벨, 무기 일람, 체력, 공격력, 방어력 등의 파라미터와, 그 캐릭터의 현재 위치, 플레이 시간 등의 정보를 저장하고 있습니다. 물론 에디트가 힘들 게 하기 위해 단순한 파일 구조(순차적)으로 구성하지 않는 경우가 대부분이긴하지만요.

모든 데이터는 바이트 단위로 저장된다는 것. 그 바이트를 구성하고 있는 것은 비트라는 것 이것이 핵심입니다.

(5) 비트 연산자

자 비트에 대해 배웠으니 비트 연산을 한번 해봐야겠죠?
비트연산은 부울대수를 배우셨던 분들은 매우 친숙하실겁니다.

비트연산에 사용되는 비트연산자란 논리 곱 (&) , 논리 합 (|), 논리 부정 (~), 베타적 논리합(^)이 있습니다.
여기서 시프트 연산자 좌측 시프트 (<<) 우측 시프트 (>>)도 있죠.

논리곱은 둘다 1일 때만 참(1)이 됩니다.

1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

논리합은 둘중에 하나라도 1이면 참(1)이 됩니다.

1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0

베타적 논리합은 두수의 값이 달라야만 참(1)이 됩니다.

1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0

논리 부정은 0은 1로, 1은 0으로 만들어줍니다.

~1 = 0
~0 = 1

이 되는 것이죠.


좌측 시프트 연산자는 비트를 왼쪽으로 옮겨주고, 빈자리엔 0을 넣어줍니다.

0011 0010 << 2

이 연산후에 0011 0010 (10진수 50)에서 1100 1000 (10진수 200) 로 바뀝니다.
왼쪽으로 2번 이동했더니 수가 4배가 되었죠?
잘 보시면 시프트 연산자 한번당 수가 2배가 된다는 것을 아실수 있을겁니다.


우측 시프트 연산자는 비트를 오른쪽으로 옮겨주고, 빈자리엔 0을 넣어줍니다.

0011 0011 >> 2

이 연산후에는 0011 0010 (10진수 50)에서 0000 1100 (10진수 12) 가 되었습니다.
좌측 시프트 연산자와 마찬가지로, 우측으로 2번 이동했더니 수가 4분의 1이 되었죠? (12.5여야하지만, 소수점 이하는 버립니다)


(6) 간단히 구현해보는 압축

압축방법에는 런 렝스 코드, 허프만 코드등 다양한 방법이 있는데요, 여기서는 시프트 연산자와 비트 연산자의 조합으로 간단한 압축을 구현해보겠습니다.

아까 데이터를 압축하려면 현재 데이터에서 변화한 정보를 담는 것이 좋다고 했었죠?

그 원리를 이용하는 것입니다.

주로 다른 파일보다는 이미지, 음악 파일, 동영상 파일등에 사용되는 데이터 압축의 원리는 이 바이트를 기본으로 한 데이터들이 비트로 이루어져있다는 것이 핵심입니다.

어떤 데이터든 현재 상태와 다음상태의 값을 모두 가지고 있는 것보다 현재값 (초기값 또는 현재값은 모든 데이터를 기록해둬야 합니다)을 기록해둔후 차후에 변화되는 값을 저장하게 되면 적은 용량으로 데이터를 기록할 수 있을겁니다. 일반적으로 한 픽셀값은 1바이트로 저장된다고 얘기했었죠? 그런데 방금전 픽셀값은 10이고 다음 좌표값은 30이면 20만 저장하는 것입니다. 그렇게 되면 6비트 (2의 5승이 32이니, 0~31까지 사용가능)내에 데이터를 담을 수 있습니다. 그리고 나서 좌측 시프트 시키면 하위 2비트에 0이 들어오겠죠? 그 2비트에 다른 정보를 담게 되면 전체적으로 용량이 줄 게 됩니다. 이것이 바로 데이터 압축의 원리죠.
반응형
펌 ) Devpia VC++ 강좌 엘키(sunghun84)
C 프로그래머가 알아야할 것들 - Chapter 1. 진법

김성훈 (sunghun84@nate.com)

Chapter 1. 진법

(1) 왜 진법에 대하여 배워야 하는가?
(2) 2진법
(3) 8진법
(4) 16진법

(1) 왜 진법에 대하여 배워야하는가?

진법이 뭘까요? 바로 수를 세는 단위입니다.

진법중에는 우리가 흔히 사용하고 있는것은 60진법(초,분을 재는데에 쓰이는 방식. 0~59까지 세고, 60이 되었을떄 자리올림하는 수체계)이나, 10진법(0~9까지 세고, 10이 되었을 때는 자리올림을 하는 우리가 기본적으로 수를 세는방식)등도 있습니다.

그 중에서도 여기서 배울 것은 2진법,8진법,16진법이 있는데요, 이 진법들에 공통점은, 모두다 2진수와 연관이 있는 진법이라는 겁니다.

왜 2진수(혹은 2진수와 관련이 깊은 진법에 대해 알아야 될까요? 그 이유는 컴퓨터는 0과 1밖에 구분하지 못하기 때문입니다. 컴퓨터는 전류가 흐를 때, 흐르지 않을때를 통해 구분하는것으로써, 2진수와 일맥 상통하게 됩니다. 2진수는 또한 참과, 거짓으로 상태를 표기하는 부울대수와도 연관이 깊죠.

그런데 게임과, 메신져 이런것들이 0과 1만으로 실행 되는거라고? 거짓말 아냐? 라고 생각하실분들도 계실텐데요, 그것에 대해서는 2번째 챕터 비트의 법칙에서 자세히 설명하도록하겠습니다.

16진수는 2진수 매우 연관이 깊어 2진수를 4개 단위로 읽을 때 유용하게 이용되기도 합니다. 8진수는 16진수와 비슷하게 2진수를 3개 단위로 읽을 때 혹은, 10진수에서의 상호 변환등에서 자주 사용되게 됩니다.

또한, 컴퓨터는 수를 계산(연산)을 하는 기계입니다. 그렇기 때문에, 수를 표현하는 방식(그 중에서도 컴퓨터가 사용하는 2진수와 관련이 깊은)에 대해서 알아둬야할 필요가 있는 것입니다.

(2) 2진법

아까 언급했듯이, 컴퓨터는 0과 1밖에 수를 인식하지 못합니다.

2진수는 0과 1로써 수를 세는 방법입니다.

자리수가 2가 되었을때 자리 올림을 하여 10으로 표시하는 수체계죠.

2진수는 표현할 때

0011 (2)

위와같이 표현하곤합니다. 괄호는 반드시 써야하는건 아니고, 혼돈의 여지가 있을 때만 사용하곤합니다.

2진수
10진수

0000
0

0001
1

0010
2

0011
3

0100
4

0101
5

0110
6

0111
7

1000
8

1001
9

1010
10


위의 표를 보시면, 대강 어떤지 감이 오실겁니다.
10진수가 11일때 2진수로는 몇이 될까요?
네 그렇죠~ 1011이 되는겁니다.


그런데...2진수가 너무 읽기 힘들다구요?

8 4 2 1
1 0 0 1 (자릿수가 1일 때마다, 머리위에 위치한 수만큼 더해줍니다)



8에 위치한 2진수가 1이니 8, 4와 2자리에 위치한 수는 0이니 그냥 내버려두고, 1의 자리에 위치한 수가 1이니 1. 그래서 나온수인 8과 1을 더하면 9가 되는거죠. 쉽죠?

마찬가지로 이진수가 커질때에도

128 64 32 16 8 4 2 1
0 0 0 0 0 0 0 0

이런식으로, 수가 하나 증가할때마다 수가 배로 증가하게 됩니다. 이렇게 하니 2진수 읽는 법은 감이 오죠?


그런데...음수를 표현할땐 어떻게 해야 할까요?

-0011 이런식으론 안되냐구요?

아쉽게도, 10진수와는 달리 -부호를 사용할수 없습니다.

그래서 최상위 비트(가장 앞에 있는 수를 최상위 비트=MSB라 부르게 되죠. 비트란 다음 강좌에서 배우게 될 바이너리 디지트=이진수의 약자입니다)를 부호 비트로 사용하게 되는것이죠.

최상위 비트가 0이면 양수, 1이면 음수로 표현 하기로 했습니다.

0001은 1, 1001은 -1 이렇게 말이죠.


그런데 이것은 문제가 있습니다.

2진수의 계산이 힘들어진것이죠.

1001 + 0001 = 1010

(-1) (1) (0 이어야 함)



2진수의 덧셈에서 자리 올림수가 발생하면 일반 덧셈에서 자리 올림수와 마찬가지로 처리해주면 됩니다.

그건 그렇고, 뭔가 이상하죠? 1010이라니...최상위 비트가 1이니 음수인거 같고, 뒤가 010이니 -2인거 같은데... 어? 0이 아니네요?

그래서 나온 것이 1의 보수입니다.


1의 보수란, 음수 표현시에 0을 1로, 1을 영으로 모든 수를 반전 시키는것이죠.

1의 보수로 표현하자면,

0001은 1, 1110은 -1이 되는거죠.


자..아까 계산이 잘못되었던 -1에서 1을 더해볼까요?

1110 + 0001 = 1111

(-1) (1) (0이어야 함)

어? 여전히 뭔가 이상하죠?? 최상위 비트가 1이니 음수 인거 같고....111이라면 -7? 왜 또 0이 아니지? 또 문제가 있었습니다.


그래서 2의 보수란 것이 나오게 된것입니다.

2의 보수란 0을 1로, 1을 0으로 모두 바꿔준후에 1을 더하는 방법입니다.

0001은 1, 1111은 -1이되는것이죠.



그럼 다시...계속 잘못되었던계산인 -7에서 7을 더해봅시다.

1111 + 0001 = 10000

(-1) (1) (0이어야 함)

음...이번에도 최상위 비트가 1이군요. 그럼 음순가? 그런데 뒤가 0000이네요? 하지만 이 계산은 맞습니다. 맨앞에 초과된 1은 무시하기 때문입니다. 그래서 0000 즉 0이 되는것이죠.



왜 2의 보수가 사용되는지 아셨겠죠?

참고로 1의 보수의 또다른 문제점은 0이 두 개이기 때문입니다.

1111 1111과, 0000 0000 각각 음수 0 양수 0을 의미합니다.

이에 비해 2의 보수는 0000 0000은 당연히 0이고, 0의 1의 보수인 1111 1111에서 1을 더하면 1 0000 0000이 되고, 이때 초과된 1은 무시하게 되므로 0은 양수 0 하나만 남게 되는것이죠. 이로써 아까 -1에서 1을 더하는 계산에서의 초과된 1을 무시하는 이유도 함께 이해가 되시죠?



(3) 8진법

8진법이란 0~7로 수를 세는 방법입니다.

8진수는 자릿수가 7이 넘어 8이 되었을때는 자리 올림하여서, 10으로 표현하는 수체계입니다.

8진수를 표기할때

12 (8)

이런식으로 작은 괄호로 8을 붙여서 표현하곤 합니다. 2진수와 마찬가지로 꼭 표기해줄 필요는 없습니다.
C언어등의 고급 언어에서 사용시에는 0(숫자 0)을 붙여서 사용하죠.

아래 표를 보시면, 2진수, 8진수, 10진수의 관계를 아실수 있을겁니다.

2진수
8진수
10진수

0000
0
0

0001
1
1

0010
2
2

0011
3
3

0100
4
4

0101
5
5

0110
6
6

0111
7
7

1000
10
8

1001
11
9

1010
12
10




8진수는 그 자체적인 의미도 중요하지만, 2진수에서의 변환이 매우 편리하다는 장점이 있습니다.

바로 수를 세 개단위로 묶어서 표현하면 되는것이죠.



4 2 1
1 1 1 (7입니다)

8진수로 바꿀때는, 4+2+1=7이 되는거죠.



이런식으로

4 2 1 4 2 1
1 1 0 0 1 1

4+2=6 2+1=3

8진수로 63이 되는겁니다.


10진수 524을 8진수로 구해봅시다.

524 / 8 = 65 나머지 4

65 / 8 = 8 나머지 1

8 / 8 = 1 나머지 0

1 / 8 = 0 나머지 1

1014(8)

즉, 나머지를 역순으로 나열하면, 10진수에서, 8진수를 구할 수 있습니다.



다시 8진수 1014를 10진수로 바꿔봅시다.

1014(8) = 4 x 8에 0승 + 1 x 8에 1승 + 1 x 8에 3승
= 4 x 1 + 1 x 8 + 1 x 512
= 4 + 8 + 512
= 524

524(10)

각 자리수에 맞춰서 곱해주면, 8진수에서 10진수로 변환되는겁니다.


(4) 16진법

16진법이란, 수를 0부터 15로 세다가, 16이 되어 자리올림할땐 10으로 표현하는 방법을 말합니다.

그런데...뭔가 조금 이상하죠? 0~15로 수를 센다면, 0~9는 괜찮겠지만, 10부터 15는 자리 올림된 10~15와 같게 되니까요.

그래서 10~15는 A~F로 표현하게 됩니다.

16진수도 8진수 2진수와 마찬가지로,

AF (16)

위와 같이 표기할 수 있습니다.

C언어등 고급언어에서 16진수 표현시 숫자 앞에 0x (숫자 0과 알파벳 x)를 붙여서 사용합니다.


아래 표를 보시면 진법에 따른 수 표현 방식을 이해하실수 있을겁니다.

2진수
8진수
16진수
10진수

0000
0
0
0

0001
1
1
1

0010
2
2
2

0011
3
3
3

0100
4
4
4

0101
5
5
5

0110
6
6
6

0111
7
7
7

1000
10
8
8

1001
11
9
9

1010
12
A
10

1011
13
B
11

1100
14
C
12

1101
15
D
13

1110
16
E
14

1111
17
F
15


16진수도 8진수처럼 2진수에서의 변환이 쉽다고 했었죠?

16진수도 2진수를 8진수로 변환하는 것과 비슷합니다. 2진수 4개씩 묶어서 16진수로 변환하면 되는것이죠.

8 4 2 1
1 1 0 0 (12입니다)

8+4=12인데, 12는 알파벳 C로 표현하기로 했으니,

8+4=C로 표현할수 있는것이죠.

8 4 2 1 8 4 2 1
1 0 1 0 0 0 1 1

8+2=A 2+1=3

A3으로 4자리씩 묶어서 변환함으로써 2진수를 좀 더 읽기 쉽게 하는것이죠.

16진수는 또한 니블(Nibble)에 기본 단위가 됩니다. 니블이란 비트를 4개씩 묶어서 표현하는 방식으로써, 16진수와 같은 방식으로 표현할 수 있게 되죠.

이것으로 각 진법에 대하여 알아봤는데요, 2진수, 8진수, 16진수가 모두 연관이 있다는 것을 알수 있었습니다. 특히나 2진수는 어떤수로나 변환이 쉽다는 것도 말이죠.
반응형
메인 윈도우 맹글고
메인 프로시저에서
MDICLIENT 윈도를 맹글다.

자식창은
MDICLIENT 윈도우에다가 WM_MDICREATE메세지를
보내는디 lParam에다가

MDICREATESTRUCT 구조체를 집어 넣는다.

메인 프로시저는 wndProc가 아니고 DefFrameProc
자식은 DefMDIChildProc
 

'프로그래밍 > MFC' 카테고리의 다른 글

CCombobox Edit 창 크기 설정  (0) 2015.03.26
MFC 디렉토리 생성  (0) 2013.08.14
쓰레드 생성 (펌글)  (0) 2013.08.14
쓰레드 생성.  (0) 2013.08.14
유니코드  (0) 2013.08.14
반응형
API 에서 지원되는 함수들은 어느정도 제약이 있기 떄문에......



좀 짜증났거든요.....  



물론 만드는게 어렵진 않지만 자주 사용해야 하므로 아예 보관하시고 카피해서 사용하시면 편하실거 같아서요...







#include <windows.h>

#include <stdio.h>

#include <string>



using namespace std;



/*



기존 디렉토리가 있을경우 안만들어지고 없으면 만든다. 부모디렉토리가 없어도 생성가능



*/

void CreateDir(char* Path)

{

  char DirName[256];  //생성할 디렉초리 이름

  char* p = Path;     //인자로 받은 디렉토리

  char* q = DirName;  



  while(*p)

  {

      if (('\\' == *p) || ('/' == *p))   //루트디렉토리 혹은 Sub디렉토리

      {

          if (':' != *(p-1))

          {

              CreateDirectory(DirName, NULL);

          }

      }

      *q++ = *p++;

      *q = '\0';

  }

  CreateDirectory(DirName, NULL);  

}





/*



하위디렉토리를 제외한 해당 디렉토리 모든 파일들을 제거



*/

void DeleteAllFiles(char* folderPath)

{

  char fileFound[256];

  WIN32_FIND_DATA info;

  HANDLE hp;



  sprintf(fileFound, "%s\\*.*", folderPath);

  hp = FindFirstFile(fileFound, &info); //디렉토리에 파일이 있는지 첫번째 파일만.

  do

  {

      sprintf(fileFound,"%s\\%s", folderPath, info.cFileName);

      DeleteFile(fileFound);



  }while(FindNextFile(hp, &info));  //다른 파일이 있을때 까지



  FindClose(hp);

}





/*



해당 하는 디렉토리에 파일이 존재해도  디렉토리가 비어있지 않아도 지울수 있다 .



*/



void EmptyDirectory(char* folderPath)

{

  char fileFound[256];

  WIN32_FIND_DATA info;

  HANDLE hp;



  sprintf(fileFound, "%s\\*.*", folderPath);

  hp = FindFirstFile(fileFound, &info);   //디렉토리에 파일이 있는지 첫번째 파일만.

  do

  {

      if (!((strcmp(info.cFileName, ".")==0)||(strcmp(info.cFileName, "..")==0)))

      {

          if((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)==FILE_ATTRIBUTE_DIRECTORY)  //Sub디렉토리가 존재하는경우

          {

              string subFolder = folderPath;

              subFolder.append("\\");

              subFolder.append(info.cFileName);

              EmptyDirectory((char*)subFolder.c_str()); /// {return (_Ptr == 0 ? _Nullstr() : _Ptr); }

              RemoveDirectory(subFolder.c_str());

          }

          else

          {

              sprintf(fileFound,"%s\\%s", folderPath, info.cFileName);

              BOOL retVal = DeleteFile(fileFound);

          }

      }



  }while(FindNextFile(hp, &info));



  FindClose(hp);

}

/////////////
아래의 글을 보다가 우연히 제가 가지고 있던 팁중에서도 아래와 비슷하다고 생각하는 내용이 있어,



생각난김에 하나 올려봅니다.



아래의 디렉토리 생성과 유사한데...



만드려고하는 폴더를 full path로 그냥 넣어주기만하면 상위가 없으면 같이 만들어줍니다.



#include <io.h>



BOOL CreateDir(char* pszDir)

{

  BOOL bRet = false;

  int nLen = strlen(pszDir);

  char* pszSubDir = NULL;



  _finddata_t fdata;

  long hFind;

  for (int i = nLen - 1; i >= 0; i--)

  {

      if (pszDir[i] == '\\')

      {

          pszSubDir = new char[i+1];

          memset(pszSubDir, 0, i+1);

          memcpy(pszSubDir, pszDir, i);

          if (hFind = _findfirst(pszSubDir, &fdata) == -1L)

          {

              if (!CreateDir(pszSubDir))

              {

                  delete pszSubDir;

                  return bRet;

              }

          }

          delete pszSubDir;

          break;

      }

  }

   

  bRet = ::CreateDirectory(pszDir, NULL);



  return bRet;

}
////////////////////////////////
Shell 명령을 쓰면 더욱 간단해 집니다.
제가 만든 코드입니다.

bool CheckDirectory(LPCTSTR dir)
{
if (::PathFileExists(dir) && ::PathIsDirectory(dir))
return true;

TCHAR parent[MAX_PATH] = _T("");
lstrcpy(parent, dir);
::PathRemoveFileSpec(parent);
if (CheckDirectory(parent))
return (::CreateDirectory(dir, NULL) != FALSE);
return false;
}
/////////////////////
int SHCreateDirectory(HWND hwnd, LPCWSTR pszPath);

'프로그래밍 > MFC' 카테고리의 다른 글

CCombobox Edit 창 크기 설정  (0) 2015.03.26
WIN32 API프로그래밍에서 MDI에서 자식 생성.  (0) 2013.08.14
쓰레드 생성 (펌글)  (0) 2013.08.14
쓰레드 생성.  (0) 2013.08.14
유니코드  (0) 2013.08.14
반응형
// 데브피아(devpia) 가욱현, 정대원 님의 글을 토대로 함다.
1. 개요

현재 대부분의 OS는 프로세스 스케쥴링에 의해 프로그램의 멀티태스킹(Multi-tasking)을 지원하고 있다.
멀티태스킹이란 실행되고있는 프로그램을 일정 단위로 잘라서(slice) 순서대로 CPU를 사용하게끔 하는 것 인데,
사용자는 마치 동시에 여러 개의 프로그램이 실행되는 것처럼 느낄 수 있게 된다.
즉, CPU 사용률을 최대화 하고, 대기시간과 응답시간의 최소화를 가능케 해주는 방법이다.

이번에는 프로세스 한 개만 놓고 보자.
한 프로세스는 구성면에서 [텍스트]-[데이터]-[스택] 영역으로 구성되어있고, 기능면에서는 텍스트의 모듈들은 각각의 역할을 가지고 있다.
프로세스에서의 공유메모리영역을 제외한 부분끼리 묶어서 쓰레드로 만든 후, 이것들을 멀티태스킹처럼 동작시키면 멀티쓰레딩이 되는 것이다.

멀티쓰레드 프로그램을 작성할 경우의 장점은 다음처럼 요약될 수 있다.
1) 병렬화가 증가되어
2) CPU사용률이 극대화되며,
3) 전체적 처리율이 빨라지고,
4) 사용자에대한 응답성이 향상된다.
5) 또한, 완벽에 가까운 기능별 구분에 의한 모듈작성을 함으로써 설계가 단순해져서,
6) 프로그램의 안정성이 향상된다.
7) 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에서 동일한 서비스를 제공할수 있다.
8) 블록될 가능성이 있는 작업을 수행할 때 프로그램이 블록되지 않게 한다.

하지만, 쓰레드를 사용하면 오히려 불리한 경우도 있다. 대표적인 예로, 교착상태(deadlock)와 기아(starvation)이다.
쓰레드 기법을 사용할 때 주의사항을 정리하자면,
1) 확실한 이유를 가지고 있지 않는 경우에는 쓰레드를 사용하면 안 된다. 즉 쓰레드는 명확히 독립적인 경우에 사용해야 한다.
2) 명확히 독립적인 쓰레드라 하여도 오히려 나눔으로 인해 OS가 쓰레드를 다루는데에 따른 부하(overload)가 발생하게 된다.
즉, 실제 쓰레드에 의해 수행되는 작업량보다 클 경우에는 사용하지 않도록한다.

멀티쓰레드를 이용한 애플리케이션을 작성하는 구조에는 3가지 방법이 있다..
1. boss/worker 모델..
2. work crew 모델.
3. pipeline 모델.

1. 첫번째 쓰레드(주쓰레드)가 필요에 따라 작업자 쓰레드를 만들어 내는 경우.
이런 경우는 C/S 환경에서 접속받는 부분을 쓰레드로 돌리고, 접속요청이 오면 새로운 쓰레드를 만들어 사용자와 연결시켜 주는 방법이다.
이때 접속 받는 쓰레드가 주 쓰레드(boss Thread) 라고 하고, 사용자와 연결된 다른 쓰레드..
즉 주 쓰레드로부터 실행된 쓰레드는 작업자 쓰레드(worker Thread) 라고 한다..
2. 두번째 방식은 어떤 한 작업을 여러 개의 쓰레드가 나눠서 하는 방식이다.
즉 집을 청소한다는 개념의 작업이 있으면, 청소하는 작업에 대한 쓰레드를 여러 개 돌리는 거..
3. 공장라인을 생각...

쓰레드는 UI(User Interface) Thread와 Worker(작업자) Thread로 나뉜다.
UI Thread는 사용자 메시지 루프를 가지고 있는(즉 어떤 메시지가 날라오면 일하는.. )쓰레드이고..
Worker Thread는, 보통 오래 걸리는 작업이나 무한루프를 가지는 작업을 하는 사용자 정의 함수의 경우 사용.
UI Thread를 사용하려면, CWinThread 파생 클래스를 만들어 사용한다.

MFC에서는 AfxBeginThread의 서로 다른 버전 두 개를 정의 하고 있다..
하나는 작업자 쓰레드를 위한 것이고, 하나는 UI쓰레드를 위한 것이져..

원형은 다음과 같다..
UINT ThreadFunc(void* pParam)

이함수는 정적(static)클래스 멤버 함수 이거나 클래스 외부에서 선언한 함수여야 한다.


2. 쓰레드의 기본

1) 쓰레드 생성
WM_CREATE 에서 쓰레드를 만들면 되는데 함수는 다음과 같다.
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter,
DWORD dwCreationFlags, LPDWORD lpThreadId);

+lpThreadAttributes : 쓰레드의 보안속성 지정. 자식 프로세스로 핸들을 상속하지 않은 한 NULL
+dwStackSize : 쓰레드의 스택 크기 지정. 안정된 동작을 위해 쓰레드마다 별도의 스택 할당.
0으로 설정하면 주 쓰레드(CreateThread를 호출한 쓰레드)와 같은 크기를 갖으며, 스택이 부족할 경우 자동으로 스택크기를 늘려주므로 0으로 지정하면 무리가 없다.
+lpStartAddress : 쓰레드의 시작함수를 지정. 가장 중요한 인수.
+lpParameter : 쓰레드로 전달할 작업 내용이되 인수가 없을경우 NULL임.
+dwCreationFlags : 생성할 쓰레드의 특성 지정. 0이면 아무 특성없는 보통 쓰레드가 생성되고
CREATE_SUSPENDED 플래그를 지정하면 쓰레드를 만들기만 하고 실행은 하지 않도록하고 실행을 원하면 ResumeThread함수를 호출하면 된다.
+lpThreadId : 쓰레드의 ID를 넘겨주기 위한 출력용 인수이므로 DWORD형의 변수 하나를 선언한 후 그 변수의 번지를 넘기면 됨.

**** 작업자 쓰레드 생성하기 ****

작업자 쓰레드로 특정한 작업을 하는 사용자 정의 함수를 맹글기 위해서, 윈도우에서는 여러가지 쓰레드 생성 함수를 제공해 준다.
그 함수의 종류를 알아보도록 하져..

1. CreateThread()
2. _beginthread(), _beginthreadex()
3. AfxBeginThread(), AfxBeginThreadEx()

이렇게 약 5가지의 쓰레드 생성함수가 존재한다.
이제부터 저 5가지 함수의 특징을 알아보도록 하져…..
그럼 첫번째 CreateThread()함수. 이 함수는 보통 사용할때 다음과 같이 사용한다.

HANDLE handle;
Handle = CreateThread( Threadfunc(), Param );

첫번째 인자는 사용자가 쓰레드로 돌려야할 작업함수를 써주는 곳이고, 두번째는 작업함수에 인자값으로 전해줄 값이 들어간다..
이 인자값 형은 VOID*으로 되어 있기 때문에 4BYTE 이내의 값은 어떤 값이든 들어갈수 있져..대신 TYPE CASTING을 해주어야 하져..
그리고 받는 쪽에서도 type casting를 해서 받아야 한다.
이함수가 올바르게 실행이 되면 쓰레드에 대한 핸들을 반환하는데.. 이 핸들을 가지고 쓰레드를 조작할 수가 있져..
대표적으로 쓰레드를 닫을 때 CloseHandle()함수를 사용해서 쓰레드 핸들을 넣어주고 쓰레드를 닫아 주어야 한다..
이함수로 생성된 쓰레드를 닫을때는 ExitThread() 면 됩니다.

그럼..두번째 _beginthread를 알아보도록 하져..CreateThread는 쓰레드에서 win32 API함수만 호출할수 있다..
즉, 사용자가 어떤작업을 하는 함수를 만들 때 그 함수 안에서 win32API만 사용할수 있다는 말이다..
즉 C함수나 MFC는 저얼대~~ 못 쓴다….
_beginthread 함수는 win32 API아 C 런타임 함수를 사용할 때 사용한다.
이 함수를 사용하면 C런타임 라이브러리가 핸들을 자동으로 닫으므로 이를 직접할 필요는 없다.
대신 _beginthreadex는 스레드 핸들을 직접 닫아야 한다. 그리고 이 쓰레드를 닫을 때는 _endthread(), _endthreadex()를 사용하면 된다.

세번째 AfxBeginThread()와 AfxBeginThreadEx()..
실질적으로 가장 자주 사용하는 쓰레드 생성함수이다..
이 함수를 이용하면 사용자 정의 함수내에서 MFC, win32 API, C 런타임 라이브러리등 여러가지 라이브러리 함수들을 전부 사용할수 있다..
주로 프로젝트를 MFC로 만들 때 사용하죠..
이 함수는 리턴값이 CWinThread* 형을 리턴하며, 이 함수와 매칭되는 종료함수는 AfxEndThread()이다…
해서 쓰레드가 종료되면 MFC는 쓰레드 핸들을 닫고 리턴값으로 받은 CWinThread*객체를 제거한다.

CWinThread* pThread = AfxBeginThread( Threadfunc, &threadinfo );

첫번째 인자는 사용자 정의 함수이고, 두번째는 첫번째 인자의 쓰레드 함수에 인자값으로 들어갈 파라미터이다..
이 형은 void* 형으로 4byte를 가지므로 어떤 형으로 넣어줄 때 type casting하면 된다….

그 예는 다음과 같다.

int nNumber = 1000;

CWinThread *pThread = ::AfxBeginThread(ThreadFunc, &nNumber);

UINT ThreadFunc(LPVOID pParam)
{
int j = (int)pParam;
for (int i=0; i<j; i++)
{
// 수행할 작업
}
}


작업자 스레드 함수에 4바이트 이상의 정보를 넘겨주어야 할 경우에는
다음과 같이 작업자 스레드 함수에 넘겨주어야 할 모든 값을 포함하는 구조체를 선언하고,

typedef struct tagTREADPARAMS {
CPoint point;
BOOL *pContinue;
BOOL *pFriend;
CWnd *pWnd;
} THREADPAPAMS;

// 그런 다음 구조체에 필요한 값들을 설정하고, 이 구조체의 포인터를 넘겨준다.
THREADPAPAMS *pThreadParams = new THREADPAPAMS; // new로 할당
pThreadParams->point = m_ptPoint;
pThreadParams->pContinue = &m_bExec; // 쓰레드 실행 플래그
pThreadParams->pFriend = &m_bYield; // 쓰레드 양보 플래그
pThreadParams->pWnd = this;
m_pThread = AfxBeginThread(ThreadFunc, pThreadParams);

UINT ThreadFunc(LPVOID pParam)
{
// 넘어온 인자를 복사
THREADPAPAMS *pThreadParams = (THREADPAPAMS *)pParam;
CPoint point = pThreadParams->point;
CWnd *pWnd = pThreadParams->pWnd;
BOOL *pContinue = pThreadParams->pContinue;
BOOL *pFriend = pThreadParams->pFriend;
delete pThreadParams; // delete로 해제

// "실행" 플래그가 TRUE인 동안 스레드가 실행됨
while(*pContinue)
{
// 수행할 작업

// "양보" 플래그가 TRUE이면 다른 스레드에 CPU를 양보
if(*pFriend) Sleep(0);
}
return 0;
}


자 그럼..정리해 보도록 하져…..쓰레드를 생성하는 함수들은 크게 3가지가 있고..(확장된것까지 생각하면 5개..^^ ) 이들 함수의 특징은 다음과 같다.

쓰레드가 win32 API만을 사용한다면 CreateThread()를 사용하면 되고, C런타임 라이브러리를 사용하다면 _beginthread()를 사용하고,
전부다 사용한다면 AfxBeginThread()를 사용하면 된다.


2) 쓰레드 종료
작업 쓰레드가 종료되었는지 조사하는 함수는 다음과 같다.
BOOL GetExitCodeThread(HANDLE hThread, PDWORD lpExitCode);

+hThread : 쓰레드의 핸들
+lpExitCode : 쓰레드의 종료코드.
+Return : 계속 실행중 : STILL_ACTIVE, 쓰레드 종료 : 스레드 시작함수가 리턴한 값 or ExitThread 함수의 인수

쓰레드가 무한루프로 작성되어 있다해도 프로세스가 종료되면 모든 쓰레드가 종료되므로 상관이 없다.
백그라운드 작업을 하는 쓰레드는 작업이 끝나면 종료되는데 때로는 작업도중 중지해야 할 경우에는 다음 두 함수가 사용된다.

VOID ExitThread(DWORD dwExitCode);
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

ExitThread는 스스로 종료할 때 사용.인수로 종료코드를 넘김. 종료코드는 주 쓰레드에서 GetExitCodeThread함수로 조사할 수 있다.
이것이 호출되면 자신의 스택을 해제하고 연결된 DLL을 모두 분리한 후 스스로 파괴된다.

TerminateThread는 쓰레드 핸들을 인수로 전달받아 해당 쓰레드를 강제종료시킨다.
이 함수는 쓰레드와 연결된 DLL에게 통지하지 않으므로 DLL들이 제대로 종료처리를 하지 못할 수 있고 리소스도 해제되지 않을 수 있다.
그래서 이 작업 후  어떤일이 발생할지를 정확히 알때에만 사용하도록한다.


스레드를 죽이는 방법엔 두가지가 있져..
1. 스레드 내부에서 return을 시킬 때.
2. AfxEndThread를 호출할 때.
안전한 방법은 스레드 내부 자체에서 return문을 이용해서 죽여주는게 안전하다. 위의 예와 같이...

다음은 쓰레드를 종료하는 함수의 예이다.
if(m_pThread != NULL)
{
HANDLE hThread = m_pThread->m_hThread; // CWinThread *m_pThread;
m_bExec = FALSE; // 실행 플래그를 FALSE로 하여 쓰레드 종료시킴..
::WaitForSingleObject(hThread, INFINITE);
// 이후 정리작업...
}


위의 첫번째 방법과 같이 return을 받았을때는 GetExitCodeThread를 이용해서 검색할수 있는 32bit의종료 코드를 볼수 있다..

DWORD dwexitcode;
::GetExitCodeThread( pThread->m_hThread, &dwExitCode );
// pThread는 CWinThread* 객체의 변수..

만약 실행중인 스레드를 대상으로 저 코드를 쓰게 된다면 dwExitCode에는 STILL_ACTIVE라는 값이 들어가게 된다.


근데..위의 코드를 사용함에 있어 제약이 좀 있다.
CWinThread*객체는 스레드가 return 되어서 종료가 되면 CWinThread객체 자신도 제거되어 버린다..즉 동반자살이져..
delete시켜주지 않아도 메모리에서 알아서 없어진다는 말이져..
즉…return이 되어서 이미 죽어버린 스레드를 가지고 pThread->m_hThread를 넣어주면, Access위반이란 error메시지가 나오게 되져..

이런 문제를 해결할라면 CWinThread* 객체를 얻은 다음 이 객체의 멤버 변수인 m_hAutoDelete를 FALSE로 설정하면
스레드가 return을 해도 CWinThread객체는 자동으로 제거 되지 않기 때문에 위의 코드는 정상적으로 수행이 된다..

이런 경우에 CWinthread*가 더 이상 필요가 없어지면 개발자 스스로 CWinThread를 delete시켜 주어야 한다.  

또다른 방법으로 스레드가 가동이 되면 CWinThread*의 멤버변수인 m_hThread를 다른 곳으로 저장을 해놓고
이 것을 직접GetExitCodeThread()에 전달을 하면 그 쓰레드가 실행중인지 한때는 실행되고 있었지만 죽어버린 스레드인지 확인이 가능하다.

int a = 100;              // 파라미터로 넘겨줄 전역변수.
CWinThread* pThread   // 전역 쓰레드 객체의 포인터 변수.
HANDLE threadhandle;  // 스레드의 핸들을 저장할 핸들변수.

Initinstance() // 프로그램초기화.
{
// 프로그램 실행과 동시에 스레드 시작.
1번방법:pThread = AfxBeginThread( func, (int) a );

// 스레드가 리턴되면 자동으로 CWinThread객체가 자동으로 파괴되지 않게 설정.
2번방법:pThread->m_hAutoDelete = FALSE;

// 쓰레드 핸드를 저장. 위의 m_hAutoDelete를 설정하지않았을경우..
threadhandle = pThread->m_hThread;
}

MessageFunction()  // 어떤 버튼을 눌러서 스레드의 상태를 알고 싶다..
{
char* temp;
DWORD dwExitcode;
// 스레드 객체의 m_hAutoDelete를 fasle로 설정해서 스레드가 return되어도
// 객체가 자동으로 파괴되지 않아서 핸들을 참조 할수 있다.
1번방법: ::GetExitCode( pThread->m_hThread, &dwExitcode);

// 스레드가 종료되고 미리 저장해둔 핸들을 이용할경우..
2번방법:::GetExitCode(threadhandle, &dwExitcode);
sprintf( temp, "Error code : %d", dwExitcode );

// 스레드 객체 삭제..
1번방법: delete pThread;
AfxMessageBox( temp );
}

func( void* pParam )
{
int b = (int) pParam;
for( int i = 0; i < b; i++)
{
// 어떤일을 한다.
}
return;  // 작업이 끝나면 리턴한다. 이때 스레드 자동으로 종료.
}


1번째 방법은 스레드를 생성하고 m_hAutoDelete를 false로 해서
스레드가 return해서 자동종료해도 CWinthread를 자동파괴하지 않게 하고, GetExitCodeThread()를 호출하져..
밑에서 delete해 주는 거 꼭 해야되고요..안그럼 메모리 누수가 되져..

2번째는 m_hThread를 다른 핸들변수에 저장해 놓고..스레드가 return되면 CWinThread*도 같이 파괴가 되는데..
원래 저장한 핸들을 가지고 GetExitcodeThread()를 호출해서 한때 존재했지만 종료된 쓰레드를 검사하는 것이져….이해 OK?????


3) 대기 함수

WaitForSingleObject(), WaitForMultipleObjects()의 원형은 다음과 같다.

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

DWORD WaitForMultipleObjects(
DWORD nCount,             // number of handles in array
CONST HANDLE *lpHandles,  // object-handle array
BOOL bWaitAll,            // wait option
DWORD dwMilliseconds      // time-out interval
);


쓰레드 종료를 위한 플래그를 설정한 후, 쓰레드가 완전히 종료된 것을 확인 후에 어떤 작업을 하고 싶으면 다음과 같이 한다.
if (::WaitForSingleObject(pThread->m_hThread, INFINITE))
{
// 쓰레드가 종료된 후 해야 할 작업들
}


(쓰레드 종료를) 어느 정도 기다리다가 프로그램을 진행시키려면 다음과 같이 한다.
DWORD dwRetCode;
dwRetCode = ::WaitForSingleObject(pThread->m_hThread, 2000);
if (dwRetCode == WAIT_OBJECT_0)
{
// 쓰레드가 종료된 후 해야 할 작업들
}
else if(dwRetCode == WAIT_TIMEOUT)
{
// 2초 동안 쓰레드가 종료되지 않았을 때 해야 할 에러 처리
}


다음과 같이 하면, 어떤 쓰레드가 현재 실행 중인지 아닌지를 알 수 있다.
if (::WaitForSingleObject(pThread->m_hThread, 0) == WAIT_TIMEOUT)
{
// 현재 쓰레드가 실행 중.
}
else
// 실행 중인 상태가 아니다.



// WaitForMultipleObjects() sample...

// 쓰레드 함수의 원형
DWORD WINAPI increment(LPVOID lParam);
DWORD WINAPI decrement(LPVOID lParam);

int main()
{
// char* ps[] = {"increment", "decrement"};
DWORD threadID;
HANDLE hThreads[2];

// hThreads[0] = CreateThread( NULL, 0, increment, (LPVOID)ps[0], 0, &threadID);
// hThreads[0] = CreateThread( NULL, 0, increment, NULL, 0, &threadID);

for (int i=0; i<2; ++i)
{
hThreads[i] = CreateThread( NULL, 0, increment, (void *)i, 0, &threadID);
}

// 모든 쓰레드가 종료할 때 까지 기다린다.
// WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);

int ret;
ret = WaitForMultipleObjects(2, hThreads, FALSE, INFINITE);
switch(ret)
{
case WAIT_OBJECT_0: // handle hThreads[0] is signaled..
break;
case WAIT_OBJECT_0+1:
break;
}

CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
return 0;
}

DWORD WINAPI increment(LPVOID lParam)
{
while (1)
{
...
}

return 0;
}

DWORD WINAPI decrement(LPVOID lParam)
{
while (1)
{
...
}

return 0;
}

4) 쓰레드 일시중지 - 재개

DWORD SuspendThread(HANDLE hThread); - 1
DWORD ResumeThread(HANDLE hThread); - 2

둘 다 내부적으로 카운터를 사용하므로 1을 두번 호출했다면 2도 두번 호출해야한다. 그래서 카운터가 0 이되면 쓰레드는 재개하게된다.


5) 우선순위 조정

향상된 멀티태스킹을 지원하기 위해서는 시분할 뿐만 아니라 프로세스의 우선순위를 지원해야 한다.
마찬가지로 프로세스 내부의 쓰레드들도 우선순위를 갖아야 하며 우선순위 클래스, 우선순위 레벨 이 두 가지의 조합으로 구성된다.

우선순위 클래스는, 스레드를 소유한 프로세스의 우선순위이며
CreateProcess 함수로 프로세스를 생성할 때 여섯번째 파라미터 dwCreationFlag로 지정한 값이다.
디폴트는 NORMAL_PRIORITY_CLASSfh 보통 우선순위를 가지므로 dwCreationFlag를 특별히 지정하지 않으면 이 값이 전달된다.

우선순위 레벨은 프로세스 내에서 쓰레드의 우선순위를 지정하며 일단 쓰레드를 생성한 후 다음 두 함수로 설정하거나 읽을 수 있다.

BOOL SetThreadPriority(HANDLE hThread, int nPriority);
Int GetThreadPriority(HANDLE hThread);

지정 가능한 우선순위 레벨은 총 7가지 중 하나이며 디폴트는 보통 우선순위인 THREAD_PRIORITY_NORMAL 이다.

우선순위 클래스와 레벨값으로부터 조합된 값을 기반우선순위(Base priority)라고 하며 쓰레드의 우선순위를 지정하는 값이 된다.
기반우선순위는 0~31 중 하나이며 0은 시스템만 가질 수 있는 가장 낮은 우선순위 이다. (낮을수록 권한이 높음)

우선순위를 높이는(에이징)방법과 낮추는 방법을 동적 우선순위 라고하며, 우선순위 부스트(Priority Boost)라고 한다.
단 이 과정은 기반 우선순위 0~15 사이의 쓰레드에만 적용되며 16~31 사이의 쓰레드에는 적용되지 않는다.
또한 사용자입력을 받거나(인터럽트) 대기상태에서 준비상태가 되는 경우에는 우선순위가 올라가고,
쓰레드가 할당된 시간을 다 쓸 때마다 우선순위를 내려  결국 다시 기반 우선순위와 같아지게 되는데,
어떠한 경우라도 동적 우선순위가 기반 우선순위보다는 더 낮아지지 않는다.

3. 쓰레드간 동기화

멀티쓰레드는 개요에서 말했듯이 한 프로세스를 여러 역할에 따라 여러 개의 쓰레드로 나뉘어 작업하는 방식이므로 각 쓰레드간의 동기화가 필요하다.
동시에 복수개의 코드가 같은 주소영역에서 실행됨으로써 서로 간섭하고 영향을 주는 경우가 빈번하기 때문이다.

멀티쓰레드의 가장 큰 문제점은 공유자원(주로 메모리의 전역변수)을 보호하기가 어렵다는 점이다.
그리고 쓰레드간의 실행순서를 제어하는 것도 쉽지 않은 문제이다.

이런 여러가지 문제점을 해결하기 위하여 쓰레드간의 실행 순서를 제어할 수 있는 여러가지 방법들을 동기화라고 한다.
동기화 방법에는, Interlocked, 임계영역, 뮤텍스, 세마포어, 이벤트등의 기법을 사용한다.

1) 임계영역 (Critical Section)

동기화문제를 해결하는 방법들 중 가장 쉬운반면 동일한 프로세스 내에서만 사용해야 하는 제약이 있다.
임계영역(Critical Section)이란 공유자원의 독점을 보장하는 코드의 영역을 가리킨다. 이는 아래 두 함수로 시작하고 끝낸다.

VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

CRITICAL_SECTION형의 포인터형은 복수개의 쓰레드가 참조해야 하므로 반드시 전역변수로 선언해야한다. 사용법은 다음과 같다.

CRITICAL_SECTION crit1, crit2;

함수 {

EnterCriticalSection(&crit1);
//공유자원1을 액서스한다.
LeaveCriticalSection(&crit1);

EnterCriticalSection(&crit2);
//공유자원2을 액서스한다.
LeaveCriticalSection(&crit2);

}

주의할것은 가급적 임계영역 내부의 코드가 빨리 끝날 수 있도록 짧은 시간을 사용하도록 작성해야 한다.
만약 Leave를 호출하지않고 쓰레드를 빠져나와버리면 이후부터는 다른 쓰레드는 이 임계영역에 들어갈 수 없게된다.
만약 이부분에서 예외가 발생하여 Leave함수가 호출되지 못하게 될 수도 있다.
그래서 임계영역을 쓸 때는 반드시 구조적 예외 처리구문에 포함시켜주는 것이 좋다.

Try {
EnterCriticalSection(&crit);

}
finally {
LeaveCriticalSection(&crit);
}

이렇게하면 설사 예외가 발생하더라도 Leave함수는 반드시 호출되므로 훨씬 안전해진다.

다음은 MFC 에서의 사용 예이다.
CCriticalSection g_critical; // 전역 변수로 선언

function()
{
AfxBeginThread(ThreadFuncA, NULL);
AfxBeginThread(ThreadFuncB, this);
}

UINT ThreadFuncA(LPVOID pParam)
{
while(1)
{
g_critical.Lock();

// ThreadFuncA가 할 일....

g_critical.Unlock();
}
return 0;
}

UINT ThreadFuncB(LPVOID pParam)
{
while(1)
{
g_critical.Lock();

// ThreadFuncB가 할 일....

g_critical.Unlock();
}
return 0;
}


2) 뮤텍스(Mutex)

임계영역은 앞서 말했듯 동일한 프로세스 내에서만 사용할 수 있다.
그러나, 뮤텍스(Mutex; Mutual Exclusion;상호배제)는 임계영역이 사용된 곳에 대신 사용될 수 있으며, 프로세스 간에도 사용할 수 있다.
뮤텍스를 사용하려면 다음 함수로 생성해야 한다.

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL blInitialOwner, LPCTSTR lpName);

lpMutexAttributes : 보안속성. 대개 NULL
blInitialOwner : 뮤텍스 생성과 동시에 소유할 것인지 지정.
lpName: 뮤텍스의 이름을 지정하는 문자열.
뮤텍스는 프로세스간의 동기화에도 사용되므로 이름이 필요하고, 이 이름은 프로세스간 뮤텍스를 공유할 때 사용된다.

뮤텍스 소유를 해지하여 다른 쓰레드가 이것을 가질 수 있도록 하려면 임계영역의 LeaveCriticalSection 에 해당하는 다음 함수를 호출하면 된다.

BOOL ReleaseMutex(HANDLE hMutex);

만일 프로세스가 다른 프로세스의 쓰레드에 의해서 이미 생성된 뮤텍스의 핸들을 얻기를 원하거나,
뮤텍스가 존재하지 않는 경우에 뮤텍스를 생성하기 원한다면 다음 함수를 사용한다.

HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);

3) 세마포어 (Semaphore)

세마포어도 뮤텍스와 유사한 동기화 객체이나 다른점은, 뮤텍스는 하나의 공유자원을 보호하기 위해 사용하지만,
세마포어는 제한된 일정 개수를 가지는 자원(HW, 윈도우, 프로세스, 쓰레드, 권한, 상태 등 컴퓨터에서의 모든 자원)을 보호하고 관리한다.
세마포어는 사용 가능한 자원의 개수를 카운트하는 동기화 객체이다.
세마포어와 관련된 함수는 다음과 같다.

HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG IlInitialCount,
LONG lMaximumCount, LPCTSTR lpName);

HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);

BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);

4) 이벤트 (Event)

임계영역, 뮤텍스, 세마포어는 주로 공유자원을 보호하기 위해 사용되는 데 비해
이벤트는 이보다는 스레드간의 작업순서나 시기를 조정하기 위해 사용한다.
특정한 조건이 만족될 때까지 대기해야 하는 쓰레드가 있을 경우 이 쓰레드의 실행을 이벤트로 제어할 수 있다.
이벤트는 자동리셋과 수동리셋이 있다.

+자동 리셋 이벤트 : 대기상태가 종료되면 자동으로 비신호상태가 된다.
+수동 리셋 이벤트 : 쓰레드가 비신호상태로 만들어줄 때까지 신호상태를 유지한다.

++신호상태 (Signaled): 쓰레드 실행가능상태. 신호상태의 동기화 객체를 가진 쓰레드는 계속 실행할 수 있다.

HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);

HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);

bManualReset은 이벤트가 수동리셋 이벤트(manual)인지 자동리셋 이벤트(automatic)인지 지정하는데 TRUE이면 수동리셋 이벤트가 된다.
bInitialState가 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 쓰레드가 곧바로 실행을 하도록 해준다.
이벤트도 이름(lpName)을 가지므로 프로세스간의 동기화에 사용될 수 있다.

또한 이벤트가 임계영역이나 뮤텍스와 다른점은
대기함수를 사용하지 않고도, 쓰레드에서 임의적으로 신호상태와 비신호상태를 설정할 수 있다는 점이다. 다음 함수를 사용한다.

BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);

SetEvent는 신호상태로 만들고 ResetEvent는 비신호상태로 만든다.

다음은 MFC 에서의 사용 예이다.
CEvent g_event; // 전역변수로 선언

FunctionA()
{
AfxBeginThread(ThreadFunc, this);
}

FunctionB()
{
g_event.SetEvent(); // Lock() 함수에서 더 이상 진행하지 못하고 잠자고 있는 쓰레드를 깨워서 일을 시키려면 SetEvent()를 호출.
}

// ThreadFunc() 함수는 이벤트가 발생할 때마다 while문을 한번씩 실행.
UINT ThreadFunc(LPVOID pParam)
{
while(1)
{
g_event.Lock();  // SetEvent()가 호출되면, Lock()함수에서 실행이 중단된 쓰레드가 다음 코드를 실행.

// ThreadFunc가 할 일....

g_event.Unlock();
}
return 0;
}

'프로그래밍 > MFC' 카테고리의 다른 글

WIN32 API프로그래밍에서 MDI에서 자식 생성.  (0) 2013.08.14
MFC 디렉토리 생성  (0) 2013.08.14
쓰레드 생성.  (0) 2013.08.14
유니코드  (0) 2013.08.14
CEvent 클래스  (0) 2013.08.14
반응형
윈도우 쓰레드 생성 방법

_beginthread
- C 라이브러리를 사용할수 있는 장점 있음
- 런타임 라이브러리 지정시 다중 쓰레드 옵션을 주면 사용할수 있음.
- 종료시 _endthread 호출해야함

CreateThread
- 종료시 함수 리턴, 혹은 ExitThread함수 호출
- UI 관련 함수 , 윈도우 컨트롤 사용시 알수 없는 문제가 많이 발생한다. 이럴경우
  AfxBeginThread로 사용한다.
 

MFC를 이용한 쓰레드 프로그래밍
- 메시지 펌프가 있어서 메시지 처리 가능함.
- 작업 쓰레드와 UI 쓰레드로 구분.
0 작업쓰레드 생성시 AfxBeginThread 사용.
- 작업 쓰레드 종료시 AfxThreadEnd() 함수 처리.
- 외부에서 종료 처리할수 있음
- TerminateThread API 함수를 사용.
0 UI 쓰레드 생성시 AfxBeginthread 호출 인자값 pfnThreadProc는 NULL이
되는 AfxBeginthread호출함.
 

'프로그래밍 > MFC' 카테고리의 다른 글

MFC 디렉토리 생성  (0) 2013.08.14
쓰레드 생성 (펌글)  (0) 2013.08.14
유니코드  (0) 2013.08.14
CEvent 클래스  (0) 2013.08.14
JPG to BMP , BMP to JPG 로 변환 코드  (0) 2013.08.14
반응형
1. 유니코드

ASCII로는 영문(1Byte)밖에 표현 못한다. 한글을 사용하려 하거나 다른 나라 언어를
사용하기 위해선 255로써는 표현을 못했다. 그래서 보통 한글을 사용하기 위해서
2Byte로 처리를 해서 조합,완성형이라는 한글이라는 방식이 생기게 되었다.

다른 나라도 각자 알아서 2Byte라든지 다른 방식으로 처리해서 표현하게 되었는데, 이게 너무 다 다르다는 거다. 그래서 "모든 나라의 글자셋을 하나로 묶어 버리자!"라는 취지 하에 만들어 진게 유니 코드이다.

그런데 유니코드를 실제 바이트로 변환이 필요하게 되었고 인코딩 방식이 생기게 되었는데 하나의 글자 표현에 2바이트를 사용하는 UCS-2(2바이트) 또는 UTF-16(16비트) ,
영어는 1바이트 그외 다른 언어는 2바이트~6바이트까지 할당하는 UTF-8등이 있다.

-유니코드 : Wide Character로 16비트 코드를 가진다. wchar_t, wchar_t*로 표현됨.

- MBCS/DBCS : MBCS(Multi-Byte Character Set)은 하나의 문자가 한 바이트 이상 으로 구성된 문자열 셋을 말한다.  정확한건 모르겠고, 한 글자가 1바이트 이상으로 표현될수 있는 문자열셋을 말하나보당.

-ANSI : 8비트로 구성된거. 설명 필요 읍지.

-TCHAR : 그냥 MS에서 유니코드로 구성된 프로젝트일때는 WCHAR로 아니면 CHAR로 변환해주는 매크로라고 보면 쉽겠지.

유니코드로 되었있든 그냥 일반 MBCS, ANSI로 되어있든 TCHAR형으로 써주면 알아서 변경 되게 한다 라는 취지겠지 모.

ANSI               UNICODE                 TCHAR
strlen               wcslen                    _tcslen
strcat               wcscat                    _tcscat
strchr               wcschr                    _tcschr
strcmp             wcscmp                    _tcscmp
strcpy              wcscpy                    _tcscpy
strstr               wcsstr                    _tcsstr
strrev               _wcsrev                    _tcsrev
printf                wprintf                       _tprintf
sprintf               wsprintf                    _tsprintf
scanf               wscanf                     _tscanf



사용 예 :
TCHAR szTemp[10] = _T("유니코드");
int Len = _tcslen(szTemp);

MBCS에서 유니코드 변환시 필요 함수 : mbstowcs, MultiByteToWideChar
유니코드에서 MBCS 변환시 함수 : wcstombs, WideCharToMultiByte

ATL에서 제공하는 A2W , W2A 도 있다.

'프로그래밍 > MFC' 카테고리의 다른 글

MFC 디렉토리 생성  (0) 2013.08.14
쓰레드 생성 (펌글)  (0) 2013.08.14
쓰레드 생성.  (0) 2013.08.14
CEvent 클래스  (0) 2013.08.14
JPG to BMP , BMP to JPG 로 변환 코드  (0) 2013.08.14
반응형
이 글의 목적은 CEvent의 사용 목적 및 방법과 WaitForSingleObject(wait function)와의 사용관계에 대한 간단한 설명입니다.



CEvent는 보통 이벤트 객체라고 불리며, 동기화 객체로 사용됩니다. 이 이벤트 객체는 딱 2가지 상태(signaled, non-signaled 또는 unsignaled)로 동기화를 지원합니다.

예를 들어 TRUE, FALSE 와 같은 개념입니다. 그럼 이 두 상태를 바탕으로 우리가 할 수 있는 일은 대부분 동기화 , 즉 둘 이상의 관계에서 서로간의 상태에 따라 작업을

구분지어 실행 순서에 제어를 두기 위함입니다. 예를 들어 두 스레드간에 서로 같은 데이터를 접근하여 사용하는 경우이겠죠.



CEvent의 생성자를 살펴보면, 다음과 같습니다.

CEvent::CEvent

(

  BOOL bInitiallyOwn = FALSE,

  BOOL bManualReset = FALSE,

  LPCTSTR lpszName = NULL,

  LPSECURITY_ATTRIBUTES lpsaAttribute = NULL

);



여기서 주의 깊게 보아야 할 부분이 bInitiallyOwn, bManualReset 입니다.

bInitiallyOwn 을 FALSE로 지정할 경우 생성되는 event 객체는 non-signaled된 상태입니다. 반대일 경우는 signaled로 생성이 됩니다.



그럼 bManualReset의 상황에 따라 WaitForSingleObject의 수행이 어떻게 다른가 살펴보겠습니다. (단, bInitiallyOwn 는 FALSE)



1. bManualReset = TRUE 일 경우 ( 수동 event 객체)



WaitforSingleObject를 통해서 스레드나 프로세스가 대기하게 됩니다. 물론 같은 event 객체에 대한 WaitForSingleObject가 수행된 곳은 모두 대기 상태입니다. 더 이상 실행이

진행이 안되는 것이죠. 이 상황에서 SetEvent()를 호출하면 내부적으로 event 객체가 signaled로 변경됩니다. 각 대기 스레드나 프로세스들은 (현재 대기하는 모든 스레드나 프로세스) 가 대기상태 에서 진행상태로 바뀌게 됩니다  SetEvent()로 인하여 event 객체는 계속 signaled된 상태를 지속하게 됩니다. 이 event 객체의 상태를 non-signaled로 바꾸려면, ResetEvent()를 호출시켜줍니다. 이 경우는 같은 event 객체를 사용하고 , waitForSIngleObject를 통해서 대기하는 모든 스레드나 프로세스가 같이 대기 상태에서

진행상태로 바꾸게 됩니다.  그리고, ResetEvent()를 통해 다시 대기 상태로 변경시킬 수 있습니다. 이 경우 PulseEvent()함수는 Event의 상태를 signaled로 변경하고 ,

모든 대기 스레드나 프로세스를 대기상태에서 벗어나게 합니다. 그리고 나서 자동으로 non-signaled로 변경시켜줍니다.



2. bManualReset = FALSE 일 경우 ( 자동 event 객체)



WaitforSingleObject를 통해서 스레드나 프로세스가 대기하게 됩니다. 물론 같은 event 객체에 대한 WaitForSingleObject가 수행된 곳은 모두 대기 상태입니다.

이 상황에서 SetEvent()를 호출하면 내부적으로 event 객체가 signaled로 변경됩니다. 현재 대기하는 모든 스레드나 프로세스 중에서 하나의 스레드나 프로세스만이 대기상태에서 진행상태로 바뀌게 됩니다. 그리고 자동으로 non-signaled 상태가 되어 다른 스레드나 프로세스가 진행상태로 되는 것을 방지해줍니다. 물론 꼭 하나의 스레드나 프로세스가 대기상태에서 진행 상태로 변경되어야만 non-signaled 상태가 됩니다. ResetEvent()는 자동 event 객체일때는 사용할 수 없습니다. 이 경우 PulseEvent()함수는 Event의 상태를 signaled로 변경하고 , 현재 대기하는 모든 스레드나 프로세스 중에서 하나의 스레드나 프로세스만이 대기상태에서  벗어나게 합니다. 그리고 나서 자동으로 non-signaled로 변경시켜줍니다.



보통 다중 스레드에서 하나의 데이터에 접근할 때 쓰는 방법은 2번이 되겠습니다. 1번의 경우는 각 스레드들이 작업을 진행하다가 어떤 이벤트에 의해서 모두 정지되거나 진행되어야 할 때 유용하게 사용할 수 있습니다.



예제) bInitiallyOwn = FALSE, bManualReset = FALSE일 경우

Thread1()

{

  while(condition)

  {  

      0.  WaitForSingleObject( event, INFINITE);

      2. // 작업...                                              

      3. SetEvent();

  }

}



Thread2()

{

  while(condition)

  {

      0. WaitForSingleObject(event, INFINITE);

      4. //작업

      5. SetEvent();

  }

}



otherFunction()

{

  1.  SetEvent();

}

위의 예제에서는 //작업이라는 구역에는 동시에 두 스레드가 접근할 수 없습니다. 이해가 되실꺼에요. 3. 5번으로 인하여 두 스레드는 계속 루프를 돌게 됩니다. while이 있지만, 서로가 서로에게 계속 이벤트를 발생시켜줌으로서 운영체제의 스레드 스케쥴링에 의해서 계속 루프를 돌면서 수행되는 이야기입니다. 만약 스레드 안에 3. 5번을 없애면 1번 처럼

이벤트를 날려줌으로 해서 스레드의 작업을 제어할 수 있습니다.

'프로그래밍 > MFC' 카테고리의 다른 글

MFC 디렉토리 생성  (0) 2013.08.14
쓰레드 생성 (펌글)  (0) 2013.08.14
쓰레드 생성.  (0) 2013.08.14
유니코드  (0) 2013.08.14
JPG to BMP , BMP to JPG 로 변환 코드  (0) 2013.08.14

+ Recent posts