스레드를 만드는 데 비용이 많이 드는 이유는 무엇입니까?


180

자바 튜토리얼에서는 스레드를 만드는 것이 비싸다고 말합니다. 그러나 왜 정확히 비쌉니까? Java 스레드가 작성 될 때 작성 비용이 많이 드는 상황은 정확히 무엇입니까? 나는 그 진술을 사실로 받아들이고 있지만 JVM에서 스레드 생성의 메커니즘에 관심이 있습니다.

스레드 수명주기 오버 헤드 스레드 생성 및 분해는 무료가 아닙니다. 실제 오버 헤드는 플랫폼마다 다르지만 스레드 작성에는 시간이 걸리므로 요청 처리에 지연 시간이 발생하며 JVM 및 OS의 일부 처리 활동이 필요합니다. 대부분의 서버 응용 프로그램에서와 같이 요청이 빈번하고 가벼운 경우 각 요청에 대해 새 스레드를 만들면 상당한 컴퓨팅 리소스가 소비 될 수 있습니다.

에서 연습 자바 동시성
으로 브라이언 게츠, 팀 Peierls, 조슈아 블로흐, 조셉 Bowbeer, 데이비드 홈즈, 더그 레아
ISBN-10을 인쇄 : 0-321-34960-1를


나는 당신이 읽은 튜토리얼이 이것을 말하는 문맥을 모른다 : 그것들은 창조 자체가 비싸거나 "스레드 생성"이 비싸다는 것을 암시 하는가. 내가 보여 주려는 차이점은 스레드를 만드는 순수한 동작 (인스턴스를 인스턴스화하거나 무언가를 호출) 또는 스레드가 있다는 사실 (따라서 스레드 사용 : 분명히 오버 헤드가 있음)입니다. // 어느쪽에 대해 물어보고 싶은가?
Nanne

9
@typoknig - 새 스레드 : 생성하지에 비해 비싼
willcodejavaforfood


1
승리를위한 쓰레드 풀. 작업을 위해 항상 새 스레드를 만들 필요가 없습니다.
Alexander Mills

답변:


149

Java 스레드 작성은 비용이 많이 듭니다.

  • 스레드 스택에 대해 큰 메모리 블록을 할당하고 초기화해야합니다.
  • 호스트 OS에 원시 스레드를 작성 / 등록하려면 시스템 호출이 필요합니다.
  • 설명자는 작성하고 초기화하여 JVM 내부 데이터 구조에 추가해야합니다.

스레드가 살아있는 한 리소스를 묶는다는 점에서 비싸다. 예를 들어 스레드 스택, 스택에서 도달 할 수있는 모든 객체, JVM 스레드 설명자, OS 기본 스레드 설명자

이 모든 것들의 비용은 플랫폼에 따라 다르지만 내가 본 Java 플랫폼에서는 저렴하지 않습니다.


Google 검색에서 2002 빈티지 Linux를 실행하는 2002 빈티지 듀얼 프로세서 Xeon의 Sun Java 1.4.1에서 초당 ~ 4000 스레드 생성 속도를보고 하는 오래된 벤치 마크 를 발견했습니다 . 더 현대적인 플랫폼은 더 나은 숫자를 줄 것입니다 ... 방법론에 대해서는 언급 할 수 없지만 ... 적어도 스레드 생성 비용얼마나 드는지에 대한 야구장을 제공 합니다.

Peter Lawrey의 벤치마킹은 요즘 스레드 생성이 절대적으로 훨씬 빠르다는 것을 보여 주지만, Java 및 / 또는 OS ... 또는 더 높은 프로세서 속도의 개선이 어느 정도인지는 확실하지 않습니다. 그러나 스레드 풀을 사용하고 매번 새 스레드를 작성 / 시작하면 그의 숫자는 여전히 150 배 이상 향상되었습니다. (그리고 그는 이것이 모두 상대적이라는 것을 강조합니다 ...)


(위의 내용은 "그린 스레드"가 아닌 "네이티브 스레드"를 가정하지만 최신 JVM은 모두 성능상의 이유로 네이티브 스레드를 사용합니다. 그린 스레드는 훨씬 저렴하지만 다른 지역에서는 비용을 지불합니다.)


Java 스레드의 스택이 실제로 어떻게 할당되는지 확인하기 위해 약간의 파기를 수행했습니다. Linux에서 OpenJDK 6의 경우 스레드 스택은 pthread_create기본 스레드를 작성하는 호출에 의해 할당됩니다 . (JVM은 pthread_create사전 할당 된 스택을 전달하지 않습니다 .)

그런 다음 pthread_create스택 내 에서 mmap다음과 같이 호출하여 할당됩니다 .

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

에 따르면 man mmap, MAP_ANONYMOUS플래그는 메모리가 0으로 초기화되도록한다.

따라서 (Java 사양에서) 새로운 Java 스레드 스택을 제로화 할 필요는 없지만 실제로는 (적어도 Linux에서는 OpenJDK 6에서는) 제로화됩니다.


