C # Olaylar ve İş Parçacığı Güvenliği


237

GÜNCELLEME

C # 6'dan itibaren, bu sorunun cevabı :

SomeEvent?.Invoke(this, e);

Aşağıdaki tavsiyeleri sık sık duyuyorum / okuyorum:

Bir etkinliği kontrol etmeden nullve tetiklemeden önce her zaman bir kopyasını oluşturun . Bu, etkinliğin nullnull olup olmadığını kontrol ettiğiniz yerle olayı tetiklediğiniz yer arasındaki noktada gerçekleşme olasılığını ortadan kaldırır :

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Güncelleme : Optimizasyonları okuduğumda bunun da olay üyesinin değişken olmasını gerektirebileceğini düşündüm, ancak Jon Skeet cevabında CLR'nin kopyayı optimize etmediğini belirtti.

Ancak bu arada, bu sorunun ortaya çıkması için başka bir iş parçacığının böyle bir şey yapmış olması gerekir:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

Gerçek dizi bu karışım olabilir:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

OnTheEventYazarın aboneliği iptal edildikten sonra devam eden nokta , yine de bunun olmasını önlemek için özel olarak aboneliği iptal edilir. Elbette gerçekten ihtiyaç duyulan şey, uygun senkronizasyona sahip özel bir etkinlik uygulamasıdır.add ve removeerişimcilerde . Ayrıca, bir olay tetiklenirken bir kilit tutulursa olası kilitlenme sorunu vardır.

Peki bu Kargo Kült Programlama mı? Öyle görünüyor - bir çok insan kodlarını birden fazla iş parçacığından korumak için bu adımı atmalı, gerçekte olayların çok iş parçacıklı bir tasarımın parçası olarak kullanılmadan önce bundan daha fazla bakım gerektirdiğini düşünüyorum. . Sonuç olarak, bu ek bakımı almayan insanlar da bu tavsiyeyi görmezden gelebilir - bu sadece tek iş parçacıklı programlar için bir sorun değildir ve aslında,volatile çoğu çevrimiçi örnek kodun , tavsiyenin hiç etkisi.

(Ve delegate { }üye beyanında boş olanı atamak çok daha kolay değil mi?null ilk etapta mi?)

Güncellenmiş:Açık değilse, her koşulda null referans istisnasından kaçınmak için tavsiyenin niyetini kavradım. Demek istediğim, bu özel null referans istisnasının yalnızca olaydan başka bir iş parçacığı çıkarması durumunda ortaya çıkabilir ve bunu yapmanın tek nedeni, bu olay aracılığıyla başka teknik çağrıların alınmamasını sağlamaktır. . Bir yarış koşulunu gizleyecektiniz - ortaya çıkarmak daha iyi olurdu! Bu boş istisna, bileşeninizin kötüye kullanımını tespit etmeye yardımcı olur. Bileşeninizin kötüye kullanıma karşı korunmasını istiyorsanız, WPF örneğini takip edebilirsiniz - iş parçacığının yapıcısında depolanıp başka bir iş parçacığı doğrudan bileşeninizle etkileşime girmeye çalışırsa bir istisna atabilirsiniz. Ya da gerçekten güvenli bir bileşen uygulayın (kolay bir iş değil).

Bu yüzden sadece bu kopyalama / kontrol deyimini yapmanın, kodunuza karışıklık ve gürültü ekleyerek kargo kült programlama olduğunu iddia ediyorum. Aslında diğer ipliklere karşı korunmak çok daha fazla iş gerektirir.

Eric Lippert'in blog gönderilerine yanıt olarak güncelleme:

Bu yüzden olay işleyicileri hakkında kaçırdığım önemli bir şey var: "olay işleyicileri, abonelik iptal edildikten sonra bile çağrılması karşısında sağlam olmalı" ve bu nedenle yalnızca olayın olasılığını önemsememiz gerekiyor. delege olmak null. Olay işleyicileri için bu gereksinim herhangi bir yerde belgelenmiş mi?

Ve böylece: "Bu sorunu çözmenin başka yolları da var; örneğin, işleyicinin hiçbir zaman kaldırılmamış boş bir eylem için başlatılması. Ancak boş bir denetim yapmak standart modeldir."

Öyleyse sorumun kalan kısmı, "standart kalıp" için neden açık-boş-kontrol? Boş delege atayan alternatif, yalnızca = delegate {}olay bildirimine eklenmeyi gerektirir ve bu, olayın yapıldığı her yerden küçük kokmuş tören yığınlarını ortadan kaldırır. Boş delegenin somutlaştırılması ucuz olduğundan emin olmak kolay olacaktır. Yoksa hala bir şey mi kaçırıyorum?

