이번에는 스레드 동기화 방법 대해 살펴보겠습니다.

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;
}