C ++ 11의 async (launch :: async)는 값 비싼 스레드 생성을 피하기 위해 스레드 풀을 쓸모 없게 만들까요?


117

이 질문과 느슨하게 관련이 있습니다. std :: thread는 C ++ 11에서 풀링됩니까? . 질문은 다르지만 의도는 동일합니다.

질문 1 : 값 비싼 스레드 생성을 방지하기 위해 자체 (또는 타사 라이브러리) 스레드 풀을 사용하는 것이 여전히 타당합니까?

다른 질문의 결론은 std::thread풀링 에 의존 할 수 없다는 것입니다 (그럴 수도 있고 아닐 수도 있음). 그러나 std::async(launch::async)풀링 될 가능성이 훨씬 더 높은 것 같습니다.

표준에 의해 강제된다고 생각하지 않지만 IMHO는 스레드 생성이 느리면 모든 좋은 C ++ 11 구현이 스레드 풀링을 사용할 것이라고 기대합니다. 새 스레드를 만드는 것이 저렴한 플랫폼에서만 항상 새 스레드를 생성 할 것으로 예상합니다.

질문 2 : 이것이 바로 제 생각이지만 증명할 사실이 없습니다. 나는 아주 잘 착각 할 수 있습니다. 교육받은 추측입니까?

마지막으로 스레드 생성이 다음과 같이 표현 될 수 있다고 생각하는 방법을 먼저 보여주는 샘플 코드를 제공했습니다 async(launch::async).

예 1 :

 thread t([]{ f(); });
 // ...
 t.join();

된다

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

예제 2 : 스레드 실행 및 삭제

 thread([]{ f(); }).detach();

된다

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

질문 3 : async버전보다 버전을 선호 thread합니까?


나머지는 더 이상 질문의 일부가 아니라 설명을위한 것입니다.

반환 값을 더미 변수에 할당해야하는 이유는 무엇입니까?

불행히도 현재 C ++ 11 표준은 반환 값을 캡처하도록 강제합니다. std::async그렇지 않으면 소멸자가 실행되어 작업이 종료 될 때까지 차단됩니다. 일부는 표준 오류로 간주합니다 (예 : Herb Sutter).

cppreference.com 의이 예제는이를 잘 보여줍니다.

{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}

또 다른 설명 :

스레드 풀이 다른 합법적 인 용도로 사용될 수 있다는 것을 알고 있지만이 질문에서는 값 비싼 스레드 생성 비용을 피하는 측면에만 관심이 있습니다 .

특히 리소스에 대한 더 많은 제어가 필요한 경우 스레드 풀이 매우 유용한 상황이 여전히 있다고 생각합니다. 예를 들어 서버는 빠른 응답 시간을 보장하고 메모리 사용량의 예측 가능성을 높이기 위해 고정 된 수의 요청 만 동시에 처리하기로 결정할 수 있습니다. 스레드 풀은 괜찮을 것입니다.

스레드 로컬 변수는 자신의 스레드 풀에 대한 인수가 될 수도 있지만 실제로 관련성이 있는지 확실하지 않습니다.

  • std::thread초기화 된 스레드 로컬 변수없이 시작 되는 새 스레드를 만듭니다. 아마도 이것은 당신이 원하는 것이 아닙니다.
  • 에 의해 생성 된 스레드에서는 스레드를 async재사용 할 수 있었기 때문에 다소 불분명합니다. 내 이해에 따르면 스레드 로컬 변수는 재설정이 보장되지 않지만 실수 할 수 있습니다.
  • 반면에 자체 (고정 크기) 스레드 풀을 사용하면 실제로 필요한 경우 모든 권한을 얻을 수 있습니다.

8
"하지만 std::async(launch::async)풀링 될 가능성이 훨씬 더 높은 것 같습니다." 아니요, 그게 std::async(launch::async | launch::deferred)합쳐질 수 있다고 생각합니다 . 단지와 함께 launch::async작업에 관계없이 다른 작업이 실행중인 새로운 스레드에서 출시 될 예정이다. 정책 launch::async | launch::deferred을 통해 구현은 어떤 정책을 선택할 수 있지만 더 중요한 것은 어떤 정책을 선택하는 것을 지연시키는 것입니다. 즉, 스레드 풀의 스레드가 사용 가능해질 때까지 기다린 다음 비동기 정책을 선택할 수 있습니다.
bames53

