잠금 해제 된 뮤텍스를 잠그는 것이 얼마나 효율적입니까? 뮤텍스의 비용은 얼마입니까?


149

저수준 언어 (C, C ++ 또는 기타)에서 : pthread가 제공하는 것 또는 네이티브 시스템 라이브러리가 제공하는 것과 같은 여러 뮤텍스를 갖는지 또는 객체에 대한 단일 언어를 선택할 수 있습니다.

뮤텍스를 잠그는 것이 얼마나 효율적입니까? 즉, 어셈블러 명령어가 몇 개나 있고 시간이 얼마나 걸립니까 (뮤텍스가 잠금 해제 된 경우)?

뮤텍스 비용은 얼마입니까? 정말 많은 뮤텍스 를 갖는 것이 문제 입니까? 또는 변수가있는만큼 코드에 뮤텍스 변수를 많이 넣을 수 있으며 int실제로 중요하지 않습니까?

(다른 하드웨어간에 얼마나 큰 차이가 있는지 잘 모르겠습니다. 있다면 하드웨어에 대해서도 알고 싶습니다. 그러나 대부분 일반적인 하드웨어에 관심이 있습니다.)

요점은 전체 객체에 대해 단일 뮤텍스 대신 객체의 일부만을 덮는 많은 뮤텍스를 사용함으로써 많은 블록을 안전하게 할 수 있다는 것입니다. 그리고 나는 이것에 대해 얼마나 멀리 가야하는지 궁금합니다. 즉, 가능한 한 얼마나 많은 블록을 안전하게 보호하려고 노력해야합니까? 이것은 얼마나 복잡하고 얼마나 많은 뮤텍스가 의미하는지에 관계없이 가능합니까?


잠금에 관한 WebKits 블로그 게시물 (2016) 은이 질문과 관련이 있으며 spinlock, adaptive lock, futex 등의 차이점을 설명합니다.


이것은 구현 및 아키텍처에 따라 다릅니다. 일부 뮤텍스는 기본 하드웨어 지원이 있으면 거의 비용이 들지 않으며 다른 뮤텍스는 비용이 많이 듭니다. 더 많은 정보 없이는 대답 할 수 없습니다.
지안

2
@Gian : 글쎄, 물론 내 질문 에이 하위 질문을 암시합니다. 일반적인 하드웨어에 대해 알고 싶지만 예외가 있다면 주목할만한 예외가 있습니다.
Albert

나는 그 곳에서 그 의미를 실제로 보지 못합니다. 당신은 "어셈블러 명령어"에 대해 질문합니다-당신이 말하는 아키텍처에 따라 대답은 1 명령어에서 1 만 명령어까지 가능합니다.
지안

15
@Gian : 그렇다면이 답을 정확히 알려주십시오. 실제로 x86 및 amd64에있는 것을 말하고 1 명령이있는 아키텍처에 대한 예를 제공하고 10k 인 아키텍처를 제공하십시오. 내 질문에서 그 사실을 알고 싶습니까?
앨버트

답변:


120

나는 많은 뮤텍스를 갖는 것과 객체에 대한 하나의 뮤텍스를 갖는 것 중에서 선택할 수 있습니다.

스레드가 많고 개체에 대한 액세스가 자주 발생하면 여러 잠금이 병렬 처리를 증가시킵니다. 잠금이 많을수록 잠금 디버깅이 많아지기 때문에 유지 관리 비용이 많이 듭니다.

뮤텍스를 잠그는 것이 얼마나 효율적입니까? 즉, 어셈블러 명령어가 얼마나 많고 시간이 얼마나 걸립니까 (뮤텍스가 잠금 해제 된 경우)?

정확한 어셈블러 명령어는 뮤텍스 의 최소 ​​오버 헤드 – 메모리 / 캐시 일관성 을 보장 메인 오버 헤드가 있습니다. 그리고 덜 자주 특정 잠금이 수행됩니다.

뮤텍스는 두 가지 주요 부분으로 구성됩니다 (과도하게 단순화) : (1) 뮤텍스가 잠겨 있는지 여부를 나타내는 플래그 및 (2) 대기 대기열.