2
@Raedwald-비싼 초기화 부분입니다. 어딘가에, 블록 (예 : GC 또는 OS)은 블록이 스레드 스택으로 바뀌기 전에 바이트를 0으로 만듭니다. 일반적인 하드웨어에서는 실제 메모리주기가 필요합니다.
Stephen C

2
"어딘가에, 무언가 (예를 들어 GC 또는 OS)가 바이트를 0으로 만들 것이다". 그럴까요? 보안상의 이유로 OS는 새 메모리 페이지를 할당해야하는 경우입니다. 그러나 그것은 드문 일입니다. 그리고 OS는 이미 0으로 된 페이지의 캐시를 유지할 수 있습니다 (IIRC, Linux). JVM이 Java 프로그램이 해당 컨텐츠를 읽지 못하게한다면 GC가 왜 귀찮게합니까? malloc()JVM이 잘 사용할 수 있는 표준 C 함수 는 할당 된 메모리가 제로화되는 것을 보장 하지 않습니다 (아마 그러한 성능 문제를 피하기 위해).
Raedwald

1
stackoverflow.com/questions/2117072/… "하나의 주요 요소는 각 스레드에 할당 된 스택 메모리"입니다.
Raedwald

2
@Raedwald-스택이 실제로 할당되는 방법에 대한 정보는 업데이트 된 답변을 참조하십시오.
Stephen C

2
mmap()호출에 의해 할당 된 메모리 페이지 는 쓰기시 복사시 0 페이지에 매핑되므로 그 mmap()자체 가 아닌 페이지가 처음 기록 될 때 페이지가 한 페이지에 시작될 때 그 세부 정보가 발생할 수 있습니다 . 시간. 즉, 스레드가 실행을 시작할 때 작성자 스레드가 아닌 작성된 스레드에 의해 비용이 발생합니다.
Raedwald

76

다른 사람들은 스레딩 비용이 어디에서 오는지 논의했습니다. 스레드를 생성하는 많은 작업에 비해 비싸지 만하지 왜이 답변 커버 상대적 있는 작업 실행 대안에 비해 비용이 상대적으로 저렴합니다.

다른 스레드에서 작업을 실행하는 가장 확실한 대안은 동일한 스레드에서 작업을 실행하는 것입니다. 더 많은 스레드가 항상 더 좋다고 가정하는 사람들에게는 이해하기 어렵습니다. 논리는 작업을 다른 스레드에 추가하는 오버 헤드가 저장 시간보다 클 경우 현재 스레드에서 작업을 수행하는 것이 더 빠를 수 있다는 것입니다.

다른 대안은 스레드 풀을 사용하는 것입니다. 스레드 풀은 두 가지 이유로 더 효율적일 수 있습니다. 1) 이미 작성된 스레드를 재사용합니다. 2) 스레드 수를 조정 / 조정하여 최적의 성능을 보장 할 수 있습니다.

다음 프로그램이 인쇄됩니다.

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

이것은 각 스레딩 옵션의 오버 헤드를 노출하는 사소한 작업에 대한 테스트입니다. 이 테스트 작업은 현재 스레드에서 실제로 가장 잘 수행되는 작업입니다.

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

보시다시피, 새로운 스레드를 생성하는 데는 ~ 70 µs 밖에 없습니다. 대부분의 경우는 아니지만 많은 사용 사례에서 이는 사소한 것으로 간주 될 수 있습니다. 상대적으로 말하자면 대안보다 비용이 많이 들며 일부 상황에서는 스레드 풀을 사용하거나 스레드를 전혀 사용하지 않는 것이 더 나은 솔루션입니다.


8
그것은 훌륭한 코드입니다. 간결하고 요점을 명확하게 표시합니다.
Nicholas

마지막 블록에서는 처음 두 블록에서 작업자 스레드가 배치함에 따라 주 스레드가 병렬로 제거되기 때문에 결과가 왜곡되었다고 생각합니다. 그러나 마지막 블록에서 취해지는 조치는 모두 순차적으로 수행되므로 값이 확장됩니다. 아마도 queue.clear ()를 사용하고 대신 CountDownLatch를 사용하여 스레드가 완료 될 때까지 기다릴 수 있습니다.
Victor Grazi

@VictorGrazi 결과를 중앙에서 수집한다고 가정합니다. 각 경우에 동일한 양의 큐잉 작업을 수행합니다. 카운트 다운 래치가 약간 빠릅니다.
Peter Lawrey

실제로 카운터를 증가시키는 것과 같이 일관되게 빠른 작업을 수행해야하는 이유는 무엇입니까? 전체 BlockingQueue 항목을 삭제하십시오. 컴파일러가 증분 연산을 최적화하지 못하도록 마지막 카운터를 확인하십시오.
Victor Grazi 1

