반응형

wonder + goal

네 단어뜻 그대로 최고의 골, 경이로운 골입니다.

누구나 넣을 수 있는 골, 주워먹기 골 아니구요,

말도 안되는 상황에서 만들어 넣는 골.

남들은 절대 못넣는 골을 뜻해요.

 

 

 

반응형
반응형

경우의 수는 이것보다 훨씬 많을 수 있지만 몇개만 추려보았고

크래쉬기 나지 않고 Lock free를 시도 하려는 중간 과정이라 보면 되겠다

 

스택이기 때문에 끝에서 추가되고 삭제 된다는것과

다른 스레드에 의해서 현재 동작하고있는것이 우선순위가 뒤로 밀리고 다시 원래 스레드로 돌아온다 할지라도

스택의 자료구조상 가장 위에 있는 부분에서 추가 또는 삭제가 된다는 부분을 생각하며 보면된다

 

 

일반 Stack 코드를 보면 다음과 같은 형태일 것이다 아직 pop은 없음

template<typename T>
class LockFreeStack
{
	struct Node 
	{
		Node(const T& value) : data(value) {}
		T data;
		Node* next;
	};

public :

	//node 추가는 head 바로 다음에 새로운 노드가 추가 되는 방식이다
	void push(const T& value)
	{
		Node* node = new Node(value);
		node->next = _head;
		_head = node;
	}


private :

	//head 가 다음 노드를 가르키는 방식
	atomic<Node*> _head;

};

새 노드 만드는 순서

 

1. 새 노드를 만든다
2. 새 노드의 next = nead
3. head = 새노드

 

//node 추가는 head 바로 다음에 새로운 노드가 추가 되는 방식이다
void push(const T& value)
{
이 라인은 new Node 가 공용의 힙에 생성하고 유일한 메모리에 생성하는 것이고 포인터로 의도적으로 어떤 조작을 하지 않는 이상 해당 주소를 할당 받는건 유니크하다 => thread safe

 

node 는 스택 이라 thread safe 
즉 이 한줄은 thread safe 하다
Node* node = new Node(value);

lock free 로 만든다는 것은 mutex 를 쓰지 않고 동기화 처리를 가능하게 만드는 것인데(단  mutex 를 사용하지 않고)

그냥 구현한 stack 을 스레드로 사용할 경우 이 라인이 문제가 되는데

_head 는 다른 스레드로 push , pop 을 돌릴때 값이 바뀔 수 있는 공유 변수가 되기 때문에 이 부분을 동기화 처리해줘야한다 

 

그런데 명령어가 두줄이기도 하고 __asm 이 어떻게 처리 되는지 확인이 되진 않은 상태이지만

__asm 을 보진 않더라도 동기화가 필요해보인다는걸 알 수있는 부분이다

node->next = _head;

이 사이에서 다른 스레드가 _head 를 바꾼다면? 문제가 생김
_head = node;


}

 

 

 

 

 

 

여기까진 Push 로직 순서대로 싱글 스레드라 가정하고 추가 하면 이렇게 된다 (추가를 몇번 한 것)

 

 

 

 

우선 위 코드에서 tryPop 을 싱글 스레드라 가정하고 코드를 추가하고

bool tryPop(T& value)
	{
		Node* oldHead = _head;

		while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false)			//atomic 으로 한번에 처리가 됨
		{
			// oldHead = _head;
		}
		if (oldHead == nullptr)
		{
			return false;
		}

		value = oldHead->data;

		//그냥 삭제 해버리면 기존에 참조하고 있던 포인터가 오염되어 기존 삭제된 포인터 사용시 크래쉬 발생
		//delete oldHead;	//나중에 해결
		return true;
	}

삭제에 대해 따라가면 아래 그림 처럼 나온다

trypop 을 두번 한 것 = 검은색 화살표 -> 붉은색 화살표 -> 녹색 화살표 순

노드를 실제 delete 하는 부분은 우선 논외로 한다

 

 

 

 

 

 

 

 

Lockfree Stack - "Test code" 1

아직 불안전한 상태 이지만 이걸로 우선 흐름을 살펴봅니다

 

push 와 pop 로직을 lockfree push 로 만든다

 

다음과 같이 push, tryPop 함수가 된다

	void push(const T& value)
	{
		Node* node = new Node(value);		
		node->next = _head;			
		while (_head.compare_exchange_weak(node->next, node) == false)			//atomic 으로 한번에 처리됨
		{
			//node->next = _head; //없어도 push 로직상 맞아 떨어짐
		}
	}
    
    bool tryPop(T& value)
	{
		Node* oldHead = _head;

		while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false)			//atomic 으로 한번에 처리가 됨
		{
			// oldHead = _head;
		}
		if (oldHead == nullptr)
		{
			return false;
		}
		value = oldHead->data;
		return true;
	}

 

 

 

 

경우의 수는 이것보다 훨씬 많을 수 있지만 몇개만 추려보았다

 

경우의 수1

 

주의사항 그림의 왼쪽 push 나 tryPop 또는 pop 글자는 무시하고 보는것이 좋다
(정확하게 쓴 내용은 아님 copy & paste 하다보니 미스가 있을 수 있음)

 

만약! push를 하던 도중 

Node* node = new Node(value);
node->next = _head;

까지만 하고 t2 스레드로 넘어가 pop 을 시도하려는 상황을 보자

아래 그림은 push 두줄 실행한 이후다

 

 

 

그다음 tryPop 쓰레드가 우선순위를 얻어 tryPop 을 모두 다 처리하게 되면 다음와 같을 것이다

아직 head 에 어떤 변화도 가하지 않았음

while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false)

tryPop 의 while 문에 compare_exchange_weak 함수의 리턴이 true 를 리턴하기 때문에 빠져나오고 종료됨

여기까지는 이렇게 된다

 

 

그다음 t1 스레드로 되돌아가 다시 push 를 이어서 하면

while (_head.compare_exchange_weak(node->next, node) == false)

compare_exchange_weak 리턴은 false 임으로 다시 한번 push 의 compare...를 시도 하게 되는데 다시 한번 시도하면

while (_head.compare_exchange_weak(node->next, node) == false)

다음 그림 처럼 된다

노란 색 박스는 유효한 노드, 그리고 compare_exchange_weak 은 true 를 리턴하여 종료 되고 push 는 끝난다

 

최초 3개의 노드에서 시작해서 중간에 tryPop 스레드가 먼저 실행되어 당시의 끝 노드가 제거 된다음
다시 push 스레드가
돌아서 노드 하나를 추가되고 끝나게 된다 결과는 3개로 끝난다
생성됐다 제거된 노드 변화 개수를 보면 : 3->4->3

 

 

 

 

경우의 수2 

 

경우의수 1 에서 반대 상황의 경우를 보면

tryPop 먼저 할때 

Node* oldHead = _head; 까지 진행 된 다음

t1 스레드로 전환되서 push 를 하는 상황이 됐다

 

 

이상황에서 push를 전부 진행된다면 다음과 같이 된다

 

Node* node = new Node(value);
node->next = _head;
while (_head.compare_exchange_weak(node->next, node) == false)  while 문도 break 됨

 

여기까지 해서 push 가 완료되고

 

 

다시 tryPop 으로 되돌아 오면..

이 상태로 복귀 되고 

 

TryPop 구문 중..

while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false)

여기부터 다시 실행이 되면 파란선 즉 다음과 같게 된다
compare_exchange_weak 리턴은 false 가 되고

 

이렇게 삭제처리가 되지 않고 한번 미뤄지게 된다 아래는 선만 정리

 

 

 

compare_exchange_weak 이 false 를 리턴해서 while 계속 돌아가게 되니 한번더 실행해보면 

TryPop 구문 중..

while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false)

head 와 oldHead 가 같음으로 다음과 같이 된다 (녹색)

compare_exchange_weak 은 true 를 리턴함으로 while 루프가 끝나게 된다

 

tryPop을 하다가 중간에 push 가 들어와서 원소가 추가 됐다가 다시 tryPop나머지 구문이 실행되면서 원래도 돌아온다

처음 노드는 4개였고 tryPop 부터 시작해서 push 가 끼어들어 다시 원래의 노드4개가 되었다

최초에 4번째 노드를 삭제 하려 했지만 중간에 push 가 되면서 5번재 노드가 추가 됐고
마지막으로 추가된 5번재 노드가 삭제되는 형태로
스택에서 최상단의 것이 삭제 되는 루틴과 동일하지만 처음 네번째 것을 삭제하려 했던 대상을 삭제하는 형태는 아닐 수 있다
4->5->4

 

 

 

경우의 수3

 

아래 순서는 프로그램 라인이 하나씩 실행 되면서 스레드가 전환될때의 상황을 가정한것이다

Push 부터 시작

compare_exchange_weak 리턴이 false 임으로 한번 더 실행해보면 

 

while (oldHead && _head.compare_exchange_weak(oldHead, oldHead->next) == false)

이렇게 되고 compare_exchange_weak 은 true 를 리턴해 while 문은 종료 되어 위의 결과 처럼 된다

 

 

처음 시작은 push 로 하고  최초에 3개이던 노드가 push, tryPop 스레드가 왔다갔다 가 push 가 우선순위로 올라와
4개가 됐다가 중간에 tryPop 이 실행이 반복 되면서 원래의 3개로 된것을 알 수 있다
3->4->3

 

 

 

경우의 수4 

 

 

위 PUSH 는 아직 실행 되지 않고 선만 정리 한것 

 

 

_head.compare_exchange_weak 리턴이 false 임으로 한번 더 실행하면

즉 원소를 추가 할때 head 가 추가 되는 노드의 다음 노드를 head 가 가르키게끔 처리를 해야 push 로직에서 노드 추가가 완료 되기 때문에 이런 처리가 있는 것

 

_head.compare_exchange_weak 라 true 를 리턴하면서 끝난다 마지막 유효한 노드는 3개가 된다

 

결과적으로 아래와 같이 된다

trypop 부터 시작해 push 와 번갈아가면서 한번씩 실행하고 tryPop 이 먼저 원소를 제거 하는 while 에 들어가게 된다
다시 push 로 복귀 하면서 마지막 추가된 노드와 head 와의 연결을 해주면서 중간 노드는 (3번재) 노드는 삭제와 비슷한 처리가 된다 그래서 최종 유효한 노드는 3개가 된다
3->4->3

 

여기서도 나머지 노드에 대한 제거 처리는 우선 고려하지 않음

 

 

 

 

4가지 케이스 모두다  push 와 tryPop 을 어떻게 번걸아 갈지 순서는 어떻게 될지

예측하긴 어렵지만 노드의 변화 과정 개수는

n-> n+1 ->n  개의 꼴을 보이는 것을 알 수 있다

n개로 시작해서 n 개로 끝난다

 

stack 의 룰을 따라 가려고 하긴 하지만 중간에 스레드 특성상 중간 노드가 제거 될때도 있고

처음 지우려고 했던 노드가 지워지지 않을 수도 있다

하지만 노드 개수는 n-> n+1 -> n 개의 꼴을 보인다

 

  1. 중요한 특징 한가지 어떤 경우던 oldHead 는 마지막에 삭제할 노드를 가리키고 있다는 것
  2. 스레드간의 간섭이 심해서 꼭 삭제 하려던 그 당시의 마지막 대상이 삭제 되는게 아니고 다른 스레드에 의해 추가된 마지막것이 삭제 되는 경우도 있다는 것

 

노드가 중간에 삭제가 될 수 있고 삭제 경로에 대해 애매한 부분들이 있기 때문에

노드 삭제에 대해선 shared_ptr 을 고려해 볼 수도 있겠지만

 

이 코드에서 보이듯 shared_ptr 은 lock free 가 아님으로 LockFree - Stack 를 만드는 과정에서

shaed_ptr 을 사용한다면 lock free 가 아닐 것이다.. 

 

반응형

'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글

Shared_ptr 와 atomic  (0) 2022.09.22
LockFree - Stack (2) - 삭제 처리  (0) 2022.09.21
LockBase Queue  (0) 2022.09.14
Lock base Stack  (0) 2022.09.14
TLS(Thread Local Storage) : thread_local  (0) 2022.09.14
반응형

 

RPC (Remote Procedure Call) 는 로컬에서 호출되지만 (호출하는 머신과는) 다른 머신에서 원격 실행되는 함수를 말합니다.

RPC 함수는 매우 유용하게 사용될 수 있으며, 네트워크 연결을 통해 클라이언트와 서버 사이에 메시지를 전송할 수 있습니다.

이 기능의 주요 용도는 속성상 장식이나 휘발성인 비신뢰성 게임플레이 이벤트를 위한 것입니다. 이는 사운드 재생, 파티클 스폰, 액터의 핵심적인 기능과는 무관한 일시적 효과와 같은 작업을 하는 이벤트를 포함합니다. 기존에 이러한 유형의 이벤트는 종종 액터 프로퍼티를 통해 리플리케이트되고는 했습니다.

RPC 사용시 오너십 작동 방식 을 이해해 두는 것이 중요합니다. 대부분의 RPC 실행 장소를 결정하기 때문입니다.

RPC 사용하기

함수를 RPC 로 선언하려면 UFUNCTION 선언에 Server, Client, NetMulticast 키워드를 붙여주기만 하면 됩니다.

예를 들어 함수를 서버에서 호출되지만 클라이언트에서 실행되는 RPC 로 선언하려면, 이렇게 합니다:

UFUNCTION( Client )
void ClientRPCFunction();

함수를 클라이언트에서 호출되지만 서버에서 실행되는 RPC 로 선언하는 것은 Server 키워드를 사용한다는 것 빼고는 매우 비슷합니다:

UFUNCTION( Server )
void ServerRPCFunction();

Multicast 라 불리는 특수 유형 RPC 함수가 하나 더 있습니다. Multicast RPC 는 서버에서 호출된 다음 서버는 물론 현재 연결된 모든 클라이언트에서도 실행되도록 고안된 것입니다. 멀티캐스트 함수를 선언하려면 그냥 NetMulticast 키워드를 사용하면 됩니다:

UFUNCTION( NetMulticast )
void MulticastRPCFunction();

멀티캐스트 RPC 는 클라이언트에서도 호출 가능하지만, 이 경우 로컬에서만 실행됩니다.

간단 팁

함수 초반에 Client, Server, Multicast 키워드를 앞쪽에 어떻게 붙였는지 보세요. 저희 내부적으로 합의한 규칙으로, 이 함수를 사용하는 프로그래머들에게 이 함수가 각각 클라이언트, 서버, 모든 클라이언트에서 호출된다는 것을 알리기 위한 것입니다.

멀티플레이어 세션 도중 이 함수가 어느 머신에서 호출되는지 한 눈에 알아볼 수 있기에 매우 유용합니다.

요건 및 주의사항

RPC 의 정상 작동을 위해 충족시켜야 하는 요건이 몇 가지 있습니다:

  1. Actor 에서 호출되어야 합니다.
  2. Actor 는 빈드시 replicated 여야 합니다.
  3. 서버에서 호출되고 클라이언트에서 실행되는 RPC 의 경우, 해당 Actor 를 실제 소유하고 있는 클라이언트에서만 함수가 실행됩니다. (client)
  4. 클라이언트에서 호출되고 서버에서 실행되는 RPC 의 경우, 클라이언트는 RPC 가 호출되는 Actor 를 소유해야 합니다.
  5. Multicast RPC 는 예외입니다:
    • 서버에서 호출되는 경우, 서버에서는 로컬에서 실행될 뿐만 아니라 현재 연결된 모든 클라이언트에서도 실행됩니다.
    • 클라이언트에서 호출되는 경우, 로컬에서만 실행되며, 서버에서는 실행되지 않습니다.
    • 현재 멀티캐스트 이벤트에 대해 단순한 스로틀 조절 메카니즘이 있습니다. 멀티캐스트 함수는 주어진 액터의 네트워크 업데이트 기간동안 두 번 이상 리플리케이트되지 않습니다. 장기적으로 크로스 채널 트래픽 관리 및 스로틀 조절 지원을 개선시킬 계획입니다.

 

Autority

Role_Autority : 서버상에 존재하는 복제된 액터, 서버만이 액터의 변수와 상태에 대한 권한을 갖고 있음,
Role_AutonomousProxy : 소유한 클라이언트에 존재하는 액터의 버전으로 간주함, 클라와 서버간의 직접적인 RPC 송수신 등의 작업을 수행함
Role_SimulatedProsy : 다른 모든 클라이언트에 있는 액터 버전으로 간주한다, 순전히 시뮬레이터이며 액터 상태에 대한 아무런 권한이 없음(조정할수 없는 다른 캐릭터들)의 역할

 

(멀티 플레이 접속시) 서버에선 플레이어 컨트롤러 2개(플레이어 수만큼), 클라이언트에선 플레이어 컨트롤러가 1개이다

 

아래는 리슨 서버로 실행하여 서버 pc 에서 본 화면

 

  • 플레이어 컨트롤러는 소유 클라이언트에서 생성되며 플레이어 컨트롤러는 서버와의 모든 통신이 이뤄지는 채널이다
    (이것이 비 소유 클라이언트와의 차이임)
  • (멀티 플레이 접속시) 서버에선 플레이어 컨트롤러 2개(플레이어 수만큼)
    클라이언트에선 플레이어 컨트롤러가 1개이다

    각 컨트롤러는 서버와 소유 클라이언트 간에 복제된다, 같은 맥락으로 게임 모드는 네트워크 권한에서만 생성된다(이경우는 대게 서버임),  클라이언트와 서버 간의 게임 플레이어 정보를 전달하는 방법은 GameState 와 PlayerState 오브젝트를 통해 이뤄지는데 이 두 오브젝트도 연결된 모든 클라이언트에 복제된다

 

 

서버와 클라이언트 통신 할때 GameState 와 PlayerState 오브젝트를 통해서 정보 전달이 이뤄진다

그리고 이 두 오브젝트는 연결된 모든 클라이언트에 복제된다

 

Player state : 플레이어 상태 : 모든 플레이어는 서버(혹은 스탠드 얼론 게임)에 각자의 PlayerState를 가집니다. 

PlayerState는 모든 클라이언트에게 리플리케이트 되며

플레이어의 네트워크와 관련 정보를 지니게 된다, 플레이어 이름, 점수 등등

 

 

다음 표는 호출 액터의 소유권(가장 왼쪽 열)에 따라 주어진 유형의 RPC 실행 위치를 나타냅니다.

서버에서 호출된 RPC

액터 소유권리플리케이트 안됨NetMulticast서버클라이언트

 

RPC 는 호출 PC 에서 액터의 소유권을 보고 호출되는 대상이 1차적으로 정해지고, 그다음 프래그를 보고 어떻게 호출할지 2차적으로 결정 된다

 

Multicast 는 소유권과 관계 없이 동작하는 특성을 갖음

리플리케이트 안됨 = 리플리케이트 없는 상태일때를 말함

서버소유 액터 일때 NetMulticast 도 서버와 모든 클라이언트에서 실행이다

 

 

 

 

클라이언트에서 호출된 RPC

액터 소유권리플리케이트 안됨NetMulticast서버클라이언트

 

 


위 표를 기준으로 Multicast 예를 들자면

[호출 PC 가 서버에서 호출인 경우 ]

Multicast RPC 는 서버에서 호출되는 경우서버와 현재 연결된 모든 클라이언트에서 실행된다

 

[호출 PC 가 클라이언트에서 호출하는 경우 ]

클라이언트에서 호출되는 경우 소유권에 관계 없이 로컬에서만 실행되며 서버에서 실행되지 않는다

 

 

 

신뢰성

기본적으로 RPC 는 비신뢰성입니다. RPC 호출이 원격 머신에서 확실히 실행되도록 하기 위해서는 Reliable 키워드를 붙이면 됩니다:

UFUNCTION( Client, Reliable )
void ClientRPCFunction();

블루프린트

RPC 로 마킹된 함수는 블루프린트에서 호출해도 리플리케이트됩니다. 이 경우 마치 C++ 에서 호출된 것과 같은 규칙을 따릅니다. 현재 블루프린트에서 동적으로 함수를 RPC 마킹하는 것은 가능하지 않습니다.

하지만 Custom event 는 블루프린트 에디터 안에서 replicated 마킹 가능합니다.

이 기능을 사용하기 위해서는, 이벤트 그래프에서 custom event 를 새로 만듭니다. custom event 에 클릭한 다음 디테일 뷰에서 Replication 세팅을 편집합니다:

인증

최근, 악성 데이터/입력 감지를 위한 관문 역할을 위해 RPC 에 인증(validation) 함수를 추가하는 기능이 생겼습니다. RPC 에 대한 인증 함수가 악성 파라미터를 감지한 경우, 해당 RPC 를 호출한 클라이언트/서버 연결을 끊도록 시스템에 알린다는 개념입니다.

RPC 에 대해 인증 함수를 선언하려면, UFUNCTION 선언문에 WithValidation 키워드를 추가해 주기만 하면 됩니다:

UFUNCTION( Server, WithValidation )
void SomeRPCFunction( int32 AddHealth );

그런 다음 Implementation 함수 옆 어딘가에 Validate 함수를 넣어주면 됩니다.

bool SomeRPCFunction_Validate( int32 AddHealth )
{
    if ( AddHealth > MAX_ADD_HEALTH )
    {
        return false;                       // This will disconnect the caller
    }
    return true;                              // This will allow the RPC to be called
}

void SomeRPCFunction_Implementation( int32 AddHealth )
{
    Health += AddHealth;
}

좀 더 최근에, 클라이언트->서버 RPC 의 경우 _Validation 함수를 포함하도록 UHT 를 변경했습니다. 서버 RPC 함수의 안전성 확보를 위해, 알려진 모든 입력 제한에 대해 각각의 모든 파라미터의 유효성 검사를 위한 코드를 쉽게 추가할 수 있도록 하기 위한 것입니다.

 

 

ref : https://docs.unrealengine.com/4.27/ko/InteractiveExperiences/Networking/Actors/RPCs/

반응형
반응형

 

#pragma once

#include <mutex>

template<typename T>
class LockQueue
{
public:
	LockQueue() = default;

	LockQueue(const LockQueue&) = delete;

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

	void push(T value)
	{
		lock_guard<mutex> lock(_mutex);
		_q.push(std::move(value));
		_conVar.notify_one();

	}
	
	bool tryPop(T& value)
	{
		lock_guard<mutex> lock(_mutex);
		if (_q.empty())
		{
			return false;
		}
		value = std::move(_q.front());
		_q.pop();
		return true;
	}


	//pop 할 데이터가 없을때 대기하다가 pop 할 데이터가 생기면 그때 pop하고 끝내는 함수
	void waitPop(T& value)
	{
		unique_lock<mutex> lock(_mutex);

		// stack 이 비어있지 않다면(false) == false => true
		// stack 이 비어 있다면 (true) == false => false  , 조건이 만족하지 않으면 lock 을 풀고 sleep 하게 된다
		_conVar.wait(lock, [this] { return _q.empty() == false; });
		value = std::move(_q.front());
		_q.pop();

	}

private:
	mutex _mutex;
	queue<T> _q;
	condition_variable _conVar;
};

 

#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"
#include "ConcurrentStack.h"
#include "ConcurrentQueue.h"

using namespace std;

//LockStack<int> st;
LockQueue<int> q;

