C ++ 11'deki zaman uyumsuz (launch :: async), pahalı iş parçacığı oluşturmadan kaçınmak için iş parçacığı havuzlarını eski yapar mı?


117

Bu soru gevşek bir şekilde ilişkilidir: std :: thread C ++ 11'de havuzlanır mı? . Soru farklı olsa da niyet aynıdır:

Soru 1: Pahalı iş parçacığı oluşturmayı önlemek için kendi iş parçacığı havuzlarınızı (veya üçüncü taraf kitaplığı) kullanmak hala mantıklı mı?

Diğer sorudaki sonuç, std::threadhavuzda toplanmaya güvenemeyeceğinizdir (olabilir ya da olmayabilir). Ancak, std::async(launch::async)havuza alınma şansı çok daha yüksek görünüyor.

Standart tarafından zorlandığını düşünmüyor, ancak IMHO İş parçacığı oluşturma yavaşsa tüm iyi C ++ 11 uygulamalarının iş parçacığı havuzunu kullanmasını beklerdim. Yalnızca yeni bir iş parçacığı oluşturmanın ucuz olduğu platformlarda, her zaman yeni bir iş parçacığı oluşturmalarını beklerdim.

Soru 2: Ben de öyle düşünüyorum, ama bunu ispatlayacak hiçbir gerçeğim yok. Çok iyi yanılıyor olabilirim. Eğitimli bir tahmin mi?

Son olarak, burada ilk önce iş parçacığı oluşturmanın nasıl ifade edilebileceğini gösteren bazı örnek kodlar sağladım async(launch::async):

Örnek 1:

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

olur

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

Örnek 2: Ateşle ve unut ipliği

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

olur

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

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

Soru 3: asyncVersiyonları versiyonlara tercih eder miydiniz thread?


Gerisi artık sorunun bir parçası değil, yalnızca açıklama için:

Dönüş değeri neden bir kukla değişkene atanmalıdır?

Ne yazık ki, dönüş değerini yakaladığınız mevcut C ++ 11 standart kuvvetleri std::async, aksi takdirde yıkıcı çalıştırılır ve eylem sona erene kadar bloke olur. Bazıları tarafından standartta bir hata olarak kabul edilir (örneğin Herb Sutter tarafından).

Cppreference.com'dan alınan bu örnek bunu güzel bir şekilde göstermektedir:

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

Başka bir açıklama:

İş parçacığı havuzlarının başka meşru kullanımları olabileceğini biliyorum, ancak bu soruda yalnızca pahalı iş parçacığı oluşturma maliyetlerinden kaçınma yönüyle ilgileniyorum .

Özellikle kaynaklar üzerinde daha fazla kontrole ihtiyacınız varsa, iş parçacığı havuzlarının çok yararlı olduğu durumlar olduğunu düşünüyorum. Örneğin, bir sunucu, hızlı yanıt sürelerini garantilemek ve bellek kullanımının tahmin edilebilirliğini artırmak için aynı anda yalnızca sabit sayıda isteği işlemeye karar verebilir. İş parçacığı havuzları burada iyi olmalıdır.

İş parçacığı yerel değişkenler kendi iş parçacığı havuzlarınız için bir argüman olabilir, ancak pratikte uygun olup olmadığından emin değilim:

  • Başlangıç std::threaddurumuna getirilmiş iş parçacığı yerel değişkenleri olmadan yeni bir iş parçacığı oluşturma . Belki de istediğin bu değildir.
  • Tarafından ortaya çıkan dizilerde async, iş parçacığı yeniden kullanılmış olabileceği için benim için biraz belirsiz. Anladığım kadarıyla, iş parçacığı yerel değişkenlerinin sıfırlanması garanti edilmiyor, ancak yanılıyor olabilirim.
  • Öte yandan, kendi (sabit boyutlu) iş parçacığı havuzlarınızı kullanmak, gerçekten ihtiyacınız olduğunda size tam kontrol sağlar.

8
"Ancak, std::async(launch::async)havuzda toplanma şansı çok daha yüksek görünüyor." Hayır, std::async(launch::async | launch::deferred)bunun havuza alınabileceğine inanıyorum . Sadece launch::asyncgörevin, başka hangi görevlerin çalıştığına bakılmaksızın yeni bir iş parçacığında başlatılması gerekiyor. Politika ile launch::async | launch::deferreddaha sonra uygulama hangi politikayı seçer, ancak daha da önemlisi hangi politikayı seçmeyi geciktirir. Diğer bir deyişle, bir iş parçacığı havuzundaki bir iş parçacığı kullanılabilir hale gelene kadar bekleyebilir ve ardından zaman uyumsuz ilkeyi seçebilir.
bames53

