아래글은 http://www.idhacker96.pe.kr/tc/27 의 글을 가져왔습니다.

개인적으로 우분투 시스템에서는 gzip을 사용하여 압축을하고, 윈도우 시스템에서는 zip을 사용하여 압축을 한다. 문제는 우분투와 윈도우에서 압축한 파일을 상호 호환성 문제로 잘 풀리지 않을때가 있다.

우분투에서 압축한 파일인 *.tar.gz 파일을 윈도우에서 풀려고하면, 두번 압축을 풀어야하고, 윈도우에서 압축한 zip 파일을 우분투에서 압축을 해제하면, 한글 파일명이 깨져 출력된다.

7z 압축은 우분투와 윈도우에서도 압축을하면 한글이 깨지지 않는다는 장점과 압축 효율이 높다고 외국에서는 많이 사용한다고 들었다. 그러나 습관이 이미 들어서 인지 압축파일을 만들때만다 7z으로 만들지는 않는다.

우분투에 7z 패키지 설치

sudo apt-get install p7zip

7z에 대한 자세한 사항은 'man 7zr'을 통해서 확인할 수 있다.

7z으로 압축

7zr a 파일명.7z [대상]
ex) 7zr a filename.7z files/

7z으로 압축해제

7zr x filename.7z


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

리눅스에서 7z 압축과 해제하기  (2) 2012.11.05
[리눅스] IP 설정 변경 하기  (0) 2009.09.04
  1. lunettes ray ban 2013.07.28 17:35

    눈을 감아봐 입가에 미소가 떠오르면 네가 사랑하는 그 사람이 널 사랑하고 있는거야.

  2. 2017.11.13 14:58

    비밀댓글입니다

공자(孔子, 기원전 551년~기원전 479년)는 유교의 시조(始祖)로 떠받들어지는 중국사상가이다. 이름은 구(丘), 자는 중니(仲尼). 공자의 자(子)는 경칭으로 '선생'이라는 뜻이 된다. < 출처 : 위키백과 - 공자 >

[공자(孔子) 명언]
- 지금과 시대적 가차관이 다른 말들이 있지만, 여전히 주옥같은 말들이 많이 있다.
  한번쯤 자신에 대해서 다시 생각해 보게 하는 말들이다.


가까이 있는 사람을 기쁘게 하고 멀리서 사람이 찾아오게 하라. -공자


가 지를 잘 쳐주고 받침대로 받쳐 준 나무는 곧게 잘 자라지만, 내버려 둔 나무는 아무렇게나 자란다. 사람도 이와 마찬가지여서 남이 자신의 잘못을 지적해 주는 말을 잘 듣고 고치는 사람은 그만큼 발전한다. -공자


군자가 이웃을 가려서 사는 것은 환난을 막기 위함이다. -공자


군자는 도(道)를 근심하고 가난을 근심하지 않는다. -공자


군 자는 말에는 더디지만, 일을 함에는 민첩하다. -공자


군자는 말을 잘하는 사람의 말에만 귀를 기울이지 않고 말이 서툰 사람의 말에도 귀담아 듣는다. -공자


군자는 말이 행함보다 앞서는 것을 부끄러워한다. -공자


군 자는 모든 것을 공경하나 신체의 공경함을 가장 으뜸으로 생각한다. 신체란 부모의 가지이니 어찌 공경하지 아니하랴? 그 몸을 공경하지 못하면 이는 곧 부모를 상하게 함이며, 부모를 상하게 함은 뿌리를 상하게 함이니, 그 뿌리가 상하면 가지도 따라서 망하게 된다. -공자


군자는 세 가지 경계할 바가 있다. 젊었을 때는 혈기가 잡히지 않았기에 여색을 경계하고, 장년이 되면 혈기가 바야흐로 굳세므로 다투는 것을 경계하고, 늙으면 혈기가 이미 쇠하였음으로 탐욕을 경계하라. -공자


군 자는 스스로 재능이 없음을 근심하며, 남이 알아주지 않음을 근심하지 않는다. -공자


군자는 타인의 좋은 점을 말하고 악한 점을 말하지 않는다. 반대로 소인은 타인의 좋은 점은 말하지 않고 악한 점만 말한다. -공자


군 자는 행위로써 말하고 소인은 혀로써 말한다. -공자


군자에게 용맹만 있고, 예가 없으면 세상을 어지럽게 한다. 소인에게 용맹만 있고, 예가 없으면 도둑이 된다. -공자


그 지위에 있지 않으면 그 정사(政事)를 논하지 말아라. -공자


길은 가까운 데에 있다. 그런데도 이것을 먼 데서 구한다. -공자


나 는 하루에 세 번 나 자신을 반성해 본다. 남을 위해 충을 다했는가, 친구와 사귀어 신(信)을 지켰는가, 배운 것을 남에게 전했는가 하고. -공자


나라를 다스리려면 식량을 충분하게 마련하고 무기를 충분히 마련하여 국민들이 위정자를 믿게 하여야 한다. 무엇보다도 국민들이 위정자를 믿지 않으면 정치를 해나갈 수 없다. -공자


나라의 질서가 바로잡혀 있을 때에는 돈이나 지위가 없다는 것이 부끄러운 일이 된다. 그러나 나라가 어지러울 때는 돈이 있고 지위가 높다는 것이 수치이다. -공자


나무가 먹줄을 좇으면 곧아지고, 사람이 충간(忠諫)함을 받아들이면 거룩해진다. -공자


나 쁜 일을 하여 하늘에서 죄를 받으면 빌 곳이 없다. -공자


남들이 듣기 싫은 성난 말을 하지 말라. 남도 그렇게 너에게 대답할 것이다. 악이 가면 화가 돌아오니 욕설이 가고 오고 매질이 오고 간다. -공자


남에게 듣기 싫은 성난 말을 하지 말라. 남도 그렇게 너에게 대답할 것이다. 악이 가면 화가 돌아오니 욕설이 가고 주먹이 오간다. -공자


남의 충고를 달갑게 받아들여라. 그러나 윗사람이나 친구간에 너무 자주 충고나 간언(諫言)을 하지는 말라. -공자


내 가르침은 간단하고 그 의미를 쉽게 터득할 수 있다. 자기 자신처럼 이웃을 사랑하라는 것이 그 전부이다. -공자


내가 원하지 않는 바를 남에게 행하지 말라. -공자


내 자신을 심하게 탓하고 남을 가볍게 책망하면, 원망을 멀리하게 된다. -공자


널리 배워서 뜻을 도탑게 하며, 간절하게 묻되 가까운 것부터 잘 생각하면 인(仁)이 그 속에 있다. -공자


높은 낭떠러지를 보지 않고서야 어찌 굴러 떨어지는 근심을 알고, 깊은 연못에 가지 않고서 어찌 빠져 죽는 근심을 알겠느냐? 큰 바다를 보지 않고서야 어찌 빠져 죽는 근심을 알겠느냐? 큰 바다를 보지 않고서야 어찌 풍파에 시달리는 근심을 알겠느냐? -공자


늙은 사람들은 편하게 모시고, 친구들은 진실한 마음으로 대하고, 젊은이는 부드럽게 대하게나. -공자


다른 사람을 대할 때 그 사람의 몸도 내 몸같이 소중히 여기라. 내 몸만 귀한 것이 아니다. 남의 몸도 소중하다는 것을 잊지 말라. 그리고 네가 다른 사람에게 바라는 일을 네가 먼저 그에게 베풀어라. -공자


다름 아닌 자신에게 전력을 다하고 충실하라. 자기를 내버려두고 남의 일에 정신이 팔려 있는 사람은 자신의 갈 길을 잃어버린 사람이다. -공자


다음의 네 가지를 두려워해야 한다. 첫째, 도의에서 벗어나는 것. 둘째, 학문을 게을리하는 것. 셋째, 정의를 듣고도 실행치 못하는 것. 넷째, 착하지 않음을 고치지 못하는 것. 이를 항상 두려워하고, 그렇지 않았을 때는 즉각 반성하고 고쳐야 한다. -공자


단 단한 돌이나 쇠는 높은 곳에서 떨어지면 깨지기 쉽다. 그러나 물은 아무리 높은 곳에서 떨어져도 깨지는 법이 없다. 물은 모든 것에 대해서 부드럽고 연한 까닭이다. 저 골짜기에 흐르는 물을 보라 그의 앞에 있는 모든 장애물에 대해서 스스로 굽히고 적응함으로써 줄기차게 흘러, 드디어는 바다에 이른다. 적응하는 힘이 자제로와야 사람도 그가 부닥친 운명에 굳센 것이다. -공자


당 신이 알고 있을 때는 똑똑히 알고 있다고 말하고 잘 모르고 있을 때는 솔직하게 모른다고 시인하는 것이 바로 참된 지식이다. -공자


대중의 소리를 막는 것은 강을 막는 것보다 어렵다. -공자


덕이 높은 사람은 외롭지 않다, 반드시 그를 따르는 이웃이 있기 때문이다. -공자


덕이 있으면 외롭지 않고 반드시 이웃이 있다. -공자


말하고자 하는 바를 먼저 실행하라. 그런 다음 말하라. -공자


맑은 거울은 형상을 살피게 하고, 지나간 옛일은 이제 되어질 일을 알게 한다. -공자


모든 것이 참되면 그것이 밖으로 나타나기 마련이다. -공자


모든 사람이 그를 좋아하더라도 반드시 살피고, 모든 사람이 그를 미워하더라도 반드시 살펴야 한다. -공자


모욕은 잊어버리고, 친절은 결코 잊지 말아라. -공자


무례 한 사람의 행위는 내 행실을 바로 잡게 해주는 스승이다. -공자


물이 너무 맑으면 고기가 없고, 사람이 너무 살피면 벗이 없다. -공자가어(孔子家語)


물처럼 스며드는 중상과 피부에 느껴지는 모략이 통하지 않는다면 가히 총명한 사람이라 할 수 있다. -공자


사는 것도 모르거늘 어찌 죽음을 알겠는가. -공자


사 람들이 알아주지 않아도 원망하지 않는 것은 또한 군자답지 아니한가. -공자


사람은 서로의 입장과 처지를 바꿔 생각해야 한다. -공자


사람은 어질더라도 배우지 않으면 현명해지지 못한다. -공자


사 람이 돈 있고 지위를 얻었을 때는 품격을 지키기는 쉽다. 불우하고 역경에 처했을 때, 그 품격이 시들지 않고 보전되어 있는 사람이 참된 품격의 사람이다. 그러므로 불행한 환경에 처했을 때일수록 처신을 잘해야 한다. -공자


사람이 큰 도덕의 영역에서 벗어나는 일은 없을지 모르지만, 작은 도덕의 영역에서 벗어나는 일은 꽤 많다. -공자


서로 위하는 마음. 네 스스로 원하지 않는 것을 다른 사람에게 행해서는 안 된다. -공자


썩은 나무에는 조각을 할 수 없고, 썩은 흙으로 쌓은 담은 흙손질을 할 수가 없다. -공자


선비가 도에 뜻을 두고서 나쁜 옷과 나쁜 음식을 부끄러워한다면 그런 사람과는 더불어 의논할 수가 없다. -공자


선을 보거든 아직도 부족한 것같이 하고, 악을 보거든 끓는 물을 만지는 것처럼 하라. -공자


선한 일을 하는 사람에게는 하늘이 복으로써 갚고, 악한 일을 하는 사람에게는 하늘이 화로써 갚는다. -공자


세 가지 길에 의하여 우리는 진리에 도달할 수가 있다. 그 하나는 사색에 의해서이다. 이것은 가장 높은 길이다. 둘째는 모방에 의해서이다. 이것은 가장 쉬운 길이다. 그리고 셋째는 경험에 의해서이다. 이것은 가장 고통스러운 길이다. -공자


세 사람과 같이 있을 때 그 중 두 사람은 나의 스승으로 삼을 수 있다. 한 사람이 좋은 말과 행동을 한다면 그것을 배울 것이고, 다른 한 사람의 말과 행동이 옳지 못하다면 그렇게 하지 않으려고 거울로 삼을 것이다. -공자


세 사람이 걸어가면, 반드시 나의 스승이 있다. -공자


순전(純全)한 완벽은 오직 신에게만 발견된다. 우리들의 삶이란 사실 신께 가까이 가는 과정으로 완성되는 것이다. 좋은 것이 좋은 것이며 악한 것이 악한 것이라는 사실을 아는 순간, 인간은 좋은 것에 가까이 간 것이며 그만큼 악에서 멀어지는 것이다. -공자


시를 읽음으로써 바른 마음이 일어나고, 예의를 지킴으로써 몸을 세우며, 음악을 들음으로써 인격을 완성하게 된다. -공자


신용을 잃어버리면 설 땅이 없게 된다. -공자


실 수하여 고치지 않으면, 곧 그것을 실수하고 만다. 실수하여 고치는 것을 꺼리지 말라. -공자


자기 반성을 엄중히 하고, 다른 사람을 꾸짖는 일을 가볍게 하면 남의 원망이 멀어진다. -공자


자기 자신을 존중함과 같이 남을 존중하자. 남이 자기 자신에게 해주기를 원하는 바 그것을 남에게 해줄 수 있는 사람은 진정한 사랑을 아는 사람이다. 이 세상에 그 이상 가치있는 것은 없다. 이것이 처세하는 최상의 비법인 것이다. -공자


자신의 가난함을 부끄럽게 여기는 일이야말로 정말 수치스러운 일이다. 오직 부끄러워 할 일은 가난을 극복하려고 노력하지 않는 일이다. -공자


자 연을 먼저 터득한 사람은 성인이다. -공자


작은 일을 참지 못하는 사람은 큰일에 낭패하고 만다. -공자


잘 못을 저지르고도 고치지 않는 것, 이를 잘못이라 이른다. -공자


젊은 시절은 일년으로 치면 봄이요, 하루로 치면 아침이다. 그러나 봄엔 꽃이 만발하고, 눈과 귀에 유혹이 많다. 눈과 귀가 향락을 쫓아가느냐, 부지런히 땅을 가느냐에 그 해의 운명이 결정된다. -공자


정치의 으뜸가는 요체는 국민의 신망을 얻는 것이다. -공자


죽 고 사는 것은 명(命)에 있고, 부(富)하고 귀하게 되는 것은 하늘에 있다. -공자


중용의 덕은 지극한 것이다. -공자


즐거워하되 즐거움에 빠지지 않고, 슬퍼하되 상심하진 말라. -공자


지혜 를 얻는 데는 세 가지 방법이 있다. 첫 번째 방법은 사색에 의한 것으로, 가장 고상한 방법이다. 두 번째는 모방으로 가장 쉬우나 만족스럽지 못한 방법이다. 세 번째는 경험을 통해 얻는 방법으로 가장 어려운 것이다. -공자


진실과 신의를 지키고 정의를 향하여 나아가는 것이 덕을 높이는 길이다. 세상 사람들은 사랑하면 그가 살기를 원하고, 미우면 그가 죽기를 원한다. 도대체 남이 살기를 원하고 또 죽기를 바라는 것은 모순이 아니고 무엇인가. 덕 있는 사람은 이런 모순을 범하지 않는다. 군자의 덕은 바람이요, 소인의 덕은 풀이다. 풀은 바람이 불면 반드시 바람에 쏠려 따르게 마련이다. 군자의 덕을 본받자. -공자


진 실은 모든 존재의 근원이며 종말이다. -공자


진짜 결점은 자신의 결점을 알면서도 고치려고 노력하지 않는 것이다. -공자


집안에 예절이 있으므로 어른과 아이의 분별이 있고, 규문(閨門)에 예가 있으므로 삼족(三族)이 화목하다. 조정에 예가 있으므로 삼족이 화목하다. 조정에 예가 있으므로 벼슬에 차례가 있고, 사냥에도 예가 있으므로 융사(戎士, 병사)가 숙련되고, 군대에 예가 있으므로 무공(武功)이 이루어진다. -공자


착한 사람과 함께 있으면 마치 지란(芝蘭)의 방에 들어간 것 같아서 오래 되면 그 향기를 느끼지 못하니 더불어 그에게 동화된 것이다. 착하지 않은 사람과 함께 있으면 마치 절인 생선가게에 들어간 듯하여 오래 되면 그 냄새를 느끼지 못하니 ? 피? 더불어 동화된 것이다. 단(丹)을 지니면 붉어지고, 칠을 지니면 검어지니 군자는 반드시 자기와 함께 있는 자를 삼가야 한다. -공자


착한 일을 하는 자에게는 하늘이 복으로 갚고, 악한 일을 하는 자에게는 하늘이 재앙으로 갚는다. -공자


천자가 참으면 나라에 해가 없고, 제후가 참으면 큰 일을 이루고, 관리가 참으면 승진하고, 형제가 참으면 집안이 부귀하고, 부부가 참으면 일생을 해로하고, 친구끼리 참으면 이름이 깎이지 않고, 자신이 참으면 재앙이 없어진다. -공자


첫째, 젊을 때에는 혈기가 안정되니 못한 지라 색(色)을 경계해야 한다. 둘째, 장년에 이르면 바야흐로 혈기가 강한 지라 투쟁을 경계해야 한다. 셋째, 노년에 이르면 혈기가 이미 쇠한 지라 탐욕을 경계해야 한다. 이 세 가지를 경계하며 살아가는 것이 가장 손쉽고 성공적인 처세의 지름길이다. -공자


총명하고 생각이 뛰어나도 어리석은 듯함으로 지켜야 하고, 공덕이 천하를 덮더라도 겸양하는 마음으로 지켜야 한다. 용맹이 세상을 진동하더라도 겁내는 듯함으로 지켜 나가며, 부유함이 사해(四海)를 차지했다 하더라도 겸손함으로써 지켜야 한다. -공자


큰 도가 행해지면 사람은 자기 부모만을 부모로 생각하지 않고, 자기 자식만을 자식으로 생각하지 않는다. -공자


평소에 공손하고, 일을 하는데 신중하고, 사람을 대하는데 진실하라. 그러면 비록 오랑캐 땅에 간다 할지라도 버림받지 않을 것이다. -공자


하나님은 존경하되 멀리하라. 그것이 백성의 바른 길이다. -공자


하늘에 죄를 지으면 용서를 빌 곳이 없다. -공자


하늘은 말하지 않는다. 사시가 운행되고 만물이 잘 자라고 있는데 하늘이 무엇을 말하랴. -공자


하늘의 이치를 따르는 자는 살고, 하늘의 이치를 거역하는 자는 망한다. -공자


학 문을 아는 자는 이를 좋아하는 사람만 못하고 학문을 좋아하는 자는 이를 즐기는 자만 못하다. -공자


학문을 좋아하는 자와 함께 가면 마치 안개 속을 가는 것과 같아서, 비록 옷은 젖지 않더라도 때때로 물기가 배어든다. 무식
한 자와 함께 가면 마치 뒷간에 앉은 것 같아서, 비록 옷은 더럽혀지지 않지만 그 냄새가 맡아진다. -공자가어(孔子家語)


한 말은 반드시 실행에 옮기고 실행하면 반드시 성과를 거두라. -공자


항상 겸손하라. 겸양과 친절은 곧 예의 기본이다. -공자


현명한 사람은 모든 것을 자신의 내부에서 찾고, 어리석은 사람은 모든 것을 타인들 속에서 찾는다. -공자


화살이 과녁을 벗어나면 훌륭한 사수는 다른 사람에게 핑계를 돌리지 않고 자신의 솜씨를 탓한다. 현명한 사람도 이와 같이 행동한다. -공자


효자의 어버이 섬김은 살아서는 공경을 다하고, 봉양함에는 즐거움을 다하고, 병드신 때에는 근심을 다하고, 돌아가신 때는 슬픔을 다하고, 제사 지낼 때엔 엄숙함을 다해야 한다. -공자


'나의 생각' 카테고리의 다른 글

[공자 명언 모음]  (1) 2010.06.17
  1. planchas ghd 2013.07.27 07:15

    귀를 기울여봐 가슴이 뛰는 소리가 들리면 네가 사랑하는 그 사람 널 사랑하고 있는거야.


출처 그대 강철빤쓰를 믿는가!!  ̄▽ ̄)乃 낄낄~ | 강철빤쓰
원문 http://blog.naver.com/bagagy/60032111167

1. 들어 가는 말

    지난 호에서는 MySQL을 이용하여 간단하게나마 슈퍼 마켓에서 매출을 관리하는 예를 들어 SQL 언어에 대해서 간단히 살펴 보았다.
    기본적인 SQL 언어만 알아도 MySQL의 Client 프로그램을 이용하여 Data들을 관리할 수 있다. 하지만, SQL 언어를 모르거나 컴퓨터를 잘 다루지 못하는 사람들은 Database를 어떻게 쓸 수 있을까. 우리네 슈퍼마켓 아저씨들 중에서 SQL 언어를 사용해서 매출을 관리할 수 있을 아저씨들이 얼마나 있을까.

    이번 호에서는 MySQL의 C API에 대해서 알아본다. MySQL의 C API를 이용하면 C나 C++로 짠 프로그램에서 MySQL 서버에 접속을 할 수 있다. (우리네 슈퍼마켓 아저씨는 어려운 SQL 언어를 배울 필요가 없다. 슈퍼마켓 아저씨를 낮춰보는 것이 아니므로 항의하는 일이 없었으면 한다.)

    MySQL과 연동하는 방법은 C API만이 전부는 아니고, PHP3, Perl, MyODBC, JDBC를 이용하여 연동을 할 수 있다. 이번 회에서는 C API에 대해서 알아보도록 하고, 다음 회에서 PHP3와 MySQL을 이용하여 Web과 연동하는 법에 대해 알아 보도록 한다.

 

2. 프로그램 컴파일

    C API를 소개하기 전에 일단 gcc의 옵션을 잘 모르는 독자를 위해 MySQL의 C API를 이용한 소스 코드를 컴파일하는 방법에 대해서 알아 본다.
    먼저 다음의 예제를 보자.

      #include <stdio.h>
      #include <mysql.h>
      #include <errno.h>

      void main(void)
      { MYSQL mysql ;
              
      mysql_init(&mysql) ;

      if(!mysql_real_connect(&mysql, NULL, “사용자이름”,”암호”, NULL ,3306, (char *)NULL, 0))
      {printf(“%s\n”,mysql_error(&mysql));
      exit(1) ;}
      printf(“성공적으로 연결되었습니다.\n”) ;
      mysql_close(&mysql) ;}

    위의 예제는 MySQL 서버에 접속을 하여, 연결이 제대로 되었을 경우 ‘성공적으로 연결되었습니다.’라는 메시지를 출력하고 종료하는 프로그램이다. 연결이 안 될 경우 왜 연결이 안 되는지 출력을 하게 된다. 에러가 나면 글의 후반부에 있는 ‘문제 해결’ 부분을 읽어 보길 바란다. 일단 위에 나온 함수들의 설명은 뒤에 하고 컴파일을 하는 방법에 대해 알아 보자.
    위의 소스 코드가 con.c라고 가정을 한다면 다음과 같이 프롬프트에서 명령을 내리면 된다.

      $ gcc -o con con.c -I/usr/local/include/mysql
      -L/usr/local/lib/mysql -lmysqlclient

    이렇게 하면 con이라는 실행 파일이 생기게 된다. -I 옵션은 헤더 파일의 경로를 정한다.
    즉, mysql.h의 경로를 지정해 주며, 필자의 경우 그 경로가/usr/local/include/mysql이다. 각자 경로가 틀리므로 자신에게 맞게 변경을 하자. 보통 /usr/include/mysql 혹은/usr/local/include/mysql 혹은 /usr/include에 존재한다. /usr/include에 있을 경우에는 이 옵션을 주지 않아도 된다.
    -l 옵션은 링킹시 사용할 라이브러리를 지정한다.
    MySQL의 C API를 사용했을 경우에는 꼭 -lmysqlclient라는 옵션을 주어야 한다.

    -L/usr/local/lib/mysql 옵션은 mysqlclient라는 라이브러리의 경로를 지정한다. 이 옵션도 사용자에 따라 다르다. MySQL의 설치 시에 어디에 라이브러리를 설치했는지 확인하자. 지정된 디렉터리 밑에서 libmysqlclient.so 라는 라이브러리를 볼 수 있다.
    옵션과 경로를 잘 지정해 주었다면 위의 소스 코드는 잘 컴파일 되고, 실행시에 MySQL 데몬이 실행 중이라면 ‘성공적으로 연결되었습니다.’라는 메시지를 볼 수 있을 것이다.

 

3. C API 자료형

    C API에서 쓰이는 자료형에 대해서 알아 보자.
    MYSQL : Database와의 연결을 관리하는 구조체이다. 대부분의 C API 함수에서 쓰인다.
    MYSQL_RES : SELECT 등 결과를 리턴하는 query의 결과를 나타내는 자료형이다.
    MYSQL_ROW : MYSQL_RES에서 하나의 레코드씩 값을 얻어 올때 쓰이는 자료형이다.
    MYSQL_FIELD : 필드의 이름과 필드의 타입 등 필드에 관한 정보를 저장하는 자료형이다.

 

4. MySQL Database에 연결하기

    이제 모든 준비를 마췄으므로 본격적으로 MySQL C API에 대해서 알아보자.

    * MYSQL* mysql_init(MYSQL *mysql)
    mysql_real_connect()를 위하여 MYSQL 객체를 초기화한다. 특별히 하는 일은 없으나 MYSQL 객체를 초기화 하므로 mysql_real_connect() 전에 꼭 호출하여 주자.

    * MYSQL* mysql_real_connect(MYSQL* mysql, const char* host, const char* user,
    const char* passwd, const char* db, uint port, const char* unix_socket, uint client_flag)
    host에 지정된 서버로 연결을 시도하는 함수이다.
    mysql_get_client_info() 함수를 제외한 모든 API 함수를 사용하기 전에 꼭 호출하여야 하는 함수이다.
    각 인자에 대한 설명은 다음과 같다.

    mysql    : MYSQL 변수에 대한 포인터 형이다.
    host      : 연결하고자 하는 서버의 IP Address 혹은 도메인 이름을 적어주면 된다.
                  NULL로 적어주면 localhost를 의미한다.
    user      : 접속시의 사용자 이름이다. NULL이면 현재 login한 user ID가 된다.
    passwd : user의 암호를 나타낸다. NULL이면 암호가 없다는 의미이다.
    db        : 접속시에 사용하고자 하는 database를 나타낸다. NULL로 지정을 하면 연결 후에
                  mysql_select_db() 혹은 mysql_query()를 이용해서 지정할 수 있고,
                  database를 바꿀 수도 있다.
    port       : TCP/IP 연결시에 사용할 포트 번호를 나타낸다.
    unix_socket : 보통 NULL로 하면된다.
    client_flag   : 이 인자도 보통 0으로 해주면 된다.
    mysql_real_connect()는 성공적으로 연결이 되면, MYSQL 포인터를 넘겨주고 연결에 실패하였을 경우 NULL을 리턴한다.
    연결에 실패 하였을 경우 이 글이 마지막에 있는 ‘문제 해결’부분을 읽어 보길 바란다. 사용 예는 위의 예제에 나와있다.

    * void mysql_close(MYSQL* mysql)
    서버와의 연결을 끊고 mysql에 할당되었던 메모리를 해제한다.

 