플래그 변경은 명령이 거의 없으며 일반적으로 시스템 호출없이 수행됩니다. mutex가 잠기면 syscall은 호출 스레드를 대기 큐에 추가하고 대기를 시작합니다. 대기 큐가 비어 있으면 잠금 해제가 저렴하지만 대기 프로세스 중 하나를 깨우려면 syscall이 필요합니다. (일부 시스템에서는 저렴한 / 빠른 시스템 콜이 뮤텍스를 구현하는 데 사용되며, 경합이 발생하는 경우에만 느린 (일반) 시스템 호출이됩니다.)

잠금 해제 된 뮤텍스 잠금은 정말 저렴합니다. 경합이없는 뮤텍스 잠금 해제도 저렴합니다.

뮤텍스 비용은 얼마입니까? 정말 많은 뮤텍스를 갖는 것이 문제입니까? 또는 int 변수가있는만큼 코드에 뮤텍스 변수를 많이 넣을 수 있습니까?

원하는만큼 뮤텍스 변수를 코드에 넣을 수 있습니다. 응용 프로그램이 할당 할 수있는 메모리 양에 의해서만 제한됩니다.

요약. 사용자 공간 잠금 (특히 뮤텍스)은 저렴하며 시스템 제한이 없습니다. 그러나 너무 많은 것들이 디버깅에 악몽을 불러 일으 킵니다. 간단한 테이블 :

  1. 잠금이 적 으면 더 많은 경합 (느린 시스템 콜, CPU 정지)과 병렬 처리가 줄어 듭니다.
  2. 적은 잠금은 멀티 스레딩 문제를 디버깅하는 데 더 적은 문제를 의미합니다.
  3. 잠금이 많을수록 경합이 적고 병렬 처리가 높아짐
  4. 잠금이 클수록 디버깅 할 수없는 교착 상태가 발생할 가능성이 높아집니다.

일반적으로 # 2와 # 3의 균형을 유지하기 위해 응용 프로그램에 대한 균형 잡힌 잠금 구성표를 찾아 유지해야합니다.


(*) 덜 자주 잠긴 뮤텍스의 문제는 응용 프로그램에서 너무 많은 잠금을 사용하면 많은 CPU / 코어 트래픽으로 인해 다른 CPU의 데이터 캐시에서 뮤텍스 메모리를 플러시하여 캐시 일관성. 캐시 플러시는 경량 인터럽트와 같으며 CPU에 의해 투명하게 처리되지만 소위 스톨을 발생시킵니다. (stall)을 검색합니다 ( "스톨"검색).

그리고 실속은 잠금 코드가 느리게 실행되는 이유이며, 종종 응용 프로그램이 왜 느린 지에 대한 명확한 표시가 없습니다. 일부 아치는 CPU 간 / 코어 트래픽 통계를 제공하지만 일부는 그렇지 않습니다.

문제를 피하기 위해 사람들은 일반적으로 잠금 경합의 가능성을 줄이고 실속을 피하기 위해 많은 수의 잠금을 사용합니다. 이것이 시스템 한계가 아닌 저렴한 사용자 공간 잠금이 존재하는 이유입니다.


고마워, 그것은 주로 내 질문에 대답합니다. 커널 (예 : Linux 커널)이 뮤텍스를 처리하고 syscall을 통해 제어한다는 것을 몰랐습니다. 그러나 Linux 자체가 스케줄링 및 컨텍스트 스위치를 관리하므로 이는 의미가 있습니다. 그러나 이제 뮤텍스 잠금 / 잠금 해제가 내부적으로 수행 할 작업에 대해 대략적인 상상을했습니다.
앨버트

2
@ 앨버트 : 아. 컨텍스트 스위치를 잊어 버렸습니다 ... 컨텍스트 스위치가 너무 성능이 저하되었습니다. 잠금 획득에 실패 하고 스레드가 대기해야하는 경우 컨텍스트 전환의 절반에 해당합니다. CS 자체는 빠르지 만 다른 프로세스에서 CPU를 사용할 수 있으므로 캐시에 외계인 데이터가 채워집니다. 스레드가 마침내 잠금을 얻은 후에는 CPU에서 RAM으로부터 거의 모든 것을 새로 고쳐야 할 가능성이 있습니다.
Dummy00001