void push()
{
	int i = 0;
	while (true)
	{
		q.push(i++);
		this_thread::sleep_for(10ms);
	}
	
}


void pop()
{
	while (true)
	{
		int popData = 0;
// 		if (q.tryPop(popData))
// 		{
// 			cout << popData << endl;
// 		}
		
		q.waitPop(popData);
		cout << popData << endl;

	}
}



int main()
{
	thread t1(push);
	thread t2(pop);
	t1.join();
	t2.join();

	return 0;
}

 

 

원리와 설명 실행결과는  (https://3dmpengines.tistory.com/2207) 이 글과 유사하다

단지 Queue  로 바꾼것일 뿐

반응형

'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글

LockFree - Stack (2) - 삭제 처리  (0) 2022.09.21
LockFree - Stack (1)  (0) 2022.09.15
Lock base Stack  (0) 2022.09.14
TLS(Thread Local Storage) : thread_local  (0) 2022.09.14
Sequential consistency (순차 일관성)  (0) 2022.09.13
반응형
#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"

using namespace std;

queue<int32> q;
stack<int32> s;

void push()
{
	while (true)
	{
		int32 value = rand() % 10;
		q.push(value);

		this_thread::sleep_for(10ms);
	}
}


void pop()
{
	while (true)
	{
		if (q.empty())
		{
			continue;;
		}

		int32 data = q.front();
		q.pop();
		cout << data << endl;
	}
}



int main()
{
	thread t1(push);
	thread t2(pop);
	t1.join();
	t2.join();

	return 0;
}

 

 

이 코드는 문제가 있는 코드로 크래쉬가 발생 할수 있다 => 동기화 처리가 되어 있지 않음

 

 

 

 

 

LockStack

이걸 좀 해결해 보면 다음 처럼 해결 해 볼 수 있을것이다

#pragma once
#include <mutex>


template<typename T>
class LockStack
{
public :

	LockStack() = default;

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

	void push(T value) 
	{
		lock_guard<mutex> lock(_mutex);
		_stack.push( std::move(value));
		_conVar.notify_one();
	}


	bool tryPop(T& value)
	{
		lock_guard<mutex> lock(_mutex);
		if (_stack.empty())
		{
			return false;
		}
		 value =  std::move(_stack.top());
		 _stack.pop();
		 return true;
	}


	//pop 할 데이터가 없을때 대기하다가 pop 할 데이터가 생기면 그때 pop하고 끝내는 함수
	void waitPop(T& value)
	{
		unique_lock<mutex> lock(_mutex);

		// stack 이 비어있지 않다면(false) == false => true
		// stack 이 비어 있다면 (true) == false => false  , 조건이 만족하지 않으면 lock 을 풀고 sleep 하게 된다
		_conVar.wait(lock, [this] { return _stack.empty() == false; }); 
		value = std::move(_stack.top());
		_stack.pop();

	}

private :
	stack<T> _stack;
	mutex _mutex;
	condition_variable _conVar;
};

 

#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"
#include "ConcurrentStack.h"

using namespace std;

queue<int32> q;
stack<int32> s;

LockStack<int> st;
void push()
{
	int i = 0;
	while (true)
	{
		st.push(i++);
		this_thread::sleep_for(10ms);
	}
	
}


void pop()
{
	while (true)
	{
		int popData = 0;
		if (st.tryPop(popData))
		{
			cout << popData << endl;
		}
		
	}
}



int main()
{
	thread t1(push);
	thread t2(pop);
	t1.join();
	t2.join();

	return 0;
}

 

실행 결과는 다음과 같다

 

데이터를 꺼낼때 여기에서 따로 구현하게 되는 Empty 함수를 호출 체크하는게 의미가 없는데,  Empty 함수안에
 lock_guard 가 있고 Empty 함수가 끝난 이후 Pop 함수로 되돌아 갈때 
 다른 스레드에서 stack 에 데이터를 추가하거나 제거 하게 된다면 현재 스레드에서 pop 을 할때 
스택이 비어 있는지 차 있는지 알수 없음으로 (그 순간 사이에서) 무의미하다
그래서 pop 할때 empty 함수를 호출하지 않고 내부에 구현한다, 유저 편의상 Empty 함수를 제공해 줄 순 있겠지만.. 

 

 

코드 중 _conVar.notify_one(); 가 있는 이유는 

waitPop 떄문인데 stack 에 데이터가 없다면 대기했다가 notifiy 가왔을때 데이터가 존재하면 스레드가 깨어나 데이터를 pop 한다음 출력하기 위함이다

 

결과는 동일하다

 

반응형

'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글

LockFree - Stack (1)  (0) 2022.09.15
LockBase Queue  (0) 2022.09.14
TLS(Thread Local Storage) : thread_local  (0) 2022.09.14
Sequential consistency (순차 일관성)  (0) 2022.09.13
Singleton multithreading programs (2)  (0) 2022.09.13
반응형

쓰레드를 잘 배분한다 했어도 작업에 따라 한곳으로 요청이 몰릴 수가 있는데 

 

Headp, Data 영역은 쓰레드들 끼리 공유해서 사용하게 되어 lock 등으로 제어를 해줘야 한다

 

TLS 라는게 스택과 (Heap,Data ) 영역 사이에 중간에 있는데 이건  (Heap,Data )  의 데이터를 각 쓰레드에 배정된 TLS 에 독자적으로 올려 사용 할 수 있다 

 

 (Heap,Data ) 를 쓰려면 동기화 처리를 해야 했지만 필요한것만 TLS 에 옮겨 놓음 

즉 TLS 쓰레드마다 갖고 있는 로컬 저장소인 것이다

 

 (Heap,Data ) 영역을 서로 갔다 쓰려고 하면 병합이 일어나고 그럼 느려지게 되는데 

자기가 사용할것을 덩어리로 갖고와 놓는 영역을 TLS 라 한다

 

그리고 전역으로도 사용 할수 있지만 다른 것들은 접근 불가능하고 해당 스레드만 접근 가능하게 된다

정리 하자면  (Heap,Data ) 큰 메모리 영역에서 필요한 메모리 덩어리만 TLS 에 옮겨놓고 작업 할수 있게 된다

 

스택과 차이점은 스택은 함수가 종료되면 메모리가 날라가듯이 지속적이진 않은데 TLS 는 전역이라 지속성이 있다

전역 변수로도 사용 할수 있지만 다른 쓰레드는 접근 불가능하다

 

 

예시..

 

#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"

using namespace std;

//thread_local 이것이 TLS (Thread Local Storage)
thread_local int32 LThreadId = 0;

void ThreadMain(int32 threadId)
{
	LThreadId = threadId;
	while (true)
	{
		cout << "thread ID " << LThreadId << endl;
		this_thread::sleep_for(1s);

	}
}

int main()
{
	vector<thread> threads;
	for (int32 i = 0; i < 10; ++i)
	{
		//쓰레드가 자신만의  ID 를 부여 받게 됨
		int32 threadId = i + 1;
		threads.push_back(thread(ThreadMain, threadId));
	}

	for (thread& t : threads)
	{
		t.join();
	}

	return 0;
}

위 코드에서 보면

 

//thread_local 이것이 TLS (Thread Local Storage)
thread_local int32 LThreadId = 0;

 

이 부분이 Thread Local Storage 를 사용 한 것이고 이 변수는 TLS 에 저장 되는 것임으로

일반 데이터 영역에 들어가는게 아니기 때문에 변수는 하나 선언한것 같지만

각 쓰레드마 별도의 TLS  영역에 들어가는 것이라 아래와 같은 thread id 를 각 쓰레드마다 출력하고 있다는 것을 알 수 있다 (결과 창은 멀티스레드 환경이라 꼬여보일 순 있음)

 

 

 

반응형

'운영체제 & 병렬처리 > Multithread' 카테고리의 다른 글

LockBase Queue  (0) 2022.09.14
Lock base Stack  (0) 2022.09.14
Sequential consistency (순차 일관성)  (0) 2022.09.13
Singleton multithreading programs (2)  (0) 2022.09.13
Singleton multithreading programs (1)  (0) 2022.09.13
반응형

한 프로세스(또는 쓰레드) 내에서의 프로그램의 실행 순서는 한 프로세스(한 쓰레드) 내에선 동일하지만, 프로세스들간의 사이서 프로그램 실행 순서는 정의 되지 않는다는 것

 

Sequential consistency is one of the consistency models used in the domain of concurrent computing (e.g. in distributed shared memory, distributed transactions, etc.).

It was first defined as the property that requires that

"... the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program."[1]

To understand this statement, it is essential to understand one key property of sequential consistency: execution order of program in the same processor (or thread) is the same as the program order, while execution order of program between processors (or threads) is undefined. In an example like this:

Execution order between A1, B1 and C1 is preserved, that is, A1 runs before B1, and B1 before C1. The same for A2 and B2. But, as execution order between processors is undefined, B2 might run before or after C1 (B2 might physically run before C1, but the effect of B2 might be seen after that of C1, which is the same as "B2 run after C1")

Conceptually, there is single global memory and a "switch" that connects an arbitrary processor to memory at any time step. Each processor issues memory operations in program order and the switch provides the global serialization among all memory operations[2]

The sequential consistency is weaker than strict consistency, which requires a read from a location to return the value of the last write to that location; strict consistency demands that operations be seen in the order in which they were actually issued.

 

 

 

ref : https://en.wikipedia.org/wiki/Sequential_consistency

반응형
반응형

 

Currently working custom merge options for WinMerge:

The options I used in Sourcetree 3.4.3 for Windows 64-bit with WinMerge 2.16.10.0 x64 and which worked for merging:

  • Options > Diff > External Diff / Merge:
    • Merge Tool: Custom
    • Merge Command: C:\Program Files\WinMerge\WinMergeU.exe
      • Arguments:
        -wl -wr -dl Remote -dm Base -dr Local \"$REMOTE\" \"$BASE\" \"$LOCAL\" -o \"$MERGED\"

 

 

ref : https://jira.atlassian.com/browse/SRCTREEWIN-13514

반응형
반응형

Guarantees of the C++ runtime

I already presented the details to the thread-safe initialization of variables in the post Thread-safe initialization of data.

Meyers Singleton

The beauty of the Meyers Singleton in C++11 is that it's automatically thread-safe. That is guaranteed by the standard: Static variables with block scope. The Meyers Singleton is a static variable with block scope, so we are done. It's still left to rewrite the program for four threads.

 

// singletonMeyers.cpp

#include <chrono>
#include <iostream>
#include <future>

constexpr auto tenMill= 10000000;

class MySingleton{
public:
  static MySingleton& getInstance(){
    static MySingleton instance;
    // volatile int dummy{};
    return instance;
  }
private:
  MySingleton()= default;
  ~MySingleton()= default;
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

};

std::chrono::duration<double> getTime(){

  auto begin= std::chrono::system_clock::now();
  for ( size_t i= 0; i <= tenMill; ++i){
      MySingleton::getInstance();
  }
  return std::chrono::system_clock::now() - begin;
  
};

int main(){
 
    auto fut1= std::async(std::launch::async,getTime);
    auto fut2= std::async(std::launch::async,getTime);
    auto fut3= std::async(std::launch::async,getTime);
    auto fut4= std::async(std::launch::async,getTime);
    
    auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
    
    std::cout << total.count() << std::endl;

}

 

 

I use the singleton object in the function getTime (line 24 - 32). The function is executed by the four promise in line 36 - 39. The results of the associate futures are summed up in line 41. That's all. Only the execution time is missing.

Without optimization

 

Maximum optimization

빠르다 Thread safe

 

The next step is the function std::call_once in combination with the flag std::once_flag.

 

 

 

 

The function std::call_once and the flag std::once_flag

You can use the function std::call_once to register a callable which will be executed exactly once. The flag std::call_once in the following implementation guarantees that the singleton will be thread-safe initialized.

 

// singletonCallOnce.cpp

#include <chrono>
#include <iostream>
#include <future>
#include <mutex>
#include <thread>

constexpr auto tenMill= 10000000;

class MySingleton{
public:
  static MySingleton& getInstance(){
    std::call_once(initInstanceFlag, &MySingleton::initSingleton);
    // volatile int dummy{};
    return *instance;
  }
private:
  MySingleton()= default;
  ~MySingleton()= default;
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

  static MySingleton* instance;
  static std::once_flag initInstanceFlag;

  static void initSingleton(){
    instance= new MySingleton;
  }
};

MySingleton* MySingleton::instance= nullptr;
std::once_flag MySingleton::initInstanceFlag;

std::chrono::duration<double> getTime(){

  auto begin= std::chrono::system_clock::now();
  for ( size_t i= 0; i <= tenMill; ++i){
      MySingleton::getInstance();
  }
  return std::chrono::system_clock::now() - begin;
  
};

int main(){

    auto fut1= std::async(std::launch::async,getTime);
    auto fut2= std::async(std::launch::async,getTime);
    auto fut3= std::async(std::launch::async,getTime);
    auto fut4= std::async(std::launch::async,getTime);
    
    auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
    
    std::cout << total.count() << std::endl;

}

 

Here are the numbers.

Without optimization

Maximum optimization

Of course, the most obvious way is it protects the singleton with a lock.

 

 

 

 

 

 

Lock

The mutex wrapped in a lock guarantees that the singleton will be thread-safe initialized.

 

// singletonLock.cpp

#include <chrono>
#include <iostream>
#include <future>
#include <mutex>

constexpr auto tenMill= 10000000;

std::mutex myMutex;

class MySingleton{
public:
  static MySingleton& getInstance(){
    std::lock_guard<std::mutex> myLock(myMutex);
    if ( !instance ){
        instance= new MySingleton();
    }
    // volatile int dummy{};
    return *instance;
  }
private:
  MySingleton()= default;
  ~MySingleton()= default;
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

  static MySingleton* instance;
};


MySingleton* MySingleton::instance= nullptr;

std::chrono::duration<double> getTime(){

  auto begin= std::chrono::system_clock::now();
  for ( size_t i= 0; i <= tenMill; ++i){
       MySingleton::getInstance();
  }
  return std::chrono::system_clock::now() - begin;
  
};

int main(){
  
    auto fut1= std::async(std::launch::async,getTime);
    auto fut2= std::async(std::launch::async,getTime);
    auto fut3= std::async(std::launch::async,getTime);
    auto fut4= std::async(std::launch::async,getTime);
    
    auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
    
    std::cout << total.count() << std::endl;
}

 

How fast is the classical thread-safe implementation of the singleton pattern?

Without optimization

 

Maximum optimization

뮤텍스만 적용하면 이때는 Meyers Singleton 에 비해 많이 느리 다는 것을 알 수 있다

아마 그냥 mutex 만 걸면 동기화는 되지만 다른 연산들이 한번에 처리 되는

대체적으로 빠른 원자적 연산이 없기 때문에 몇번 더 연산을 해야 함 => 최적화가 덜 됨

 

Not so fast. Atomics should make the difference.

 

 

 

하지만 Atomic 을 적용하면 Meyers Singleton 만큼 빨라진다

 

Atomic variables

With atomic variables, my job becomes extremely challenging. Now I have to use the C++ memory model. I base my implementation on the well-known  double-checked locking pattern.

 double-checked locking pattern. : (간략하게 말하면 if 문 으로 두번 instnace 를 두번 체크 하는 것)

 

 

참고 : 더블체크드 락 패턴 (https://3dmpengines.tistory.com/2201)

 

Singleton multithreading programs (1)

Static variables with block scope Static variables with block scope will be created exactly once. This characteristic is the base of the so called Meyers Singleton, named after Scott Meyers.  Thi..

3dmpengines.tistory.com

 

Sequential consistency

The handle to the singleton is atomic. Because I didn't specify the C++ memory model the default applies: Sequential consistency.

 

 

// singletonAcquireRelease.cpp

#include <atomic>
#include <iostream>
#include <future>
#include <mutex>
#include <thread>

constexpr auto tenMill= 10000000;

class MySingleton{
public:
  static MySingleton* getInstance(){
    MySingleton* sin= instance.load();
    if ( !sin ){
      std::lock_guard<std::mutex> myLock(myMutex);
      sin= instance.load();
      if( !sin ){
        sin= new MySingleton();
        instance.store(sin);
      }
    }   
    // volatile int dummy{};
    return sin;
  }
private:
  MySingleton()= default;
  ~MySingleton()= default;
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

  static std::atomic<MySingleton*> instance;
  static std::mutex myMutex;
};


std::atomic<MySingleton*> MySingleton::instance;
std::mutex MySingleton::myMutex;

std::chrono::duration<double> getTime(){

  auto begin= std::chrono::system_clock::now();
  for ( size_t i= 0; i <= tenMill; ++i){
       MySingleton::getInstance();
  }
  return std::chrono::system_clock::now() - begin;
  
};


int main(){

    auto fut1= std::async(std::launch::async,getTime);
    auto fut2= std::async(std::launch::async,getTime);
    auto fut3= std::async(std::launch::async,getTime);
    auto fut4= std::async(std::launch::async,getTime);
    
    auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
    
    std::cout << total.count() << std::endl;

}

 

Now I'm curious.

Without optimization

 

Maximum optimization

 

But we can do better. There is an additional optimization possibility.

 

atomic 만 추가 했는데  Meyers Singleton  만큼 빨라진 것을 볼 수 있다

 

 

여기서 약간 더 최적화 할 수 있는데 Acquire-release 체계를 사용 하는것 Memory Model

 

Acquire-release Semantic

The reading of the singleton (line 14) is an acquire operation, the writing a release operation (line 20). Because both operations take place on the same atomic I don't need sequential consistency. The C++ standard guarantees that an acquire operation synchronizes with a release operation on the same atomic. These conditions hold in this case therefore I can weaken the C++ memory model in line 14 and 20. Acquire-release semantic is sufficient.

 

// singletonAcquireRelease.cpp

#include <atomic>
#include <iostream>
#include <future>
#include <mutex>
#include <thread>

constexpr auto tenMill= 10000000;

class MySingleton{
public:
  static MySingleton* getInstance(){
    MySingleton* sin= instance.load(std::memory_order_acquire);
    if ( !sin ){
      std::lock_guard<std::mutex> myLock(myMutex);
      sin= instance.load(std::memory_order_relaxed);
      if( !sin ){
        sin= new MySingleton();
        instance.store(sin,std::memory_order_release);
      }
    }   
    // volatile int dummy{};
    return sin;
  }
private:
  MySingleton()= default;
  ~MySingleton()= default;
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

  static std::atomic<MySingleton*> instance;
  static std::mutex myMutex;
};


std::atomic<MySingleton*> MySingleton::instance;
std::mutex MySingleton::myMutex;

std::chrono::duration<double> getTime(){

  auto begin= std::chrono::system_clock::now();
  for ( size_t i= 0; i <= tenMill; ++i){
       MySingleton::getInstance();
  }
  return std::chrono::system_clock::now() - begin;
  
};


int main(){

    auto fut1= std::async(std::launch::async,getTime);
    auto fut2= std::async(std::launch::async,getTime);
    auto fut3= std::async(std::launch::async,getTime);
    auto fut4= std::async(std::launch::async,getTime);
    
    auto total= fut1.get() + fut2.get() + fut3.get() + fut4.get();
    
    std::cout << total.count() << std::endl;

}

 

The acquire-release semantic has a similar performance as the sequential consistency. That's not surprising, because on x86 both memory models are very similar. We would get totally different numbers on an ARMv7 or PowerPC architecture. You can read the details on Jeff Preshings blog Preshing on Programming.

Without optimization

 

Maximum optimization

.

If I forget an import variant of the thread-safe singleton pattern, please let me know and send me the code. I will measure it and add the numbers to the comparison.

 

미소하지만 약간더 빨라 진 것을 볼 수 있다

 

Momory Model 중

acquire-release 방식의 의미는 순차적 일관성(https://3dmpengines.tistory.com/2205)과 유사하다

 

그리고 acquire과 release 는 다음과 같은 의미를 갖고 있는데

  1.  // Load-Acquire 수행
  2.     int ready = g_guard.load(memory_order_acquire);
  3.     // memory_order_acquire 이후부터는 Release 이전에 쓰여진 메모리 값들이 모두 제대로 보인다

 

  1.    // 여기에서 Write-Release 수행
  2.     // memory_order_release 키워드 이후 부터는 지금까지 쓴 내용들이 Acquire 이후에 보여진다.
  3.     g_guard.store(1, memory_order_release);

 

명령어가 순서적으로 실행된다는 의미 정도로 생각 하면 될것으로 보인다

 

acqurie-release 방식으로 사용하면 이 중간에 있는 코드는 메모리 재배치를 하지 않겠다는 의미

 

fence 라고 해서 앞뒤로 장벽을 처서 명령어 재배치 되지 않게 할수 있는데 (acquire-release 방식으로..)

하지만 결과는 거의동일하다

참고 : fence 을 활용한 Singleton (https://3dmpengines.tistory.com/2200)

 

 

ref :http://www.modernescpp.com/index.php/component/jaggyblog/thread-safe-initialization-of-a-singleton

ref : http://egloos.zum.com/sweeper/v/3059861

반응형
반응형

Static variables with block scope

Static variables with block scope will be created exactly once. This characteristic is the base of the so called Meyers Singleton, named after Scott Meyers.  This is by far the most elegant implementation of the singleton pattern.

#include <thread>

class MySingleton{
public:
  static MySingleton& getInstance(){
    static MySingleton instance;
    return instance;
  }
private:
  MySingleton();
  ~MySingleton();
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

};

MySingleton::MySingleton()= default;
MySingleton::~MySingleton()= default;

int main(){

  MySingleton::getInstance();

}

 

By using the keyword default, you can request special methods from the compiler. They are Special because only compiler can create them. With delete, the result is, that the automatically generated methods (constructor, for example) from the compiler will not be created and, therefore, can not be called. If you try to use them you'll get an compile time error. What's the point of the Meyers Singleton in multithreading programs? The Meyers Singleton is thread safe.

A side note: Double-checked locking pattern

Wrong beliefe exists, that an additional way for the thread safe initialization of a singleton in a multithreading environment is the double-checked locking pattern. The double-checked locking pattern is - in general -  an unsafe way to initialize a singleton. It assumes guarantees in the classical implementation, which aren't given by the Java, C# or C++ memory model. The assumption is, that the access of the singleton is atomic.

But, what is the double-checked locking pattern? The first idea to implement the singleton pattern in a thread safe way, is  to protected the initialization of the singleton by a lock.

mutex myMutex;

class MySingleton{
public:  
  static MySingleton& getInstance(){    
    lock_guard<mutex> myLock(myMutex);      
    if( !instance ) instance= new MySingleton();    
    return *instance;  
  }
private:  
  MySingleton();  
  ~MySingleton();  
  MySingleton(const MySingleton&)= delete;  
  MySingleton& operator=(const MySingleton&)= delete;
  static MySingleton* instance;
};
MySingleton::MySingleton()= default;
MySingleton::~MySingleton()= default;
MySingleton* MySingleton::instance= nullptr;
 

 

Any issues? Yes and no. The implementation is thread safe. But there is a great performance penalty. Each access of the singleton in line 6 is protected by an expansive lock. That applies also for the reading access. Most time it's not necessary. Here comes the double-checked locking pattern to our rescue.

 

static MySingleton& getInstance(){    
  if ( !instance ){      
    lock_guard<mutex> myLock(myMutex);      
    if( !instance ) instance= new MySingleton();    
  }
  return *instance; 
}

 

 

I use inexpensive pointer comparison  in the line 2 instead of an expensive lock a. Only if I get a null pointer, I apply the expensive lock on the singleton (line 3). Because there is the possibility that another thread will initialize the singleton between the pointer comparison (line 2) and the lock (line3), I have to perform an additional pointer comparison the on line 4. So the name is clear. Two times a check and one time a lock.

Smart? Yes. Thread safe? No.

What is the problem? The call instance= new MySingleton() in line 4 consists of at least three steps.

  1. Allocate memory for MySingleton
  2. Create the MySingleton object in the memory
  3. Let instance refer to the MySingleton object


The problem: there is no guarantee about  the sequence of these steps. For example, out of optimization reasons, the processor can reorder the steps to the sequence 1,3 and 2. So, in the first step the memory will be allocated and in the second step, instance refers to an incomplete singleton. If at that time another thread tries to access the singleton, it compares the pointer and gets the answer true. So, the other thread has the illusion that it's dealing with a complete singleton.

The consequence is simple: program behaviour is undefined.

 

최적화에 의해서 명령어 순서가 1,3 ,2 순서로 뒤 바뀔 수 있기 때문에 다른 스레드에서 잘못된 instance 포인터를 얻어가게 되어 문제가 발생 할 수 있게 된다

핵심은 Mutex 가  reorder 를 막아주진 못한다는 것!!!

 

 

What's next?

At first, I thought, I should continue in the next post with the singleton pattern. But to write about the singleton pattern, you should have a basic knowledge of the memory model. So I continue in the sequence of my German blog. The next post will be about-thread local storage. In case we are done with the high end API of multithreading in C++, I'll go further with the low end API. (Proofreader Alexey Elymanov)

 

 

ref : https://www.modernescpp.com/index.php/thread-safe-initialization-of-data

 

반응형
반응형

 

memory_order_release, memory_order_acquire 는  아래 글을 참고 하면 되겠다

(atomic 예시 와 Memory Model (정책) , memory_order_...) https://3dmpengines.tistory.com/2199

 

atomic 예시 와 Memory Model (정책) , memory_order_...

atomic flag = false; //true 가 나오면 원래 원자적으로 처리 되는거라 lock 할 필요 없음 bool isAtomic = flag.is_lock_free(); //flag.store(true); flag.store(true, memory_order::memory_order_seq_cst); /..

3dmpengines.tistory.com

 

 

 

 

C++11은 두 개의 memory fence(memory barrier) 기능을 제공한다.

 

fence는 non-atomic operation 또는 memory_order_relaxed로 수행되는
atomic operation 메모리 배리어 역할을 수행한다.

 

 

1) atomic_thread_fence(memory_order order)

메모리 가시성 강제메모리 재배치금지한다.
사실 fence는 atomic 클래스나 atomic instrinsic 함수들을 사용한다면, 굳이 사용할 필요가 없다고 생각한다.
 
굳이 이를 사용하려면, atomic 멤버 함수 호출시 memory_order_relaxed(메모리 배치에 관여하지 않음) 방식을 쓴 뒤,
인위적으로 fence를 호출해 주는 방식을 써야 하는데 굳이 쓸 일이 있을까 싶다.
 
앞서 소개했던 Write-Release / Read-Acquire 예제를 atomic_thread_fence 버전으로 바꿔보자.
 
  1. // 쓰레드 1에서의 producer
  2. void TrySendMessage()
  3. {
  4.     //...
  5.    
  6.     g_payload.tick = clock();
  7.     g_payload.str = "TestMessage";
  8.     g_payload.param = param;
  9.  
  10.     // 지금까지 쓴 내용들이 Acquire를 수행한 쓰레드에서 보여져야 한다.
  11.     atomic_thread_fence(memory_order_release);
  12.  
  13.     g_guard.store(1, memory_order_relaxed);
  14. }
  15.  
  16. // 쓰레드 2에서 대기중인 consumer
  17. void TryReceiveMessage()
  18. {
  19.     // ...
  20.  
  21.     // Load 수행
  22.     int ready = g_guard.load(memory_order_relaxed);
  23.     if (ready != 0)
  24.     {
  25.         atomic_thread_fence(memory_order_acquire);
  26.         // 이후부터는 Release 이전에 쓰여진 메모리 값들이 모두 제대로 보여야 한다
  27.  
  28.         result.tick = g_payload.tick;
  29.         result.str = g_payload.str;
  30.         result.param = g_payload.param;
  31.     }
  32. }
 
위 코드의 흐름을 그림으로 표현하면 아래와 같다.
 
 
또 하나의 예를 들자면, 싱글턴에서 자주 사용되는 Double-Checked Locking 기법일 것이다.
 
  1. using namespace std;
  2.  
  3. atomic<Singleton*> Singleton::m_instance;
  4. mutex Singleton::m_mutex;
  5.  
  6. // Double-Checked Locking 기법에 relaxed order와 memory fence 활용
  7. Singleton* Singleton::getInstance()
  8. {
  9.     Singleton* tmp = m_instance.load(memory_order_relaxed);
  10.     atomic_thread_fence(memory_order_acquire)
  11.     if (tmp == nullptr)
  12.     {
  13.         lock_guard<mutex> lock(m_mutex);
  14.  
  15.         tmp = m_instance.load(memory_order_relaxed);
  16.         if (tmp == nullptr)
  17.         {
  18.             tmp = new Singleton;
  19.  
  20.             atomic_thread_fence(memory_order_release);
  21.             m_instance.store(tmp, memory_order_relaxed);
  22.         }
  23.     }
  24.     return tmp;
  25. }
 
 
 
 
 
 
위 코드의 흐름은 아래 그림과 같다.
 
헌데, 아무리 sequential consistency가 위 방식보다 조금 덜 효율적이라 하지만, 
실제 퍼포먼스 테스트를 해 본 결과 의미를 부여할 만한 성능 차이를 전혀 발견하지 못했다.
 
아래 예제는 C++ atomic default인 memory_order_seq_cst 버전이다. 깔끔!!!
(Intel TBB로 구현했다면, default가 release-acquire)
 
  1. // C++11 디폴트 sequential consistency 버전
  2. Singleton* Singleton::getInstance()
  3. {
  4.     Singleton* inst = m_instance.load();
  5.     if (inst == nullptr)
  6.     {
  7.         lock_guard<std::mutex> lock(m_mutex);
  8.  
  9.         inst = m_instance.load(/*memory_order_seq_cst*/);
  10.         if (inst == nullptr)
  11.         {
  12.             inst = new Singleton;
  13.             m_instance.store(inst/*, memory_order_seq_cst*/);
  14.         }
  15.     }
  16.     return inst;
  17. }
 
 

2) atomic_signal_fence(memory_order order)

 
메모리 재배치를 금지한다.
 
4. 추가 링크
 
아래 링크들이 워낙 좋아 추가로 링크를 걸어둔다.
아티클 안에 링크된 페이지들도 읽어 볼 만한 내용이 많으므로, 시간날 때마다 탐독할 것.
(사실 위 내용들의 예제/그림들 중 아래 링크들에서 퍼온 게 많다)
 
반응형
반응형
	atomic<bool> flag = false;

	//true 가 나오면 원래 원자적으로 처리 되는거라 lock 할 필요 없음
	bool isAtomic = flag.is_lock_free();
	

	//flag.store(true);
	flag.store(true, memory_order::memory_order_seq_cst);	//위의 한줄과 동일한 연산이다

	bool val = flag.load(memory_order::memory_order_seq_cst);

기본 적인 storeload 이전에 사용했던 것에서 뒤에 인자를 넣어 줄수 있다는 것

 

 

 

 

#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"
using namespace std;

atomic<bool> flag = false;

int main()
{
	flag = false;

	flag.store(true, memory_order::memory_order_seq_cst);	//memory_order_seq_cst 가 위에선 기본으로 들어간다

	bool val = flag.load(memory_order::memory_order_seq_cst);

	{
		bool prev = flag;		//이렇게 하면 만약 다른 쓰레드에서 flag 값을 중간에 바꾸면 prev 는 falg 과 동일한 값이 아닐 수도 있게된다
		flag = true;
	}


	//위의 것을 원자적으로 한번에 변경 하려면 다음 처럼 하면 된다 
	{
		//이렇게 한번에 처리하는 atomic 의 함수를 사용 하면 된다
		bool prev = flag.exchange(true);
	}

	//cas (compare-and-swap) 조건부 수정
	{
		bool expected = false;
		bool desired = true;
		flag.compare_exchange_strong(expected, desired);		//현재 값이 기대한 값과 같다면 희망하는 값으로 바꿔주는 함수

	}

	{
		bool expected = false;
		bool desired = true;
		flag.compare_exchange_weak(expected, desired);		//동작은 compare_exchange_strong 과 같은데 spruious failure 가 발생 할 수 있다
		//즉 이 함수 내에서 다른 스레드의 Interruption 을 받아서 값 변경이 중간에 실패 할 수 있음
		//lock 해야 하는데 다른곳에서 먼저 lock 해서 가짜 wakup 이 일어나 제대로 처리 되지 않는 묘한 상황이 발생 할 수 있다는 것

		//compare_exchange_weak 이게 기본적인 형태인데 만약에 이렇게 실패가 일어나면 다시 한번 시도해서 성공할대까지 시도하는게 compare_exchange_strong 이 된다 
		//compare_exchange_strong 그래서 이게 좀 더 부하가 있을 수 있다

		//그래서 compare_exchange_weak 이걸 사용할대는 while 루프를 사용해서 처리 한다, 하지만 성능 차이가 엄청 크게 차이 나진 않는다
	}
	return 0;
}

compare_exchange_weak 같은 경우

compare_exchange_weak  함수 내에서  if(flag==expected){ 여기 안에서 실패 발생 가능성  }  이런 내부적 유사구문에서 실패를 할수 있기 때문에 가짜기상이 일어날 수 있다 compare_exchange_strong 같은경우는 이 실패가 일어날 경우 성공 할떄까지 반복해서 실행하게 된다

그래서 compare_exchange_weak  은  while 루프와 함께 사용 되어서 성공할때까지 시도 하는 형태로 코드를 작성하게 된다

 

 

 

 

Memory Model (정책) 에는 다음과 같은 것들이 있다

 

load, store 의 뒤에 넣는 인자들이 다음 처럼 있다는 얘기

  1. Sequentianlly Consistent (seq_cst)
  2. Acquire-Release (consume, acquire, release, acq_rel)
  3. Relaxed (relaxed)

 

consume 은 뭔가 문제가 있음

 

 

 

여기서 중요한것은 seq_cst, acquire, release, acq_rel 이렇게 4개 이다

이 중 acq_rel  이것은 acquire, release 을 합처 놓은 것이다

 

정리하면..

  1. Sequentianlly Consistent (memory_order_seq_cst) : 가장 엄격
    [컴파일러에서 최적화 여지 적음=>코드 직관적] =>코드 재배치 잘 안됨 : 가시성 문제와 코드 재배치 문제가 해결 된다

  2. Acquire-Release (memory_order_acquire, memory_order_release) : 중간 엄격
    ex) ready.store(true, memory_order_release);
    release 명령 이전의 메모리 명령들이, 해당 명령 이후로 재배치 되는 것을 막는다 => 즉 재배치 억제
    ready.store(true, memory_order_acquire); //  acquire 로 같은 변수를 읽는 쓰레드가 있다면
    release 이전의 명령들이 ->acqurie 하는 순간에 관찰 가능하다(가시성 보장) 
    즉 memory_order_release와 memory_order_relase 이전의 내용들이
    memory_order_acquire 한 이후의 가시성이 보장이 된다는 얘기,
    다시 말해  memory_order_release 이전에 오는 모든 메모리 명령들이 memory_order_acquire  한
    이후의
    해당 쓰레드에 의해서 
    관찰 될 수 있어야 합니다.

  3. Relaxed (memory_order_relaxed)  : 사용할 일이 거의 없다
    자유로워짐 [컴파일러 최적화 여지가 많다 => 코드가 복잡] =>
    코드 재배치 잘 될 수 있음, 가시성도 해결 되지 못한다
    단 동일 객체에 대한 동일 수정 순서만 보장한다
    참고 (https://3dmpengines.tistory.com/2198?category=511463)

 

atomic 은 아무것도 입력하지 않으면 seq_cst 이 기본 버전으로 처리 된다 => 가장 많이 사용 됨

 

 

 

 


 

 

[1] atomic 중에서 memory_order::memory_order_seq_cst

#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"
using namespace std;

atomic<bool> ready = false;
int32 value;

void producer()
{
	value = 10;
    
    //seq_cst를 하게 되면 ready 이 값이 consumer 에서의 ready 에서도 그대로 보이고 재배치문제도 해결됨
    // cst 를 하게 되면 value 값이 cst 이후에 정확하게 갱신되어 값이 이후에 보여지게 된다
    //releaxed 같은 경우는 재배치로 인해 0이 나올 수도 있다, 대부분 테스트하면 10이 나오긴 하지만..
	ready.store(true, memory_order::memory_order_seq_cst);
}

void consumer()
{
	while (ready.load(memory_order::memory_order_seq_cst) == false)
		;
	cout << value << endl;
}

int main()
{
	ready = false;
	value = 0;
	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();

	/*
	memory_order_seq_cst 이걸 쓰면 가시성 문제와 코드 재배치 문제가 해결 된다!!!
	모든게 정상적으로 작동
	*/

	return 0;
}

ready.store(true, memory_order::memory_order_seq_cst); 이 한줄에 대해서만

 

 

memory_order_seq_cst

memory_order_seq_cst 는 메모리 명령의 순차적 일관성(sequential consistency) 을 보장해줍니다.

순차적 일관성이란, 메모리 명령 재배치도 없고, 모든 쓰레드에서 모든 시점에 동일한 값을 관찰할 수 있는, 여러분이 생각하는 그대로 CPU 가 작동하는 방식이라 생각하면 됩니다.

memory_order_seq_cst 를 사용하는 메모리 명령들 사이에선 이러한 순차적 일관성을 보장해줍니다.

 

 

 


 

 

[3] atomic 중에서 memory_order_relaxed

#include "pch.h"
#include <iostream>
#include <thread>	
#include <mutex>
#include <chrono>
#include <future>
#include "windows.h"
using namespace std;

atomic<bool> ready = false;
int32 value;

void producer()
{
	value = 10;
	ready.store(true, memory_order::memory_order_relaxed);
}

void consumer()
{
	while (ready.load(memory_order::memory_order_relaxed) == false)
		;
	cout << value << endl;
}

int main()
{
	ready = false;
	value = 0;
	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();
    
	return 0;
}

memory_order_relaxed : 가장 루즈한 규칙으로 가시성과 코드 재배치가 해결되지 않는다
사실상 거의 쓰이지 않는다

즉 
value = 10;
ready.store(true, memory_order::memory_order_relaxed);

이 코드가

ready.store(true, memory_order::memory_order_relaxed);
value = 10;

이렇게 컴파일 될 수도 있단 얘기임

 

 

 

또 다른 얘시

#include <atomic>
#include <cstdio>
#include <thread>
#include <vector>
using std::memory_order_relaxed;

void t1(std::atomic<int>* a, std::atomic<int>* b) {
  b->store(1, memory_order_relaxed);      // b = 1 (쓰기)
  int x = a->load(memory_order_relaxed);  // x = a (읽기)

  printf("x : %d \n", x);
}

void t2(std::atomic<int>* a, std::atomic<int>* b) {
  a->store(1, memory_order_relaxed);      // a = 1 (쓰기)
  int y = b->load(memory_order_relaxed);  // y = b (읽기)

  printf("y : %d \n", y);
}

int main() {
  std::vector<std::thread> threads;

  std::atomic<int> a(0);
  std::atomic<int> b(0);

  threads.push_back(std::thread(t1, &a, &b));
  threads.push_back(std::thread(t2, &a, &b));

  for (int i = 0; i < 2; i++) {
    threads[i].join();
  }
}

성공적으로 컴파일 하였다면 아래와 같은 결과들을 확인할 수 있습니다.

실행 결과

x : 1 
y : 0 

혹은

실행 결과

x : 0 
y : 1 

혹은

실행 결과

y : 1 
x : 1

을 말이지요.

 

b->store(1, memory_order_relaxed);      // b = 1 (쓰기)
int x = a->load(memory_order_relaxed);  // x = a (읽기)

store  load  atomic 객체들에 대해서 원자적으로 쓰기와 읽기를 지원해주는 함수 입니다. 이 때, 추가적인 인자로, 어떠한 형태로 memory_order 을 지정할 것인지 전달할 수 있는데, 우리의 경우 가장 느슨한 방식인 memory_order_relaxed 를 전달하였습니다.

 

여기서 잠깐 궁금한게 있습니다. 과연 아래와 같은 결과를 볼 수 있을까요?

 

 

실행 결과

x : 0 
y : 0

상식적으로는 불가능 합니다. 왜냐하면 x, y 둘다 0 이 나오기 위해서는 x = a  y = b 시점에서 a  b 모두 0 이어야만 합니다. 하지만 위 명령어들이 순서대로 실행된다면 이는 불가능 하다는 사실을 알 수 있습니다.

예를 들어서 x 에 0 이 들어가려면 a 가 0 이어야 합니다. 이 말은 즉슨, x = a 가 실행된 시점에서 a = 1 이 실행되지 않았어야만 합니다. 따라서 t2 에서 y = b 를 할 때 이미 b 는 1 인 상태이므로, y 는 반드시 1 이 출력되어야 하지요.

하지만, 실제로는 그렇지 않습니다. memory_order_relaxed 는 앞서 말했듯이, 메모리 연산들 사이에서 어떠한 제약조건도 없다고 하였습니다. 다시 말해 서로 다른 변수의 relaxed 메모리 연산은 CPU 마음대로 재배치 할 수 있습니다 (단일 쓰레드 관점에서 결과가 동일하다면).

 

예를 들어서

int x = a->load(memory_order_relaxed);  // x = a (읽기)
b->store(1, memory_order_relaxed);      // b = 1 (쓰기)

순으로 CPU 가 순서를 재배치 하여 실행해도 무방하다는 뜻입니다.

그렇다면 이 경우 x  y 에 모두 0 이 들어가겠네요. memory_order_relaxed 는 CPU 에서 메모리 연산 순서에 관련해서 무한한 자유를 주는 것과 같습니다. 덕분에 CPU 에서 매우 빠른 속도로 실행할 수 있게됩니다.

이렇게 relaxed 메모리 연산을 사용하면 예상치 못한 결과를 나을 수 있지만, 종종 사용할 수 있는 경우가 있습니다.

 

 

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
using std::memory_order_relaxed;

void worker(std::atomic<int>* counter) {
  for (int i = 0; i < 10000; i++) {
    // 다른 연산들 수행

    counter->fetch_add(1, memory_order_relaxed);
  }
}
int main() {
  std::vector<std::thread> threads;

  std::atomic<int> counter(0);

  for (int i = 0; i < 4; i++) {
    threads.push_back(std::thread(worker, &counter));
  }

  for (int i = 0; i < 4; i++) {
    threads[i].join();
  }

  std::cout << "Counter : " << counter << std::endl;
}

 

 

성공적으로 컴파일 하였다면

실행 결과

Counter : 40000

와 같이 나옵니다. 여기서 중요한 부분은

 

counter->fetch_add(1, memory_order_relaxed);

로 이는 counter ++ 와 정확히 하는 일이 동일하지만, counter++ 와는 다르게 메모리 접근 방식을 설정할 수 있습니다. 위 문장 역시 원자적으로 counter 의 값을 읽고  1 을 더하고 다시 그 결과를 씁니다.

 

다만 memory_order_relaxed 를 사용할 수 있었던 이유는, 다른 메모리 연산들 사이에서 굳이 counter 를 증가시키는 작업을 재배치 못하게 막을 필요가 없기 때문입니다. 비록 다른 메모리 연산들 보다 counter ++ 이 늦게 된다고 하더라도 결과적으로 증가 되기만 하면 문제 될게 없기 때문 입니다, 그렇다고 이것을 권하는 것은 아님

 

 

 

 

[2] memory_order_acquire 과 memory_order_release

memory_order_relaxed 가 사용되는 경우가 있다고 하더라도 너무나 CPU 에 많은 자유를 부여하기에 그 사용 용도는 꽤나 제한적입니다. 이번에 살펴볼 것들은 memory_order_relaxed 보다 살짝 더 엄격한 친구들 입니다.

 

relaxed 로 된ㄷ 아래와 같은 producer - consumer 관계를 생각해봅시다.

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
using std::memory_order_relaxed;

void producer(std::atomic<bool>* is_ready, int* data) {
  *data = 10;
  is_ready->store(true, memory_order_relaxed);
}

void consumer(std::atomic<bool>* is_ready, int* data) {
  // data 가 준비될 때 까지 기다린다.
  while (!is_ready->load(memory_order_relaxed)) {
  }

  std::cout << "Data : " << *data << std::endl;
}

int main() {
  std::vector<std::thread> threads;

  std::atomic<bool> is_ready(false);
  int data = 0;

  threads.push_back(std::thread(producer, &is_ready, &data));
  threads.push_back(std::thread(consumer, &is_ready, &data));

  for (int i = 0; i < 2; i++) {
    threads[i].join();
  }
}

 

성공적으로 컴파일 하였다면

실행 결과

Data : 10

일반적인 경우 위와 같이 나옵니다. 하지만, 아래와 같은 결과를 얻을 수 도 있을까요?

실행 결과

Data : 0

있습니다! 왜냐하면 producer 쓰레드를 살펴보자면

 

*data = 10;
is_ready->store(true, memory_order_relaxed);

 is_ready 에 쓰는 연산이 relaxed 이기 때문에 위의 *data = 10 과 순서가 바뀌어서 실행된다면 is_ready  true 임에도 *data = 10 이 채 실행이 끝나지 않을 수 있다는 것이지요.

따라서 consumer 쓰레드에서 is_ready  true 가 되었음에도 제대로된 data 를 읽을 수 없는 상황이 벌어집니다.

 

 

consumer 쓰레드에서도 마찬가지 입니다.

while (!is_ready->load(memory_order_relaxed)) {
}

std::cout << "Data : " << *data << std::endl;

아래에 data 를 읽는 부분과 위 is_ready 에서 읽는 부분이 순서가 바뀌어 버린다면, is_ready  true 가 되기 이전의 data 값을 읽어버릴 수 있다는 문제가 생깁니다. 따라서 위와 같은 생산자 - 소비자 관계에서는 memory_order_relaxed 를 사용할 수 없습니다.

 

 

 

본론으로 돌아와서.. memory_order_release, memory_order_acquire 된 코드

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

void producer(std::atomic<bool>* is_ready, int* data) {
  *data = 10;
  is_ready->store(true, std::memory_order_release);
  //----------------------여기부터 ---------------------------------
  //is_ready->store(true, std::memory_order_release); 이코드 위에 있는 것들이 
  //--------줄 아래로 내려갈 수 없다는 것을 말한다
}

void consumer(std::atomic<bool>* is_ready, int* data) {
  
  // data 가 준비될 때 까지 기다린다.
  //memory_order_acquire 는 같은 변수로 읽는 스레드가 있다면 release 이전의 명령들이
  //acquire 하는 순간에 관찰 가능하다
  //----------------------여기까지 ---------------------------------
  //!is_ready->load(std::memory_order_acquire) 아래 있는 명령들이 이 위로 못올라오게 막고
  //is_ready->store(true, std::memory_order_release); 이전의 내용들이 
  //memory_order_acquire 이후에 모두다 값들이 갱신되어 정확히 보여지게 된다
  while (!is_ready->load(std::memory_order_acquire)) 
  {
  }

  std::cout << "Data : " << *data << std::endl;
}

int main() {
  std::vector<std::thread> threads;

  std::atomic<bool> is_ready(false);
  int data = 0;

  threads.push_back(std::thread(producer, &is_ready, &data));
  threads.push_back(std::thread(consumer, &is_ready, &data));

  for (int i = 0; i < 2; i++) {
    threads[i].join();
  }
}

 

성공적으로 컴파일 하였다면

실행 결과

Data : 10

와 같이 나옵니다. 이 경우 data 에 0 이 들어가는 일은 불가능 합니다. 이유는 아래와 같습니다.

 

*data = 10;
is_ready->store(true, std::memory_order_release);

memory_order_release  해당 명령 이전의 모든 메모리 명령들이 해당 명령 이후로 재배치 되는 것을 금지 합니다. 또한, 만약에 같은 변수를 memory_order_acquire 으로 읽는 쓰레드가 있다면, memory_order_release 이전에 오는 모든 메모리 명령들이 해당 쓰레드에 의해서 관찰 될 수 있어야 합니다.

 

 

쉽게 말해 is_ready->store(true, std::memory_order_release); 밑으로 *data = 10 이 올 수 없게 됩니다.

또한 is_ready  true 가 된다면, memory_order_acquire  is_ready 를 읽는 쓰레드에서 data 의 값을 확인했을 때 10 임을 관찰할 수 있어야하죠.

 

while (!is_ready->load(std::memory_order_acquire)) {
}

 

실제로 cosnumer 쓰레드에서 is_ready  memory_order_acquire  load 하고 있기에, is_ready  true 가 된다면, data 는 반드시 10 이어야만 합니다.

 

memory_order_acquire 의 경우, release 와는 반대로 해당 명령 뒤에 오는 모든 메모리 명령들이 해당 명령 위로 재배치 되는 것을 금지 합니다.

 

이와 같이 두 개의 다른 쓰레드들이 같은 변수의 release  acquire 를 통해서 동기화 (synchronize) 를 수행하는 것을 볼 수 있습니다.

 

 

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
using std::memory_order_relaxed;

std::atomic<bool> is_ready;
std::atomic<int> data[3];

void producer() {
  data[0].store(1, memory_order_relaxed);
  data[1].store(2, memory_order_relaxed);
  data[2].store(3, memory_order_relaxed);
  is_ready.store(true, std::memory_order_release);
}

void consumer() {
  // data 가 준비될 때 까지 기다린다.
  while (!is_ready.load(std::memory_order_acquire)) {
  }

  std::cout << "data[0] : " << data[0].load(memory_order_relaxed) << std::endl;
  std::cout << "data[1] : " << data[1].load(memory_order_relaxed) << std::endl;
  std::cout << "data[2] : " << data[2].load(memory_order_relaxed) << std::endl;
}

int main() {
  std::vector<std::thread> threads;

  threads.push_back(std::thread(producer));
  threads.push_back(std::thread(consumer));

  for (int i = 0; i < 2; i++) {
    threads[i].join();
  }
}

성공적으로 컴파일 하였다면

실행 결과

data[0] : 1
data[1] : 2
data[2] : 3

 

 

data[0].store(1, memory_order_relaxed);
data[1].store(2, memory_order_relaxed);
data[2].store(3, memory_order_relaxed);
is_ready.store(true, std::memory_order_release);

여기서 data 의 원소들을 store 하는 명령들은 모두 relaxed 때문에 자기들 끼리는 CPU 에서 마음대로 재배치될 수 있겠지만, 아래 release 명령을 넘어가서 재배치될 수 는 없습니다.

release - acquire 동기화

따라서 consumer 에서 data 들의 값을 확인했을 때 언제나 정확히 1, 2, 3 이 들어있게 됩니다.

 

 

 

 


[4] memory_order_acq_rel

memory_order_acq_rel 은 이름에서도 알 수 있듯이, acquire  release 를 모두 수행하는 것입니다. 이는, 읽기와 쓰기를 모두 수행하는 명령들, 예를 들어서 fetch_add 와 같은 함수에서 사용될 수 있습니다.

 

 

 


 

 

 

 

Sequentially-consistent Ordering

memory_order_seq_cst 플래그로 설정된 아토믹 객체는 memory_order_seq_cst와 마찬가지로 store 이전의 상황이 다른 스레드의 load시 동기화가 되는 기능을 제공한다. 또한 memory_order_seq_cst는 "single total modification order" 단일 수정 순서 = 전지적 시점에서 실행된 순서가 모든 아토믹 객체를 사용하는 쓰레드 사이에 동기화된다.

 

 

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<bool> x(false);
std::atomic<bool> y(false);
std::atomic<int> z(0);

void write_x() { x.store(true, std::memory_order_release); }

void write_y() { y.store(true, std::memory_order_release); }

void read_x_then_y() {
  while (!x.load(std::memory_order_acquire)) {
  }
  if (y.load(std::memory_order_acquire)) {
    ++z;
  }
}

void read_y_then_x() {
  while (!y.load(std::memory_order_acquire)) {
  }
  if (x.load(std::memory_order_acquire)) {
    ++z;
  }
}

int main() {
  thread a(write_x);
  thread b(write_y);
  thread c(read_x_then_y);
  thread d(read_y_then_x);
  a.join();
  b.join();
  c.join();
  d.join();
  std::cout << "z : " << z << std::endl;
}

 

성공적으로 컴파일 하였다면

실행 결과

z : 2

혹은

실행 결과

z : 1

과 같이 나옵니다. 그렇다면

실행 결과

z : 0

은 발생할 수 있을까요?

 

 

 

기존 release-acquire 관계를 이용하는 a, b, c, d의 스레드를 실행했을 때 보장하는 것은 다음과 같다.

  • write_x()에서 x(store) 이전의 문맥 = read_x_then_y()의 x(load) 시점의 문맥
  • write_y()에서 y(store) 이전의 문맥 = read_y_then_x()의 y(load) 시점의 문맥

따라서 실행 결과 값은 다음과 같이 나올 수 있다.

Z 값 전지적 실행 순서  
z = 2 a -> b -> c(d) -> d(c) x,y 결과가 정해진 이후 이기 때문에 c, d의 결과는 순서에 상관없이 같다.
z = 1 a -> c -> b -> d
b -> d -> a -> c
쓰레드 c는 y가 0으로 관측되기 때문에 z를 증가시키지 않는다. 혹은 그 반대.
z = 0 CPU 1: a -> c
CPU 2: b -> d
이러한 상황은 두 흐름이 다른 CPU에서 일어날 때 발생한다.

z = 0인 상황이 발생하는 이유

CPU의 계산 결과가 항상 메모리에 써지는 것은 아니다. 실제 CPU 명령어들을 빨리 처리하기 위해 메모리에 접근하는 것을 피하기 때문에 캐시에 두었다가 캐시에서 내보내질 때 메모리에 쓰는 방식 write-back을 많이 사용한다. 이런 경우 각 CPU마다 고유한 캐시를 사용할 경우 메모리에 직접 쓰기 전까지는 동기화가 안 되어 있을 수 있다.

 

기존 release-acquire는 store 이전의 상황이 load 시점에서 보이는 것을 보장하나 그러한 release-acquire 발생 사건이 모든 쓰레드에 보여지는 것은 아니다. 

 

 

예를 들어서

write_x 와 read_x_and_y 쓰레드가 코어 1 에,

write_y 와 read_y_and_x 쓰레드가 코어 2 에

배치되어 있다고 해봅시다.

 

코어 1 에서 write x 가 끝나고 load x 를 한 시점에서 x 의 값은 true 겠지요?

근데 acquire-release sync 는 해당 사실이 (x 가 true 라는게) 코어 2 에서도 visible 하다는 것은 보장되지 않습니다.

 

그쪽 세상에서는 x 가 아직 false 여도 괜찮습니다. 물론 여기서 acquire-release sync 에 위배되는 것은 전혀 없습니다. acquire-release 가 보장하는 것은 store x 전에 무언가 작업을 했다면 load x 할 때 해당 작업이 그 쓰레드에서 보여진다 라는 약속밖에 없죠.

 

 

반면 memory_order_seq_cst 는 어떤 아토믹 객체에 대한 실제 수정 순서가 모든 스레드에게 동일하게 보임을 보장한다.

 

atomoic 객체의 기본 memory_order 플래그의 기본 값은 memory_order_seq_cst이다.

 

 

하지만 memory_order_seq_cst 를 사용하게 된다면
해당 명령을 사용하는 메모리 연산들 끼리는 모든 쓰레드에서 동일한 연산 순서를 관찰할 수 있도록 보장해줍니다.
참고로 우리가 atomic 객체를 사용할 때, memory_order 를 지정해주지 않는다면 디폴트로 memory_order_seq_cst 가 지정이 됩니다. 예컨대 이전에 counter ++ 은 사실 counter.fetch_add(1, memory_order_seq_cst) 와 동일한 연산입니다.

 

 

문제는 멀티 코어 시스템에서 memory_order_seq_cst 가 꽤나 비싼 연산이라는 것입니다.

인텔 혹은 AMD 의 x86(-64) CPU 의 경우에는 사실 거의 순차적 일관성이 보장되서 memory_order_seq_cst 를 강제하더라도 그 차이가 그렇게 크지 않습니다. 하지만 ARM 계열의 CPU 와 같은 경우 순차적 일관성을 보장하기 위해서는 CPU 의 동기화 비용이 매우 큽니다. 따라서 해당 명령은 정말 꼭 필요 할 때만 사용해야 합니다.

#include <atomic>
#include <iostream>
#include <thread>
using std::memory_order_seq_cst;
using std::thread;

std::atomic<bool> x(false);
std::atomic<bool> y(false);
std::atomic<int> z(0);

void write_x() { x.store(true, memory_order_seq_cst); }

void write_y() { y.store(true, memory_order_seq_cst); }

void read_x_then_y() {
  while (!x.load(memory_order_seq_cst)) {
  }
  if (y.load(memory_order_seq_cst)) {
    ++z;
  }
}

void read_y_then_x() {
  while (!y.load(memory_order_seq_cst)) {
  }
  if (x.load(memory_order_seq_cst)) {
    ++z;
  }
}

int main() {
  thread a(write_x);
  thread b(write_y);
  thread c(read_x_then_y);
  thread d(read_y_then_x);
  a.join();
  b.join();
  c.join();
  d.join();
  std::cout << "z : " << z << std::endl;
}

위 코드는 memory_order_seq_cst 로 바꾼 코드이다

 

성공적으로 컴파일 하였다면

실행 결과

z : 2

혹은

실행 결과

z : 1

과 같이 나옵니다. x.store  y.store 가 모두 memory_order_seq_cst 이므로, read_x_then_y  read_y_then_x 에서 관찰했을 때 x.store  y.store 가 같은 순서로 발생해야 합니다. 따라서 z 의 값이 0 이 되는 경우는 발생하지 않습니다.

 

 

즉 다른 CPU 에서 

CPU 1: a -> c
CPU 2: b -> d

이렇게 돌아도 다른 cpu 에서 상대의 공유 값을 가시적으로 볼 수 있다는것(정확히 볼 수 있다는 것)

 

정리해보자면 다음과 같습니다.

연산허용된 memory order

쓰기 (store) memory_order_relaxed, memory_order_release, memory_order_seq_cst
읽기 (load) memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_seq_cst
읽고 - 수정하고 - 쓰기 (read - modify - write) memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst

 

참고로 memory_order_consume 은 다루지 않았는데 C++ 17 현재, memory_order_consume 의 정의가 살짝 수정 중에 있기에 memory_order_consume 의 사용이 권장되지 않습니다.

이렇게 C++ 에서 atomic 연산들에 대해 memory_order 을 지정하는 방법에 대해 알아보았습니다. C++ atomic 객체들의 경우 따로 지정하지 않는다면 기본으로 memory_order_seq_cst 로 설정되는데,

이는 일부 CPU 에서 매우 값비싼 명령 입니다. 만약에 제약 조건을 좀 더 느슨하게 할 수 있을 때 더 약한 수준의 memory_order 을 사용한다면 프로그램의 성능을 더 크게 향상 시킬 수 있습니다.

 

 

ref : https://modoocode.com/271

ref : https://narakit.tistory.com/139

반응형
반응형

코드 재배치 및 메모리 가시성문제 예제(https://3dmpengines.tistory.com/2197)

 

이 글에 이어가기 전에 약간 복습을 하고 가면..

 

동시에 값을 접근하여 읽는건 문제가 되지 않지만 쓸때 문제가 되고 

이때 경합이 일어난다 (Race Condition)

쓸때 아무 처리 하지 않으면 정의되지 않은 행동이 일어난다

 

결국엔 공용데이터에 접근 하려면 lock (mutex) 또는 atomic 연산을 이용하여 해결해야 한다

즉 lock 또는 원자적(Atomic = All or Nothing) 방식 중 하나를 선택해서 해결해야한다

원자적연산이란 더이상 쪼개 질수 없는 연산 즉 한번에 실행 될수 있는 연산을 말한다 꼭 atomic<> 을 말하는 것은 아님

 

 

본론으로 돌아와서...

 

Atomic 에 한하여 모든 스레드가 동일 객체에 대해 동일한 수정 순서를 관찰한다는 법칙이 있다

 

atomic<__int64> num;

void thread_1()
{
	num.store(1);
}

void thread_2()
{
	num.store(2);
}

void threadObserver()
{
	while (true)
	{
		__int64 value = num.load();
	}
}

 

thread_1 , thread_2 에서 값을 num 에 쓸때는 한번에 하나 밖에 쓰지 못한다

그런데 다른 스레드에서 값을 계속 관찰 한다고 했을때 value = num.load()  

thread_1, thread_2 의 스레드에서 연산이 이뤄지고 있어서 서로 다른 스레드에서 다른 값이 어떻게 수정되고 있는지에 대해서는 threadObserver 에서 그 값을 꺼내와서 관찰 할 수가 없다!

 

하지만 이때 동일한 객체에 대해서 동일한 수정 순서를 관찰 하는 것은 가능하다

 

 

A:

 

B:

 

변수에 대한 수정 순서가 0 , 2, 1, 5 의 순으로 수정되는 순서를 갖는다고 할때

A 에서 변수 값을 그 당시 현재 관찰할때 0을 관찰한 상황이라 가정한다

 

시간이 좀 지나서

B: 상태가 되고 화상표 있는 곳 까지 진행 되는 도중에 값을 관찰 할때는 다른 스레드에서는 값이 2로 관찰 될 수도 있고 또는 0으로 관찰 될 수가 있다는 것 (다른 코어 캐시에 의해) 이때 중요한것은 A 에서 이미 값은 0 이였음으로 0 이전에 어떤 값이 있었다면 그 이전으론 당연히 가지 않는다는 것이다, 0 을 포함 미래에 대한 것만 관찰이 가능하다는 것이다

 

그리고 관찰 되는 값은 B: 시점 기준에서

0 2,1,5

0,2 5

0, 1,5 

이렇게 미래에 대한것들로 관찰 될 수가 있다

 

 

 

 

C++ 의 모든 객체들은 수정 순서(modification order) 라는 것을 정의할 수 있습니다. 수정 순서라 하는 것은, 만약에 어떤 객체의 값을 실시간으로 확인할 수 있는 전지전능한 무언가가 있다고 하였을 때, 해당 객체의 값의 변화를 기록한 것이라 보면 됩니다. (물론 실제로 존재하지 않고, 가상의 수정 순서가 있다고 생각해봅시다.)

 

a 의 수정 순서

 

만약에 위 처럼, 어떤 변수 a 의 값이 위와 같은 형태로 변화해왔다고 해봅시다.

 

C++ 에서 보장하는 사실은, 원자적 연산을 할 경우에 모든 쓰레드에서 같은 객체에 대해동일한 수정 순서 를 관찰할 수 있다는 사실입니다.

여기서 강조할 점은 순서 가 동일하다는 것이라는 점입니다.

 

쉽게 말해 어떤 쓰레드가 a 의 값을 읽었을 때, 8 로 읽었다면, 그 다음에 읽어지는 a 의 값은 반드시 8, 6, 3 중에 하나여야 할 것입니다. 수정 순서를 거꾸로 거슬러 올라가서 5 를 읽는 일은 없습니다.

 

모든 쓰레드에서 변수의 수정 순서에 동의만 한다면 문제될 것이 없습니다.

이 말이 무슨 말이냐면, 같은 시간에 변수 a 의 값을 관찰했다고 해서

굳이 모든 쓰레드들이 동일한 값을 관찰할 필요는 없다 라는 점입니다.

 

예를 들어서 정확히 같은 시간에 쓰레드 1 과 2 에서 a 의 값을 관찰하였을 때

쓰레드 1 에서는 5 를 관찰하고,

쓰레드 2 에서는 8 을 관찰해도 문제될 것이 없습니다.

 

심지어, 동일한 코드를 각기 다른 쓰레드에서 실행하였을 때, 실행하는 순서가 달라도 (결과만 같다면) 문제가 안됩니다.

쓰레드 간에서 같은 시간에 변수의 값을 읽었을 때 다른 값을 리턴해도 된다는 점은 조금 충격적입니다.

하지만, 이 조건을 강제할 수 없는 이유는 그 이유는 아래 그림 처럼 CPU 캐시가 각 코어별로 존재하기 때문입니다.

Cache (2) - 코어당 캐시메모리 (https://3dmpengines.tistory.com/2194) 참고

 

 

코어 각각 L1, L2 캐시를 가지고 있다.

 

 

보시다시피, 각 코어가 각각 자신들의 L1, L2 캐시들을 갖고 있는 것을 알 수 있습니다.

따라서, 만약에 쓰레드 1 에서 a = 5 을 한 후에 자신들의 캐시에만 기록해 놓고 다른 코어들에게 알리지 않는다면,

쓰레드 3 에서 a 의 값을 확인할 때, 5 를 얻는다는 보장이 없다는 이야기 입니다.

물론, 매번 값을 기록할 때 마다, 모든 캐시에 동기화를 시킬 수 있겠지만 동기화 작업은 시간을 꽤나 잡아먹는 일입니다. 다행이도, C++ 에서는 로우 레벨 언어 답게, 여러분들이 이를 세밀하게 조정할 수 있는 여러가지 도구들을 제공하고 있습니다.

 

 

 

앞서 이야기 했듯이, C++ 에서 모든 쓰레드들이 수정 순서에 동의해야만 하는 경우는

바로 모든 연산들이 원자적 일 때 라고 하였습니다.

 

원자적인 연산이 아닌 경우에는 모든 쓰레드에서 같은 수정 순서를 관찰할 수 있음이 보장되지 않기에 여러분이 직접 적절한 동기화 방법을 통해서 처리해야 합니다. 만일 이를 지키지 않는다면, 프로그램이 정의되지 않은 행동(undefined behavior)을 할 수 있습니다. ( 즉 한번에 연산 되어야 하는 것에만 해당 된다는 얘기 )

 

 

그렇다면 원자적 이라는 것이 무엇일까요?

이미 이름에서 짐작하셨겠지만, 원자적 연산이란, CPU 가 명령어 1 개로 처리하는 명령으로, 중간에 다른 쓰레드가 끼어들 여지가 전혀 없는 연산을 말합니다. 즉, 이 연산을 반 정도 했다 는 있을 수 없고 이 연산을 했다 혹은 안 했다 만 존재할 수 있습니다. 마치 원자처럼 쪼갤 수 없다 해서 원자적(atomic) 이라고 합니다.

 

하지만 원자적 연산 자체가 바로 문제를 해결해 주진 않는다

 

 

 

 

Atomic 연산은 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 관찰 할수 있게 한다는것이 기본 법칙

 

 

원자적 Atomic 에 대한 예시를 한번 살펴보자

 

 

원자 연산의 특성 1

동일한 수정 순서란 무엇인가?

atomic<__int64> num;

void thread_1()
{
	num.store(1);
}

void thread_2()
{
	num.store(2);
}



//관찰자 스레드로 실시간으로 num 값을 관찰하고 있다고 가정한다
void threadObserver()
{
	while (true)
	{

		//이때 읽어오는 값은 어떻게 되는가?
		//atomic 또한 다른 쓰레드에서 값을 스고 있지만 코어당 캐쉬가 별도 있고  cpu 와 캐쉬 간의 연산할때의 값과 램에있는
		//값을 threadObserver 에서 관찰할떄의 가시성 때문에
		//이 쓰래드에선 순간적으로 num 값이 cpu와 캐쉬에서 현재 연산하고 있는 값과 다를 수 있다
		// 
		//하지만 동일한 수정 순서를 관찰 할수 있는데 이것은 값이 변경 될때 현재와 앞으로의 값중에
		//어떤 값이 될지모르지만 그중에 하나를 얻을수 있다는 것인데 과거 것은 안된다는 것
		//수정순서는 이런 얘기

		__int64 value = num.load();

	}
}

while(true)

{

value  이때 읽어오는 값은 어떻게 되는가?
atomic 또한 다른 쓰레드에서 값을 쓰고 있지만 코어당 캐쉬가 별도 있고  cpu 와 캐쉬 간의 연산할때의 값과 램에있는
값을 threadObserver 에서 관찰할떄의 가시성 때문에
이 쓰래드에선 순간적으로 num 값이 cpu와 캐쉬에서 현재 연산하고 있는 값과 다를 수 있다
  
하지만 동일한 수정 순서를 관찰 할수 있는데 이것은 값이 변경 될때 현재와 앞으로의 값중에
어떤 값이 될지모르지만 그중에 하나를 얻을수 있다는 것인데 과거 것은 안된다는 것
동일한 수정 순서는 이런 얘기

  __int64 value = num.load();

 

 

 

 

원자 연산의 특성 2

 

C++ 에서는 몇몇 타입들에 원자적인 연산을 쉽게 할 수 있도록 여러가지 도구들을 지원하고 있습니다.

또한 이러한 원자적 연산들은 올바른 연산을 위해 굳이 뮤텍스가 필요하지 않습니다! 즉 속도가 더 빠릅니다.

 

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

void worker(std::atomic<int>& counter) {
  for (int i = 0; i < 10000; i++) {
    counter++;
  }
}

int main() {
  std::atomic<int> counter(0);

  std::vector<std::thread> workers;
  for (int i = 0; i < 4; i++) {
    workers.push_back(std::thread(worker, ref(counter)));
  }

  for (int i = 0; i < 4; i++) {
    workers[i].join();
  }

  std::cout << "Counter 최종 값 : " << counter << std::endl;
}

 

void worker(std::atomic<int>& counter) {

가 지역변수인 

std::atomic<int> counter(0);

를 받고 공용으로 수정하려고 함으로 atomic 을 사용 한것 (전역 변수로 사용한것과 엇 비슷한 효과)

 

출력 결과는 

Counter 최종 값 : 40000

 

 

놀랍게도 counter ++; 을 아무런 뮤텍스로 보호하지 않았음에도 불구하고, 정확히 Counter  40000 으로 출력 되었습니다. 원래 counter ++ 을 하기 위해서는 CPU 가 메모리에서 counter 의 값을 읽고 - 1 더하고 - 쓰는 총 3 개의 단계를 거쳐야만 했습니다. 그런데, 여기서는 lock 없이도, 제대로 계산하였지요.

 

 

 

그렇다면 컴파일러는 이를 어떻게 원자적 연산으로 만들었을까요? 이를 알기 위해서는 다시 컴파일러가 어떤 어셈블리 코드를 생성했는지 살펴봐야 합니다.

붉은색 테두리가 counter ++ 부분이다.

놀랍게도 counter ++ 부분이 실제로 어셈블리 명령 한 줄인

  lock add DWORD PTR [rdi], 1

로 나타남을 알 수 있습니다. 원래 CPU 는 한 명령어에서 메모리에 읽기 혹은 쓰기 둘 중 하나 밖에 하지 못합니다. 메모리에 읽기 쓰기를 동시에 하는 명령은 없습니다. 하지만, 이 lock add 의 경우 rdi 에 위치한 메모리를 읽고 - 1 더하고 - 다시 rdi 에 위치한 메모리에 쓰기를 모두 해버립니다.

참고로 이러한 명령어를 컴파일러가 사용할 수 있었던 이유는 우리가 어느 CPU 에서 실행할 지 (x86) 컴파일러가 알고 있기 때문에 이런 CPU 특이적인 명령어를 제공할 수 있던 것입니다. 물론, CPU 에 따라 위와 같은 명령이 없는 경우도 있습니다.

이 경우 CPU 는 위와 같은 원자적인 코드를 생성할 수 없습니다. 이는 해당 atomic 객체의 연산들이 과연 정말로 원자적으로 구현될 수 있는지 확인하는 is_lock_free() 함수를 호출해보면 됩니다. 예를 들어서

 

std::atomic<int> x;
std::cout << "is lock free ? : " << boolalpha << x.is_lock_free() << std::endl;

를 실행해보면

실행 결과

Is lock free ? : true

와 같이 나옵니다. 여기서 lock free  lock 과 실제 어셈블리 명령에서의 lock 과는 다른 lock 을 의미합니다.

위 어셈블리 명령어에서의 lock 은 해당 명령을 원자적으로 수행하라는 의미로 사용되고, 

lock free 에서의 lock 이 없다 라는 의미는 뮤텍스와 같은 객체들의 lock, unlock 없이도 해당 연산을 올바르게 수행할 수 있다는 뜻입니다.

 

 

 

struct Knight

{

  int32 level;

  int32 hp;

  int32 mp;

};

 

atomic<Knight> v;

cout<< v.is_lick_free() << endl;

 

이 결과는 false 가 나오는데 즉 원자적으로 연산을 일반적으론 할 수없어서 atocmi<> 내에서 뮤텍스처럼 lock 을 잡고 값을 수정한다음 다시 unlock 하는 식으로 내부적으로 구현 처리가 된다

 

 

 

ref : https://modoocode.com/271

반응형
반응형

 

아래 코드를 실행 하면 이코드는 싱글 스레드에선 실행되지 않는 코드 이지만

코드 재배치 또는 메모리 가시성 문제로 인하여 멀티 스레드 환경에서

실행 되는 상황이 발생 될 수도 또는 발생 되지 않을 수도 있다

 

아래 코드가 종료 되는 이유는 크게 두가지가 있다

 

먼저 알아야 햐는 부분이 각 코어마다 별도의 캐쉬를 갖고 있는다
(https://3dmpengines.tistory.com/2194)


문제가 될만한 가능성은 두가지이다

  1. 가시성 (https://3dmpengines.tistory.com/2195)
    = 캐쉬와 램 사이에 데이터가 보이는가

    cpu 가 어떤 값을 쓰거나 읽을때 램까지 가서 읽어올수도 있는데
    이것은 캐쉬에 없을때 얘기다 , 이때 CPU 는 각 코어마다 별도의 캐쉬를 갖고 있는데 
    실제 램까시 가서 데이터를 불러와서 변수에 쓴건지에 대한 보장이 없을 수 있다
    코어와 스레드는(t1) 하나로 묶여서 연산 되고 cpu 에서 캐쉬의 데이터를 연산을 하면서 아직 Ram 에 데이터를 쓰지 않았을 수 있는데
    이때 다른 쓰레드(t2)에서 현재 t1 에서 아직  공유변수의 연산중인 값의 최종을 모르고 이전 램에 있는 값만 알기 때문에 값이 정확하게 일치하지 않는다는 문제가 비가시성을 말함

  2. 코드 재배치 (https://3dmpengines.tistory.com/2196)
    = 결과가 동일하다면, 컴파일러가 코들르 보고 더 빠를 것 같으면 코드 순서를 바꿀 수도 있다
     이것 때문에 값이 제대로 원하는 값이 아닐 수도 있다, 코드로 보는것과 달리
    그런데 이 코드 재배치는 컴파일러 뿐만 아니라 CPU 에서 멋대로 제배치 할 수도 있다


과연 컴파일러만 재배치를 할까?

한 가지 더 재미있는 점은, 꼭 컴파일러만이 명령어를 재배치하는게 아니라는 점입니다. 예를 들어서 다음과 같은 두 명령을 생각해봅시다.

 C/C++ 확대 축소
// 현재 a = 0, b = 0;
a = 1;  // 캐시에 없음
b = 1;  // 캐시에 있음

a = 1 의 경우 현재 a 가 캐시에 없으므로, 매우 오래 걸립니다. 반면에 b = 1; 의 경우 현재 b 가 캐시에 있기 때문에 빠르게 처리할 수 있겠지요. 따라서 CPU 에서 위 코드가 실행될 때, b = 1;  a = 1; 보다 먼저 실행될 수 있습니다.

따라서, 다른 쓰레드에서 a 는 0 인데, b 가 1 인 순간을 관찰할 수 있다는 것입니다.

 

 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <future>
#include "windows.h"

using namespace std;


int32 x = 0;
int32 y = 0;
int32 r1 = 0;
int32 r2 = 0;


volatile bool ready;


void Thread_1()
{
	while (!ready)
	{
		volatile int i = 39;
		i += 10;
	}
    
    //가시성 관점으로 봤을때 특정 시점에서 이것 또한 램까지 가서 
    //y 에 데이터를 썼다는 보장이 없을 수 있다 
	y = 1;	//store y	
	r1 = x;	//load x
}

void Thread_2()
{
	while (!ready)
	{
		volatile int i = 39;
		i += 10;
	}
	x = 1;
    
    //가시성 문제로 봤을때 특정 시점에서 y 는 1이 아닐 수도 있다
	r2 = y;				
}

int main()
{

	int32 count = 0;
	while (true)
	{
		ready = false;
		count++;

		x = y = r1 = r2 = 0;
		thread t1(Thread_1);
		thread t2(Thread_2);
		ready = true;

		t1.join();
		t2.join();

		if (r1 == 0 && r2 == 0)
		{
			break;
		}
	}

	std::cout << count << std::endl;



	return 0;
}

 

ready 하단에 들어간 volatile 은 테스트를 위해 의미 없이 넣은 코드조각이다

 

ref :https://modoocode.com/271

반응형
반응형

 

 

아래 4개의 빨래 일감이 있는데 빨래 하나만 들고 빨래가 다 끝나면 다리미 하고 건조 하고 여기 까지 완료 된다음 다음빨래를 들고가면 비효율적이니 빨래 하나를 세틱기 하나에 넣고 그다음 빨래도 세틱기에 넣고

앞의 빨래가 끝나면 건조기에 넣고 바로 그다음 빨래를 세틱기에 넣고 하는 식으로 낭비 없이 명령어를 처리 하는것이 CPU 파이프라인이다


 그래서 생각해본 방법 중 하나가 한가지 작업을 수행한 후에는 다른 Instruction을 동시에 수행할 수 있게 하자는 아이디어다. 그림으로 표현하면 다음과 같다.

 

 

어차피 동시에 같은 작업을 수행하는 게 아니라면 순차적으로 다음 instruction이 수행할 수 있도록 배려하자는 것이다. 이러면 D가 끝나기 까지 걸리는 시간이 앞의 Sequential Execution 방식에 비해 약 절반으로 줄어들게 된다. 마치 이런 방식이 pipeline에 Instruction을 꾸역꾸역 집어넣는 방식같다고 해서 Pipelined Execution 이라고 한다. 꾸역꾸역 집어넣으니 먼저 집어넣은 Instruction은 먼저수행되고 빈틈없이 다음 Instruction이 수행될 것이다. 그러면 맨 앞에서 언급했던 것처럼 세탁기와 건조기가 Time을 Waste하는 일은 없을 것이다. 이걸 Computer에서는 다음과 같이 구현된다.

 

 

결론적으로 pipeline을 통해서 추구하고자 했던 바는 한번에 처리할 수 있는 instruction의 수(stage)를 늘림으로써 instruction에 대한 throughput, 즉 정해진 시간동안 instruction을 얼마나 처리할 수 있는지를 높이기 위해서 였다고 할 수 있다. 유의할 것은 한개의 instruction이 처리되는 속도가 증가하는 것이 아니라 동시에 처리할 수 있는 instruction 수를 늘려서 전체 instruction이 처리되는 속도를 증가시키는 효과를 얻는 것이다. 참고로 위의 예시는 MIPS의 5-stage pipelined 구조이고, Pentium 4의 pipeline은 31 stage였다. 그런데 연구결과 무조건 stage만을 늘려서 효율성을 얻기에는 한계가 있어서 다른 부분을 발전시키는 방향으로 발전해가고 있으며, 현재 출시된 core i7의 stage는 16이다. 

 


 

 

그런데 각각 빨라마다 걸리는 시간이 다를 수 있어서 어떤걸 먼저 넣고 어떤걸 나중에 넣어야 더빠른 상황이 될 수 있는데 이와 같은 상황이 코드 재배치에 해당한다 (컴파일러와 CPU 에서 명령어를 바꾸는 상황)

 

CPU 파이프라인은 위 처럼 생겼는데 

명령 처리가  명령을 읽어오는 Fetch ,  명령을 해석하는 Decode, 명령을 실행하는 Execute, Register Write Back (WB)

이때 명령어를 바꿔서 더 빠르게 동작하게 할수 있다면 바꿔주는 것이 코드 재배치

 

즉 결과가 동일하다면, 컴파일러가 코들르 보고 더 빠를 것 같으면 코드 순서를 바꿀 수도 있다
 이것 때문에 값이 제대로 원하는 값이 아닐 수도 있다, 코드로 보는것과 달리
그런데 이 코드 재배치는 컴파일러 뿐만 아니라 CPU 에서 멋대로 제배치 할 수도 있다

 

 

- Instruction Fetch (IF)

-> Instruction Decode / Register Fetch (ID)

-> Instruction Execution (EX)

-> Memory Access (MEM)

-> Register Write Back (WB)

 

 

 

 

ref : https://talkingaboutme.tistory.com/entry/Study-Pipeline

반응형
반응형

프롤로그

 가시성과 원자성 이 두가지를 이해 하면 Multi-thread프로로그램을 작성할 때 무엇을 주의해하는지 명확해진다. 또 다른 표현으로 설명 하자면 단일 Thread 프로그램에서는 가시성과 원자성을 괘념치 않아도 프로그램 작성하는데 문제가 없다.  그렇다고 해서 가시성과 원자성이 Multi-thread를 할 때만 뚜둥 나타나는 개념은 아니라는점 명확히 밝혀둔다. 하드웨어를 설계한 사람들에 의해 만들어진 원래 컴퓨터 내의 구조에 관한 이야기다. 이번 편에서는 지난편에서 설명한 이 두 가지 개념중에 가시성을 좀더 깊게 파고들어보려 한다.

 

비 가시성(가시성 이슈)

 다른 자료나 책을 통해 가시성에 대한 개념을 이해하고 있는 독자라면 필자가 외 비 가시성이라는 반대가 되는 단어를 만들어 썼는지 감이 왔을것이다. 지난 편에도 아래 그림이 등장했는데 다시한번 고찰 해보자. 

 


비 가시성 이슈

 

 각기 다른  Thread 2개는 CPU 1과 CPU2를 할당 받아 공유자원에 해당하는 변수를 연산한다.

이 때 메인 메모리에서 값을 읽어 연산에 사용하는 것이 아니라 가 CPU에 존재하는 Cache에 옮겨놓고 연산을 한다.

그 사이에 다른 쓰레드에서 같은 변수를 대상으로 연산을 한다. 이 때 Thread는 타 Thread가 사용하고 있는 CPU의 Cache상의 값을 알지 못한다. 언제 Cache의 값이 메인 메모리로 쓰여질지도 모른다.

그래서 아래코드를 돌려보면 각각의 Thread가 100회씩 변수를 증가연산을 실시 했는데 200에 휠씬 못 미치는 103 ~ 105쯤이 나타난다. (이 실험 결과는 하드웨어의 성능에 따라 상이 할 수 있으니 참고 바란다.) 만약 동시에 시작하는 Thread가 아닌 순차 처리라면 200이 나와야 하는 현상이다. Cache에 담아서 연산을 하더라도 바로 바로 메모리에 적용을 했으면 200은 아니더라도 180~190정도는 날올것 같은데 너무나도 100가까운 결과에 놀라는 독자도 있을 것이다. 다음 단계에서 이 문제를 조금(?) 해결 해보자.

 

 

가시성이란 멀티 코어의 캐시(Cache) 간 불일치의 문제를 말하는데, 하나의 스레드에서 특정한 변수의 값을 수정하였는데, 다른 스레드에서 그 수정된 값을 제대로 읽어 들인다는 보장이 없다.

(참고로 C++의 volatile은 JAVA와는 달리 가시성 문제를 해결해주지 않는다. 단순히 컴파일러 최적화를 막기만 함)

 

* Java 에서 가시성 관련된 이야기 시작
암튼 JAVA에선 위에서 설명한 비 가시성 이슈를 해결하기 위해 JAVA 1.4 부터 volatile을 지원하기 시작 했다. 변수를 선언 할 때 해당 단어를 앞에 써주기만 하면 되는데 이렇게만 해도 위 테스트 코드가 거의 200을 반환한다. 
 (사실 이 결과는 미신같은 이야기다. 정확히 이야기 하면 200이거나 200보다 작은 값을 반환한다.) 원리는 이렇다. 
Java 에선 volatile로 선언된 변수를 CPU에서 연산을 하면 바로 메모리에 쓴다. (Cache에서 메모리로 값이 이동하는 것을 다른 책이나 문서에서는 flush라고 표현한다.) 그러니 운이 좋게 Thread 두 개가 주거니 받거니 하면서 증가를 시키면 200에 가까운 결과를 얻어내는 것이다.

 하지만 개발자라면 이 미신같은 결과에 흡족해 하면 안된다. 필자의 PC를 기준으로 각 Thread당 100회가 아닌 1000회정도 연산을 시키면 2000이 아닌 1998같은 결과를 얻어낸다. 
이 이야기인 즉 가시성이 확보된다 하더라도 원자성 문제(동시에 같은 값을 읽어다 증가시키고 flush하는...)로 인해 이와 같은 문제가 생기는 것이다. 

이 문제는 원자성 다루면서 해결 해보자.

* Java 에서 가시성 관련된 이야기 끝

 

 

 

아래 코드는 자바이지만 제대로 되지 않는 다는 정도만 보면 된다

private int count = 0;

    @Test
    public void Test_AtomicIssue() {
        ExecutorService es = Executors.newFixedThreadPool(2);

        es.execute(new ForThreadTest());
        es.execute(new ForThreadTest());

        es.shutdown();

        try {
            es.awaitTermination(10, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("TEST result >>> " + count);
    }

    class ForThreadTest implements Runnable {
        @Override
        public void run() {
            for(int i = 0 ; i < 100; i++) {
                AtomicStampedRefTest.this.count++;
            }
        }
    }

 

 

 

ref :https://marmelo12.tistory.com/320

ref : https://rightnowdo.tistory.com/entry/JAVA-concurrent-programming-Visibility%EA%B0%80%EC%8B%9C%EC%84%B1

반응형
반응형

모든 최신 프로세서는 소량의 캐시 메모리를 특징으로 한다. 지난 수십 년 동안 캐시 아키텍처는 점점 더 복잡해졌다. CPU 캐시의 레벨이 증가했고, 각 블록의 크기가 커졌으며, 캐시 연관성도 몇 가지 변화를 겪었다. 자세한 내용을 살펴보기 전에, 메인 메모리와 캐시 메모리의 차이점은 무엇인가? 이미 RAM을 가지고 있는데 왜 소량의 캐시 메모리가 필요하십니까? 한 마디: 속도!

 

 

캐시 메모리 대 시스템 메모리: SRAM VS DRAM

캐시 메모리는 훨씬 더 빠르고 비싼 정적 RAM을 기반으로 하며 시스템 메모리는 더 느린 D램(Dynamic RAM)을 활용한다. 두 가지 주요 차이점은 전자가 CMOS 기술과 트랜지스터(블록당 6개)로 만들어진 반면 후자는 콘덴서와 트랜지스터를 사용한다는 점이다.

 

데이터를 장기간 보존하려면 D램을 지속적으로 새로 고쳐야 한다. 이 때문에 훨씬 더 많은 전력을 소비하고 또한 더 느리게 된다. SRAM은 새로 고칠 필요가 없고 훨씬 더 효율적이다. 그러나 가격이 높아져 주류 채택이 금지되어 프로세서 캐시로 사용이 제한되고 있다.

 

프로세서에서 캐시 메모리의 중요성?

현대의 프로세서는 80년대와 90년대 초반의 원시 조상들보다 몇 년 앞선다. 오늘날 대부분의 DDR4 메모리 모듈은 1800MHz 미만으로 정격인 반면, 최고급 소비자 칩은 4GHz 이상에서 작동한다. 그 결과 시스템 메모리가 너무 느려서 CPU를 심하게 늦추지 않고 직접 작업할 수 없다. 캐쉬 메모리가 들어오는 곳이 바로 여기에 있다. 그것은 반복적으로 사용된 데이터의 작은 덩어리를 저장하거나 경우에 따라 그 파일의 메모리 주소를 저장하면서 둘 사이의 중간 역할을 한다.

 

 

L1, L2 및 L3 캐시: 차이점은?

현대 프로세서에서 캐시 메모리는 크기를 증가시키고 속도를 감소시키기 위해 L1, L2, L3 캐시의 세 부분으로 나뉜다. L3 캐시는 가장 크고 가장 느리다(3세대 Ryzen CPU는 최대 64MB의 L3 캐시를 특징으로 한다). L2와 L1은 L3보다 훨씬 작고 빠르며 각 코어에 대해 분리되어 있다. 구형 프로세서에는 3단계 L3 캐시가 포함되지 않았으며 L2 캐시와 직접 상호 작용하는 시스템 메모리:

 

L1 캐시는 L1 데이터 캐시와 L1 명령 캐시의 두 가지 섹션으로 더 세분된다. 후자는 CPU에 의해 실행되어야 하는 지시사항을 포함하고 전자는 메인 메모리에 다시 기록될 데이터를 보유하는 데 사용된다.

 

L1 캐시는 명령 캐시 역할을 할 뿐만 아니라 사전 디코드 데이터와 분기 정보도 가지고 있다. 또한 L1 데이터 캐시가 출력 캐쉬 역할을 하는 경우가 많지만 명령 캐시는 입력 캐쉬처럼 동작한다. 이는 필요한 지침이 가져오기 장치 바로 옆에 있으므로 루프가 체결될 때 유용하다.

 

 

최신 CPU는 플래그십 프로세서의 경우 최대 512KB의 L1 캐시(코어당 64KB)를 포함하며 서버 부품은 거의 두 배나 많은 기능을 한다.

 

L2 캐시는 L1보다 훨씬 크지만 동시에 느리다. 플래그십 CPU에서 4~8MB(코어당 512KB)로 다양하다.

각 코어는 자체 L1 및 L2 캐시를 가지고 있으며, 마지막 레벨인 L3 캐시는 모든 코어에 걸쳐 공유된다.

 

L3 캐시는 가장 낮은 수준의 캐시다. 10MB에서 64MB까지 다양하며, 서버 칩은 256MB의 L3 캐시를 특징으로 한다. 더욱이 AMD의 라이젠 CPU는 경쟁사인 인텔 칩에 비해 캐시 크기가 훨씬 크다. 인텔 측의 MCM 디자인 대 모놀리성 때문이다. 자세한 내용은 여기를 참조하십시오.(https://www.flayus.com/55228516

 

)

 

CPU가 데이터를 필요로 할 때, 먼저 관련 코어의 L1 캐시를 검색한다. 발견되지 않으면 다음에 L2와 L3 캐시가 검색된다. 필요한 자료가 발견되면 캐시히트라고 한다. 한편, 캐시에 데이터가 없을 경우 CPU는 메인 메모리나 스토리지에서 캐시에 로딩되도록 요청해야 한다. 이것은 시간이 걸리고 성능에 악영향을 미친다. 이것을 캐시 미스라고 한다.

 

일반적으로 캐시 크기가 증가하면 캐시 적중률이 향상된다. 특히 게임 및 기타 지연 시간에 민감한 워크로드의 경우 더욱 그러하다.

 

 

메모리 매핑

기본적인 설명은 생략하고, 시스템 메모리가 캐시 메모리와 어떻게 대화하는지에 대해 이야기해 보자. 캐시 메모리는 블록으로 나뉜다. 이 블록들은 n개의 64바이트 선으로 나누어 회전하고 있다.

시스템 메모리는 캐시와 동일한 수의 블록으로 나뉘고 그 다음 두 개가 연결된다.

 

1GB의 시스템 RAM이 있으면 캐시는 8192줄로 나뉘고 블록으로 분리된다. 이것을 n-way 연관 캐시라고 한다. 2방향 연관 캐시의 경우, 각 블록에는 각각 2개의 선이 있으며, 4방향에는 각각 4개, 8방향에는 8개, 16개 라인이 포함되어 있다. 총 RAM 크기가 1GB인 경우 메모리의 각 블록 크기는 512KB가 된다.

 

512KB의 4방향 관련 캐시가 있는 경우, RAM은 2,048개의 블록(1GB의 경우 8192/4)으로 나뉘며 동일한 수의 4라인 캐시 블록에 연결된다.

 

 

16방향 연관 캐시와 동일한 방식으로, 캐시는 메모리의 512(2048KB) 블록에 연결된 512개의 블록으로 나뉘며, 각 캐시 블록은 16개의 선을 포함한다. 캐시에 데이터 블록이 부족하면 캐시 컨트롤러는 프로세서 실행을 계속하는 데 필요한 데이터가 포함된 새로운 블록 집합을 다시 로드한다.

 

N-way 연관 캐시는 가장 일반적으로 사용되는 매핑 방법이다. 직접 매핑과 완전히 연관된 매핑으로 알려진 두 가지 방법이 더 있다. 전자에서는 캐시 라인과 메모리 사이에 하드 링크가 있는 반면 후자의 경우 캐시는 어떤 메모리 주소도 포함할 수 있다. 기본적으로 각 라인은 어떤 메인 메모리 블록에도 접근할 수 있다. 이 방법은 적중률이 가장 높다. 하지만, 구현하는 것은 비용이 많이 들고 대부분 반도체 제조업체들이 피한다.

 

완전히 연결된 매핑

 

ref : https://www.flayus.com/it/55228456

반응형
반응형

콤마 사용법 - 주절과 독립

 

 as 부서절이 먼저오고 , there is 주절이 온 케이스임

여기서 원래는 주절이 먼저 앞에 와야 하는데

부사절이 앞에 오게 되면 원래는 도치가 되야한다

그래서 부사절, 동사 + 주어 이렇게 되야 하는데

 

하지만 도치 되는것을 막기 위해서 , 가 오게 되는것!!!!

그래서 위 문장도 보면 as ~ , 콤마가 와서 there is 순서가 된것

 

 

독립 부사절에서 독립이란 얘기는 , 콤마 때문에 생겨 난 것이다 

 

독립 부사절이나 독립 부사구를 앞으로 보내서 콤마를 찍어 끊어 버려서 도치가 되지 않게 한다

 

 

 

[1] 같은 품사 또는 동질의 문장을 배열/나열/열거 한다

[2] 삽입 역할을 해준다 

     독립 부사절(구) - 계속적 용법 , 동격 이라는 것이 있다 [동격은 독립 부사절 분류에 들어가게 된다!!]

     관.대 형용사 이런게 오게 된다

[3] 주절과의 독립

 

반응형
반응형

[2] 삽입 되는 경우

삽입 되는 경우에는 크게 두가지가 있음

1. 독립 부사절(독립 분사구문) 이 오는 경우와

2. 관계대명사가 이끄는 형용사절 

이렇게 크게 두가지가 있다

 

(부사절에 대한 설명은 약간 아래에..)

 

2. 관계대명사가 이끄는 형용사절 

(1) 계속적인 용법

계속적 용법은 

~, 관.대   and/but + 대명사 가 오는 것을 계속적 용법이라 함

 

이전편에서 보면 , 는 순서적으로 그다음을 말함

만약 , 가 없다면 제한적용법으로 범위를 제한 하겠다는 뉘앙스

 

 

The importance of this can hardly be exaggerated, for whole new 

industries were emerging to exploit and develop the leisure market, 

which was to become a huge source of consumer demand, employment, and profit.

 

 

 

, for whole new industries were emerging to exploit and develop the leisure market, 

여기 가지가 삽입형태이며 독립 부사절이다 그리고 

끝에 , 가 오고 which 가 오는데 => 관계 대명사의 계속적 용법이 된다

 

 

 

 

부사절 이란?

☆부사절이란 종속절 중에 '명사절 & 형용사절' 을 제외한 나머지 전부라고 생각하면 됩니다. 즉, 종속절이 문장속에서 그 문장의 주어(S),목적어(O),보어(C) 역할을 하면 명사절, 앞에 있는 선행사를 꾸며주면 형용사절, 나머지 모두는 부사절이라 합니다.

 

부사절 접속사와 전치사의 구분

부사절은 절이기 때문에 반드시 주어와 동사가 있어야 합니다. 즉 부사절 접속사 다음에 주어와 동사가 옵니다.

 

. '때' 를 나타내는 부사절

* when + 주어 + 동사 ...

   주어가 ~할 때에

* as + 주어 + 동사 ...

   주어가 ~할 때에

 

2. '조건' 의 부사절

* if + 주어 + 동사 ...

만약 주어가 ~한다면

* unless + 주어 + 동사 ...

만약 주어가 ~하지 않는다면

* in case + 주어 + 동사 ...

 

3. '원인, 이유' 의 부사절

* because + 주어 + 동사...

   = as + 주어 + 동사...

   = since + 주어 + 동사...

      주어가 ~하기 때문에

*now that + 주어 + 동사...

  이제 주어가 ~하니까

 

4. '목적, 결과' 의 부사절

* so that + 주어 + 동사...

   = in order that + 주어 + 동사...

      주어가 ~하기 위하여/~할 수 있게...

* so + 형 + 명 + that + 주 + 동...

   = such a + 형 + 명 + that + 주 + 동...

    매우 어떠한 무엇이어서 주어가 ~한다

 

등등등..

 

 

 

다시 콤마로 돌아와서..

 

 

 

 

[3] 동격인경우의 콤마

 

For example, the energy output from solar panels or wind power
engines, where most investment happens before they begin
producing, may need to be assessed differently when compared
to most fossil fuel extraction technologies, where a large
proportion of the energy output comes much sooner, and  a
larger (relative) proportion of inputs is applied during the
extraction process, and not upfront.

 

 

동격이란

Jim , my freind , was ~

에서 , my freind , 이 동격으로 들어간것이고 원래 문장은  

Jim , (who was)  my freind , was ~ 로 주격 관계 대명사가 생략된 문장이다

 

, where most investment happens before they begin
producing,  는 삽입으로 들어간 부분이다

 

즉 where 이 engines 를 수식 하는 형태가 된다 동격이됨

where 는 이때 in which 로 바꿔 쓸 수 있다

 

 

그런데 

 technologies, where a large
proportion of the energy output comes much sooner, and  a
larger (relative) proportion of inputs is applied during the
extraction process, and not upfront.

 

, where 부터 이 부분도 삽입이다, 삽입은 중간에만 오는것이 아닌 맨 끝에도 올 수 있다

where 부터 upfront 까지가 삽입 구문이다

 

 technologies, where a large
proportion of the energy output comes much sooner,

where는  solar panels or wind power engines, 를 수식 하는게 아니고 

바로 앞의 technologies를 수식 하는 것 

동격임으로  

 

upfront :  선행, 선불의

and not upfront : 선불이 아닌 (초기 투자비용은 아닌)

 

, and  a larger (relative) proportion ..

 

이 부분을 어떻게 봐야 할지 생각 할수 있는데 문장의 맥락상 where 삽입 구문과 이어지는 맥락이라는 것을 알 수 있다

 

 

콤마 정리

[1] 같은 품사 또는 동질의 문장을 배열/나열/열거 한다

[2] 삽입 역할을 해준다 

     1.독립 부사절(구) => (계속적 용법 , 동격) 이라는 것이 있다 [동격은 독립 부사절 분류에 들어가게 된다!!]

     2. 관.대 형용사 이런게 오게 된다

[3] 주절과의 독립 (이건 나중에..)

 

 

 

 

부사절 : 

ref : https://www.englishcube.net/grammar_view.php?category1=20&category2=147

ref : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=ddanddan84&logNo=221234982203

반응형
반응형

[1] 같은 품사 또는 동질의 문장을 배열/나열/열거에 사용한다

 

 

1. A, B, and C

이렇게 쓰지만 두개 일때는 A, B 이렇게는 쓰지 않는다

굳이 ;쓴다면 A , and B 이렇게 쓴다

 

2. A and B, C and D, and E and F (이게 기본 로직)

이렇게 콤마를 쓸 수 있는데 

마지막에 한 덩어리를 쓸때 and 를 쓰고 E and F 를 쓰기 때문에

보통 이걸 A, B, C and D 이렇게 끝에만 and 접속사를 써서 연결해 준다

 

3. I like Tom and Jim  

I like Tom, and Jim   이 아닌 이유는 콤마를 찍었다면 순서가 생기게 된다

콤마가 있다면

나는 톱을 좋아한다 그리고 짐을 좋아한다인데 순서대로 Tom 을 좋아하고 그다음으로 Jim 을 좋아한다가 되는데

위 문장은 순서가 필요 없다는 것을 말하려 의도가 있는 것임으로 순서 상관없이 동시에 좋아하는 것을 말한다

 

 

4. I like Tom, so I want to travel with him.

, 앞에도 주절이고, 뒤에도 주절이왔다 => 동질의 문장이 앞뒤로 온건데

이때 콤마는 순서가 있는것 그래서 나는 톱을 좋아하고 그다음에 그래서 나는 그와 여행하길 원한다가 됨

 

 

Wat has its own strategic, tractical, and other rules and points of view, but (they all....  주절)

 

이 문장을 묶을때 아래 처럼 묶게 된다면..

Wat has (its own strategic, tractical, and other rules) and points of view, but (they all....  주절)

이렇게 보게 되면 잘못 본것임!! 이렇게 묶는게 아니고

 

위 2번 룰에 따라서 기본 로직인  A and B, C and D, and E and F (이게 기본 로직)

A and B 와 C and D 는 각 한 단어씩만 와도 됨

다시 정리해 보면 다음 처럼 볼 수 있다

Wat has its own  A, B, and  A and B, but (they all....  주절)

 

 

Wat has its own strategic, tractical, and other rules and points of view, but (they all....  주절)

그래서 strategic, tractical 다음에 마무리하는 and 가 오고

그다음 마지막에 하나만 온 것이 아닌 한 덩어리인 other rules and points of view(= E and F) 가 온 것

 

 

만약 Wat has (its own strategic, tractical, and other rules) and points of view, but (they all....  주절)

이렇게 묶을려면 중간에 and 가 없어야 함 그리고 뒤의 and 앞에 , 를 추가해줘야 함

Wat has (its own strategic, tractical, other rules) , and points of view, but (they all....  주절)

이렇게 해야 2번 룰에 맞아 떨어짐 and  앞에 , 추가 해서 


마지막 but 앞에 콤마가 있음으로 앞에 있는걸 한다음 그리고 그다음으로 but 의 내용의 순서로 진행 되는 것을 말한다

 

presuppose : 예상하다, 추정하다

as to ~ 에 관해

 

 

 

[2] 삽입 되는 경우

삽입 되는 경우에는 크게 두가지가 있음

1. 독립 부사절(독립 분사구문) 이 오는 경우

2. 관계대명사가 이끄는 형용사절 

이렇게 크게 두가지가 있다

 

 

'Leisure' as a distinct non-work time, whether in the form of the holiday, weekend, or evening, was a result of .......

 

'Leisure' as a distinct non-work time, (whether in the form of the holiday), weekend, or evening, was a result of .......

중간에 , , 가 있으니 (whether in the form of the holiday)  이걸 날리고  weekend, or ..

이렇게 생각 보는게 아니고

 

Leisure 가 주어 ... was 가 동사임 그래서 , whether 부터 evening 까지가 삽입 구문인데

 

,whether in the form of the holiday weekend, or evening,

 

whether 의 주어가 없다!

그렇다면 whether  의 주어는 같아서 생략된 것임으로 

,whether (Leisure) in the form of the holiday, weekend, or evening,

동사는 뒤의 was 에 따라서 

,whether (it was) in the form of the holiday, weekend, or evening,

이렇게 된다

 

여기서 was 는 뒤의 문장이 아니라 Leisure 주어와 was 동사 사이에 부사절이 삽입 된것으로 원래의 주정의 동사를 그대로 whether 에서 사용 하는것 하지만 이걸 생략 한 것이다

it was 단순+능동=> being 이 생략된것

 

그럼 다음 처럼 볼 수 있는데

,whether (it was) in the form of the holiday, weekend, or evening,

 

holiday 다음에 콤마 그다음 weekend , 다음에 or evening 인데 

evening 앞에  or 가 있음으로 holiday , 에서 , 는 or 로 볼 수 있다

즉 (holiday, weekend, or evening,) 이 구간에서 콤마들은 or로 보면 된다

 



즉 삽입 되는 부사절의 경우 삽입된 부사절 안에서도 , , 가 들어갈 수 있다는것 , 주어 동사가생략 되면서

그리고 whether 또한 의미에 큰 차이가 없으면 생략 될 수도있다

 

created by capitalist production 은 

 

(which was) created by capitalist production 가 생략 된것이고 was 는 위의이유와 같은 이유로 was 가 온다

 

 

 

 

 

반응형
반응형

메모리는 엄청 느리다.

기본적으로 CPU 와 컴퓨터 메모리인 RAM 은 물리적으로 떨어져 있습니다. 따라서 CPU 가 메모리에서 데이터를 읽어 오기 위해서는 꽤 많은 시간이 걸립니다. 실제로, 인텔의 i7-6700 CPU 의 경우 최소 42 사이클 정도 걸린다고 보시면 됩니다. CPU 에서 덧셈 한 번을 1 사이클에 끝낼 수 있는데, 메모리에서 데이터 오는 것을 기다리느라, 42 번 덧셈을 연산할 시간을 놓치게 되는 것입니다.

이는 CPU 입장에 굉장한 손해가 아닐 수 없습니다. 메모리에서 데이터 한 번 읽을 때 마다 42 사이클 동안 아무것도 못한다니 말입니다.

 

이래서 캐시가 존재하는 것

 

cache

1) Temporal Locality : 방금 사용한것을 또 사용할 확률이 높다는 것

2) Spatial Locality : 한번 사용한 메모리 근방에의 것들 다시 사용할 확률이 높다는 것

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <future>
#include "windows.h"

using namespace std;


int32 buffer[10000][10000] = { {0,} ,};

int main()
{

	{
		uint64 start = GetTickCount64();

		int64 sum = 0;
		for (int32 i=0;i<10000;++i)
		{
			for (int32 j = 0; j < 10000; ++j)
			{
				sum += buffer[i][j];
			}
		}

		uint64 endt = GetTickCount64();
		std::cout << endt - start << std::endl;
	}

	{
		uint64 start = GetTickCount64();

		int64 sum = 0;
		for (int32 i = 0; i < 10000; ++i)
		{
			for (int32 j = 0; j < 10000; ++j)
			{
				sum += buffer[j][i];
			}
		}

		uint64 endt = GetTickCount64();
		std::cout << endt - start << std::endl;
	}
	


	return 0;
}

 

위 코드 실행 결과를 보면 다음과 같은 시간 차이를 보인다

 

i, j 순서만 바꿨을뿐인데 왜 시간 차이가 이렇게 나는 것인가?

 

2차원 배열은 사실상 1차원 배열이다 

하지만 j 칸 을 앞부분 배열 인덱스로 지정해주면 근방의 메모리가 캐쉬에 있지 않기 때문에 캐쉬 로스로 이런 성능 저하가 발생한다 => 캐쉬에 없다면 램 까지 갔다 와야 함으로 느려짐

 

 

각 코어는 자체 L1 및 L2 캐시를 가지고 있으며, 마지막 레벨인 L3 캐시는 모든 코어에 걸쳐 공유된다.

자세한 설명은 Cache (2) (https://3dmpengines.tistory.com/2194)

 

 

ref : https://modoocode.com/271

반응형
반응형

 

future, async 는 mutex, condition variable 까지 쓰지 않고 단순한 쓰레드 처리시 유용하다

1회성 사용시에 유용 

 

ex)파일로딩

 

1) async : 원하는 함수를 비동기적으로 실행

2) promise : 결과물을 promise를 통해 future 를 통해 받아줌

3) packged_task : 원하는 함수의 실행 결과를 packged_task를 통해 future 로 받아 줌

2,3은 비슷하다

 

비동기 != 멀티쓰레드 

단지  용어 상의 차이인데 비동기는 나중에 함수를 호출하는 개념

즉 지연 호출의 개념이 있어서 완전 멀티 스레드라고 할 수는 없다 

 

 

wait_for

연결된 비동기 작업이 완료되거나 지정된 _Rel_time 시간이 경과할 때까지 차단합니다.

구문

C++복사
template <
    class _Rep,
    class _Period
>
std::future_status::future_status wait_for(
    const std::chrono::duration< _Rep, _Period>& _Rel_time ) const;

매개 변수

_Rep
틱 수를 나타내는 산술 형식입니다.

_Period
틱당 경과된 시간(초)을 나타내는 std::ratio입니다.

_Rel_time
작업이 완료될 때까지 대기하는 최대 시간입니다.

반환 값

HRESULT = NO_ERROR를

  • std::future_status::deferred 연결된 비동기 작업이 실행되고 있지 않으면
  • std::future_status::ready 연결된 비동기 작업이 완료된 경우
  • std::future_status::timeout 지정된 기간이 경과된 경우

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <future>
#include "windows.h"

using namespace std;

int64 calculate()
{
	int64 sum = 0;
	for (int32 i = 0; i < 100'000; ++i)
	{
		sum += 1;
	}
	return sum;
}


int main()
{
	{
		//비 동기 호출, calculate 가 호출 
		std::future<int64> future = std::async(std::launch::async, calculate);		

		std::future_status status = future.wait_for(1ms);	//1밀리 세컨동안 대기했다가 상태를 얻어와서

		//지금 스레드의 상태가 어떤 상태인지 알 수 있다
		//std::future_status::ready, std::future_status::deferred, std::future_status::timeout
		/*
		*	std::future_status::deferred 연결된 비동기 작업이 실행되고 있지 않으면

			std::future_status::ready 연결된 비동기 작업이 완료된 경우

			std::future_status::timeout 지정된 기간이 경과된 경우
		*/
		if(status == std::future_status::ready)		//ready 면 일감이 완료  된상태
		{
			
		}


		int64 sum = future.get();		//get 을 만나면 이때까지 스레드가 실행 안됐다면 대기했다가 결과 반환


	}

	return 0;
}

 

ready 면 일감이 완료된 상태

 

 

 

future.wait() == wiat_for 를 무한정 기다리는 상태

		//비 동기 호출, calculate 가 호출 
		std::future<int64> future = std::async(std::launch::async, calculate);		

		//std::future_status status = future.wait_for(1ms);	//1밀리 세컨동안 대기했다가 상태를 얻어와서
		future.wait();		//wiat_for 를 무한정 기다리는 상태
		
		//그래서  wait 을 했다가 결과를 호출하나  아래에서 get 을 호출하나 결과는 같다

		int64 sum = future.get();		//get 을 만나면 이때까지 스레드가 실행 안됐다면 대기했다가 결과 반환

 

 

 

 

 

wiat_for  추가 설명


future 는 thread에서 연산된 결과 값을 전달 받기 위해 사용된다.

wait() / get()은 thread에서 return 또한 set_value() 함수가 호출될 때까지 무한정 대기한다. 

wait_for()를 사용하면 일정 시간만큼 thread에서 값이 전달되기 기다리다, timout이 되면 block 상태가 해제된다. 

 

#include <iostream>
#include <future>
#include <thread>
#include <chrono>
 
int main()
{
    std::future<int> future = std::async(std::launch::async, [](){ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        return 8;  
    }); 
 
    std::cout << "waiting...\n";
    std::future_status status;
    do {
        status = future.wait_for(std::chrono::seconds(1));
        if (status == std::future_status::deferred) {
            std::cout << "deferred\n";
        } else if (status == std::future_status::timeout) {
            std::cout << "timeout\n";
        } else if (status == std::future_status::ready) {
            std::cout << "ready!\n";
        }
    } while (status != std::future_status::ready); 
 
    std::cout << "result is " << future.get() << '\n';
}

 

Line별로 자세히 살펴보도록 하자. 

std::future<int> future = std::async(std::launch::async, [](){ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        return 8;  
    });

std::launch::async 정책으로 async()를 수행하는데, lamda 표현식을 사용하여 thread를 생성하였다. ( 3초를 sleep하고 8을 return ) 

 

std::future_status status;
    do {
        status = future.wait_for(std::chrono::seconds(1));
        if (status == std::future_status::deferred) {
            std::cout << "deferred\n";
        } else if (status == std::future_status::timeout) {
            std::cout << "timeout\n";
        } else if (status == std::future_status::ready) {
            std::cout << "ready!\n";
        }
    } while (status != std::future_status::ready);

이 섹션에서 핵심 코드라고 할 수 있다. 

wait_for() 함수는 아래와 같은 return 값을 가진다. 

wait_for()에서 second(1)을 전달하여 1초 대기하도록 선언하였다. 앞서 본 thread에서 3초 sleep 후 8을 return 하기에 

do - while 1번째에는 timeout이 발생하게된다. (timeout 로그 출력) 

이후 do - while 조건문에서 status가 ready가 아니므로, 다시 wait_for() 함수가 호출된다. thread는 아직 3초가 지나지 않았기에 다시 한번 timeout이 발생한다. (timeout 로그 출력) 

다시 do - while 3번째에서 wait_for() 함수가 호출되어 1초를 wait하게 되는데, 이제는 thread가 3초 sleep에서 깨어나 8을 return 하게 된다. 

그래서 wait_for() 함수는 ready를 리턴하게 된다. 

do - while 문에서 status가 ready가 되어 do - while문을 빠져 나와 get() 함수를 통해 8을 출력하게 된다. 


클래스 인스턴스와 멤버 함수 호출 시

 		class ABC
		{
		public :
			int64 getalph() {
				return 10;
			}
		};

		ABC abc;

		std::future<int64> future2 = std::async(std::launch::async, &ABC::getalph, abc);

		auto result = future2.get();

결과는 10

 

 

 

std::promise 와 std::future 조합

 

스레드 만들고 전역변수 만들어 데이터 전달하기 보단 스레드 함수에서 데이터를

assign 하여 스래드 대기하는 쪽에서 데이터를 받아 볼 수 있는 기법

 

 

void promiseWorker(std::promise<string>&& promise)
{
	promise.set_value("secret message");
}

main 함수 중....
{
			//아래 코드처럼 작성하면
			//promise 에서 future 를 얻어올 수 있음
			//만약 promise 에 어떤 데이트를 세팅하면
			//future 에서 데이터를 얻어올 수 있음
			//미래에 결과물을 반환해달라는 일종의 약속(promise) 하는 것

			std::promise<string> promise;
			std::future<string> future = promise.get_future();
			//여기 까지 설정한다음
			
			//스레드를 만들어 소유권을 넘겨주면
			//즉 std::move(promise) 를 promiseWorker 스레드에 인자로 넘겨 준다
			thread t(promiseWorker, std::move(promise));

			//그리고 위 코드에서 
			//promise.set_value("secret message");
			//를 했음으로 promise 에서 이 데이터를 다음 처럼 받아 올 수 있다

			string data = future.get();

			std::cout << data;
            t.join();
}

 

promise 에서 future 를 얻어올 수 있음
만약 promise 에 어떤 데이트를 세팅하면
future 에서 데이터를 얻어올 수 있음
미래에 결과물을 반환해달라는 일종의 약속(promise) 하는 것

 

std::promise<string> promise;
std::future<string> future = promise.get_future();

여기 까지 설정한다음

스레드를 만들어 소유권을 넘겨주면
즉 std::move(promise) 를 promiseWorker 스레드에 인자로 넘겨 준다
thread t(promiseWorker, std::move(promise));

 

 

그리고 위 코드에서 
promise.set_value("secret message");
를 했음으로 promise 에서 이 데이터를 다음 처럼 받아 올 수 있다

 

string data = future.get();

std::cout << data;

 

 

결과 화면

 

 

 

 

 

코드에서 보면 move 하기 전에 promise 가 pending 상태지만 move 하고 난 이후엔 main 에서의 promise 는 소유권을 넘겨주기 때문에 empty 가 되어 더이상 지역변수 promise 를 사용하면 안된다 

즉 결과는 fugure.get()  을 통해 넘겨 받게 된다

 

이렇게 하는 이유는 실수로 여러번 get을 호출하면 문제가 되기 때문에

 

async 와 차이점이 있다면 promise 는 객체를 만들어서 여기저기 재사용이 가능하다

 

 

 

 

 

packaged_task 와 future

promise 와 유사하지만 사용이 간단한 packaged_task 가 있음

 

	void taskWorker(std::packaged_task<int64(void)>&& task)
	{
		task();
	}

	//main 어딘가..
	{
		//promise 와 유사하지만 사용이 간단한 packaged_task 가 있음
		std::packaged_task<int64(void)> task(calculate);
		std::future<int64> future = task.get_future();

		thread t(taskWorker, std::move(task));

		std::cout << future.get() << std::endl;;

		t.join();
	}

 

 

결과는 

100000

 

 

promise 와 차이점이 있다면 동일한 함수타입에 대한 함수들에 task를 넘겨줘서 

결과물을 받아올때 유용

 

 

https://answer-me.tistory.com/32

ref : https://docs.microsoft.com/ko-kr/cpp/parallel/amp/reference/completion-future-class?view=msvc-170 

반응형
반응형

 

async 를 실행할대 인자는 대표적으로 두개가 있다

 

Specifies the launch policy for a task executed by the std::async function. 

std::launch is an enumeration used as BitmaskType.

The following constants denoting individual bits are defined by the standard library:

Constant Explanation
std::launch::async the task is executed on a different thread, potentially by creating and launching it first
std::launch::deferred the task is executed on the calling thread the first time its result is requested (lazy evaluation)

 

thread 를 굳이 만들지 않고 lock_guard 나 간편하게 비동기로 처리 하고 싶을 때 사용 할 수 있다

ex) 파일 로딩..

 

  • std::future  쓰레드 만들때 좀 더 간단한 방식으로 만드는 형태, 간단하게 쓰일 때 유용
  • calculate 를 async 방식으로 호출 하는 것

  • 대표적으론 두가지 혼합해서 한가지 총 3가지 방식이 있다
    • std::launch::deffered : 지연된 연산(lazy evaluation)인데 지연해서 실행해라 라는 얘기
      get() 를 만날때 실행된다

    • std::launch::async : 별도의 스레드를 만들어서 실행하라는 것
      실제 스레드 개수가 늘어나는것을 Thread 창에서 확인 할수 있다
      스레드를 별도로 만들지 않아도 내부적으로 알아서 만들어서 비동기로 실행함  ex) 데이터 파일 로딩시..
      get() 함수를 통해 join 같은효과를 낼 수 있으며 결과 값을 반환 받을 수 있다

    • std::launch::deffered | std::launch::async : 둘 중에 아무거나 골라서 실행 하라는 것 



 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <future>
#include "windows.h"

using namespace std;

int64 calculate()
{
	int64 sum = 0;
	for (int32 i = 0; i < 100'000; ++i)
	{
		sum += 1;
	}
	return sum;
}


int main()
{
	{
		//std::future  쓰레드 만들때 좀 더 간단한 방식으로 만드는 형태, 간단하게 쓰일 때 유용
		//calculate 를 async 방식으로 호출 하는 것
		/*
		* std::launch::deffered : 지연된 연산(lazy evaluation)인데 지연해서 실행해라 라는 얘기
		  std::launch::async : 별도의 스레드를 만들어서 실행하라는 것
		  std::launch::deffered | std::launch::async : 둘 중에 아무거나 골라서 실행 하라는 것 
		*/
		//실제 스레드 개수가 늘어나는것을 Thread 창에서 확인 할수 있다
		//스레드를 별도로 만들지 않아도 내부적으로 알아서 만들어서 비동기로 실행함  ex) 데이터 파일 로딩시..
		std::future<int64> future = std::async(std::launch::async, calculate);		//비 동기 호출, calculate 가 호출 됨

		//이후에 다른 코드를 작성하다가 
		//시간이 지나서 해당 결과를 필요할대 꺼내 올 수 있다
		int64 sum = future.get();		//get 을 만나면 이때까지 스레드가 실행 안됐다면 대기했다가 결과 반환



		std::future<int64> futureDef = std::async(std::launch::deferred, calculate);		//이건 스레드가 아니고 나중에 호출 하겠다는 얘기임

		//실제 스레드 개수가 늘어나지 않는 다는 것을 Thread 창에서 확인 할 수 있다
		int64 sum2 = futureDef.get();		//이때 실행 됨

	}

	return 0;
}

 

 

 

ref : https://en.cppreference.com/w/cpp/thread/launch

반응형
반응형

condition_variable  은 전통적인 CreateEvent, SetEvet 를 대체하는 방식으로 볼 수 있다

condition_variable 은 멀티 플랫폼에서도 작동하는 표준으로 들어가 있다

 

condition_variable 은 커널오브젝트인 event 와 유사한것 같지만
유저모드 레벨 오브젝라는 점이 다르다

즉, 동일한 프로그램 내에서만 사용 가능하다

표준 mutex 와 짝지어 사용 가능하다
condition_variable cv;

 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <queue>
#include "windows.h"

using namespace std;

mutex m;
queue<int32> q;
//HANDLE handle;

//condition_variable 은 커널오브젝트인 event 와 유사한것 같지만
//유저모드 레벨 오브젝라는 점이 다르다 즉, 동일한 프로그램 내에서만 사용 가능하다
//표준 mutex 와 짝지어 사용 가능하다
condition_variable cv;			



void producer()
{
	while (true)
	{

		// 1) lock 을 잡고
		//2 공유변수 값 수정 q
		//3 lock 을 풀고
		//4 조건 변수 통해 다른 스레드에게 통지 (condition variable 로 통지
		{
			unique_lock<mutex> lock(m);		//1. lock 을 잡는다
			q.push(100);								//2. 공유 값 수정
		}												//3. lock 을 풀어줌
		
		//::SetEvent(handle);		//event 를 signal 상태로 만든다
		cv.notify_one();				//SetEvent 대신 notify 를 해줘서 wait 중인 스레드들 중 딱 1개를 깨운다

		//this_thread::sleep_for(100ms);
	}
}


//q 에 데이터가 있을 때에만 pop 하여 꺼내서 출력하고 q 에 데이터가 없다면 대기하게 된다
void consumer()
{
	while (true)
	{
		//::WaitForSingleObject(handle, INFINITE);					//SetEvent 사용시 wait for 이함수로 대기했지만...
		//std::cout << "ConSumer 함수" << std::endl;
		{
			unique_lock<mutex> lock(m);								//우선 lock 을 해서 스레드를 한번 block 해줌 (push 나 pop 중 하나만 일어나도록 하기 위함)
			cv.wait(lock, []() {  return q.empty() == false;  });		//조건을 넣어주는데, 조건이 참일 때까지 기다리게 한다

			//q가 비어 있으면 결과는 (true == false) => false  인데 wait(false) 로 들어오면 lock 풀고 대기 한다, 즉 스레드가 중지된다
            //이렇게 lock 을 풀어줘야 하는 상황이 발생 할 수 있기 때문에 unique_lock 을 써야만 한다, conditional_variable 을 사용할때는
			//q가 비어 있지 않으면 결과는 (false == false) => true wait(true) 로 들어오면 빠져나와서 다음 코드를 진행한다

			//wait 에 false 가 들어가면 lock풀고 대기
			//wait 에 true 가 들어가면 다음 코드 진행

			//q가 비어 있다면 => (true==false) => false , wait(false) =>결과적으로 lock 풀고 대기 한다
			//q가 차 있다면 => (false==false) => true , wait(true) => 결과적으로 다음 실행해서 데이터 꺼내서 출력


			//조건이 맞으면 다음으로 넘어가고 그렇지 않으면 lock을 풀어주고 대기 상태로 전환된다

			//lock 을 또 다시 wait 하는 이유는 Spurious wakeup(가짜 기상) 때문인데
			//unique_lock<mutex> lock(m); 해서 대기하고 있는 상황에서 다른 스레드에서 notify_one를해서 쓰레드를 깨웠는데
			//unique_lock<mutex> lock(m); 과     cv.wait(lock, []() {  return q.empty() == false;  }); 사이에서 다른곳에서 lock 을 할 수도 있기 때문에
			//wait 에서 lock 을 한번더 확인해 주는것인데 그 사이에 다른 스레드가 lock 해버리는 상황을 Spurious wakeup 이라 한다

			
			//condition variable 에서 wait 하는 순간 발생 하는 것들
			//1 : lock 을 잡고 ( 만약 wait 위에서 lock 을 잡았다면 wait 에서 다시 lock 을 잡진 않는다
			//2 : 조건 확인
			//3-1 : 조건 만족 하면 빠젼 나와서 이어서 코드를 진행한다
			//3-2 : 조건 만족하지 않으면 lock 을 풀어주고 대기 상태로 전환한다	(=> 경우에 따라서 constant variable 이 unlock 을 해야 하기 대문에 unique_lock 을 사용해야만 함)
					//3-2-sub : 3-2 에서 대기 하고 있다가 다른곳에서 notify 를 호출해주면 그때 다시 1번 부터 확인하여 실행 여부가 결정된다

			if (q.empty() == false)
			{
				int32 data = q.front();
				q.pop();
				cout << data <<"\t"<< q.size() << std::endl;
			}
		}

		this_thread::sleep_for(100ms);
	}
}

int main()
{

	//커널오브젝트
	//Usage Count

	//이벤트 속성 : NULL
	//auto rest 방식 으로 지정
	//초기 값
	//이름은 null
	//Event 는 유저모드가 아닌 커널모드에서 관리 되는것이기 떄문에 커널단에서 처리된다
	//handle = ::CreateEvent(nullptr, true, false, nullptr);

	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();


	//::CloseHandle(handle);

	return 0;
}

 

 

 

produce 에선 (데이터를 추가해주는 쪽)에선 비교적 간단하게 다음 처럼 처리해 준다

 

Conditional_variable 의 처리 순서

1) lock 을 잡고
2 공유변수 값 수정 q
3 lock 을 풀고
4 조건 변수 통해 다른 스레드에게 통지 (condition variable 로 통지

 

이런 프로세스로 추가해주고

 Produce  예시를 적용해보자면 이렇게 된다

 

{
  unique_lock<mutex> lock(m); //1. lock 을 잡는다
  q.push(100); //2. 공유 값 수정
} //3. lock 을 풀어줌

 

4. 대기하고 있는 스레드 중에서 스레드들 중 딱 1개만 깨운다
cv.notify_one();  

 

데이터 추가할때 다른 곳에서 쓰는것과 충돌나는 일을 막기 위해 mutex 로 묶어준다

이후 처리가 끝나면 그냥단순 통지만 하고 끝

 

 

 

consumer 에선 다음 처리 처리한다

 

unique_lock<mutex> lock(m); //우선 lock 을 해서 스레드를 한번 block 해줌 (push 나 pop 중 하나만 일어나도록 하기 위함)
cv.wait(lock, []() {  return q.empty() == false;  }); //조건을 넣어주는데, 조건이 참일 때까지 기다리게 한다

 

Condition variable 은 다음 특징이 있다

wait(false) 에 false 가 들어가면 lock풀고 대기
wait(true) 에 true 가 들어가면 다음 코드 진행

q가 비어 있다면 => (true==false) => false , wait(false) =>결과적으로 lock 풀고 대기 한다
q가 차 있다면 => (false==false) => true , wait(true) => 결과적으로 다음 실행해서 데이터 꺼내서 출력

 

 

Spurious wakeup

 

lock 을 또 다시 wait 하는 이유는 Spurious wakeup(가짜 기상) 때문인데
unique_lock<mutex> lock(m); 해서 대기하고 있는 상황에서 다른 스레드에서 notify_one를해서 쓰레드를 깨웠는데
unique_lock<mutex> lock(m); 과     cv.wait(lock, []() {  return q.empty() == false;  }); 사이에서 다른곳에서 lock 을 할 수도 있기 때문에
wait 에서 lock 을 한번더 확인해 주는것(즉 lock 을 한번 더 건다)인데 하지만 그 사이에 다른 스레드가 lock 해버리는 상황이 발생 되면 wait 은 다른곳이 lock 을 했기 때문에 대기 해야 하고 이 것을 Spurious wakeup 즉 가짜 wakeup 이라 한다

 

다시 정리 하자면 이중으로 unique_lock 에 이어 한번더  wait 에서 lock 을 체크 하는데

unique_lock 과 wait 사이에서 다른 스레드가 lock 을 가져 갈수 있기 때문에 방지 차원에서 wati 에서 lock 을

체크하는 로직이 들어가게 된것이다

 

 

 

Spurious wakeup

A spurious wakeup happens when a thread wakes up from waiting on a condition variable that's been signaled, only to discover that the condition it was waiting for isn't satisfied. It's called spurious because the thread has seemingly been awakened for no reason. But spurious wakeups don't happen for no reason: they usually happen because, in between the time when the condition variable was signaled and when the waiting thread finally ran, another thread ran and changed the condition. There was a race condition between the threads, with the typical result that sometimes, the thread waking up on the condition variable runs first, winning the race, and sometimes it runs second, losing the race.

On many systems, especially multiprocessor systems, the problem of spurious wakeups is exacerbated because if there are several threads waiting on the condition variable when it's signaled, the system may decide to wake them all up, treating every signal( ) to wake one thread as a broadcast( ) to wake all of them, thus breaking any possibly expected 1:1 relationship between signals and wakeups.[1] If there are ten threads waiting, only one will win and the other nine will experience spurious wakeups.

To allow for implementation flexibility in dealing with error conditions and races inside the operating system, condition variables may also be allowed to return from a wait even if not signaled, though it is not clear how many implementations actually do that. In the Solaris implementation of condition variables, a spurious wakeup may occur without the condition being signaled if the process is signaled; the wait system call aborts and returns EINTR.[2] The Linux p-thread implementation of condition variables guarantees it will not do that.[3][4]

Because spurious wakeups can happen whenever there's a race and possibly even in the absence of a race or a signal, when a thread wakes on a condition variable, it should always check that the condition it sought is satisfied. If it is not, it should go back to sleeping on the condition variable, waiting for another opportunity.

 

 

가짜 wakeup 로 인해 성능이 악화 되는 상황

가짜 wakeup 의 상황이 되면 현재 돌아가려고 하는 스레드가 우선권을 race 에서 뺐기고 다른 스레드가 계속 가져가게 되서 느려지는데 만약 실행되야 하는 스레드가 있고 다른 스레드 10개가 돌아가고 있다면 이때 레이스를 하면서 실행되야 하는 스레드는 더욱더 지연 될 수 있게 되어 성능이 악화 될 수 있다

 

 

ref : https://en.wikipedia.org/wiki/Spurious_wakeup

반응형
반응형

waitcauses the current thread to block until the condition variable is notified or a spurious wakeup occurs, optionally looping until some predicate is satisfied (bool(stop_waiting()) == true).

 

 

예제

 

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>
 
std::condition_variable cv;
std::mutex cv_m; // This mutex is used for three purposes:
                 // 1) to synchronize accesses to i
                 // 2) to synchronize accesses to std::cerr
                 // 3) for the condition variable cv
int i = 0;
 
void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cerr << "...finished waiting. i == 1\n";
}
 
void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lk(cv_m);
        std::cerr << "Notifying...\n";
    }
    cv.notify_all();
 
    std::this_thread::sleep_for(std::chrono::seconds(1));
 
    {
        std::lock_guard<std::mutex> lk(cv_m);
        i = 1;
        std::cerr << "Notifying again...\n";
    }
    cv.notify_all();
}
 
int main()
{
    std::thread t1(waits), t2(waits), t3(waits), t4(signals);
    t1.join(); 
    t2.join(); 
    t3.join();
    t4.join();
}

Possible output:

Waiting...
Waiting...
Waiting...
Notifying...
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
...finished waiting. i == 1

 

Conditional Variable 은 

wait 할떄 

wiat( false ) 면 lock 을 풀고 대기 하고

wiat( true ) 면 빠져나와 다음 코드를 진행한다

 

 

 

ref : https://en.cppreference.com/w/cpp/thread/condition_variable/wait

반응형
반응형

분화 복수란 쉽게 말해서

단수와 복수의 의미가 다른 명사

의미하는 말인데요.

 

보통 특정 명사가 한 개 일 때는 단수,

그 명사가 여러개일때는 복수로 표현됩니다.

예를 들어,

사과가 한 개면 an apple,

사과가 여러 개면 apples,

이렇게 표현되죠.

즉 단수와 복수 표현의 의미는

달라지지 않습니다.

 

그런데 명사 중에는 단수와 복수 표현의 그 의미가

달라지는 명사 단어들이 있습니다.

분화 복수
advice 충고 air 공기 arm 팔 authority 권위
advices 통지

airs 뽐내는 꼴

arms 무기

authorities 당국

bone brain color 빛깔 content 만족
bones 해골, 시체

brains 학력, 두뇌

colors 국기

contents 목차

custom 습관 drawer 서랍 effect 효과, 결과 feature 특징
customs 관세, 세관

drawers 속옷, 장롱

effects 물건, 물품

features 용모

force glass 유리 good 이익 heaven 천국
forces 군대

glasses 안경

goods 상품,

heavens 하늘

iron 쇠 letter 문자 look 봄, 얼굴 manner 방법
irons 수갑

letters 문학, 서식

looks 용모, 외관

manners 예법

moral 교훈 pain 고통 paper 종이 part 부분
morals 품행, 윤리

pains 수고

papers 서류, 여권, 비자

parts 지방, 부품, 재능

premise 전제 provision 준비 quarter 4분의 1 regard 존경, 경의
premises 부지, 구내

provisions 식량

quarters 숙소

regards 안부

remain 나머지 return 돌아옴 respect 존경 sand 모래
remains 유해, 유적

returns 수익

respects 인사, 안부

sands 사막

service 접대, 시중 spirit 정신 saving 절약 time 때
services 설비, 용역

spirits 기분

savings 저축

times 시대

water 물 work 일 writing 쓰기  
waters 근해, 바다 works 공장, 공사, 작품 writings 저작, 작품  

 

 

 


 

 

분화복수는 복수 취급

 

advice : 충고

advices : 통지

 

measure :  양

measures : 도량법

 

air : 공기

airs : 잘난척하는 태도

 

number : 숫자

numbers : 시구(詩句)

 

arm : 팔

arms : 무기

 

pain : 고통

pains : 노력, 수고

 

ash : 재

ashes : 잿더미, 유골

 

paper : 종이

papers : 논문, 서류

 

authority : 권위, 권한

authorities : 당국

 

people : 사람들

peoples : 민족

 

cloth : 천 (옷감)

clothes : 옷

 

premise : 전제

premises : 부지, 구내

 

content : 만족, 내용

contents : 내용물, 목차

 

provision : 준비

provisions : 식량

 

color : 색깔

colors : 깃발, 기

 

quarter : 1/4

quarters : 숙소

 

custom : 관습

customs : 세관, 세관원

 

regard : 관심, 존경

regards : 안부

 

damage : 피해

damages : 피해액, 배상금

 

remain : 나머지

remains : 유해, 유적

 

effect : 효과

effects : 물건, 물품

 

return : 돌아옴, 귀환

returns : 수익

 

force : 힘

forces : 군대, 군사력

 

sand : 모래

sands : 사막

 

good : 이익, 도움

goods : 상품

 

saving : 절약

savings : 저축

 

glass : 유리

glasses : 안경

 

spectacle : 광경

spectacles : 안경

 

letter : 문자

letters : 알파벳, 문서, 문학

 

spirit : 정신

spirits : 주류(증류주)

 

look : 눈길, 표정

looks : 외모

 

time : 시간

times : 시대

 

manner : 방법

manners : 예의

 

water : 물

waters : 바다, 강

 

mean : 평균

means : 수단, 재산

 

work : 일

works : 작품

 

분화복수는 복수 취급

분화복수는 단수명사에 '-s(es)'를 붙여서 만드는 ‘복수명사’. 당연히 ‘복수’취급. 

영어에서 복수 취급한다는 것은 다음과 같은 의미.

● 주어 자리에 있을 경우 복수 동사로 받는다.
● many, few로 수식한다.
● much, little로 수식할 수 없다.

 

복수명사로 만들었다는 것은 그 명사를 ‘셀 수 있다’는 것. ‘셀 수 있다, 셀 수 없다’는 명사에서 가장 중요한 개념

 

 주어 자리에 있을 경우 복수 동사

Manners ARE important. (O) (예의는 중요하다)

Manners IS important. (X)

Where ARE manners? (O) (예의는 어디 있니?) 예외, is도 통용

 

 many, few로 수식

분화복수는 복수형이므로 셀 수 있다. 셀 수 있는 명사의 복수형은 many, few로 수식. 'much, little'은 쓸 수 없다.

How many clothes are enough? (O) (얼마나 옷이 많아야 충분한 거니?)

How much clothes are enough? (X)

He sold a few goods. (O)(그는 몇 가지 물건을 팔았다)

He sold a little goods. (X)

 

 

 

ref : https://tutoria.tistory.com/581

ref : https://blog.naver.com/eng1588/222624514988

ref : https://adipo.tistory.com/entry/단수-분화복수공무원-영어-문법?category=802206

반응형
반응형

com.google.games:gpgs-plugin-support:0.11.01 --> com.google.games:gpgs-plugin-support:+

 

 

 

I use GPGS v0.11.01 and was same error java.lang.ClassNotFoundException: com.google.android.gms.games.PlayGames.
Android Force Resolve failed with an error Failed to fetch the following dependencies: com.google.games:gpgs-plugin-support:+.
How did I fix it:

  • open the file Assets/GooglePlayGames/com.google.play.games/Editor/GooglePlayGamesPluginDependencies.xml
  • change line Packages/com.google.play.games/Editor/m2repository to Assets/GooglePlayGames/com.google.play.games/Editor/m2repository
  • run Android Force Resolve

 

 

 

https://github.com/playgameservices/play-games-plugin-for-unity/releases

 

 

ref : https://github.com/playgameservices/play-games-plugin-for-unity/issues/2796#issuecomment-1166347984

ref : https://github.com/playgameservices/play-games-plugin-for-unity/issues/2796

반응형
반응형
  • 프로세스 간의 동기화와 유저모드간의 동기화 모두 가능하다 (유저모드 모단 느리다)
  • spinlock 은 유저레벨에서 동기화 가능하다

 

이벤트에는 두가지가 있다

 

auto reset event

manual reset event

 

unique_lock 이글 참고

https://3dmpengines.tistory.com/2176

 

 

 

한쪽에선 클라이언트 데이터를 수신 받아와서 q에 밀어넣고 
게임 패킷과 관련된 스레드에서 q에서 데이터를 추출해오는 상황이라 가정하여 이를 작성하면 다음 코드 처럼 되고

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <queue>
#include "windows.h"

using namespace std;

mutex m;
queue<int32> q;

//한쪽에선 클라이언트 데이터를 수신 받아와서 q에 밀어넣고 
//게임 패킷과 관련된 스레드에서 q에서 데이터를 추출해오는 상황이라 가정

void producer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(100);
		}
		
		this_thread::sleep_for(100ms);
	}
}

