Güncelleme ve ayrı iş parçacıkları oluşturma


12

Basit bir 2D oyun motoru oluşturuyorum ve nasıl yapıldığını öğrenmek için spriteları farklı iş parçacıklarında güncellemek ve oluşturmak istiyorum.

Güncelleme iş parçacığı ve render bir senkronize etmek gerekiyor. Şu anda iki atom bayrağı kullanıyorum. İş akışı şuna benzer:

Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done

Bu kurulumda, render iş parçacığının FPS'sini güncelleme iş parçacığının FPS'si ile sınırlandırıyorum. Ayrıca, işleme sleep()ve güncelleme iş parçacığının FPS'sini 60 ile sınırlamak için kullanıyorum , böylece iki bekleme işlevi çok fazla beklemeyecek.

Problem şu:

Ortalama CPU kullanımı yaklaşık% 0.1'dir. Bazen% 25'e kadar çıkar (dört çekirdekli bir bilgisayarda). Bu, bir iş parçacığının diğerini beklediği anlamına gelir, çünkü wait işlevi bir test ve set işlevine sahip bir while döngüsüdür ve while döngüsü tüm CPU kaynaklarınızı kullanır.

İlk sorum şu: İki iş parçacığını senkronize etmenin başka bir yolu var mı? std::mutex::lockBir kaynak kilitlemek için beklerken CPU'yu kullanmadığımı fark ettim, bu yüzden bir süre döngüsü değil. O nasıl çalışır? Kullanamıyorum std::mutexçünkü bir iş parçacığında onları kilitlemek ve başka bir iş parçacığında kilidini açmak gerekir.

Diğer soru; program her zaman 60 FPS'de çalıştığı için neden bazen CPU kullanımı% 25'e atlıyor, yani iki beklemeden biri çok şey bekliyor? (her iki iş parçacığı da 60 fps ile sınırlıdır, bu nedenle ideal olarak çok fazla senkronizasyona ihtiyaç duymazlar).

Edit: Tüm cevaplar için teşekkürler. Öncelikle her çerçeve için yeni bir iş parçacığı oluşturmadığımı söylemek istiyorum. Başlangıçta hem güncelleme hem de oluşturma döngüsünü başlatıyorum. Multithreading biraz zaman kazandırabilir düşünüyorum: Aşağıdaki işlevleri var: FastAlg () ve Alg (). Alg () benim Güncelleme obj ve render obj ve Fastalg () "renderer" için "render sırası". Tek bir iş parçacığında:

Alg() //update 
FastAgl() 
Alg() //render

İki iş parçacığında:

Alg() //update  while Alg() //render last frame
FastAlg() 