@ Dummy00001 다른 프로세스로 전환하면 CPU의 메모리 매핑을 변경해야합니다. 그렇게 싸지 않습니다.
curiousguy

27

같은 것을 알고 싶었 기 때문에 측정했습니다. 내 상자 (3.612361GHz의 AMD FX (TM) -8150 8 코어 프로세서)에서 자체 캐시 라인에 있고 이미 캐시 된 잠금 해제 된 뮤텍스를 잠금 및 잠금 해제하려면 47 클럭 (13ns)이 걸립니다.

두 개의 코어 (CPU # 0 및 # 1 사용) 간의 동기화로 인해 두 스레드에서 102 ns마다 한 번만 잠금 / 잠금 해제 쌍을 호출 할 수 있었으므로 51 ns마다 한 번씩 호출하면 약 38 시간이 걸립니다. 스레드가 잠금을 해제 한 후 복구하려면 ns 다음 스레드가 다시 잠금을 해제합니다.

이것을 조사하는 데 사용한 프로그램은 여기에서 찾을 수 있습니다. https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

내 상자에 특정한 하드 코딩 된 값 (xrange, yrange 및 rdtsc 오버 헤드)이 있으므로 작동하기 전에 실험해야합니다.

해당 상태에서 생성되는 그래프는 다음과 같습니다.

여기에 이미지 설명을 입력하십시오

다음 코드에서 벤치 마크 실행 결과를 보여줍니다.

uint64_t do_Ndec(int thread, int loop_count)
{
  uint64_t start;
  uint64_t end;
  int __d0;

  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
  mutex.lock();
  mutex.unlock();
  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
  asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
  return end - start;
}

두 개의 rdtsc 호출은 '뮤텍스'를 잠그고 잠금을 해제하는 데 걸리는 클럭 수를 측정합니다 (내 상자의 rdtsc 호출에 대한 39 클럭 오버 헤드). 세 번째 asm은 지연 루프입니다. 지연 루프의 크기는 스레드 0의 경우보다 스레드 1의 경우 1보다 작으므로 스레드 1이 약간 더 빠릅니다.

위 함수는 크기가 100,000 인 타이트한 루프에서 호출됩니다. 스레드 1에서는이 기능이 약간 빠르지 만 뮤텍스 호출로 인해 두 루프가 동기화됩니다. 이것은 잠금 / 잠금 해제 쌍에 대해 측정 된 클록 수가 스레드 1에 대해 약간 더 크다는 사실에서 그래프에서 볼 수 있습니다. 그 아래 루프에서 더 짧은 지연을 설명합니다.

위의 그래프에서 오른쪽 아래 지점은 지연 loop_count가 150 인 측정 값이며, 아래에서 왼쪽을 향한 지점을 따라 loop_count는 각 측정마다 하나씩 감소합니다. 77이되면 두 스레드에서 102 ns마다 함수가 호출됩니다. 그에 따라 loop_count가 더 줄어들면 더 이상 스레드를 동기화 할 수 없으며 뮤텍스가 실제로 대부분 잠기기 시작하여 잠금 / 잠금 해제에 필요한 클럭 수가 증가합니다. 또한 함수 호출의 평균 시간은 이로 인해 증가합니다. 플롯 포인트가 다시 오른쪽으로 올라갑니다.

이것으로 우리는 50 ns마다 뮤텍스를 잠금 및 잠금 해제하는 것이 내 상자에 문제가 아니라는 결론을 내릴 수 있습니다.

결론적으로 OP의 질문에 대한 답변은 더 많은 뮤텍스를 추가하는 것이 경합이 적은 한 더 좋습니다.

뮤텍스를 가능한 짧게 잠그십시오. 루프 외부에 배치하는 유일한 이유는 루프가 100ns마다 한 번 (또는 동시에 해당 루프를 50ns로 실행하려는 스레드 수) 또는 13ns 번 반복되는 경우입니다. 루프 크기는 경합에 의한 지연보다 지연됩니다.

편집 : 나는 지금 그 주제에 대해 더 많은 지식을 얻었으며 여기에 제시 한 결론을 의심하기 시작합니다. 우선, CPU 0과 1은 하이퍼 스레딩 된 것으로 판명되었습니다. AMD가 8 개의 실제 코어를 가지고 있다고 주장하지만, 다른 두 코어 사이의 지연이 훨씬 더 크기 때문에 (즉, 2와 3, 4와 5, 6과 7과 같이 0과 1이 쌍을 이룹니다.) ). 둘째, std :: mutex는 뮤텍스에 대한 잠금을 즉시 얻지 못할 때 실제로 시스템 호출을 수행하기 전에 잠금을 약간 회전시키는 방식으로 구현됩니다 (의심 할 정도로 느릴 것입니다). 제가 여기서 측정 한 것은 절대적으로 가장 이상적인 위치입니다. 실제로 잠금 및 잠금 해제는 잠금 / 잠금 해제마다 시간이 훨씬 더 걸릴 수 있습니다.