void consumer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			if (q.empty() == false)
			{
				int32 data = q.front();
				q.pop();
				cout << data << std::endl;
			}
		}

		this_thread::sleep_for(100ms);
	}
}

int main()
{

	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();


	return 0;
}

 

결과는 100ms 마다 q 에 데이터를 push 하고 consumer 함수에 데이터를 꺼내다 쓰는 상황이 된다

 

 

 

하지만 만야겡 입력이 엄청 긴시간 동일 일어나지 않고 어쩌다 한번씩 입력이 일어난다고 하면

consumer 스레드는 계속 lock 과 unlock 을 반복하고 있긴 하기 때문에 이 부분에 스레드가 차이하는 비용이 그냥 이 코드만 보면 높진 않지만 이런게 많이 쌓이면 성능에 좋지 않음으로 이 부분을 event 를 고려하여 처리 하면

더 효율적으로 작업 할 수 있다

 

현재는 테스트 pc 가 좋아서 이후 테스트 결과와 크게 차이가 없는 cpu 점유율 0 % 인데

성능이 낮은 PC 일 수록 이것이 8~10% 까지도 올라 갈 순 있다

 

 

처리 방식 : 데이터가 q 에 있는 상황 즉 consume 해도 되는 상황에서 consumer 스레드가 돌도록 한다

 

 

 

