Bir kilit ifadesine ne kadar iş koymalıyım?


27

Üçüncü taraf bir çözümden veri alan, bir veritabanında depolayan ve ardından verileri başka bir üçüncü taraf çözüm tarafından kullanılmak üzere şartlandıran bir yazılım güncellemesi yazmaya çalışan küçük bir geliştiriciyim. Yazılımımız bir Windows servisi olarak çalışmaktadır.

Önceki sürümdeki koda bakarak, şunu görüyorum:

        static Object _workerLocker = new object();
        static int _runningWorkers = 0;
        int MaxSimultaneousThreads = 5;

        foreach(int SomeObject in ListOfObjects)
        {
            lock (_workerLocker)
            {
                while (_runningWorkers >= MaxSimultaneousThreads)
                {
                    Monitor.Wait(_workerLocker);
                }
            }

            // check to see if the service has been stopped. If yes, then exit
            if (this.IsRunning() == false)
            {
                break;
            }

            lock (_workerLocker)
            {
                _runningWorkers++;
            }

            ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);

        }

Mantık açık görünüyor: İş parçacığı havuzunda odayı bekleyin, hizmetin durmadığından emin olun, sonra iş parçacığı sayacını artırın ve işi sıralayın. sonra çağıran bir cümlenin içinde _runningWorkersazalır .SomeMethod()lockMonitor.Pulse(_workerLocker)

Sorum şu: Tek içindeki tüm kod gruplama içinde bir yararı var mı lockbunun gibi,:

        static Object _workerLocker = new object();
        static int _runningWorkers = 0;
        int MaxSimultaneousThreads = 5;

        foreach (int SomeObject in ListOfObjects)
        {
            // Is doing all the work inside a single lock better?
            lock (_workerLocker)
            {
                // wait for room in ThreadPool
                while (_runningWorkers >= MaxSimultaneousThreads) 
                {
                    Monitor.Wait(_workerLocker);
                }
                // check to see if the service has been stopped.
                if (this.IsRunning())
                {
                    ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
                    _runningWorkers++;                  
                }
                else
                {
                    break;
                }
            }
        }

Öyle görünüyor ki, diğer iş parçacıkları için biraz daha fazla beklemeye neden olabilir, ancak daha sonra tek bir mantıksal blokta tekrar tekrar kilitlenmek gibi görünmesi de biraz zaman alabilir. Ancak, çok iş parçacıklı konusunda yeniyim, bu yüzden burada bilmediğim başka endişeler olduğunu varsayıyorum.

_workerLockerKilitli kalan diğer yerler SomeMethod()sadece ve yalnızca azaltma amacıyladır _runningWorkersve daha sonra dışarıda loglama ve geri dönmeden önce foreachsayının _runningWorkerssıfıra gitmesini beklemek .

Herhangi bir yardım için teşekkürler.

EDIT 4/8/15

Bir semafor kullanmanız için @delnan'a teşekkür ederiz. Kod olur:

        static int MaxSimultaneousThreads = 5;
        static Semaphore WorkerSem = new Semaphore(MaxSimultaneousThreads, MaxSimultaneousThreads);

        foreach (int SomeObject in ListOfObjects)
        {
            // wait for an available thread
            WorkerSem.WaitOne();

            // check if the service has stopped
            if (this.IsRunning())
            {
                ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
            }
            else
            {
                break;
            }
        }

WorkerSem.Release()içine denir SomeMethod().


1
Tüm blok kilitliyse, SomeMethod, _runningWorkers'ı azaltmak için kilidi nasıl elde eder?
Russell, ISC'de

@RussellatISC: ThreadPool.QueueUserWorkItem SomeMethodzaman uyumsuz olarak çağırır , yukarıdaki "kilit" bölümü yeni iş parçacığı çalışmaya SomeMethodbaşladıktan önce veya en azından kısa bir süre sonra bırakılacaktır .
Doktor Brown

İyi bir nokta. Anladığım kadarıyla, amacı Monitor.Wait(), kilidi başka bir kaynağın ( SomeMethodbu durumda) kullanabilmesi için kilidi açmak ve yeniden elde etmek. Diğer taraftan SomeMethodkilidi alır, sayacı azaltır ve sonra Monitor.Pulse()kilidi söz konusu yönteme geri döndüren çağrılar . Yine, bu benim kendi anlayışım.
Joseph,