결론적으로, 뮤텍스는 원자로 구현됩니다. 코어 간 원자를 동기화하려면 내부 버스를 잠 가야하며 이는 수백 클록 사이클 동안 해당 캐시 라인을 고정시킵니다. 잠금을 확보 할 수없는 경우 스레드를 휴면 상태로 만들기 위해 시스템 호출을 수행해야합니다. 시스템 호출은 10 mircoseconds 정도입니다. 일반적으로 스레드가 어쨌든 휴면 상태이기 때문에 실제로 문제가되지는 않습니다.하지만 스레드가 정상적으로 회전하는 시간 동안 잠금을 얻을 수없고 시스템 호출도 마찬가지이므로 높은 경합에 문제가있을 수 있습니다. 잠시 후 자물쇠를 가져 가십시오. 예를 들어, 여러 스레드가 꽉 루프에서 뮤텍스를 잠그고 잠금을 해제하고 각각이 1 마이크로 초 정도 잠금을 유지하는 경우, 그들은 끊임없이 잠들고 다시 깨어났다는 사실에 의해 엄청나게 느려질 수 있습니다. 또한 스레드가 잠 들어 다른 스레드가 깨어 나면 해당 스레드는 시스템 호출을 수행해야하며 ~ 10 마이크로 초 지연됩니다. 이 지연은 다른 스레드가 커널에서 해당 뮤텍스를 기다리고있을 때 뮤텍스를 잠금 해제하는 동안 발생합니다 (회전이 너무 오래 걸린 후).


10

이것은 실제로 "mutex", OS 모드 등에 따라 다릅니다.

에서 최소 가 연동 메모리 동작의 비용입니다. 상대적으로 무거운 작업입니다 (다른 기본 어셈블러 명령과 비교).

그러나 그것은 훨씬 더 높을 수 있습니다. 커널 개체 (OS에서 관리하는 개체)를 "mutex"라고 부르고 사용자 모드에서 실행하는 경우 모든 작업에서 커널 모드 트랜잭션이 발생하며 이는 매우 무겁습니다.

예를 들어, Intel Core Duo 프로세서, Windows XP에서. 연동 작동 : 약 40 개의 CPU 사이클이 필요합니다. 커널 모드 호출 (즉, 시스템 호출)-약 2000 CPU주기.

이 경우 중요한 섹션 사용을 고려할 수 있습니다. 커널 뮤텍스와 연동 된 메모리 액세스의 하이브리드입니다.


7
Windows 중요 섹션은 뮤텍스에 훨씬 가깝습니다. 그것들은 규칙적인 뮤텍스 의미론을 가지고 있지만 프로세스 로컬입니다. 마지막 부분은 프로세스 (및 사용자 모드 코드) 내에서 완전히 처리 할 수 ​​있기 때문에 훨씬 빠릅니다.
MSalters

2
공통 연산의 CPU주기 량 (예 : 산술 / if-else / 캐시-미스 / 간접)이 비교를 위해 제공되는 경우이 수가 더 유용합니다. .... 번호에 대한 참조가 있으면 좋을 것입니다. 인터넷에서는 그러한 정보를 찾기가 매우 어렵습니다.
javaLover

@javaLover 작업은주기마다 실행되지 않습니다. 그들은 여러 사이클 동안 산술 단위로 실행됩니다. 매우 다릅니다. 어떤 명령 비용도 정해진 수량이 아니며 자원 사용 비용 만 있습니다. 이러한 리소스는 공유됩니다. 메모리 명령의 영향은 많은 캐싱 등에 달려 있습니다.
curiousguy