Event 는 유저모드가 아닌 커널모드에서 관리 되는것이기 떄문에 커널단에서 처리된다

 

//이벤트 속성 : NULL
//auto rest 방식 으로 지정
//초기 값
//이름은 null
//Event 는 유저모드가 아닌 커널모드에서 관리 되는것이기 떄문에 커널단에서 처리된다

handle = ::CreateEvent(nullptr, false, false, nullptr);  //non-signal 상태

 

handle 이 물고 있는건 커널오브젝트인데 커널에서 관리하는 이벤트 오브젝트라 보면 된다

 

커널오브젝트에는 Usage Count = 이 오브젝트를 몇명이 사용하고 있는지 

그리고 Signal / Non-Signal 두가지 중 하나의 상태를 갖고 있다 (bool)

true 명 Signal 상태

자동모드/수동모드 에 대한 정보(이벤트는 자동모드 수동모드가 있다)

(자동모드 : 이벤트를 바로 리셋 해주는 모드)

 

SetEvent(handle) 하면 Signla 상태가 된다

 

 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <queue>
#include "windows.h"

using namespace std;

mutex m;
queue<int32> q;
HANDLE handle;

//한쪽에선 클라이언트 데이터를 수신 받아와서 q에 밀어넣고 
//게임 패킷과 관련된 스레드에서 q에서 데이터를 추출해오는 상황이라 가정

