2011-02-11 4 views
9

나는 수백 개의 스레드를 생성하는 프로젝트를 수행하고 있습니다. 이러한 모든 스레드는 "절전 모드"상태입니다 (모니터 개체에 잠김). 나는 "sleeping"쓰레드의 수가 증가하면 프로그램의 속도가 느려진다는 것을 알았다. "재미있는"것은 작업 관리자를 보면 스레드 수가 많을수록 프로세서가 더 많이 사용되는 것처럼 보입니다. 객체 생성에 대한 문제를 좁혔습니다.많은 스레드가있는 개체 생성 속도 저하

누군가 나에게 설명해 줄 수 있습니까?

나는 그것을 테스트하기 위해 작은 샘플을 만들었습니다. 그것은 콘솔 프로그램입니다. 그것은 각 프로세서에 대한 스레드를 생성하고 간단한 테스트로 속도를 측정합니다 ("새 Object()"). 아니요, "새로운 객체()"는 떨어져 나가지 않습니다 (당신이 나를 믿지 않으면 시도하십시오). 주 스레드는 각 스레드의 속도를 보여줍니다. CTRL-C를 누르면이 프로그램은 50 개의 "휴면"스레드를 생성합니다. 속도 저하는 단지 50 개의 스레드로 시작됩니다. 약 250 개는 CPU가 100 % 사용되지 않은 작업 관리자에서 매우 잘 보입니다 (내 경우는 82 %입니다).

"잠자기"스레드를 잠그는 세 가지 방법을 시도했습니다. Thread.CurrentThread.Suspend() (나쁜, 나쁨, 나는 알고 있습니다 :-)), 이미 잠긴 개체와 Thread.Sleep (Timeout . 무한대). 그것은 동일합니다. 새 Object()를 사용하여 행에 주석을 추가하고 Math.Sqrt (또는 아무것도 사용하지 않음)로 바꾸면 문제가 없습니다. 속도는 스레드 수에 따라 변하지 않습니다. 다른 사람이 확인할 수 있습니까? 병목이 어디 있는지 아는 사람 있습니까?

아 ... 비주얼 스튜디오에서 시작하지 않고 릴리스 모드에서 테스트해야합니다. 이중 프로세서 (HT 없음)에서 XP sp3을 사용하고 있습니다. 뭔가 하나 필요 - 나는

namespace TestSpeed 
{ 
    using System; 
    using System.Collections.Generic; 
    using System.Threading; 

    class Program 
    { 
     private const long ticksInSec = 10000000; 
     private const long ticksInMs = ticksInSec/1000; 
     private const int threadsTime = 50; 
     private const int stackSizeBytes = 256 * 1024; 
     private const int waitTimeMs = 1000; 

     private static List<int> collects = new List<int>(); 
     private static int[] objsCreated; 

     static void Main(string[] args) 
     { 
      objsCreated = new int[Environment.ProcessorCount]; 
      Monitor.Enter(objsCreated); 

      for (int i = 0; i < objsCreated.Length; i++) 
      { 
       new Thread(Worker).Start(i); 
      } 

      int[] oldCount = new int[objsCreated.Length]; 

      DateTime last = DateTime.UtcNow; 

      Console.Clear(); 

      int numThreads = 0; 
      Console.WriteLine("Press Ctrl-C to generate {0} sleeping threads, Ctrl-Break to end.", threadsTime); 

      Console.CancelKeyPress += (sender, e) => 
      { 
       if (e.SpecialKey != ConsoleSpecialKey.ControlC) 
       { 
        return; 
       } 

       for (int i = 0; i < threadsTime; i++) 
       { 
        new Thread(() => 
        { 
         /* The same for all the three "ways" to lock forever a thread */ 
         //Thread.CurrentThread.Suspend(); 
         //Thread.Sleep(Timeout.Infinite); 
         lock (objsCreated) { } 
        }, stackSizeBytes).Start(); 

        Interlocked.Increment(ref numThreads); 
       } 

       e.Cancel = true; 
      }; 

      while (true) 
      { 
       Thread.Sleep(waitTimeMs); 

       Console.SetCursorPosition(0, 1); 

       DateTime now = DateTime.UtcNow; 

       long ticks = (now - last).Ticks; 

       Console.WriteLine("Slept for {0}ms", ticks/ticksInMs); 

       Thread.MemoryBarrier(); 

       for (int i = 0; i < objsCreated.Length; i++) 
       { 
        int count = objsCreated[i]; 
        Console.WriteLine("{0} [{1} Threads]: {2}/sec ", i, numThreads, ((long)(count - oldCount[i])) * ticksInSec/ticks); 
        oldCount[i] = count; 
       } 

       Console.WriteLine(); 

       CheckCollects(); 

       last = now; 
      } 
     } 

