ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C++ 멀티스레드 동기화, 이벤트 사용법
    C++/C++ 멀티스레드 2022. 10. 12. 20:38
    728x90

    저번 시간까지 멀티스레드 환경에서 lock을 기다릴 때 사용할 수 있는 방법으로

    두 가지를 알아보았습니다.

    1. spinlock(무한정대기)

    2. sleep(랜덤한 시간동안 대기)

     

    이번에는 3번째 방법인 이벤트를 사용하는 방법에 대해서 알아보도록 하겠습니다.


    저번에 들었던 헬스장 예시를 다시 들어볼게요.

    사용하고 싶은 헬스기구를 다른 사람이 사용하고 있다. 나는 헬스기구에 자리가 나면 사용할 예정이다.

    1. spinlock은 기구 뒤에서서 무작정 기다리는 방법이고

    2. sleep은 일정시간동안 다른 곳에서 기다리다가 자리가 났는지 확인하는 방법입니다.

     

    세 번째방법인 이벤트 방법은 이런겁니다.

    어이~ 김비서! 나 밖에서 쉬고있을테니까 여기 자리나면 나한테 알려줘!

     

    이 개념을 C++에서 나타내면 다음과 같이 나타낼 수 있습니다.

     

     

    이 때 "자리나면 알려줘~"를 구현하기 위해 C++을 비롯한 많은 언어에서 사용하는 개념이 Event입니다.

    특정 Event를 발생시켜서 알려주는 것입니다.

     

    이러한 Event는 lock과 관련된 상황이 아니더라도, 다양한 상황에서 범용적으로 사용할 수 있습니다.

    따라서 오늘은 lock과 관련된 상황이 아니라, 조금 더 일반적인 상황을 가정하고 설명드려 보겠습니다.

     


    이벤트 생성

    C++에서 이벤트를 생성하는 방법은 다음과 같습니다.

    #include <Windows.h>
    
    int main()
    {
    	HANDLE handle = ::CreateEvent(NULL, false, false, NULL);
    }

    windows.h를 include하고

    CreateEvent로 이벤트를 생성합니다. 각 인자의 의미는 다음과 같습니다.

     

    • 첫 번째 : 보안과 관련된 설정입니다.
    • 두 번째 : bManualReset이라는 파라미터입니다.
      • true : 이벤트의 시그널이 켜졌을때, 다시 시그널을 끄기 위해서는 수동(manual)으로 시그널을 꺼야합니다.
      • false : 이벤트의 시그널이 켜졌을때, 커널에서 자동으로 시그널을 꺼줍니다.
    • 세 번째 : bInitialState라는 파라미터입니다.
      • 이벤트의 초기상태를 지정할 수 있습니다.
      • true : 시그널이 켜진 상태가 event의 초기상태가 됩니다.
      • false : 시그널이 꺼진 상태가 event의 초기상태가 됩니다.
    • 네 번째 : 이벤트의 이름입니다.

    이렇게 이벤트를 만들었으면, 언제 이벤트를 발생시킬지 결정하고, 코드를 작성하면 됩니다.

     

    먼저 이벤트를 사용하지 않은 오늘의 예제코드를 보겠습니다.

     

    #include <iostream>
    #include <thread>
    #include <atomic>
    #include <mutex>
    #include <Windows.h>
    
    mutex m;
    queue<int> q;
    HANDLE handle;
    
    void Producer()
    {
    	while (true)
    	{		
    		{
    			unique_lock<mutex> lock(m);
    			q.push(100);
    		}
    
    		this_thread::sleep_for(1000ms);
    	}
    }
    
    void Consumer()
    {
    	while (true)
    	{
    		cout << "waiting..." << endl;
    		{
    			unique_lock<mutex> lock(m);
    			if (q.empty() == false)
    			{
    				int data = q.front();
    				q.pop();
    				cout << data << endl;
    			}
    		}
    	}
    }
    
    int main()
    {
    	std::thread t1(Producer);
    	std::thread t2(Consumer);
    
    	t1.join();
    	t2.join();
    }

    코드에 대해 간단히 설명해보겠습니다.

    • 스레드 t1이 실행하는 Producer는 1초에 한 개씩 큐에 데이터를 추가합니다.
    • 스레드 t2가 실행하는 Consumer는 큐가 비어있지 않으면, 데이터를 뺍니다.

    위의 코드를 실행해보면, Waiting이 수없이 출력되는 것을 볼 수 있습니다.

    그 말은, Consumer가 계속해서, 큐가 비어있는지 아닌지를 검사하며 while문을 돌고있는 것입니다.

     

    하지만, 이러한 방법은 매우 비효율적입니다.

    특히나, Producer가 데이터를 삽입하는 주기가 길어질수록 더 비효율적입니다.

    Queue는 비어있고, 한참뒤에 데이터가 들어오는데, 혹은 언제 데이터가 들어올지 모르는데

    계속 큐에 데이터가 있는지를 검사하는 것은 자원의 낭비죠.

     

    이럴 때 이벤트를 사용하는 겁니다.

     

    Event : Producer가 데이터를 넣으면 시그널(이벤트)을 보내서 Consumer에게 알려준다!

     

    그러면 Consumer는 while문을 계속 돌며 검사할 필요없이, Event를 기다리고 있으면 되는거죠.

    Event를 사용해서 코드를 수정해보겠습니다.

     

    #include <iostream>
    #include <thread>
    #include <atomic>
    #include <mutex>
    #include <Windows.h>
    
    mutex m;
    queue<int> q;
    HANDLE handle;
    
    void Producer()
    {
    	while (true)
    	{		
    		{
    			unique_lock<mutex> lock(m);
    			q.push(100);
    		}
    		SetEvent(handle);
    
    		this_thread::sleep_for(1000ms);
    	}
    }
    
    void Consumer()
    {
    	while (true)
    	{
    		cout << "waiting..." << endl;
    		WaitForSingleObject(handle, INFINITE);
    		{
    			unique_lock<mutex> lock(m);
    			if (q.empty() == false)
    			{
    				int data = q.front();
    				q.pop();
    				cout << data << endl;
    			}
    		}
    	}
    }
    
    int main()
    {
    	handle = ::CreateEvent(NULL, false, false, NULL);
    
    	std::thread t1(Producer);
    	std::thread t2(Consumer);
    
    	t1.join();
    	t2.join();
    
    	CloseHandle(handle);
    }
    • setEvent : handle의 이벤트를 발생시켜줍니다.
    • WaitForSingleObject : handle의 이벤트가 발생할 때까지 대기합니다. 두번째 인자를 INFINITE로 줬기 때문에, 시간제한없이 무제한적으로 대기합니다.
    • CloseHandle : handle을 다 썼으면, Close해줍니다.

     

    이 코드를 실행하면, waiting...이라는 글자가 한 번씩만 출력되는것을 확인할 수 있습니다.

     


    참고사항과 이벤트에 대한 특징을 몇가지 적어보겠습니다.

    • createEvent를 할 때 두번째 인자(bManualReset)을 true로 설정하면, WaitForSingleObject밑에 ResetEvent를 해서, Event를 다시 초기상태로 reset해줘야 합니다.
    • false로 설정하면 autoReset방식으로 작동해서, 이벤트가 발생하자마자, 다시 초기상태로 돌아갑니다

    그리고, 이벤트는 커널오브젝트입니다.

    커널오브젝트는 간단히 말해서, 커널레벨에서 관리하는 객체라는 것입니다.

    그 반대로는 유저레벨에서 관리하는 오브젝트가 있습니다.

     

    가장 큰 차이점은 유저레벨에서 관리하는 오브젝트는, 각 프로그램마다 독립적으로 존재하고, 해당 프로그램에서만 사용할 수 있습니다.(ex : spinlock)

    하지만, 커널레벨에서 관리하는 오브젝트는, 모든 프로그램이 접근할 수 있습니다.

     

    이벤트도 커널오브젝트이므로, 다른 프로그램끼리의 동기화가 필요할 경우에 이벤트를 활용할 수 있습니다.

    이러한 장점이 있지만, 커널은 이벤트를 관리하는것 말고도 할 일이 아주 많습니다.

     

    따라서, 이벤트를 마구잡이로 발생시키는 것은 귀중한 커널의 자원을 낭비하는 것이 되므로,

    자주 발생하지는 않는 이벤트 혹은 프로그램간의 동기화가 필요한 경우에 이벤트를 등록하여 이용하면 좋을 것입니다.

     

    이상으로 포스팅을 마치겠습니다.

    (저는 공부하는 입장으로, 복습차원에서 작성하는 포스팅입니다. 틀린것이 있으면 주저하지 말고 지적부탁드립니다.)

    728x90

    'C++ > C++ 멀티스레드' 카테고리의 다른 글

    C++ future(promise, async, packaged_task)  (2) 2022.10.15
    C++ Conditional Variable(조건변수)  (0) 2022.10.14
    C++, Sleep을 이용한 Lock구현  (0) 2022.10.10
    C++ SpinLock구현  (0) 2022.10.09
    멀티스레드 Lock구현  (0) 2022.10.09

    댓글

Designed by Tistory.