2
내가 아는 한 VC ++ 만 std::async(). 스레드 풀에서 사소하지 않은 thread_local 소멸자를 지원하는 방법이 궁금합니다.
bames53

2
@ bames53 gcc 4.7.2와 함께 제공되는 libstdc ++를 살펴본 결과 시작 정책이 정확 하지 않은 launch::async경우에만있는 것처럼 처리 launch::deferred하고 비동기 적으로 실행하지 않는 것으로 나타났습니다. 따라서 실제로 해당 버전의 libstdc ++는 "선택"합니다. 달리 강제하지 않는 한 항상 deferred를 사용합니다.
doug65536 2014

3
@ doug65536 thread_local 소멸자에 대한 내 요점은 스레드 풀을 사용할 때 스레드 종료시 파괴가 정확하지 않다는 것입니다. 작업이 비동기 적으로 실행되면 사양에 따라 '새 스레드 에서처럼'실행됩니다. 즉, 모든 비동기 작업은 자체 thread_local 개체를 가져옵니다. 스레드 풀 기반 구현은 동일한 백업 스레드를 공유하는 작업이 마치 자신의 thread_local 객체를 가진 것처럼 계속 작동하도록 특별히주의해야합니다. 이 프로그램을 고려하십시오 : pastebin.com/9nWUT40h
bames53

2
@ bames53 사양에서 "새 스레드에있는 것처럼"를 사용하는 것은 제 생각 에는 실수 였습니다 . std::async성능면에서 아름다운 것이 될 수 있습니다. 스레드 풀에 의해 자연스럽게 지원되는 표준 단기 실행 작업 실행 시스템 일 수 있습니다. 지금 당장 std::thread은 스레드 함수가 값을 반환 할 수 있도록하기 위해 약간의 쓰레기가 붙어 있습니다. 아, 그리고 그들은 std::function완전히 겹치는 중복 된 "지연된"기능을 추가했습니다 .
doug65536 2014

답변:


54

질문 1 :

원본이 잘못 되었기 때문에 원본에서 변경했습니다. 나는 Linux 스레드 생성이 매우 저렴 하다는 인상을 받았으며 테스트 후 새 스레드와 일반 스레드에서 함수 호출의 오버 헤드가 엄청나다는 것을 확인했습니다. 함수 호출을 처리하기 위해 스레드를 생성하는 오버 헤드는 일반 함수 호출보다 10000 배 이상 느립니다. 따라서 작은 함수 호출을 많이 실행하는 경우 스레드 풀이 좋은 생각 일 수 있습니다.

g ++와 함께 제공되는 표준 C ++ 라이브러리에는 스레드 풀이없는 것이 분명합니다. 그러나 나는 그들에 대한 사례를 확실히 볼 수 있습니다. 어떤 종류의 스레드 간 대기열을 통해 호출을 밀어 넣어야하는 오버 헤드가 있더라도 새 스레드를 시작하는 것보다 저렴할 수 있습니다. 그리고 표준은 이것을 허용합니다.

IMHO, Linux 커널 사람들은 스레드 생성을 현재보다 저렴하게 만들기 위해 노력해야합니다. 그러나 표준 C ++ 라이브러리는 풀을 사용하여 launch::async | launch::deferred.

그리고 OP는 정확합니다. ::std::thread물론 스레드를 시작하는 데 사용하면 풀에서 하나를 사용하는 대신 새 스레드가 생성됩니다. 그래서 ::std::async(::std::launch::async, ...)선호됩니다.

질문 2 :

예, 기본적으로 이것은 '암묵적으로'스레드를 시작합니다. 그러나 실제로 무슨 일이 일어나고 있는지는 여전히 분명합니다. 그래서 저는 그 단어가 암묵적으로 특별히 좋은 단어라고 생각하지 않습니다.

또한 파괴되기 전에 반환을 기다리도록 강요하는 것이 반드시 오류라고 확신하지 않습니다. async반환 할 것으로 예상되지 않는 '데몬'스레드를 만들기 위해 호출을 사용해야한다는 것을 모르겠습니다 . 그리고 그들이 돌아올 것으로 예상된다면 예외를 무시하는 것은 괜찮지 않습니다.

