C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #4 Com. Science

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #4

지난글
- HOWTO #1 : http://sunyzero.egloos.com/4227785
- HOWTO #2 : http://sunyzero.egloos.com/4229235
- HOWTO #3 : http://sunyzero.egloos.com/4234766

목차
0. 시작하기에 앞서
1. OpenMP란?
2. OpenMP의 시작
3. OpenMP를 이용한 work-sharing 모델
4. Loop construct
  4.a Loop consturct 기본 형태
  4.b 병렬 처리시 주의점
  4.c 스케줄링
* 연습문제 (멀티 쓰레딩과 reentrant 함수의 관계에 대한 문제)
5. Section construct
* 연습문제 (섹션별로 task를 할당하는 방법)
6. Single Construct
7. Master Construct
8. Barrier
  8.a Implicit barrier
  8.b Explicit barrier

9. critical & atomic construct
  9.a critical construct
  9.b atomic construct
10. OpenMP API
11. OpenMP에서 제공하는 환경변수 및 전처리기
12. OpenMP vs pthread (POSIX thread)
13. 마치면서


9. critical & atomic construct

critical construct와 atomic construct는 공유(shared) 영역을 보호하는 lock의 일종이다. 따라서 critical과 atomic을 사용한 영역은 쓰레드 중에 단 하나의 쓰레드만 진입하고 나머지는 대기하게 된다. 즉 직렬실행구간이 된다. 따라서 역으로 말하면 critical과 atomic을 남발하는 프로그래밍을 하면 성능이 확 떨어진다는 소리이다. 그럼에도 불구하고 꼭 lock이 필요한 기능이므로 잘 익혀두어야만 한다.

우선 둘의 차이부터 이해하고 넘어가자. 만일 두 단어만 보고도 감이 오는 사람은 학생 때 운영체제 수업에서 critical section이나 atomic operation을 배운 사람일 것이다.

• critical : 범용적인 lock
• atomic : critical보다 가볍고 빠른 lock (critical의 special한 케이스)