@Doc, bunu kaçırdı, ama yine de ... Görünüşe göre, bazı yöntemlerin bir sonraki yinelemede kilitli kalmadan önce başlaması gerekecekti ya da "while (_runningWorkers> = MaxSimurrentThreads)" ifadesiyle tutulan kilide asılacaktı gibi görünüyor.
Russell, ISC'de

@RussellatISC: Joseph'in daha önce de söylediği gibi: Monitor.Waitkilidi serbest bırakır. Dokümanlara bir göz atmanızı öneririm.
Doktor Brown

Yanıtlar:


33

Bu bir performans meselesi değil. Her şeyden önce bir doğruluk sorunudur. İki kilit ifadeniz varsa, bunlar arasında ya da kısmen kilit ifadesinin dışında yayılan işlemler için atomiteyi garanti edemezsiniz. Kodunuzun eski sürümü için uyarlanmış olan, şu anlama gelir:

Sonuna Arasında while (_runningWorkers >= MaxSimultaneousThreads)ve _runningWorkers++, hiç birşey yaşanabilir kod teslim olmalarının ve aradaki kilit yeniden edinme nedeniyle,. Örneğin, iş parçacığı A kilidi ilk kez alabilir, başka bir iş parçacığı çıkana kadar bekleyin ve sonra döngüden ve lock. Daha sonra önceden tutulur ve B dişi resme girer ve ayrıca diş havuzunda yer bekler. Çünkü diğer iş parçacığı istifa etti, yer var , o yüzden hiç beklemiyor. Hem A dişi hem de B dişi şimdi sırayla devam eder, her biri artan _runningWorkersşekilde çalışmaya başlar.

Şimdi, görebildiğim kadarıyla veri yarışları yok, ama şimdi çalışan işçilerden daha fazlası olduğu için mantıksal olarak yanlış MaxSimultaneousThreads. Çek (bazen) etkisizdir, çünkü iş parçacığı havuzunda bir yuva alma görevi atomik değildir. Bu, kilit ayrıntı dereceleri etrafındaki küçük optimizasyonlardan daha fazla sizi ilgilendirmez! (Tersine, çok erken veya çok uzun süreyle kilitlemenin kilitlenmelere kolayca yol açabileceğini unutmayın.)

İkinci pasaj, gördüğüm kadarıyla bu sorunu düzeltir. Sorunu gidermek için daha az istilacı bir değişiklik ++_runningWorkers, whilegörünümden hemen sonra , ilk kilit ifadesinin içine koymak olabilir .

Şimdi, doğruluk bir yana, peki ya performans? Bunu anlatması zor. Genel olarak daha uzun bir süre için kilitleme ("kaba") eşzamanlılığı engeller, ancak dediğiniz gibi, bunun ince taneli kilitlemenin ek senkronizasyonundan ek yüke karşı dengelenmesi gerekir. Genel olarak tek çözüm, kıyaslama yapmak ve "her yerde her şeyi kilitlemek" ve "sadece en düşük düzeyde kilitlemek" den daha fazla seçenek olduğunun farkında olmak. Mevcut çok sayıda desen ve eşzamanlılık ilkelleri ve iş parçacığı güvenli veri yapıları vardır. Örneğin, bu uygulama semaforlarının icat edildiği gibi görünüyor, bu yüzden bu elle haddelenmiş elle kilitlenen sayaç yerine bunlardan birini kullanmayı düşünün.


11

IMHO yanlış soruyu soruyorsunuz - verimlilik değişimleri hakkında çok fazla önem vermemelisiniz, daha çok doğruluk hakkında.

İlk değişken _runningWorkerssadece bir kilitleme sırasında erişildiğinden emin olur, ancak _runningWorkersilk kilit ile ikinci arasındaki boşluktaki başka bir diş tarafından değiştirilebilecek durumu özlüyor . Dürüst olmak gerekirse, kod, birileri _runningWorkersengeller ve olası hatalar hakkında düşünmeden tüm erişim noktalarının etrafına kilitlenip kilitlenmediğini gösteriyor . Belki de yazar bloğun breakiçinde ifadeyi yürütmekle ilgili bazı batıl korkular vardır lock, ama kim bilir?

