lock관련 클래스들은 mutex를 좀 더 사용하기 쉽도록 지원 해 줍니다. 

 mutex를 객체를 할당받아서 lock과 unlock을 자동으로 수행하도록 합니다.

 스마트 포인터(unique_ptr, shared_ptr)들이 포인터를 관리하듯이 lock 클래스를 통해서 mutex를 안전하게 

 관리 해야 합니다.

Lock 관련 named requirements(명명된 요구사항)

 c++ 표준에서는 다양한 named requirements를 요구합니다. 그중에서 lock과 관련된 내용을 공유 드립니다. 

BasicLockable 

 BasicLockable 요구사항은 수행 대상(예 스레드)에 배타적인 blocking semantic을 제공하는 타입의 최소 특성을

 설명합니다. 

 해당 타입이 BasicLockable이 되려면 아래 요구 조건을 충족 해야 합니다.

표현식 요구조건 결 과
m.lock()   현재 수행 중인 대상이 lock을 획득 할 때까지
차단합니다. 
만약 예외가 발생하면 lock을 획득하지 않습니다.
m.unlock() 현재 수행 대상은 m의 lock을
소유하고 있어야 합니다.
수행 대상이 보유한 lock을 해제 합니다. 
예외 발생 하지 않습니다.

Lockable 

 lockable 요구 사항은 시도된 locking(try lock)을 포함해서 BasicLockable을 확장합니다.

표현식 결 과 결과값
m.try_lock() blocking없이 현재 수행 대상에서 lock을 얻기 위해 시도합니다. 
예외가 발생하면 lock을 획득하지 않습니다.
true : lock 획득
fase : lock 획득 못함

TimedLockable 

 Timedlockable 요구 사항은 수행 대상(예 스레드)에 배타적인 시간 제한이 있는 blocking semantic을

 제공하는 타입의 최소 특성을 설명합니다. 

표현식 요구조건 결과값
m.try_lock_for(duration) lock을 획득하거나 또는 입력한 기간(duration)동안
block합니다. 
true : lock 획득
fase : lock 획득 못함
m.try_lock_untin(time_limit) lock을 획득하거나 또는 입력한 시간(time_limit)까지 
block합니다. 
true : lock 획득
fase : lock 획득 못함

링크

https://en.cppreference.com/w/cpp/named_req/BasicLockable

 

C++ named requirements: BasicLockable - cppreference.com

The BasicLockable requirements describe the minimal characteristics of types that provide exclusive blocking semantics for execution agents (i.e. threads). [edit] Requirements For type L to be BasicLockable, the following conditions have to be satisfied fo

en.cppreference.com

https://en.cppreference.com/w/cpp/named_req/Lockable

https://en.cppreference.com/w/cpp/named_req/TimedLockable

std::lock_guard

설 명

 lock_guard는 scoped 블록안에서 RAII 스타일 기능을 제공하는 mutex wrapper 클래스입니다. 

 (RAII란 Resource Acquisition is initialization의 약자이며 리소스의 사용 scope가 끝날 경우 획득한 자원을 자동으로

 해제해서 leak을 방지하는 디자인 패턴입니다.)

 lock_guard 객체는 생성 될 때 mutex의 소유권을 획득하려고 시도합니다.

 lock_guard 객체가 생성된 scope를 벗어 날 때 소멸자를 통해서 mutex를 해제합니다.

 lock_guard는 non-copyable한 클래스입니다. 

내부구현

// CLASS TEMPLATE lock_guard
template <class _Mutex>
class lock_guard { // class with destructor that unlocks a mutex
public:
    using mutex_type = _Mutex;

    // 생성자에서 lock함수를 통해서 락 획득
    explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
        _MyMutex.lock();
    }

    // adopt_lock_t 태그를 사용해서 락 객체 획득
    // 이미 lock을 획득된 객체에 대해서 호출한다. 
    lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { // construct but don't lock
    }

   // 생성자에서 lock함수를 통해서 락 획득
    ~lock_guard() noexcept {
        _MyMutex.unlock();
    }

    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    _Mutex& _MyMutex;
};

 lock_guard의 내부구현은 간단합니다. template으로 mutex객체를 전달 받아서 생성자에서 lock()함수를 호출하고

 소멸자에서 unlock()함수를 호출합니다.

 non-copyable이기 때문에 복사 생성자 및 대입 연산자가 delete로 구현되어 있습니다.

 BasicLockable 구조를 지원하는 어떠한 객체도 lock_guard를 사용할 수 있습니다. 

사용 예제

// lock_guard를 사용하기 위한 유저 클래스 
class UseLockGuard {
public :
    void lock() {
        std::cout << "UseLockGuard lock" << std::endl;
    }

    void unlock() noexcept {
        std::cout << "UseLockGuard unlock" << std::endl;
    }
};


int main()
{
    {
        // scope 범위에서 mutex의 lock을 획득합니다
        std::mutex sm_;
        std::lock_guard<std::mutex> l(sm_);
    } // 이곳을 지나면 lock_guard l의 소멸자가 호출 됩니다.

    {
    	// BasicLockable 인터페이스를 지원하는 어떤 클래스도 사용 가능합니다. 
        UseLockGuard ulg_;
        std::lock_guard<UseLockGuard> l(ulg_);
    }
}