Şüphesiz (Jon Skeet'in önerdiği gibi) bu, 2005'te yapması gerektiği gibi, ölmemiş olan sadece .NET 1.x tavsiyesi olmalı?


4
Bu soru bir süre önce iç tartışmalarda ortaya çıktı; Bunu bir süredir bloglamak niyetindeyim. Konu ile ilgili
yazım

3
Stephen Cleary'nin bu konuyu inceleyen bir CodeProject makalesi var ve genel bir amacı var, "iş parçacığı açısından güvenli" bir çözüm yok. Temsilcinin boş olmadığından emin olmak olay davet ediciye ve abonelik iptal edildikten sonra çağrılmak için olay işleyiciye bağlıdır.
rkagerer

3
@rkagerer - aslında ikinci konu iş parçacıkları dahil olmasa bile bazen olay işleyici tarafından ele alınmalıdır. Bir olay işleyicisi başka bir işleyiciye şu anda işlenmekte olan olaydan olan aboneliği iptal etmesini söylese de olabilir, ancak 2. abone olayı yine de alır (çünkü işlem devam ederken aboneliği iptal edilir).
Daniel Earwicker

3
Sıfır aboneli bir etkinliğe abonelik eklemek, etkinliğin tek aboneliğini kaldırmak, sıfır aboneli bir etkinliği çağırmak ve tam olarak bir abonesi olan bir etkinliği çağırmak, diğer sayıları içeren senaryoları eklemek / kaldırmak / çağırmaktan çok daha hızlı işlemlerdir. aboneler. Sahte bir temsilci eklemek genel durumu yavaşlatır. C # ile ilgili asıl sorun, yaratıcılarının EventName(arguments)etkinliğin temsilcisini koşulsuz olarak çağırmaya karar vermesidir, yalnızca temsilci yalnızca boş değilse çağırır (boş değilse hiçbir şey yapmaz).
supercat

Yanıtlar:


100

Durum nedeniyle, JIT'in ilk bölümde bahsettiğiniz optimizasyonu yapmasına izin verilmiyor. Bunun bir hayalet olarak ortaya çıktığını biliyorum, ama geçerli değil. (Bir süre önce Joe Duffy veya Vance Morrison ile kontrol ettim; hangisini hatırlayamıyorum.)

Uçucu değiştirici olmadan, alınan yerel kopyanın eski olması mümkündür, ancak hepsi bu. Bir neden olmaz NullReferenceException.

Ve evet, kesinlikle bir yarış durumu var - ama her zaman olacak. Kodu sadece şu şekilde değiştirdiğimizi varsayalım:

TheEvent(this, EventArgs.Empty);

Şimdi delege için çağrı listesinde 1000 giriş bulunduğunu varsayalım. Başka bir iş parçacığı listenin sonuna yakın bir işleyiciyi iptal etmeden önce, listenin başlangıcındaki eylemin gerçekleştirilmiş olması mümkündür. Ancak, bu işleyici yeni bir liste olacağından yürütülmeye devam edecektir. (Delegeler değişmez.) Görebildiğim kadarıyla bu kaçınılmaz.

Boş bir delege kullanmak kesinlikle geçersizlik kontrolünden kaçınır, ancak yarış koşulunu düzeltmez. Ayrıca, değişkenin her zaman en son değerini "göreceğinizi" garanti etmez.


4
Joe Duffy'nin "Windows'ta Eşzamanlı Programlama", sorunun JIT optimizasyonunu ve bellek modeli yönünü kapsar; bkz. code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
Ben C # 2 öncesi "standart" tavsiye hakkında yorum dayanarak bunu kabul ettim ve ben kimsenin bununla çelişen duymuyorum. Etkinlik argümanlarınızı somutlaştırmak gerçekten pahalı olmadığı sürece, etkinlik bildiriminizin sonuna '= delegate {}' ifadesini koyun ve etkinliklerinizi doğrudan yöntemmiş gibi çağırın; onlara asla null atama. (Bir işleyiciyi listelemeden sonra çağrılmadı, hepsi ilgisizdi ve tek iş parçacıklı kod için bile, örneğin işleyici 1, işleyiciye 2 listelemeyi isterse, işleyici 2 hala çağrılacak sonraki.)
Daniel Earwicker 5:09

2
Tek sorun durumu (her zaman olduğu gibi), üyelerinde boş değerlerden başka bir şeyle somutlaştırılacağından emin olamadığınız yapılardır. Ama yapılar berbat.
Daniel Earwicker

1
Boş temsilci hakkında ayrıca şu soruya bakın: stackoverflow.com/questions/170907/… .
Vladimir

2
@Tony: Abonelik / abonelikten çıkarılma ile delege yürütülen bir şey arasında hala temelde bir yarış koşulu var. Kodunuz (kısaca göz atmak), abonelik / abonelikten çıkma işleminin yürürlüğe girmesine izin vererek bu yarış durumunu azaltır, ancak çoğu durumda normal davranışın yeterince iyi olmadığından şüphelenirim.
Jon Skeet

52

Bunu yapmanın uzatma yöntemine giden birçok insan görüyorum ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Bu, etkinliği yükseltmek için daha iyi bir sözdizimi sağlar ...

MyEvent.Raise( this, new MyEventArgs() );

Ayrıca yöntem çağrısı zamanında yakalandığından yerel kopyayı ortadan kaldırır.


9
Sözdizimini seviyorum, ancak açık olalım ... kayıt olmadıktan sonra bile eski bir işleyicinin çağrılmasıyla sorunu çözmez. Bu sadece boş dereference problemini çözer. Ben sözdizimi gibi olsa da, gerçekten daha iyi olup olmadığını sorguluyorum: public event EventHandler <T> MyEvent = delete {}; ... MyEvent (bu, yeni MyEventArgs ()); Bu aynı zamanda sadeliği için sevdiğim çok düşük sürtünmeli bir çözümdür.
Simon Gillbee

@Simon Farklı insanların bu konuda farklı şeyler söylediğini görüyorum. Test ettim ve yaptığım şey bunun boş işleyici sorununu çözdüğünü gösteriyor. Orijinal Lavabo, işleyici! = Null denetiminden sonra Olay'dan kaydolansa bile, Olay yine de kaldırılır ve herhangi bir istisna atılmaz.
JP Alioto

evet

1
+1. Bu yöntemi kendim yazdım, iplik güvenliği hakkında düşünmeye başladım, biraz araştırma yaptım ve bu soru üzerine tökezledim.
Niels van der Rest

Bu VB.NET'ten nasıl çağrılabilir? Yoksa 'RaiseEvent' zaten çok iş parçacıklı senaryolara hitap ediyor mu?

35

"Neden 'açık' null-standart 'kalıbı kontrol ediyor?"

Bunun sebebinin sıfır kontrolünün daha performanslı olabileceğinden şüpheleniyorum.

Oluşturulduklarında etkinliklerinize her zaman boş bir temsilci abone olursanız, bazı ek yükler olacaktır:

  • Boş delege oluşturma maliyeti.
  • Onu içerecek bir delege zinciri oluşturma maliyeti.
  • Etkinlik her başlatıldığında anlamsız delege çağırmanın maliyeti.

(Kullanıcı Arabirimi denetimlerinin çoğu zaman hiç abone olmadıkları çok sayıda etkinliğe sahip olduğunu unutmayın. Her etkinliğe kukla bir abone oluşturmak ve daha sonra çağırmak önemli bir performans isabeti olacaktır.)

Abone-boş-delege yaklaşımının etkisini görmek için bazı üstünkörü performans testleri yaptım ve işte sonuçlarım:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

Sıfır veya bir abone durumunda (etkinliklerin bol olduğu UI denetimleri için ortaktır), boş bir delege ile önceden başlatılan etkinliğin oldukça yavaş olduğunu (50 milyondan fazla yineleme ...) unutmayın.

Daha fazla bilgi ve kaynak kodu için, bu sorunun sorulmasından bir gün önce yayınladığım .NET Olay çağırma iş parçacığı güvenliği hakkındaki bu blog gönderisini ziyaret edin (!)

(Test kurulumum kusurlu olabilir, bu nedenle kaynak kodunu indirip kendiniz inceleyebilirsiniz. Herhangi bir geri bildirim çok takdir edilmektedir.)


8
Blog yayınınızdaki önemli noktayı yaptığınızı düşünüyorum: bir darboğaz olana kadar performans sonuçları hakkında endişelenmenize gerek yok. Neden çirkin yol önerilen yol olsun? Açıklık yerine erken optimizasyon isteseydik, montajcı kullanırdık - bu yüzden sorum devam ediyor ve bence olası cevap, tavsiyenin anonim delegelerin önüne geçmesi ve insan kültürünün eski tavsiyeyi değiştirmesi uzun zaman alıyor. ünlü "pot rosto hikayesi".
Daniel Earwicker,

13
Ve rakamlarınız bu noktayı çok iyi kanıtlıyor: Genel gider, yükseltilen etkinlik başına sadece iki buçuk NANOSECONDS'a (!!!) düşüyor (başlangıç ​​öncesi ile klasik-null arasında). Gerçek işi olan neredeyse uygulamalarda bu tespit edilemez, ancak etkinlik kullanımının büyük çoğunluğunun GUI çerçevelerinde olduğu göz önüne alındığında, bunu Winforms'ta bir ekranın parçalarını yeniden boyama maliyetiyle karşılaştırmanız gerekir. gerçek CPU çalışmasının baskısında ve kaynakların beklemesinde daha da görünmez olur. Zaten zor iş için benden +1 alıyorsun. :)
Daniel Earwicker,