2
Bildiğim kadarıyla sadece VC ++ std::async(),. Hala bir iş parçacığı havuzunda önemsiz olmayan evre_yerel yıkıcıları nasıl desteklediklerini merak ediyorum.
bames53

2
@ bames53 gcc 4.7.2 ile birlikte gelen libstdc ++ 'yı geçtim ve başlatma ilkesi tam launch::async olarak değilse, o zaman sadece öyleymiş gibi davrandığını launch::deferredve asla eşzamansız olarak çalıştırmadığını - yani aslında libstdc ++ sürümünün "seçer" olduğunu gördüm aksi zorlanmadıkça her zaman ertelenmiş kullanmak.
doug65536

3
@ doug65536 thread_local yıkıcılar hakkındaki düşüncem, iş parçacığı havuzlarını kullanırken iş parçacığı çıkışındaki yıkımın tam olarak doğru olmamasıydı. Bir görev eşzamansız olarak çalıştırıldığında, spesifikasyona göre 'yeni bir iş parçacığı üzerindeymiş gibi' çalıştırılır, bu da her zaman uyumsuz görevin kendi evre_yerel nesnelerini aldığı anlamına gelir. İş parçacığı havuzu tabanlı bir uygulama, aynı destek iş parçacığını paylaşan görevlerin hala kendi evre_yerel nesneleri varmış gibi davranmasını sağlamak için özel dikkat göstermelidir. Bu programı düşünün: pastebin.com/9nWUT40h
bames53

2
@ bames53 Spesifikasyonda "sanki yeni bir iş parçacığı üzerindeymiş gibi" kullanmak bence büyük bir hataydı. std::asyncperformans için güzel bir şey olabilirdi - doğal olarak bir iş parçacığı havuzuyla desteklenen standart kısa süreli görev yürütme sistemi olabilirdi. Şu anda, std::threadiş parçacığı işlevinin bir değer döndürmesini sağlamak için bazı saçmalıklar üzerinde çalışılıyor. Oh, ve işi std::functiontamamen örtüşen gereksiz "ertelenmiş" işlevsellik eklediler .
doug65536

Yanıtlar:


55

Soru 1 :

Bunu orijinalinden değiştirdim çünkü orijinal yanlıştı. Linux iş parçacığı oluşturmanın çok ucuz olduğu izlenimine kapılmıştım ve test ettikten sonra, normal bir iş parçacığına karşı yeni bir iş parçacığındaki işlev çağrısının ek yükünün çok büyük olduğunu belirledim. Bir işlev çağrısını işlemek için bir evre oluşturmanın ek yükü, düz bir işlev çağrısından 10000 veya daha fazla kat daha yavaş bir şeydir. Bu nedenle, çok sayıda küçük işlev çağrısı yapıyorsanız, iş parçacığı havuzu iyi bir fikir olabilir.

G ++ ile birlikte gelen standart C ++ kitaplığının iş parçacığı havuzlarına sahip olmadığı oldukça açıktır. Ama kesinlikle onlar için bir durum görebiliyorum. Çağrıyı bir tür iş parçacığı arası kuyruğa itmek zorunda kalmanın ek yüküyle bile, yeni bir iş parçacığı başlatmaktan muhtemelen daha ucuz olacaktır. Ve standart buna izin veriyor.

IMHO, Linux çekirdeği insanları iş parçacığı oluşturmayı şu anda olduğundan daha ucuza yapmaya çalışmalı. Ancak, standart C ++ kitaplığı uygulamak için havuzu kullanmayı da düşünmelidir launch::async | launch::deferred.

Ve OP doğru, ::std::threadbir iş parçacığı başlatmak için kullanmak, havuzdan bir iş parçacığı kullanmak yerine yeni bir iş parçacığı oluşturulmasını zorlar. Bu yüzden ::std::async(::std::launch::async, ...)tercih edilir.

Soru 2 :

Evet, temelde bu 'örtük olarak' bir iş parçacığı başlatır. Ama gerçekten, neler olduğu hala oldukça açık. Bu yüzden örtük olarak kelimenin özellikle iyi bir kelime olduğunu gerçekten düşünmüyorum.