void producer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(100);
		}
		
		::SetEvent(handle);		//event 를 signal 상태로 만든다

		this_thread::sleep_for(100ms);
	}
}

void consumer()
{
	while (true)
	{
		//무한 대기, handle 가 Signal 상태가 될때까지
		::WaitForSingleObject(handle, INFINITE);					//evet 가 signal 상태가 되면 빠져나옴 이 함수를
		//즉 WaitForSingleObject 를 만나면 이 쓰레드는 wake up 되지 않고 즉 실행 되지 않고 수면 상태(sleep)로 빠져서 잠들게 된다
		//WaitForSingleObject 에 걸린 이벤트는 handle 자동모드라 WaitForSingleObject 이 함수가 실행된고 빠져나오는 즉시 event 가 non-signal 이 된다

		std::cout << "ConSumer 함수" << std::endl;
		{
			unique_lock<mutex> lock(m);
			if (q.empty() == false)
			{
				int32 data = q.front();
				q.pop();
				cout << data << std::endl;
			}
		}

		this_thread::sleep_for(100ms);
	}
}

int main()
{

	//커널오브젝트
	//Usage Count

	//이벤트 속성 : NULL
	//auto rest 방식 으로 지정
	//초기 값
	//이름은 null
	//Event 는 유저모드가 아닌 커널모드에서 관리 되는것이기 떄문에 커널단에서 처리된다
	handle = ::CreateEvent(nullptr, false, false, nullptr);

	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();


	::CloseHandle(handle);

	return 0;
}

 