1
@DanielEarwicker doğru dedi, beni genel etkinliğe inanan biri oldunuz WrapperDoneHandler OnWrapperDone = (x, y) => {}; modeli.
Mickey Perlstein

1
Etkinliğin sıfır, bir veya iki aboneye sahip olduğu durumlarda a Delegate.Combine/ Delegate.Removeçift zamanlamak da iyi olabilir ; biri aynı temsilci örneğini tekrar tekrar ekleyip kaldırırsa, Combinebağımsız değişkenlerden biri null(yalnızca diğerini döndürmek) olduğunda hızlı özel durum davranışına sahip Removeolduğundan ve iki bağımsız değişken olduğunda çok hızlı olduğundan, durumlar arasındaki maliyet farklılıkları özellikle telaffuz edilecektir. eşittir (sadece boş döndür).
supercat

12

Bu okuma gerçekten keyif aldım - değil! Olaylar adı verilen C # özelliği ile çalışmak için ihtiyacım olmasına rağmen!

Bunu derleyicide neden düzeltmiyorsunuz? Bu yazıları okuyan MS kullanıcıları olduğunu biliyorum, bu yüzden lütfen bunu alevlendirmeyin!

1 - Null sorunu ) Neden ilk etapta null yerine boş. Boş kontrol için veya = delegate {}bildirime a yapıştırmak için kaç satır kod kaydedilebilir ? Derleyici Boş durumda işlemesine izin verin, IE hiçbir şey yapmayın! Her şey etkinliğin yaratıcısı için önemliyse, .Empty'yi kontrol edebilir ve ilgilendiklerini yapabilirler! Aksi takdirde tüm null çekler / delege ekler sorunun etrafında kesmek vardır!

