call_once를 이용한 Singleton 패턴

|

Thread-Safe Singleton

기본적으로 Singleton 패턴은 Thread-Safe 하지 않습니다. 그래서 생성자에 synchronized 키워드나(Java의 경우) ‘Mutex’ 등을 이용해서 Thread-Safe 하도록 만들어주는 경우가 많습니다. 그게 아니면, 프로그램 실행 초반에 인스턴스를 생성하도록 하는 방법을 많이 씁니다.

C++11 부터 사용가능한 call_once()를 이용하면 좀 더 간편하게 Singleton 패턴을 사용할 수 있습니다.

std::call_once()std::once_flag()와 함께 사용하여 복수의 Thread 환경에서 특정 함수를 단 한 번만 구동되도록 할 수 있습니다. 이러한 호출을 ‘Effective Call Once Invocation’라고 합니다.


Singleton.h

#ifndef SNOWTHREAD_SINGLETON_H
#define SNOWTHREAD_SINGLETON_H

#include <cstdio>
#include <mutex>
#include <memory>

using namespace std;

class Singleton {
 public:
  static Singleton &getInstance() {
    call_once(Singleton::mOnceFlag, []() {
      printf("Singleton Instance is created...\n");
      mInstance.reset(new Singleton);
    });

    return *(mInstance.get());
  }

  void log() {
    printf("hello\n");
  }


 private:
  static unique_ptr<Singleton> mInstance;
  static once_flag mOnceFlag;

  Singleton() = default;
  Singleton(const Singleton &) = delete;
  Singleton &operator=(const Singleton &) = delete;
};

unique_ptr<Singleton> Singleton::mInstance;
once_flag Singleton::mOnceFlag;

#endif //SNOWTHREAD_SINGLETON_H


main.cpp

#include "Singleton.h"

int main() {
  Singleton::getInstance().log();
  Singleton::getInstance().log();

  return 0;
}


실행 결과

Singleton Instance is created...
hello
hello

C++11 Thread 생성 방법들

|

함수 포인터를 이용하는 방법

함수 포인터를 이용하는 예제 코드는 다음과 같습니다.

#include <cstdio>
#include <thread>

using namespace std;

void counter(int id, int length) {
  for(int i=1; i<=length; i++) {
    printf("counter[%d] : %d\n", id, i);
  }
}

int main() {
  thread t1(counter, 1, 5);
  thread t2(counter, 2, 7);
  t1.join();
  t2.join();

  return 0;
}

위에서 join()은 각 Thread가 작업 완료될 때까지 Blocking되어 있도록 하는 명령어입니다. Blocking은 일반적으로 자원의 낭비를 가져오기 때문에 실제 프로그램에서는 join()의 사용을 최대한 피하는 것이 좋습니다. 대신 Thread에 메세지(Message)를 처리하는 루틴을 만들고, Thread에 메세지를 보내어서 작업을 수행하는 방식이 좀 더 바람직합니다.


함수 객체를 이용하는 방법

#include <cstdio>
#include <thread>

using namespace std;

class Counter {
 public:
  Counter(int id, int length) {
    mId = id;
    mLength = length;
  }

  void operator()() const {
    for (int i = 1; i <= mLength; i++) {
      printf("counter[%d] : %d\n", mId, i);
    }
  }

 private:
  int mId;
  int mLength;
};

int main() {
  // #1
  thread t1{Counter(1, 5)};

  // #2
  Counter c2(2, 7);
  thread t2(c2);

  // #3
  thread t3(Counter(3, 8));

  t1.join();
  t2.join();
  t3.join();

  return 0;
}

위의 예제에서 3가지 방식이 있었는데 3번째 방식은 특수한 경우(예를 들어 인자로 들어가는 클래스의 생성자에 파라메터가 없는 경우)에 컴파일 에러가 뜰 수 있기 때문에 가급적 첫 번째 방식을 사용하는 편이 더 낫습니다.


람다 표현식을 이용하는 방법