Ayrıca sizi imha etmeden önce dönüşü beklemeye zorlamanın mutlaka bir hata olduğuna ikna olmadım. asyncGeri dönmesi beklenmeyen 'daemon' evrelerini oluşturmak için çağrıyı kullanmanız gerektiğini bilmiyorum . Ve geri dönmeleri bekleniyorsa, istisnaları görmezden gelmek doğru değildir.

Soru 3 :

Şahsen, iş parçacığı lansmanlarının açık olmasını seviyorum. Seri erişimi garanti edebileceğiniz adalara çok değer veriyorum. Aksi takdirde, her zaman bir yerde bir muteks sarmalamanız ve onu kullanmayı hatırlamanız gereken değişken bir duruma sahip olursunuz.

İş kuyruğu modelini 'gelecek' modelinden çok daha fazla sevdim çünkü ortalıkta yatan 'seri adalar' var, böylece değişken durumu daha etkili bir şekilde idare edebilirsiniz.

Ama gerçekten, tam olarak ne yaptığınıza bağlı.

Performans testi

Bu yüzden, çeşitli arama yöntemlerinin performansını test ettim ve bu numaraları clang sürüm 7.0.1 ve libc ++ (libstdc ++ değil) ile derlenen Fedora 29 çalıştıran 8 çekirdekli (AMD Ryzen 7 2700X) bir sistemde buldum:

   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                                      

Ve yerel, Apple LLVM version 10.0.0 (clang-1000.10.44.4)OSX 10.13.6 altındaki MacBook Pro 15 "(Intel (R) Core (TM) i7-7820HQ CPU @ 2.90GHz) cihazımda şunu anlıyorum:

   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

Çalışan iş parçacığı için, bir iş parçacığı başlattım, sonra başka bir iş parçacığına istek göndermek için kilitsiz bir kuyruk kullandım ve ardından "Tamamlandı" yanıtının geri gönderilmesini bekledim.

"Hiçbir şey yapma", yalnızca test koşum takımının başının üstünü test etmek içindir.

Bir iş parçacığı başlatmanın ek yükünün çok büyük olduğu açıktır. Ve iş parçacığı arası kuyruğa sahip çalışan iş parçacığı bile işleri bir VM'de Fedora 25'te 20 kat ve yerel OS X'te yaklaşık 8 kat yavaşlatır.

Performans testi için kullandığım kodu içeren bir Bitbucket projesi oluşturdum. Burada bulunabilir: https://bitbucket.org/omnifarious/launch_thread_performance


3
İş kuyruğu modeline katılıyorum, ancak bu, eşzamanlı erişimin her kullanımı için geçerli olmayabilecek bir "boru hattı" modeline sahip olmayı gerektirir.
Matthieu M.

1
Bana, sonuçları oluşturmak için ifade şablonları (operatörler için) kullanılabilir gibi görünüyor, işlev çağrıları için bir çağrı yöntemine ihtiyacınız olacağını tahmin ediyorum, ancak aşırı yüklenmeler nedeniyle biraz daha zor olabilir.
Matthieu M.

3
"çok ucuz", deneyiminize göre değişir. Linux iş parçacığı oluşturma ek yükünün kullanımım için önemli olduğunu düşünüyorum .
Jeff

1
@Jeff - Olduğundan çok daha ucuz olduğunu düşündüm. Cevabımı bir süre önce gerçek maliyeti keşfetmek için yaptığım bir testi yansıtacak şekilde güncelledim.
Omnifarious

4
İlk bölümde, bir tehdit oluşturmak için ne kadar yapılması gerektiğini ve bir işlevi çağırmak için ne kadar az şey yapılması gerektiğini biraz hafife alıyorsunuz. Bir işlev çağrısı ve dönüşü, yığının tepesinde birkaç baytı işleyen birkaç CPU talimattır. Bir tehdit oluşturma şu anlama gelir: 1. bir yığın tahsis etmek, 2. bir sistem çağrısı gerçekleştirmek, 3. çekirdekte veri yapıları oluşturmak ve bunları bağlamak, yol boyunca kilitleri yakalamak, 4. Planlayıcının iş parçacığını yürütmesini beklemek, 5. anahtarlama iş parçacığına bağlam. Bu adımların her biri kendi içinde en karmaşık işlev çağrılarından çok daha uzun sürer .
cmaster
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.