Dürüst olmak gerekirse, her olayla bunu yapmaktan bıktım - aka boilerplate kodu!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - yarış koşulu sorunu ) Eric'in blog gönderisini okudum, H (işleyici) dereferences kendisini ele almalıdır, ancak olay değişmez / iş parçacığı güvenli hale getirilemez mi? IE, oluşturulmasında bir kilit bayrağı ayarlar, böylece her çağrıldığında, yürütme sırasında ona tüm aboneliği ve aboneliği engeller?

Sonuç ,

Günümüz dillerinin bizim için bu gibi sorunları çözmesi gerekmez mi?


Kabul ediyorum, derleyicide bunun için daha iyi destek olmalı. O zamana kadar, bunu bir derleme sonrası adımda yapan bir PostSharp yönü oluşturdum . :)
Steven Jeuris

4
İstenmeyen dış kodun tamamlanmasını beklerken ağır iş parçacığı aboneliği / abonelikten çıkma istekleri engellenir, abonelikler iptal edildikten sonra abonelerin olay almasını sağlamaktan çok daha kötüdür , özellikle de ikinci "sorun", olay işleyicilerinin bir bayrağı kontrol ederek kolayca çözülebildiğinden hala etkinliklerini almakla ilgileniyorlar, ancak eski tasarımdan kaynaklanan çıkmazlar inatçı olabilir.
supercat

@supercat. Imo, "çok daha kötü" yorum oldukça uygulamaya bağlıdır. Kim bir seçenek olduğunda ekstra bayraklar olmadan çok sıkı kilitleme istemez ki? Kilitlenme aynı iş parçacığı yeniden girildiğinden ve orijinal olay işleyicisindeki bir abone / abonelik engellenmeyeceğinden, yalnızca olay işleme iş parçacığı başka bir iş parçacığında (alt abonelik / abonelikten çıkma) beklerse kilitlenme meydana gelmelidir. Tasarımın bir parçası olacak bir olay işleyicisinin parçası olarak bekleyen çapraz iplik varsa, yeniden çalışmayı tercih ederim. Öngörülebilir desenlere sahip bir sunucu tarafı uygulama açısından geliyorum.
crokusek

2
@crokusek: Analiz için gerekli kanıtlamak bir sistem çıkmazdan serbesttir kilitler tümüne her kilidi bağlayan bir yönlendirilmiş grafiğinde hiçbir döngüleri olurdu Eğer kolay olduğunu belki o düzenlenen oldu iken ihtiyaç duyulacak [döngülerin eksikliği sistemini kanıtlayan kilitlenme içermeyen]. Bir kilit basılıyken rastgele kodun çağrılmasına izin vermek, bu kilitten, rastgele kodun alabileceği herhangi bir kilide (sistemdeki her kilit değil, çok uzak olmayan) bir kilit için "gerekli olabilir" grafiğinde bir kenar oluşturacaktır. ). Döngülerin varlığı, çıkmazın meydana geleceği anlamına gelmez, ama ...
supercat