무한 대기, handle 가 Signal 상태가 될때까지
::WaitForSingleObject(handle, INFINITE); //evet 가 signal 상태가 되면 빠져나옴 이 함수를
즉 WaitForSingleObject 를 만나면 이 쓰레드는 wake up 되지 않고 즉 실행 되지 않고 수면 상태(sleep)로 빠져서 잠들게 된다, 즉 필요 없는 스레드가 돌아가지 않게 효율을 올릴 수 있다


WaitForSingleObject 에 걸린 이벤트는 handle 자동모드라 WaitForSingleObject 이 함수가 실행된고 빠져나오는 즉시 event 가 non-signal 이 된다

 

 

 

결과 또한 데이터가 있을때 Consumer 를 통해 소진 되는 것을 얼추 알 수 있다

즉 데이터가 있을때 Comsumer 함수가 실행 되도록 evet 로 실행 순서를 제어한 것

 

이렇게 처리 하면 CPU 점유율이 0 % 대로 떨어지는 것을 알 수 있다

 

 

 

결과는 같고 아래는 Event 가  수동모드이다

wait.. 을 빠져나온 다음 바로 ResetEvent(handle); 을 처리 해주면 자동모드와 동일하다

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>
#include <queue>
#include "windows.h"

using namespace std;

mutex m;
queue<int32> q;
HANDLE handle;