critical은 노멀한 lock이지만 atomic은 매우 가벼운 용도에만 사용한다. 가볍다는 것은 보통 1개의 스칼라 변수를 업데이트 하는 정도를 의미한다. 만일 변수 업데이트를 넘어서 계산이나 캐스팅, 복잡한 데이터 핸들링을 하는 경우라면 critical을 사용하도록 한다.
[참고로 정수형 변수 1개의 값을 변경하는 정도라면 atomic 없이 프로그래밍을 하는 것도 추천한다. atomic을 쓰지 않으면 수치상의 오류가 생기는 경우도 있겠지만 일반적으로는 10ppm(part per million) 이하인 경우이므로 무시할 수 있는 오차 일 수 있다. 왜냐하면 atomic의 가벼운 lock도 멀티 쓰레드에서는 성능을 많이 떨어뜨리기 때문이다.]
[보충 설명: 일반적으로 공유된 데이터에 복수개의 쓰레드가 접근할 경우에는 무조건 lock으로 보호해야만 한다. 모든 교과서나 매뉴얼에는 이렇게 나와있다. 그러나 프로그래머가 어느 정도 멀티 쓰레드에 대한 이해가 깊어지면 교과서나 매뉴얼을 응용할 수 있어야만 한다. 필자가 앞에서 정수형 변수 1개의 값을 업데이트하는 경우에는 lock을 쓰지 않는 것도 좋다고 했는데 이는 감히 교과서에는 쓸 수 없는 내용이다. 하지만 공유된 변수가 약간의 오차가 생겨도 괜찮은 경우, 즉 근접해나 근사치를 계산하는 시뮬레이션이나 휴리스틱 코드라면 atomic을 쓰지 않을 경우 더 빠르게 작동할 수 있다. 그리고 빠른 실행이 가능해졌기 때문에 시행횟수를 더 늘림으로서 오차를 보정할 수 있게된다. 그러므로 오차 보정이 가능한 수치계산이나 오차보다 속도가 더 중요한 문제라면 atomic을 생략하는 것도 고려하라는 뜻이 담겨있는 것이다. 그러나 주의할 점은 꼭 프로그래밍을 할 때 atomic을 선택적으로 적용할 수 있도록 코딩해두고 atomic을 사용한 버전과 사용하지 않은 버전의 차이를 꼭 비교해봐야 한다. 만일 atomic을 사용하지 않은 버전이 메리트가 없다면 반드시 lock을 사용한 버전을 택하는 것이 좋다.


9.a critical construct
#pragma omp critical [(lock_name)]

critical에서 락 이름(lock_name)을 생략하는 경우는 동일한 lock을 사용하는 것으로 간주되어 critical 지시어가 여러 번 나오더라도 동일하게 보호되는 구간이 된다. 자세한 것은 예제를 보면서 익히도록 하자. 예제로 아래와 같은 pseudo code가 있다.
#pragma omp parallel for
for (i=0; i<MAX_ITERATION; i++) {
process_a();
chk_process();
process_b();
chk_process();
process_c();
chk_summary();
}
위의 예제에서 chk_process() 함수가 lock으로 보호되어야 한다면 critical 지시어를 사용하여 아래와 같이 할 수 있다.
#pragma omp parallel for
for (i=0; i<MAX_ITERATION; i++) {
process_a();
#pragma omp critical
chk_process();
process_b();
#pragma omp critical
chk_process();
process_c();
chk_summary();
}
critical construct을 2군데에서 사용했지만 lock_name을 지정하지 않았기 때문에 같은 lock으로 인식하여 보호된다. [위와 같이 프로그래밍하지 않고 chk_process()함수 내부에 critical을 선언하여도 결과는 같다.]

그렇다면 이번에는 chk_summary()에 critical을 추가해보겠다. 하지만 chk_process()와는 상관없이 다른 락으로 보호하도록 하겠다. 이 경우에는 필수적으로 lock_name을 사용해야 한다.
#pragma omp parallel for
for (i=0; i<MAX_ITERATION; i++) {
process_a();
#pragma omp critical (lock1)
chk_process();
process_b();
#pragma omp critical (lock1)
chk_process();
process_c();
#pragma omp critical (lock2)
chk_summary();
}
이제 critical의 사용방법에 대해서는 어느정도 감이 왔을 것이다. 하지만 거듭 강조하지만 왠만하면 lock은 최소한으로 사용하는 것이 좋다. 멀티 쓰레드 프로그래밍에서 lock을 통한 직렬구간을 많이 만들면 프로그래밍은 쉽지만 성능은 바닥을 치기 때문에 멀티 쓰레드 프로그래밍을하는 의미가 사라지게 된다.



9.b atomic construct
#pragma omp atomic
atomic은 스칼라 변수 한개를 업데이트 한다고 했다. 너무 간단하기 때문에 간단한 예제 하나로 끝내겠다.
#pragma omp parallel
...생략...;
#pragma omp atomic
n_count++;
}
atomic에서 쓸 수 있는 표현식은 ++나 --같은 unary operation이나 "변수=값"정도의 간단한 대입식 정도이다.





10. OpenMP API
앞에서 omp_get_thread_num() 같은 openmp의 함수들을 보았다. 이번에는 주로 사용되는 함수들을 정리해서 보도록 하겠다.

* 쓰레드 풀에 관련된 함수들
void omp_set_num_threads(int num_threads); 병렬 구간에서 생성할 쓰레드 개수를 설정
int omp_get_num_threads(void); 병렬 구간에서 생성할 쓰레드 개수를 리턴
int omp_get_max_threads(void); 쓰레드 최대값 리턴
int omp_get_thread_num(void); 현재 쓰레드 번호 리턴
int omp_get_num_procs(void); 물리적 프로세서의 개수 리턴

int omp_in_parallel(void); 병렬 구간일 경우 true(0이 아닌값), 아니면 false(0)을 리턴

void omp_set_dynamic(int dynamic_threads); 생성할 쓰레드 개수를 동적으로 조정할 지 여부 (dynamic_threads가 1이면 true, 0이면 false)
int omp_get_dynamic(void); 위의 omp_set_dynamic의 설정을 리턴


아래 예제처럼 omp_set_dynamic이 설정되면 사용량에 따라서 쓰레드의 개수가 10개를 넘지 않는 범위에서 적당하게 조정된다.
#include <omp.h>
int main()
{
omp_set_dynamic(1);
#pragma omp parallel num_threads(10)
{
/* do work here */
}
return 0;
}
예제 출처: OpenMP 2.5 Specification