1
... yapamayacağını kanıtlamak için gereken analiz düzeyini büyük ölçüde artıracaktır.
supercat

8

İle C'nin 6. üstünde ve kod yeni kullanılarak basitleştirilmiş olabilir ?.gibi operatörünü:

TheEvent?.Invoke(this, EventArgs.Empty);

İşte MSDN belgeleri.


5

C # üzerinden CLR kitabındaki Jeffrey Richter'e göre , doğru yöntem:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

Çünkü bir referans kopyasını zorlar. Daha fazla bilgi için kitaptaki Etkinlik bölümüne bakın.


Ben smth eksik olabilir, ama Interlocked.CompareExchange ilk argümanı null ise tam olarak kaçınmak istediğimiz şey NullReferenceException atar. msdn.microsoft.com/tr-tr/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangeBir şekilde bir null geçirilmesi halinde başarısız olur ref, ama bu geçirilen aynı şey değil refbir depolama konumuna (örneğin için NewMailvar ve başlangıçta olan) tutan bir null başvuru.
supercat

4

Olay işleyicilerinin abonelikleri kaldırıldıktan sonra yürütülmediğinden emin olmak için bu tasarım desenini kullanıyorum. Performans profillemeyi denemememe rağmen şu ana kadar oldukça iyi çalışıyor.

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

Bu günlerde çoğunlukla Android için Mono ile çalışıyorum ve Etkinliği arka plana gönderildikten sonra bir Görünümü güncellemeye çalıştığınızda Android bunu beğenmiyor gibi görünüyor.


Aslında, başka birinin burada çok benzer bir desen kullandığını görüyorum: stackoverflow.com/questions/3668953/…
Ash

2

Bu uygulama belirli bir operasyon sırasını uygulamakla ilgili değildir. Aslında null referans istisnasından kaçınmakla ilgilidir.

Yarış koşulunu değil, boş referans istisnasını önemseyen insanların arkasındaki mantık, bazı derin psikolojik araştırmalar gerektirecektir. Ben boş referans sorunu düzeltmek çok daha kolay olduğu gerçeği ile ilgili bir şey olduğunu düşünüyorum. Bu sorun çözüldükten sonra, kodlarına büyük bir "Görev Tamamlandı" afişi asarlar ve uçuş kıyafetlerini çıkartırlar.

Not: yarış koşulunun düzeltilmesi, muhtemelen işleyicinin çalışıp çalışmayacağı konusunda eşzamanlı bir bayrak izi kullanmayı içerir


Bu soruna bir çözüm istemiyorum. Neden sadece hala algılanması zor bir yarış durumu olduğunda boş bir istisnayı önlediğinde, olay tetikleme etrafında ekstra kod karışıklığı püskürtmek için yaygın tavsiye olduğunu merak ediyorum.
Daniel Earwicker

1
Demek istediğim bu. Onlar yok bakım yarış durumu hakkında. Yalnızca null referans istisnasını önemserler. Bunu cevabımda düzenleyeceğim.
dss539

Ve benim açımdan: neden null başvuru özel ilgili bakım mantıklıdır ve henüz değil yarış durumu umurumda?
Daniel Earwicker

4
Düzgün yazılmış bir olay işleyicisi, işlenmesi, olayı ekleme veya kaldırma talebiyle çakışabilecek herhangi bir özel talep talebinin, eklenen veya kaldırılan olayı gündeme getirebileceği veya vermeyebileceği gerçeğini ele almak için hazırlanmalıdır. Programcıların yarış koşulunu umursamamasının nedeni, düzgün yazılmış kodda kimin kazandığını fark etmemesidir .
supercat

2
@ dss539: Bekleyen olay çağrıları bitene kadar abonelikten çıkma taleplerini engelleyecek bir olay çerçevesi tasarlanabilirken, böyle bir tasarım, herhangi bir olayın (bir Unloadolay gibi bir şey ) bir nesnenin diğer olaylara aboneliklerini güvenli bir şekilde iptal etmesini imkansız hale getirecektir . Pis. Daha iyisi, olay aboneliği iptal isteklerinin olayların "sonunda" abonelikten çıkmasına neden olacağını ve olay abonelerinin ne zaman çağrıldıklarını kontrol etmeleri gerektiğini kontrol etmeleri gerektiğini söylemektir.
supercat

1

Bu yüzden partiye biraz geç kaldım. :)

Abonesi olmayan olayları temsil etmek için null nesne kalıbı yerine null nesnenin kullanımına gelince, bu senaryoyu dikkate alın. Bir etkinliği çağırmanız gerekir, ancak nesneyi (EventArgs) oluşturmak önemsizdir ve genel durumda etkinliğinizin abonesi yoktur. Argümanları oluşturmak ve olayı çağırmak için işleme çabası göstermeden önce, herhangi bir aboneniz olup olmadığını kontrol etmek için kodunuzu optimize edebilmeniz sizin için yararlı olacaktır.