//한쪽에선 클라이언트 데이터를 수신 받아와서 q에 밀어넣고 
//게임 패킷과 관련된 스레드에서 q에서 데이터를 추출해오는 상황이라 가정

void producer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(100);
		}
		
		::SetEvent(handle);		//event 를 signal 상태로 만든다

		this_thread::sleep_for(100ms);
	}
}

void consumer()
{
	while (true)
	{
		//무한 대기, handle 가 Signal 상태가 될때까지
		::WaitForSingleObject(handle, INFINITE);
		::ResetEvent(handle);
		
	

		std::cout << "ConSumer 함수" << std::endl;
		{
			unique_lock<mutex> lock(m);
			if (q.empty() == false)
			{
				int32 data = q.front();
				q.pop();
				cout << data << std::endl;
			}
		}

		this_thread::sleep_for(100ms);
	}
}

int main()
{

	//커널오브젝트
	//Usage Count

	//이벤트 속성 : NULL
	//auto rest 방식 으로 지정
	//초기 값
	//이름은 null
	//Event 는 유저모드가 아닌 커널모드에서 관리 되는것이기 떄문에 커널단에서 처리된다
	handle = ::CreateEvent(nullptr, true, false, nullptr);

	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();


	::CloseHandle(handle);

	return 0;
}

 

 

 