     private static void Worker(object obj) 
     { 
      int ix = (int)obj; 

      while (true) 
      { 
       /* First and second are slowed by threads, third, fourth, fifth and "nothing" aren't*/ 

       new Object(); 
       //if (new Object().Equals(null)) return; 
       //Math.Sqrt(objsCreated[ix]); 
       //if (Math.Sqrt(objsCreated[ix]) < 0) return; 
       //Interlocked.Add(ref objsCreated[ix], 0); 

       Interlocked.Increment(ref objsCreated[ix]); 
      } 
     } 

     private static void CheckCollects() 
     { 
      int newMax = GC.MaxGeneration; 

      while (newMax > collects.Count) 
      { 
       collects.Add(0); 
      } 

      for (int i = 0; i < collects.Count; i++) 
      { 
       int newCol = GC.CollectionCount(i); 

       if (newCol != collects[i]) 
       { 
        collects[i] = newCol; 
        Console.WriteLine("Collect gen {0}: {1}", i, newCol); 
       } 
      } 
     } 
    } 
} 
+3

성능에 대해 우려하는 경우 (cpucount) 개 이상의 스레드가 없어야합니다. (cpucount + 2)와 (cpucount * 2) 사이에는 좋은 규칙이 있습니다 (시스템에서 둘 다 4로 나옵니다). 비동기 I/O 조작의 대기열을 사용하여 잠자는 대신 소수의 스레드를 사용중인 상태로 유지하십시오. 스레드가 대기해야하는 유일한 시간은 잠금을 요구할 때입니다. –

+0

"슬로우 모션"코 루틴을하고 있습니다. 스레드 사이의 "전환 시간"은 관련이 없으므로 스레드를 사용할 수 있습니다 (이전의 스레드와 새 스레드간에 전환을 수행하기 위해 일부 ms가 손실 되더라도 초당 "전환"/ 초가 있습니다). 모든 문제). 프로세서와 동일한 수의 스레드가 항상 실행되지만 수면 스레드가 모든 것을 느리게하면 문제가 발생합니다. 아니요, MS의 비동기 라이브러리를 사용할 수 없습니다. 왜냐하면 "가짜"이기 때문입니다. 프로그램을 "다시 작성"합니다. 기존 라이브러리를 사용해야합니다. – xanatos

+0

명시 적으로 쓰레드를 생성하는 대신 TPL을 사용하는 것이 고려 되었습니까? 그렇게하면 프레임 워크가 가장 적절한 수의 원시 스레드를 결정하여 작업을 수행 할 수 있습니다. –

답변

5

추측이 문제가 가비지 컬렉션 스레드 간의 협력의 일정 금액을 필요로한다는 점이다 (다른 프레임 워크 런타임을 테스트)의 .NET 3.5 및 4.0을 테스트 한 모두 일시 중지되었는지 확인하거나 일시 중지하도록 요청하고 일어날 때까지 기다리십시오. (심지어 이 (가)으로 일시 중지 된 경우에도 깨우지 말라고 알려야합니다.)

"세계를 멈추십시오"가비지 컬렉터를 설명합니다. 병렬 처리에 관한 세부 사항이 다른 GC 구현은 적어도 두 개 또는 세 개가 있다고 생각합니다. 그러나 모두가 스레드가 협조하도록하는 데있어 작업을 수행 할 것으로 예상됩니다.

+0

"서버"GC를 사용해 보았습니다. 각 프로세서에 대해 GC와 힙을 할당합니다. 앱이 더 잘 확장됩니다. 100 개의 쓰레드를 사용하면 객체 할당 속도의 "10 %"만 손실됩니다. – xanatos

+0

더 많은 테스트를 수행할수록 그것이 GC라고 확신합니다. GC를 "벤치마킹"하고 객체 생성 시간과 시간을 구별하는 것은 매우 어렵지만, 결국 이것은 내 POV에서 아무 것도 변경하지 않습니다. 많은 스레드 = 느린 "새로운"객체 (적어도 새로운 개체가 GC 수집을 유발 함). 서버 GC = 많은 스레드가있을 때 좋습니다. 객체 풀링을 시도 할 수는 있지만 복잡성이 증가 할 것이라고 생각합니다 ... 보겠습니다. 감사! – xanatos

10

Taskmgr.exe, 프로세스 탭을 시작하십시오. 보기 + 열을 선택하고 "페이지 오류 델타"를 확인하십시오. 수백 메가 바이트 할당의 영향을 보게 될 것입니다. 생성 한 모든 스레드의 스택을 저장하기 만하면됩니다. 숫자가 프로세스에 나타날 때마다 프로그램은 운영 체제에서 디스크의 데이터를 RAM으로 페이징하는 것을 기다립니다.

탕스타프 (TANSTAAFL) 무료 점심 같은 것은 없습니다.

+0

1MB 사용자 모드 스택 공간 + 1MB 기본 모드 스택 공간, 생성시 각 스레드의 기본 크기. –

+2

@Chris, 기본 모드 스택이 없으며, 하나의 스택이 둘 다 제공됩니다. 그러나 생성 된 모든 스레드에는 24KB 커널 모드 스택이 있습니다. –

+0

