C++11 상호 배제(Mutex)

|

Mutex 종류

C++ 표준에서는 Mutex 종류는 크게 2가지가 있습니다. 하나는 일반 Mutex이며, 다른 하나는 타임 아웃 Mutex 입니다.


일반 Mutex

일반 Mutex에는 std::mutex, std::recursive_mutext 두 종류가 있습니다. 두 클래스는 공통으로 다음과 같은 함수를 갖고 있습니다.

  • lock() : Thread에서 락 점유를 시도하며, 락을 획득할 때까지 무한정 대기함.
  • try_lock() : 락 점유를 시도하며, 성공시 true, 실패시 false를 바로 리턴함.
  • unlock() : 락을 해제함.

std::mutex는 중복해서 lock()을 호출할 경우 ‘데드락(Deadlock)’ 상황에 빠질 수 있습니다.

std::recursive_mutexlock()을 여러 번 호출해도 상관없지만, 대신 락을 해제할 때는 동일한 횟수만큼 unlock()을 호출해주어야 락이 해제됩니다.


타임 아웃 Mutex

타임 아웃 Mutex에는 std::timed_mutex, std::recursive_timed_mutex가 있습니다. 그리고 다음과 같은 함수들을 갖고 있습니다.

  • lock() : Thread에서 락 점유를 시도하며, 락을 획득할 때까지 무한정 대기함.
  • try_lock() : 락 점유를 시도하며, 성공시 true, 실패시 false를 바로 리턴함.
  • unlock() : 락을 해제함.
  • try_lock_for(rel_time) : 주어진 시간까지 락 점유를 시도. 시간은 상대 시간임. 성공시 true, 실패시 false를 바로 리턴.
  • try_lock_until(abs_time) : 주어진 시간까지 락 점유를 시도. 시간을 절대 시간임. 성공시 true, 실패시 false를 바로 리턴.


lock 클래스

Mutex를 좀 더 사용하기 쉽게 Wrapping한 클래스입니다. lock 클래스의 소멸자는 자동으로 내부의 Mutex를 해제해주기 때문에, 각 함수의 로컬 변수로 사용할 경우 함수를 빠져나오면서 자연스럽게 락이 해제됩니다. 즉, 락 해제에 신경을 덜 쓸 수 있습니다.

lock_guard는 생성자에서 락 점유를 시도하며, 점유를 성공할 때까지 Blocking 됩니다.

unique_lock은 객체를 미리 선언해두고, 나중에 필요한 시점에 락 점유를 시도할 수 있습니다. lock(), try_lock(), try_lock_for(), try_lock_until() 등의 함수도 제공합니다.


lock 예제

#include <mutex>

using namespace std;

class LockExample {
 public:
  void sendMessage(int what, int arg1, int arg2);

 private:
  mutable mutex mMutex;
};

void LockExample::sendMessage(int what, int arg1, int arg2) {
  unique_lock<mutex> lock<mMutex>

  mMessageSender->sendMessage(what, arg1, arg2);
}


이중 검사 락 알고리즘

멀티 쓰레드 환경에서 어떤 부분이 단 한 번만 호출이 되어야 한다면 이중 검사 락 알고리즘을 사용할 수 있습니다. 물론, call_once()를 사용한다면 굳이 이 알고리즘을 사용할 필요는 없을 것 같습니다.

아래 예제는 isInitialized 변수를 확인한 다음 초기화를 수행하고 isInitialized 변수의 값을 true로 세팅하는 예제입니다. isInitialized 변수가 true로 세팅되기 직전에 다른 쓰레드가 끼어드는 것을 방지하기 위해서 이중 검사 락 알고리즘을 사용했습니다.

#include <cstdio>
#include <thread>
#include <mutex>
#include <vector>

using namespace std;

class LockExample {
 public:
  void init() {
    printf("Init()\n");
  }
};

bool isInitialized = false;
mutex mtx;

LockExample exam;

void func() {
  if (isInitialized == false) {
    unique_lock<mutex> lock(mtx);

    if (isInitialized == false) {
      exam.init();
      isInitialized = true;
    }
  }

  printf("func()\n");
}

int main() {
  vector<thread> threads;

  for (int i = 0; i < 5; i++) {
    threads.push_back(thread{func});
  }

  for (auto &t : threads) {
    t.join();
  }

  return 0;
}

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 옵션은 해당 파일의 변경을 감시하라는 옵션입니다.

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