5. Query와 결과 값을 얻어 오기

    서버와 성공적으로 연결이 되었다면, 이제 원하는 Query를 하고, 그 결과 값을 얻어 올 수 있다. Query를 할 수 있는 함수는 mysql_query()와 mysql_real query() 두 가지가 있다.

    * int mysql_query(MYSQL* mysql, const char* query) :
    query를 실행 시킨다. mysql 클라이언트에서 했던 것 처럼 query의 끝에 ‘;’가 포함되어서는 안 된다. query의 끝은 NULL 문자(‘\0’)이다. 따라서 바이너리 데이타가 섞인 query는 수행을 할 수 없다. 바이너리 데이타가 섞은 query의 중간에는 NULL 문자가 올 수도 있기 때문이다. query를 성공적으로 마췄다면 0을 리턴한다.

    * int mysql_real_query(MYSQL* mysql, const char* query, unsigned int length) :
    mysql_query()는 query의 끝을 NULL 문자로 구분을 하는데 반해, mysql_real_query는 query의 끝을 length 만큼의 길이로 구분한다. 따라서 NULL 문자를 포함한 바이너리 데이타가 있는 query도 수행을 할 수 있다. 또한mysql_query()는내부적으로strlen()를 사용하지만, mysql_real_query()는 그렇지 않으므로 좀더 빠르다. 마찬가지로 query를 성공적으로 수행하였을 경우 0을 리턴한다.
    위의 함수를 이용하여 query를 수행할 수가 있다. 호출하는 방법은 매우 간단하다.

    mysql_query(&mysql, “SELECT * FROM dome”) ;
    위와 같이 query를 수행하면 된다.
    query를 성공적으로 수행했다면, 이제 결과 값을 얻어 와야 한다.

    * MYSQL_RES* mysql_store_result(MYSQL* mysql)
    * MYSQL_RES* mysql_use_result(MYSQL* mysql)
    위 두 함수는 모두 서버로부터 결과 값을 얻어 오는데, 차이는 query의 결과로 리턴되는 ROW들을 한꺼번에 모두 서버로부터 얻어 올 것인지(mysql_store_result()), 혹은 한번에 한 개의 ROW를 얻어 올 것(mysql_use_result())의 차이이다.
    mysql_store_result()는 ROW들을 한 꺼번에 모두 얻어와 클라이언트의 메모리에 저장을 한다. 따라서 매번 ROW를 얻어 오기 위해 서버에 접근을 할 필요가 없으므로 속도가 빠르다. 대신 결과로 넘어온 ROW의 크기가 클 경우 많은 메모리가 필요하게 된다.
    또 mysql_store_result()의 장점은 mysql_data_seek()나 mysql_row_seek()를 이용하여 현재 ROW에서 앞이나 뒤의 ROW로 자유자재로 왔다 갔다 할 수 있다는 것이다. 또한, mysql_num_rows()를 이용하여, 몇 개의 ROW가 리턴됐는지도 알 수 있다.

    반면 mysql_use_result()는 한 번에 한 개의 ROW를 서버로부터 가져 온다. 따라서 메모리를 많이 사용하지 않는다. 하지만, mysql_store_result()와 같은 장점을 가지 지 않는다. 리턴되는 ROW가 특별히 크지 않은 경우라면, 보통 mysql_store_result()를 호출하는 것이 좋다.

    결과 값을 얻어 왔다면 mysql_fetch_row()를 이용하여 각각의 row에 있는 데이타 들에 접근을 할 수 있다.

    MYSQL_ROW mysql_fetch_row(MYSQL_ROW* result)
    result에 있는 ROW들에서 한 개의 ROW를 얻어 온다. 한 개의 ROW에서 각각의 field는 배열 형태로 들어 있다. 더 이상 가져올 ROW가 없으면 NULL을 리턴한다.
    현재의 result에 몇 개의 field가 있는지 mysql_num_fields()를 이용하여 알 수 있다.

    int mysql_num_fields(MYSQL_RES* result)

    이제 실제로 query를 수행하고, 결과를 출력하여 보자.

      1:      #include <stdio.h>
      2:      #include <mysql.h>
      3:      #include <errno.h>
      4:
      5:      void main(void)
      6:      {
      7:      MYSQL           mysql ;
      8:      MYSQL_RES*      res ;   
      9:      MYSQL_ROW       row ;
      10:     int             fields ;
      11:             
      12:     mysql_init(&mysql) ;
      13:     
      14:     if(!mysql_real_connect(&mysql, NULL, “사용자”,”암호”, “test”, 3306, (char *)NULL, 0))
      15:     {
      16:     printf(“%s\n”,mysql_error(&mysql));
      17:                       exit(1) ;
      18:             }
      19:     
      20:     if(mysql_query(&mysql, “USE super”) )
               // mysql_query()는 query 수행시에 에러가 나게 되면
               // 0이 아닌 값을 리턴한다.
      {
      printf(“%s\n”, mysql_error(&mysql) ;
                              exit(1) ;
                        }
      21:     if(mysql_query(&mysql, “SELECT * FROM dome”) )
      {
      printf(“%s\n”, mysql_error(&mysql) ;
                              exit(1) ;
                                          }
      22:     
      23:     res = mysql_store_result( &mysql ) ;
      24:     fields = mysql_num_fields(res) ;
      25:     
      26:     while( ( row = mysql_fetch_row( res ) ))
      27:             {
      28:     for( cnt = 0 ; cnt < fields ; ++cnt)
      29:     printf(“%12s “, row[cnt]) ;
      30:
      31:     printf(“\n”) ;
      32:             }
      33:
      34:       mysql_free_result( res ) ;
      35:     mysql_close(&mysql) ;
      36:     }

    12번 줄은 mysql_init()을 이용하여 MYSQL 객체를 초기화 하는 과정이다.
    14번 줄은 실제 MySQL 서버와 연결을 시도하는 부분이다. 연결에 실패하였을 경우 NULL을 리턴하므로 15-18번 줄에서, 에러 메시지를 출력하고 프로그램을 종료한다.
    20번 줄은 “use super”라는 query를 이용하여 ‘super’ database를 사용하도록 지정하는 부분이다. 사용하고자 하는 database는 mysql_real_connect()에서 지정을 할 수도 있고, 프로그램 실행 중에 바꾸고자 할 때는 “use DB이름”과 같은query를 이용할 수도 있고, 혹은 mysql_select_db()를 이용할 수도 있다.
    21번 줄은 “SELECT * FROM dome” query를 수행하는 부분이다. dome 테이블의 모든 컬럼을 리턴하도록 하였다.
    23번 줄은 mysql_store_result()를 이용하여 결과 값을 저장하는 부분이다.
    24번 줄은 mysql_num_fields()를 이용하여 현재 결과 값에 몇 개의 필드가 있는지 알아 내는 부분이다.
    mysql_fetch_row()는 배열 형태로 각 필드에 접근을 하므로 배열의 인덱스를 알아내야 정확히 데이타들을 얻어 올 수 있다.

    26-32번 줄은 실제로 각 ROW들로부터 데이타에 접근하는 부분이다.
    mysql_fetch_row()를 이용하여 각각의 ROW를 얻어 온다(더이상 가져올 ROW가 없으면 NULL을 리턴한다.) 그 후 for문을 이용하여 ROW의 각각 필드를 출력하였다.
    34-35번 줄은 res와 mysql에 할당된 메모리를 해제하는 부분이다. 하지만, query에 따라서는 ROW를 리턴하지 않는 query도 있다(UPDATE나 DELETE등) 만약 위의 예제에서 사용자가 UPDATE 등을 수행하였다면, 곧바로 Segmentation Fault가 나버린다.
    이때는 mysql_num_fields()를 이용하여, 필드의 개수를 조사함으로써 문제를 해결할 수 있다. query가 ROW를 리턴하였다면, 필드는 최소한 0보다 크기 때문이다. 다음의 예제를 보자(참고로 밑의 예제는 MySQL Manual 19장에서 발췌한 내용이다.)

      MYSQL_RES*      result ;
      unsigned        int num_fields ;
      unsigned        int num_rows ;

      if(     mysql_query(&mysql, query_string) )
      {
      // mysql_query는 성공적으로
      // 수행할 경우 0을 리턴하므로
      // 이 부분이 실행 된다면
      // 에러가 있다는 의미이다.
      // 적절한 에러 핸들링을 해주다.
      }
      else // 성공적으로 query가 수행되었다.
      {
      result = mysql_store_result( &mysql ) ;
      if( result ) // 리턴된 ROW가 있다 !
      {
      num_fields = mysql_num_fields( result ) ;
      // row의 값을 얻어오는 루틴을 집어 넣으면 된다.
      // 마지막에는 mysql_free_result()를 이용하여
      // 메모리를 해제시키자.
      }
      else // 리턴된 ROW가 없다. ROW를 리턴하지 않는 query를 수행하였는지 // 혹은 query의 수행 중에 에러나 났는지 알 수 없다.
      {
      if( mysql_num_fields( &mysql ) == 0)
      {
      // ROW를 리턴하지 않는 query를 수행하였다.
      num_rows = mysql_affected_rows( &mysql ) ;
      // mysql_affected_rows()는 DELETE 등의
      // query에서 몇 개의 ROW가 영향을 받았는지
      // 리턴하는 함수이다.

      }
      else // 무언가 잘못된 것이 있다.
      fprintf(“stderr,Error : %s\n”,mysql_error( &mysql) ) ;
      // mysql_error()는 가장 최근의 에러를 리턴하는 함수이다.
      }
      }
      위의 예제를 이용하여, 결과를 리턴하는 query 든지(SELECT), 아니면 결과를 리턴하지 않는 query 든지(UPDATE, DELECT 등) 어떠한 query라도 수행을 성공적으로 할 수 있다.

    이번에는 ROW의 필드에 대한 정보를 얻어 오는 방법에 대해서 알아 보자. MYSQL_FIELD라는 구조체가 쓰이는데 MYSQL_FIELD에는 다음과 같은 멤버들이 있다.

    char* name  : 필드의 이름을 나타낸다.
    char* table : 현재 필드의 테이블 이름을 나타낸다.
    char* def   : 필드의 기본값을 나타낸다.

    MYSQL_FIELD* mysql_fetch_field( MYSQL_RES* result )
    : 한번에 하나의 MYSQL_FIELD를 리턴한다.
    MYSQL_FIELD* mysql_fetch_fields(MYSQL_RES* result )
    : 배열의 형태로 모든 MYSQL_FIELD를 리턴한다.

      예1)
      MYSQL_FIELD *field ;
      while( (field = mysql_fetch_field(result) )
      printf(“field name %s\n”, field -> name ) ;

      예2)
      unsigned int num_fields ;
      unsigned int i ;
      MYSQL_FIELD* fields ;
      num_fields = mysql_num_fields( result ) ;
      fields = mysql_fetch_fields( result ) ;
      for( i = 0 ; i < num_fields ; ++i)
      printf(“field name %s\n”, fields[i].name) ;

    위의 두 개의 예는 모두 동일안 결과를 출력한다.

 

6. 문제 해결

    이번에는 C API를 사용한 소스 코드를 컴파일할 때 혹은, 실행 도중 만나는 문제점을 해결할 수 있는 방법을 설명한다.

      1) “con.c:2: mysql.h: 그런 파일이나 디렉토리가 없음”
         이 경우는 mysql.h의 경로를 찾을 수 없어서 나는 에러이다. 다시 한번

         mysql.h가  어디에 있는지 확인을 하고, -I옵션으로 그 경로를 지정해 주자.

         -I옵션과 경로는 붙여 써야 한다.

      2) “con.o(.text+0x11): undefined reference to `mysql_init’”  위와 같이
          ‘undefined reference......’ 라고 나오는 에러는 -lmysqlclient 옵션을 주지 않았

          기 때문이다.

      3) “ld: cannot open -lmysqlclient: 그런 파일이나 디렉토리가 없음” 위의 에러는
          -L옵션 뒤에 붙은 라이브러리의 경로가 잘못 되었기 때문이다.
            libmysqlclient.so 파일의 경로를 찾아서 그 경로로 지정을 해 주자.
          -I 옵션과 마찬가지로 -L과 경로는 붙여 써야 한다. 위의 에러들은 컴파일시

            에 옵션이 잘못되었을 경우 나오는 에러 메시지이다.
            계속해서 프로그램의 실행 중에 나오는 에러 메시지를 보자.

      4) “Can’t connect to local MySQL server (2)”
          위의 에러는 MySQL의 서버에 연결을 할 수 없다는 메시지로서, MySQL 서

          버의 데몬이 실행 중이지 않을 때 나오는 메시지이다.
          safe_mysqld 명령 등을 이용하여 데몬을 실행시켜 주자.

      5) “Access denied for user: ‘rot@localhost’ (Using password: YES)”
          접근이 금지되었다는 메시지로서 사용자 아이디를 잘못 입력하였 거나, 혹

          은 암호를 잘못 입력하였을 때 나오는 메시지이다.
          MySQL의 사용자는 모두 mysql database의 user 테이블에 있으므로, 참고를

          하여 적도록 하자.

      6) “error in loading shared libraries libmysqlclient.so.6:
          cannot open shared object file: No such file or directory”
          MySQL의 라이브러리를 열지 못한다는 메시지이다. 컴파일 할 때 MySQL의

          동적 라이브러리를 사용하느데, 동적 라이브러리이므로 실행시에도 라이브

          러리가 필요하게 된다.
          libmysqlclient.so가 /usr/lib 혹은 /usr/lib/mysql 디렉터리에 존재하지 않을

          경우에 발생하는 문제이다. 가장 간단한 해결법으로는 모든 MySQL 라이브

          러리를 /usr/lib/나 /usr/local/lib밑으로 복사하는 것인데 별로 추천하는

          방법은 아니다.
          두 가지 방법이 있는데,
          먼저 시스템의 운영자라면, /etc/ld.so.conf 파일에libmysqlclient.so가있는 경

         로를 적어 준 후에 ldconfig 라는 명령을 프롬프트에서 실행하여 주면 된다.

         이러한 권한이 없는 일반 사용자라면, 자신의 쉘의 환경 변수를 이용하면 된

         다.
          각자의 쉘이 맞게, LD_LIBRARY_PATH를 libmysqlclient.so가 있는 디렉터리로
          지정을 해주자.
          C 쉘 사용자는  setenv LD_LIBRARY_PATH  경로명 본 쉘 사용자는
          export LD_LIBRARY_PATH  경로명 이렇게 해주면 된다.
          보통 위의 6개의 에러가 가장 많이 발생한다. 혹시, 해결하지 못할 에러가 있

          다면 필자에게 메일을 보내면 친절히 답변해 주겠다.

 

7. Quick Reference

    위에서 설명한 API만을 가지고도 서버에 연결하여, Query를 수행하고 그 결과를 확인 할 수 있다. 이번에는 C API의 중요한 함수들을 모아서, 함수의 프로토타입과 함수의 기능들에 대해서 간단히 알아도록 하자. 개인적으로 그다지 중요하지 않다고 생각되는 함수들은 제외시켰으므로 모든 C API를 보고 싶은 독자들은 MySQL Reference Manual의 19장을 보기 바란다. 참고로 필자가 참조한 메뉴얼은 MySQL 3.22.21용 메뉴얼이었다.

      1) my_ulonglong mysql_affected_rows(MYSQL* mysql) INSERT, UPDATE, 

          DELETE 등의 query로 영향을 받은 ROW의 수를 리턴한다.

      2) void mysql_close(MYSQL* mysql) 서버와의 연결을 종료한다.

      3) void mysql_data_seek(MYSQL_RES* result, unsigned int offset) result에서 임

          의의 ROW에 접근을 하도록 하는 함수이다.
          offset이 row의 번호를 나타낸다. 0이면 처음 ROW,  mysql_num_rows( result )

          - 1 은 마지막 row를 나타낸다.

      4) unsigned int mysql_errno(MYSQL* mysql) 가장 최근에 mysql에 일어난 에러

          의 번호를 리턴한다.

      5) char* mysql_error(MYSQL* mysql) 가장 최근에 일어난 에러 메시지를 리턴

          한다.

      6) MYSQL_FIELD*mysql_fetch_field(MYSQL_RES* result)한번 호출할 때마다 하

          나의 필드에 대한 정보를 리턴한다.

      7) MYSQL_FIELDS*mysql_fetch_fields(MYSQL_RES* result) 배열형대로 result

          의 필드에 대한 정보를 한꺼번에 리턴한다.

      8) MYSQL_FIELD_OFFSETmysql_field_seek(MYSQL* mysql,     MYSQL_FIELD_OFFSET offset)임의의 필드에 접근을 하도록 한다.

      9) MYSQL_FIELD_OFFSETmysql_field_tell(MYSQL_RES* result)현재 필드의

          offset을 리턴한다.

      10) void mysql_free_result(MYSQL_RES* result) result에 할당된 메모리를 해제

           한다.

      11) MYSQL* mysql_init(MYSQL* mysql) mysql 객체를 초기화한다.
            인자가 NULL이면 새로운 MYSQL 객체를 생성하고, 초기화하여 리턴한다.

      12) MYSQL_RES* mysql_list_dbs(MYSQL* mysql, const char* wild)
            현재 서버에 있는 데이타베이스의 목록을 리턴한다. wild는 MySQL에서 사

           용할 수 있는 정규식을 나타낸다.     
           result = mysql_list_dbs( &mysql,”%” )는 모든 데이터베이스를 리턴하는 예이

           다. (%는 ‘모든’을 나타낸다)

      13) MYSQL_RES* mysql_list_tables(MYSQL* mysql, const char* wild)
            현재 데이타베이스에 있는 테이블들의 목록을 리턴한다.

      14) unsigned int mysql_num_fields(MYSQL_RES*result) 혹은
            unsigned int mysql_num_fields(MYSQL* mysql)필드의 수를 리턴한다.

      15) my_ulonglong mysql_num_rows(MYSQL_RES* result) result에 총 몇 개의

           ROW가 있는지 리턴한다. query 수행 후 mysql_store_result()를 호출하였을

           경우에만 사용 할 수 있고, mysql_use_result()는 사용할 수 없다.

      16) int mysql_ping(MYSQL* mysql) 서버에 연결 중인지를 리턴한다. 연결이 끊

           어 졌을  경우, 다시 연결을 시도한다. 서버와 연결을 한 후, 오랫동안 가만

           히 있으면 서버가 연결을 끊어버리는데, 이런 경우에 사용한다.

      17) int mysql_query(MYSQL* mysql, const char* query) query가 포인트 하는 쿼

           리를 수행한다. query의 끝은 NULL 문자이다. 성공적으로 query를 수행하였

           으면 0을 리턴한다.

      18) MYSQL* mysql_real_connect(MYSQL* mysql, const char* host,
            const char* user, const char* passwd, const char* db, uint port,
            const char* unix_socket, unit client_flag ) host와의 연결을 시도한다.
            인자별로 자세한 설명은 윗 부분에 되어 있다.

      19) int mysql_real_query(MYSQL* mysql, const char* query,
            unsigned int length)mysql_query()처럼 query를 수행하나, query의 끝이

            legnth인 것이 다르다.

      20) MYSQL_ROW_OFFSET mysql_row_tell(MYSQL_RES* result)현재 ROW의 

           offset을 리턴한다.

      21) int mysql_select_db(MYSQL* mysql, const char* db)사용하고자 하는

           database 를 변경한다.
           mysql_query()를 이용하여 “use db이름”의 query를  수행해도 같은 결과를 
           얻는다.

      22) int mysql_shutdown(MYSQL* mysql) 서버를 종료시킨다.
            현재 사용자에게 shutdown 권한이 있어야 한다.

      23) MYSQL_RES* mysql_store_result(MYSQL* mysql) query의 수행결과를 서버

           로 부터 한 번에 모두 받아 온다.

      24) MYSQL_RES* mysql_use_result(MYSQL* mysql) query의 수행결과를 서버

           로부터 한 개의 ROW 씩 받아 온다.

      [출처] [본문스크랩] MYSQL C API|작성자 JS

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

MYSQL C API  (0) 2009.12.02
[데이터베이스] 윈도우에서 mysql에 접근하여 사용하는법  (0) 2009.12.02
윈도우에서 mysql에 접근하여 사용하는법
1. 우선 윈도우에서 mysql을 설치하되 프로그램을 설치할때 Select type에서 complete를 선택하여 프로그램을 설치한다.(Typical로 설치하면 개발에 관련된 파일들이 설치되자 않고 mysql을 사용하는데 필요한 파일만 깔리므로 사용자 정의로 들어가 추가하여도 되지만 그냥 편하게 전체 설치를 하는 것을 권장한다.)

그다음은 이곳을 참조하여 프로그램을 설치하도록 하자

정보문화사 블로그 : Window에서 MySql설치하기
[http://blog.naver.com/infopub/100052196232]

자 이제 일단 MySql에 대한 준비는 끝이 났다.

그럼 프로그램을 위한 세팅을 해야 할 차례이다.

우선 사용 프로그램은 Visual C++ 6.0을 기준으로 설명을 진행하겠다.

우선적으로 Visual C++을 실행시켜 Tools의 메뉴중 Options를 선택한다.

그러면 창이 뜨는데 그중 탭에서 Directories탭을 선택한다.

그럼 창에 2가지의 드롭다운 메뉴와 1가지의 에티트 창이 보일 것이다.
그중 첫번째 드롭다운 메뉴를 건드릴 필요는 전혀 없고, 두번째 드롭다운 메뉴인 Show directories for: 에서 설정을 추가해야 하는데 아래 그림과 같이 추가를 한다.
1. 우선적으로 Include files에서 설치한 MySql의 Include폴더를 추가시킨다.

2. Show directories for: 를 Library files로 바꾼후 MySql의 Lib폴더를 추가시키고, 추가로 Lib폴더 아래에 있는 opt폴더를 추가시키자. opt폴더를 추가시키지 않으면 libmysql.lib파일이 없다는 에러를 한동안 맞이해야 할 것이다. :)

이제 옵션의 세팅은 끝이 났다.

그러면 이제 프로그램을 타이핑 하기 전에 다음과 같이 프로젝트 세팅에 적어주자

이제 프로그램을 타이핑을 해보자.
* 코드는 c로 작성되었고, console환경에서 프로그램을 실행시켰다.
아래 소스코드는 지돌스타
#define SOCKET int

#include <string.h>
#include <stdio.h>
#include <mysql.h>     // mysql.h를 제일 밑에 두지 않으면 에러가 발생합니다. 주의하세요

// 아래 define문은 자신의 환경에 맞추어 세팅을 해주어야 합니다.
#define DB_HOST "호스트 IP, 도메인 또는 localhost"
#define DB_USER "접속 ID"
#define DB_PASS "접속 암호"
#define DB_NAME "DB명"

#define SQL_CREATE_TABLE "CREATE TABLE `mysql_api_test` (\
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,\
    `num` INT NULL ,\
    `string` VARCHAR( 40 ) NULL \
    ) TYPE = MYISAM ;" //"
#define SQL_INSERT_RECORD "INSERT INTO `mysql_api_test` ( `id` , `num` , `string` ) \
    VALUES (\
    NULL , '%d', '%s'\
    );" //"
#define SQL_SELECT_RECORD "SELECT * FROM `mysql_api_test`"
#define SQL_DROP_TABLE "DROP TABLE `mysql_api_test`"

int main()
{
    MYSQL *connection=NULL, conn;
    MYSQL_RES *sql_result;
    MYSQL_ROW sql_row;
    int query_stat;
    int i;

    char query[255];

    mysql_init(&conn);

    // DB 연결
    connection = mysql_real_connect(&conn, DB_HOST,
        DB_USER, DB_PASS,DB_NAME, 3306,(char *)NULL, 0);
    if(connection==NULL)
    {
        fprintf(stderr, "Mysql connection error : %s", mysql_error(&conn));
        return 1;
    }

    // 테이블 생성
    query_stat=mysql_query(connection,SQL_CREATE_TABLE);
    if (query_stat != 0)
    {
        fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
        return 1;
    }

    // 레코드 삽입
    for(i=0;i<5;i++)
    {
        sprintf(query,SQL_INSERT_RECORD,100+i,"Mysql세팅 어렵다 어려워~");
        query_stat = mysql_query(connection, query);
        if (query_stat != 0)
        {
            fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
            return 1;
        }
    }

    // 셀렉트
    query_stat=mysql_query(connection,SQL_SELECT_RECORD);
    if (query_stat != 0)
    {
        fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
        return 1;
    }

    // 결과 출력
    sql_result=mysql_store_result(connection);
    while((sql_row=mysql_fetch_row(sql_result))!=NULL)
    {
        printf("%2s %2s %s\n",sql_row[0],sql_row[1],sql_row[2]);
    }
    mysql_free_result(sql_result);

    // 테이블 삭제
    query_stat=mysql_query(connection,SQL_DROP_TABLE);
    if (query_stat != 0)
    {
        fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
        return 1;
    }

    // DB 연결 닫기
    mysql_close(connection);
    return 0;
}

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

MYSQL C API  (0) 2009.12.02
[데이터베이스] 윈도우에서 mysql에 접근하여 사용하는법  (0) 2009.12.02

원본 : http://kldp.org/node/79109
원문 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/COptim...
이 문서는 계속 추가/수정 됩니다.

1 소개
얼마전에 모바일기기에서 일정수준의 품질을 유지하면서 실행되는 JPEG라이브러리를 만드는 프로젝트를 진행한적이 있었다. 이 프로젝트를 진행하면서, 여러가지 방법으로 프로그램을 더 빨리 만들 수 있다는 사실을 경험적으로 알게 되었다. 이 문서는 C로된 코드를 속도와 메모리 양측모두에서 최적화하기 위한 경험적인 정보들을 포함하고 있다.

물론 여러분은 C 코드를 최적화 하는 방법에 대한 참고문서를 어렵지 않게 획득할 수 있을 것이다. 그러나 대부분의 문서가 팁수준에서 문제에 접근할 뿐으로, 컴파일러나 기계수준에서 어떻게 프로그래밍을 해야 하는지에 대한 정보는 담고 있지 않다.

보통 프로그램의 속도를 높이게 되면 코드의 크기가 늘어나게 된다. 코드의 크기가 늘어나면 프로그램이 복잡해지고, 읽고 이해하기 어려워진다. 메모리 자원이 넉넉한 개인PC혹은 서버 컴퓨터라면 문제가 되지 않겠지만 PDA와 같은 제한된 메모리 자원을 가진 기기일 경우 심각한 문제가 될 수 있다. 1%의 속도향상을 위해서 코드의 크기가 10%만큼 늘어난다면 분명 문제가 될 것이다. 이런 이유로 속도와 코드크기 모두에 대한 최적화를 수행하기로 결정을 했다.

2 선언
내가 진행하는 프로젝트가 ARM 플랫폼에서 진행된 관계로, ARM 최적화와 관련된 팁들이 필요했었다. 나는 인터넷을 통해서 ARM 최적화와 관련된 많은 문서를 검색하고 이중 유용한 것들 중심으로 수집해서 테스트를 했었다. 그러나 대부분의 문서들이 나에게는 도움이 되지 않았음을 고백한다. 이러한 실수를 줄이기 위해서 유용하고 효과적인 몇개의 팁만을 모으기로 결정했다.

3 어디에 필요한가
토론의 주제를 명확히 하고 넘어가자. 컴퓨터 프로그램을 최적화하기 위한 가장 중요한 것은 프로그램을 이루는 각각의 모듈중 어느 부분이 느리게 작동하거나, 큰 메모리를 소비하는지를 찾아내는 것이다. 이들 각각의 부분을 최적화하면 프로그램이 전체적으로 빨라질 것이기 때문이다. 이러한 모듈단위의 최적화는 최적화를 위한 부분을 비교적 쉽게 찾고, 쉽게 해결할 수 있다는 장점을 가진다.

The optimizations should be done on those parts of the program that are run the most, especially those methods which are called repeatedly by various inner loops that the program can have.

일반적으로 경험이 풍부한 프로그래머들은 아주 쉽게 프로그램이 요구하는 최적화될 필요가 있는 핵심을 쉽게 찾아낼 수 있을 것이다. 가장 좋은 최적화 방법은 경험많은 프로그래머를 고용하는 것이다. 그러나 경험많은 프로그래머는 매우 비싸며, 경험이 많다고 해도 더 좋은 결과를 위해서는 최적화를 위한 좋은 툴을 사용할 필요가 있다. Visual C++ 과 같은 통합 개발환경은 함수단위로 프로그램의 소비시간을 측정할 수 있는 profiler를 제공한다. 리눅스의 경우에는 gprof와 같은 profiler를 사용할 수 있다. 혹은 Intel Vtune와 같은 프로그램을 사용할 수 있는데, 이들 프로그램을 사용하면 프로그램의 어느부분이 가장 많은 시간을 소비하는지를 확인할 수 있다. 개인적인 경험으로 루프 혹은 third party 라이브러리 메서드를 호출하는 영역이 프로그램을 느리게 하는 경우가 많았다.

4 정수
우리가 사용할 값이 음수가 아니라면 int 형대신에 unsigned int형을 사용해야 한다. 어떤 프로세스들은 unsigned integer의 연산이 signed 연산보다 매우 빠르다. 또한 나누기/나눗셈 작업의 경우에도 음수가 필요 없다면 unsigned 를 명시해주는게 좋다.

루프에 사용될 변수라고 한다면, 다음과 같이 깔끔하고 효율적으로 선언할 수 있을 것이다.

register unsigned int variable_name;

기억해야할 또다른 점은 floating point 연산은 매우 느리다라는 점이다. floating point 데이터 타입은 자바와 함께 하는 컴퓨터과학문 서를 참고하기 바란다. 척 봐도 floating point 숫자는 다루기가 꽤나 복잡하다는 것을 알 수 있을 것이다. 만약 여러분이 소숫점 2자리까지의 정확도를 유지하는 회계프로그램을 만든다면, 모든 값에 x100을해서 int 형으로 바꾼다음 연산을 하도록 한다. 가능하면 외부의 수학라이브러리를 사용하지 않도록 한다. FPUs와 같은 라이브러리는 매우 느리다.

5 나눗셈 그리고 나머지
표준적인 프로세서에서의 분모와 분자의 32bit 나눗셈은 20~140의 실행 사이클을 가지고 있다. 나눗셈을 이용하면 다음과 같은 시간이 소비된다.

Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator)
= C0 + C1 * (log2 (numerator) - log2 (denominator)).

널리 쓰이는 버젼은 약 20+4.3N의 사이클을 보여준다. ARM 뿐만 아니라 프로세서를 막론하고 이런 연산은 피하는게 바람직하다. 나눗셈연산은 가능하다면 곱셈으로 대체해서 사용하기 바란다.

예를들어 (a/b) > c 는 b * c가 integer 범위안이라는 것을 안다면 a > (c * b)로 다시 쓰일 수 있다.

6 Combining division and remainder
나눗셈 (x/y) 그리고 나머지(x%y)둘다 종종 필요한 케이스이다
그러한 케이스에 비추어보아 나눗셈펑션을 컴파일러에 결합하는것이좋다 왜냐하면 나눗셈펑션은 항상 나눈값과 나머지를 리턴하기 필요하다 만약둘다 필요하다면 우리는 이와같은 예제를 같이 쓸수있어야한다

int func_div_and_mod (int a, int b) {
        return (a / b) + (a % b);
    }

7 2의 배수로 나누기
나누기를 할 때 2의 배수를 분자로 함으로써, 코드를 더 효율적으로 만들 수 있다. 이경우에 컴파일러는 나누기 연산대신에 shift 연산을 할 수 있기 때문이다. shift 연산은 가장빠른 연산중의 하나다. 그러므로 가능하면 2의 배수로 나눌 수 있도록 스케일을 조절할 필요가 있다. (예를 들어 66으로 나누어야 한다면 64로 나눌 수 있도록 스케일을 조절하라).

typedef unsigned int uint;

uint div32u (uint a) {
   return a / 32;
}
int div32s (int a){
   return a / 32;
}

이경우에도 signed 값보다는 unsigned 로 나누어질 수 있도록 함수를 조절할 필요가 있다. signed의 경우에는 더많은 시간이 소비된다. 왜냐하면 오른쪽으로 쉬프트 시킬경우 가장왼쪽의 비트를 0으로 만들어주는 연산이 한번더 들어가기 때문이다.

#include <stdio.h>

int main()
{
  unsigned int a = 1024;
  unsigned b, c;
  b = a/32;    // --- 1
  c = a >> 5;  // --- 2
}

1과 2는 동일한 결과를 보여주며, 컴파일러내에서도 동일하게 shift 처리된다. 다음은 intel 프로세서에서 gcc로 컴파일된 어셈블리어중 1과 2에 해당되는 부분의 코드다.

movl    $1024, -12(%ebp)
movl    -12(%ebp), %eax
shrl    $5, %eax           # b = a / 32
movl    %eax, -8(%ebp)
movl    -12(%ebp), %eax
shrl    $5, %eax           # c = a >> 5

8 배열을 이용한 index 생성
특정값에 대응되는 문자를 변수에 입력하는 코드를 만든다면 다음과 같이 switch 문을 사용할 것이다.

switch ( queue ) {
  case 0 :   letter = 'W';
     break;
  case 1 :   letter = 'S';
     break;
  case 2 :   letter = 'U';
     break;
}

혹은 if else 문을 사용할 수도 있을 것이다.

if ( queue == 0 )
   letter = 'W';
else if ( queue == 1 )
   letter = 'S';
else
   letter = 'U';

다음과 같이 문자의 배열을 인덱스화 하면 더 빠른 접근이 가능하다. - 사용하기도 쉽다 -

static char *classes="WSU";
letter = classes[queue];

9 나머지 연산자의 대체
우리는 나눗셈의 나머지를 알기 위해서 나머지 연산자 %를 사용한다. 이경우 % 연산대신 판단문을 사용해서 시간을 줄일 수 있다. 아래의 두 코드를 비교해 보기 바란다.

uint modulo_func1 (uint count)
{
   return (++count % 60);
}

uint modulo_func2 (uint count)
{
   if (++count >= 60)
  count = 0;
  return (count);
}

if 문은 나머지 연산자보다 빠른코드를 생성한다. 주의 할점은 2번째 함수의 경우 0에서 60사이의 값에 대해서만 제대로 측정이 된다는 점이다.

10 전역 변수
전역 변수는 절대 레지스터에 할당할 수 없다. 포인터를 사용하여 간접적으로 할당하거나 함수호출을 이용해서 전역변수를 변환할 수 있다.

따라서 컴파일러는 전역변수의 값을 레지스터에 올려서 캐쉬할 수 없게 되고 때문에 글로벌 변수를 이용할 때마다 다시 읽어들이는 오버로드가 생기게 된다. 그러므로 가능하면 글로벌 변수를 직접 호출하는 대신에, 로컬변수를 이용해서 필요한 연산을 하고 그 결과를 글로별 변수에 할당하는 방법을 사용해야 한다.

int f(void);
int g(void);
int h(void);
int errs;

void test1(void)
{
  errs += f();
  errs += g();
  errs += h();
}

void test2(void)
{
  int localerrs = errs;
  localerrs += f();
  localerrs += g();
  localerrs += h();
  errs = localerrs;
}

test1은 매번 전역변수를 로드해야 한다. 반면 test2의 경우 레지스터에 등록된 localerrs에 값을 저장하고 마지막에 한번만 전역변수에 접근함을 알 수 있다.

11 Using Aliases
아래의 코드를 보기 바란다.

  void func1( int *data )    {
      int i;

     for(i=0; i<10; i++)
     {
            anyfunc( *data, i);
     }
  }

*data 가 결코 변하지 않는다고 하더라도, anyfunc 함수를 호출하는 컴파일러는 이걸 알 수가 없다. 그래서 변수가 사용될 때마다 메모리로 부터 다시 읽어들이게 된다. 이 문제는 지역변수를 하나더 둠으로써 해결할 수 있다.

  void func1( int *data )
  {
      int i;
      int localdata;

      localdata = *data;
      for(i=0; i<10; i++)
      {
          anyfunc ( localdata, i);
      }
  }

12 데이터 타입
C 컴파일러는 char, short, int, long, float, double 등의 다양한 원시 데이터 타입을 제공한다. 필요한 영역에 필요한 수준의 데이터 타입을 사용하도록 하자.

13 지역변수
가능하면 지역변수로 char 이나 short를 사용하지 않도록 한다. char와 short가 사용될 경우 컴파일러는 값을 저장하기 위해서 8bit 혹은 16bit를 할당한 후, 남는 크기를 줄이는 작업을 하게 된다. 이는 24bit, 16bit 만큼을 shift 시키는 연산을 하게 됨을 의미한다. 그러므로 입력되는 데이터가 8 혹은 16 비트라고 하더라도, 32bit로 연산을 하도록 함수를 만들 필요가 있다.

int wordinc (int a)
{
   return a + 1;
}

short shortinc (short a)
{
    return a + 1;
}

char charinc (char a)
{
    return a + 1;
}

3번째 코드가 가장 빠른결과를 보여줄 것이라고 생각할지도 모르지만, 1번째 코드가 가장 빠르게 작동한다.

14 포인터
구조체를 그대로 넘길경우 구조체의 모든 값이 스택에 올라가기 때문에 느리게 작동한다. 그래서 구조체의 포인터를 넘기는 경우가 많다. 나는 수 kbyte의 구조체를 넘기는 프로그램을 본적이 있다. 이런 경우 포인터를 쓰도록 하자.

포인터를 통해서 구조체를 넘길때, 구조체의 멤버를 수정할일이 없다면 상수로 선언해서 넘기도록 하자.

void print_data_of_a_structure ( const Thestruct  *data_pointer)
{
   ...printf contents of the structure...
}

이렇게 하면 컴파일러는 인자로 넘어온 포인터가 수정할 수 없는 외부 구조체라는 것을 알게 된다. 이렇게 되면, 값이 사용될 때마다 다시 읽혀질 필요가 없어지게 된다. 또한 이러한 코드는 실수로 구조체 멤버의 변수를 바꾸는 것과 같은 실수를 하지 않도록 해준다.

15 Pointer chains
구조체내의 정보에 접근하려다 보면 포인터의 chain을 사용해야 할 때가 있다. 다음과 같은 경우다.

typedef struct { int x, y, z; } Point3;
typedef struct { Point3 *pos, *direction; } Object;

void InitPos1(Object *p)
{
   p->pos->x = 0;
   p->pos->y = 0;
   p->pos->z = 0;
}

이럴 경우 p->pos 를 다른 포인터에 할당해서 접근하도록 하자. 이렇게 하면 p->pos 가 캐쉬되므로 좀더 효율적으로 작동하게 된다.

void InitPos2(Object *p)
{
   Point3 *pos = p->pos;
   pos->x = 0;
   pos->y = 0;
   pos->z = 0;
}

코드가 좀더 보기 좋아진다는 효과도 노릴 수 있다.

16 Binary Breakdown
여러개의 조건을 검사하다 보면, if와 else if를 여러개 사용하는 경우가 생긴다.

if(a==1) {
} else if(a==2) {
} else if(a==3) {
} else if(a==4) {
} else if(a==5) {
} else if(a==6) {
} else if(a==7) {
} else if(a==8)

{
}

이경우 2개로 나누어서 조건 검사를 하도록 한다.

if(a<=4) {
    if(a==1)     {
    }  else if(a==2)  {
    }  else if(a==3)  {
    }  else if(a==4)   {
 
    }
}
else
{
    if(a==5)  {
    } else if(a==6)   {
    } else if(a==7)  {
    } else if(a==8)  {
    }
}

이렇게 하면 최악의 경우 비교횟수가 절반이 됨을 알 수 있다. 필요에 따라서는 아래와 같이 3중루프 코드로 만들 수도 있다. 좀더 빠르게 동작하긴 하겠지만 코드가 보기 어려워진다는 단점이 생긴다.

if(a<=4)
{
    if(a<=2)
    {
        if(a==1)
        {
            /* a is 1 */
        }
        else
        {
            /* a must be 2 */
        }
    }
    else
    {
        if(a==3)
        {
            /* a is 3 */
        }
        else
        {
            /* a must be 4 */
        }
    }
}
else
{
    if(a<=6)
    {
        if(a==5)
        {
            /* a is 5 */
        }
        else
        {
            /* a must be 6 */
        }
    }
    else
    {
        if(a==7)
        {
            /* a is 7 */
        }
        else
        {
            /* a must be 8 */
        }
    }
}

17 Switch 대신 lookup table 를 사용하라
switch는 다음과 같은 경우 사용한다.

* 여러개의 함수중 하나를 호출해야할 필요가 있을 때
* 다양한 리턴값을 넘겨받고 이를 처리해야 할때
* 여러개의 코드중 하나를 실행시켜야 할때

예를 들어서 조건값을 입력받아서 거기에 맞는 문자열을 리턴하는 아래와 같은 코드가 있다고 가정해보자.

char * Condition_String1(int condition) {
  switch(condition) {
     case 0: return "EQ";
     case 1: return "NE";
     case 2: return "CS";
     case 3: return "CC";
     case 4: return "MI";
     case 5: return "PL";
     case 6: return "VS";
     case 7: return "VC";
     case 8: return "HI";
     case 9: return "LS";
     case 10: return "GE";
     case 11: return "LT";
     case 12: return "GT";
     case 13: return "LE";
     case 14: return "";
     default: return 0;
  }
}

위의 코드는 아래와 같이 좀 더 효율적인 코드로 만들 수 있다. 덤으로 보기에도 편하다.

char * Condition_String2(int condition) {
   if ((unsigned) condition >= 15) return 0;
      return
      "EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" +
       3 * condition;
}

첫번째 루틴은 240byte가 필요하지만 두번째 루틴은 72바이트만 소모되고 있다.

18 루프
루프는 모든 프로그램에서 사용되는데, 많은 경우 루프에서 과다한 시간을 소비하게 된다. 여러번 실행되는 루프틔 특성상 조그마한 시간의 낭비가 게속 누적되기 때문이다.

18.1 Loop termination
루프를 종료시키기 위한 검사는 항상 count-down-to-zero 방식을 사용하도록 한다. 이것은 좀더 적은 시간을 소비한다. 아래의 두개의 예제는 동일한 일을한다. 다른점이 있다면 첫번째 코드는 루프를 증가시킨다는 점이고 두번째는 루프를 감소시킨다는 점이다.

int fact1_func (int n)
{
    int i, fact = 1;
    for (i = 1; i <= n; i++)
      fact *= i;
    return (fact);
}

int fact2_func(int n)
{
    int i, fact = 1;
    for (i = n; i != 0; i--)
       fact *= i;
    return (fact);
}

18.2 더욱 빠른 for 문
다음은 0부터 10까지의 숫자를 연산하기 위해서 for 문을 사용한 일반적인 예다.

for (i = 0; i < 10; i++) {...}

i는 0,1,2,3,4,5,6,7,8,9 로 1씩 증가할 것이다.

가능하면 아래와 같이 숫자를 감소시키는 방향으로 for 문을 사용하라.

for (i = 10; i--;) {...}

첫번재 코드보다 두번째 코드가 더 빠른 수행능력을 보여준다.

두번째 코드는 i가 0이 아니면 i를 감소시키고 다음 코드를 진행하라의 의미인데, 조건 검사의 경우 0인지 아닌지를 비교하는데 더 작은 시간이 소비되기 때문이다. 그러므로 두번째 코드는 아래와 같이 재작성할 수 있다. 두번째 예제코드 보다는 아래의 코드가 더 보기 쉬우므로, 아래의 코드를 사용하는게 가독성 측면에서 유리할 것이다.

for (i = 10; i ; i--) { }

혹은

for (i = 10; i!=0; i--) { }

이들은 모두 동일한 수행능력을 보여준다.

18.3 Loop jamming

18.4 함수 루프
함수는 호출되기 위한 분명한 오버헤드가 존재한다. 실행해야될 함수가 있는 포인터만 변경하는게 아닌, 값들을 stack에 push하는 것과 새로운 변수의 할당과 같은 작업이 수행되기 때문이다. 때문에 루프에서 함수를 호출하는 등의 코드는 작성하지 않는게 좋다. 이런류의 코드는 반대로 함수에서 루프를 수행하도록 변경하는걸 추천한다.

for(i=0 ; i<100 ; i++)
{
    func(t,i);
}
-
-
-
void func(int w,d)
{
    lots of stuff.
}

위의 코드는 아래처럼 바꿀 수 있다. 동일한 일을 좀더 빠르게 수행할 수 있다.

func(t);
-
-
-
void func(w)
{
    for(i=0 ; i<100 ; i++)
    {
        //lots of stuff.
    }
}

18.5 Population count - 비트 계수하기
아래의 코드는는 주어진 값에 1bit가 몇개인지를 검사하는 코드다. 0000 1010 이라면 2를 리턴하는 식이다. 이러한 비트필드는 일정한 범위의 값이 참인지 거짓인지를 빠르게 체크하기 위해서 널리 사용될 수 있다.

다음과 같이 1씩 오른쪽으로 쉬프트 하면서, & 연산을 한다.

int countbit1(uint n)
{
  int bits = 0;
  while (n != 0)
  {
    if (n & 1) bits++;
    n >>= 1;
   }
  return bits;
}

이 코드는 다음과 같이 4만큼 쉬프트 하는 식으로 바꿔서, 성능을 높일 수 있다.

int countbit2(uint n)
{
   int bits = 0;
   while (n != 0)
   {
      if (n & 1) bits++;
      if (n & 2) bits++;
      if (n & 4) bits++;
      if (n & 8) bits++;
      n >>= 4;
   }
   return bits;
}

18.6 Earyl loop breaking
루프를 사용하다보면, 일정 조건이 만족되면 뒤의 프로세스가 더이상 필요 없어지는 경우가 있다. 이 경우에는 break를 이용해서 루프를 벗어나도록 한다.

found = FALSE;
for(i=0;i<10000;i++)
{
    if( list[i] == -99 )
    {
        found = TRUE;
    }
}

if( found ) printf("Yes, there is a -99. Hooray!\n");

위의 코드는 -99가 포함되어 있는지 아닌지를 확인하는 프로그램이므로, 일단 발생이 되었다면, 루프를 돌 필요가 없다. 아래와 같이 break 문으로 빠져나가면 쓸데없는 루프의 낭비를 줄일 수 있다.

    found = FALSE;
    for(i=0; i<10000; i++)
    {
        if( list[i] == -99 )
        {
            found = TRUE;
            break;
        }
    }
    if( found ) printf("Yes, there is a -99. Hooray!\n");

18.7 Loop unrolling

19 함수 디자인
함수를 작고 가볍게 많드는건 좋은 생각이다. 이렇게 함으로써 컴파일러는 register 할당과 같은 영역에서 좀더 쉽게 최적화 할수 있게 된다.

19.1 함수 호출 Overhead
프로세서에서 함수의 호출은 예상과 달리 그리 큰 비용이 들지는 않는다. 함수가 호출되면 register에 함수의 인자를 넘기게 된다. 이 인자들은 char, short, int, float, structure등 이 올 수 있다. 이들 인자는 실제 4개만을 전달할 수 있다는 한계를 가진다. 이 이상으로 인자가 넘어가게 되면, stack를 이용해서 함수의 인자를 넘기게 된다. 당연히 함수를 호출함에 있어서 OverHead가 발생하게 된다. 함수호출시 발생하는 인자의 제한에 대해서는 Linux에서의 Assembly문서를 참고하기 바란다.

예제코드

    int f1(int a, int b, int c, int d) {
       return a + b + c + d;
    }

    int g1(void) {
       return f1(1, 2, 3, 4);
    }


    int f2(int a, int b, int c, int d, int e, int f) {
      return a + b + c + d + e + f;
    }

    ing g2(void) {
     return f2(1, 2, 3, 4, 5, 6);
    }

6개의 인자를 사용하는 f2와 g2함수는 스택에 저장되어 있는 인자를 꺼내기 위해서 2번의 메모리 접근이 더 발생하게 된다.

19.2 가능한 인자의 수를 줄여라
그러므로 가능한 적은 수의 인자를 넘겨받도록 함수를 설계할 필요가 있다.

* 4개 이하의 인자를 가지도록 함수를 설계하라. 4개가 넘어가면 스택을 통해서 인자를 넘기게 된다.
* 만약 함수가 4개 이상의 인자가 사용되면, 스택을 통해서 인자를 넘기게 되고 스택의 크기만큼 메모리 접근이 발생하게 된다.
* 이럴 경우 구조체를 선언하고, 구조체에 대한 포인터를 넘기는 방식을 사용하도록 한다.
* 구조체를 사용하면 인자의 양을 줄일 수 있으며, 코드 활용성도 높아지게 된다.
* 인자에 사용되는 자료형은 long크기 이상으로 하도록 하자.
* Avoid functions with a variable number of parameters. Those functions effectively pass all their arguments on the stack.

19.3 인라인 함수
__inline키 워드를 이용하면 함수를 인라인화 할 수 있게 된다. 이것은 일종의 매크로 처럼 작용을 하며, 함수가 호출되는 대신 함수의 본체가 직접 치환이 되어 버린다. 이렇게 함으로써, 함수를 호출하는데 드는 비용을 줄일 수 있게 된다. 반면 코드가 모두 치환되어 버리므로, 코드의 크기가 커지게 된다.

    __inline int square(int x) {
       return x * x;
    }

    #include <MATH.H>

    double length(int x, int y){
        return sqrt(square(x) + square(y));
    }

comment:
Thanks for very good and intersting postings! (Sorry, can't type Korean). I have some comments:

9) I'm not sure how many cycles are needed to compute % operator; however, IMHO, this would be better than branchness. Including ARM processors, most modern processros are exploiting branch predctions for aggressive dynamic scheduling. So, branch mis-prediction should be considered to minimize negative effects. If branch mis-predctions occur in the second code, I'm pretty sure that this code is slower than the first one. In other words, removing branchness is better.

I think you can replace % operator with & operator for calculating remainder. & should be quite faster than % and branch.

15) Unless we have a really stupid compier, I don't think that this kind of replacement would bring a better performance. A comper will automatically uses *cached* (more precisely, cached into a register) value in computing others unless a variable is declared as volatile.

17) Really good!! I've never seen that.

18) Absolutely agree with you. I'm always using * for (i = n - 1; i >= 0; --i) * insted of * for (i = 0; i < n ; ++i) *

ohhara의 이미지

http://www.azillionmonkeys.co

http://www.azillionmonkeys.com/qed/optimize.html 이 글을 읽어보시면 아마 느끼시는게 많이 있으실 듯... :)

여러가지 명언이 나옵니다.
Memory in modern computers is most definitely NOT "random access".
라던가...

나름대로 감동적으로 읽은 글. :)

Taeho Oh ( ohhara@postech.edu , ohhara@plus.or.kr ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
Digital Media Professionals Inc. http://www.dmprof.com

권순선의 이미지

<code>...</code> 태그를

<code>...</code> 태그를 제가 추가하였습니다. :-)

yundreamm의 이미지

// 오하라님 좋은 글

// 오하라님
좋은 글 감사합니다. 함 읽어봐야 겠네요.

// 권순선님
고맙습니다. 훨씬 보기 좋군요.
약간의 귀차니즘만 극복하면 될건데 -.-;

blkstorm의 이미지

오호...

예전에 회사 다닐 때 ARM프로세서 강사로 오셨던 분의 강의 내용과 아주 흡사하군요. @.@

17번은 인상적이군요. @.@

오랜만에 보니깐 재미있네요... ^^;;

조성현의 이미지

감사~

재미있군요. :) 앞으로 프로그래밍 할 때, 많은 도움이 될 것 같습니다.
----
MyWiki http://linu.sarang.net
MyBlog http://ntames8.linuxstudy.pe.kr
----
;p $ rm -rf ~ && mkdir ~ && wget $열정 and $연애

anyshin의 이미지

좋은정보 감사합니다.

와.. 참고하겠습니다. 감사합니다.

kalstein의 이미지

미세 성능 높이기

미세 성능 높이기 관련 아티클이네요...

요즘은 제가 C++ SW design쪽에 관심을 가지면서...약간 덜해지긴했지만...그래도 몇가지 추가 및 수정을 하자면...

일단 객체뒤에 ++ 를 쓰는것 보다는 (postfix) 앞에 쓰는것이 좋습니다 (prefix)
i++ 보다는 ++i 라는거지요. 이유는...i++의 경우는 temp로 리턴될 값을 저장해야 하기 때문인데요...아마 int 형태일 경우에는 컴파일러가 order를 뒤바꿈으로써 최적화를 할테지만...C++에서 객체사용중이라면 좀 비효율적이 됩니다. (MEC++ 책에 나옵니다 ^^;)

그리고...18.5를 좀 더 개선해 보도록 하지요. 그냥 내용 자체는...loop unrolling인가? 하는 기법같은데요...그렇게 하지말고 이런 방식은 어떨까요?

int countbit3(uint n)
{
  int bits = 0;
  while (n != 0)
  {
    bits += n & 1;
    n >>= 1;
  }
  return bits;
}

이 방식은 루핑 횟수는 많겠지만, 분기문이 없으므로 캐쉬미스가 없습니다. 상위 2개의 샘플보단 빠르겠죠?

^^

ps : 근데...countbit2가 countbit1에 비해...월등히 분기문이 많은데...;; 과연 성능 향상이 어느정도인지 궁금하네요...

owlet의 이미지

while문에서도 분기가

while문에서도 분기가 일어난다는것을 잊으신것같습니다. 루프 언롤링을 사용하는 이유가 분기 횟수를 줄이기 위함입니다. 32bit가 체킹된다고 가정했을때 분기 횟수를 비교해보면
countbit1 : while = 33, if = 32, total = 65
countbit2 : while = 5, if = 32, total = 37
countbit3 : while = 33, if = 0, total = 33
3이 2보다 total은 적지만 무조건 분기하는 while보다는 조건에 따라서 분기하지 않을수도 있는 if문이 더 유리할것같아 보입니다. 또한 3에서는 n & 1의 결과를 레지스터에 저장한 후 bits에 더해야 하기때문에 환경에 따라서 손실이 더 있을수도 있을것같습니다.

kalstein의 이미지

아차차...while문

아차차...while문 분기는 생각을 안했군요 ㅎㅎ

레지스터에 저장한 후 bits에 더하는 문제는...if문안에서 도 어차피 AND연산의 결과를 어딘가에 저장한 후에 0인지 아닌지 비교하니까 큰 문제는 없을꺼 같아요.

countbit3을 제시한 이유가...분기문 자체는 적으면 적을수록 컴파일러가 최적화 시키는게 편하다고 하더군요. 그쪽 분야는...워낙 대단한 분들이 많아놔서리 ㅎㅎㅎ

ohhara의 이미지

약간 다른 얘기긴

약간 다른 얘기긴 한데...

max, abs, min을 분기 안 쓰고 구현하는 것도 다음과 같이 가능하다더군요. 분기 안 쓰고 구현하려는 처절함이 느껴지는 -_-;;;

static int int_max(int a, int b) {
b = a-b;
a -= b & (b>>31);
return a;
}

static int int_abs(int a) {
return a - ((a+a) & (a>>31));
}

static int int_min(int a, int b) {
b = b-a;
a += b & (b>>31);
return a;
}

Taeho Oh ( ohhara@postech.edu , ohhara@plus.or.kr ) http://ohhara.sarang.net
Postech ( Pohang University of Science and Technology ) http://www.postech.edu
Digital Media Professionals Inc. http://www.dmprof.com

magingax의 이미지

급 질문...~!!

그럼 global 변수를 애용하는게 비효율적이란 건가요?

덩어리가 크고 자주 쓰이는 클래스를 global로 좋고 각 함수에서 여러번

사용하는 구조를 애용했는데..이건 아닌건가요?

큰괭이의 이미지

어떤 각도로 코드를 보느냐가 차이가 아닐가요?

"unsigned가 signed보다 효율적이고 parameter pointer를 직접 Access하지 않고. Local에 pointer변수를 만들어서 casting해서 쓰고"

사실 이런 원리의 중요성은 그 사람이 어떤일을 하느냐에 따라서 이런 fact의 중요도가 결정되는 것 같습니다.
저도 Embedded하는 사람입니다만.
driver를 작성할때는 되도록 이런 부분에 대해서 신경을 씁니다만.
Application을 할때 이런 부분까지 고민하면서 코딩을 하면... 코드 짜기 정말 힘들죠.
(혹은 이런데 신경쓰는 것보다 다른 관점의 것을 신경쓰는것이 App코딩에서 더 성능향상을 가져올 수 있습니다.
ex) File Access를 최소화 한다던가, MemAlloc을 최소화 할 수 있도록 코딩한다던가...)

실제 Driver작성시/DSP Code작성시/Kernel 코드 작성시/Assembler코드 작성시 일때는 이러한 부분이 많이 생각해서
읽기 쉽고 최적화된 코드를 작성해야겠지요.

Global변수가 비효율적이다 라는 관점은 Application Level로 올라가면 무의미 할것 같습니다 ^^.

tj의 이미지

컴파일러가 값이 변할지 변하지 않을지 몰라서 그래요

int global;

void myfn()
{
        global++;
        some_function();
        global++;
}

누구나 global을 바꿀 수 있고, some_function()이 global을 바꿀지 안바꿀지를 컴파일러가 알 수 없어서 레지스터에 값을 캐싱할 수가 없는건데, 원글에서처럼 스택 변수에 캐싱을 해도 되고 아니면 컴파일러에게 some_function()이 pure function이라는 걸 __attribute__로 알려줘도 됩니다.

음, 마이크로 최적화가 필요한 부분이 있긴 하지만 아키텍쳐 의존적인 부분도 많고, 컴파일러들도 똑똑한데다가, 무작정 적용해선 대부분의 경우 의미있는 성능 차이로 나타나지 않거나 오히려 성능이 나빠지는 경우도 있습니다 (최적화한다고 쓰는 코드들이 원래 코드보다 빨라보이지만 크기는 커지는 경우가 많아요. 결과적으론 더 느리죠). 가독성을 목표로 코드를 쓰는게 훨씬 나으리라 생각합니다. 마이크로 최적화는 코드 돌리면서 oprofile로 보고 정말 hot path들만 해줘도 충분해요.

ㅡ,.ㅡ;;의 이미지

if문에 대해서...

16번..if문에 대해서...
>>16 Binary Breakdown
>>여러개의 조건을 검사하다 보면, if와 else if를 여러개 사용하는 경우가 생긴다.

이부분... 그와같은경우는 switch 문이 더빠름.

그리고..8번.
>>8 배열을 이용한 index 생성
default 처리가 어렵다.. 즉, 범위이상의 값이 들어오면 메모리에러로 바로죽어버리는 매우위험한방법입니다.
또한 그리큰이득은 얻기힘들다.사실어느것이 우월한지 잘모르겠네요..
인덱스는 포인터계산이 한번들어가니..

>>11 Using Aliases
>>아래의 코드를 보기 바란다.
이부분도...좀...이상한거같네요..
함수호출은 이미 문법의 의미상 값을 복사하도록합니다. 포인터를쓰던 로컬변수를 쓰던말이죠..그러나.컴파일러에의해
최적화를 기대하셨다면.. 글쎄요.. 요즘컴파일러는또어떨지.. 아니면 조만간 가까운미래의 컴파일러는또어떨지..
그럴경우..로컬변수카피는 오히려 부하를 가중시키게될텐데요..

다른건 안봤는데 그냥 눈에띄는거같아...적어봤습니다.
----------------------------------------------------------------------------
C Library Development Project

소타의 이미지

어떻게 쓰느냐에

어떻게 쓰느냐에 따라 많은 if 문 보다는 switch가 편하고 더 빠른건 맞는것 같습니다.
문자열 비교로 HTTP 파서를 만들때
if (strncasecmp) 이런걸 자주 써야 하는데 좀 느린감이 있었는데

switch (요청헤더[0]) {
case 'c':
case 'C':
  switch (요청헤더[1]) {
   case 'o':
   case 'O':
    //Connection: 헤더 처리
    if (strncasecmp(&(요청헤더[2])))
   break;
  }
break;
}

이런식으로 풀어서 많은 성능 향상을 맛본적이 있습니다

그리고 8번 같은 경우는 저도 자주 쓰는데 그전에 범위에 대한 처리는 미리 해야겠죠. 그부분은 사전에 해야 하는 작업이거나 또는 임베디드나 모바일 쪽이라 한정적인 환경이라 생략하신것 같네요 ㅎㅎ

cronex의 이미지

16번. 비교하는 값이

16번.
비교하는 값이 한가지라면 당연히 switch 가 좋겠지만...
여러가지 값을 비교해가면서 상황이나 대상을 분류해나가는 if 문이라면
당연히 if ... else if를 여러개 써야만 하는경우가 생깁니다.
(윤드림 님의 예가 좀 너무 단순한 면이 없지는 않습니다만...)
(만약 0이하일 때, 0일때, 1~30일때, 31~100일 때 , 101~300일때, 301~1000 일때, 1001~3000일 때 3001~10000 일때, 10001 이상일 때 등으로 분류해야 할때 과연 switch 로 해결할 수 있을까요?)

8번 값의 범위를 벗어나는 처리는 배열 사용 앞에서 체크를 해줘야겠지요.

11번
윤드림 님이 말씀하신건 함수가 아니라 for문 앞에 local variable을 선언하는 것을
말씀하신 겁니다. 포인터 값의 원래값을 가져다 쓰려면 메모리 참조를 2번하게 됩니다.
따라서 그 값을 local에다가 저장해두고 그 값을 가져다 쓰는 것이
메모리 참조 횟수를 줄일 수는 있지요. 저게 루프를 10번 도는게 아니라
한 1000번쯤 도는 거라고 생각하면 엄청난 차이가 생깁니다.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

ㅡ,.ㅡ;;의 이미지

설사

설사 스트링이라하더라도 저렇게 if 를 문기할것이라면 switch가 유리하죠.
그러나 매우 잦은호출이 일어 나지 않는다면 스트링은 그냥 if else로 줄줄이 씁니다..
11번은 포인터참조를줄인다는말이었던가요.. 잘못이해했네요..

>17 Switch 대신 lookup table 를 사용하라
>switch는 다음과 같은 경우 사용한다.
>예를 들어서 조건값을 입력받아서 거기에 맞는 문자열을 리턴하는 아래와 같은 코드가 있다고 가정해보자.
>위의 코드는 아래와 같이 좀 더 효율적인 코드로 만들 수 있다. 덤으로 보기에도 편하다.
>첫번째 루틴은 240byte가 필요하지만 두번째 루틴은 72바이트만 소모되고 있다.

이것도 코딩량은 줄어들었으나 성능은떨어짐..

----------------------------------------------------------------------------
C Library Development Project

cronex의 이미지

댓글을 수정해서

댓글을 수정해서 설명했지만 단순 값의 비교가 아닌
값의 범위가 설정되는 경우는 if ~ else if를 쓸 수밖에 없다고 생각합니다.

17번의 경우는 코딩량이 아니라 코드가 컴파일 됐을 때
메모리에서 차지하는 양을 의미합니다.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

ㅡ,.ㅡ;;의 이미지

물론 아예 if else 를

물론 아예 if else 를 쓰지말라는게 아니죠.. 써야할곳이 있죠 그러나 예제에서 보면 그렇다는겁니다.
상황에 맞게 써야죠. 저도 if else 더많이 씁니다.
제가 게을러서 궂이 작은 성능개선을 위해서 switch 쓰고 싶지 않거든요..
적절히 쓰죠.. 그러나 글쓰신분은 성능을위해 더 힘든? 코드를 마다하지 않으셨기에 그렇것같으면 차라리 switch가
성능도 더좋고 보기도 더좋지 않냐는것입니다.

17번
윗글에도 코드량이 적다는건 이미 말씀드린거구요..
컴파일됬을때 덩치가 더커지는지는 해보지 않아 어느것이 많은지는 모르겠습니다만.
위의 다른내용들을보면 성능을위해서 더많은 코드를 마다하지 않고 있죠...
그런의미에서 말한겁니다.

----------------------------------------------------------------------------
C Library Development Project

cronex의 이미지

최적화라는 건 성능

최적화라는 건 성능 최적화와 메모리 최적화가 있을 수 있습니다.
하지만 어느 경우에도 trade-off라는게 있어서 항상 성능 최적화만을 추구할 수도 없고
항상 메모리 최적화만을 추구할 수도 없습니다.

어느 부분은 메모리 사용량을 절반 이하로 줄여도 성능이 아주 약간 떨어질 뿐인 반면
어느 부분은 메모리 사용량을 10%만 줄여도 성능이 두배 이상으로 느려지는 경우도 종종 있죠.
반대로 성능을 10% 향상 시키기 위해 메모리 사용량을 두배를 투자해도 힘든 경우도 있고
약간의 캐시를 두는 것만으로도 성능이 두배로 향상되는 경우도 있습니다.

따라서 상황에 맞는 최적화를 수행해야 해야 합니다.
윤드림님의 글은 최적화를 수행하고자 할 때 기존의 코드를 고칠 수 있는 방법들을 소개하고 있을 뿐..
모든 경우에 대해서 꼭 이렇게 해야만 모든 경우와 상황에서 항상 최적화된 코드가 나온다는
메뉴얼이나 무슨 법칙같은 걸 써놓으신게 아닙니다. 이건 본문 처음에도 나와있지요.

yundream7 씀:
보통 프로그램의 속도를 높이게 되면 코드의 크기가 늘어나게 된다. 코드의 크기가 늘어나면 프로그램이 복잡해지고, 읽고 이해하기 어려워진다. 메모리 자원이 넉넉한 개인PC혹은 서버 컴퓨터라면 문제가 되지 않겠지만 PDA와 같은 제한된 메모리 자원을 가진 기기일 경우 심각한 문제가 될 수 있다. 1%의 속도향상을 위해서 코드의 크기가 10%만큼 늘어난다면 분명 문제가 될 것이다. 이런 이유로 속도와 코드크기 모두에 대한 최적화를 수행하기로 결정을 했다.

윗글의 내용은 단순한 경험적인 예들일 뿐입니다. 명제들이 아니구요.
어느 경우에는 단순히 성능이나 메모리만 잡아먹는 코드 일 수도 있고
어느 경우에는 둘다 잡을 수 있는 좋은 해법일 수도 있는 거죠.
다 쓰기 나름일 것입니다. 다만 글의 일부가 맘에 안드신다면 그건 안쓰시면 되겠습니다.
부디 본문의 목적과 지향하는 바를 이해해주셨으면 하네요.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

tj의 이미지

약간 부연하면,

cronex님 말씀에 대부분 동의하구요. 약간 부연하면, 요즘 포로세서들의 경우엔 메모리 최적화와 성능 최적화가 같이 가는 경우가 많습니다. 캐시 미스 레이턴시가 워낙 크기 때문에, 작은 코드가 빠른 코드인 경우가 많고, 소프트웨어가 어느정도만 복잡해지면 마이크로 밴치시 성능이 좋아졌던 코드를 실제에 적용하면 캐시 사용량 문제로 전체 속도에 떨어지는 경우도 생깁니다. 좀 더 장기적으로 생각하면, 아키텍쳐나 프로세서 세대 변화에 따라 득이 되던게 해가 될 수도 있고, 컴파일러에게 의도가 아니라 구현을 설명하게 되서 컴파일러가 최적화할 수 있는 가능성을 떨어뜨립니다. 특수 환경이나, 프로세싱 시간의 대부분을 사용하는 tight loop이 아니면 가독성을 떨어뜨리는 최적화는 최선을 다해 피하는게 좋습니다.

익명사용자의 이미지

8번/11번

음, 제 생각에는 범위를 벗어나는 값이 들어왔을 때 바로 죽어버리는 게 아주 안전하고 좋은 방법입니다. (물론, "범위를 벗어나는 값이 들어오지 않는다"라고 약속이 되어 있을 때.) 그러면 왜 죽었는지 core 떠보고 바로 디버깅을 할 수 있죠.

범위를 벗어난 값이 원래 들어오면 안되는데도 불구하고 죽지 않으면 오히려 그만큼 버그 발견이 늦어집니다. 최악의 경우, 들어오면 안되는데 일부러 default를 써서 안 죽게 만들어 놓으면, 이건 버그를 숨기는 코드가 되는 거죠. QA에서 테스트할 때까지 잘 돌다가 고객 앞에서 장렬히 산화하게 됩니다. -.-

그리고 11번은 확실히 차이가 있습니다. 포인터를 쓰면 aliasing의 가능성 때문에 컴파일러가 매번 메모리에서 읽어와야 합니다. 예제로 나온 코드의 경우 메모리를 열 번 참조해야 하죠. 하지만 로컬 변수로 복사하면 (레지스터가 넉넉하단 전제 하에) 메모리에서 단 한번만 읽어오면 되고, aliasing이 없다는 게 보장이 되기 때문에 최적화의 가능성이 훨씬 넓어집니다.

컴파일러는 무슨 최적화를 해도 절대 원래 코드와 동작이 달라지면 안되기 때문에 아주 보수적으로 (최악의 가능성을 항상 생각하면서) 작업할 수밖에 없죠. 로컬 변수는 aliasing 가능성을 없애줘서 optimizer의 운신의 폭을 넓혀줍니다.

예를 들어 다음과 같은 코드를 보죠.

void foo(int *a, int *array)
{
  while (*a) { array[*a] = (어쩌구); (*a)--; }
}

이 경우 a와 array가 겹치지 않는다는 보장이 없으므로 컴파일러는 루프를 돌 때마다 a를 다시 읽어와야 하는 것은 물론, array에 값을 넣는 순서도 순차적이 아닐 수 있다고 가정해야 하죠.

하지만 *a를 먼저 로컬 변수에 복사해 놓고 시작하면, 컴파일러는 a가 하나씩 감소하여 0이 될 때까지 루프를 돈다는 것을 알 수 있으므로 loop를 위해 최적화된 기계어 명령을 쓸 수도 있고, array에 값이 순서대로 들어가는 게 보장되므로 이 경우에 특화된 각종 최적화를 수행할 수 있습니다. (예를 들어 x86이라면 string instruction 등을 쓸 수도 있겠죠.)

- jick

ㅡ,.ㅡ;;의 이미지

개인적인

개인적인 의견이라하셨지만...약간반론을 달자면....

메모리 범위가 넘어서면 바로 안죽습니다.. 죽는거는 잡아도 안죽으면 나중에실상황에서 죽겠죠..

>>default를 써서 안 죽게 만들어 놓으면, 이건 버그를 숨기는 코드가 되는 거죠. QA에서 테스트할 때까지 잘 돌다가 >>고객 앞에서 장렬히 산화하게 됩니다
이건좀...ㅡ,.ㅡ;;
default로들어오면 당연히 에러로 빠지든지하지 그걸 머어떻게 해놓길래 테스트때 안죽고 고객앞에서 죽는다는건지 이해할수 없군요.. 전 일부러 만들려해도 힘들것같군요..

----------------------------------------------------------------------------
C Library Development Project

시지프스의 이미지

11번에 대해 질문이 있습니다.

11 Using Aliases
int *data가 결코 변하지 않으면 const int *data라고 하면 되지 않나요? 이렇게 되면 *data가 상수니까 알아서 한 번만 읽어 올 것 같은데요.
아니면 int * restrict data라고 하면 되지 않나요?

cronex의 이미지

포인터 값이

포인터 값이 변경되서는 안되기 때문에 const int *data 를 쓰는 것과는 별개의 문제 입니다.
즉, int형 포인터인 data가 가리키는 값(즉 *data)을 읽어오기 위해서
값 참조를 2번 해야 한다는 것이 문제지요.

즉, 변수 data 에 저장된 포인터 값을 읽어와서(1)
그 포인터가 가리키는 메모리 위치에 있는 실제 int 값을(2)
읽어오기 때문이죠.

하지만 만약 저 data 값을 로컬 변수에 저장하면
포인터의 위치를 참조할 필요 없이 바로 로컬 변수에 접근해서 값을 읽을 수 있습니다.

물론 단 한번만 쓰고 끝날 값이라면 이렇게 할 필요가 없겠지만
루프내에서 사용되거나 비교문에서 중첩되서 사용되는 경우라면
로컬 변수로 저장하는 편이 좋습니다.

그리고 restrict와는 그다지 관련이 없는 것 같습니다.

------------------------------------------------------------
이 멍청이~! 나한테 이길 수 있다고 생각했었냐~?
광란의 귀공자 데코스 와이즈멜 님이라구~!

전웅의 이미지

> > 11 Using Aliases > > int

> > 11 Using Aliases
> > int *data가 결코 변하지 않으면 const int *data라고 하면 되지 않나요?

const 라는 형한정어는 값이 변하지 않음을 보장해 주는 역할을 하는 것이
아닙니다. 단지 "해당 포인터를 통해서 값을 변경할 수 없음"을 의미하는
것뿐입니다 - 아래 코드를 생각해 보시기 바랍니다.

void f(const int *a, int *b)
{
    int temp = *a;
    *b = 0;
    printf("%d, %d\n", temp, *a);
}

int i = 100;
f(&i, &i);

> > 이렇게 되면 *data가 상수니까 알아서 한 번만 읽어 올 것 같은데요.
> > 아니면 int * restrict data라고 하면 되지 않나요?
> >

넵, 맞습니다. 바로 restrict 가 해당 포인터가 가리키는 변수는 그
포인터를 통해서만 접근이 된다는 사실을 분명히 해주기 위한 것입니다!
만약, 위의 제 예에서 a, b 포인터를 restrict 로 한정할 경우, f(&i, &i)
라는 호출 자체가 잘못된 행동(undefined behavior)이 됩니다. 따라서
컴파일러는 함수 f 안에서 안전하게 *a 와 temp 가 결국 같은 값이라는
가정을 할 수 있게 되는 것입니다.

>
> 포인터 값이 변경되서는 안되기 때문에 const int *data 를 쓰는 것과는 별개의 문제 입니다.
> 즉, int형 포인터인 data가 가리키는 값(즉 *data)을 읽어오기 위해서
> 값 참조를 2번 해야 한다는 것이 문제지요.
>
> 즉, 변수 data 에 저장된 포인터 값을 읽어와서(1)
> 그 포인터가 가리키는 메모리 위치에 있는 실제 int 값을(2)
> 읽어오기 때문이죠.
>
> 하지만 만약 저 data 값을 로컬 변수에 저장하면
> 포인터의 위치를 참조할 필요 없이 바로 로컬 변수에 접근해서 값을 읽을 수 있습니다.
>
> 물론 단 한번만 쓰고 끝날 값이라면 이렇게 할 필요가 없겠지만
> 루프내에서 사용되거나 비교문에서 중첩되서 사용되는 경우라면
> 로컬 변수로 저장하는 편이 좋습니다.
>

11번 항목의 핵심은 포인터의 위치를 추가로 참조하냐 안 하느냐의 문제가
아닙니다.

void a(int *a, int *b)
{
    ...
    for (i = 0; i < N; i++)
        process(*a);
    ...
}

이 코드에서 컴파일러가 "포인터 a 가 가리키는 변수가 다른 포인터나
변수에 의해 aliasing 되었는지 알기 어렵기 때문에" *a 를 고정된 값으로
치환해 최적화할 수 없다는 뜻입니다. 즉, a 가 가리키는 변수가 맨 처음
보인 예에서 처럼 다른 포인터나 변수 등을 통해 수정될 수 있기 때문에
매번 a 가 가리키는 변수의 현재 저장된 값을 확인해야 하는 문제가 발생
한다는 것입니다 - 물론, data flow analysis 등을 통하면 aliasing 에
대한 사실을 확인하는 것도 가능합니다만, 항상 가능한 문제도 아닐 뿐더러
쉽지 않은 과정입니다.

이 문제는 위에서 언급했듯이 restrict 를 적절히 도입해 해결할 수 있는
문제입니다. 즉, 포인터 a 가 가리키는 변수가 aliasing 되지 않았음을
컴파일러에 알려주면 컴파일러가 문제의 코드를 "맘놓고" 최적화할 수 있게
됩니다. 즉, 위의 코드를

void a(int * restrict a, int * restrict b)
...

로 써줄 경우 컴파일러는 aliasing 에 대한 걱정 없이 루프 수행 전 *a 의
값을 한번만 확인해 process() 호출에 사용할 수 있게 됩니다.

임시 변수를 직접 도입해 사용하는 것은 restrict 를 통한 이러한 최적화를
손수 해주는 것으로 볼 수 있습니다. 지원된다면 restrict 를 사용하는
것이 바람직하겠지만, restrict 가 지원되지 않거나 지원하더라도 이를
통한 최적화가 형편 없는 (제 컴파일러 같은 --;) 컴파일러일 경우에
유용한 팁으로 생각할 수 있습니다.

> 그리고 restrict와는 그다지 관련이 없는 것 같습니다.
>

고로 restrict 와 관련된 문제입니다.

그럼...

--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)

시지프스의 이미지

답변 감사합니다.

인용:
const 라는 형한정어는 값이 변하지 않음을 보장해 주는 역할을 하는 것이
아닙니다. 단지 "해당 포인터를 통해서 값을 변경할 수 없음"을 의미하는
것뿐입니다 - 아래 코드를 생각해 보시기 바랍니다.

전혀 생각하지 못한 부분이었네요.
인용:
임시 변수를 직접 도입해 사용하는 것은 restrict 를 통한 이러한 최적화를
손수 해주는 것으로 볼 수 있습니다. 지원된다면 restrict 를 사용하는
것이 바람직하겠지만, restrict 가 지원되지 않거나 지원하더라도 이를
통한 최적화가 형편 없는 (제 컴파일러 같은 --;) 컴파일러일 경우에
유용한 팁으로 생각할 수 있습니다.

그런데 gcc는 restricted pointer를 지원하지 않나요? http://gcc.gnu.org/c99status.html에는 지원한다고 나오는데, syntax 에러가 나네요.
      1 #include 
      2
      3 void noname(int * restrict a);
      4
      5 int main(void) {
      6         int *a = malloc(sizeof (int));
      7         *a=8;
      8         noname(a);
      9         return 0;
     10 }
     11
     12 void noname(int * restrict a) {
     13         printf("%d",*a);
     14 }

rest.c:3: error: syntax error before "a"
rest.c:12: error: syntax error before "a"
rest.c: In function `noname':
rest.c:13: error: `a' undeclared (first use in this function)
rest.c:13: error: (Each undeclared identifier is reported only once
rest.c:13: error: for each function it appears in.)

전웅의 이미지

> > 임시 변수를 직접

>
> 임시 변수를 직접 도입해 사용하는 것은 restrict 를 통한 이러한 최적화를
> 손수 해주는 것으로 볼 수 있습니다. 지원된다면 restrict 를 사용하는
> 것이 바람직하겠지만, restrict 가 지원되지 않거나 지원하더라도 이를
> 통한 최적화가 형편 없는 (제 컴파일러 같은 --;) 컴파일러일 경우에
> 유용한 팁으로 생각할 수 있습니다.
>
> 그런데 gcc는 restricted pointer를 지원하지 않나요? http://gcc.gnu.org/c99status.html에는 지원한다고 나오는데, syntax 에러가 나네요.
>
> 1 #include
> 2
> 3 void noname(int * restrict a);
> 4
> 5 int main(void) {
> 6 int *a = malloc(sizeof (int));
> 7 *a=8;
> 8 noname(a);
> 9 return 0;
> 10 }
> 11
> 12 void noname(int * restrict a) {
> 13 printf("%d",*a);
> 14 }
>
> rest.c:3: error: syntax error before "a"
> rest.c:12: error: syntax error before "a"
> rest.c: In function `noname':
> rest.c:13: error: `a' undeclared (first use in this function)
> rest.c:13: error: (Each undeclared identifier is reported only once
> rest.c:13: error: for each function it appears in.)
>

-std=c99 옵션이 필요합니다.

--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)

yarmini의 이미지

좋은 글 감사합니다.

도움이 많이 되는 글이었습니다^^

항상 최적화와 가독성 두 갈래에 있는 데 둘 다 잡는 팁도 많네요^^

언 제 나 멋 진

익명 사용자의 이미지

장문의 좋은 글이네요.

장문의 좋은 글이네요.

부족하게나마 저도 나름대로 아래 블로그에 정리를 해보았습니다.

http://a.tk.co.kr/126

Kyuseo의 최적화 원칙:

- CPU 부하가 큰 반복작업은 최대한 최적화 코드로 작성하라.

- 코딩 수정으로 최적화를 하지 말고 알고리즘으로 최적화를 하라.

- 더 빠른 속도가 필요하다면 어셈블리(Assembler) 언어를 사용하라

- 코드의 최적화보다는 코드의 유지보수를 우선하라.

- 사소한 최적화에 시간과 노력을 투자하지 말아라.

- 중복코드를 최소화 하라.

- inline을 활용하라.

  1. 2010.03.29 00:07

    비밀댓글입니다

  2. 2010.04.16 20:31

    비밀댓글입니다

  3. michael kors outlet 2013.07.26 20:51

    희미한 달빛이 샘물 위에 떠있으면,나는 너를 생각한다.

  4. ghd 2013.07.29 08:35

    좋으면 좋고 싫으면 싫은 거지, 뭐가 이렇게 어렵고 복잡하냐구

  5. ugg boots 2013.08.05 06:08

    사람들은 죽을걸 알면서도 살잖아 .사랑은 원래 유치한거에요

  6. cheesam31@gmail.com 2021.02.08 17:23

    9번 나머지 연산자의 대체 부분 궁금한점이 있어서 글 남깁니다.
    "우리는 나눗셈의 나머지를 알기 위해서 나머지 연산자 %를 사용한다." 라고 말씀하시고... 2번째 modulo_func2 같은 경우 0 ~ 60 같은 경우에만 정확한 값이 반영된다고 적혀있는데 보통 나머지 연산자 %를 쓸때 나누어지는 값(60) 보다 큰 값의 나머지를 구하기위해 사용되는 연산자 아닌가요...? 저렇게 사용하면 빨라지는건 알겠는데.. 보통 저렇게 사용을 하나요..? 지식이 짧아서 글 남깁니다.

서울대 기계과 김종원 교수님의 글을 퍼왔습니다.^^

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

또 여름방학이 시작 되었네요.. 방학을 맞아 학교를 떠나는 학부생에게 항상 하는 말입니다. 꿈과 비전을 세우고 돌아오라고... 옛날에 올렸던 글을 다시 올립니다. 맨날 하는 이야기이지만 아직도 읽어 보지 못한 학생들을 위해서...

또다시 한 학기가 끝났다. 이제 곧 자네들은 나름대로의 여름을 보내기 위해서 학교를 떠날 것이다. 그런 자네들에게 이번 여름에는 필히 자신들의 꿈과 비전을 만들고 돌아 오라고 외치고 싶다.

자네들은 곧 이 교정을 떠나서 사회로 나갈 것이다. 대학원을 진학하든 산업체에 취직을 하든 그것은 당장 눈 앞의 진로일 뿐이다. 제일 중요한 것은 자네들이 과연 20년 뒤에 자기가 어떤 곳에서 어떤 모습으로 일과 연구를 할 것인지에 대해서 확실한 꿈과 비전을 가지고 있느냐 하는 것이다.

아마 대부분의 학부생들이 그러한 꿈과 비전이 없이 이 순간 그저 학기말 고사나 준비하고 있을 것이다. 그런데 자네들은 과연 학기말 고사 공부를 하는 정도의 시간과 노력만이라도 자네들의 꿈과 비전을 굳히기 위해서 투자를 해보았는지 잘 모르겠다. 한번만이라도 대기업을 성공적으로 경영하고 있는 엔지니어 출신의 CEO가 쓴 책을 읽고 나도 20년 뒤에는 바로 이런 모습이 되고 싶다고 꿈꾸는 노력을 했는지 모르겠다는 말이다.

그저 이공계 기피 현상이라는 현실에 좌절하면서 20년 뒤에는 없어지겠지 하는 멍청한 생각을 하고 있지나 않은지 모르겠다. 점점 더 포화 상태로 치닫는 경제 현실에서 아무런 꿈과 비전 없이 그저 친구들이 하는 말이나 신문에서 떠드는 피상적인 기사에 자네들의 소중한 미래를 맡기고 있지나 않은지 걱정이 된다.

서울공대에 와서도 여전히 평균적인 위치의 엔지니어의 모습을 자네들의 미래의 소박한 꿈으로 삼고 나도 20년 뒤에는 혹시 회사에서 짤려나는 것은 아니지 하는 막연한 불안감으로 졸업을 기다리지나 않는지 걱정이 된다. 왜 자네들은 서울공대생으로서 20년 뒤에 top 1% 이내에 드는 CEO, 전문 연구직, 교수, 창업가 등을 꿈꾸지 않는가? 왜 자네들은 지금 이 순간 자네들 나름대로의 큰바위 얼굴을 그리지 않는가? 왜 사회 현상만 탓하고 있는가? 과연 자네들은 얼마나 자기 자신의 꿈과 비전을 확실히 세우기 위해서 시간과 노력을 투입했던가?

20년 뒤의 자기 자신의 모습, 즉, 꿈과 비전이 가슴 속에 확실하게 없는 상태에서 지금 죽을 힘을 다 할 수는 없다. 그러면, 결국 평균적인 위치의 엔지니어가 되고 마는 것이다. 아무리 서울대를 없앤다고 난리를 쳐도 자네들은 top 1% 엔지니어가 되어 리더그룹에 들어야 한다고 생각한다. 우리가 사는 세상은 리더가 필요하다. 나는 자네들이 바로 이런 리더가 되기를 원하며, 그런 리더가 될 권리와 의무를 동시에 가지고 있다고 굳게 믿는다.

여러 번 이야기 하지만, 엔지니어로서 20년 뒤의 자네 모습으로서 결국 다음과 같이 크게 다섯 종류의 모습을 꿈꿀 수 있다:

- [대기업 CEO] Global top class 대기업의 CEO 또는 핵심 중역이 되어 활동한다.

- [창업가] 기술 기반의 top class의 세계적인 벤처기업을 창업하여 사회에 커다란 영향을 주면서 엄청난 돈을 번다.

- [전문연구직] 세계적인 연구소에서 프로젝트 팀장으로서 도전적인 프로젝트를 수행한다.

- [교수] 세계적인 대학교에서 훌륭한 교수가 되어 교육과 연구에 몰두한다.

- [전문행정직] 공학적 소양을 바탕으로 top class의 변호사가 되거나 정부 관료가 되어 기술문제가 개입된 법적 소송을 처리하거나 중요한 국가 정책을 수립해서 시행한다.

여기서 제발 내가 과연 그런 모습이 될 수 있나 하는 멍청한 소리를 좀 하지 말기 바란다. 큰바위얼굴 소년은 자기가 큰바위얼굴이 될 것이라고 생각도 하지 않았다. 그리고, 그렇다면 도대체 자네들은 20년 뒤에 무엇이 될 것이냐고 묻고 싶다. 축구 선수는 골대가 있기 때문에 90분 동안 죽을 힘을 다해서 공을 찬다. 자네들은 A학점을 꿈꾸기 때문에 죽을 힘을 다해서 시험 공부를 한다. 고등학생들은 서울대 합격하기 위해서 죽을 힘을 다한다.

내가 과연 그런 모습이 될 수 있나 하는 생각은 결국 모두 다 공을 넣는 것은 아니고, 시험도 다 잘 보는 것은 아니며, 서울대말고도 다른 대학도 많은데 왜 내가 죽을 힘을 다 할 필요가 없다고 생각하는 것과 같다. 그렇다, 실패를 두려워 하면 가지 않으면 된다. 그러나, 우리는 인생을 사는 것이기 때문에 누구나 다 가야 하며, 결국 아무런 목표가 없이 살아가도 결국 20년 뒤에 어떠한 모습으로 되어 있을 것이기 때문에 큰 문제인 것이다. 그리고, 이 놈의 인생은 단 한 번의 기회 밖에 주지 않는다. 자네는 이런 이유로 그냥 그렇게 살다가 20년 뒤에 그냥 되는대로 살면서 그 때도 여전히 이 놈의 사회가 이래서 안 된다고 푸념할 것이냐? 그 때가서도 여전히 사회보고 책임을 지라고 할 것이냐?

위의 다섯 가지의 모습 중에서 어떠한 것도 자기 가슴에 공진과 같이 와 닿는 모습이 없으면 하루 속히 엔지니어가 아닌 다른 길로 가야 한다. 그래 다 좋다. 그런데 한 가지 정말로 묻고 싶은 것은, 학기말 고사 준비하는 정도의 시간과 노력을 투입해서 위의 다섯 가지 길을 간 사람이 쓴 책도 읽고 인터넷도 검색하고 하면서 엔지니어로서의 자네의 꿈과 비전을 만들기 위해서 손톱만큼의 노력은 해보았는지 하는 것이다.

혹시나 부모나 친구들이나 선배들이 그저 지나가면서 던지는 그 한마디에 엔지니어로서는 나는 이런 모습이 될 것이야 하고 있지나 않은지 모르겠다. 그저 언론에서 걱정하는 이공계 기피 현상에 대해서 자네도 같이 걱정하며 주저앉고 있지나 않은지 모르겠다. 이공계 기피 현상보다도 더 걱정스러운 것은 자네들의 꿈과 비전이 없음이 더 걱정이다.

도대체 자네 인생은 누가 살아 주는가? 친구가, 부모가, 신문이? 도대체 자네의 꿈과 비전을 누가 만들어 주는가? 친구가, 부모가, 언론이? 꿈과 비전은 참으로 만들기 어려운 것이다. 역학 문제 풀듯이 unique한 정답이 있는 것이 절대로 아니다.

이번 여름방학에는 제발 좀 시간과 노력을 투입해서 위의 다섯 가지 길을 가고 있는 현재의 선배들이 쓴 책들을 위인전처럼 읽거나, 인터넷을 뒤지거나, 직접 인터뷰를 해서라도 그 사람들이 어떻게 각각 그 길로 갔으며, 지금 과연 무슨 일을 하고 있는지 알아보기를 바란다 (첨부 목록 참조).

대기업 CEO, 창업가, 전문연구직, 교수, 전문행정가 등의 다섯 가지 모습에 대해서 적어도 각각 세 사람 정도를 정해서 철저하게 그 사람에 대해서 탐구를 해보라는 말이다. 스티브 잡스를 모르고 어떻게 창업가가 되겠다고 할 것이며, 화성 탐사선 프로젝트 팀장이 어떤 인생을 살고 있는지 모르고서 어떻게 전문연구직이 되겠다고 할 것이냐? 성공한 창업가가 돈을 과연 얼마나 버는지 자세히 알고는 있느냐? 빌 게이츠가 돈 많이 버는 것은 대충은 알고 있겠지만, 그 밖의 창업가는 과연 얼마나 많은 돈을 버는지 알고는 있느냐?

다섯 가지 길을 간 사람들의 모습을 알면 알수록 점점 더 자네들 나름대로의 20년 뒤의 모습이 그래도 더 확실하게 잡힐 것이다. 이것은 마치 5명의 여자 또는 남자 친구 후보들 중에서 누구를 마지막에 선택할 것인가 결정하는 것과 같다. 각 5명을 만나보고 이야기 해보고 해서 점점 더 잘 알수록 이 여자 또는 남자야 말로 정말로 내 친구로 삼고 싶다 하는 마음이 확실해 진다. 그런 노력도 없이 피상적인 모습만 보고 어떻게 결정을 하겠느냐? 자기 나름대로의 꿈과 비전을 정하는 것도 마찬가지다. 이것은 절대로 이성적이고 논리적으로 결정되는 것이 아니다. 그것은 고도의 감정적이고 주관적인 결정이다..

그 꿈과 비전은 가슴 벅찬 그런 것이다. 그러나, 실현하기에는 지금은 거의 불가능해 보이는 그런 것이다. 그렇지만, 아 정말로 나는 이런 굉장한 모습이 되고 싶다 하는 그런 것을 찾아야 한다. 술 먹고 방 구석에 쳐 박혀서 천장만 쳐다보면 꿈과 비전이 가슴 속에 저절로 새겨지는 것이 아니다.

그리고, 이러한 벅찬 꿈과 비전을 생각하면 바로 1ms 정도나 되겠나하는 찰라의 순간 후에 자기 자신이 자기에게 단칼을 내리치게 된다. "니가 무슨 그게 되겠냐? 너는 이러 이러한 성격이고, 돈도 없고, 경쟁이 심한데 그게 되겠냐구. 그 사람은 천재이어서 그렇게 되었지, 네가 무슨.." 이런 식의 단칼이 자네의 꿈과 비전을 무자비하게 박살낸다. 그것은 일종의 열등감이라고도 할 수 있다. 그러나, 아마도 서울대생이면 말은 안하고 있지만 열등감은 더 강할 것이다. 내가 바로 그랬으니까. 명문고와 서울공대에 박사까지 했지만 오히려 우수한 놈들이 모여 있는 집단에서 기가 죽을 때가 더 많았으니까. 심지어 잘 노는 놈에게도 열등감을 느낀다. 자네들도 다 그럴 것이다. 그러나 그것이 바로 서울대에 들어 온 이유이다. 모두 다 결국 자네를 도와 줄 인적 자원이다. 그래서 그런 단칼을 바로 하나 "아직 해보지도 않았지 않았냐. 해 보고나서.. 죽을 힘을 다 해서 해보고 나서, 그 때 가서 결과를 보자" 이렇게 생각하고 꿈과 비전을 세우는 것을 박살내는 그 단칼을 바로 박살을 내야 한다. 세상에 가장 멍청한 것은 해보지도 않고 포기하는 일이다. 해보지도 않고 이 꿈 저 꿈 잘라버리다가는 남는 것이 없다. 결국 그럭 저럭 살다가 아무리 서울대 나와도 평균치기 something이 되어 있는 내 자신을 발견하게 된다. 꿈과 비전을 미리 잡고 그러면 죽을 힘을 다하게 되며 그러면 실현 불가능하게만 보여 졌던 꿈과 비전을 결국 실현하게 된다. 죽을 힘을 다했으니까..

이번 여름방학 동안에 영어 회화 공부나 해야 하겠다고 하는 계획보다도 더 중요한 것이 바로 여름 방학 끝나고 학교로 돌아 올 때에는 이 가슴 속에 절대로 지워지지 않게 각인된 그런 꿈과 비전을 새기고 돌아오기를 바란다.

그런 꿈과 비전이 확실하면 2학기에는 무슨 과목을 수강할 지부터 시작해서, 군대는 언제 어떻게 가고, 대학원을 갈 것인지, 유학을 갈 것인지, 회사는 어떤 회사에 취직을 할 것인지 등등의 모든 결정이 쉬워질 것이며, 그 보다도 더 지금 이 순간 자네가 하고 있는 모든 공부와 사회활동에 대한 의미가 생기며, 비로소 고등학교 3학년 때처럼 또다시 미래을 위해서 죽을 힘을 다 해야 하겠다고 하는 동기가 생길 것이다.

예를 들어서, 도대체 영어 회화 공부는 왜 하려고 하는가? 토플 토익 성적 높이려고? 이런 동기로 영어 공부하는 친구도 있을 것이지만, 20년 뒤에 Global top class 대기업의 CEO로서 세계 각국에서 집결된 임원급 회의를 할 때를 위해서 영어 공부한다고 생각하면 모골이 송연할 정도로 죽을 힘을 다해서라도 잘 해야 하겠다는 생각이 들 것이다.

자네의 미래를 꿈꾸는 것은 자네의 특권이다. 그런데, 서울공대생인 이상 그러한 찬란한 미래를 만드는 것은 하나의 의무 사항이기도 하다. 그것은 군대 가는 것은 비교할 수 없을 정도로 중요하게 자네들에게 지워지는 무거운 짐이기도 한 것이다.

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

이 글을 읽으니 많은 것을 생각하게 해 주네요. 꿈이 있고 비전이 있는 사람과 없는 사람은 확실하게 시간이 흐를수록 차이가 나는것 같습니다.

마인드 컨트롤을 할때도 자신의 미래의 모습을 그리는 것을 중요시 하는 면이 있는데 자신이 원하는 모습이 되기 위해서 그만큼 노력을 하게 만들어 주기 때문이것 같습니다.

대한민국을 살아가는 많은 청년들과 소년들은 자신의 꿈을 생각할 시간도 없이 주변에서 시키는 데로 공부만을 하고 자신에 대한 생각을 할 시간 조차 없는 거 같아서 많이 안타깝습니다.

모두 자신의 꿈과 비전을 세워서 그 꿈을 이루기 위해 정진 하시길 바랍니다.

CFile 사용법

1. 생성
    
    1) 생성자
        CFile( );
        CFile( int hFile );
        CFile( LPCTSTR lpszFileName, UINT nOpenFlags );
        throw( CFileException );

        nOpenFlags:
            CFile::modeCreate
                파일을 생성한다. 만일 이미 동명의 파일이 존재한다면, 파일 크기를 0으로 만들어 생성한다.
            CFile::modeNoTruncate
                modeCreate option과 함께 사용된다.
                동명의 파일이 없을 경우 새로 생성하고, 있다면, Open한다.
            CFile::modeRead
                read only 로 파일을 open한다.
            CFile::modeReadWrite
                읽기/쓰기 용으로 open한다.
            CFile::modeWrite
                write only로 파일을 open한다.
            CFile::modeNoInherit
                child process에의해 상속되어 지슨 ㄴ것을 방지한다.
            CFile::shareDenyNone
                다른 process가 파일에 접근할 수 있도록 한다.
                Opens the file without denying other processes read or write access to the file.
                
                Create fails if the file has been opened in compatibility mode by any other process.
            CFile::shareDenyRead
                다른 process가 이 파일을 읽을 수 없도록 하여 Open한다.
            CFile::shareDenyWrite
                다른 process가 이 파일을 쓰지 못하도록 하여 Open한다.
            CFile::shareExclusive
                다른 process가 이 파일을 읽거나 쓰지 못하도록 하여 Open한다.
            CFile::shareCompat
                This flag is not available in 32 bit MFC.
            CFile::typeText
                Sets text mode with special processing for carriage return?linefeed pairs (used in derived classes only).
            CFile::typeBinary
                Sets binary mode (used in derived classes only).

    2) Sample
    
        char* pFileName = "test.dat";
        
        try
        {
           CFile f( pFileName, CFile::modeCreate | CFile::modeWrite );
        }
        catch( CFileException, e )
        {
            AfxMessageBox( "File Open failed" );
        }
        
2. Open

    1) Proto-Type
        virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL );
        
        lpszFileName: 파일명
        nOpenFlags: 생성자 참조
        pError: CFileException object의 pointer
    
        return: 성공하면, 0 이 아닌 숫자. 실패하면, 0
        
    2) Sample code
    
        CFile f;
        CFileException e;
        char* pFileName = "test.dat";
        if( !f.Open( pFileName, CFile::modeCreate | CFile::modeWrite, &e ) )
        {
            AfxMessageBox( "File open fail:" + e.m_cause );
        }
    
3. Read

    1) Proto-Type
    
        ☞ 64KB 미만을 읽을 경우
        
        virtual UINT Read( void* lpBuf, UINT nCount );
        throw( CFileException );

        ☞ 64KB 이상을 읽을 경우
        
        DWORD ReadHuge( void* lpBuffer, DWORD dwCount );
        throw( CFileException );

        return: 읽혀진 data의 byte수. return 값이 nCount보다 작다면, 파일 끝에 왔다는 뜻.
        
    2) Sample code
    
        char pbuf[100];
        UINT nBytesRead = cfile.Read( pbuf, 100 );

4. Write

    1) Proto-Type
    
        ☞ 64 KB 미만을 쓸 경우
        
        virtual void Write( const void* lpBuf, UINT nCount );
        throw( CFileException );

        ☞ 64 KB 이상을 쓸 경우
        
        void WriteHuge( const void* lpBuf, DWORD dwCount );
        throw( CFileException );
        
        
    2) Sample code
    
        char pbuf[100];
        cfile.Write( pbuf, 100 );

5. Positioning

    File pointer를 옮기는 기능
    
    1) Proto-Type
    
        ☞ 위치 이동
        
        virtual LONG Seek( LONG lOff, UINT nFrom );
        throw( CFileException );
            nFrom: CFile::begin, CFile::current, CFile::end(이 경우 lOff는 음수여야 한다)
        
        ☞ 처음으로
        
        void SeekToBegin( );
        throw( CFileException );
        
        ☞ 끝으로
        
        DWORD SeekToEnd( );
        throw( CFileException );
        
        ☞ 파일 크기
        
        virtual DWORD GetLength( ) const;
        throw( CFileException );
        
        ☞ 파일 크기 변경
        
        virtual void SetLength( DWORD dwNewLen );
        throw( CFileException );

6. 닫기

    virtual void Close( );
    throw( CFileException );
    



출처 : http://jhgoo1398.tistory.com/14?srchid=BR1http%3A%2F%2Fjhgoo1398.tistory.com%2F14


================================================================================================

원본 : http://tong.nate.com/jimmyp68/47181870

 

[강좌] CFile 클래스....

윈도우즈에서 파일 입출력에 대한 처리방법은 여러가지가 있는데, 가장 쉬
운방법은 MFC 클래스 라이브러리에서 기본적으로 제공되고 있는 표준 다이
얼로그 박스(Common Dialog Box)클래스를 이용하는 것입니다. 표준 다이얼
로그 박스는 문자열의 검색과 치환, 색상의 선택, 폰트의 선택, 프린팅에
대한 기능들도 표준 다이얼로그 박스 클래스로 제공되고 있기 때문에 이러
한 기능들을 프로그램에 지원하고자 할 때는 표준 다이얼로그 박스를 이용
하는 것이 훨씬 용이합니다.

Visual C++은 윈도우즈 상에서 보다 효율적으로 파일처리를 할 수 있도록
CFile 클래스와 직렬화(Serialization)기능을 제공하고 있습니다.

CFile 클래스

MFC는 표준 파일 처리를 위해 CFile 클래스를 제공하고 있습니다. 이 CFile
클래스에는 파일처리를 위한 많은 멤버함수들이 포함되어 있는데, 이러한
함수들은 이전에 도스상에서 자주 다루어 보았던 함수들과 유사합니다.

 

멤버함수

의미

CFile()
Abort()
Duplicate()
Open()
Close() 
 
Read()
Write()
Flush() 
 
Seek()
SeekToBegin()
SeekToEnd()
GetLength()
SetLength()
 
LockRange()
UnlockRange()
 
GetPosition()
GetStatus()
GetFileName()
GetFileTitle()
GetFilePath()
SetFilePath()
 
Rename()
Remove()
GetStatus()
SetStatus() 
객체 생성자.
모든 경고와 에러들을 무시하고 화일을 닫는다.
오브젝트 복사.
파일을 연다.
파일 종료 및 오브젝트 삭제.
 
파일로 부터 데이타를 읽는다.
현재파일 위치에 데이타를 쓴다.
데이타를 제거한다.   
  
파일포인터를 이동한다.
파일 포인터를 처음으로 이동한다.
파일 포인터를 끝으로 이동한다.
파일의 길이를 얻는다.
파일의 길이를 지정한다.
   
파일의 일부를 락(Lock)을 건다.
파일의 일부를 락에서 해제한다. 
 
파일의 포인터를 얻는다.
지정된 파일의 상태를 얻는다.
선택된 파일의 파일이름을 얻는다.
선택된 파일의 타이틀을 얻는다.
선택된 파일의 전체 경로를 얻는다.
선택된 파일의 전체 경로를 설정한다.  
 
지정된 파일을 다른이름으로 변경한다.
지정된 파일을 제거한다.
지정된 파일의 상태를 얻는다.
지정된 파일의 상태를 설정한다.      

 

멤버함수들을 알았으니 CFile 클래스로 파일 쓰기와 파일 읽기 예제를 보
면서 공부해 보겠습니다.

#파일쓰기 예제

void CFileioView::OnWritefile()
{
   CFile Wfile;
   if(!Wfile.Open("TestFile.txt", CFile::modeCreate :
                    CFile::modeWrite))
   {
       MessageBox("Can't Create testfile.txt !", "Warning",
                       MB_OK : MB_ICONHAND);
       return;
   }
   char* ps = new char[27];
   char* ps2 = ps;

   for(int i=0;i<26;i++)
       *ps2++ = 'A'+i;
   *ps2 = NULL;    // NULL 문자로 끝나게 한다.
   Wfile.Write(ps,27);
   Wfile.Close();
   delete ps;
}

#파일읽기 예제

void CFileioView::OnReadfile()
{
   CFile Rfile;
   if(!Wfile.Open("TestFile.txt", CFile::modeRead))
   {
       MessageBox("Can't Open testfile.txt !", "Warning",
                       MB_OK : MB_ICONHAND);
       return;
   }
   UINT FileLength = (UINT)Rfile.GetLength();

   char* ps = new char[FileLength];
   Rfile.Read(ps,FileLength);
   Rfile.Close();

   CClientDC dc(this);
   dc.TextOut(0,0,ps,lstrlen(ps));
   delete ps;
}

마지막으로 파일의 특정위치에 데이타를 읽거나 쓸수 있도록 처리해주는
CFile 클래스의 Seek()멤버함수를 이용해보겠습니다.

#랜덤 파일 처리 예제

void CFileioView::OnAddfile()
{
   CFile Afile;
   if(!Afile.Open("TestFile.txt", CFile::modeRead :
                    CFile::modeWrite))
   {

       MessageBox("Can't Create testfile.txt !", "Warning",
                       MB_OK : MB_ICONHAND);
       return;
   }
   Afile.Seek(-1,CFile::end);
   ~~~~~~~~~~~~~
   char* ps = new char[27];
   char* ps2 = ps;
   for(int i=0;i<26;i++)
       *ps2++ = 'a'+i;
   *ps2 = NULL;    // NULL 문자로 끝나게 한다.
   Wfile.Write(ps,27);
   Wfile.Close();
   delete ps;
}

참고로 Seek()함수에 대해서 알아보겠습니다...

Seek()함수의 첫번째 매개 변수는 양수이면 앞쪽으로 파일포인터를 이동하
고, 음수이면 뒷쪽으로 파일포인터를 이동합니다. 두번째 매개 변수는 파일
의 첫번째 위치를 기준으로 파일포인터를 이동하고자 하면 CFile:begin 플
래그를 사용하고, 현재위치나 파일의 마지막을 기준으로 이동하고자 한다면
CFile::end 플래그를 사용하면 됩니다.
CFile 클래스는 이 함수 외에도 SeekToBegin()와 SeekToEnd()함수를 제공
하고 있습니다.

 

이번엔 파일의 예외처리에 대해서 알아보겠습니다...

⊙ 예외처리(Exception Handling)

MFC는 파일예외처리를 위해서 CFileException 클래스를 제공하고 있습니다
CFileException 클래스는 파일을 오픈하면서 발생되는 여러가지 에러에 관
련된 정보를 포함하고 있습니다.
파일처리에 있어서 발생되는 에러는 CFileException 클래스의 m_cause 데
이타 멤버의 값을 참조하게 됩니다. 파일 에러가 발생했을때 m_cause데이타
멤버에 발생된 에러코드가 들어가게 되는데, 이것들에는 다음과 같은 것들
이 있습니다.

   에러코드

none
generic
fileNotFound
badPath
tooManyOpenFiles
accessDenied
invalidFile
removeCurrentDir
directoryFull
badSeek
hardIO
sharingViolation
lockViolation
diskFull
endOfFile
에러가 없다
일반적인 에러
파일이 존재하지 않는다
파일 경로가 맞지 않다
허용된 파일 오픈 수를 넘었다
파일 접근 금지
파일에 이상이 있다
현재 디렉토리를 제거할 수 없다
디렉토리가 모두 찼다
파일 포인터 지정에러
하드웨어 에러
SHARE.EXE가 로드되지 않았거나 공유된 지역에 락이 걸렸을 경우
이미 락이 걸려 있는 지역에 락을 시도했다
디스크가 모두 찼다
파일의 끝에 도달하였다

 

실행에러 즉, 예외처리를 하는 TRY, CATCH, END_CATCH 매크로가 사용됩니
다. 이들 매크로는 메모리(CMemoryException), 리소스(CResourceException)
, OLE(COleException)등에 관련된 예외처리를 담당합니다.

예>
    // example for CFileException::m_cause
    extern char* pFileName;
    TRY
    {
         CFile f(pFileName, CFile::modeCreate : CFile::modeWrite);
    }
    CATCH(CFileException, e)
    {
         if(e->m_cause == CFileException::fileNotFound)
                                          ??????
              printf("ERROR : File not found\n");
    }

이번엔 switch ~ case 문으로 해볼까요?

    extern char* pFileName;

    TRY
    {
         CFile f(pFileName, CFile::modeCreate : CFile::modeWrite);
    }

    CATCH(CFileException, e)
    {
         switch(e->m_cause)
         {
              case CFileException::fileNotFound :
                                   ??????
                   MessageBox("File not found");
                   break;
              case CFileException::diskFull :
                                   ????
                   MessageBox("Disk is full");
                   break;
                   :
                   :
         }

    }
    END_CATCH


  1. planchas ghd 2013.07.27 09:00

    당신은 내가사랑할 만한 사람이 아니예요,사랑하지 않으면 안될 사람이예요.

CString 을 일반적으로 사용한다던지 또는 DEBUG로 사용할 때는 보통 별 문제 없이 사용이 된다.
하지만, 파일을 Read()하는 것 처럼 큰 데이터를 사용하려 할 때는 아래와 같이 정확히 원하는 만큼의 버퍼를 잡아주고, 사용이 끝나고 난 후에는 버퍼를 해제해 주어야 에러없이 잘 동작한다..
주의하자..!!!!

>>> 참고 : 윈도우 프로그래밍 플러스 : 김은철 저 <<<
LPTSTR GetBuffer(int nMinBufferLength);
--- nMinBufLength : C 형태로 사용할 최소 버퍼의 크기. 현재 CString 버퍼보다 크게 설정하면 CString 버퍼가 그 크기만큼 자동으로 재 설정되며, 작게 설정하면 CString 버퍼 크기가 그대로 유지된다. CString 버퍼를 char*로 변환하는 용도로만 사용한다면 0으로 하면되고, CString 버퍼를 직접 접근하여 문자열을 수정하려면, 최대 문자열 길이 +1 해주면 된다.
--- 반환값 : C 형태로 접근할 수 있는 CString 버퍼의 포인터
LPTSTR GetBufferSetLength(int nNewLength);
--- nNewLength : C 형태로 사용할 버퍼 크기. 현재 CString 버퍼크기보다 크게 설정하면 CString 버퍼가 그 크기만큼 자동으로 재 설정되며, 작게 설정하면 버퍼의 크기는 CString 버퍼크기가 그대로 유지되지만 문자열은 그 크기만 남고 잘려 나간다. CString 버퍼의 char*를 얻으면서 지정된 크기만큼의 문자열만 사용할 때 유용하다.
--- 반환값 : C 형태로 접근할 수 있는 CString 버퍼의 포인터
void ReleaseBuffer(int nNewLength = -1);

사용된 메모리 할당 해제 ->GetBuffer()와 함께 사용
--- nNewLength : 보통 생략. CString 버퍼의 char*를 사용하여 문자열을 수정한 경우 현재 버퍼의 크기를 정확하게 입력해야 한다.
UINT Read(void* lpBuf, UINT nCount);
--- 개방된 파일에서 64K 미만의 텍스트 또는 바이너리를 읽을때 사용
--- 반환값 : 실제 읽혀진 바이트 수
UINT ReadHuge(void* lpBuf, UINT nCount);
--- 개방된 파일에서 64K 이상의 텍스트 또는 바이너리를 읽을때 사용
--- 반환값 : 실제 읽혀진 바이트 수
*** Read, ReadHuge는 텍스트형이나 바이너리형 모두 읽을 수 있다.
*** Read 함수를 상속 받은 클래스에서 파일을 텍스트 모드로 사용시 ("\r\n")은 자동으로 개행문자("\n")로 변환된다
void Write(void* lpBuf, UINT nCount);
--- Read와 반대
void WriteHuge(void* lpBuf, UINT nCount);
--- ReadHuge와 반대

예1 ) GetBuffer, ReleaseBuffer
char* p;
CString str("12345");
p = str.GetBuffer(0); //str문자열 크기만큼 자동할당
....
str.ReleaseBuffer(); //사용해제

예2 ) GetBufferSetLength, ReleaseBuffer
char* p;
p = str.GetBufferSetLength(3); //3바이트 만큼 할당
...
str.ReleaseBuffer(); //사용해제

파일읽기 사용예3 )
DWORD dwLength = tesfile.GetLength();
dwLength = tesfile.ReadHuge(strOldData.GetBufferSetLength(dwLength), dwLength);
strOldData.GetBufferSetLength(dwLength);
strOldData.ReleaseBuffer();
tesfile.Close();


MFC - UpdateData() 함수

원형: BOOL UpdateData(BOOL bSaveAndValidate=TRUE);

UpdateData() 함수는 DDX_Text 계열, 즉 Value 형태로 연결되었을 때 사용한다.
파라미터가 TRUE일 때는 컨트롤의 데이터를 컨트롤과 연결된 멤버 변수로 가져오는 기능을 하며,
반대로 FALSE일 때는 컨트롤과 연결된 변수의 내용을 컨트롤에 세팅하는 기능을 한다.

예) 간단한 사칙연산 계산기

void CTttDlg::Calcul(char type)
{
  //Edit Box로 부터 값을 받아옴.
  m_NUM1;   
  m_NUM2;
  UpdateData(TRUE);
  // 받아온 값을 type에 따라 계산해서 Edit Box 에 출력함
  switch(type)
  { 
  case '*':
     m_OutPut=m_NUM1*m_NUM2;
     UpdateData(FALSE);
   break;
  case '+':
    m_OutPu  t=m_NUM1+m_NUM2;
    UpdateData(FALSE);
   break;
  case '-':
    m_OutPut=m_NUM1-m_NUM2;
    UpdateData(FALSE);
   break;
  case '/':
    m_OutPut=m_NUM1/m_NUM2;
    UpdateData(FALSE);
  break;
}

혹 DDX를 사용하지 않고 컨트롤의 핸들을 가져 오려면, 컨트롤의 포인터를 받을 포인터 변수가 필요하다.
즉, Edit Box의 포인터를 얻기 위해 CEdit 클래스의 포인터 변수를 선언한다.

예)
CEdit *pEdit;
pEdit = (CEdit *)GetDlgItem(IDC_EDIT1);
pEdit->SetWindowText("안녕하세요!");
출처 블로그 > comeOn&tearMeDown
원본 http://blog.naver.com/phoelan/140019953126
윈도우의 프로세스와 스레드

1.1 시분할 멀티태스킹 (Time slicing Multitasking)

운영체제는 동시에 많은 프로그램을 수행하고 있는 것처럼 보이지만, 실제로는 아주 짧은 하나의 실행 단위 시간동안 운영체제의 일부와 조합된 특정 코드가 시스템 전체를 장악하고 있으며, 사람이 느끼지 못할 정도의 짧은 시간이 계속 전환되면서 마치 시스템 사용자들의 입장에서는 동시에 많은 작업이 수행되는 것처럼 착각하게 된다. 이러한 방식을 시분할 멀티태스킹이라고 한다.

보통 CPU의 사용 점유율을 이야기할 때, 시분할 멀티태스킹의 개념을 사용하게 되며, 따라서 특정 프로그램이 CPU를 10% 점유하고 있다는 의미는 1 시간동안 컴퓨터를 사용한다면 이 중 0.1시간은 특정 프로그램 수행을 위해 사용하고 있다는 의미가 된다.

시분할 멀티태스킹은 협업(cooperative, 비선점형)과 선점형(preemptive) 멀티태스킹으로 구분한다. 비선점형 멀티태스킹은 운영체제가 강제로 작업 제어권을 획득할 수 없으며, 각종 자원의 점유 문제로 인해 시스템에서 불필요하게 소모되는 시간이 많다. 현재 운영되고 있는 실제 운영 체제는 선점형 멀티태스킹을 사용하고 있다.

선점형 멀티태스킹은 운영체제에 의해 프로그램 수행 여부가 결정되며, 운영체제의 동작 역시 백그라운드 (윈도우 시스템에서 백그라운드/포그라운드의 개념은 유닉스에서의 의미가 아니다)로 동작한다.

운영체제에 의해 시스템 자원이 배분되고 프로세스의 동작이 결정되기 때문에 특정 프로세스의 자원 점유로 인한 시스템의 안정도 하락 문제가 거의 발생하지 않는다.

1.2 스케줄링(Scheduling)

시분할로 멀티태스킹을 한다면 일정 시간 동안 어떤 순서로 프로그램을 수행시킬 것인가가 문제로 대두된다. 윈도우 시스템에서는 이 단위를 퀀텀(Quantum)이라고 하며 근본적으로 20ms의 수행 시간 간극을 갖고 있다. 또한, 스레드 자체도 위계 질서를 갖고 있으며, 항상 높은 위계 질서의 스레드가 수행 우선권(priority)을 갖고 있다. 수행 우선권이 가장 높은 작업이 먼저 수행된다.

윈도우에서는 스케줄링 퀀텀이 스레드에게 배분된다. 이것은 스케줄의 기본 단위가 스레드이며, 프로세스 단위로 스케줄링이 배분되는 유닉스 시스템과는 차이를 갖게 된다. 스케줄의 기본 단위가 프로세스가 아닌 스레드라는 것은 모든 프로세스는 기본적으로 스레드를 갖게 된다는 의미이다.


2.1 프로세스

프로세스는 애플리케이션에서 사용되는 리소스를 관리하는 가장 큰 체계이다. 프로세스는 일정 고유 영역의 메모리를 할당받고 운영되며, 하나의 프로세스는 전역변수와 환경변수 및 힙과 스레드 스택 및 코드를 갖고 있다. 스레드는 프로세스의 일부분에 속한 개념이다.

윈도우 프로세스는 유닉스 개념의 프로세스와 그렇게 큰 차이는 없다. 다만 프로세스 자체가 포괄하는 개념적 측면이 더 많기 때문에 근본적으로 그 규모가 더 크게 보이며, 윈도우 시스템이 멀티스레딩을 기준으로 설계되어 있기 때문에 다량의 프로세스가 발생할 때 시스템의 속도가 저하되는 문제점을 맞이할 수 있다.

프로세스 단위로 멀티태스킹이 수행되는 것은 전통적인 윈도우 방식이지만, 이는 몇 가지 한계점을 갖고 있다. 먼저 프로세스 상호간 통신 방법상의 한계점이 가장 크다. 각각의 프로세스는 모드 다른 사용자 권한을 갖고 있으며, 구분되는 메모리 영역을 갖고 있기 때문에 별도의 프로그램적 과정이 필요하다. 보통 파이프(Pipe)라고 불리우는 프로세스 상호간의 통신 수단과 IPC(Interprocess Call)를 사용할 수 있게 코드가 작성되어야 프로세스 간의 데이터 이송을 할 수 있다. 전통적으로 프로세스 사이에서 통신할 수 있는 여러 방법론들이 제시되었다.

IPC가 프로세스 상호간의 통신을 담당하지만, 실제 IPC를 쓰는 것은 DCE/RPC를 사용할 경우 외에는 그렇게 많이 존재하지 않는다. 로컬 컴퓨터에서 멀티태스킹으로 애플리케이션이 수행될 경우 멀티스레딩으로 처리하기 때문에 프로세스 상호간의 변수 전달과 같은 문제들이 발생하지 않기 때문이다.

2.2 스레드

스레드는 프로세스 내의 실행부 코드이다. 유닉스에서는 굳이 스레드를 작성하지 않으면 스레드가 만들어지지 않지만, 윈도우 시스템의 경우에는 스레드를 작성하지 않아도 스케쥴링의 문제 때문에 최소한 하나의 스레드가 동작하게 된다. 예를 들어 다음과 같은 간단한 프로그램을 컴파일하더라도 하나의 스레드가 발생된다.

#include <iostream.h>

void main()

  char a;
  cin >> a;
  cout << a << endl; 
}

현재 운영되고 있는 스레드는 작업 관리자에서 확인할 수 있다. 작업 관리자에는 현재 운영중인 모든 프로세스에 할당된 스레드를 볼 수 있게 나타나 있으며, 이렇게 스레드를 별도로 제작하지 않은 상태에서 발생되는 스레드를 주 스레드(Primary Thread) 라고 일컫는다. 주 스레드는 인스턴스 번호가 0 번으로 나타난다.

 

스레드는 보편적으로 함수 하나 정도의 규모이며, 메모리나 자원의 할당과 같은 문제들을 생각하지 않아도 되고 스레드는 모두 프로세스의 전역 변수 등을 공용으로 사용하고 있기 때문에 인수 전달에서의 장점을 갖게 된다. 예를 들어, 하나의 프로그램에서 인쇄 작업을 별도의 스레드로 제작하였을 경우 사용자들은 인쇄 작업을 수행하면서 다른 작업을 같이 영위할 수 있다. 이 때, 인쇄 명령을 내리고 스풀러에 데이터가 전달되기 전에 해당 스레드를 호출한 프로세스를 닫아 버리면 인쇄가 중단되어 버리는 사태를 맞게 된다. 스레드가 참조하는 프로세스의 자원이 모조리 사라져 버리기 때문이다.

또한 프로세스 간 데이터를 전달하기 위해서 별도의 작업이 필요하지 않다. 하나의 프로세스 내에서 모든 인수를 갖고 있으므로, 전역 변수의 경우나 할당된 자원은 모든 스레드가 기본적으로 같이 사용하게 된다.

인쇄와 같은 작업이 프로세스 사이에서 이루어진다면 상대적으로 데이터를 설계하기 힘들고, DLL을 참조하여 통합 코드를 사용한 인쇄 등이 어렵게 된다. 물론 굳이 멀티스레딩으로 제작하지 않고, 단일 프로세스에서 DLL 내의 인쇄 루틴을 호출하는 방법도 생각해 볼 수 있지만, 그런 경우 인쇄 명령을 내리면 인쇄 작업이 끝나기 전까지 (정확하게는 스풀러에 들어가기 전까지) 다른 작업을 수행할 수 없을 것이다. (스레드는 프로세스와는 달리 경량으로 제작되므로 동시에 많은 스레드가 동작하더라도 시스템에 부하를 그렇게 많이 주지 않는다. 반면 윈도우 시스템에서 프로세스가 여럿 동작하면 시스템은 상대적으로 심각한 부하를 받게 된다 이런 이유로 웹 서버 성능 테스트 및 상호 성능 비교를 유닉스 계열과 윈도우 계열에서 동일한 방식으로 할 수 없다. 실행 프로세스 기반의 CGI를 사용하면 윈도우 시스템이 백전백패할 것이 명백하기 때문이다).

2.3 컨텍스트

많은 디버깅 서적, 혹은 프로그래밍 서적에서 갑자기 프로그래밍 도중 "문맥"과 같은 엉뚱한 말이 등장하는 것을 볼 수 있다. 그 말은 이 컨텍스트라는 단어를 우리말로 직역하다 생긴 용어이다.

컨텍스트는 “문맥”이라고 번역되는 것보다는 “정황”으로 번역되는 것이 훨씬 더 적절하다. 윈도우 프로그램에서의 컨텍스트는 운영중인 시스템에서 스레드가 동작하기 위한 정황 정보, 즉 멀티태스킹 환경에서는 시간에 따라 자원을 점유하는 스레드가 달라지는데, 미처 자신의 작업을 마무리하지 못하고 전환 시간을 맞게 되었을 경우 스레드가 다시 자신이 동작할 수 있는 시간으로 돌아왔을 때, 원래 동작하던 내용과 완벽히 일치하도록 상황을 저장해 놓은 다음 복귀해야 한다. 이 때 저장되는 값들, 스레드가 원점으로 되돌아왔을 경우 원래 동작하던 내용을 그대로 저장해 놓은 것을 컨텍스트라고 한다. 따라서, 레지스터의 내용, 스택 포인트, 스레드 주소 공간, 운영 변수 등이 이 컨텍스트이며, 마무리되지 못한 스레드가 원래 동작하던 정확한 지점으로 복귀될 수 있는 최소한의 필요 정보가 삽입된다. 따라서, 컨텍스트는 “정황”으로 번역하여 사용하는 것이 더욱 정확하지만, 여기서는 그냥 컨텍스트로 사용하도록 하겠다.

컨텍스트 전환(Switching)은 시분할 멀티태스킹에 따라 현재 실행중인 스레드가 시간에 따라 자꾸 변하면서 관련된 정확한 컨텍스트를 연결해 주는 작업이다. 다시 말하면 스레드의 전환 그 자체라고 생각해도 크게 다르지 않다.

컨텍스트는 Win32에서 사용하는 구조체 중 CPU에 종속적인 부분이다. 따라서, 윈도우 멀티스레딩 방식의 프로그램을 다른 컴퓨터로 이전할 경우 컨텍스트와 관계된 여러 문제가 발생할 수 있다.

2.4 멀티스레드

스레드는 독자적으로 행동하는 프로세스와 성질이 크게 다르지 않다. 그런데 시작과 종료가 항상 스레드를 발생시킨 프로세스에게 종속되어 있다. 이 말은 스레드가 종료되기 전 프로세스가 종료되면 스레드는 모두 허공에 사라져 버리는 결과를 낳는다는 것이다. 대부분의 윈도우 프로그램은 메시지 큐 기반의 무한 루프를 타는 방식을 사용하므로 이러한 문제가 민감하게 다가오지 않겠지만, 경우에 따라 순차적인 프로그램을 멀티스레드로 운용하는 수가 있다. 일정 작업을 한 뒤 종료하는 순차적 프로그램일 경우에는 모 프로세스가 항상 스레드보다 먼저 종료하지 않도록 주의하는 것이 매우 중요하다.

스레드는 프로세스와 자원을 공유하는 장점이 있지만, 이러한 문제 때문에 자원의 점유 경쟁이 벌어진다. 자원의 점유 경쟁은 스레드 상호간에 이루어지며, 스레드를 발생시킨 프로세스 역시 운영의 핵심은 주 스레드이기 때문에 하나의 스레드로 취급된다. 자원의 경쟁 때문에 실제로 프로그램이 중단되는 일은 잘 벌어지지 않지만, 예상하지 못한 결과가 종종 발생하게 된다. 따라서, 멀티스레드를 사용하는 프로그램에서는 현재 원하는 변수 혹은 입출력을 다른 스레드가 쓰고 있을 확률이 없는지 면밀히 검토해야 한다.


3.1 Win32 API 함수로 스레드 생성

스레드는 Win32 API에서 생성하는 방법과 MFC의 CWinThread 클래스를 통하는 두 가지 방법이 있다. Win32 API는 다시 CreateThread() 함수를 사용하는 API 함수와 _beginthread() 런타임 라이브러리 함수 호출이 존재한다. 런타임 라이브러리에서 호출하는 것은 근본적으로 Win32 래퍼를 통한 것이다.

당연히 API 단계의 스레드 생성 호출이 저급 호출로 분류된다. 따라서, MFC를 사용하여 멀티스레딩을 구현한다면 근본적으로 CWinThread 클래스를 사용하여 다른 MFC 코드와 조화를 맞추어 사용하도록 처리하는 것이 좋다. MFC가 거의 사용되지 않는 시스템 레벨의 프로그래밍에서는 CreateThread() 함수 정도로 족하다.

스레드는 함수를 독립적으로 실행시키는 것 이상의 개념은 아니다. 따라서, 스레드로 수행되는 함수는 기존 C/C++ 프로그램에서 사용하는 함수와 생긴 모양이 크게 다르지 않다. 다만, 함수형이 DWORD 형으로 고정되고, 입력값으로 VOID 형 포인터를 받아들이면 된다. 적절한 값을 포인터로 전달하고, 해당 값을 필요에 따라 형변환하여 사용하면 된다.

다음 소스는 hello, there를 스레드를 사용하여 인쇄하는 예제이다.

#include <windows.h> 
#include <iostream> 
using namespace std; 
DWORD WINAPI myfunction(LPVOID); 
int main()
{
  HANDLE hThread = NULL; 
  DWORD threadId = NULL; BOOL bOK; 
  char *hello = "hello, there";
  cout << "몇 개의 Hello, there를 인쇄할까요?(-1:끝냄)" << endl;
  int i,j;
  
  do
  {
    cin << i; 

    if (i==-1)
      return -1;
    
    for(j=0;j<i;j++)
    {
      hThread = CreateThread (NULL, // ACL 제어 여부, NULL:안함 
                0// 할당될 스택 공간, 0:초기상속
                &myfunction,// 참조값이 아니라도 참조값으로 인식
                (LPVOID)hello,// 인수에 대한 포인터 0: VOID 
                0// 생성 플래그
                &threadId); // 반환값=스레드 ID
      if(hThread == NULL) 
      { 
        cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; 
        return -1
      } 
      bOK=CloseHandle(hThread);
    } 
  }
  while(1); 
  return 0


DWORD WINAPI myfunction(LPVOID p)
{
  cout << (char*)p << endl; 
  return 0
}

이 프로그램은 사용자로부터 숫자를 입력받아 해당 숫자만큼 루프를 수행하며 hello, there를 인쇄하는 기능을 수행한다. 주 스레드는 스레드를 호출하는 역할만을 수행하며, 만일 주 스레드에서 루프문에 다른 메시지를 출력하기 위해 cout을 하나 추가할 때마다 생성된 스레드의 출력값과 주 스레드의 출력값이 엉키는 결과가 발생된다.

루프문 중간에 cin을 둔 이유는 프로그램을 잠시 중단하기 위한 까닭이 크다. 이 순차적 프로그램은 스레드 생성과는 별개로 종료 시점을 판단하기 때문에 메인 루틴이 종료하면, 즉 주 스레드가 종료하면 생성한 스레드의 생존 여부와 관계 없이 모든 스레드는 소멸되어 버린다. 따라서, 컴퓨터가 무척 느리지 않다면 결과를 확인하기 위해 잠시 주 스레드를 중단시켜 놓는 루틴을 삽입해 주어야 한다. 그렇지 않다면 이 프로그램의 결과 아무 것도 출력되지 않을 것이다.

프로그램의 전체 구조는 그다지 이해하기 어렵지 않을 것이다. 함수를 호출할 때 CreateThread() 함수를 사용하여 스레드로 실행할 함수명과 인수 포인터를 삽입한다. CreateThread() 함수의 반환값은 스레드 핸들로 주어지며, 이 핸들이 하는 역할은 단지 스레드에서 정보를 수집하거나 스레드를 종료시키는 것 뿐이다. CreateThread() 함수가 성공적으로 수행되었으면 이제 스레드는 독립적인 하나의 객체로 생명을 얻게 된다.

CreateThread 함수의 ACL은 윈도우 9x 버전에서는 별로 의미가 없다. 하지만 윈도우 NT, 2000, XP 등에 적용된 보안 구조체가 삽입된다. 이는 이 스레드의 보안성을 판별하며, 다음과 같은 구조체의 상속 지점을 정의한다.

typedef struct _SECURITY_ATTRIBUTES
{
  DWORD nLength; 
  LPVOID lpSecurityDescriptor;
  BOOL bInheritHandle;
} SECURITY_ATTRIBUTES

상속 지점을 NULL로 입력하면 특별히 스레드 핸들에 대한 보안성을 지정하지 않고, 기존에 갖고 있던 보안성 개체(즉 모 프로세스의 보안 개체)가 상속된다. 윈도우 9x 버전에서는 어쩔 수 없이 NULL을 입력해야 할 것이며, 이는 윈도우 NT, 2000, XP 등의 커널과 9x 시스템의 근원적인 차이가 된다. 9x 커널에서는 NULL 외에 입력할 수 있는 옵션이 존재하지 않는다.

ACL을 상속받지 않고 직접 입력이 가능하다는 것을 일단 기억해 두기 바라며, 필요한 경우 사용하도록 한다. 보편적으로는 NULL로도 만족할 만한 프로그램을 작성할 수 있다.

할당될 스택 공간에 0을 입력하게 되면 초기 스택은 원래 지정받아야 될 크기로 입력되며, 0이외의 값을 삽입하면 필요에 따라 다른 스레드 스택 크기를 제공할 수 있다. 보편적으로는 건드리지 않는 값이며, 필요할 경우 DEF 파일을 조정하여 STACKSIZE 구문으로 스택의 크기를 조정할 수 있다. 별도로 필요한 옵션이 존재하지 않는다면 NULL을 사용하면 된다.

정의한 함수명은 스레드로 실행될 DWORD 반환 함수를 사용하면 되겠지만 함수의 인수가 삽입되는 방식은 VOID 형 포인터이기 때문에, 입력과 출력이 생각보다 형변환이라는 문제에 걸려 신경이 많이 쓰이게 된다. 따라서, 실제로 Win32 API 프로그램을 사용할 경우 이런 문제를 직접 컨트롤해야 한다. MFC에서 CWinThread 클래스를 사용할 경우는 조금 더 깔끔한 코드를 얻을 수 있으며, 포그라운드 애플리케이션을 제작할 경우 주로 MFC를 사용하므로, 백그라운드 프로그램을 주로 작성할 것이 아니라면 가급적 MFC의 CWinThread로 통일하는 것이 좋다.

생성된 스레드는 결과값을 반환함과 동시에 사라진다. 따라서, 별도의 명령을 내리지 않아도 운영체제는 해당 스레드를 참조하는 참조 수를 내리거나, 핸들을 없애 버린다. 하지만, 이것은 어디까지나 운영체제의 이론적 동작일 뿐이며, 실제 구현상의 문제 때문에 사용이 끝난 핸들이 남아 있는 경우가 생긴다. 이를 막기 위해 명시적으로 핸들을 닫아 줄 필요가 있다. 핸들과 관계된 함수는 공통으로 사용할 수 있으며, CloseHandle() 함수를 사용하면 된다. 이는 핸들의 확인사살이며, 실제로는 운영체제에게 해당 스레드의 참조 수를 내리게 되는 역할을 한다. 그러므로, 스레드를 확인사살하지 않는다. 스레드는 프로세스와 성격이 비슷하여 굳이 별도의 작업을 하지 않아도 리턴값을 반환하면 사라지게 된다.

스레드의 종료 역시 함수의 리턴값으로 처리할 수 있지만, 경우에 따라 일반적인 exit() 함수와 비슷한 역할을 요구할 때가 있다. 이 경우에는 결과값을 되돌리지 않고 스레드를 종료하며 ExitThread() 함수를 사용하여 스레드 중간에서 동작을 중지시킬 수 있다. 스레드 외부에서는 TerminateThread() 함수를 사용하여 주 스레드로부터 생성된 스레드를 삭제할 수 있다.

3.2 스레드 상태 구하기

크게 사용되지는 않겠지만 스레드의 상태를 구하는 몇 가지 함수들이 존재하고 가장 대표적인 예가 GetExitCodeThread() 함수이다. 이 함수는 스레드의 종료 상태를 반환하며, BOOL 형이기 때문에 종료되었는지 실행중인지 두 가지 경우 중 하나만 등장하게 된다. 스레드는 기본적으로 운영체제에 의해 소멸이 결정되므로 실제 스레드를 종료시키는 명령을 사용하는 것과 스레드가 자연히 소멸하는 것은 약간의 시간 간극이 존재한다. 실제로 스레드의 종료 상태 등을 구하는 함수의 용도는 그렇게 많지 않다.

#include <windows.h>
#include <iostream>
using namespace std; 

DWORD WINAPI myfunction(LPVOID);
int main()

  HANDLE hThread = NULL; 
  DWORD threadId = NULL; 
  BOOL bOK; 
  FILETIME CreationTime; 
  FILETIME ExitTime; 
  FILETIME KernelTime; 
  FILETIME UserTime; 
  SYSTEMTIME Csystem; 
  SYSTEMTIME Esystem; 
  SYSTEMTIME Usystem; 
  DWORD retCode=0
  hThread = CreateThread(NULL, // ACL 제어 여부, NULL:안함 
               0// 할당될 스택 공간, 0:초기상속 
               myfunction, // 정의한 함수
               NULL,// 인수에 대한 포인터 
               0// 생성 플래그 
               &threadId); // 반환값=스레드 ID 
  if(hThread == NULL) 
  { 
    cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; 
    return -1
  } 
  WaitForSingleObject(hThread, INFINITE); //스레드 상태가 변하는 것을 기다린다 
  GetExitCodeThread(hThread, &retCode); 
  if(retCode!=STILL_ACTIVE)
  { 
    GetThreadTimes( hThread, //스레드 
            &CreationTime, //시작시간 
            &ExitTime, //종료시간 
            &KernelTime, //커널모드 동작시간 
            &UserTime); //사용자모드 동작시간 
    FileTimeToSystemTime(&CreationTime, &Csystem); 
    FileTimeToSystemTime(&ExitTime, &Esystem); 
    FileTimeToSystemTime(&UserTime, &Usystem); 
    cout << "스레드 ID" << threadId << endl; 

    cout << "스레드 생성 시각 " << Csystem.wYear 
       << "년" << Csystem.wMonth 
       << "월 " << Csystem.wDay 
       << "일 " << Csystem.wHour 
       << "시 " << Csystem.wMinute 
       << "분 " << Csystem.wSecond 
       << "초" << endl;
    if (Esystem.wYear=1601)
      cout << "종료시각이 정의되지 않았습니다." << endl;
    else
      cout << "스레드 종료 시각 " << Esystem.wYear 
         << "년" << Esystem.wMonth 
         << "월 " << Esystem.wDay 
         << "일 " << Esystem.wHour 
         << "시 " << Esystem.wMinute 
         << "분 " << Esystem.wSecond 
         << "초" << endl; 
    
    cout << "사용자모드 동작 시간 " << Usystem.wMinute 
       << "분 " << Usystem.wSecond 
       << "초 " << Usystem.wMilliseconds 
       << "밀리초" << endl; 
    
    bOK=CloseHandle(hThread); 
    exit(0); 
  } 
  return 0


DWORD WINAPI myfunction(LPVOID p)

  long j,k; k=0
  
  for(j=1;j<1000000000;j++) //클럭이 높은 요즘 PC는 많이 돌아야 초단위가 나온다. 
  
  k=j+k; 
  ExitThread(0); 
  return 0
}



위에서 제시된 코드는 스레드의 여러 상태 정보를 출력하는 것이다. 실제로 이 코드는 아주 성능이 엉망이며, 가급적 종료 상태를 확인하기 위해 루프문을 사용하는 일은 하지 않기 바란다. 가장 간략한 예를 들기 위해 필자는 루프문을 사용한 것 뿐이다.

프로그램에서 스레드의 생성 시간과 소멸 시간은 모두 GMT 시간으로 표현된다. 따라서, 현재 시간과 맞지 않다고 해서 시계가 틀렸다고 생각하지 않기 바란다. 시스템 시간 함수에 대해서는 시스템을 다루는 부분에서 언급하게 될 것이다.

스레드는 여러 다양한 상태를 가지며, 이 부분은 사실 운영체제의 소관이다. 따라서, API 레벨에서는 크게 언급할 부분은 아니다. 다만 상태 변화에 따른 정보를 프로그래머가 받아 처리할 수 있다. 여기서 사용한 WaitForSingleObject() 함수는 커널 객체의 상태가 변경되기 전까지 기다리는 DWORD 형의 함수이다. 스레드의 상태가 변한 뒤 주 스레드에서 생성된 스레드의 정보를 획득하도록 처리하는 것이므로, 쓸데없는 루프문을 생성하지 않고 시스템의 효율성을 위해 삽입하였다. 커널 객체는 상태가 변할 때 메시지를 보내야 할 상황이 정의되어 있으며, 이러한 커널 객체 함수들을 사용하면 CPU 시간을 낭비하지 않고도 원하는 작업을 수행할 수 있다. 뒤에서 언급하게 될 세마포어와 뮤텍스는 모두 커널 객체이므로 이들의 상태 변화에 따른 추가적인 작업을 수행할 수 있도록 응용할 수 있다.

제시된 코드를 컴파일하면 약간의 시간 지연 후에 스레드와 관계된 정보가 나타난다. 시간 지연은 CPU 소모를 거의 하지 않으면서 일어나기 때문에 시스템에 별다른 무리를 주지 않는다. 같은 작업을 do-while 문으로 처리한 뒤 작업 관리자를 사용하여 CPU 소모량을 한 번 조사해 보기 바란다.

3.3 C 런타임 라이브러리의 사용

C 런타임 라이브러리는 Win32 API와 병행하여 사용될 수 있으며, 스레드 핸들을 다루는 형태의 깔끔한 윈도우 프로그램이 아닌 다소 투박한 모습으로 나타난다. 사용되는 것은 _beginthread(), _beginthreeadex(), _endthread(), endthreadex() 총 네 개의 함수로, 최초 간략한 사용을 위해 _beginthread() 함수가 정의되었지만 현재는 주로 _beginthreadex() 함수를 사용하고 있다.

C 런타임 라이브러리를 멀티스레드에 사용할 경우 프로젝트 설정을 다소 바꾸어야 할 필요가 있다. VC++을 사용한다면 다음 그림과 같은 설정으로 바꾸어 줄 수 있다.

 

프로그램 자체는 호출 함수형을 Win32형이 아닌 표준 C/C++ 형으로 처리한 것 이외에는 별다른 차이점이 존재하는 것은 아니다. 그러나 런타임 함수만으로 모든 것이 처리되지 않으므로 Win32 API를 전혀 사용할 수 없도록 코드를 작성하는 것은 금한다. 따라서, 전체적인 코드는 다음과 같은 형태로 설정된다.

 



C 런타임 라이브러리와 API 함수의 가장 큰 차이는 하위 호환성이다. 가령 C로 작성된 기존 애플리케이션을 가급적 최소한의 수정으로 멀티스레드 프로그램으로 제작할 경우 C 런타임 라이브러리를 사용하는 것을 권장한다. 또한 부동 소수점 연산이 매개된다면 가급적 런타임 라이브러리를 권장한다. 그 이외의 경우라면 API 함수를 사용하는 것이 훨씬 프로그램 작성상의 통일성을 기할 수 있을 것이다.

멀티프로세스 프로그램은 동시에 여러 작업을 수행할 수 있도록 하는 장점이 있지만, 이에 따른 단점도 같이 존재한다. 가장 먼저 대두되는 내용은, 스레드가 시분할로 나뉘어 있어서 각각의 시간대 별로 전환되는 컨텍스트 관리를 위해 시스템은 여분의 작업을 해 주어야 하고 이는 전체 성능의 측면에 좋은 영향을 끼칠 리 만무하다.

그러나 성능 문제보다 더 심각한 문제는 여러 스레드가 공통의 전역변수나 공용 리소스를 사용하고 있다는 점이다. 따라서, 컨텍스트 전환이 일어날 경우, 변수가 다른 형태로 예측할 수 없이 변경되는 일들이 벌어진다.

가령 주 스레드에 A 라는 변수가 정의되어 있고, 이 스레드에서 파생된 ThreadA, ThreadB 두 개의 파생 스레드가 있다고 가정하자. 주 스레드를 제외한 파생 스레드가 동시에 동작하고 있고 컨텍스트 전환을 계속 반복하고 있으며, A라는 전역 변수를 사용한다고 할 때, 컨텍스트 전환이 한 번 일어날 때마다 전역 변수를 건드리는 스레드가 달라진다. 따라서 변수의 값이 제멋대로 변하는 결과가 벌어지거나, 변수를 사용하지 못하는 결과가 벌어질 수도 있다.

4.1 경쟁상태(Race Condition)

하나의 전역 변수를 다수의 스레드가 동시에 사용할 경우, 경쟁 상태라는 문제가 벌어진다. 이는 변수를 여러 스레드가 같이 사용하다 보니 해당 변수가 계속 엉뚱한 값으로 변경된다는 것이다. 또한 한 번에 입출력이 이루어져야 하는 여러 장비와 관계된 것도 마찬가지다. 가령 텍스트 콘솔로 출력되는 프로그램을 멀티스레드로 작성한 경우 콘솔 출력 화면은 각각의 스레드가 출력하는 메시지로 뒤덮이는 사태가 벌어진다.

따라서, 스레드는 컨텍스트 전환이 일어날 때마다 자신이 변수를 갖고 있는 유일한 스레드라고 착각하게 되며, 공용 변수나 공용 장치 등을 장악하는 스레드는 멋대로 공용 자원을 처리해 버리게 된다. 따라서, 순차적 프로그램과 같은 방법으로 공용 변수를 주었다고 한다면, 실제 실행은 순차적 프로그램처럼 호출된 스레드 순서대로 일어나는 것이 아니라, 동시에 전체 발생된 스레드 및 주 스레드가 동시에 달리기(Race)를 시작하게 된다. 결국 자원을 먼저 점유한 측에서 먼저 자기 멋대로 자원을 사용할 권한이 주어지고, 프로그램이나 출력 결과는 전혀 예상한 대로 나타나지 않고, 프로그램을 실행할 때마다 결과가 달라지는 일이 벌어지게 된다. 다음 제시되는 프로그램의 실행 결과를 예측해 본 다음 한 번 실행해 보라. 실행할 때마다 다른 결과가 나타날 것이다. 이 프로그램은 k가 증가함에 따라 I가 동반증가하므로 결코 중단됨이 없어야 한다. 하지만, 프로그램이 돌아가면서 경쟁 상태를 맞게 되므로 프로그램이 어떤 순간에 죽어 버리고, 그 시점은 아무도 예측할 수 없다.

#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=1; int main(){ HANDLE hThread = NULL; DWORD threadId = NULL; BOOL bOK; int j; for(j=0;j<5;j++){ hThread = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } bOK=CloseHandle(hThread); } Sleep(3000); return 0; } DWORD WINAPI myfunction(LPVOID p){ int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; i=i+j; } return 0; } 

경쟁 상태를 막기 위해서는 하나의 스레드가 자원을 점유하고 있을 때, 해당 자원을 그 스레드가 점유하고 있다는 사실을 다른 스레드에게 알려 주는 것이 중요하다. 따라서 일단 점유된 자원에 대한 사용 금지 조처가 필요하며, 이러한 방법으로 사용하는 것이 크리티컬 섹션(Critical Section)이다. 그러나, 크리티컬 섹션만으로는 다음 제시되는 교착 상태와 관계된 문제를 해결할 수 없다.

4.2 철학자의 만찬

다음 그림은 아주 유명한 “철학자의 만찬(The Dining-Philosophers Problem)"에 관한 것이다. 운영체제론을 다루는 데 있어서 아주 고전적인 문제이다.

 

철학자들은 하는 일이라고는 생각하고 밥을 먹는 일밖에 없으며, 다섯 명의 철학자들이 식탁에 둘러앉아 있다. 이들은 항상 밥을 먹는 시간과 양이 동일하지만, 언제 밥을 먹고 싶은지는 아무도 모른다. 철학자들은 자기가 내킬 때마다 식탁 위의 포크를 들어 밥을 먹게 된다. 철학자들은 좌우의 포크 중 임의로 하나를 사용할 수 있다. 일단 밥을 먹기 시작했으면 끝까지 밥을 먹어야 한다.

문제는 동시에 몇 사람의 철학자가 밥을 먹고 싶을 때 종종 발생한다. 포크를 한 개만 사용할 경우, 그림에서 플라톤과 니체, 헤겔이 동시에 밥을 먹고 싶은 상황을 한 번 가정해 보자. 이 경우, 플라톤은 자신의 오른쪽 포크를 사용하고 헤겔은 자신의 왼쪽 포크를 집었다고 할 때 니체는 갑자기 밥을 먹을 포크가 사라져 버린 결과를 낳게 된다. 그 결과 니체는 무척 배가 고플 것이다.

포크를 두 개를 사용할 때를 한 번 가정해 보자. 모든 사람은 자신의 좌우 포크를 다 사용할 수 있다. 플라톤과 니체가 동시에 배가 고플 경우, 플라톤은 모든 포크를 다 들고, 니체는 오른쪽 포크만을 들고 있는 상황을 가정해 볼 수 있다. 니체는 배가 고프기는 하겠지만 플라톤이 밥을 다 먹고 난 뒤 왼쪽 포크를 확보할 수 있을 것이다.

자 그런데, 모든 철학자가 동시에 배가 고파 자신의 왼쪽 포크를 들었다는 가정을 해 보자. 이 때, 철학자들은 모두 오른쪽 포크 사용이 끝나기를 기다리게 된다. 그러나, 오른쪽 포크는 모든 왼쪽 포크가 사용중인 까닭에 하나도 사용 가능 상태로 돌아오지 않고, 결국 철학자들은 배고픔을 견디다 못해 굶어 죽어 버린다. 이러한 상황을 교착 상태(Deadlock)라고 일컫는다.


5.1 스레드 동기 방법

스레드 상호간 리소스를 적절하게 분배하기 위해서 사용하는 이 작업은 크게 몇 가지가 나뉜다. 가장 많이 사용하는 것은 크리티컬 섹션과 세마포어, 모니터, 이벤트 등이다. 다음은 Win32에서 사용하고 있는 동기 요소이다. 동기 요소들은 커널 객체 (Kernel Object)인 경우와 그렇지 않은 경우가 있다. 커널 객체인 경우 핸들을 사용하고 참조 숫자를 지정할 수 있는 특성이 있다.

  1. 크리티컬 섹션(Critical Section): 스레드로부터 리소스의 일부를 보호하는 기법으로, 하나의 스레드가 어떤 리소tm를 점유하겠다고 예약하는 기법이다. 예약된 리소스는 예약한 스레드가 놓아 주지 않는 한 다른 스레드가 점유하지 못한다.
  2. 뮤텍스(Mutexes): 커널 객체로 운영되기 때문에 핸들을 사용할 수 있다. 크리티컬 섹션이 가지는 교착 상태 문제를 해결할 수 있는 방법이다. 다른 멀티스레딩을 지원하는 언어(Java, C# 등)에서 언급하는 모니터는 윈도우에서 뮤텍스의 일종이다.
  3. 세마포어(Semaphore): 역시 커널 객체이며, 뮤텍스가 단일 스레드를 기준으로 한다면, 세마포어는 스레드의 수를 컨트롤할 수 있는 방법이다.
  4. 이벤트(Event): 범용적으로 쓰일 수 있는 방법. 신호에 의존하게 된다.
  5. 대기 타이머(Waitable Timer): 스레드가 특정 시간에 도달하였는지 여부 파악. 엄밀하게 말해 동기 기능은 거의 존재하지 않는다고 보아도 좋다.

크리티컬 섹션은 커널 객체가 아니므로 수행성능이 좋을 수밖에 없다. 커널 객체를 사용하는 뮤텍스나 세마포어는 객체 관리자(Object Manager)와 연관지어 움직이기 때문에 커널이 참조 숫자를 일일이 관리하게 된다. 따라서, 전반적으로 오버헤드가 걸리게 된다. 같은 프로그램을 크리티컬 섹션으로 처리할 것인지 뮤텍스나 세마포어로 처리할 것인지의 판단은 전적으로 교착 상태가 발생할 가능성이 있는지에 달려 있다. 만일 수백만 분의 1의 확률로 교착 상태가 발생할 수 있다면 크리티컬 섹션은 사용할 수 없다.


5.2 크리티컬 섹션

크리티컬 섹션은 열차의 화장실과 비슷하다. 열차 화장실은 누가 들어가면 화장실 사용중이라는 등이 켜지고, 그 등이 꺼지기 전까지는 아무도 화장실에 들어가지 않는다. 이와 같이 화장실용 전등을 지칭하는 구조체 하나만을 더 만들어 주면 된다. 크리티컬 섹션이 삽입되었다고 하더라도 프로그램의 구조가 아주 많이 변하는 것은 아니다. 열차 화장실에 등을 달기 위해서는 문고리에 스위치를, 외부에 등을 달면 된다. 다만, 화장실에 누가 들어간 것을 감지해야 하므로, 들어간 즉시 문에 있는 스위치를 건드리도록 조정해야 한다.

크리티컬 섹션은 하나의 구조체 CRITICAL_SECTION을 사용하며, 다행히도 이 내부 사정을 별로 알아야 될 필요성은 없다. 이 구조체의 주소는 다른 스레드로부터 보호되는 것들이 삽입되어 있다는 사실을 알려 준다. 커널 오브젝트가 아니라서 핸들에 의해 구현되는 것이 아니라 직접 메모리 주소로 구분하게 된다. 앞서 경쟁 상태에 빠졌던 소스를 한 번 사용해 보도록 하자.

#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); CRITICAL_SECTION critic; int i=1; int main(){ HANDLE hThread = NULL; DWORD threadId = NULL; BOOL bOK; int j; 
 InitializeCriticalSection(&critic); for(j=0;j<5;j++){ hThread = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForSingleObject(hThread, INFINITE); // 스레드보다 섹션이 먼저 삭제될 수 없음. 여러 개의 스레드가 생성되므로 // 정상적으로 하자면 WaitForMultipleObject를 사용해야 한다. DeleteCriticalSection(&critic); bOK=CloseHandle(hThread); 
return 0; } DWORD WINAPI myfunction(LPVOID p){
 EnterCriticalSection(&critic); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; i=i+j; } LeaveCriticalSection(&critic); 
return 0; }

경쟁 상태가 일어난 소스 일부분을 크리티컬 섹션으로 가렸다. 따라서, 이 프로그램은 원래 예측한 대로 무한 루프를 타야 정상일 것이며, 실제로도 중지되지 않고 계속 실행되어 버린다. WaitForSingleObject()와 같은 커널 객체를 다루는 함수는 이와 같은 순차적 프로그램, 혹은 스레드를 호출한 이후 스레드를 호출했던 모 프로세스가 중단되는 경우 크리티컬 섹션의 시작 메모리 영역이 강제로 삭제되는 사태가 벌어진다. 시스템 입장에서는 이를 예외로 받아들일 수밖에 없으며, 따라서 메모리 침범 오류를 받을 수밖에 없다.

크리티컬 섹션은 하나의 구조체를 정의하는 것으로부터 시작한다. 그런데 프로그래머들이 이 구조체를 직접 다루는 일은 거의 발생하지 않으며 이 구조체는 커널과 연관지어 작업하는 경우에만 의미를 지니게 된다. 구조체는 보호되는 영역의 시작 주소와만 관계하며 다른 작업과는 연관이 없다.

크리티컬 섹션이 시작하는 부분은 여러 스레드가 경쟁하는 초입부에서 정의해 주고, 초기화시킨다. InitializeCriticalSection() 함수로부터 크리티컬 섹션이 출발하며, 각각의 스레드에 크리티컬 섹션으로 진입하는 EnterCriticalSection() 함수를 사용한다. 이렇게 설정되면 가장 먼저 크리티컬 섹션으로 진입하는 스레드가 우선권을 갖고 다른 작업을 수행하게 된다.

위에서 제시된 소스는 총 5개의 스레드를 형성한다. 그렇다면, 제일 먼저 진입된 스레드를 제외한 나머지 스레드는 어떤 상태로 추정되는가? 나머지 4개의 스레드는 현재 활성화된 상태이기는 하지만, 자신의 큐에서 아무런 작업도 하지 못하고, 크리티컬 섹션으로 먼저 들어간 스레드가 나오기만을 기다리고 있다. 따라서, 이 프로세스의 스레드를 측정해 보면 총 5개의 스레드가 검출될 것이다. 성능 모니터로 확인해 볼 수 있을 것이다.

 

크리티컬 섹션이 경쟁 상태를 해소했다고 볼 수 있지만, 이렇게 동작한다면 잘못하면 새로운 문제를 야기할 수 있다. 앞선 열차 화장실 문제에서 이미 드러난 것처럼, 누군가가 화장실을 점유하고 있다면 열차 내에 있는 사람은 아무도 화장실에 가지 못하는 일이 벌어진다. 즉, 보편적으로 멀티스레드 프로그램은 효율성을 나타내기 위해 운영되지만, 크리티컬 섹션을 잘못 사용하면 프로그램이 정지하여 아무 일도 하지 못하는 사태가 벌어질 수 있다. 이것은 일종의 교착 상태이다. 이는 다음 두 가지 경우로 나눌 수 있다.

  • 프로그램 수행이 너무 길어지는 경우, 혹은 프로그램이 제 3의 문제로 종료하지 않는 경우
  • 스레드가 중간에 예외를 발생시켜 미처 크리티컬 섹션을 빠져나오지 못한 상황에서 스레드가 종료되는 바람에, 시스템은 계속 해당 자원이 점유되고 있다고 착각하는 경우.

첫 번째의 경우는 크리티컬 섹션으로 사실 해결할 마땅한 방법이 없다. 따라서, 뮤텍스나 세마포어를 도입한다. 뮤텍스의 경우에는 해당 자원을 사용하기 위해서 보호되는 영역으로 진입하려고 얼마나 기다려야 적절한지 시간 계산을 할 수 있다. 따라서, 열차 화장실과 같은 케이스로 해결할 수 있다. 일정 시간을 기다려서 열차 화장실에 아무런 반응이 없으면 누군가 화장실에서 쓰러졌을 가능성이 높으므로, 119에 연락하면 된다. 뮤텍스는 크리티컬 섹션이 갖지 못하는 이러한 기능을 갖고 있다.

세마포어를 도입하는 경우는 버스 승강장과 같은 경우이다. 출근 시간에 버스 승강장은 매우 혼잡하지만, 승강장이 하나밖에 없다고 가정할 때, 가장 좋은 해결책은 승강장을 몇 개 더 만드는 것이다. 승강장이 많아지게 되면 이런 문제는 효율적으로 끝난다.

물론, 세마포어가 승강장을 무턱대고 많이 만든다고 해서 효율적인 것은 아니다. 세마포어는 스레드 동기를 위한 커널 객체 중 가장 느린 상황이며, 예외적인 상황이기는 하지만, 전체 세마포어가 모두 시간지연이 벌어지는 경우가 많다. 식사중에 이 책을 읽고 있다면 실례겠지만, 공중화장실의 모든 변기가 가득 차 있는데, 줄도 무척 긴 상황을 맞이할 수 있다. 멀티스레드 프로그램은 이러한 다양한 변수가 있기 때문에 실제로 프로그래머들이 생각하는 것처럼 쉽게 많은 문제들이 해결되지 않는다. 이러한 문제가 단지 공상이라고 생각되는 독자 중 혹시 웹서버를 운영하는 독자가 있다면 지금 즉시 데이터베이스 서버를 사용 불능 상태로 놓아 두고, 스레드에 할당되는 대기열을 조사해 보면 된다. 일반적으로 데이터베이스 연결 풀링은 세마포어를 사용하는 경우가 많고, 데이터베이스를 중단하면 해당 데이터베이스를 사용하는 스레드가 모조리 타임아웃 상태가 날 때까지 자신의 자원을 반환하지 않는다. 크리티컬 섹션으로 만약 제작하였다면? 타임아웃조차도 나오지 않기 때문에 뒷일을 책임질 수 없다.

크리티컬 섹션의 취약점은 스레드가 중간에 죽어버린 경우에 종종 발생한다. 시스템 예외 상황을 맞아 스레드만 종료된 경우, 시스템은 해당 스레드가 계속 리소스를 점유중이라고 생각하고 다른 스레드의 진입을 막는다. 언제까지 막을 것인가가 관건인데, 크리티컬 섹션의 경우는 프로세스를 중단하기 전까지 영원히 막아버린다. 이것이 두 번째 케이스이다. 따라서, 크리티컬 섹션으로 진입하는 경우 반드시 예외 처리를 해 주어야 한다. 다음과 같은 케이스면 좋다.

DWORD WINAPI myfunction(LPVOID p){ __try{ EnterCriticalSection(&critic); int j,k; j=(int)p; for(k=0;k 

따라서, 이렇게 예외 처리를 하는 것을 습관처럼 삼아야 한다. 크리티컬 섹션 류의 동기 방법론은 잘못 설계되면 시스템의 성능을 형편없이 떨어뜨리는 주범이 되므로 주의하여야 한다.


5.3 뮤텍스

뮤텍스와 크리티컬 섹션의 가장 큰 차이점은 뮤텍스는 커널 객체라는 점이다. 따라서, 크리티컬 섹션이 하나의 스레드의 일정 영역에 한정되었다면, 뮤텍스는 그 자체로 하나의 객체로 다루어지기 때문에, 다른 스레드가 핸들을 획득하여 접근이 가능하다. 따라서, 크리티컬 섹션과는 달리 자원 자체를 다루는 것이 하나의 별도 객체로 생성되는 셈이다. 결국 프로세스나 스레드를 형성하는 것과 비슷한 루틴이 작용하는 것을 생각해 볼 수 있다.

뮤텍스는 CreateMutex() 함수를 사용하여 생성된다. 그런데, CreateMutex() 함수는 매번 생성될 때 뮤텍스의 이름을 사용자로부터 요구하며, 이 이름은 정형화되어 있지 않다. 다시 말하면, 사용자가 임의로 뮤텍스의 이름을 지정하도록 처리하는 셈이다. 이런 측면들은 GUID나 기타 정형적이면서도 고유한 이름 체계가 사용되기 이전에 제작된 운영체제의 단점이 계승된 것이다. 따라서, 어떤 뮤텍스를 생성할 때, 혹은 생성된 뮤텍스를 사용할 때 같은 문제점들이 계속 발생하게 된다.

뮤텍스는 이 이름을 사용하여 핸들을 얻을 수 있다. 따라서, 다른 프로세스로부터 뮤텍스 핸들을 얻기 위해서는 어떠한 이름을 공유해야 한다. 만일, 뮤텍스가 공유된다면 많은 작업자 사이에서 뮤텍스 이름이 겹칠 가능성이 충분히 존재한다. 결국, 멀티프로세스 프로그램 설계자들은 프로그램을 설계하기 전 뮤텍스 이름에 대해서 “나름대로 표준적인” 체계를 지정해 줄 필요가 있다.

뮤텍스는 다음 루틴으로 적용된다.

  1. . 뮤텍스 생성: CreateMutex()
  2. . 생성된 뮤텍스로부터 핸들 획득: OpenMutex()
  3. . 핸들 제거: CloseHandle()
  4. . 뮤텍스 제거: ReleaseMutex()

뮤텍스는 다른 커널 객체와 마찬가지로 상태가 변할 때 자신의 상태를 객체 관리자에게 송신한다. 따라서, WaitForSingleObject()문을 사용하거나 WaitForMultipleObject() 함수를 사용하여 해당 뮤텍스가 상태 변경 신호를 보냈을 때를 기준으로 다른 작업을 실행하면 된다. 계속 언급하고 있지만, 상태 변화 신호를 보낸다는 것은 그만큼 시스템이 느려짐을 의미하게 된다.

WaitForSingleObject() 등의 함수를 사용하기 때문에 뮤텍스는 일정 시간 이상 반응이 나타나지 않으면 프로그램은 뮤텍스를 기다리지 않고 독자적으로 다른 작업을 수행할 수 있다. 즉, 앞서 말한 화장실 대기열의 경우, 화장실 내에 있는 사람이 이상을 감지하는 것이 아니라, 이상 상태가 존재하면 그것은 외부의 시간으로부터 감지되며, 객체가 별도의 신호를 관리자에게 보내지 않는다면 프로그램이 나름대로 시간을 잰 뒤, 문제가 있으면 다음 동작을 취할 수 있는 것이다. 따라서, 대기열의 문제는 뮤텍스의 특성이 아니라, 커널 객체이기 때문에 동작할 수 있는 방법론일 따름이다.

뮤텍스는 다른 프로세스에 의해 점유되거나 점유되지 않는 두 가지 상태를 갖고 있다. 뮤텍스는 한 번에 하나의 스레드만이 사용할 수 있기 때문에 앞에서 언급한 참조 숫자가 나타나는 것이 아니라 점유/비점유 두 가지 상태를 나타낼 뿐이며, 뮤텍스는 점유 해제가 일어날 경우 객체 관리자에게 자신이 비점유 상태라는 것을 알린다. 뮤텍스를 사용하고자 하는 스레드는 커널 관리자에서 뮤텍스가 비점유 상태라는 것을 알아낸 이후부터 사용이 가능하다.

따라서, 크리티컬 섹션에서 사용한 코드를 여기에 적용하면 다음과 같다. 앞에서 언급한 WaitForMultipleObjects() 함수를 사용한 예를 들어 보았다.

#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); HANDLE hMutex; int i=5; int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; hMutex=CreateMutex(NULL, // ACL FALSE, // 현재 이 스레드가 점유 여부 "MyMutex"); // 뮤텍스 이름 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hMutex); for(j=0;j<5;j++){ bOK=CloseHandle(hThread[j]); } return 0; } DWORD WINAPI myfunction(LPVOID p){ __try{ WaitForSingleObject(hMutex, INFINITE); int j,k; j=(int)p; for(k=0;k 

스레드에서는 이제 뮤텍스의 상태 변화에 따라 해당 스레드가 뮤텍스를 사용하고 있는지 여부를 파악하게 된다. CreateMutex() 함수에서는 최초 ACL과 해당 뮤텍스를 지금 현재 스레드가 점유할 것인지 여부를 파악하게 된다. 그 다음 적절한 뮤텍스 이름을 생성해 주면 된다. 이미 생성된 뮤텍스를 사용하기 위해서는 OpenMutex() 함수를 사용할 수 있고, 이미 생성된 뮤텍스라고 할지라도 CreateMutex() 함수로 해당 뮤텍스 핸들을 받아올 수 있다. 전자의 경우는 이미 생성되지 않은 뮤텍스일 경우 오류값을 반환할 것이지만, 후자의 경우는 오류값을 반환하지 않고 새로운 뮤텍스를 생성한다. 두 함수의 차이점을 명백히 인식하고 필요한 함수를 사용할 것을 권한다.

CreateMutex() 함수에서 두 번째 인수를 TRUE로 변경하면, 현재 이 스레드가 일차적으로 점유하는 뮤텍스가 형성된다. 이는 뮤텍스 자체가 컨텍스트 전환이 일어나면서 엉뚱한 프로세스에 의해 점유되는 결과가 발생할 수 있다.

뮤텍스는 커널 객체이기 때문에 다음과 같은 방법으로 현재 이 뮤텍스가 다른 스레드에 의해 점유중인지 파악할 수 있다. 만일 특정 시간 내에 점유가 풀리지 않는다면 다른 방법을 강구해 보거나, 코드 수정을 할 필요가 있을 것이다.

DWORD state; state=WaitForSingleObject(hMutex, n); if (state != WAIT_TIMEOUT) { // Job } else cout << "timeout" << endl; 

뮤텍스는 WaitForMultipleObjects()를 사용할 수 있도록 설정되었으며, 이는 필연적으로 다중의 뮤텍스의 상태 변화가 있을 때까지 스레드의 자원 사용을 허가하지 않는 형태의 코드를 작성할 수 있다. 이는 교착 상태를 해소하는 데 조금 쉬운 방법을 제공하며, 각각의 리소스를 건드리는 뮤텍스를 순차적으로 나열한 뒤, 가장 늦게 종료되는 뮤텍스가 상태를 반환하였을 경우 프로그램을 재개하는 방법을 생각해 볼 수 있다.


5.4 OOP로 동기화

지금까지는 클래스를 사용하지 않은 C++로만 프로그램을 계속 작성하였다. 그러나, 이렇게 작성한 것은 뒤에서 언급하게 될 모니터를 예로 들기에는 그다지 좋은 솔루션이라 보기 어렵다. 70년대 초반 등장한 개념인 모니터는 일종의 구조체이지만 거의 클래스에 가깝게 작성되므로, 아예 뮤텍스를 클래스로 운영하는 방법에 대해서 생각해 보기로 하겠다. 가독성을 높이기 위해서 뮤텍스를 전부 하나의 파일로 가정하고 작성하였다. 독자들이 실제로 사용할 때는 함수 선언부와 함수 실체, 그리고 메인 코드를 모두 분리하여 사용할 것을 권한다. 지금부터는 사용하던 코드를 약간 변형하여 i+j 구문을 넣지 않는다. 이는 가독성을 높이기 위함이며, 만일 경쟁 상태가 발생한다면 모니터에 출력되는 내용이 엉켜 나타나게 될 것이다.

#include <windows.h> #include <iostream> using namespace std; // 클래스 정의 class Mutex { protected: HANDLE hMutex; public: Mutex(); ~Mutex(); BOOL getMutex(LPSECURITY_ATTRIBUTES lpsec, BOOL bOwn=FALSE, LPCTSTR pName=NULL); BOOL startMutex(); BOOL endMutex(); }; // 멤버 함수 정의 Mutex::Mutex(){ hMutex=NULL; } Mutex::~Mutex(){ if(hMutex!=NULL) { ::CloseHandle(hMutex); hMutex=NULL; } } BOOL Mutex::getMutex(LPSECURITY_ATTRIBUTES lpsec, BOOL bOwn, LPCTSTR pName){ hMutex=::CreateMutex(lpsec, bOwn, pName); if (hMutex == NULL){ throw "뮤텍스가 형성되지 않았습니다."; return FALSE; } else return TRUE; } BOOL Mutex::startMutex(){ DWORD dwResult; dwResult = WaitForSingleObject(hMutex, INFINITE); if (dwResult != WAIT_TIMEOUT) return TRUE; else return FALSE; } BOOL Mutex::endMutex(){ if (::ReleaseMutex(hMutex)) return TRUE; else return FALSE; } // 소스 메인 부분 DWORD WINAPI myfunction(LPVOID); Mutex myMutex; int i=100; int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; myMutex.getMutex(NULL, FALSE, "myMutex"); for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); for(j=0;j<5;j++){ bOK=CloseHandle(hThread[j]); } return 0; } DWORD WINAPI myfunction(LPVOID p){ myMutex.startMutex(); int j,k; j=(int)p; for(k=0;k<i;k++) cout << k << endl; myMutex.endMutex(); return 0; } 

클래스로 정의하면 명백하게 생성과 소멸 과정을 거치므로, C에서의 __try를 사용한 예외 처리는 클래스 내에 삽입된 것으로 생각하면 된다. 클래스로 코드를 나타내면 메인 코드의 가독성이 높아지며, 쓰레기 값 처리를 위해서 들어가는 노력이 줄어든다. 핸들을 직접 다루지 않기 때문에 오류 가능성이 줄어들기도 하지만, 속도 측면에서 큰 이득을 볼 수는 없을 것이다. 뮤텍스 핸들은 외부에서 접근하지 않도록 할 것이므로 보호 영역에 삽입한다. 뮤텍스를 생성하는 생성자는 NULL로 초기화된 하나의 뮤텍스를 생성하며, 소멸자는 현재 핸들이 NULL상태가 아니면 핸들을 없애 버리는 과정을 거친다. 따라서, 전체 소스에서는 뮤텍스의 핸들링과 관계된 코드는 들어가지 않는다.


5.5 교착 상태와 모니터

1970년대에 정의된 모니터는 주로 구조체 형태로 정의되었다. 모니터는 데이터 구조체와 함수가 한번에 실행되는 것으로, OOP 개념이 일반화되지 않은 시점에서는 매우 작성하기 어려웠다. 앞서 철학자의 만찬 문제로 다시 돌아가 보기로 하자.

철학자의 만찬에서는 무작위로 철학자들이 밥을 먹는다. 하지만 모니터를 사용할 경우, 철학자들은 갑자기 훈련 잘 받은 “개”가 되어 버린다. 개를 조련시킬 때 가장 먼저 하는 명령 중의 하나가 “기다려” 그리고 “먹어”다. 개는 주인이 주는 음식 외에는 먹어서도 안 되며, 그것도 아무 때나 밥상에 달려들지 않도록 조련해야 한다. 철학자의 문제에서도 마찬가지로, 현재가 먹어야 할 시점인지 기다려야 할 시점인지 판단한다면, 교착 상태가 일어나지 않도록 설정된다.

동시에 모든 철학자들은 아무런 판단 없이 식사를 하는 것이 아니라, 누군가가 먹어야 할 시점과 먹지 말아야 할 시점을 구분한다. 그 다음, 누가 먹기 시작했을 때는 식기가 또 한 사람이 먹기 충분한지 판단하는 시간을 갖는다. 이것은 항상 순차적으로 일어나야 하고, 실제로 컴퓨터는 시분할 멀티태스킹으로 항상 시간 우선 순위를 갖는다. 누군가 빠른 시간에 작업을 수행했고, 리소스가 충분하지 않다면 늦은 시간은 항상 우선 “기다려” 상태로 가게 된다. 강아지 훈련과 철학자의 만찬은 결국 같은 문제로 귀결된다.

 

모니터란, 멀티스레드를 사용하는 클래스(혹은 구조체)에 이와 같은 기능을 미리 설계해 넣은 것을 의미한다. 아주 간단히 제작하려면 클래스로 작성된 뮤텍스를 해당 클래스에 삽입하면 된다. 모니터는 이렇게 코드 자체에 감시 기능이 존재하는 상태이며, 앞에서 다룬 코드들을 모두 모니터 클래스 객체로 변형하면 다음과 같은 코드를 얻을 수 있다.

class Printit { private: int i; Mutex myMutex; public: Printit(); void writeit(int j); }; Printit::Printit(){ i=100; } void Printit::writeit(int j){ myMutex.startMutex(); int k; for(k=0;k 

먼저 선언된 Mutex 클래스를 사용하여, 특정 작업을 수행할 하나의 클래스를 형성하며, 해당 작업을 클래스의 멤버 함수로 삽입하였다. 그 다음 작업의 시작과 끝을 모두 뮤텍스로 둘러쌌다. 이렇게 정의하면 멤버 함수가 호출될 때 필요한 리소스를 잡고 수정 작업을 할 수 있다.

이제 메인 프로그램은 더 간단히 처리되며, 정의된 모든 클래스를 헤더 함수에 삽입하든지 하나의 C++ 파일에 전부 집어넣든지 관계없이 다음과 같은 최종 코드를 볼 수 있을 것이다.

DWORD WINAPI myfunction(LPVOID); Printit Ptest; int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); for(j=0;j<5;j++){ bOK=CloseHandle(hThread[j]); } return 0; } DWORD WINAPI myfunction(LPVOID p){ int j=(int)p; Ptest.writeit(j); return 0; } 

모니터를 설계할 때 주의할 점은 일단 실행이 이루어졌으면 모든 작업이 끝날 때까지 다른 자원이 침범하지 못하도록 해 주어야 한다는 것이다. 따라서, 중간에 컨텍스트 전환이 일어나지 않도록 작업이 뮤텍스로 적절히 둘러싸 주어야 한다.

모니터는 가장 전통적인 멀티프로세스 또는 멀티스레드 프로그램에서 일어나는 경쟁 및 교착 상태의 해결책이기는 하지만, 각각의 객체 단위로 설계가 이루어져야 하기 때문에 상대적으로 설계 시간이 다소 소모되는 단점이 있다.


5.6 세마포어

세마포어는 다익스트라(Dijkstra)에 의해 제시된 개념으로, 철학자들이 굶어죽는 문제를 처음 제기한 이가 다익스트라이다. 세마포어는 병렬적인 리소스와 그 리소스를 차지하는 스레드 사이의 다대다 관계로 해석하면 된다. 세마포어는 철도의 신호기에서 온 것으로 하나의 철길을 두 대 이상의 열차가 사용하기 위해 신호를 따른다는 의미이다.

뮤텍스가 한 번에 단 하나의 보호되는 자원을 설정할 수 있음에 반해, 세마포어는 해당 자원을 몇 개의 스레드가 동시에 사용할 수 있을지 정의한다. 일반적으로 뮤텍스를 하나의 스레드만이 접근할 수 있는 까닭에 바이너리 세마포어라고 일컫기도 하지만, 윈도우 시스템에서는 최대 하나의 스레드만이 접근할 수 있는 세마포어와 뮤텍스는 완전히 다른 개체로 인정되므로, 처음부터 뮤텍스로 설정한 것과 세마포어로 설정한 것은 차이점을 갖는다는 사실을 잊어서는 안 된다.

뮤텍스나 세마포어는 스레드를 순서대로 동작시키는 것에 주안점을 맞추므로, 실제 CPU의 활용 상황에서 그렇게 최적화되지 못하는 경우가 많다. 하나의 리소스를 잠금 설정을 했을 경우, 해당 리소스에 복수 접근이 가능하더라도 최대 접근 수는 항상 1로 설정되기 때문에 결국 멀티스레드 프로그램에서 나타나는 복수 스레드가 특정 자원을 뮤텍스로 감쌌을 경우 여러 스레드 사이에서 대기 상태가 계속 나타나게 된다. 이는, 컨텍스트 전환 이후 아이들링 상태의 CPU 동작을 빈번히 가져오게 되며 결국 프로그램은 멀티스레드의 이점을 퇴대한 활용할 수 없다.

세마포어는 하나의 자원, 혹은 루틴에 최대 접근 숫자를 정의해 놓고, 접근 숫자가 모두 차면 여분의 접근 숫자를 0으로 설정한다. 이후 여분 접근 숫자가 0이 아니면 그만큼의 스레드의 요청을 받아들일 수 있다. 따라서, 잘못 설계하게 되면 세마포어는 멀티스레드가 갖는 문제를 그대로 나타내게 된다. 단지 동시에 동작할 수 있는 스레드의 수만 한정해 놓는 셈이다. 따라서, 상호 간섭이 나타나는 리소스에는 세마포어를 사용할 수 없다. 가령, 열차 화장실 문제로 다시 복귀해서, 화장실이 모자라서 하나를 증설하는 경우 뮤텍스나 크리티컬 섹션으로는 동작하지 않고 세마포어로 동작하게 된다. 그러나, 화장실로 가는 스레드가 동시에 두 개까지 허용된다는 것은 실제 화장실이 두 개인 것을 의미한다. 따라서, 화장실이 하나임에도 불구하고 스레드를 두 개 만들면 어쩔 수 없이 경쟁 상태를 맞게 된다.

세마포어가 가장 효율적으로 사용되는 것은 데이터베이스의 커넥션 등의 작업이다. 보통 데이터베이스 접근과 같은 작업들을 멀티스레딩으로 처리하는 경우가 많은데, 실제로 데이터베이스 연결은 라이선스, 혹은 성능에 의해 최대 연결 가능 숫자가 지정되어 있다. 따라서, 정의한 숫자 이내의 연결을 위해서 리소스를 사용하는 스레드를 컨트롤해야 한다. 단 하나의 리소스만을 사용할 경우에는 뮤텍스를 쓸 수 있겠지만, 리소스가 두 개 이상이 될 경우에는 효율적인 시스템 운영을 위해서 세마포어를 사용할 수밖에 없다.

세마포어는 뮤텍스와 약간 다른 방법으로 동작한다. 뮤텍스가 전역 변수로 설정된 것에 반해 세마포어는 지역 변수로 설정된다. 세마포어는 동시에 다중 참조가 가능한 커널 객체이므로, 세마포어 이름을 알고 있다면 일단 생성되어 있는 세마포어를 열어 사용할 수 있기 때문이다. 따라서, 주 스레드에서 세마포어를 연 뒤 파생되는 스레드에서 세마포어를 사용할 수 있는 체계가 확립되는 셈이다.

이제 뮤텍스를 사용했던 예제를 세마포어까지 확대시켜 보자.

#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=100; int main(){ HANDLE hThread[5]; HANDLE hSemaphore; DWORD threadId = NULL; BOOL bOK; int j; hSemaphore=CreateSemaphore(NULL, // ACL 1, // Initial Locking Count 1, // Maximum Locking Count "MySemaphore"); // 세마포어명 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); //스레드 죽기 전에 세마포어가 삭제되면 안됨 bOK=CloseHandle(hSemaphore); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ HANDLE hSemaphore=OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, "MySemaphore"); WaitForSingleObject(hSemaphore, INFINITE); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } long lCount; ReleaseSemaphore(hSemaphore, 1, &lCount); CloseHandle(hSemaphore); return 0; } 

전체적인 구성에 있어서는 그다지 차이를 나타내지 않는다. 세마포어는 참조가 가능한 형태로 운영되는 커널 객체이므로 일단 주 스레드에서 세마포어를 선언하고, 해당 CreateSemaphre() 함수를 사용하여 세마포어를 생성하였다. 세마포어는 참조 숫자를 갖고 있기 때문에 현재 자신이 얼마나 참조되고 있는지 ReleaseSemaphore() 함수로 확인할 수 있다. 다만 주의할 점은 ReleaseSemaphore() 함수를 사용하고 난 뒤에는 세마포어가 보호하는 루틴으로부터 빠져 나온 것이기 때문에 컨텍스트 전환의 문제를 맞이할 수 있다는 것이다. 따라서 이 참조 숫자를 사용할 필요가 있을 때 역시 컨텍스트 전환을 방지할 수 있는 방법론을 도입할 필요가 있다.

OpenSemaphore() 함수는 세마포어를 어떻게 접근할 것인지 접근 옵션을 지정하고 있으며, 이는 일반적인 커널 객체의 경우 거의 비슷한 방법으로 접근이 가능하다. 세마포어의 경우에는 SEMAPHORE_ALL_ACCESS, SEMAPHORE_MODIFY_STATE, SYNCHRONIZE 모두 세 가지가 지원이 가능하며, 모든 접근 가능, 카운터 변경 가능, 대기 함수에서 핸들 사용 가능의 옵션이다.


5.7 이벤트

이벤트는 지금까지 언급했던 동기화 객체들과는 다소 차이가 있다. 지금까지 말한 커널 객체나 크리티컬 섹션은 운영체제에 의해 영위되는 전자동식 운영이었다면 이벤트는 자동이 아니라 수동으로 운영된다. 즉 프로그래머의 의도에 의한 컨텍스트 전환이 이루어진다.

이벤트는 일종의 전기 스위치로 볼 수 있다. 이 전기 스위치는 두 가지가 있으며, 일반적으로 보통 볼 수 있는 ON/OFF 스위치와, 작업이 끝나면 자동으로 꺼지는 스위치가 있다. 윈도우에서는 전자를 수동 재설정 이벤트(Manual Reset Event)라고 하며 후자를 자동 이벤트(Automatic Event)라고 한다. 이 두 가지의 차이점은 아주 명백하여, 수동 재설정 이벤트는 명시적으로 켜거나 끄지 않으면 항상 원래 있던 상태를 유지하고 있으며, 자동 이벤트는 항상 꺼져 있는 상태로 유지되다가, 스위치를 누르면 특정 작업을 수행하는 순간만 켜져 있고, 작업이 끝나면 도로 꺼져버리는 특성이 있다.

이벤트 역시 핸들러로 지원되는 커널 객체이다. 따라서, 여지껏 해온 문맥대로라면 CreateEvent() 함수로 설정하고 OpenEvent() 함수로 이벤트를 열 것이다. CreateEvent() 함수에서 이벤트가 어떤 종류(수동 이벤트, 자동 이벤트)로 설정될 것인지 나타낼 수 있으며, 이벤트를 다루는 것은 다음 세 가지 추가 함수를 사용한다.

  • SetEvent()
  • ResetEvent()
  • PulseEvent()

SetEvent() 함수는 이벤트를 켜는 명령어이다. 수동 이벤트라면 계속 켜져 있을 것이며, 자동 이벤트라면 단일 스레드가 작동한 뒤 다시 꺼질 것이다. ResetEvent() 명령은 이벤트를 끄는 것으로, 자동 이벤트의 경우에는 별다른 영향을 주지 못한다. PulseEvent 명령어는 자동 이벤트의 경우 SetEvent()와 동일한 효과를 나타내며, 수동 이벤트는 대기열에 있는 스레드를 동작시키게 된다.

이벤트는 프로세스 사이, 혹은 스레드 사이에서 통신할 수 있으며, 프로세스 사이에서 통신하는 것도 스레드와 동일한 코드를 사용하게 된다. 이제 질리도록 친근해진 소스를 이벤트를 사용하는 방법으로 변형해 보기로 하자.

#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=100; int main(){ HANDLE hThread[5]; HANDLE hEvent; DWORD threadId = NULL; BOOL bOK; int j; hEvent=CreateEvent(NULL, // ACL FALSE, // 자동 이벤트 TRUE, // 초기 상태: ON "MyEvent"); // 이벤트명 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hEvent); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS, FALSE, "MyEvent"); WaitForSingleObject(hEvent, INFINITE); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } SetEvent(hEvent); CloseHandle(hEvent); return 0; } 

WaitForSingleObject()는 Event의 경우 ON 상태에서 감지된다. 따라서, 자동 이벤트를 사용하였으므로 일단 이벤트가 켜진 상태로 프로그램을 설정한다. 그러면 제일 처음 스레드가 실행되면서 이벤트가 자동으로 꺼지게 된다. 이후 발생되는 스레드는 모두 대기 상태로 들어갈 수밖에 없다. 처음 스레드가 종료되어야 SetEvent() 함수가 실행되어 다시 스레드를 원래 상태로 돌려놓을 것이기 때문이다.

이벤트는 이처럼 특정 이벤트 객체의 상태를 파악하여 모든 작업을 제어한다. 따라서, 상대적으로 단순한 구조를 갖고 있다. 또한 ACL을 올바르게 설정하였다면 다른 스레드는 물론 프로세스에서도 해당 이벤트를 같은 이름으로 받을 수 있다. 다만, 프로세스 사이에서 통신이 이루어질 경우에는 이벤트 이름 형식을 정확히 정의해 주어야 한다. 가령 다음과 같이 하면 될 것이다.

   HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS, FALSE, _T("MyEvent"));

5.8 대기 타이머(Waitable Timer) 및 타이머 큐(Timer Queue)

지금부터 언급하는 내용들은 근본적으로 위에서 언급된 루틴과는 다르다. 아예 Sleep()과 같은 함수와 동일한 개념으로 동작한다고 생각하면 된다. 즉, 일정 시간의 유예를 둔 뒤 해당 시간이 지나면 자동으로 해당 작업을 멈추어 버리는 방법이다. 타이머는 뮤텍스나 세마포어처럼 명시적으로 스레드 동기화에 관여하지 않으므로, 실제로는 동기 함수라기 보다는 특별한 경우에만 주로 사용된다. 이는 Sleep() 함수보다 더 시스템을 효율적으로 다룬다는 특징을 지닐 뿐이다. 따라서 Sleep()이 필요한 시점에서 대기 타이머나 타이머 큐를 사용하는 것을 권한다.

타이머 큐는 단순히 일정 시간을 기다리는 콜백 함수이며, 대기 타이머는 단순히 기다리는 것이 아니라, 이벤트와 같이 ON/OFF 등을 구분하는 상태를 나타낼 수도 있고, 특정 시간이 지나면 주기적으로 활성화되는 형태를 나타내기도 한다. 대기 타이머 역시 이벤트와 비슷하게 수동 타이머(Manual Reset Timer)와 동기 타이머(Synchronization Timer)가 존재한다. 동기 타이머가 이벤트의 자동 타이머와 비슷한 구실을 하며, 그 외에 주기 타이머(Periodic Timer)가 있다. 타이머 역시 생성과 소멸은 비슷한 문법을 가지므로, CreateWaitableTimer() 함수로 생성된다. 문법은 거의 이벤트와 흡사하며, 동기화에서 그렇게 많이 사용되지 않으므로 더 자세한 내용을 원하는 이들은 MSDN에서 사용법을 참고할 것을 권한다.


5.9 TLS

스레드에서 전역 변수를 사용할 경우 모든 스레드가 해당 변수를 건드리기 때문에, 다른 스레드가 전혀 동작하지 않는다고 하더라도, 컨텍스트 전환의 결과로 특정 데이터를 엉망으로 만들어 버릴 가능성이 높다. 이 경우 스레드 로컬 저장소를 사용하면 문제가 깔끔히 해결된다. 스레드 로컬 저장 영역은 32비트 블록으로 지정되며, 각각의 스레드별로 사용 공간을 할당할 수 있다. 포인터로 값을 전달하기 좋아하는 C 계열의 프로그래머들이 일반적인 멀티스레드 프로그램을 작성할 때, 스레드와 해당 스레드가 호출하는 함수 사이에서 인수 전달이 포인터로 이루어진다면 여러 문제가 발생할 수 있다.

제일 큰 문제는 포인터가 전역 변수로 지정되어야 한다는 것이다. 이 말은 컨텍스트 스위칭이 일어나지 않더라도, 각각의 멀티스레드 프로그램이 포인터로 지정된 변수를 일그러뜨릴 가능성이 매우 높다는 것이다. 가령, 항상 초기값이 0으로 세팅되어 있어야 특정 스레드와 그 연관 함수가 만족하지만, 스레드 수행 결과 그 포인터 변수는 초기값이 스레드의 결과값으로 치환된 이후 컨텍스트 전환이 일어난 경우 다른 스레드는 엉뚱한 초기값을 받게 된다. 물론, 이러한 부분은 뮤텍스나 세마포어 등을 사용하여 초기값을 항상 원래대로 복구하는 것으로 해결할 수 있다.

그렇지만, 뮤텍스나 세마포어는 비교적 수행 속도가 느리고, 경쟁 상태나 교착 상태의 가능성이 없이 단순히 변수의 변화를 막기 위해 뮤텍스나 세마포어를 사용하는 것은 비효율적이다. 또한 스레드 동기화는 근본적으로 컨텍스트 전환은 계속 수행하고 있는 상황이기 때문에 CPU의 시간을 계속 소모하면서 대기하는 방법을 사용하게 된다. 따라서, 스레드와 그 스레드가 호출하는 함수 사이에서만 사용할 수 있는 포인터 변수를 도입하면 메모리 측면에서는 모든 참조형 변수의 사본을 만드는 손해를 볼 수 있지만, 각각의 스레드를 병렬적으로 수행하면서 컨텍스트 전환에 의해 발생하는 문제들을 근원적으로 해결할 수 있다. 경쟁 상태가 나타나지 않는, 단순히 멀티스레드의 이점을 최대한 활용하기 위해 이러한 방법론을 사용한다. 물론 저장 영역을 손해보고, 메모리 연산을 빈번히 사용하기 때문에 복잡해지는 경우 오류 가능성이 더욱 커질 수 있다.

TLS는 각 스레드를 위한 전용 주소 공간을 제공하며, 각 주소 공간의 인덱스로 해당 공간을 접근한다. 이는 마치 지하철 물품보관소와 같이 각자가 물품보관소 열쇠를 갖고 있고, 해당 열쇠를 가진 사람만이 물건을 넣고 꺼낼 수 있는 형태로 운영된다. 각 스레드는 호출되는 함수를 통해 자신이 삽입한 변수를 공유할 수 있다. TLS는 정적, 혹은 동적으로 할당될 수 있으며, 마치 일반적인 C++ 프로그램의 메모리 할당과 비슷한 방법으로 동작하게 된다. 다음 그림은 TLS와 스레드의 관계를 나타낸 것이다.

 

각각의 스레드는 TLS 영역에 보관함을 가질 수 있으며, 이 스레드는 자신이 호출한 함수, 혹은 자신 내에서만 이 TLS 영역에 접근할 수 있다. 빈번히 사용되는 동적 TLS는 TlsAlloc() 함수로 저장 공간을 설정하며, TlsFree() 함수로 저장 공간을 해제한다. 저장 공간에 값을 입력할 때는 TlsSetValue() 함수로 값을 입력하고, 출력은 TlsGetValue() 함수로 처리한다.

다음 코드는 TLS를 동적으로 사용한 예제이다.

#include <windows.h> #include <iostream> #include <cstdio> using namespace std; DWORD WINAPI myfunction(LPVOID); VOID CommonFunc(VOID); DWORD dwIndex; int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; dwIndex=TlsAlloc(); //Tls 영역 설정 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hThread); TlsFree(dwIndex); //Tls 영역 해제 return 0; } DWORD WINAPI myfunction(LPVOID p){ int j; j=(int)p; int *m=new int; *m=j+1; TlsSetValue(dwIndex, (LPVOID)m); //변수를 TLS Index를 받고 TLS 영역에 삽입한다. CommonFunc(); return 0; } // 스레드와 함수 사이에서는 *m을 공유하지만, 이는 각각 스레드별로 독립적으로 행동한다. VOID CommonFunc(VOID) { int i, *m, k; m=(int *)TlsGetValue(dwIndex); //호출된 for(i=0;i<*m;i++) k=*m+i; cout << "k=" << k << "(Thread:" << GetCurrentThreadId() << ")" << endl; } 

이 코드는 스레드 동기화를 위해 어떠한 방법론도 사용하지 않고 있다. 출력 화면에서 경쟁 상태가 나타나기는 하겠지만, 실제로 프로그램의 수행 결과는 예측대로 나타나게 된다.

정적 TLS는 조금 더 다루기 쉽다. 동적 TLS가 전역 메모리 할당과 흡사하게 동작한다고 하면 정적 TLS는 전역 변수 선언과 비슷하게 선언된다. 앞선 동적 할당이 주로 포인터 변수를 처리하기 위한 방법으로 사용된다면, 정적 TLS는 전역변수가 항상 동일한 초기값을 갖도록 처리할 수 있다. 정적 TLS를 사용하는 방법은 단지 전역 변수를 선언할 때 __declspec(thread)키워드를 사용하면 된다. 다음 코드를 한 번 살펴보자.

#include <windows.h> #include <iostream> #include <cstdio> using namespace std; DWORD WINAPI myfunction(LPVOID); VOID CommonFunc(VOID); DWORD dwIndex; __declspec(thread) int m=1; // TLS 전역 변수 사용 int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ int j; j=(int)p; CommonFunc(); m=m+j; // 만약 일반적인 전역 변수라면 이 구문이 호출된 이후 변수 자체가 변할 것이다. return 0; } // 스레드와 함수, 그리고 전체 코드는 m을 공유하지만, m은 생성된 스레드에 따라 다른 변수로 // 취급받는다. VOID CommonFunc(VOID) { int i, k; for(i=0;i<m;i++) k=m+i; cout << "k=" << k << "(Thread:" << GetCurrentThreadId() << ")" << endl; } 

이 프로그램이 초기 선언된 m 값을 일반적인 변수로 선언했다면 스레드에서 m 값을 계속 변경시키기 때문에 전역 변수는 각각의 스레드마다 다른 초기값을 받게 되며, 출력 결과는 예측할 수 없도록 변할 것이다. 그러나, 정적 TLS 변수로 선언되었기 때문에 모든 변수는 스레드 내에서 같은 초기값을 갖게 된다.

정적 TLS를 사용할 때 TLS 인덱스는 명시적으로 생성되거나 파괴되지 않는다. 스레드가 생성될 때 TLS 저장 영역이 생성되며, 스레드가 파괴될 때 같이 파괴된다. 주로 멀티스레드 프로그램을 수행하면서 정적 초기 변수가 변경될 가능성이 있지만 이러한 변수들을 제외하고는 교착 상태나 경쟁 상태가 벌어지지 않을 경우 TLS는 유용하다.


6.1 스레드 우선권

윈도우 시스템은 스레드를 기준으로 다중 작업이 이루어지는 멀티스레딩 프로그램이라고 앞에서 언급하였다. 스레드는 각각 일정 시간의 실행 시분할을 갖게 되며, 일단 동시에 모든 요청 스레드를 다 처리할 수 없으므로, 순서대로 처리하는 스레드 큐(Thread Queue)가 형성되게 된다. 그런데, 이 큐가 한 줄로 늘어선 것이 아니라, 우선권이라는 개념을 가진 상태에서 병렬적인 큐로 형성되어 있다. 우선권은 숫자가 커지면 커질수록 우선적인 개념을 갖고 있고 이는 시스템이 빨리 처리해야 하는 스레드이다.

가령, 은행 객장에 우대고객 전용 창구가 따로 있고, 아무리 은행 줄이 길어도 이 우대고객 전용 창구에 줄을 선 우대고객은 가장 먼저 은행일을 볼 수 있는 것과 같은 개념이다. 윈도우는 이 우선권이 31번까지 있다. 따라서, 스레드가 어떤 큐에 들어가느냐에 따라 처리되는 속도가 달라지게 된다. 다음 그림은 스레드가 각각의 큐에 들어가 있는 모습이다.

 

우선권이 높은 큐를 할당받는 스레드는 주로 I/O 등이나 시스템과 관계된 중요한 시스템들이다. 엄밀하게 말해서 우선권이 높은 큐는 그만큼 많은 시간 할당을 받게 된다. 시분할 멀티태스킹이기 때문이다.

윈도우 시스템은 동적으로 우선권을 관리한다. 이는, 프로그램이 실행되는 동안에도 계속 우선권이 변경될 수 있으며, 시스템은 항상 우선권을 적절한 방법으로 통제하고 관리한다. 가령, 윈도우 NT 이상의 시스템에서 나타나는 성능 옵션에서, 포그라운드 서비스와 백그라운드 서비스의 응용 프로그램 응답 옵션을 바꿀 수 있다. 참고로 윈도우 시스템은 유닉스 시스템과 포그라운드 서비스와 백그라운드 서비스의 개념상의 차이가 존재한다. 윈도우에서는 로그온 여부와 프로세스의 생존이 바로 직결되어 있으므로 로그오프 이후에도 운영되는 프로그램은 존재하지 않는다. 어떤 방법이든 사용자 로그온이 처리되어 있어야 프로세스가 생존할 수 있으며, 이렇게 따진다면 유닉스 개념의 포그라운드와 백그라운드가 나타나지 않는다.

윈도우 시스템에서 백그라운드 서비스는 현재 활성화되지 않은 모든 다른 프로세스를 의미한다. 즉, 로컬 로그온이 처리되어 창의 상단의 바가 파란색으로 바뀌지 않은 다른 모든 프로세스는 백그라운드 서비스이다. 이 경우, 성능 옵션에서의 다음 조정은 결국 우선권의 조정일 수밖에 없다.

 

실제로 위와 같은 성능 옵션의 응용프로그램 최적화는 원래 이 응용프로그램이 갖도록 정의되었던 스레드의 우선권보다 다소 높은 우선권을 동적으로 설정하라는 것이다. 우선권의 개념은 작업 관리자에서도 볼 수 있으며, 작업 관리자에서는 우선권 클래스에 해당하는 다섯 가지 개념이 나타나게 된다. 이 우선권들은 일차적으로 프로세스가 형성될 경우 지정된다. IDLE(4), NORMAL(7,9 또는 7+1=8, 9+1=10), HIGH(13), REALTIME(24)가 일차적으로 부여되며, 이 값에 가감을 하여 우선권 적용이 이루어진다. 가감은 +4~-3까지 가능하며, 이 조합으로 전체 우선권을 만들어 낼 수 있다. 따라서 우선권은 총 32개가 있기는 하지만 여기서 만들어 낼 수 있는 것은 총 21개가 된다. 이 중 가감이 될 수 없는 절대값(1, 15, 31)이 존재하며, 포그라운드 성능 최적화를 한 경우 초기부터 9-10 사이의 우선권이 주어진 채로 출발하게 된다. 음영을 넣은 부분은 실시간(Real Time) 우선권에 해당하는 부분이며, 일반적으로 사용자 응용프로그램의 우선권이 15 이상으로 올라가는 경우는 거의 없다. 만일 사용자 프로그램의 우선권이 15 이상으로 올라간다면 이는 프로그램상의 버그이거나 프로그래머가 의도적으로 실시간 큐에 삽입한 것이며, 이런 상황에서 프로그램의 응답이 지연되는 경우 전체 시스템이 같이 느려지는 현상이 벌어진다. 이는 상위 큐에 시간 할당이 더 많이 되어 있으며, 별다른 이유 없이(콜백 함수나 객체 상태 함수를 사용하지 않고) 시스템이 느려진다면 CPU 자원을 계속 잡아먹고 있는 셈이 된다. 15 미만에서 시작한 스레드의 우선권 상한도는 15까지이며, 15 이상에서 시작한 우선권이 상한선 31을 갖게 된다. 실제로 이렇게 높은 우선권을 갖고 있는 경우는 찾기 어렵다. 윈도우 CE의 경우 우선권은 32가지를 모두 사용하며, 이러한 측면들은 윈도우 소프트웨어가 발전할수록 계속 변하는 부분이다. 다음 표는 윈도우 스레드 우선권을 전부 나열한 것이며, 어떤 우선권이 있으며, 어떻게 우선권 변경이 되는지 확인해 볼 수 있다. 15, 31 은 절대값이며, 더 이상 가감되지 않는다.

6.2 우선권 가감

스레드의 우선권 가감 연산은 누적하여 수행할 수 없다. 운영체제는 하나의 스레드에 대한 기저 우선권(프로세스가 시작되는 시점에서의 우선권, Base Priority)을 기억하고 있으며, 스레드의 우선권 조정 함수는 이 기저 우선권에 대해서만 가감을 한다. 즉, 원래 기저 우선권이 10인 스레드에 우선권 조정을 몇 번 반복하더라도 우선권은 12 또는 15로밖에는 설정할 수 없다는 것이다. 따라서, 우선권은 사용할 수 있는 범위 한계가 있다. 다음과 같이 프로그램을 작성하고 컴파일해 보자.

#include <windows.h> #include <iostream> using namespace std; int main(){ HANDLE hThread; char a; hThread=GetCurrentThread(); cin >> a; // 잠시 정지를 위함 SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; return 0; } 

이 프로그램은 현재 동작하는 스레드(주 스레드)로부터 핸들을 얻어, 자신 스레드의 우선 권을 변경하는 프로그램이다. GetCurrentThread() 함수는 현재 동작하는 스레드의 핸들을 반환하며, SetThreadPriority() 함수는 현재 스레드의 우선권을 변경한다. 초기 기저 우선권은 프로세스가 형성될 때 생성되며, 별다른 옵션 없이 프로세스를 생성하였다면 기저 우선권은 운영체제가 적절하게 정의한다.

이 프로그램은 기저 우선권에 우선권을 계속 더하는 기능을 하고 있다. THREAD_PRIORITY_NORMAL은 +2를 하는 키워드이며, 이 프로그램을 수행하면서 성능 모니터를 같이 띄워 스레드의 우선권이 어떻게 변하는지 한 번 확인해 보기 바란다. 만일 기저 우선권이 8이었다면 10, 12, 14 이렇게 계속 증가할 것인지, 아니면 8+2 만 계속 반복하여 결과는 항상 10이 나오는지 측정해 보면 된다.

각자의 컴퓨터는 모두 다 다른 설정을 지니므로, 기저 우선권은 1~2 정도 오차가 있을 수 있다. 따라서, 프로그램의 수행 결과 초기값에 비해 어느 정도의 변경이 있었는지를 알아내는 것이 중요하며, 컴파일해서 실행해보면 알겠지만, 기저 우선권에 비해 결코 2 이상의 값이 올라가지 않는다. 시스템은 해당 스레드의 기저 우선권을 기억하고 있고, 해당 명령을 계속 수행하면 기저 우선권에 계속 2를 더하게 되는 까닭이다.

우선권의 변경은 일반적으로는 운영체제의 필요에 의해 이루어진다. 이 말은 운영체제의 필요가 아닌 경우라면 우선권의 변경은 프로그램적으로는 거의 이루어지지 않는다는 것이다. 간혹 인터넷 익스플로러 등의 프로그램이 작업 관리자의 종료 명령도 무시하고 계속 수행되는 경우를 볼 수 있으며, 이런 현상은 프로그램의 버그 등에 의해 작업 관리자 보다 해당 프로그램의 스레드 우선권이 높은 경우에 나타나게 된다.


7.1 윈도우와 프로세스

유닉스에서는 여러 개의 프로세스를 동시에 운영하는 것이 보편적이었다. 분기(fork)라는 형태로 하나의 프로세스에서는 다른 프로세스를 형성하고, 그 기저에는 셸 프로세스가 존재한다. 일차적으로 셸 프로세스가 생성되고 그 위에서 프로세스는 계속 시스템 분기를 사용하여 프로세스를 형성해 나간다. 만들어진 프로세스는 이전 프로세스의 거의 대부분의 속성을 계승하며, 특히 보안성과 관련되어서는 출발한 프로세스의 대부분의 속성을 갖고 있다.

프로세스는 해당 프로그램에서 필요한 주소 공간을 확보하고 있으며, 관계 리소스를 모두 보유하고 있다. 유닉스에서는 fork() 함수를 사용하여 시스템 호출로 새로운 프로세스를 만들어 내며, 프로세스 자체가 차지하는 오버헤드가 그렇게 크지 않기 때문에 멀티프로세스 프로그램이 힘들지 않게 운영된다.

윈도우 시스템에서는 런타임 라이브러리 레벨의 _spawn() 혹은 _exec() 함수를 사용하거나, Win32 API 레벨의 CreateProcess() 함수를 사용할 수 있다. CreateProcess() 함수는 상당히 많은 인수를 갖고 있으며 그 대부분은 프로세스에 대한 미세 조정이다.

주의할 점은 윈도우에서는 프로세스를 여러 개 생성시키는 형태의 작업은 거의 이루어지지 않으며, 필요에 의해 프로세스를 생성한다고 할지라도 최소한의 프로세스 작업으로 처리해 주어야 한다. 윈도우 프로세스는 새로 프로세스를 형성하였을 경우에는 기존 프로세스와 완전히 별개인 하나의 프로세스가 형성된다. 실제로 윈도우 시스템에서 새로운 프로세스를 형성하여 작업을 시도하는 경우는 그렇게 많지 않다. 동시에 많은 창이 뜨는 윈도우 프로그램은 거의 다 새로운 창은 하나의 스레드로 동작하도록 설정한다. 프로세스와 셸을 가득 띄워 놓은 유닉스 프로그램에 익숙하다면 이렇게 동작하는 방법론이 다소 답답할 수도 있을 것이다.

작업 관리자를 사용하면 현재 운영되고 있는 프로세스와 프로세스가 사용하고 있는 CPU의 시간 점유율을 볼 수 있다. 프로세스가 사용하는 시간 점유율은 그 프로세스가 형성한 모든 스레드의 동작 시간의 총합으로 정의된다. 스레드를 전혀 생성하지 않도록 프로그램을 설계했다고 하더라도 최소 하나의 스레드가 정의된다고 앞에서 이미 언급하였다.

실제로 프로세스를 생성하는 것은 아주 다양하고 복잡한 개념을 포괄하고 있다. 기본적으로 윈도우 2000은 보안성이 강한 운영체제이며, 이에 따라 어떤 스테이션, 어떤 세션, 그리고 어떤 ACL을 갖고 있는지 프로세스 자체에 명시되어야 한다. 일상적으로 이러한 작업들은 유닉스와 비슷하게 모 프로세스의 사본을 사용하는 경우가 많지만, 윈도우에서는 근본적으로 이런 설정들을 아예 처음부터 “새로” 만들 수 있다는 사실을 주목하자. 프로세스와 스레드 자체는 윈도우 보안성과 아주 밀접히 관련되어 있으며, 이런 일들은 시스템에 누가 들어와 있는지 판별하는 과정과 아주 밀접하게 관련된다.

7.2 프로세스 생성

프로세스 생성을 위해서는 BOOL 형의 CreateProcess() 함수가 사용된다. 이 함수는 생각보다 인수가 많으며, 각각의 인수들 중 일부는 구조체로 정의되어 있어 전부를 이해하는 것이 그렇게 생각보다 쉽지는 않다. 또한 보안성이라는 것이 같이 맞물려 있어, 해당 프로세스를 어떻게 형성할 것인지에 대해 정의할 수 있다.

윈도우는 다양한 종류의 프로세스가 존재한다. 여기서 Win32 API에 한정한다면 16비트 코드를 해석할 수 있는 WOW(Windows On Win32) VDM(Virtual DOS Machine) 내에 프로세스를 한정시킬 것인지, WOW VDM도 공유 VDM을 쓸 것인지 전용 VDM을 쓸 것인지 고려해야 하는 문제가 벌어진다. 하나의 프로세스에서 다른 프로세스를 형성할 때 해당 프로세스가 혹시 Win16 코드를 갖고 있지 않는지 한 번쯤 살펴볼 필요가 있다. 윈도우 XP가 나오는 이 시점에서 Win16 코드를 논하는 것이 시대착오적인 것 같지만 실제로 중소규모 소프트웨어 회사에서 제작한 아주 많은 애플리케이션은 여전히 Win16 코드를 일부 갖고 있다.

다음은 아주 간단한 프로세스 생성의 예이다. 이 코드는 인터넷 익스플로러를 실행시키고 모 프로세스는 중단된다.

#include <windows.h> #include <iostream> using namespace std; int main(){ STARTUPINFO stInfo; PROCESS_INFORMATION pInfo; BOOL bOk; LPTSTR strPath="C:\\Program Files\\Internet Explorer\\iexplore.exe"; ZeroMemory(&pInfo, sizeof(PROCESS_INFORMATION)); ZeroMemory(&stInfo, sizeof(STARTUPINFO)); stInfo.cb=sizeof(STARTUPINFO); bOk = CreateProcess(0, strPath, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &stInfo, &pInfo); if(!bOk){ cout << "프로세스 생성 오류:" << GetLastError() << endl; return -1; } WaitForSingleObject(pInfo.hProcess, INFINITE); // 생성한 프로세스가 종료될 때까지 기다린다. CloseHandle(pInfo.hProcess); CloseHandle(pInfo.hThread); return 0; } 

CreateProcess() 함수는 꽤 많은 인수를 갖고 있으며, MSDN에 이에 대해서는 아주 상세히 설명되어 있으므로 MSDN을 참조하기 바란다. 프로세스를 생성하는 것은 크게 네 가지 중요 정보를 제공해 준다. 일단 CreateProcess()가 받아들이는 인수는 다음과 같다.

BOOL CreateProcess( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); 

보안 옵션은 사용자그룹 보안 분과에서 해당 내용을 찾아볼 수 있을 것이다. 생성 플래그는 몇 가지 방법론을 제공하며, 이 플래그를 어떻게 조작하느냐에 따라 16비트 VDM 혹은 디버그 이벤트 받음 여부 등을 설정할 수 있다. 또한 생성된 프로세스의 주 스레드를 대기 모드(suspended state)로 생성할 수도 있다. 프로세스를 생성하기 위한 환경 변수로 STARTUPINFO 구조체를 사용하며, 프로세스가 생성한 스레드 및 프로세스의 핸들을 받기 위해서 PROCESS_INFORMATION 구조체를 사용하게 된다. 환경 변수는 초기 실행이 고정되지 않은 프로세스를 대상으로 프로세스의 시작 위치, 윈도우 크기 등을 지정할 수 있으며, 프로세스 정보 구조체에서는 생성된 프로세스와 주 스레드의 ID 및 핸들을 얻을 수 있다.

뒤에서 다루겠지만, 윈도우에서 프로세스가 새 프로세스를 생성할 때는 기존 프로세스의 속성을 계승할 수 있도록 설정하기도 하고, 계승하지 않도록 설정하기도 한다. 다음 제시되는 함수는 CreateProcess() 함수와 생긴 모양은 흡사하지만, 기존 프로세스와는 다른 사용자 계정에서 운영되는 새 프로세스를 형성한다. 이것은 4장에서 상세히 다루어질 것이다.

BOOL CreateProcessAsUser( HANDLE hToken, LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); 

7.3 프로세스 종료 및 정보 얻기

유닉스에서는 전통적으로 kill 명령어를 사용하여 프로세스를 강제로 종료하였다. 윈도우 시스템에는 작업 관리자가 존재하지만 작업 관리자는 반드시 GUI로 실행해 주어야만 되는 단점이 존재하고 있다. 그런데 윈도우에서 프로세스를 종료하는 것은 그다지 어려운 작업이 아니다.

프로세스 역시 커널 객체이므로, 이제 커널 객체에 익숙해졌다면 생성된 프로세스 ID로 프로세스 핸들을 받아 와서 해당 핸들을 가진 프로세스를 종료하면 된다는 등식이 머릿속에 도식적으로 나타날 것이다.

그런데 윈도우 프로세스는 일반 객체가 아니라 보안성까지 체크되는 보안 객체이다. 이러한 까닭에 단순히 프로세스 번호로 해당 프로세스를 종료시키는 것이 아니라, 해당 프로세스의 ACL 혹은 시스템 프로세스 여부 등을 파악하여 최종적으로 종료시킬 수 있는지를 판단한다. 사용자가 충분한 권한이 있으며, 해당 프로세스를 종료시킬 수 있다면 얻어낸 핸들값을 사용하여 프로세스를 완전히 종료시킬 수 있다.

시스템 프로세스의 경우 작업 관리자를 통해서도 액세스 거부가 나타나며, 이는 보안 분과에서 이유를 알아보도록 하라. 작업 관리자 등을 사용하여 알아낸 특정 프로세스를 종료하기 위한 코드는 다음과 같다.

#include <windows.h> #include <iostream> using namespace std; int main(int argc, char* argv[]){ if (argc !=2) { cout << "사용법: kill Pid" << endl; return -1; } int processId=atoi(argv[1]); unsigned uExitCode=0; HANDLE hProcess; hProcess=OpenProcess(PROCESS_TERMINATE, FALSE, processId); if(TerminateProcess(hProcess, uExitCode)) cout << "프로세스 " << processId << "번을 종료했습니다." << endl; else cout << "종료하지 못했습니다." << endl; return 0; } 

파일이나 기타 핸들을 사용하는 여러 구문들은 전부 이와 비슷한 형태로 동작하게 된다. 만일 프로세스를 종료하지 못했다면 프로세스가 시스템에 소속된 것이거나, ACL이 맞지 않아 종료할 수 없는 것일 가능성이 높다.

프로세스를 중단하기 위해서 TernminateProcess() 함수를 주로 사용하지만, 간혹 프로세스가 자살할 필요가 있다. 보편적으로 윈도우 프로그램은 명시적으로 프로세스를 종료하라는 말이 없을 경우에는 프로세스를 종료하지 않으므로 ExitProcess() 함수를 사용하여 종료할 수 있다. 이 함수는 자살을 위해 사용하므로 void 형이면 충분하다.

ExitProcess() 함수는 사용중인 DLL에 대한 참조값을 제거하고 발생시킨 모든 스레드의 참조값 역시 제거한다. 그리고 스스로 죽고 싶다는 메시지를 객체 관리자에게 송신한다. 그러나, 객체 관리자가 “죽여” 주기 전까지는 실제로 프로세스 객체가 사라지는 것은 아니다. 이는 앞에서 언급하였다.


8.1 파이버

파이버는 스레드보다 더 작은 단위의 일을 의미한다. 보통 SQL 서버를 운영하면서 파이버라는 단어를 처음 볼 수 있다. 다음과 같은 정보 창에서 윈도우 파이버를 이용하는 체크박스를 한 번쯤 보았을 것이다.

 

멀티 스레딩 프로그램에서 동시에 엄청난 다중 작업이 들어왔을 경우, 시스템은 컨텍스트 전환 오버헤드가 걸리게 된다. 특히 다중 프로세서를 운영하는 중앙 데이터 서버와 같은 경우는 초당 수천 건의 컨텍스트 전환과 100%에 육박하는 CPU 점유율을 보이는 경우가 다반사이다. 이러한 경우에 특화된 해결책이 파이버이다.

스레드(Thread)가 실을 의미하고 있기 때문에 실의 원료가 되는 섬유(fiber)로 이름을 붙인 것으로 생각된다. 파이버는 스레드와 달리 컨텍스트 전환을 시스템의 스케쥴에 따르지 않고 수동으로 해 주어야 한다. 따라서, 컨텍스트 전환에 따른 오버헤드가 발생하지 않는다. 그런데, 컨텍스트 전환을 수동으로 하는 것은 결국 멀티스레딩의 강점을 상당 부분 잃어버리는 것을 의미한다. 따라서, 파이버로 모든 코드를 작성하는 경우는 존재할 수 없다. 이는 프로세스 단위에서 항상 싱글스레딩을 영위하고 있는 결과를 낳게 된다.

만일 파이버가 스레드에서 파생될 수 있다면 어떤 의미를 지닐까? 이는 컨텍스트 전환은 스레드 단위에서 이루어지고, 업무는 파이버 단위에서 이루어지는 것을 의미한다. 가령 100개의 스레드를 사용하는 응용프로그램이 존재한다고 할 때, 스레드 100개를 영위하기 위해서는 100가지의 서로 다른 상태를 저장하고, 이들 사이에서 전환이 이루어져야 하지만, 50개의 스레드와 2개의 파이버를 사용한다면 컨텍스트 전환에 따르는 오버헤드를 다소 줄일 수 있다 시스템에서 이루어지는 컨텍스트 전환은 스레드 단위이기 때문에 각각의 스레드는 어차피 할 컨텍스트 전환이 반으로 줄어드는 셈이 된다. 그리고, 파이버 상호간에 이루어지는 컨텍스트 전환은 그 오버헤드도 작고 수동으로 이루어지므로 많은 장점이 존재한다.

그러나 스레드 대신 파이버를 사용하는 것은 성능 계산을 정확하게 해야 하는 단점이 존재한다. 따라서, 대용량 멀티스레드 프로그램에서 주로 채용되며, 현재까지 가장 효과적으로 사용되고 있는 것은 멀티프로세서가 장착된 MS-SQL 데이터베이스 서비스이다. 파이버를 사용할 경우 가장 큰 약점은 자기 자신의 프로세스가 전체 CPU를 대부분 장악하는 상황이 아니라면 컨텍스트 스위칭에서 후순위로 밀려버리는 상황이 벌어질 수 있다는 것이다. 즉, 스레드 숫자가 적기 때문에 할당에 따른 상대적 차별을 당할 수 있다. 따라서, SQL Server의 경우 CPU 점유량이 많지 않다면 파이버로 스레딩을 대신하는 것은 아무런 득이 되지 못할 가능성이 높다.

다음은 파이버를 사용하는 예제 코드이다.

파이버는 컨텍스트 전환에 따른 동기 문제가 발생하지 않기 때문에 동기 관계 객체들이나 이벤트 등을 사용할 필요가 전혀 없다. 다만 생성된 파이버 상호간 수동으로 전환하는 코드를 삽입하여야 한다. 스레드가 DWORD형의 함수를 갖는 데 반해 파이버는 VOID 형이라는 차이가 있다. 일단 전역 파이버를 선언한다. 그리고, 각각의 파이버를 생성한다. 다른 객체와 마찬가지로 파이버를 생성하는 것도 CreateObject 형식의 CreateFiber() 함수를 사용한다. CreateFiber()함수는 파이버에 할당될 초기 스택 크기와 함수 포인터, 그리고 전달 인수를 받아들인다. 파이버 당 할당될 수 있는 초기 최대 스택 크기는 따로 정의하지 않는 한 1MB이며, 이 인수를 제외하고는 스레드를 사용하는 것과 큰 차이가 나지 않는다.

파이버는 상호 컨텍스트 전환을 수동으로 해 주어야 하며 이 경우 주 스레드 역시 파이버로 변경되어야 컨텍스트 전환이 가능할 것이다. 따라서, 주 스레드를 파이버로 변경하는 ConvertThreadToFiber() 함수를 따로 제공하고 있다. 이 함수를 호출하게 되면 주 스레드가 파이버로 인식되며 (사실은 모든 파이버를 포함한 전체 주 스레드가 하나의 스레드로 시스템 상에서는 간주되는 것이지만) 파이버 상호간 컨텍스트를 수동으로 전환하는 SwitchToFiber() 함수를 사용하여 다른 파이버로 실행 권한을 넘기게 된다. 이 경우 제어권을 넘길 파이버는 어떤 파이버로 자신의 제어권을 넘길 것인지를 명시해 주어야 하며, 선언된 VOID형의 파이버 이름을 인수로 전달한다.

위 프로그램을 컴파일하여 실행하면 지금까지 보았던 예와 동일한 결과를 얻을 수 있을 것이다. 그렇지만 파이버로 작성한 프로그램은 프로세스 내에서 스케쥴 퀀텀을 받을 수 있는 기회가 줄어든다는 것을 잊어서는 안 된다.

8.2 작업 객체

작업 객체는 프로세스와 스레드를 다루는 객체 단위 중 가장 큰 것으로, 일련의 프로세스를 하나의 작업(job)으로 묶어 놓은 것이다.


본내용은 회원으로 가입하고있는 server관련 웹모임의 운영진에서 집필된 것임으로 인용시 출처를 밝혀주시기 바랍니다.


+ Recent posts