* OpenMP에서 제공하는 lock 기능 API : 정교한 모델을 만들때 유용하므로 꼭 읽어두자.

void omp_set_nested(int nested); nested가 1이면 병렬구간의 중첩을 허용, 0이면 허용하지 않음
int omp_get_nested(void); 위의 nested 설정을 리턴

void omp_init_lock(omp_lock_t *lock); OMP의 lock을 초기화
void omp_init_nest_lock(omp_nest_lock_t *lock); 중첩 lock버전의 위와 같은 기능의 함수 (=재귀잠금이 가능한 lock)

void omp_destroy_lock(omp_lock_t *lock); OMP의 lock을 제거
void omp_destroy_nest_lock(omp_nest_lock_t *lock); nested lock버전의 위와 같은 기능의 함수

void omp_set_lock(omp_lock_t *lock); OMP의 lock을 잠금
void omp_set_nest_lock(omp_nest_lock_t *lock); nested lock버전의 위와 같은 기능의 함수

void omp_unset_lock(omp_lock_t *lock); OMP의 lock을 잠금 해제
void omp_unset_nest_lock(omp_nest_lock_t *lock); nested lock버전의 위와 같은 기능의 함수

int omp_test_lock(omp_lock_t *lock); 잠금 여부를 테스트하여 가능한 상태면 잠그고 다른 쓰레드에 의해 잠긴 상태면 false를 리턴 (=nonblocking 버전의 함수임)
int omp_test_nest_lock(omp_nest_lock_t *lock); nested lock버전의 위와 같은 기능의 함수



double omp_get_wtime(void); 병렬처리 구간에서의 수행 시간(단위: 초)
double omp_get_wtick(void); omp_get_wtime()에서 제공하는 시간 정밀도 (최소 단위 시간)
double start;
double end;
start = omp_get_wtime();
... work to be timed ...
end = omp_get_wtime();
printf("Work took %f seconds\n", end - start);
예제 출처: OpenMP 2.5 Specification

OpenMP 2.5에서 제공하는 모든 API를 다루었다. 특히 lock관련 함수는 중요하기 때문에 잘 알아두면 도움이 된다.





11. OpenMP에서 제공하는 환경변수 및 전처리기

OpenMP에서는 스케줄링, 생성할 쓰레드 개수, 동적 조정 여부, 병렬구간에서 쓰레드의 중첩 허용을 runtime시에 결정할 수 있도록 환경변수를 설정할 수 있다.

• OMP_SCHEDULE : 스케줄링 타입과 chunk 크기를 설정
• OMP_NUM_THREADS : 병렬 구간에서 생성할 쓰레드 개수
• OMP_DYNAMIC : 쓰레드 개수의 동적 조정 여부를 설정
• OMP_NESTED : 쓰레드 중첩의 허용 여부를 설정

사용예 : bash (or POSIX shell)의 명령
export OMP_SCHEDULE="dynamic"
export OMP_SCHEDULE "guided,4"
export OMP_NUM_THREADS 16
export OMP_DYNAMIC true
export OMP_NESTED false


* OMP 조건부 컴파일을 위한 전처리기

#include <stdio.h>
int main()
{
# ifdef _OPENMP
printf("Compiled by an OpenMP-compliant implementation.\n");
# endif
return 0;
}






12. OpenMP vs pthread (POSIX thread)

OpenMP로 작성한 프로그램을 그대로 pthread 버전으로 바꾸면서 둘의 차이를 보도록 하겠다. 
예제로 볼 것은 앞에서 numerical integration으로 pi를 계산한 프로그램을 두가지 버전으로 작성해서 비교하도록 하겠다.

우선 HOWTO #1에서 봤던 OpenMP 버전을 다시 보도록 하겠다.
예제 파일 : pi_numerical_integration.c
#include <stdio.h>
#include <stdlib.h>
int num_steps=1000000000; /* 10억번 : 너무 많으면 조금 줄이시길... */