Yani belki çoklu iş parçacığı aynı zamandan tasarruf edebilir. (aslında, alg'in uzun bir algoritma olduğu ve daha hızlı bir algoritma olduğu basit bir matematik uygulamasında)

Asla sorun yaşamamama rağmen uykunun iyi bir fikir olmadığını biliyorum. Bu daha iyi olacak mı?

While(true) 
{
   If(timer.gettimefromlastcall() >= 1/fps)
   Do_update()
}

Ancak bu, tüm CPU'yu kullanacak sonsuz bir döngü olacaktır. Kullanımı sınırlamak için uyku (<15) sayısı kullanabilir miyim? Bu şekilde, örneğin 100 fps hızında çalışır ve güncelleme işlevi saniyede sadece 60 kez çağrılır.

İki iş parçacığı senkronize etmek için waitforsingleobject ile createSemaphore kullanacağım, böylece farklı iş parçacığında (while döngüsü kullanarak whitout) kilitleyip kilidini açabiliyorum, değil mi?


5
"Çok iş parçacığımın bu durumda işe yaramaz olduğunu söyleme, sadece nasıl yapılacağını öğrenmek istiyorum" - bu durumda bir şeyler düzgün öğrenmelisiniz, yani (a) çerçeveyi nadiren kontrol etmek için uyku () kullanma , hiç bir zaman ve (b) bileşen başına iş parçacığı tasarımından kaçının ve kilit adımını çalıştırmaktan kaçının, bunun yerine görevleri görevlerde ayırın ve görevleri iş kuyruğundan yönetin.
Damon

1
@Damon (a) sleep () bir kare hızı mekanizması olarak kullanılabilir ve aslında oldukça popüler, ancak çok daha iyi seçeneklerin olduğu konusunda hemfikirim. (b) Buradaki kullanıcı, hem güncellemeyi hem de oluşturmayı iki farklı iş parçacığında ayırmak istiyor. Bu, bir oyun motorundaki normal bir ayrımdır ve "bileşen başına iş parçacığı" değildir. Net avantajlar sağlar, ancak yanlış yapılırsa sorunlara yol açabilir.
Alexandre Desbiens

@AlphSpirit: Bir şeyin "ortak" olması, bunun yanlış olmadığı anlamına gelmez . Farklı zamanlayıcılara bile girmeden, en az bir popüler masaüstü işletim sisteminde sadece uykunun ayrıntı düzeyi, mevcut her tüketici sistemindeki tasarım başına güvenilmez olmasa bile yeterlidir . Güncelleştirmenin ve oluşturmanın açıklandığı gibi iki iş parçacığına ayrılmasının neden yanlış olduğunu ve değerinden daha fazla soruna neden olduğunu açıklamak çok uzun sürecektir. OP'nin amacı, nasıl yapıldığını öğrenmek, doğru şekilde nasıl yapıldığını öğrenmek olmalıdır . Modern MT motor tasarımı hakkında birçok makale.
Damon

@Damon Popüler veya yaygın olduğunu söylediğimde, bunun doğru olduğunu söylemek istemedim. Sadece birçok insan tarafından kullanıldığını kastediyorum. “... ancak çok daha iyi seçeneklerin olduğu konusunda hemfikir olmalıyım. Yanlış anlaşılma için üzgünüm.
Alexandre Desbiens

@AlphSpirit: Endişeye gerek yok :-) Dünya birçok insanın yaptığı şeylerle doludur (ve her zaman iyi bir nedenden ötürü değil), ancak kişi öğrenmeye başladığında, hala en açık şekilde yanlış olanlardan kaçınmaya çalışmalıdır.
Damon

Yanıtlar:


25

Spritelı basit bir 2D motor için, tek iş parçacıklı bir yaklaşım mükemmel bir şekilde iyidir. Ancak, çoklu kullanımın nasıl yapılacağını öğrenmek istediğiniz için, bunu doğru bir şekilde yapmayı öğrenmelisiniz.

Yapma

  • Birkaç adımda tek iş parçacıklı bir davranış uygulayarak az ya da çok kilit adımı çalıştıran 2 iş parçacığı kullanın. Bu aynı paralellik düzeyine (sıfır) sahiptir, ancak bağlam anahtarları ve senkronizasyon için ek yük getirir. Ayrıca, mantığa bakılması daha zordur.
  • sleepKare hızını kontrol etmek için kullanın . Asla. Birisi size söylerse, vurun.
    İlk olarak, tüm monitörler 60Hz'de çalışmaz. İkincisi, aynı hızda yan yana çalışan iki zamanlayıcı her zaman sonunda senkronizasyondan çıkar (iki pingpong topunu aynı yükseklikte bir masaya bırakın ve dinleyin). Üçüncüsü, sleepbir tasarım gereği doğru ne de güvenilir de. Ayrıntı düzeyi 15.6ms kadar kötü olabilir (aslında, Windows [1] ' de varsayılan değerdir ) ve bir çerçeve 60fps'de sadece 16.6ms'dir, bu da her şey için sadece 1ms bırakır. Ayrıca, 16,6'yı 15,6'nın katı olmak zordur ...
    Ayrıca, sleepsadece 30 veya 50 veya 100 ms sonra veya daha da uzun bir süre sonra geri dönmesine izin verilir (ve bazen de!).
  • std::mutexBaşka bir konuyu bilgilendirmek için kullanın . Bunun için bu değil.
  • TaskManager'ın size neler olduğunu anlatmakta iyi olduğunu varsayalım, özellikle de kodunuzda, usermode sürücüsünde veya başka bir yerde harcanabilecek "% 25 CPU" gibi bir sayıdan yola çıkarak.
  • Yüksek seviyeli bileşen başına bir iplik var (elbette bazı istisnalar vardır).
  • Görev başına "rastgele zamanlarda" iş parçacıkları oluşturun. Konu oluşturma şaşırtıcı derecede pahalı olabilir ve onlara söylediklerinizi acutally yapmadan önce şaşırtıcı derecede uzun zaman alabilir (özellikle çok sayıda DLL yüklüyse!).