#include <cstdio>
#include <thread>

using namespace std;

int main() {
  thread t1([](int id, int length) {
    for (int i = 1; i <= length; i++) {
      printf("counter[%d] : %d\n", id, i);
    }
  }, 1, 7);

  t1.join();

  return 0;
}


클래스 메소드를 이용하는 방법

#include <cstdio>
#include <thread>

using namespace std;

class Counter {
 public:
  Counter(int id, int length) {
    mId = id;
    mLength = length;
  }

  void loop() const {
    for (int i = 1; i <= mLength; i++) {
      printf("counter[%d] : %d\n", mId, i);
    }
  }

 private:
  int mId;
  int mLength;
};

int main() {
  Counter c1(1, 7);

  thread t1{&Counter::loop, &c1};

  t1.join();

  return 0;
}

이 방법은 특정 인스턴스의 메소드를 별도 Thread로 실행시킬 수 있는 장점이 있습니다.

top 사용법

|

시스템 모니터링

top는 시스템 모니터링을 하는 명령어입니다. 시스템의 부하 관련 정보를 수초마다 체크하여 다음과 같이 화면에 갱신해줍니다.

image

  • load average : CPU가 처리하는 걸 기다리는 작업 개수. 1 분당 평균으로 몇 개의 일이 쌓이는지 나타냄
  • TIME+ : 해당 프로세스가 실제로 CPU를 사용하는 시간
  • COMMAND : 프로세스가 실행되었을 때 실행한 명령어 커맨드. C를 눌러 상세 표시 전환 가능


프로세스에 대한 내용

항목 내용
PID 프로세스 ID
USER 프로세스를 실행한 사용자 ID
PR 프로세스 우선 순위
NI 작업 수행의 Nice Value 값으로 마이너스를 갖는 값이 우선 순위가 높음
VIRT 가상 메모리 사용량(SWAP + RES)
RES 현재 페이지의 상주 크기(Resident Size)
SHR 분할된 페이지로 프로세스에 의해 사용된 메모리를 나눈 메모리의 총합
S 프로세스의 상태. S(Sleeping), R(Running), W(Swapped out process), Z(Zombies) 등의 상태를 가짐
%CPU CPU 사용률
%MEM 메모리 사용률


단축키

단축키 설명
Shift + M 메모리 소비량 순으로 정렬
Shift + T CPU 실행 시간 순으로 정렬
Shift + P CPU 점유량 순으로 정렬
Space 화면 갱신

파이프라인(Pipeline) 사용법

|

파이프라인

파이프라인은 어떤 명령의 실행 결과 출력을 그대로 다른 명령어에 전달하는 것을 의미합니다. 예를 들어 엄청난 양의 Log가 있다고 할 때, 여기서 원하는 단어가 들어간 라인만 필터링하고, 그 결과에서 또 다른 검색어로 필터링해서 그 결과를 조회하는 것도 파이프라인을 사용하는 것이라고 생각할 수 있습니다.

안드로이드의 logcat의 예를 들어보겠습니다.

adb shell로 안드로이드 쉘(Shell)에 접속한 다음

logcat

을 입력하면 엄청난 양의 Log가 화면에 출력이 됩니다. 눈으로 쫓아가기도 힘들 정도인데, 여기에 grep을 이용해서 필터링을 해보도록 하겠습니다.

logcat | grep "snowdeer"
여기서 ‘ ‘는 파이프라인을 의미합니다. 양쪽의 명령어를 연결해주는 역할을 합니다. 즉, logcat으로 나온 결과를 grep "snowdeer"로 다시 필터링을 하도록 만들어줍니다.

파이프라인은 다음과 같이 여러 개 연결할 수 있습니다.

logcat | grep "snowdeer" | grep -v "ignore"

grep-v 옵션은 해당 검색어를 제외하라는 옵션입니다.

그리고 만약, 마지막 결과를 less와 같은 텍스트뷰어에서 조회하는 것도 가능합니다. (안드로이드 Shell에는 less가 없습니다.)