int main()
{
int i;
double x, step, sum = 0.0;
step = 1.0/(double) num_steps;
#pragma omp parallel for private(x) reduction(+:sum)
for (i=0; i<num_steps; i++) {
x = (i+0.5) * step;
sum += 4.0/(1.0 + x*x);
}
printf("PI = %.8f (sum = %.8f)\n", step*sum, sum);
return EXIT_SUCCESS;
}
OpenMP버전은 이미 설명했었기 때문에 이를 바로 Pthread 버전으로 바꾸도록 하겠다.

pthread 버전 예제 파일 : pi_integration_pth.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static int num_steps=1000000000;
double pi, step;
struct start_arg {
int i_start;
int i_end;
double sum;
} start_arg[2];

void *start_func(void *arg)
{
int i;
double x, sum = 0.0;
struct start_arg *p_arg = (struct start_arg *) arg;

for (i=p_arg->i_start; i<p_arg->i_end; i++) {
x = (i+0.5) * step;
sum += 4.0/(1.0 + x*x);
}
p_arg->sum = sum;
return NULL;
}

int main()
{
int i;
double sum;
pthread_t pt_id[2];

step = 1.0/(double) num_steps;
start_arg[0].i_start = 0;
start_arg[0].i_end = num_steps>>1;
start_arg[1].i_start = start_arg[0].i_end + 1;
start_arg[1].i_end = num_steps;

printf("%d~%d, %d~%d\n",
start_arg[0].i_start, start_arg[0].i_end ,
start_arg[1].i_start, start_arg[1].i_end );

for (i=2; i--; ) { /* create pthread */
if (pthread_create(&pt_id[i], NULL, start_func, &start_arg[i])){
perror("pthread_create");
return 1;
}
}
for (i=2; i--; ) { /* join pthread : explicit barrier */
if (pthread_join(pt_id[i], NULL)){
perror("pthread_join");
return 1;
}
}
sum = start_arg[0].sum + start_arg[1].sum;
printf("PI = %.8f (sum = %.8f)\n", step*sum, sum);
return EXIT_SUCCESS;
}
pthread 버전으로 바꾸면 openmp버전보다 코딩량이 훨씬 늘어난다.

POSIX thread를 사용하면 OpenMP에서 없는 explicit barrier를 넣어주어야 하므로 pthread_join()함수를 사용했으며, OpenMP에서 간단하게 reduction을 했던 것이 POSIX thread에서는 개별적인 start_arg라는 구조체를 통해서 확인했다. 이로 인해서 코딩량과 자료구조까지 신경써야 하는 불편함이 생긴다.

그러나 OpenMP가 POSIX thread보다 우월하다고 말하는 것은 아니다. 문제가 정교함을 요구하는 경우에는 오히려 OpenMP가 적합하지 않은 경우도 많기 때문이다. 실제로 조건변수를 이용한다든지 쓰레드 키를 이용한 라이브러리등을 사용하는 경우라면 POSIX thread외에는 대안이 없는 경우가 많기 때문이다. 이 글은 OpenMP를 소개하는 글이므로 OpenMP에 대해 좋은 말만 늘어놓고 있지만 필자의 말을 비판없이 받아들여서 OpenMP가 최고라는 편협한 사고에 휩싸이지는 않았으면 좋겠다.

잡설이 길었는데 마지막으로 두가지 버전의 실행 결과를 확인해보자. 첫번째 실행한 파일인 pi_integration이 OpenMP버전이고, 두번째의 pi_integration_pth가 Pthread버전이다.
$ time ./pi_integration
PI = 3.14159265 (sum = 3141592653.59036255)

real 0m3.910s
user 0m7.484s
sys 0m0.018s

$ time ./pi_integration_pth
0~500000000, 500000001~1000000000
PI = 3.14159265 (sum = 3141592650.38979244)

real 0m4.071s
user 0m7.399s
sys 0m0.022s
결과를 보면 실행결과나 성능에 큰 차이가 없어 보인다. 하지만 몇몇 컴파일러에서는 특정 버전이 더 느린 경우도 있다.(이는 해당 플랫폼의 특징에서 기인한다. 표준안에는 성능에 대한 제약은 없기 때문에 몇몇 플랫폼은 OpenMP 구현시 덜 최적화 되어있는 경우도 있다.)





13. 마치면서.