Yapmak

  • İşlerin olabildiğince eşzamansız olarak çalışması için çoklu iş parçacığını kullanın . Hız, diş çekme ana fikri değildir, ancak işleri paralel olarak yapmaktır (bu nedenle, daha uzun sürseler bile, hepsinin toplamı hala daha azdır).
  • Kare hızını sınırlamak için dikey senkronizasyonu kullanın. Bunu yapmanın tek doğru (ve başarısız olmayan) yolu budur. Kullanıcı ekran sürücüsünün kontrol panelinde sizi geçersiz kılarsa ("zorla"), öyle olsun. Sonuçta bu onun bilgisayarı, senin değil.
  • Bir şeyi düzenli aralıklarla "işaretlemeniz" gerekiyorsa, bir zamanlayıcı kullanın . Zamanlayıcıların avantajı sleep[2] ile karşılaştırıldığında çok daha iyi doğruluk ve güvenilirliğe sahiptir . Ayrıca, yinelenen bir zamanlayıcı zamanı doğru olarak hesaplar (aradan geçen zaman dahil), ancak 16.6ms (veya 16.6ms eksi ölçülen_saat_kalma) için uyku uymaz.
  • Sabit bir zaman adımında sayısal entegrasyon içeren fizik simülasyonları çalıştırın (veya denklemleriniz patlayacak!), Adımlar arasındaki grafikleri enterpolasyonlayın (bu , bileşen başına ayrı bir iş parçacığı için bir bahane olabilir , ancak onsuz da yapılabilir).
  • Kullanım std::mutexseferinde ( "karşılıklı dışlama") yalnızca bir iplik erişimi kaynak olması ve garip semantik uymak std::condition_variable.
  • Kaynaklar için iş parçacıklarının rekabet etmesinden kaçının. Gerektiği kadar kilitleyin (ama daha az değil!) Ve kilitleri sadece kesinlikle gerektiği kadar tutun.
  • Salt okunur verileri iş parçacıkları arasında paylaşın (önbellek sorunu yok ve kilitleme gerekmez), ancak verileri aynı anda değiştirmeyin (senkronizasyon gerekir ve önbelleği öldürür). Bu, başka birinin okuyabileceği bir konuma yakın olan verileri değiştirmeyi içerir .
  • std::condition_variableBir koşul geçerli olana kadar başka bir iş parçacığını engellemek için kullanın . Bu std::condition_variableekstra muteksin semantikleri kuşkusuz oldukça garip ve bükülmüş (çoğunlukla POSIX iş parçacıklarından miras kalan tarihi nedenlerle), ancak bir koşul değişkeni istediğiniz şey için kullanılacak doğru ilkeldir. Onunla rahat
    olmak için std::condition_variableçok garip bulursanız , bunun yerine bir Windows olayını da (biraz daha yavaş) kullanabilirsiniz veya cesursanız, NtKeyedEvents etrafında kendi basit etkinliğinizi oluşturabilirsiniz (korkunç düşük düzey şeyler içerir). DirectX'i kullandıkça zaten Windows'a zaten bağlısınız, bu nedenle taşınabilirlik kaybı biggie olmamalıdır.
  • İşi, sabit boyutlu bir işçi iş parçacığı havuzu (çekirdek başına birden fazla değil, hiper iş parçacıkları saymaz) tarafından yürütülen makul boyuttaki görevlere bölün. Son işlemlerin bağımlı görevleri sıralamasına izin verin (ücretsiz, otomatik senkronizasyon). Her biri en az birkaç yüz önemsiz işlem (veya bir disk okuması gibi bir uzun engelleme işlemi) olan görevleri gerçekleştirin. Önbellek bitişik erişimi tercih edin.
  • Tüm başlıkları program başlangıcında oluşturun.
  • İşletim sisteminin veya grafik API'sinin yalnızca program düzeyinde değil, donanımda da daha iyi / ek paralellik için sunduğu eşzamansız işlevlerden yararlanın (PCIe aktarımlarını, CPU-GPU paralelliğini, disk DMA vb.).
  • Söylemeyi unuttuğum 10.000 başka şey.