@ grazi이 경우에는 그렇게 할 수 있지만 카운터를 기다리는 것이 비효율적 일 수 있으므로 가장 현실적인 경우에는 그렇지 않습니다. 그렇게하면 예제 간의 차이가 훨씬 커질 것입니다.
Peter Lawrey

31

이론적으로 이것은 JVM에 따라 다릅니다. 실제로 모든 스레드에는 상대적으로 많은 양의 스택 메모리가 있습니다 (기본값은 256KB입니다). 또한 스레드는 OS 스레드로 구현되므로이를 만들려면 컨텍스트 호출과 같은 OS 호출이 필요합니다.

컴퓨팅에서 "고가"는 항상 상대적이라는 것을 인식하십시오. 스레드 생성은 대부분의 개체 생성에 비해 비용이 많이 들지만 임의의 하드 디스크 검색에 비해 비용이 많이 들지 않습니다. 모든 비용으로 스레드 생성을 피할 필요는 없지만 초당 수백 개의 스레드를 생성하는 것은 현명한 방법이 아닙니다. 대부분의 경우 디자인에 많은 스레드가 필요한 경우 제한된 크기의 스레드 풀을 사용해야합니다.


9
Btw kb = 킬로 비트, kB = 킬로바이트. Gb = 기가비트, GB = 기가 바이트.
Peter Lawrey

@PeterLawrey 'kb'와 'kB'에서 'k'를 대문자로 사용하므로 'Gb'와 'GB'에 대칭이 있습니까? 이런 것들이 나를 괴롭힌다.
Jack

3
@Jack K= 1024와 k= 1000이 있습니다.;) en.wikipedia.org/wiki/Kibibyte
Peter Lawrey

9

스레드에는 두 가지 종류가 있습니다.

  1. 적절한 스레드 : 기본 운영 체제의 스레딩 기능에 대한 추상화입니다. 따라서 스레드 생성은 시스템의 비용만큼 비쌉니다. 항상 오버 헤드가 있습니다.

  2. "녹색"스레드 : JVM에 의해 작성 및 스케줄되며 저렴하지만 적절한 병렬화가 발생하지 않습니다. 이들은 스레드처럼 동작하지만 OS의 JVM 스레드 내에서 실행됩니다. 그들은 종종 내 지식에 익숙하지 않습니다.

스레드 생성 오버 헤드에서 생각할 수있는 가장 큰 요소는 스레드에 대해 정의한 스택 크기 입니다. VM을 실행할 때 스레드 스택 크기를 매개 변수로 전달할 수 있습니다.

그 외에도 스레드 생성은 대부분 OS에 따라 다르며 심지어 VM 구현에 따라 다릅니다.

이제 주목할 점이 있습니다. 런타임마다 초당 2000 개의 스레드를 실행 하려는 경우 스레드를 만드는 데 많은 비용이 듭니다 . JVM은이를 처리하도록 설계되지 않았습니다 . 계속해서 해고 당하지 않는 몇 명의 안정된 근로자가 있다면 휴식을 취하십시오.


19
"... 해고 당하지 않는 안정된 노동자 몇 명 ..." 왜 직장 상황에 대해 생각하기 시작 했습니까? :-)
Stephen C

6

생성 Threads에는 하나가 아니라 두 개의 새로운 스택 (하나는 자바 코드, 하나는 기본 코드)을 만들어야하기 때문에 상당한 양의 메모리를 할당해야합니다. 의 사용 집행 인은 / 스레드 풀은 여러 작업을 위해 스레드를 재사용하여, 오버 헤드를 피할 수 집행자 .


@Raedwald, 별도의 스택을 사용하는 jvm은 무엇입니까?
bestsss 2016 년

1
필립 JP는 두 가지 스택을 말합니다.
Raedwald

내가 아는 한 모든 JVM은 스레드 당 두 개의 스택을 할당합니다. 가비지 콜렉션이 Java 코드 (JITed 인 경우에도)를 프리 캐스팅과 다르게 처리하는 데 도움이됩니다. c.
Philip JF

@Philip JF 자세히 설명해 주시겠습니까? Java 코드와 네이티브 코드 각각에 대해 2 개의 스택이 무엇을 의미합니까? 무엇을합니까?
Gurinder

"내가 아는 한 모든 JVM은 스레드 당 두 개의 스택을 할당합니다." -이것을 뒷받침 할만한 증거는 본 적이 없습니다. 아마도 JVM 스펙에서 opstack의 본질을 오해하고있을 것입니다. (이것은 바이트 코드의 동작을 모델링하는 방법이며, 런타임에 바이트 코드를 실행하기 위해 사용해야하는 것은 아닙니다.)
Stephen C

1

분명히 문제의 요점은 '비싸다'는 의미입니다.

스레드는 스택을 작성하고 run 메소드를 기반으로 스택을 초기화해야합니다.

제어 상태 구조, 즉 실행 가능한 상태, 대기 상태 등을 설정해야합니다.

이러한 것들을 설정하는 데는 상당한 동기화가있을 것입니다.

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