처음 시작하면서 멀티 쓰레드 프로그래밍의 필요성에 대해서 언급했었다. CPU의 개별 코어의 주파수(frequency)는 거의 한계치에 와 있기 때문에 개별 코어의 속도는 파이프라인의 증설이나 몇가지 기술로 약간의 진보밖에 이룰 수 없게 되었다. 따라서 멀티코어를 적극적으로 사용하는 기술이 뒷받침되지 않고는 프로그램의 성능을 높일 수 있는 방법은 거의 없다.(일각에서 언급되는 그래픽카드의 코어를 병렬로 이용하는 GPGPU도 같은 맥락이다.) 하지만 멀티 쓰레드 프로그래밍은 다음과 같은 난제가 있다.

  1. 멀티 쓰레드 프로그램의 문제점 : 디버깅이 힘들다. (재현성이 나쁘기 때문)
  2. 아키텍처와 메모리 모델에 대한 이해가 필요하다. (본인도 부족하다. 그래서 실수를 할 때가 있다.)


첫째로 문제인 디버깅에서 언급되는 재현성은 쓰레드 프로그래밍에서 큰 난제이다. 디버깅을 위해서는 같은 조건하에서 시행하였을때 동일한 문제가 재현되어야만 버그를 찾아낼 수 있다. 그러나 멀티 쓰레드 프로그램은 각각의 쓰레드들이 같은 순서로 실행된다는 보장이 없기 때문에 버그가 발생한 순서조합이 디버깅때도 항상 발생하지 않는다. 즉 문제를 찾아내려고 할때는 오히려 제대로 작동하는 경우도 많다는 것이다.

따라서 멀티 쓰레드 프로그램을 대형 모델에 적용할 때는 투명성을 높여서 각각의 데이터나 태스크의 흐름을 모니터링할 수 있도록 만들어야 한다. 이를 위해서 여러 방법론이 나와있지만 스페셜한 케이스에는 딱 들어맞는 것은 없다. 그러므로 반복적인 프로그래밍 훈련만이 투명성을 높이고 디버깅이 쉬운 개발철학을 만들 수 있게 해준다.

둘째로 우리가 종종 사용하는 x86 인텔 호환 플랫폼은 PC환경에서 가장 많이 쓰이지만 산업체에서는 IA64, Sparc, MIPS, PPC(PowerPC) 등등의 CPU에 여러가지 OS조합의 플랫폼이 사용된다. 그러나 같은 프로그래밍을 짜도 표준안에서 언급한 semantic을 만족하면서 미묘한 차이를 보이는 경우가 많다. 특히 기능은 제대로 작동하는데 성능은 이상하게 차이나는 경우가 발생할 수 있다. 이를 해결하기 위해서는 각각의 플랫폼에 대한 이해와 하드웨어 지식이 필요한데, 한사람의 프로그래머가 모든 플랫폼에 정통할 수는 없기 때문에 적극적으로 다른 플랫폼의 전문가와 소통할 수 있는 채널이 있어야만 한다.


Posted by 김정준
l

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #3 Com. Science

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #3


지난글
- HOWTO #1 : http://sunyzero.egloos.com/4227785
- HOWTO #2 : http://sunyzero.egloos.com/4229235

목차
0. 시작하기에 앞서
1. OpenMP란?
2. OpenMP의 시작
3. OpenMP를 이용한 work-sharing 모델
4. Loop construct
  4.a Loop consturct 기본 형태
  4.b 병렬 처리시 주의점
  4.c 스케줄링
* 연습문제 (멀티 쓰레딩과 reentrant 함수의 관계에 대한 문제)
5. Section construct
* 연습문제 (섹션별로 task를 할당하는 방법)

6. Single Construct
7. Master Construct
8. Barrier
  8.a Implicit barrier
  8.b Explicit barrier
9. critical & atomic construct
  9.a critical construct
  9.b atomic construct
10. OpenMP API
11. OpenMP에서 제공하는 환경변수 및 전처리기
12. OpenMP vs pthread (POSIX thread)
13. 마치면서



6. Single Construct

single construct로 지시된 구간은 단 한번만 실행된다. 실행되는 쓰레드는 여러 쓰레드중에 제일 먼저 진입하는 쓰레드이다.

  • single construct는 처음으로 진입한 쓰레드가 실행한다.
  • 나머지 쓰레드들은 single construct 끝에 존재하는 implicit barrier에서 대기한다.
  • single construct가 끝나고 모든 쓰레드들은 implicit barrier에서 동시에 시작한다.