@curiousguy 동의합니다. 나는 명확하지 않았다. std::mutex평균 사용 시간 (초)보다 10 배 더 많은 답변을 원합니다 int++. 그러나 나는 많은 것에 달려 있기 때문에 대답하기가 어렵다는 것을 알고 있습니다.
javaLover

6

비용은 구현에 따라 다르지만 다음 두 가지를 명심해야합니다.

  • 상당히 원시적 인 작업이므로 사용 패턴 ( 많이 사용됨)으로 인해 최대한 최적화되므로 비용이 최소화 될 가능성이 높습니다 .
  • 안전한 멀티 스레드 작업을 원할 경우 사용해야하기 때문에 비용이 얼마나 드는지는 중요하지 않습니다. 필요한 경우 필요합니다.

단일 프로세서 시스템에서는 일반적으로 데이터를 원자 적으로 변경할 수있을 정도로 오랫동안 인터럽트를 비활성화 할 수 있습니다. 다중 프로세서 시스템은 테스트 및 설정 전략을 사용할 수 있습니다 .

두 경우 모두 지침이 상대적으로 효율적입니다.

대규모 데이터 구조를 위해 단일 뮤텍스를 제공해야하는지 또는 각 섹션마다 하나씩 많은 뮤텍스를 가져야하는지 여부는 밸런싱 행위입니다.

단일 뮤텍스를 사용하면 여러 스레드간에 경합 위험이 높아집니다. 섹션 당 뮤텍스를 가짐으로써이 위험을 줄일 수 있지만 스레드가 작업을 수행하기 위해 180 개의 뮤텍스를 잠그는 상황에 들어가고 싶지는 않습니다.


1
예, 그러나 얼마나 효율적입니까? 단일 기계 명령입니까? 아니면 약 10? 아니면 약 100? 1000? 더? 이 모든 것이 여전히 효율적이지만 극단적 인 상황에서 차이를 만들 수 있습니다.
Albert

1
글쎄, 그것은 전적으로 구현에 달려 있습니다 . 약 6 개의 기계 명령으로 인터럽트를 끄고, 정수를 테스트 / 설정하고, 루프에서 인터럽트를 다시 활성화 할 수 있습니다. 프로세서가 단일 명령으로 제공하는 경향이 있기 때문에 테스트 및 설정은 약 많은 수로 수행 할 수 있습니다.
paxdiablo

버스 잠금 테스트 및 설정은 x86에서 하나의 긴 명령입니다. 그것을 사용하는 기계의 나머지 부분은 꽤 빠르다 ( "테스트 성공?"은 CPU가 빠르다는 점이다). 그러나 버스 잠금 명령의 길이는 실제로 사물을 차단하는 부분이기 때문에 중요하다. 인터럽트를 처리하는 솔루션은 일반적으로 사소한 DoS 공격을 중지하기 위해 OS 커널로 제한되므로 훨씬 느립니다.
Donal Fellows

BTW, 다른 사람에게 스레드 수율을 갖는 수단으로 삭제 / 재 획득을 사용하지 마십시오. 그것은 멀티 코어 시스템을 빨아들이는 전략입니다. (그것은 CPython이 잘못하는 비교적 적은 것들 중 하나입니다.)
Donal Fellows

@Donal : 드롭 / 재 취득의 의미는 무엇입니까? 그것은 중요하게 들린다; 그것에 대해 더 많은 정보를 줄 수 있습니까?
앨버트

5

나는 pthreads와 mutex를 완전히 처음 사용하지만, 경쟁이 없을 때 뮤텍스를 잠금 / 잠금 해제하는 데 드는 비용이 거의 변함이 없음을 실험에서 확인할 수 있지만, 경합이있을 때 차단 비용이 매우 높다는 것을 알 수 있습니다. 작업이 mutex 잠금으로 보호되는 전역 변수에서 합계를 계산하는 스레드 풀로 간단한 코드를 실행했습니다.

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

하나의 스레드로 프로그램은 사실상 순간적으로 (1 초 미만) 10,000,000 개의 값을 합칩니다. 두 개의 스레드 (4 개의 코어가있는 MacBook)에서 동일한 프로그램은 39 초가 걸립니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.