@Hans, 설명 주셔서 감사합니다, 나는 네이티브 대신에 커널을 의미했지만이 크기도 1MB라고 생각했습니다. –

1

여기에 표시되는 것은 GC 작동 방식입니다. 디버거를 프로세스에 연결하면

Unknown exception - code e0434f4e (first chance) 

등의 예외가 발생합니다. GC가 일시 중단 된 스레드를 다시 시작하여 발생하는 예외입니다. 아시다시피 프로세스 내에서 Suspend/ResumeThread를 호출하는 것이 좋습니다. 이는 관리 세계에서 더욱 사실입니다. 이것을 안전하게 수행 할 수있는 유일한 권한은 GC입니다.당신이 SuspendThread로에서 브레이크 포인트를 설정하면 당신은 GC 그는 전체 모음을 수행 할 수 있습니다 전에 모든 스레드를 일시 중단하려고 않습니다

0118f010 5f3674da 00000000 00000000 83e36f53 KERNEL32!SuspendThread 
0118f064 5f28c51d 00000000 83e36e63 00000000 mscorwks!Thread::SysSuspendForGC+0x2b0 (FPO: [Non-Fpo]) 
0118f154 5f28a83d 00000001 00000000 00000000 mscorwks!WKS::GCHeap::SuspendEE+0x194 (FPO: [Non-Fpo]) 
0118f17c 5f28c78c 00000000 00000000 0000000c mscorwks!WKS::GCHeap::GarbageCollectGeneration+0x136 (FPO: [Non-Fpo]) 
0118f208 5f28a0d3 002a43b0 0000000c 00000000 mscorwks!WKS::gc_heap::try_allocate_more_space+0x15a (FPO: [Non-Fpo]) 
0118f21c 5f28a16e 002a43b0 0000000c 00000000 mscorwks!WKS::gc_heap::allocate_more_space+0x11 (FPO: [Non-Fpo]) 
0118f23c 5f202341 002a43b0 0000000c 00000000 mscorwks!WKS::GCHeap::Alloc+0x3b (FPO: [Non-Fpo]) 
0118f258 5f209721 0000000c 00000000 00000000 mscorwks!Alloc+0x60 (FPO: [Non-Fpo]) 
0118f298 5f2097e6 5e2d078c 83e36c0b 00000000 mscorwks!FastAllocateObject+0x38 (FPO: [Non-Fpo]) 

을 볼 수 있습니다. 내 컴퓨터 (32 비트, Windows 7, .NET 3.5 SP1)에서 속도 저하는 그렇게 극적이지 않습니다. 스레드 수와 CPU (비 사용) 사이의 선형 종속성을 확인했습니다. GC가 전체 수집을 수행하기 전에 더 많은 스레드를 일시 중단해야하기 때문에 각 GC에 대한 비용이 증가하는 것 같습니다. 재미있게도 시간은 usermode에서 주로 소비되므로 커널은 제한적인 요소가 아닙니다.

나는 쓰레드를 줄이거 나 비 관리 코드를 사용하는 것을 제외하고는 어떻게 대처할 수 있는지 살펴 봅니다. 너 자신이 CLR을 호스트하고 GC가 훨씬 잘 확장 될 물리적 스레드 대신 Fibers를 사용하는 경우 일 수 있습니다. 불행하게도이 기능은 .NET 2.0의 relase주기 동안 cut out입니다. 6 년 후인 지금부터 다시 추가 될 것이라는 희망은 거의 없습니다.

스레드 카운트 외에도 GC는 개체 그래프의 복잡성으로 인해 제한됩니다. 이 "Do You Know The Costs Of Garbage?"을 살펴보십시오.

+0

+1 문제를 일으키는 GC임을 발견했습니다. . 그는 아마도 이미 대기중인 스레드를 일시 중지하려고 시도합니다. 따라서 O (n) = O (m) 대신 n = 총 스레드 수 (m = 실행중인 스레드 수)입니다. 안타깝게도 이미 광섬유 트릭을 연구했고 그것이 잘려 나갔다는 것을 알고있었습니다 .-(그리고 오래된 Async CTP는 다른 작업을 기다리지 않고 즉시 종료되는 작업을 실행하는 데 느린 몇 가지 문제가있었습니다 (그들은 새로운 비동기 CTP하지만 그 사이에 다른 프로젝트에서 일을 시작한) – xanatos

+0

내가 왜 O (m) 수없는 이유는 당신이 일부 스레드가 GC의 중간에 일어날 수도 시간 초과로 예를 기다리는 경우입니다. 게다가 당신은 GC가 그것이 모든 스레드를 보류하는 동안 일시 중단되었다고 생각하는 스레드를 깨울 수 있습니다. –

+0

그들은 다른 방법으로 그것을 해결할 수있었습니다. 다양한 대기는 OS와 직접 통신 할 필요가 없습니다. GC에 의해 "중재"되었을 수 있습니다. 그들은이 방법으로 그것을 선택했고, 우리는 그것을해야합니다. – xanatos

관련 문제