그림에서 보이듯이 parallel 구간에서 쓰레드들 중에 한 개만  single construct를 실행하고 나머지는 뒤에 존재하는 implicit barrier에서 대기하는 것을 볼 수 있다. 그러면 위 소스코드를 컴파일하고 실행해보자. 실행결과는 예상대로 "1. Hello world"는 1번 출력되고, "2. Hello world"는 2번 실행된다.(테스트 호스트는 듀얼 코어이므로)
$ gcc -o omp_single -fopenmp omp_single.c
$ ./omp_single
1. Hello world
2. Hello world
2. Hello world



7. Master Construct

master construct는 single construct와 매우 비슷하다.
하지만 다른 점이 2가지 있다.

  • master construct 구간은 무조건 master thread (main thread)가 1번 실행한다.
  • master construct 구간뒤에 implicit barrier가 없다. 
    즉 모든 쓰레드는 master construct 실행되는 동안에도 계속 실행한다.
실행 결과는 위의 single construct와 같지만 위 그림에서 보듯이 약간의 차이는 있다. master construct는 implicit barrier가 없다는 점이다. 중요한 차이므로 꼭 기억해야 한다. 




8. Barrier

배리어란 동기화(synchronization)을 위해서 사용되는 기능이다.

동기화는 시간적 개념이다. 풀어서 설명하기 위해 예를 들자. 스타크래프트 배틀넷은 왠만한 사람이면 다 해봤을 것이다. 최대 8명까지 게임에 참가할 수 있는데, 어떤 유저가 매우 느린 모뎀을 쓰고 있으면 게임 중간에 타임을 세는 화면이 뜨고 기다려주는 것을 볼 수 있다. 이는 빠른 네트워크/컴퓨터를 가진 유저와 느린 네트워크/컴퓨터를 가진 유저의 게임 속도를 맞추기 위해서 배리어가 작동한 것이다. 따라서 결과적으로 배리어는 느린 사람에 맞춰서 앞서 가는 사람이 대기하도록 하는 기능이다.

그러면 프로그래밍에서는 배리어를 어떻게 사용해야 하는가? 작업이 병렬적으로 이뤄진다고 하더라도 전처리, 후처리 작업들이 나눠져 있을 경우에는 전처리 작업들을 병렬처리했을때 어떤 특정 쓰레드가 빨리 처리했다고 후처리 작업을 먼저 출발하면 데이터가 꼬이거나 로직이 망가질 수 있다. 이럴 경우 중간중간에 적절한 배리어를 넣어주면 깔끔하게 해결된다.(하지만 역으로 배리어가 많으면 그 만큼 대기도 많아질 수 있다.)


8.a Implicit barrier

앞에서 implicit barrier(암묵적 배리어)에 대해서 이야기를 많이 했다. OpenMP는 각 작업의 동기화에 대한 편의성을 제공하기 위해서 implicit barrier를 잘 제공한다. 어떤 construct 에 대해서 implicit barrier가 제공되는지 정리하고 넘어가자.

  • #pragma omp for
  • #pragma omp sections
  • #pragma omp single
위의 4가지의 경우는 블록 끝에 자동적으로 implicit barrier가 들어간다. 하지만 위의 4가지 construct 의 끝에 nowait clause를 지정하면 implicit barrier가 제거되고 대기하지 않고 이후 코드가 즉시 실행된다.

위의 예제에서는 single construct에 nowait를 적용하여 implicit barrier를 제거하는 것을 볼 수 있다. 
(그림 아래에 있는 implicit barrier는 parallel construct에 있는 barrier다.)


8.b Explicit barrier

이번에는 사용자가 직접 지정할 수 있는 explicit barrier 기능에 대해서 보겠습니다.
  • #pragma omp barrier 구문을 지정하면 해당 부분에서 모든 쓰레드가 도착할 때까지 대기하게 된다.
char * get_time0(char *buf, size_t sz_buf);

int main()
{
int t_sleep; char buf[16];
#pragma omp parallel private(t_sleep, buf)
{
#pragma omp single nowait
sleep(1);
printf("[%s] phase1:sleep %ld sec.\n", get_time0(buf, sizeof(buf)), t_sleep = times(NULL)%8);
sleep(t_sleep);
#pragma omp barrier
/- explicit barrier *-
printf("[%s] phase2. Hello world\n", get_time0(buf, sizeof(buf)));
}
return 0;
}

