C++ Concurrent - Synchronization[3]
이번에는 스레드 동기화 방법 대해 살펴보겠습니다.
mutex
표준 mutex 종류
- std::mutex (C++11)
- std::timed_mutex (C++11)
- std::recursive_mutex (C++11)
- std::recursive_timed_mutex (C++11)
- std::shared_mutex (C++17)
- std::shared_timed_mutex (C++17)
mutex 멤버 함수
구분 | 내용 |
---|---|
lock() | 락 획득 및 블럭 대기 |
try_lock() | 락 획득 시도, 논 블럭 |
unlock() | 락 해제 |
native_handle | OS 수준 핸들 획득 |
std::mutex
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;
std::mutex m;
int share_data = 0;
void foo()
{
//m.lock();
if ( m.try_lock() )
{
share_data = 100;
std::cout << "using share_data" << std::endl;
m.unlock();
}
else
{
std::cout << "뮤텍스 획득 실패" << std::endl;
}
}
int main()
{
std::thread t1(foo);
std::this_thread::sleep_for(20ms);
std::thread t2(foo);
t1.join();
t2.join();
// mutex 의 native handle을 얻는 코드
std::mutex::native_handle_type h = m.native_handle();
// std::mutex m2 = m; // error
}
std::timed_mutex
- try_lock_for, try_lock_until 멤버가 추가됨(타임 아웃 개념)
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;
std::timed_mutex m;
int share_data = 0;
void foo()
{
//m.lock();
//if ( m.try_lock() )
if ( m.try_lock_for(2s) )
{
share_data = 100;
std::cout << "using share_data" << std::endl;
std::this_thread::sleep_for(3s);
m.unlock();
}
else
{
std::cout << "뮤텍스 획득 실패" << std::endl;
}
}
int main()
{
std::thread t1(foo);
std::thread t2(foo);
t1.join();
t2.join();
}
std::recursive_mutex
- 동일 스레드가 여러번 lock 가능, lock 횟수만큼 unlock 해야함
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;
class Machine
{
int shared_data = 0;
std::recursive_mutex m;
public:
void f1()
{
m.lock();
shared_data = 100;
m.unlock();
}
void f2()
{
m.lock();
shared_data = 200;
f1();
m.unlock();
}
};
int main()
{
Machine m;
std::thread t1(&Machine::f1, &m);
std::thread t2(&Machine::f2, &m);
t1.join();
t2.join();
}
std::shared_mutex
- mutex lock 처리시, read와 write 상태를 구분(read시에는 lock_shared로 성능 향상, 여러 스레드에서 동시 접근 가능)
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <string_view>
using namespace std::literals;
//std::mutex m;
std::shared_mutex m;
int share_data = 0;
void Writer()
{
while(1)
{
m.lock();
share_data = share_data + 1;
std::cout << "Writer : " << share_data << std::endl;
std::this_thread::sleep_for(1s);
m.unlock();
std::this_thread::sleep_for(10ms);
}
}
void Reader(std::string_view name)
{
while(1)
{
//m.lock();
m.lock_shared();
std::cout << "Reader(" << name << ") : " << share_data << std::endl;
std::this_thread::sleep_for(500ms);
//m.unlock();
m.unlock_shared();
std::this_thread::sleep_for(10ms);
}
}
int main()
{
std::thread t1(Writer);
std::thread t2(Reader, "A");
std::thread t3(Reader, "B");
std::thread t4(Reader, "C");
t1.join();
t2.join();
t3.join();
t4.join();
}
lock management
std::lock_guard
- mutex 사용시 실수로 unlock()을 하지 않을 경우 데드락 발생
- 코드 실행중 예외 발생시 unlock 구문 실행 안될 수 있음
- lock_guard는 생성자와 소멸자에서 lock, unlock 하도록 구현 되어 있음
#include <iostream>
#include <thread>
#include <mutex>
#include <exception>
std::mutex m;
void goo()
{
std::lock_guard<std::mutex> lg(m);
// m.lock();
std::cout << "using shared data" << std::endl;
// throw std::exception();
// m.unlock();
}
void foo()
{
try
{
goo();
}
catch(...)
{
std::cout << "catch exception" << std::endl;
}
}
int main()
{
std::thread t1(foo);
std::thread t2(foo);
t1.join();
t2.join();
}
#include <mutex>
std::mutex m;
int main()
{
// lock_guard 를 사용하는 2가지 방법
// 1. 생성자에서 lock 획득
{
std::lock_guard<std::mutex> lg(m);
}
// 2. 이미 lock 획득한 상태의 뮤텍스 관리
if ( m.try_lock() )
{
std::lock_guard<std::mutex> lg(m, std::adopt_lock);
}
}
std::unique_lock
- lock_guard의 기본 기능에 추가적인 기능을 제공
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std::literals;
std::mutex m1, m2, m3;
std::timed_mutex tm1, tm2, tm3;
int main()
{
std::unique_lock<std::mutex> u1;
std::unique_lock<std::mutex> u2(m1); // 생성자에서 m1.lock()
std::unique_lock<std::mutex> u3(m2, std::try_to_lock); // m2.try_lock()
if ( u3.owns_lock() )
std::cout << "acquire lock" << std::endl;
else
std::cout << "fail to lock" << std::endl;
m3.lock();
std::unique_lock<std::mutex> u4(m3, std::adopt_lock); // 이미 lock 을 획득한 뮤텍스 관리
std::unique_lock<std::timed_mutex> u5(tm1, std::defer_lock); // 나중에 lock 을 호출
auto ret = u5.try_lock_for(2s);
std::unique_lock<std::timed_mutex> u6(tm2, 2s); // tm2.try_lock_for() 사용
std::unique_lock<std::timed_mutex> u7(tm3, std::chrono::steady_clock::now() + 2s );
// tm3.try_lock_until() 사용
}
std::scoped_lock (C++17)
- deadlock 회피 기술을 사용해서 여러개의 mutex를 안전하게 lock
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;
struct Account
{
std::mutex m;
int money = 100;
};
void transfer(Account& acc1, Account& acc2, int cnt)
{
// std::scoped_lock<std::mutex, std::mutex> lg(acc1.m, acc2.m);// ok
std::scoped_lock lg(acc1.m, acc2.m);// ok
acc1.money -= cnt;
acc2.money += cnt;
std::cout << "finish transfer" << std::endl;
}
int main()
{
Account kim, lee;
std::thread t1(transfer, std::ref(kim), std::ref(lee), 5);
std::thread t2(transfer, std::ref(lee), std::ref(kim), 5);
t1.join();
t2.join();
}
std::shared_lock
- 하나의 스레드가 읽는 동안 다른 스레드도 읽도록 처리 -> std::shared_mutex
- shared_lock은 생성자와 소멸자에서 lock_shared(), unlock_shared() 호출
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <string_view>
using namespace std::literals;
std::shared_mutex m;
int share_data = 0;
void Writer()
{
while(1)
{
{
std::lock_guard<std::shared_mutex> lg(m);
share_data = share_data + 1;
std::cout << "Writer : " << share_data << std::endl;
std::this_thread::sleep_for(1s);
}
std::this_thread::sleep_for(10ms);
}
}
void Reader(std::string_view name)
{
while(1)
{
{
//m.lock_shared();
//std::lock_guard<std::shared_mutex> lg(m);
std::shared_lock<std::shared_mutex> lg(m);
std::cout << "Reader(" << name << ") : " << share_data << std::endl;
std::this_thread::sleep_for(500ms);
//m.unlock_shared();
}
std::this_thread::sleep_for(10ms);
}
}
int main()
{
std::thread t1(Writer);
std::thread t2(Reader, "A");
std::thread t3(Reader, "B");
std::thread t4(Reader, "C");
t1.join();
t2.join();
t3.join();
t4.join();
}
condition variable
신호 기반의 동기화 도구
- 생산자 소비자 패턴에서 소비자에게 생산이 되었음을 통지 할 수 있음
- unique_lock을 사용해야 함
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>
using namespace std::literals;
std::mutex m;
std::condition_variable cv;
bool data_ready = false;
int shared_data = 0;
void consumer()
{
std::unique_lock<std::mutex> ul(m);
cv.wait( ul, [] { return data_ready;} );
std::cout << "consume : " << shared_data << std::endl;
}
void producer()
{
std::this_thread::sleep_for(100ms);
{
std::lock_guard<std::mutex> lg(m);
shared_data = 100;
data_ready = true;
std::cout << "produce : " << shared_data << std::endl;
}
cv.notify_all();
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
std::thread t3(consumer);
std::thread t4(consumer);
t1.join(); t2.join();
t3.join(); t4.join();
}
std::semaphore (C++20)
자원을 N개 스레드가 공유 하고 싶을 경우
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <semaphore>
using namespace std::literals;
//std::counting_semaphore<1> sem(1);
std::binary_semaphore sem(1);
void Download(std::string name)
{
sem.acquire();
for (int i = 0; i < 100; i++)
{
std::cout << name;
std::this_thread::sleep_for(30ms);
}
sem.release();
}
int main()
{
std::thread t1(Download, "1");
std::thread t2(Download, "2");
std::thread t3(Download, "3");
std::thread t4(Download, "4");
std::thread t5(Download, "5");
t1.join(); t2.join();
t3.join(); t4.join();
t5.join();
}
std::call_once, std::once_flag
동일한 함수를 한번만 실행하도록 처리 하고 싶을 때
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std::literals;
std::once_flag init_flag;
//std::once_flag a = init_flag;// error
void init(int a, double d)
{
std::cout << "init" << std::endl;
std::this_thread::sleep_for(2s);
}
void foo()
{
std::cout << "start foo" << std::endl;
//init(10, 3.4);
std::call_once(init_flag, init, 10, 3.4 );
std::cout << "finish foo" << std::endl;
}
int main()
{
std::thread t1(foo);
std::thread t2(foo);
std::thread t3(foo);
t1.join();
t2.join();
t3.join();
}
활용예제 : lock 없는 다중스레드 환경의 싱글턴 패턴
- but C++11부터는 static 멤버 변수 방식의 싱글턴 객체도 다중 스레드 초기화시 안전함
#include <iostream>
#include <thread>
#include <mutex>
using namespace std::literals;
class Singleton
{
private:
Singleton() = default;
static Singleton* sinstance;
static std::once_flag create_flag;
public:
Singleton(const Singleton& ) = delete;
Singleton& operator=(const Singleton& ) = delete;
static Singleton* getInstance()
{
std::call_once(create_flag, initSingleton);
return sinstance;
}
static void initSingleton()
{
sinstance = new Singleton;
}
};
Singleton* Singleton::sinstance = nullptr;
std::once_flag Singleton::create_flag;
int main()
{
std::cout << Singleton::getInstance() << std::endl;
std::cout << Singleton::getInstance() << std::endl;
}