Bunu göz önünde bulundurarak, bir çözüm "iyi, sıfır abone null ile temsil edilir" demektir. Sonra pahalı işleminizi yapmadan önce null kontrolünü yapın. Bunu yapmanın başka bir yolunun Delege türünde bir Count özelliğine sahip olacağını varsayalım, bu yüzden pahalı işlemi yalnızca myDelegate.Count> 0 ise gerçekleştirirsiniz. ve aynı zamanda bir NullReferenceException özelliğine neden olmadan çağrılabilme özelliğine de sahiptir.

Bununla birlikte, delegeler referans türleri olduğu için null olmalarına izin verildiğini unutmayın. Belki de bu gerçeği kapakların altında saklamanın ve yalnızca olaylar için null nesne modelini desteklemenin iyi bir yolu yoktu, bu yüzden alternatif geliştiricileri hem null hem de sıfır abone için kontrol etmeye zorlamış olabilir. Mevcut durumdan bile daha çirkin olurdu.

Not: Bu saf spekülasyon. .NET dilleri veya CLR ile ilgilenmiyorum.


"Delege yerine boş delege kullanımı" demek istediğinizi varsayalım. Boş delege için başlatılan bir etkinlikle önerilerinizi zaten yapabilirsiniz. İlk boş delege listedeki tek şeyse sınama (MyEvent.GetInvocationList (). Length == 1) doğrudur. Yine de önce bir kopya çıkarmaya gerek yoktur. Her ne kadar tarif ettiğiniz durumda zaten son derece nadir olacağını düşünüyorum.
Daniel Earwicker

Sanırım burada delegelerin ve olayların fikirlerini karıştırıyoruz. Sınıfımda bir olay Foo varsa, dış kullanıcılar MyType.Foo + = / - = çağırdığında, aslında add_Foo () ve remove_Foo () yöntemlerini çağırıyorlar. Ancak, tanımlandığı sınıf içinde Foo başvuru, aslında temelde temsilci doğrudan add_Foo () ve remove_Foo () yöntemleri başvurulan. Ve EventHandlerList gibi türlerin varlığıyla, delege ve etkinliğin aynı yerde olmasını zorunlu kılan hiçbir şey yoktur. Cevabımdaki "Akılda tut" paragrafı ile kastettiğim budur.
Levi

(devam) Bunun kafa karıştırıcı bir tasarım olduğunu kabul ediyorum, ancak alternatif daha kötü olabilir. Sonunda sahip olduğunuz tek şey bir temsilci olduğundan - temel delege doğrudan başvurabilirsiniz, bir koleksiyondan alabilirsiniz, anında başlatabilirsiniz - "denetlemek" dışında herhangi bir şeyi desteklemek teknik olarak mümkün olmayabilir null "deseni.
Levi

Etkinliği tetiklemekten bahsederken, erişim / ekleme erişimcilerinin burada neden önemli olduğunu göremiyorum.
Daniel Earwicker

@Levi: C # 'ın olayları işleme biçiminden gerçekten hoşlanmıyorum. Benim druthers'ım olsaydı, delegeye olaydan farklı bir isim verilecekti. Bir sınıfın dışından, olay adında izin verilen tek işlem +=ve olur -=. Sınıf içinde izin verilen işlemler arasında çağırma (yerleşik null denetimi ile), test etme nullveya ayarlamayı da içerir null. Başka herhangi bir şey için, adı belirli bir önek veya sonek ile olay adı olacak temsilci kullanmak gerekir.
supercat

0

tek iş parçacıklı aplikatörler için, bu bir sorun değildir düzeltir.

Ancak, olayları ortaya çıkaran bir bileşen oluşturuyorsanız, bileşeninizin bir tüketicisinin çok iş parçacığına gitmeyeceğine dair bir garanti yoktur, bu durumda en kötüsüne hazırlanmanız gerekir.

Boş temsilci kullanmak sorunu çözer, ancak etkinliğe yapılan her çağrıda performansa neden olur ve muhtemelen GC sonuçları olabilir.

Bunun gerçekleşmesi için tüketici aboneliğini iptal etmekte haklısınız, ancak geçici kopyayı geçtiyse, zaten iletilen iletiyi düşünün.

Geçici değişkeni kullanmazsanız ve boş temsilci kullanmazsanız ve biri abonelikten çıkarsa, ölümcül olan bir boş başvuru istisnası alırsınız, bu yüzden maliyetin buna değer olduğunu düşünüyorum.


0

Bunu gerçekten çok fazla sorun olarak görmedim, çünkü genellikle yalnızca yeniden kullanılabilir bileşenlerimdeki statik yöntemlerde (vb.) Bu tür potansiyel iş parçacığı kötülüğüne karşı koruma sağlıyorum ve statik olaylar yapmıyorum.

