메뉴 닫기

C++ mutex와 lock 사용법 (1)

이번 글은 mutexlockwrapper 클래스(boost_guardscoped_lock)을 소개합니다.

좀 더 복잡한 lock wrapper 클래스인 unique_lock은 다음 편에 소개하려 합니다.


mutex와 lock

MUTual EXclusion에서 따온 말로, 한국어로 번역하면 상호 배제입니다. 그런데 이렇게 쓰면 잘 와닿지가 않습니다. 일반적으로 mutex를 설명하기 위해 자물쇠 or 화장실 잠금장치 등을 예로 사용합니다. 자물쇠는 열거나(lock), 잠그는(unlock) 동작을 수행할 수 있습니다. 그래서 mutex 클래스에는 lock, try_lock, unlock이라는 locking에 대한 함수가 존재합니다.

①사람(쓰레드)이 특정 영역(화장실)에 들어가서 ②mutex(자물쇠 or 잠금장치)를 lock을 하면 ③아무도 들어갈 수 없고 사람들은 대기합니다. ④mutex(자물쇠 or 잠금장치)가 unlock이 되어 사람이 나오면, 다음 사람이 사용할 수 있습니다.

화장실에 들어가더라도(mutex를 선언만 하고) 잠금장치를 잠그지 않으면(lock을 안하면), 누구든지 들어올 수 있겠지요? 끔찍하네요…


std::mutex

쓰레드는 lock (또는 try_lock) 호출이 성공한 시점부터 unlock을 호출 할 때까지 mutex를 소유합니다.

void lock() mutex를 잠급니다. 다른 스레드가 mutex를 소유하고 있다면, lock이 풀릴 때까지 블로킹 합니다.
bool try_lock() mutex 잠금을 시도합니다. 다른 스레드가 mutex를 소유하고 있다면 false를 return하고, 그렇지 않으면 mutex를 잠그고 소유하게 됩니다.
주로 mutex를 소유할 수 없을 때 다른 액션을 취하고 싶을 경우 사용합니다.
void unlock() mutex 잠금을 풉니다.

사용예제는 아래와 같습니다.

#include <thread>
#include <mutex>
#include <iostream>

void func()
{
    static std::mutex m;

    // Example1
    m.lock();
    /* 작업 중...*/
    m.unlock();
    
    
    // Example2
    if(m.try_lock())
    {
        /* 작업 중...*/
        m.unlock();
    }
    else
    {
        // lock 실패... 안녕.. 
    }
}

std::lock_guard<std::mutex>  (since C++11)

또는

std::scoped_lock  (since C++17)

둘 다 mutex wrapper 클래스입니다. lock_guard 또는 scoped_lock 객체가 생성될 때, 주어진 mutex의 소유권을 얻기 위해 잠금을 시도합니다. 객체의 생성 범위(Scope)를 벗어나면 소멸자가 호출되면서 Mutex 잠금이 해제됩니다. lock ~ unlock을 직접 하는 것보다 편리할 뿐 아니라 코드도 깔끔해지고, unlock을 깜박할 가능성을 원천 차단시켜 줍니다.

둘 간의 차이는 scoped_lock이 나중에 나왔다…? 정도로 보이고, 역할은 거의 비슷한 것 같은데요. 확실한 차이점은 scoped_lock은 다중잠금이 가능하다는 점입니다. scoped_lock 객체가 소멸될 때는 mutex 잠금이 역방향으로 해제됩니다.

	std::mutex m1, m2, m3;
	std::scoped_lock lock(m1, m2, m3);	// 가능
	std::lock_guard<std::mutex> guard(m1); // 1개만 가능

예제 코드

1) Mutex를 사용하지 않을 때

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

void func(std::string message)
{	
	std::this_thread::sleep_for(std::chrono::seconds(1));
	std::cout << "func..." << message << std::endl;
}

int main()
{
	std::vector<std::thread> threads;
	threads.emplace_back(func, "test1");
	threads.emplace_back(func, "test2");
	threads.emplace_back(func, "test3");
	threads.emplace_back(func, "test4");
	threads.emplace_back(func, "test5");

	for (auto &thread : threads) 
		thread.join();

	return 0;
}


결과는 엉망진창입니다.2) Mutex – lock & unlock 사용

의도했던 순서대로 나옵니다.

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

void func(std::string message)
{
	static std::mutex m;
	m.lock();

	std::this_thread::sleep_for(std::chrono::seconds(1));
	std::cout << "func..." << message << std::endl;

	m.unlock();
}

int main()
{
	std::vector<std::thread> threads;
	threads.emplace_back(func, "test1");
	threads.emplace_back(func, "test2");
	threads.emplace_back(func, "test3");
	threads.emplace_back(func, "test4");
	threads.emplace_back(func, "test5");

	for (auto &thread : threads) 
		thread.join();

	return 0;
}


순서대로 잘 나옵니다.3) Mutex – try_lock & unlock 사용

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

void func(std::string message)
{
	static std::mutex m;
	if (m.try_lock())
	{
		std::this_thread::sleep_for(std::chrono::seconds(1));
		std::cout << "func..." << message << std::endl;
		m.unlock();
	}
}

int main()
{
	std::vector<std::thread> threads;
	threads.emplace_back(func, "test1");
	threads.emplace_back(func, "test2");
	threads.emplace_back(func, "test3");
	threads.emplace_back(func, "test4");
	threads.emplace_back(func, "test5");

	for (auto &thread : threads) 
		thread.join();

	return 0;
}


첫번째 thread만 정상적으로 수행됩니다.4) Mutex – scoped_lock 사용

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

void func(std::string message)
{
	static std::mutex m;
	std::scoped_lock lock(m);
	//std::lock_guard<std::mutex> guard(m); // scoped_lock 대신 사용해도 같은 결과

	std::this_thread::sleep_for(std::chrono::seconds(1));
	std::cout << "func..." << message << std::endl;
}

int main()
{
	std::vector<std::thread> threads;
	threads.emplace_back(func, "test1");
	threads.emplace_back(func, "test2");
	threads.emplace_back(func, "test3");
	threads.emplace_back(func, "test4");
	threads.emplace_back(func, "test5");

	for (auto &thread : threads) 
		thread.join();

	return 0;
}


답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다