lock_guard 생성자 호출시

 위 그림은 lock_guard 생성자 호출시 진행되는 동작을 그림으로 나타냅니다.

lock_guard 소멸자 호출시

 위 그림은 lock_guard 소멸자 호출시 진행되는 동작을 그림으로 나타냅니다.

std::unique_lock

설 명

 unique_lock 클래스는 지연된 locking, 시간이 제한된 locking, 재귀적 locking, lock의 소유권 이전과 조건 변수와

 함께 사용을 허용하는 범용 뮤텍스 소유권 wrapper 입니다.

 unique_lock은 이동이 가능한 객체이며 복사는 허용되지 않습니다. 

 unique_lock은 BasicLockable 요구사항을 충족합니다.

 만약 mutex가 Lockable 요구사항을 만족한다면 unique_lock도 Lockable 요구사항을 충족합니다.

 만약 TimedLockable 요구 사항을 충족하다면 unique_lock도 충족합니다. 

내 부 구 현

template <class _Mutex>
class unique_lock { // whizzy class with destructor that unlocks mutex
public:
    using mutex_type = _Mutex;

    // CONSTRUCT, ASSIGN, AND DESTROY
    unique_lock() noexcept : _Pmtx(nullptr), _Owns(false) {}

    // 일반적인 mutex객체를 받은 생성자
    explicit unique_lock(_Mutex& _Mtx) : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // construct and lock
        _Pmtx->lock();
        _Owns = true;
    }
    
    // 이미 락이 획득한 mutex에 대한 생성자
    unique_lock(_Mutex& _Mtx, adopt_lock_t)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(true) { // construct and assume already locked
    }
    
    // 지연된 lock을 획득하는 생성자
    // 추후에 사용자는 lock을 원하는 시점에 획득 
    unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept
        : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // construct but don't lock
    }

    // try_lock을 통한 lock을 획득하는 생성자
    unique_lock(_Mutex& _Mtx, try_to_lock_t)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock()) { // construct and try to lock
    }

    // TimedLockable을 만족하는 mutex에 대한 생성
    template <class _Rep, class _Period>
    unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_for(_Rel_time)) { // construct and lock with timeout
    }
    // TimedLockable을 만족하는 mutex에 대한 생성
    template <class _Clock, class _Duration>
    unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_until(_Abs_time)) { // construct and lock with timeout
    }
   // TimedLockable을 만족하는 mutex에 대한 생성
    unique_lock(_Mutex& _Mtx, const xtime* _Abs_time)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // try to lock until _Abs_time
        _Owns = _Pmtx->try_lock_until(_Abs_time);
    }
    //.... 생략
 }

unique_lock은 lock_guard와는 다르게 다양한 생성자를 지원합니다. 

잠금에 대한 전략을 명시하는 태그 상수 

태 그 설 명
defer_lock_t 뮤텍스 객체를 저장한 하고 락을 시도하지 않습니다. 
락의 시점을 지연시켜 사용자가 필요할 때 호출 할 수 있습니다.
try_to_lock_t  참조로 받은 mutex에 락을 시도합니다. 실패하면 바로 리턴합니다 
adopt_lock_t 이미 호출한 스레드가 락을 소유 했고 스레드에서 자체적으로 락을 관리한다고 가정합니다.  

std::shared_lock

설 명

 shared_lock 클래스는 지연된 locking과 timed locking과 lock의 소유권을 이동 할 수 있는 범용 shared_mutex 소유권 

 wrapper 클래스입니다. shared_lock의 잠금은 연결된 shared mutex는 shared 모드로 잠깁니다.

 (exclusive 모드로 잠그려면 std::unique_lock을 사용 해야 합니다.)

 shared_lock 클래스는 이동은 가능하지만 복사는 할 수 없습니다. 

 공유 소유권 모드에서 shared mutex를 대기하려면 std::condition_variable_any를 사용 할 수 있습니다. 

 (std::condition_variable은 std::unique_lock이 필요하고 그래서 unique 소유권 모드에서만 대기 할 수 있습니다. )

 std::unique_lock과 마찬가지로 다양한 lock 전략을 태그로 지정할 수 있으며 시간 값을 지정 할 수 있습니다.

예제 코드

// 해당 예제는 유저를 관리하는 manager 클래스입니다. 
struct UserManager::Impl
{	
	using UserCon = std::unordered_map<uint64_t, std::shared_ptr<User>>;
	UserCon							user_con_;
	mutable std::shared_mutex		user_con_sm_;
	std::shared_ptr<User>			null_user_;
};

UserManager::UserManager()
{
	mimpl_ = std::make_unique<Impl>()
	assert(mimpl_->null_user_ == nullptr);
}