Yanlış mı yapıyorum?


Değişken durumu olan bir sınıf örneği (değerlerini değiştiren alanlar) tahsis ederseniz ve bu alanların aynı anda iki iş parçacığı tarafından değiştirilmesini önlemek için kilitleme kullanmadan birkaç iş parçacığının aynı örneğe aynı anda erişmesine izin verirseniz, muhtemelen yanlış yapıyor. Tüm evreleriniz kendi ayrı örneklerine sahipse (hiçbir şey paylaşmazsa) veya tüm nesneleriniz değiştirilemezse (ayrıldıktan sonra alanlarının değerleri asla değişmez), muhtemelen tamamsınız demektir.
Daniel Earwicker

Genel yaklaşımım, statik yöntemler dışında senkronizasyonu arayana kadar bırakmaktır. Arayan isem, o zaman daha yüksek seviyede senkronize edeceğim. (tek amacı elbette senkronize erişimi kullanmak olan bir nesne hariç. :))
Greg D

@GregD, yöntemin ne kadar karmaşık olduğuna ve hangi verileri kullandığına bağlıdır. dahili üyeleri etkiliyorsa ve dişli / Görevlendirilmiş bir durumda çalışmaya karar verirseniz, çok fazla acı
çekersiniz

0

Tüm etkinliklerinizi hazırlayın ve yalnız bırakın. Delegate sınıfının tasarımı, bu yayının son paragrafında açıklayacağım gibi, başka herhangi bir kullanımı doğru bir şekilde kullanamaz.

Her şeyden önce, olay işleyicilerinizin bildirime yanıt verip vermemeye / nasıl yanıt vereceğine dair senkronize bir karar vermesi gerektiğinde bir olay bildirimini durdurmaya çalışmanın bir anlamı yoktur .

Bildirilebilecek her şey bildirilmelidir. Olay işleyicileriniz bildirimleri düzgün bir şekilde işliyorsa (yani yetkili bir başvuru durumuna erişimleri varsa ve yalnızca uygun olduğunda yanıt veriyorlarsa), bunları istedikleri zaman bilgilendirmek ve doğru şekilde yanıt vereceğine güvenmek iyi olacaktır.

Bir işleyicinin bir olayın meydana geldiği konusunda bilgilendirilmemesi gereken tek şey, olayın gerçekleşmediği zamandır! Bu nedenle, bir işleyicinin bilgilendirilmesini istemiyorsanız, olayları oluşturmayı durdurun (yani, denetimi devre dışı bırakın veya olayı ilk başta tespit etmek ve var olandan sorumlu kılmak için).

Dürüst olmak gerekirse, Delege sınıfının kurtarılamaz olduğunu düşünüyorum. Bir MulticastDelegate ile birleşme / geçiş çok büyük bir hataydı, çünkü etkinliğin (yararlı) tanımını tek bir anda gerçekleşen bir şeyden bir zaman aralığında gerçekleşen bir şeye etkili bir şekilde değiştirdi. Böyle bir değişiklik, mantıksal olarak tek bir anı içine geri daraltabilen bir senkronizasyon mekanizması gerektirir, ancak MulticastDelegate böyle bir mekanizmadan yoksundur. Senkronizasyon, olayın gerçekleştiği tüm zaman aralığını veya anı kapsamalıdır, böylece bir uygulama bir olayı işlemeye başlamak için senkronize edilmiş bir karar verdiğinde, olayı tamamen (işlemsel) işlemeyi bitirir. MulticastDelegate / Delegate hibrit sınıfı olan kara kutuyla bu neredeyse imkansızdır, yanitek bir abone kullanmaya ve / veya işleyici zinciri kullanılırken / değiştirilirken çıkarılabilen bir senkronizasyon tutamağına sahip kendi MulticastDelegate türünü uygular . Bunu tavsiye ediyorum, çünkü alternatif, tüm işleyicilerinizde gereksizce / gereksiz derecede karmaşık olacak şekilde eşitleme / işlemsel bütünlüğü uygulamak olacaktır.


[1] "Tek bir anda" gerçekleşen kullanışlı bir olay işleyici yoktur. Tüm işlemlerin bir zaman aralığı vardır. Herhangi bir tek işleyici, gerçekleştirilmesi gereken önemsiz bir adım dizisine sahip olabilir. İşleyici listesini desteklemek hiçbir şeyi değiştirmez.
Daniel Earwicker