[1] Evet, zamanlayıcının hızını 1 ms'ye düşürebilirsiniz, ancak çok daha fazla bağlam anahtarına neden olduğu ve çok daha fazla güç tüketdiği için (daha fazla cihazın mobil cihaz olduğu bir dünyada) kaşlarını çattı. Aynı zamanda bir çözüm değil, çünkü hala uykuyu daha güvenilir hale getirmiyor.
[2] Bir zamanlayıcı, iş parçacığının önceliğini artıracak ve bu da eşit öncelikli başka bir iş parçacığının orta kuantumunu kesmesine ve ilk olarak bir RT-davranışı olan zamanlanmasına izin verecektir. Tabii ki gerçek RT değil, ama çok yakın geliyor. Uyku modundan çıkmak, sadece ipliğin bir zamanda, ne zaman olursa olsun programlanmaya hazır hale gelmesi anlamına gelir.


Neden "Üst düzey bileşen başına bir iş parçacığına sahip olmamanız" gerektiğini açıklayabilir misiniz? Yani iki ayrı iş parçacığında fizik ve ses karıştırmaya gerek yok mu? Bunu yapmamak için hiçbir neden göremiyorum.
Elviss Strazdins

3

Güncellemenin FPS'sini ve Render'ı 60 ile sınırlayarak ne elde etmek istediğinizden emin değilim. Bunları aynı değerle sınırlarsanız, bunları aynı iş parçacığına koyabilirsiniz.

Farklı iş parçacıklarında Güncelleme ve İşleme'yi ayırmanın amacı, GPU'nun 500 FPS oluşturabilmesi ve Güncelleme mantığının hala 60 FPS'ye geçebilmesi için her ikisinin de "neredeyse" birbirinden bağımsız olmasını sağlamaktır. Bunu yaparak çok yüksek bir performans kazancı elde edemezsiniz.

Ama sen sadece nasıl çalıştığını bilmek istediğini söyledin ve sorun değil. C ++ 'da bir muteks, diğer iş parçacıklarının belirli kaynaklarına erişimi kilitlemek için kullanılan özel bir nesnedir. Başka bir deyişle, mantıklı verilere o anda yalnızca bir iş parçacığı tarafından erişilebilir olmasını sağlamak için bir muteks kullanırsınız. Bunu yapmak için oldukça basit:

std::mutex mutex;
mutex.lock();
// Do sensible stuff here...
mutex.unlock();

Kaynak: http://en.cppreference.com/w/cpp/thread/mutex

DÜZENLEME : Muteksinizin verilen bağlantıda olduğu gibi sınıf veya dosya çapında olduğundan emin olun, yoksa her iş parçacığı kendi muteksini oluşturur ve hiçbir şey elde edemezsiniz.

Muteksi kilitleyen ilk iş parçacığı içindeki koda erişebilir. İkinci bir evre lock () işlevini çağırmaya çalışırsa, ilk evre kilidini açana kadar engeller. Yani mutex, bir while döngüsünün aksine bir engelleme fonksiyonudur. Engelleme işlevleri CPU'ya baskı yapmaz.


Ve blok nasıl çalışır?
Liuka

İkinci iş parçacığı lock () öğesini çağırdığında, ilk iş parçacığının muteksin kilidini açmasını sabırla bekleyecek ve sonraki satırda devam edecektir (bu örnekte, mantıklı şeyler). DÜZENLE: İkinci iş parçacığı muteksi kendisi için kilitler.
Alexandre Desbiens


1
Kullanın std::lock_guardveya benzememektedir .lock()/ .unlock(). RAII sadece bellek yönetimi için değil!
bcrist
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.