// 유저를 추가할 때는 unique_lock을 통해서 exclusive lock을 획득합니다.
bool 
UserManager::AddUser(uint64_t sn, std::shared_ptr<User> & puser) {
	std::unique_lock<std::shared_mutex> lock(mimpl_->user_con_sm_);
	const auto itr = mimpl_->user_con_.find(sn);

	// 컨테이너에 이미 유저가 존재함
	if (itr != mimpl_->user_con_.end()) {
		// 로그 아웃 처리 및 삭제
		mimpl_->user_con_.erase(sn);
	}

	// 유저 추가 작업
	puser->SetUserSn(sn);
	auto result = mimpl_->user_con_.insert(Impl::UserCon::value_type(sn, puser));
	return result.second;
}

// 유저를 삭제 할 때는 unique_lock을 통해서 exclusive lock을 획득합니다.
bool 
UserManager::DelUser(uint64_t sn) {
	std::unique_lock<std::shared_mutex> lock(mimpl_->user_con_sm_);
	const auto itr = mimpl_->user_con_.find(sn);

	if (itr != mimpl_->user_con_.end()) {
		// 로그 아웃 처리 및 삭제
		mimpl_->user_con_.erase(sn);
		return true;
	}
	return false;
}

// shared_lock을 통해서 shared lock을 획득합니다.
// shared lock끼리는 동시에 수행이 가능합니다.
const bool
UserManager::FindUser(uint64_t sn) const {
	std::shared_lock<std::shared_mutex> lock(mimpl_->user_con_sm_);
	const auto itr = mimpl_->user_con_.find(sn);

	if (itr != mimpl_->user_con_.end()) {
		return true;
	}
	return false;
}

const bool 
UserManager::FindUserFunc(uint64_t sn, UserCallBack callback) const {
	std::shared_lock<std::shared_mutex> lock(mimpl_->user_con_sm_);
	const auto itr = mimpl_->user_con_.find(sn);
	if (itr != mimpl_->user_con_.end()) {
		callback(itr->second);
		return true;
	}
	return false;
}

// shared_lock을 통해서 shared lock을 획득합니다.
// shared lock끼리는 동시에 수행이 가능합니다.
size_t
UserManager::UserCount() {
	std::shared_lock<std::shared_mutex> lock(mimpl_->user_con_sm_);
	return mimpl_->user_con_.size();
}

 위의 예제를 보게 되면 유저가 추가나 삭제가 될 때는 container가 수정되기 때문에 unique_lock을 사용해서 

 exclusive mode로 락을 획득합니다. 

 유저을 검색하거나 정보를 얻을 때는 shared_lock을 사용해서 shared mode로 락을 획득합니다. 

 만약 유저의 추가, 삭제는 빈번하지 않다면 shared mode는 동시에 수행될 가능성이 높아집니다.  

동시에 여러개의 lock을 획득하는 방법

 c++ 11에서는 여러 개의 mutex객체를 한꺼번에 점유할 수 있도록 가변인자 템플릿 함수인

 std::lock()과 std::try_lock()을 지원합니다.

std::lock()

 std::lock()은 deadlock 회피 알고리즘을 사용해서 입력된 인자를 순서대로 획득합니다. 

 만약에 lock또는 unlock 호출로 예외가 발생하면 잠긴 객체에 대해서 unlock이 순차적으로 호출됩니다. 

 std::lock()의 인자 순서를 모든 스레드에 지켜줘야지 deadlock이 방지 가능합니다. 

std::try_lock()

 std::try_lock()은 입력된 인자를 순서대로 try_lock()을 호출하고 모든 mutex가 성공하면 -1을 리턴하고 

 실패하면 0을 기준으로 인자값 순서번호를 리턴합니다. 

예제코드 

struct bank_account {
    explicit bank_account(int balance) : balance(balance) {}
    int balance;
    std::mutex m;
};
 
void transfer(bank_account &from, bank_account &to, int amount)
{
    // lock both mutexes without deadlock
    std::lock(from.m, to.m);
    // make sure both already-locked mutexes are unlocked at the end of scope
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
 
// equivalent approach:
//    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
//    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
//    std::lock(lock1, lock2);
 
    from.balance -= amount;
    to.balance += amount;
}
 
int main()
{
    bank_account my_account(100);
    bank_account your_account(50);
 
    std::thread t1(transfer, std::ref(my_account), std::ref(your_account), 10);
    std::thread t2(transfer, std::ref(your_account), std::ref(my_account), 5);
 
    t1.join();
    t2.join();
}

 위의 예제를 보면 2개의 은행계좌의 lock을 동시에 획득할때 std::lock을 사용해서 데드락을 회피합니다. 

 이미 획득한 mutex를 std::lock_guard를 관리하기 위해서 전달하는데 이미 획득된 락을 전달 받기 위해서 

 std::adopt_lock 태그를 사용해서 lock 획득 전략을 전달합니다. 

 또는 주석처럼 std::defer_lock 태그를 사용해서 지연된 std::unique_lock 객체를 생성한 후 std::lock에 전달해서

 락을 획득 할 수도 있습니다. 

'c++ > modern c++' 카테고리의 다른 글

[c++20] Concept  (0) 2022.06.10
[c++] condition variable(조건 변수)  (0) 2020.07.10
[c++] std::mutex  (0) 2020.03.28
[c++17] string_view  (0) 2020.03.21
[c++] constexpr 키워드  (0) 2020.03.19

+ Recent posts