Bir olay başlatılırken kilit tutmak tamamen deliliktir. Kaçınılmaz olarak kilitlenmeye yol açar. Kaynak A kilidini, yangın olayını, lavabo B kilidini çıkarır, şimdi iki kilit tutulur. Başka bir iş parçacığında yapılan bazı işlemler kilitlerin ters sırada çıkarılmasına neden olursa ne olur? Kilitleme sorumluluğu ayrı ayrı tasarlanmış / test edilmiş bileşenler (tüm olay noktasıdır) arasında bölündüğünde bu tür ölümcül kombinasyonlar nasıl göz ardı edilebilir?
Daniel Earwicker

[3] Bu sorunların hiçbiri, özellikle GUI çerçevelerinde, tek iş parçacıklı bileşen bileşimindeki normal çok noktaya yayın delegelerinin / olaylarının yaygınlığını hiçbir şekilde azaltmaz. Bu kullanım durumu, olayların kullanımının büyük çoğunluğunu kapsamaktadır. Olayları serbest iş parçacıklı bir şekilde kullanmak şüpheli bir değerdir; bu hiçbir şekilde tasarımlarını veya anlamlı oldukları bağlamlarda bariz faydalarını geçersiz kılmaz.
Daniel Earwicker

[4] Konular + senkron olaylar aslında kırmızı bir ringa balığıdır. Sıraya alınmış asenkron iletişim yoludur.
Daniel Earwicker

[1] Ölçülen zamandan bahsetmiyordum ... Mantıksal olarak anında gerçekleşen atomik operasyonlardan bahsediyordum ... ve böylece olayları kullanırken kullandıkları kaynaklarla ilgili başka hiçbir şeyin değişemeyeceği anlamına geliyordum çünkü bir kilitle serileştirildi.
Triynko

0

Lütfen buraya bir göz atın: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Bu doğru çözümdür ve diğer tüm geçici çözümler yerine her zaman kullanılmalıdır.

“Dahili çağırma listesinin her zaman en az bir üyeye sahip olmasını sağlamak için, bunu yapmadan isimsiz bir yöntemle başlatabilirsiniz. Hiçbir dış taraf anonim yönteme başvuru yapamayacağından, dış taraf hiçbir yöntemi kaldıramaz, bu nedenle temsilci hiçbir zaman boş kalmayacaktır ”- Programlama .NET Bileşenleri, 2. Baskı, Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

0

Sorunun c # "olay" türüyle sınırlı olduğuna inanmıyorum. Bu kısıtlamayı kaldırarak, neden tekerleği biraz yeniden icat edip bu çizgiler boyunca bir şey yapmıyorsunuz?

Etkinlik dizisini güvenle kaldırın - en iyi yöntem

  • Yükseltme dahilindeyken herhangi bir iplikten alt / abonelikten çıkma yeteneği (yarış durumu kaldırıldı)
  • Sınıf düzeyinde + = ve - = için aşırı yük.
  • Genel arayan tanımlı temsilci

0

Yararlı bir tartışma için teşekkürler. Son zamanlarda bu sorun üzerinde çalışıyordu ve biraz daha yavaş, ama atılan nesnelere çağrıları önlemek için izin veren aşağıdaki sınıfı yaptı.

Burada asıl nokta, olay gündeme gelse bile çağrı listesinin değiştirilebileceğidir.

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

Ve kullanımı:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Ölçek

Aşağıdaki şekilde test ettim. Bu gibi nesneleri oluşturur ve yok eden bir iş parçacığı var:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

Bir Bar(dinleyici nesnesi) yapıcısında abone oldum SomeEvent(yukarıda gösterildiği gibi uygulanır) ve abonelikten çıkarım Dispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

Ayrıca bir döngüde olay yükselten iş parçacığı bir çift var.

Tüm bu eylemler aynı anda gerçekleştirilir: birçok dinleyici oluşturulur ve yok edilir ve olay aynı anda tetiklenir.

Bir yarış koşulu varsa, bir konsolda bir mesaj görmeliyim, ancak boş. Ancak clr olaylarını her zamanki gibi kullanırsam uyarı mesajlarıyla dolu görürüm. Yani, c # bir iş parçacığı güvenli olaylar uygulamak mümkün olduğu sonucuna varabiliriz.

Ne düşünüyorsun?


Bana yeterince iyi gözüküyor. Test başvurunuzda daha disposed = trueönce (teoride) mümkün olabileceğini düşünmeme rağmen foo.SomeEvent -= Handleryanlış pozitif üreterek. Ama bunun dışında değiştirmek isteyebileceğiniz birkaç şey var. Gerçekten try ... finallykilitler için kullanmak istiyorsunuz - bu sadece iplik güvenli değil, aynı zamanda iptal güvenli hale getirmenize yardımcı olacaktır. O zaman bu aptallıktan kurtulabileceğinden bahsetmiyorum bile try catch. Ve Add/ / iletilen delegeyi kontrol etmiyor Removeolabilirsiniz - olabilir null( Add/ içine hemen atmalısınız Remove).
Luaan
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.