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 명령어로 확인할 수 있습니다.

네이글 알고리즘(Nagle Algorithm)

|

일반적인 TCP 통신 방법

TCP 통신은 상대방이 패킷을 받았는지 안 받았는지 확인하기 위해서, 데이터를 받은 쪽에서 ACK 신호를 보냅니다. ACK 신호를 확인해야 송신부에서는 패킷 전송이 제대로 되었음을 확인하고 그 다음 패킷을 계속해서 전송할 수 있습니다.


Nagle Algorithm

네이글 알고리즘은 ‘가능하면 조금씩 여러 번 보내지 말고, 한 번에 많이 보내라’는 원칙을 기반으로 만들어진 알고리즘입니다.

기존에는 ‘NAGLE’라는 단어를 패킷으로 보낸다고 할 때, ‘N’을 전송한 다음 상대방으로부터 ACK를 받으면 ‘A’를 전송. 또 `ACK’를 받은 다음 ‘G’를 전송하는 형태로 이루어집니다. 이런식으로 반복해서 한 글자씩 차례대로 전송하게 됩니다.

하지만, 네이글 알고리즘을 적용하게 되면 처음에 ‘N’을 전송한다음 상대방으로부터 ACK를 받기 전까지 ‘AGLE’에 대한 패킷 정보를 송신 버퍼에 저장한다음 ACK가 오면 여러 개의 패킷을 모아서 한 번에 전송하게 됩니다.

image


네이글 알고리즘의 장단점

네이글 알고리즘의 장단점은 다음과 같습니다.

  • 장점 : 같은 양의 데이터더라도 한 번에 많이 보내기 때문에 데이터 전송 횟수가 줄어들기 때문에 네트워크의 효율성이 높아짐
  • 단점 : ACK를 받을 때까지 패킷을 모으고 있기 때문에 반응 속도가 느려짐


C++ 코드 예제

C++에서 TCP 소켓 통신은 기본적으로 네이글 알고리즘이 적용되어 있습니다. setsockopt() 함수를 이용해서 네이글 알고리즘을 On/Off 할 수 있습니다.

다음 예제는 네이글 알고리즘을 ‘Off’하는 예제입니다. (값이 True이면 ‘Off’라는 것을 주의합시다.)

int opt_val = TRUE;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt_val, sizeof(opt_val));
  • 값이 TRUE 또는 1 이면 Off
  • 값이 FALSE 또는 2 이면 On