Bu yüzden aslında ikinci değişkeni kullanmalısınız, çünkü az ya da çok verimli değil, (umarım) ilkinden daha doğru.


Kapak tarafında, başka bir kilit almayı gerektirebilecek bir görevi üstlenirken kilidi tutmak, "doğru" davranış olarak zor denilen bir kilitlenmeye neden olabilir. Kişi bir birim olarak yapılması gereken tüm kodların ortak bir kilitle çevrili olmasını sağlamalıdır, ancak biri o birimde yer alması gerekmeyen şeylerin, özellikle de diğer kilitlerin alınması gerekebilecek nesnelerin dışına taşınmalıdır. .
supercat,

@supercat: buradaki durum değil, lütfen orijinal sorunun altındaki yorumları okuyun.
Doktor Brown

9

Diğer cevaplar oldukça iyidir ve doğruluk endişelerini açıkça ele almaktadır. Daha genel sorunuza cevap vereyim:

Bir kilit ifadesine ne kadar iş koymalıyım?

Kabul ettiğiniz cevabın son paragrafında itiraz ettiğiniz ve kabul ettiğiniz standart tavsiyeyle başlayalım:

  • Belirli bir nesneyi kilitlerken mümkün olduğu kadar az iş yapın. Uzun süre tutulan kilitler çekişmeye maruz kalır ve çekişme yavaştır. Bu ima geldiğini hatırlatırız toplam bir kod miktarı belli kilit ve toplam kod miktarını aynı nesne üzerinde kilit tüm kilit ifadeleri alakalı olduğu.

  • Kilitlenme olasılığını (veya livelocks) daha düşük yapmak için mümkün olduğu kadar az kilit kullanın.

Akıllı okuyucu, bunların zıt olduğunu not edecektir. İlk nokta, çekişmeleri önlemek için büyük kilitlerin daha küçük, daha ince taneli kilitlere bölünmesini önerir. İkincisi, kilitlenmeleri önlemek için ayrı kilitlerin aynı kilit nesnesine birleştirilmesini önerir.

En iyi standart tavsiyenin tamamen çelişkili olduğu gerçeğinden ne çıkartabiliriz? Aslında iyi bir tavsiye aldık:

  • Oraya ilk etapta gitme. Eğer iş parçacığı arasında hafıza paylaşıyorsanız, kendinizi acı dolu bir dünyaya açıyorsunuzdur.

Benim tavsiyem, eşzamanlılık istiyorsanız, süreçleri eşzamanlılık birimi olarak kullanmaktır. İşlemleri kullanamıyorsanız, uygulama etki alanlarını kullanın. Uygulama etki alanlarını kullanamıyorsanız, iş parçacıklarınızın Görev Paralel Kitaplığı tarafından yönetilmesini sağlayın ve kodunuzu düşük düzey iş parçacıkları yerine üst düzey görevler (işler) açısından yazın.

Kesinlikle pozitif olarak, konu veya semafor gibi düşük seviyeli eşzamanlılık ilkellerini kullanmanız gerekiyorsa , bunları gerçekten ihtiyacınız olanı yakalayan daha üst düzey bir soyutlama oluşturmak için kullanın . Yüksek seviyeli soyutlamanın "kullanıcı tarafından iptal edilebilecek asenkronize bir görevi yerine getirme" gibi bir şey olduğunu muhtemelen göreceksiniz ve hey, TPL zaten bunu destekliyor, bu yüzden kendi işinizi yapmanız gerekmiyor. İplik güvenli tembel başlatma gibi bir şeye ihtiyacınız olduğunu muhtemelen göreceksiniz; Lazy<T>uzmanlar tarafından yazılmış, kendi kullanımınızı kullanmayın . Uzmanlar tarafından yazılan, konuya uygun olmayan koleksiyonları kullanın (değiştirilemez veya başka türlü). Soyutlama seviyesini mümkün olduğu kadar yukarı kaldırın.

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.