명령어 | grep "snowdeer" | grep -v "ignore" | less


tail

실시간으로 바뀌는 파일의 끝 부분만 출력하는 명령어로 tail이 있습니다. (마찬가지로 안드로이드 Shell에는 없습니다.)

tail -F access.log

라고 하면, ‘access.log’ 파일이 갱신될 때마다 추가된 내용을 실시간으로 갱신해서 보여주는 기능을 합니다. -F 옵션은 해당 파일의 변경을 감시하라는 옵션입니다.

여기에도 마찬가지로 파이프라인으로 추가 필터링을 걸어줄 수 있습니다.

ldd 사용법

|

실행 파일은 있는데 필요한 라이브러리가 없는 경우

실행 파일은 있는데 필요한 라이브러리가 없는 경우 다음과 같은 오류 메세지를 출력합니다.

./snowdeer: error while loading shared libraries: libglog.so.0: cannot open shared object file: No such file or directory

위 오류 메세지는 libglog.so.0 이라는 동적 라이브러리가 없다는 메세지입니다. 이 경우 해당 라이브러리를 시스템 폴더(주로 ‘/usr/lib’ 아래)에 복사해주면 해결이 됩니다.

다만, 프로그램을 실행했을 때 오류 메세지로는 필요한 라이브러리를 1개씩밖에 확인이 안되게 때문에 많이 번거롭습니다. 이 경우 ldd 명령어를 이용하면 필요한 라이브러리 리스트를 모두 확인할 수 있습니다.


List Dynamic Dependencies`

ldd 명령어는 프로그램이 사용하고 있는 공유 라이브러리(Shared Library) 리스트를 출력합니다. ‘List Dynamic Dependencies’의 약자입니다.

? ldd ./snowdeer
	/usr/lib/arm-linux-gnueabihf/libarmmem.so (0xb6f3f000)
        libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0xb6f04000)
        libglog.so.0 => not found
        libprotobuf.so.9 => not found
        libboost_system.so.1.55.0 => /usr/lib/arm-linux-gnueabihf/libboost_system.so.1.55.0 (0xb6ef1000)
        libcaffe.so.1.0.0 => not found
        libasound.so.2 => /usr/lib/arm-linux-gnueabihf/libasound.so.2 (0xb6e17000)
        libopencv_core.so.2.4 => /usr/lib/arm-linux-gnueabihf/libopencv_core.so.2.4 (0xb6bfe000)
        libopencv_highgui.so.2.4 => /usr/lib/arm-linux-gnueabihf/libopencv_highgui.so.2.4 (0xb6bad000)
        libopencv_objdetect.so.2.4 => /usr/lib/arm-linux-gnueabihf/libopencv_objdetect.so.2.4 (0xb6b32000)
        libopencv_imgproc.so.2.4 => /usr/lib/arm-linux-gnueabihf/libopencv_imgproc.so.2.4 (0xb68e3000)
        libopencv_video.so.2.4 => /usr/lib/arm-linux-gnueabihf/libopencv_video.so.2.4 (0xb6889000)
        libwiringPi.so => /usr/lib/libwiringPi.so (0xb686b000)
        ...

위와 같이 snowdeer라는 파일이 사용하는 라이브러리 리스트가 출력이 되며, 라이브러리 이름 옆에 위치가 출력이 됩니다. 만약 라이브러리가 없다면 이름 옆에 => not found 라는 메세지가 출력됩니다. 즉, 필요한 라이브러리들을 확인해서 시스템 폴더 아래 복사를 해주면 됩니다.


사용 예제

ldd ./snowdeer | grep "not"

다만 ldd 명령어로도 필요한 라이브러리들이 한 번에 안 나오는 경우도 종종 있는 것 같습니다. 그럴 때는 필요한 라이브러리들을 먼저 설치한다음 다시 ldd 명령어로 확인할 수 있습니다.