질문 3 :

개인적으로 스레드 시작이 명시 적으로 표시되는 것을 좋아합니다. 직렬 액세스를 보장 할 수있는 섬에 많은 가치를 부여합니다. 그렇지 않으면 항상 어딘가에 뮤텍스를 감싸고 그것을 사용하는 것을 기억해야하는 변경 가능한 상태가됩니다.

저는 작업 대기열 모델이 '미래'모델보다 훨씬 더 좋았습니다. 왜냐하면 '직렬 섬'이 주변에 놓여 있기 때문에 변경 가능한 상태를보다 효과적으로 처리 할 수 ​​있기 때문입니다.

하지만 실제로는 정확히 무엇을하는지에 따라 다릅니다.

성능 테스트

그래서 저는 다양한 호출 방법의 성능을 테스트하고 clang 버전 7.0.1 및 libc ++ (libstdc ++ 아님)로 컴파일 된 Fedora 29를 실행하는 8 코어 (AMD Ryzen 7 2700X) 시스템에서 이러한 숫자를 생각해 냈습니다.

   Do nothing calls per second:   35365257                                      
        Empty calls per second:   35210682                                      
   New thread calls per second:      62356                                      
 Async launch calls per second:      68869                                      
Worker thread calls per second:     970415                                      

Apple LLVM version 10.0.0 (clang-1000.10.44.4)OSX 10.13.6 미만의 MacBook Pro 15 형 (Intel (R) Core (TM) i7-7820HQ CPU @ 2.90GHz) 에서 기본적으로 다음과 같은 결과를 얻을 수 있습니다.

   Do nothing calls per second:   22078079
        Empty calls per second:   21847547
   New thread calls per second:      43326
 Async launch calls per second:      58684
Worker thread calls per second:    2053775

작업자 스레드의 경우 스레드를 시작한 다음 잠금없는 큐를 사용하여 다른 스레드에 요청을 보낸 다음 "완료"응답이 다시 전송 될 때까지 기다립니다.

"아무것도하지 않음"은 테스트 장치의 오버 헤드를 테스트하는 것입니다.

스레드를 시작하는 오버 헤드가 엄청나다는 것은 분명합니다. 스레드 간 대기열이있는 작업자 스레드조차도 VM의 Fedora 25에서는 20 배 정도, 기본 OS X에서는 8 배 정도 느려집니다.

성능 테스트에 사용한 코드가 들어있는 Bitbucket 프로젝트를 만들었습니다. https://bitbucket.org/omnifarious/launch_thread_performance 에서 찾을 수 있습니다.


3
작업 대기열 모델에 동의하지만 모든 동시 액세스 사용에 적용 할 수없는 "파이프 라인"모델이 필요합니다.
Matthieu M.

1
결과를 작성하는 데 표현식 템플릿 (연산자 용)을 사용할 수있는 것 같습니다. 함수 호출의 경우 호출 메서드 가 필요 하지만 오버로드로 인해 약간 더 어려울 수 있습니다.
Matthieu M.

3
"매우 싸다"는 것은 귀하의 경험과 관련이 있습니다. Linux 스레드 생성 오버 헤드가 내 사용에 상당 하다는 것을 알았습니다 .
제프

1
@Jeff-나는 그것이 그것보다 훨씬 저렴하다고 생각했습니다. 나는 실제 비용을 발견하기 위해 한 테스트를 반영하기 위해 얼마 전에 대답을 업데이트했습니다.
Omnifarious

4
첫 번째 부분에서는 위협을 생성하기 위해 수행해야하는 작업과 함수를 호출하기 위해 수행해야하는 작업이 얼마나 적은지를 다소 과소 평가하고 있습니다. 함수 호출 및 반환은 스택 상단에서 몇 바이트를 조작하는 몇 가지 CPU 명령어입니다. 위협 생성은 다음을 의미합니다. 1. 스택 할당, 2. 시스템 호출 수행, 3. 커널에서 데이터 구조 생성 및 연결, 도중에 잠금 파악, 4. 스케줄러가 스레드 실행을 기다림, 5. 전환 스레드에 대한 컨텍스트. 이러한 각 단계 자체는 가장 복잡한 함수 호출보다 훨씬 오래 걸립니다 .
cmaster-monica 복원
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.