Blog Full Notice
back to main page

3 분 소요

motivation: 네이버 블로그

#thread, mutex, and RAII(C++) : 네이버 블로그

mutex, thread 훑어보기 + RAII 개념 알아보기

나중에 mutex, thread, condition variable에 대해 공부하고 정리해서 올릴 것이다.

밑의 코드를 보자.

#include <iostream> #include <thread> #include <mutex> #include <ctime> typedef unsigned long long ull_t; int x = 0; std::mutex m1; ull_t oddSum = 0, evenSum = 0; void addEvenNum(ull_t n); void addOddNum(ull_t n); int main() { // sum with multithreading auto time1 = clock(); std::thread t1(addEvenNum, 19000000); std::thread t2(addOddNum, 19000000); t1.join(); t2.join(); auto time2 = clock(); std::cout << "time spent with multithreading: " << (time2 - time1) << '\n'; std::cout << "evenSum: " << evenSum << '\n'; std::cout << "oddSum: " << oddSum << '\n'; // sum without multithreading oddSum = 0, evenSum = 0; auto time3 = clock(); addEvenNum(19000000); addOddNum(19000000); auto time4 = clock(); std::cout << "time spent without multithreading: " << (time4 - time3)<< '\n'; std::cout << "evenSum: " << evenSum << '\n'; std::cout << "oddSum: " << oddSum << '\n'; } void addEvenNum(ull_t n) { ull_t condition = 0; while (++condition < n) { if (condition % 2 == 0) { evenSum += condition; } } m1.lock(); x++; m1.unlock(); } void addOddNum(ull_t n) { ull_t condition = 0; while (++condition < n) { if (condition % 2 == 1) { oddSum += condition; } } m1.lock(); x++; m1.unlock(); }

이 코드를 실행시키면 multithreading 한 것이 수행 시간이 더 짧다.

mutex (C++11 이상)

  • 2개 이상의 서로 다른 thread가 같은 전역변수를 접근하고 수정 하는 상황을 race condition이라고 한다. 이 race condition이 있으면 보호해야 하는 부분을 critical section/region이라고 한다. mutex는 race condition을 피하기 위해 만들어졌고, std::mutex m1; m1.lock(), m1.unlock()을 사용해 race condition을 피할 수 있다.

  • 어쨋든 race condition을 피하기 위해서, multithreading에서 쓸 함수에서 어떤 전역변수를 동시에 사용하게 되는 부분을 lock()과 unlock()으로 감싸준다. 그렇게 되면 서로 다른 thread에서 다른 일은 동시에 처리할 수 있는 반면, lock()과 unlock()으로 감싸진 부분은 만약 thread1이 m1.lock()을 실행했다면, thread1에서 m1.lock()이 m1.unlock()으로 풀려야 thread2가 m1.lock()을 수행할 수 있게 된다. 만약 thread1에서 m1.unlock()을 안해준다면 thread2의 critical region은 영원히 접근될 수 없는 것이다.

  • mutex의 늘린말은 mutual exclusion이라고 한다.

  • (출처: https://youtu.be/eZ8yKZo-PGw)

thread (C++11 이상)

  • multithreading을 통해 parallelism을 구현하기 위해 thread를 사용한다. thread는 OS가 배정해준다.

  • thread를 사용하는 방법은 1. function pointer 2. lambda function 3. functor (function object) 4. member function, 5. static member function 이 있다.

  • 1. function pointer로 사용하는 방법은, std::thread t1(함수 이름, 함수에 들어가야하는 인수1, 인수2,...); 형식으로 사용할 수 있다. 4. member function을 사용하는 방법은 class Base 안에 public 함수 run(int x)가 있다고 하면, Base obj; std::thread t1(&Base::run, &obj, 10); 형식으로, 함수의 주소, class 객체의 주소, 파라미터 순으로 입력한다.

  • std::thread.join()은 thread가 끝날때까지 기다리는 것이다. std::thread t1; ...; t1.join()을 하면 그 다음 line은 thread t1이 끝나야 수행되는 것이다. 여기서 주의해야 할 점은 만약 multiple thread를 동시에 만든다면, 무엇이 먼저 시작할지 모른다. 그러나 join은 t1.join(); t2.join(); 을 하면 t1이 끝나기를 기다리고 t2가 끝나기를 기다린다.

  • 특히 double join은 run-time으로 프로그램을 terminate 시킨다. 따라서 if(t1.joinable()) t1.join();처럼 joinable() 함수를 통해 join을 실행시켜주는 것이 좋다.

  • join은 "wait for this thread to complete"인 반면 detach는 "detach this thread form this parent(main) thread"이다. t1.detach()하면 이 thread를 기다리고 싶지 않다. 이 thread가 실행되는지 관심이 없다.

  • 따라서 만약 어떤 thread를 detach했는데 main 함수가 return하게 되었다면, thread가 모두 실행되지 않고 프로그램이 끝난다. 이때 detached thread의 execution은 suspended 된다고 한다. 즉 detach의 경우 부모 thread가 return되서 없어지면 자식 thread는 실행에 상관없이 다 중단된다.

  • 또한 join() 또는 detach()는 모든 thread에 대해서 반드시 실행되어야 한다. 그 이유는 join() 또는 detach()가 되지 않으면 thread object가 아직 joinable한지 확인해 보고, joinable하다면 thread object의 destructor가 terminate 시키기 때문이다.

  • (출처: https://youtu.be/TPVH_coGAQs)

RAII

  • resource aquisition is initialization의 줄임말이다.

  • resource의 성질은 1. 사용되기 전에 습득해야 하는 것이고, 2. 보통 자원이 limited된 것이다. resource의 예로 heap memory(dynamic allocation), files, mutexes 등이 있다.

  • 그렇다면 RAII의 결과는, resource lifetime에 대해 더이상 생각할 필요가 없고, object lifetime에 대해서만 신경써면 된다. 따라서 new, delete, free, explicit하게 mutex를 lock/unlock하지 않는다.

  • 왜 new, delete를 절대 쓰지 않아도 되는가? 왜 object lifetime만 신경써도 되는가? RAII class는 constructor에서 resource aquire를 하고, destructor에서 resource release를 해주기 때문이다. 대표적인 RAII class로는 std::vector, std::string, std::unique_ptr, std::shared_ptr, std::lock_gaurd 등이 있다. 이런 것들을 사용하면 프로그램이 안정적이게 되는 것이다. 수동으로 lock/unlock을 안해도 되는 것이다.

  • (출처: https://youtu.be/q6dVKMgeEkk)

댓글남기기