char * get_time0(char *buf, size_t sz_buf)
{
time_t t0; struct tm tm_now;
if (buf == NULL) return NULL;
if (time(&t0) == ((time_t)-1)) return NULL;
localtime_r(&t0, &tm_now);
if (strftime(buf, sz_buf, "%H:%M:%S", &tm_now) == 0) return NULL;
return buf;
}
이제 실행해보면 배리어 효과때문에 마지막 실행한 19:12:28에서 5초뒤에 phase2가 실행되는 것을 볼 수 있다.
$ ./omp_barrier
[19:12:27] phase1:sleep 1 sec.
[19:12:28] phase1:sleep 5 sec.
[19:12:33] phase2. Hello world
[19:12:33] phase2. Hello world

이번에는 여기까지 마치고, 다음에 critical section이나 atomic, OpenMP API함수등에 대해서 다뤄보도록 하겠다.


Posted by 김정준
l

뇌는 현실과 언어를 구별하는 능력이 없기 때문에,
입으로 ‘짜증나’를 반복하면 그 소리가 귀를 통해 뇌로 전달되고,
뇌는 ‘짜증이 나 있는 것인데 왜 멀쩡한 척하느냐’면서
온몸에 불쾌한 스트레스 호르몬을 쫙 뿌린다. 
말버릇은 그야말로 버릇으로 출발하지만
버릇이 거듭되면 마음과 몸에 굳어버린다.
- 우종민 교수, ‘뒤집는 힘’에서
 
흥미롭게도 뇌는 말과 현실을 구분하지 못한다고 합니다.
말은 밖으로 나왔다가
‘뇌의 지령’에 따라 다시 자신에게로 돌아갑니다.
노상 ‘짜증난다’고 하는 사람 주변에는 짜증 날 일이 계속 생깁니다.
반면 긍정적이고 좋은 말만 계속하면 실제로 좋은 일만 생기게 됩니다.
말버릇이 자신의 가치를 결정하는 것입니다.
훈련을 거듭하면 좋은 말 습관을 만들 수 있습니다. 
Posted by 김정준
l

사과는 모든 희망과 바람, 또 불안함의 가면을 벗겨낸다.
사과할 때 인간은 가장 인간다워지고
일상생활에서 쓰고 있던 가면을 벗고 진실한 얼굴을 하게 된다.
사과는 더 이상 약자나 패자의 변명이 아니라
‘리더의 언어’로 바뀌어야 한다. 
사과란 단지 잘못을 시인하고 용서를 구하는 행위 이상의 가치를 지녔다.
- 존 케이더(John Kador)
 
‘제 잘못입니다’라는 말 한마디가 다른 사람의 마음을 열어줍니다.
책임질 줄 아는 사람, 신뢰할수 있는 사람으로 인식되게 합니다.
사과는 인간관계를 돈독하게 해주는 역할뿐만 아니라,
기업의 수익창출에도 도움이 됩니다.
사과는 다른 어떤 언어로도 대체할 수 없는 마법의 힘을 가졌습니다. 
Posted by 김정준
l

정상에 오르기란 어렵다.
그런데 정상에 오른 뒤에 그 자리를 지키는 사람이 드문 이유는
십중팔구 성공바이러스에 감염되었기 때문이다.
성공바이러스의 감염증상은 만족감으로 나타난다.
성취는 성공 바이러스가
당신과 당신이 이끄는 조직을 감염시키지 않도록 주의할 때만
같은 수준 또는 더 높은 수준으로 지속된다.
-존 우든, ‘리더라면 우든 처럼’에서
 
스펜서 존슨은 “전성기에서 일찍 밀려나게 되는 가장 일반적인 원인은
자신감이라는 탈을 쓴 오만함”이라고 말했습니다.
모든 실패의 근저에는 자만감이라는 공통점이 있습니다.
자만심은 자신감을 부풀리고 현실을 잘못 보게 만듭니다.
벼는 익을수록 고개를 숙인다는 평범한 지혜를 다시한번 새길 때입니다. 
Posted by 김정준
l