Programing

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

lottogame 2020. 6. 29. 07:58
반응형

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


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

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

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

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

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


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


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

스레드가 많고 오브젝트에 대한 액세스가 자주 발생하면 여러 잠금이 병렬 처리를 증가시킵니다. 잠금이 많을수록 잠금 디버깅이 더 많으므로 유지 관리 비용이 많이 듭니다.

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

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

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

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

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

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

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

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

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

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


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

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

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


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

두 개의 코어 (CPU # 0 및 # 1 사용) 간의 동기화로 인해 두 스레드에서 102 ns마다 한 번만 잠금 / 잠금 해제 쌍을 호출 할 수 있으므로 51 ns마다 한 번만 소요됩니다. 스레드가 잠금을 해제 한 후 복구하려면 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이 약간 더 빠릅니다.

The above function is called in a tight loop of size 100,000. Despite that the function is slightly faster for thread 1, both loops synchronize because of the call to the mutex. This is visible in the graph from the fact that the number of clocks measured for the lock/unlock pair is slightly larger for thread 1, to account for the shorter delay in the loop below it.

In the above graph the bottom right point is a measurement with a delay loop_count of 150, and then following the points at the bottom, towards the left, the loop_count is reduced by one each measurement. When it becomes 77 the function is called every 102 ns in both threads. If subsequently loop_count is reduced even further it is no longer possible to synchronize the threads and the mutex starts to be actually locked most of the time, resulting in an increased amount of clocks that it takes to do the lock/unlock. Also the average time of the function call increases because of this; so the plot points now go up and towards the right again.

From this we can conclude that locking and unlocking a mutex every 50 ns is not a problem on my box.

All in all my conclusion is that the answer to question of OP is that adding more mutexes is better as long as that results in less contention.

Try to lock mutexes as short as possible. The only reason to put them -say- outside a loop would be if that loop loops faster than once every 100 ns (or rather, number of threads that want to run that loop at the same time times 50 ns) or when 13 ns times the loop size is more delay than the delay you get by contention.

EDIT: I got a lot more knowledgable on the subject now and start to doubt the conclusion that I presented here. First of all, CPU 0 and 1 turn out to be hyper-threaded; even though AMD claims to have 8 real cores, there is certainly something very fishy because the delays between two other cores is much larger (ie, 0 and 1 form a pair, as do 2 and 3, 4 and 5, and 6 and 7). Secondly, the std::mutex is implemented in way that it spin locks for a bit before actually doing system calls when it fails to immediately obtain the lock on a mutex (which no doubt will be extremely slow). So what I have measured here is the absolute most ideal situtation and in practise locking and unlocking might take drastically more time per lock/unlock.

Bottom line, a mutex is implemented with atomics. To synchronize atomics between cores an internal bus must be locked which freezes the corresponding cache line for several hundred clock cycles. In the case that a lock can not be obtained, a system call has to be performed to put the thread to sleep; that is obviously extremely slow. Normally that is not really a problem because that thread has to sleep anyway-- but it could be a problem with high contention where a thread can't obtain the lock for the time that it normally spins and so does the system call, but CAN take the lock shortly there after. For example, if several threads lock and unlock a mutex in a tight loop and each keeps the lock for 1 microsecond or so, then they might be slowed down enormously by the fact that they are constantly put to sleep and woken up again.


This depends on what you actually call "mutex", OS mode and etc.

At minimum it's a cost of an interlocked memory operation. It's a relatively heavy operation (compared to other primitive assembler commands).

However, that can be very much higher. If what you call "mutex" a kernel object (i.e. - object managed by the OS) and run in the user mode - every operation on it leads to a kernel mode transaction, which is very heavy.

For example on Intel Core Duo processor, Windows XP. Interlocked operation: takes about 40 CPU cycles. Kernel mode call (i.e. system call) - about 2000 CPU cycles.

If this is the case - you may consider using critical sections. It's a hybrid of a kernel mutex and interlocked memory access.


The cost will vary depending on the implementation but you should keep in mind two things:

  • the cost will be most likely be minimal since it's both a fairly primitive operation and it will be optimised as much as possible due to its use pattern (used a lot).
  • it doesn't matter how expensive it is since you need to use it if you want safe multi-threaded operation. If you need it, then you need it.

On single processor systems, you can generally just disable interrupts long enough to atomically change data. Multi-processor systems can use a test-and-set strategy.

In both those cases, the instructions are relatively efficient.

As to whether you should provide a single mutex for a massive data structure, or have many mutexes, one for each section of it, that's a balancing act.

By having a single mutex, you have a higher risk of contention between multiple threads. You can reduce this risk by having a mutex per section but you don't want to get into a situation where a thread has to lock 180 mutexes to do its job :-)


I'm completely new to pthreads and mutex, but I can confirm from experimentation that the cost of locking/unlocking a mutex is almost zilch when there is no contention, but when there is contention, the cost of blocking is extremely high. I ran a simple code with a thread pool in which the task was just to compute a sum in a global variable protected by a mutex lock:

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

With one thread, the program sums 10,000,000 values virtually instantaneously (less than one second); with two threads (on a MacBook with 4 cores), the same program takes 39 seconds.

참고 URL : https://stackoverflow.com/questions/3652056/how-efficient-is-locking-an-unlocked-mutex-what-is-the-cost-of-a-mutex

반응형