반응형
반응형

 

system call : 자발적으로 현재 스래드가 실행할 필요 없다고 커널(os) 에게 알려주는 것

cout 같은걸 행했다면 커널모드에 요청을해야 처리가 가능해서 

이때 system call 을 요청하면 커널모드로 들어가게 되고 요청 받은걸 실행한 다음 다시 스레드를 재생하는 형태로 실행된다

 

 

스케줄러 그림인데

스레드 단위로 실행 될때 실행상태에서 자신이 다 실행했다고 하면 context switch 를 통해서 준비 단계로 이동 할 수 있다

그럼 나중에 cpu 가 스케줄러를 보고서 실행할 스레드를 결정하여 실행 처리를 한다

 

cout 은 자발적 문맥교환 이라 볼 수 있다

 

 

 

 

Sleep 함수 

 

유저 모드와 커널모드를 왔다갔다 할때

 

유저모드의 스레드들은 시간을 분할 받아 실행을 일정 주기 마다 하게 되는데 

 

sleep 함수는 커널모드에서 스케줄러에 의해 일정 시간 대기했다가 다시 실행시키는 함수이다

 

Sleep (system call)

From Wikipedia, the free encyclopedia
A computer program (process, task, or thread) may sleep, which places it into an inactive state for a period of time. Eventually the expiration of an interval timer, or the receipt of a signal or interrupt causes the program to resume execution.

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"
#include <chrono>

using namespace std;

mutex m1;
int32 sum = 0;

class SpinLock
{
public:
	void lock() 
	{
		bool expected = false;
		bool desired = true;

		//_locked 값이 기대 하는 값과 같으면 계속 무한 루프를 돌면서 spinlock 으로 계속 대기 하고, 그렇지 않고 expected 값이 locked 다르면
		//내부적으로 _locked 값을 desired 값으로 바꾼다음 while 을 탈출해 spinlock 에서 빠져나오게 한다
		while (_locked.compare_exchange_strong(expected, desired) == false)
		{
			
			//expected = false; 이렇게 설정해주면 locked 를 얻을 때까지 무한 대기 하게 되는데 cpu 를 사용하면서,
			expected = false;		//compare_exchange_strong 내부에서 expected 값을 & 로 바꾸기 때문에 원래 초기 상태로 바꾼다
			this_thread::sleep_for(100ms);		//100 ms 동안 sleep 하도록 함 => 이렇게 하면 cpu 파워를 낭비 하지 않고 context switch 된 이후에 일정 시간이후에 다시 와서 locked 를 획을 할 수 있을지 확인하게 되어
			//무한 cpu 자원을 사용하는건 막을 수 있지만 , 다시 돌아왔을때 이미 다른 스레드에서 lock 을 얻어 갔다면 일정 시간이후에 다시 돌아온 스레드는 또다시 wait 해야 하는상황이 발생되게 되는 악순환이 될 수도 있다
			//this_thread::yield();	// ==this_thread::sleep_for(0ms);  이것과 동일하다
			//이렇게 되면 엄밀히 말하면 spinlock 이 되는것은 아니다
			
		}
	}

	void unlock()
	{
		_locked.store(false);	//bool 을 바꾸는 것도 또한 원자적으로 일어나야함 
	}
	volatile atomic<bool>  _locked = false;	//bool 값도 그냥 바꾸면 명령어가 두개로 처리됨
};

SpinLock spinLock;

void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum++;
	}
}

void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

	t1.join();
	t2.join();

	std::cout << sum << std::endl;

	return 0;
}

 

while (_locked.compare_exchange_strong(expected, desired) == false)
{

     //expected = false; 이렇게 설정해주면 locked 를 얻을 때까지 무한 대기 하게 되는데 cpu 를 사용하면서,
     expected = false; //compare_exchange_strong 내부에서 expected 값을 & 로 바꾸기 때문에 원래 초기 상태로 바꾼다

 

this_thread::sleep_for(100ms);    100 ms 동안 sleep 하도록 함

 

이렇게 하면 cpu 파워를 낭비 하지 않고 context switch 된 이후에 일정 시간이후에 다시 와서

 

locked 를 획을 할 수 있을지 확인하게 되어
무한 cpu 자원을 사용하는건 막을 수 있지만 , 다시 돌아왔을때 이미 다른 스레드에서 lock 을 얻어 갔다면 일정 시간이후에 다시 돌아온 스레드는 또다시 wait 해야 하는상황이 발생되게 되는 악순환이 될 수도 있다

 

this_thread::yield();

위으 것과 this_thread::sleep_for(0ms);  이것과 동일하다

 

이렇게 되면 엄밀히 말하면 spinlock 이 되는것은 아니다

 

 

여기에서 포인트는 커널모드로 들어가서 Context Switch 를 유발하고 
유저모드에서 커널모드로 들어가게 할수 있다는 것임 

그리고 cout 같은 시스템 콜이 있다면 위와 같은 이유로 느려지게 된다는 것이다 

 

핵심은 system call 을 필요 없이 요청하는건 자제해야 한다는것!

 

 

 

 

 

ref : https://en.wikipedia.org/wiki/Sleep_(system_call)

 

 

반응형
반응형

spin lock 무한 루프를 돌면서 lock 이 풀릴때까지 계속 대기 하는것(Busy wait)

context swith 가 자주 되지 않는 다는 장점이 있지만 오랬 동안 걸릴것 같다면 그 사이 아무것도 못함으로 그 기다리는 동안 부하가 생기게 된다

바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU 양보하지 않는 것이다.

 

 

Spin Lock  이름이 뜻하는대로만약 다른 스레드가 lock 소유하고 있다면  lock 반환될 때까지 계속 확인하며 기다리는 것이다. "조금만 기다리면 바로   있는데 굳이 컨텍스트 스위칭으로 부하를  필요가 있나?" 라는 컨셉으로 개발된 것으로 크리티컬 섹션에 진입이 불가능할때 컨텍스트 스위칭을 하지 않고 잠시 루프를 돌면서 재시도 하는 것을 말합니다. Lock-Unlcok 과정이 아주 짧아서 락하는 경우가 드문 경우(적절하게 크리티컬 섹션을 사용한 경우유용하다Spin Lock  다음과 같은 특성을 갖는다.

  1. Lock 얻을  없다면계속해서 Lock 확인하며 얻을 때까지 기다린다이른바 바쁘게 기다리는 busy wating이다.
  2. 바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU 양보하지 않는 것이다.
  3. Lock  사용가능해질 경우 컨택스트 스위치를 줄여 CPU 부담을 덜어준다하지만만약 어떤 스레드가 Lock 오랫동안 유지한다면 오히려 CPU 시간을 많이 소모할 가능성이 있다.
  4.  하나의 CPU 하나의 코어만 있는 경우에는 유용하지 않다 이유는 만약 다른 스레드가 Lock 가지고 있고  스레드가 Lock 풀어 주려면 싱글 CPU 시스템에서는 어차피 컨택스트 스위치가 일어나야 하기 때문이다. 주의할  스핀락을 잘못 사용하면 CPU 사용률 100% 만드는 상황이 발생하므로 주의 해야 한다스핀락은 기본적으로 무한 for 루프를 돌면서 lock 기다리므로 하나의 쓰레드가 lock 오랫동안 가지고 있다면다른 blocking 쓰레드는 busy waiting 하므로 CPU 쓸데없이 낭비하게 된다.

장점은 스핀락을  사용하면 context switch 줄여 효율을 높일  있습니다. 무한 루프를 돌기 보다는 일정 시간 lock 얻을  없다면 잠시 sleep하는 back off 알고리즘을 사용하는 것이 훨씬 좋습니다.

 

 

 

 

스핀 락(Spin lock)과 뮤텍스(Mutex)의 차이

둘 모두 자원에 대해 락을 걸고 사용하려고 할 시에 락이 풀릴 때까지 기다려야 한다는 점은 같지만, 둘은 내부적으로 로우레벨에서 차이점이 있다.

 

 우선 뮤텍스의 경우, 자원에 이미 락이 걸려 있을 경우 락이 풀릴 때까지 기다리며 컨텍스트 스위칭을 실행한다. 

즉, 다른 병렬적인 태스크를 처리하기 위해 CPU를 양보할 수 있다는 것이며 이는 자원을 얻기 위해 오랜 시간을 기다려야 할 것이 예상될 때 다른 작업을 동시에 진행할 수 있다는 것이다. 하지만 이는 자원이 단시간 내로 얻을 수 있게 될 경우 컨텍스트 스위칭에 더 큰 자원을 낭비하게 될 수 있다는 문제가 있다.

 스핀 락의 경우에는 이름에서부터 알 수 있듯이, 자원에 락이 걸려 있을 경우 이를 얻을 때까지 무한 루프를 돌면서 다른 태스크에 CPU를 양보하지 않는 것이다. 자원이 단시간 내로 얻을 수 있게 된다면 컨텍스트 스위칭 비용이 들지 않으므로 효율을 높일 수 있지만, 그 반대의 경우 다른 태스크에 CPU를 양보하지 않으므로 오히려 CPU 효율을 떨어뜨릴 수 있는 문제가 있다.

 

 

 

 

 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

using namespace std;

mutex m1;
int32 sum = 0;


void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<mutex> guard(m1);
		sum++;
	}
}


void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<mutex> guard(m1);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

	t1.join();
	t2.join();

	std::cout << sum << std::endl;


	return 0;
}

 

 

위 코드 결과는 0 이며 mutex 를 활용한 것이다 정상적으로처리 된다

 

하지만 아래처럼 custom 한 lock 클래스를 구현하여 처리 할려고 보면 아래와 같이 정사적인 연산이 되지 않는다는 것을 알 수 있다

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

using namespace std;

mutex m1;
int32 sum = 0;

class SpinLock
{
public:
	void lock() 
	{
		while (_locked)
		{
		}
		_locked = true;
	}

	void unlock()
	{
		_locked = false;
	}
	bool _locked = false;

};


SpinLock spinLock;


void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum++;
	}
}


void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

	t1.join();
	t2.join();

	std::cout << sum << std::endl;


	return 0;
}

 

결과 -5512  등등 이상한 값이 나옴..

 

 

volatile 는 해당 코드를 최적화 하지 말라고 하는것 반복문 등에서.. 하지만 이게 지금의문제는 아니지만 참고사항으로

 

_locked = true; 이 코드는 한줄이 아님

_locked = true;
00007FF60EC7405F  mov         rax,qword ptr [this]  
00007FF60EC74066  mov         byte ptr [rax],1

bool 값도 원자적으로 일어나야지 문제가 발생하지 않음으로 atomic 으로 처리해야한다

또한 lock_guard 에서 lock 함수와 unlock 함수를 사용하는 이유에서라도 atomic 을 사용해야한다

 

 

 

문제는 아래 명령이 하나로 처리 되야 하는데 로직이 두개가 보니 즉 wait 하다가 값이 바뀌면 바로 빠져나가야 하는데 스레드가 전환 되면서 값이 제대로 바뀌지 않을 수 있기 때문에 문제가 발생하기 때문에

while (_locked)
{
}
_locked = true;

 

이게 한번의 명령으로 처리 되지 않아 context switch 가 되면서 상태가 망가지는 것

while 쪽의 조건과 _locked 값을 한번에 처리 하는 방식으로 대기하도록 바꿔줘야 할 필요가 있다

즉 한방에 처리해야 한다는 것

 

 

이것을 한방에 수행하도록 묶어주는 방법이 있음 

CAS (Compare-And-Swap)  : OS 에 따라서 InterlockedExchagned(..) 같은 함수인데

atomic 을 사용하면 여기에 이것이 포함되어 있음 

 

atomic.compare_exchange_strong 함수임

 

 

 compare_exchange_strong : 이 함수는 atomic 하게 일어나며 (원자적으로 한방에처리 all or nothing) 다음과 유사하다 

 

요약 하자면 expected 와 _locked 값이 같으면 desired 으로 _locked 값을 바꾸고, 그렇지 않다면 _locked 값은 그대로 유지된다
  

 

compare_exchange_strong  : 함수에 대한 설명 결국 값을 바꾸는 것이긴 한데 원하는 값으로 바꾸느냐임


if(_locked == expected )   //만약 _locked 가 기대되는 값이 false 와 같다면
{
    expected = _locked; //_lock 값을 기대되는 값에 넣어준다
    _locked = desired; //그 다음 _lock 값을 원하는 값으로 넣어준다
    return true; //즉 locked 가 기대하는 값과 같다면 _locked 를 원하는 값으로 변경한다
}
else    // _locked 가 기대하는 값과 다르다면
{
      expected = _locked; //기대 값을 _locked값으로 바꾼다
      return false;
}



 

	void lock() 
	{
		bool expected = false;
		bool desired = true;

		/*
		* compare_exchange_strong : 이 함수는 atomic 하게 일어나며
         (원자적으로 한방에처리 all or nothing) 다음과 유사하다 
		* 요약 하자면 expected 와 _locked 값이 같으면 desired 으로 _locked 값을 바꾸고, 
          그렇지 않다면 _locked 값은 그대로 유지된다
		* 
			if(_locked == expected )  	//만약 _locked 가 기대되는 값이 false 와 같다면
			{
				expected = _locked;		//_lock 값을 기대되는 값에 넣어준다
				_locked = desired;		//그 다음 _lock 값을 원하는 값으로 넣어준다
				return true;			//즉 locked 가 기대하는 값과 같다면 
                                        //_locked 를 원하는 값으로 변경한다
			}
			else	// _locked 가 기대하는 값과 다르다면
			{
				expected = _locked;		//기대 값을 _locked값으로 바꾼다
				return false;
			}
		*/
		
		//_locked 값이 기대 하는 값과 같으면 계속 무한 루프를 돌면서 spinlock 으로 계속 대기
        //하고, 그렇지 않고 expected 값이 locked 다르면 내부적으로 _locked 값을 desired 값으로
        //바꾼다음 while 을 탈출해 spinlock 에서 
        //빠져나오게 한다
        
		while (_locked.compare_exchange_strong(expected, desired) == false)
		{
        	//compare_exchange_strong 내부에서 expected 값을 & 로 바꾸기 때문에 
        	//원래 초기 상태로 바꾼다
			expected = false;			
            
		}
	}

	void unlock()
	{
		_locked.store(false);	//bool 을 바꾸는 것도 또한 원자적으로 일어나야함 
	}

 

 

 

 

 

Custom class 로 구혆나 Spinlock 클래스 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>	
#include <atomic>			//멀티 플랫폼에서 작동 가능
#include <vector>
#include <mutex>
#include "AccountManager.h"
#include "UserManager.h"

using namespace std;

mutex m1;
int32 sum = 0;

class SpinLock
{
public:
	void lock() 
	{
		bool expected = false;
		bool desired = true;

		/*
		* compare_exchange_strong : 이 함수는 atomic 하게 일어나며(원자적으로 한방에처리 all or nothing) 다음과 유사하다 
		* 요약 하자면 expected 와 _locked 값이 같으면 desired 으로 _locked 값을 바꾸고 , 그렇지 않다면 _locked 값은 그대로 유지된다
		* 
			if(_locked == expected )  //만약 _locked 가 기대되는 값이 false 와 같다면
			{
				expected = _locked;	//_lock 값을 기대되는 값에 넣어준다
				_locked = desired;		//그 다음 _lock 값을 원하는 값으로 넣어준다
				return true;				//즉 locked 가 기대하는 값과 같다면 _locked 를 원하는 값으로 변경한다
			}
			else	// _locked 가 기대하는 값과 다르다면
			{
				expected = _locked;		//기대 값을 _locked값으로 바꾼다
				return false;
			}
		*/
		
		//_locked 값이 기대 하는 값과 같으면 계속 무한 루프를 돌면서 spinlock 으로 계속 대기 하고, 그렇지 않고 expected 값이 locked 다르면
		//내부적으로 _locked 값을 desired 값으로 바꾼다음 while 을 탈출해 spinlock 에서 빠져나오게 한다
		while (_locked.compare_exchange_strong(expected, desired) == false)
		{
			expected = false;		//compare_exchange_strong 내부에서 expected 값을 & 로 바꾸기 때문에 원래 초기 상태로 바꾼다
		}
		
	}

	void unlock()
	{
		_locked.store(false);	//bool 을 바꾸는 것도 또한 원자적으로 일어나야함 
	}
	volatile atomic<bool>  _locked = false;	//bool 값도 그냥 바꾸면 명령어가 두개로 처리됨

};


SpinLock spinLock;


void func1()
{
	for (int32 i=0;i<10000;++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum++;
	}
}


void func2()
{
	for (int32 i = 0; i < 10000; ++i)
	{
		lock_guard<SpinLock> guard(spinLock);
		sum--;
	}
}

int main()
{

	thread t1(func1);
	thread t2(func2);

	t1.join();
	t2.join();

	std::cout << sum << std::endl;


	return 0;
}

 

 

spinlock 을 cpu 를 계속 먹는 상태가 된다 context switch 가 되지 않음으로

그래서 spinlock 끼리 서로 병합을 벌이게 되면 cpu 점유율이 확 올라가게 된다

SpinLock 은 커널레벨이 아닌 유저레벨에서 처리 되는 것임

context switch 가 되지 않아 점유율이 올라감으로 

 

 

 

 

 

반응형

+ Recent posts