C # Acara dan Keamanan Utas

237

MEMPERBARUI

Mulai dari C # 6, jawaban untuk pertanyaan ini adalah:

SomeEvent?.Invoke(this, e);

Saya sering mendengar / membaca saran berikut:

Selalu buat salinan acara sebelum Anda memeriksanya nulldan memecatnya. Ini akan menghilangkan potensi masalah dengan threading di mana acara tersebut terjadinull berada di lokasi yang tepat di antara tempat Anda memeriksa nol dan tempat Anda memecat acara:

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

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

Diperbarui : Saya berpikir dari membaca tentang optimasi bahwa ini mungkin juga mengharuskan anggota acara menjadi tidak stabil, tetapi Jon Skeet menyatakan dalam jawabannya bahwa CLR tidak mengoptimalkan salinan.

Tetapi sementara itu, agar masalah ini terjadi, utas lainnya pasti telah melakukan sesuatu seperti ini:

// 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...

Urutan sebenarnya mungkin campuran ini:

// 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

Intinya adalah itu OnTheEvent berjalan setelah penulis telah berhenti berlangganan, namun mereka hanya berhenti berlangganan khusus untuk menghindari hal itu terjadi. Tentunya yang benar-benar dibutuhkan adalah implementasi acara kustom dengan sinkronisasi yang sesuai di adddan removeaccessor. Dan di samping itu ada masalah kemungkinan kebuntuan jika kunci ditahan saat suatu peristiwa dipecat.

Begitu juga Pemrograman Kargo Kargo ini ? Sepertinya begitu - banyak orang harus mengambil langkah ini untuk melindungi kode mereka dari banyak utas, padahal dalam kenyataannya menurut saya peristiwa memerlukan jauh lebih banyak perhatian daripada ini sebelum mereka dapat digunakan sebagai bagian dari desain multi-utas . Akibatnya, orang-orang yang tidak mengambil perawatan tambahan mungkin juga mengabaikan saran ini - itu bukan masalah untuk program single-threaded, dan pada kenyataannya, mengingat tidak adanya volatilesebagian besar kode contoh online, saran tersebut mungkin tidak memiliki efek sama sekali.

(Dan bukankah jauh lebih mudah untuk hanya menetapkan yang kosong delegate { }pada deklarasi anggota sehingga Anda tidak perlu memeriksa nullsejak awal?)

Diperbarui:Jika tidak jelas, saya memahami maksud dari saran - untuk menghindari pengecualian referensi nol dalam semua keadaan. Maksud saya adalah pengecualian referensi nol khusus ini hanya dapat terjadi jika utas lain menghapus daftar acara, dan satu-satunya alasan untuk melakukan itu adalah untuk memastikan bahwa tidak ada panggilan lebih lanjut akan diterima melalui peristiwa itu, yang jelas TIDAK BISA dicapai dengan teknik ini . Anda akan menyembunyikan kondisi balapan - akan lebih baik untuk mengungkapkannya! Pengecualian nol itu membantu mendeteksi penyalahgunaan komponen Anda. Jika Anda ingin komponen Anda dilindungi dari penyalahgunaan, Anda dapat mengikuti contoh WPF - simpan ID utas di konstruktor Anda dan kemudian berikan pengecualian jika utas lain mencoba berinteraksi langsung dengan komponen Anda. Atau menerapkan komponen yang benar-benar aman (bukan tugas yang mudah).

Jadi saya berpendapat bahwa hanya melakukan ini copy / cek idiom adalah pemrograman kargo, menambahkan kekacauan dan kebisingan ke kode Anda. Untuk benar-benar melindungi dari utas lainnya membutuhkan lebih banyak pekerjaan.

Pembaruan dalam menanggapi posting blog Eric Lippert:

Jadi ada hal utama yang saya lewatkan tentang penangan acara: "penangan acara harus kuat dalam menghadapi dipanggil bahkan setelah acara telah berhenti berlangganan", dan jelas karena itu kita hanya perlu peduli tentang kemungkinan acara mendelegasikan keberadaan null. Apakah persyaratan pada penangan acara didokumentasikan di mana saja?

Maka: "Ada cara lain untuk menyelesaikan masalah ini; misalnya, menginisialisasi pawang untuk memiliki tindakan kosong yang tidak pernah dihapus. Tetapi melakukan pemeriksaan nol adalah pola standar."

Jadi satu fragmen yang tersisa dari pertanyaan saya adalah, mengapa eksplisit-nol-periksa "pola standar"? Alternatif, menugaskan delegasi kosong, hanya = delegate {}perlu ditambahkan ke deklarasi acara, dan ini menghilangkan tumpukan-tumpukan kecil upacara bau dari setiap tempat di mana acara dibesarkan. Akan mudah untuk memastikan bahwa delegasi yang kosong murah untuk instantiate. Atau apakah saya masih melewatkan sesuatu?

Pasti itu (seperti yang disarankan Jon Skeet) ini hanya .NET 1.x saran yang belum padam, seperti yang seharusnya dilakukan pada tahun 2005?

Daniel Earwicker
sumber
4
Pertanyaan ini muncul dalam diskusi internal beberapa waktu lalu; Saya sudah berniat untuk blog ini selama beberapa waktu sekarang. Posting saya pada subjek ada di sini: Acara dan Ras
Eric Lippert
3
Stephen Cleary memiliki artikel CodeProject yang meneliti masalah ini, dan dia menyimpulkan tujuan umum, solusi "thread-safe" tidak ada. Pada dasarnya terserah pada event invoker untuk memastikan delegasi tidak null, dan hingga event handler untuk dapat menangani dipanggil setelah dibatalkan berlangganan.
rkagerer
3
@rkagerer - sebenarnya masalah kedua terkadang harus ditangani oleh event handler bahkan jika utas tidak terlibat. Dapat terjadi jika salah satu penangan acara memberi tahu penangan lain untuk berhenti berlangganan dari acara yang sedang ditangani, tetapi pelanggan kedua tersebut akan tetap menerima acara tersebut (karena tidak berlangganan saat penanganan sedang berlangsung).
Daniel Earwicker
3
Menambahkan langganan ke acara dengan nol pelanggan, menghapus satu-satunya langganan untuk acara tersebut, memanggil acara dengan nol pelanggan, dan memanggil acara dengan tepat satu pelanggan, semuanya operasi yang jauh lebih cepat daripada menambah / menghapus / memanggil skenario yang melibatkan jumlah lainnya dari pelanggan. Menambahkan delegasi tiruan memperlambat kasus umum. Masalah sebenarnya dengan C # adalah bahwa pembuatnya memutuskan untuk EventName(arguments)memanggil delegasi acara tanpa syarat, daripada meminta delegasi acara hanya jika tidak nol (tidak melakukan apa pun jika nol).
supercat

Jawaban:

100

JIT tidak diperbolehkan untuk melakukan optimasi yang Anda bicarakan di bagian pertama, karena kondisi tersebut. Saya tahu ini dibesarkan sebagai momok beberapa waktu lalu, tetapi itu tidak valid. (Aku memeriksanya dengan Joe Duffy atau Vance Morrison beberapa waktu yang lalu; Aku tidak ingat yang mana.)

Tanpa pengubah volatil itu mungkin bahwa salinan lokal yang diambil akan ketinggalan zaman, tapi itu saja. Itu tidak akan menyebabkan aNullReferenceException .

Dan ya, tentu saja ada kondisi balapan - tetapi akan selalu ada. Misalkan kita hanya mengubah kode menjadi:

TheEvent(this, EventArgs.Empty);

Sekarang anggaplah bahwa daftar doa untuk delegasi itu memiliki 1000 entri. Sangat mungkin bahwa tindakan di awal daftar akan dieksekusi sebelum utas lain berhenti berlangganan handler di dekat akhir daftar. Namun, pawang itu masih akan dieksekusi karena itu akan menjadi daftar baru. (Delegasi tidak bisa berubah.) Sejauh yang saya bisa lihat, ini tidak bisa dihindari.

Menggunakan delegasi kosong tentu saja menghindari cek nullity, tetapi tidak memperbaiki kondisi balapan. Ini juga tidak menjamin bahwa Anda selalu "melihat" nilai terbaru dari variabel.

Jon Skeet
sumber
4
Joe Duffy "Concurrent Programming on Windows" membahas aspek optimasi JIT dan model memori dari pertanyaan; lihat code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger
2
Saya telah menerima ini berdasarkan komentar tentang saran "standar" sebagai pra-C # 2, dan saya tidak mendengar ada orang yang membantahnya. Kecuali jika benar-benar mahal untuk instantiate argumen acara Anda, cukup cantumkan '= delegasikan {}' di akhir deklarasi acara Anda dan kemudian panggil acara Anda secara langsung seolah-olah itu adalah metode; tidak pernah menetapkan nol untuk mereka. (Hal-hal lain yang saya bawa tentang memastikan handler tidak dipanggil setelah dihapuskan, itu semua tidak relevan, dan tidak mungkin untuk memastikan, bahkan untuk kode berulir tunggal, misalnya jika penangan 1 meminta penangan 2 untuk dihapus, penangan 2 masih akan dipanggil selanjutnya.)
Daniel Earwicker
2
Satu-satunya kasus masalah (seperti biasa) adalah struct, di mana Anda tidak dapat memastikan bahwa mereka akan dipakai dengan apa pun selain nilai nol di anggota mereka. Tapi struct mengisap.
Daniel Earwicker
1
Tentang delegasi kosong, lihat juga pertanyaan ini: stackoverflow.com/questions/170907/… .
Vladimir
2
@Tony: Masih ada kondisi ras yang mendasar antara sesuatu berlangganan / berhenti berlangganan dan delegasi dieksekusi. Kode Anda (baru saja menjelajahinya secara singkat) mengurangi kondisi balapan dengan mengizinkan langganan / berhenti berlangganan berlaku saat sedang dinaikkan, tetapi saya menduga bahwa dalam kebanyakan kasus di mana perilaku normal tidak cukup baik, ini juga tidak.
Jon Skeet
52

Saya melihat banyak orang pergi ke arah metode perpanjangan melakukan ini ...

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);   
  }   
}

Itu memberi Anda sintaksis yang lebih baik untuk meningkatkan acara ...

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

Dan juga menghilangkan salinan lokal karena ditangkap pada waktu panggilan metode.

JP Alioto
sumber
9
Saya suka sintaksnya, tetapi mari kita perjelas ... ini tidak menyelesaikan masalah dengan basi yang dipanggil bahkan setelah tidak terdaftar. Ini hanya memecahkan masalah null dereference. Meskipun saya menyukai sintaksisnya, saya mempertanyakan apakah ini benar-benar lebih baik daripada: acara publik EventHandler <T> MyEvent = delete {}; ... MyEvent (ini, MyEventArgs baru ()); Ini juga merupakan solusi gesekan sangat rendah yang saya sukai karena kesederhanaannya.
Simon Gillbee
@Simon Saya melihat orang yang berbeda mengatakan hal yang berbeda tentang ini. Saya telah mengujinya dan apa yang saya lakukan menunjukkan kepada saya bahwa ini menangani masalah penangan nol. Bahkan jika Sink yang asli tidak terdaftar dari Event setelah handler! = Null check, Event tetap dinaikkan dan tidak terkecuali dilemparkan.
JP Alioto
yup, cf pertanyaan ini: stackoverflow.com/questions/192980/…
Benjol
1
+1. Saya sendiri yang menulis metode ini, mulai berpikir tentang keamanan utas, melakukan riset dan menemukan pertanyaan ini.
Niels van der Rest
Bagaimana ini bisa disebut dari VB.NET? Atau apakah 'RaiseEvent' sudah memenuhi skenario multi-threading?
35

"Mengapa secara eksplisit-nol-periksa 'pola standar'?"

Saya menduga alasan untuk ini mungkin karena cek-nol lebih banyak performan.

Jika Anda selalu berlangganan delegasi kosong ke acara Anda saat dibuat, akan ada beberapa biaya tambahan:

  • Biaya membangun delegasi kosong.
  • Biaya membangun rantai delegasi untuk menampungnya.
  • Biaya memohon delegasi tak berguna setiap kali acara dinaikkan.

(Perhatikan bahwa kontrol UI sering memiliki sejumlah besar acara, yang sebagian besar tidak pernah dilanggan. Harus membuat pelanggan dummy untuk setiap peristiwa dan kemudian memohonnya kemungkinan akan menjadi hit kinerja yang signifikan.)

Saya melakukan beberapa pengujian kinerja sepintas untuk melihat dampak dari pendekatan delegasi-kosong-langganan, dan berikut ini adalah hasil saya:

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

Perhatikan bahwa untuk kasus nol atau satu pelanggan (umum untuk kontrol UI, di mana banyak peristiwa), acara yang diinisialisasi dengan delegasi kosong terutama lebih lambat (lebih dari 50 juta iterasi ...)

Untuk informasi lebih lanjut dan kode sumber, kunjungi posting blog ini di . Utas utas acara NET yang saya publikasikan sehari sebelum pertanyaan ini ditanyakan (!)

(Pengaturan pengujian saya mungkin salah, jadi silakan mengunduh kode sumber dan memeriksanya sendiri. Umpan balik sangat dihargai.)

Daniel Fortunov
sumber
8
Saya pikir Anda membuat titik kunci dalam posting blog Anda: tidak perlu khawatir tentang implikasi kinerja sampai itu menjadi hambatan. Mengapa membiarkan jalan yang jelek menjadi cara yang disarankan? Jika kami menginginkan pengoptimalan prematur alih-alih kejelasan, kami akan menggunakan assembler - jadi pertanyaan saya tetap, dan saya pikir jawabannya adalah bahwa saran tersebut lebih dulu dari delegasi anonim dan butuh waktu lama bagi budaya manusia untuk mengubah saran lama, seperti dalam "kisah panggang pot" yang terkenal.
Daniel Earwicker
13
Dan angka-angka Anda membuktikan hal itu dengan sangat baik: biaya overhead turun menjadi hanya dua setengah NANOSECONDS (!!!) per peristiwa yang diangkat (pra-init vs klasik-nol). Ini tidak dapat terdeteksi di hampir semua aplikasi dengan pekerjaan nyata yang harus dilakukan, tetapi mengingat bahwa sebagian besar penggunaan acara ada dalam kerangka kerja GUI, Anda harus membandingkannya dengan biaya mengecat bagian layar di Winforms, dll., Jadi itu menjadi lebih tidak terlihat dalam banjir pekerjaan CPU nyata dan menunggu sumber daya. Anda mendapatkan +1 dari saya untuk kerja kerasnya. :)
Daniel Earwicker
1
@DanielEarwicker mengatakannya dengan benar, Anda telah menggerakkan saya untuk menjadi orang percaya dalam acara publik WrapperDoneHandler OnWrapperDone = (x, y) => {}; model.
Mickey Perlstein
1
Ini juga mungkin baik untuk waktu Delegate.Combine/ Delegate.Removepasangan dalam kasus di mana acara tersebut memiliki nol, satu, atau dua pelanggan; jika seseorang berulang kali menambah dan menghapus instance delegasi yang sama, perbedaan biaya di antara kasus-kasus akan secara khusus diucapkan karena Combinememiliki perilaku kasus khusus cepat ketika salah satu argumen null(hanya mengembalikan yang lain), dan Removesangat cepat ketika kedua argumen tersebut sama dengan (hanya mengembalikan nol).
supercat
12

Saya benar-benar menikmati bacaan ini - bukan! Meskipun saya membutuhkannya untuk bekerja dengan fitur yang disebut fitur C #!

Mengapa tidak memperbaiki ini di kompiler? Saya tahu ada MS orang yang membaca posting ini, jadi tolong jangan nyalakan ini!

1 - masalah Null ) Mengapa tidak membuat acara menjadi .Empty bukannya null di tempat pertama? Berapa banyak baris kode yang akan disimpan untuk pemeriksaan nol atau harus tetap = delegate {}pada deklarasi? Biarkan kompilator menangani kasus Kosong, yaitu IE tidak melakukan apa pun! Jika semuanya penting bagi pembuat acara, mereka dapat memeriksa .Empty dan melakukan apa pun yang mereka pedulikan! Kalau tidak, semua null check / delegate add adalah hacks di sekitar masalahnya!

Jujur saya lelah harus melakukan ini dengan setiap peristiwa - alias kode boilerplate!

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 - masalah kondisi balapan ) Saya membaca posting blog Eric, saya setuju bahwa H (handler) harus menangani ketika dereferensi itu sendiri, tetapi tidak bisakah acara tersebut dibuat abadi / aman? IE, mengatur bendera kunci pada pembuatannya, sehingga setiap kali dipanggil, ia mengunci semua berlangganan dan berhenti berlangganan saat menjalankannya?

Kesimpulan ,

Bukankah bahasa modern seharusnya memecahkan masalah seperti ini untuk kita?

Chuck Savage
sumber
Setuju, harus ada dukungan yang lebih baik untuk ini di kompiler. Sampai saat itu, saya membuat aspek PostSharp yang melakukan ini dalam langkah pasca-kompilasi . :)
Steven Jeuris
4
Permintaan langganan / pembatalan berlangganan thread yang diblokir saat menunggu kode luar yang arbitrer selesai jauh lebih buruk daripada meminta pelanggan menerima acara setelah langganan dibatalkan, terutama karena "masalah" yang terakhir dapat diselesaikan dengan mudah hanya dengan meminta penangan acara memeriksa bendera untuk melihat apakah mereka masih tertarik untuk menerima acara mereka, tetapi kebuntuan yang dihasilkan dari desain sebelumnya mungkin tidak bisa diatasi.
supercat
@ supercat. Imo, komentar "jauh lebih buruk" sangat tergantung pada aplikasi. Siapa yang tidak ingin penguncian yang sangat ketat tanpa bendera tambahan saat ini merupakan opsi? Kebuntuan hanya akan terjadi jika acara yang menangani utas sedang menunggu utas lain (yaitu berlangganan / berhenti berlangganan) karena kunci adalah entri-thread yang sama masuk kembali dan berlangganan / berhenti berlangganan dalam event handler asli tidak akan diblokir. Jika ada cross thread yang menunggu sebagai bagian dari event handler yang akan menjadi bagian dari desain saya lebih suka mengerjakan ulang. Saya datang dari sudut aplikasi sisi server yang memiliki pola yang dapat diprediksi.
crokusek
2
@crokusek: Analisis yang diperlukan untuk membuktikan bahwa sistem bebas dari jalan buntu adalah mudah jika tidak akan ada siklus dalam grafik terarah yang menghubungkan setiap kunci ke semua kunci yang mungkin diperlukan saat dipegang [kurangnya siklus membuktikan sistem jalan buntu]. Mengizinkan kode arbitrer dipanggil saat kunci dipegang akan membuat tepi pada grafik "mungkin dibutuhkan" dari kunci itu ke kunci apa pun yang kode arbitrer mungkin peroleh (tidak cukup setiap kunci dalam sistem, tetapi tidak jauh dari itu) ). Adanya konsekuensi dari siklus tidak akan menyiratkan bahwa kebuntuan akan terjadi, tapi ...
supercat
1
... akan sangat meningkatkan tingkat analisis yang diperlukan untuk membuktikan bahwa itu tidak bisa.
supercat
8

Dengan C # 6 ke atas, kode dapat disederhanakan menggunakan ?.operator baru seperti pada:

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

Berikut ini dokumentasi MSDN.

Phil1970
sumber
5

Menurut Jeffrey Richter dalam buku CLR via C # , metode yang benar adalah:

// 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);

Karena itu memaksa salinan referensi. Untuk informasi lebih lanjut, lihat bagian Acara di buku.

alyx
sumber
Mungkin saya kehilangan sesuatu, tetapi Interlocked.CompareExchange melempar NullReferenceException jika argumen pertamanya adalah null yang persis apa yang ingin kita hindari. msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke
1
Interlocked.CompareExchangeakan gagal jika entah bagaimana dilewatkan nol ref, tetapi itu tidak sama dengan diteruskan refke lokasi penyimpanan (misalnya NewMail) yang ada dan yang awalnya memegang referensi nol.
supercat
4

Saya telah menggunakan pola desain ini untuk memastikan bahwa event handler tidak dieksekusi setelah mereka berhenti berlangganan. Sejauh ini sudah berfungsi dengan baik, meskipun saya belum mencoba profil kinerja apa pun.

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);
  }
}

Saya sebagian besar bekerja dengan Mono untuk Android hari ini, dan Android sepertinya tidak menyukainya ketika Anda mencoba memperbarui Tampilan setelah Aktivitasnya dikirim ke latar belakang.

Abu
sumber
Sebenarnya, saya melihat orang lain menggunakan pola yang sangat mirip di sini: stackoverflow.com/questions/3668953/…
Ash
2

Praktik ini bukan tentang menegakkan urutan operasi tertentu. Ini sebenarnya tentang menghindari pengecualian referensi nol.

Alasan di balik orang yang peduli tentang pengecualian referensi nol dan bukan kondisi ras akan membutuhkan penelitian psikologis yang mendalam. Saya pikir itu ada hubungannya dengan fakta bahwa memperbaiki masalah referensi nol jauh lebih mudah. Setelah itu diperbaiki, mereka menggantung spanduk besar "Mission Accomplished" pada kode mereka dan membuka ritsleting setelan penerbangan mereka.

Catatan: memperbaiki kondisi balapan mungkin melibatkan menggunakan trek bendera sinkron apakah pawang harus berjalan

DSS539
sumber
Saya tidak meminta solusi untuk masalah itu. Saya bertanya-tanya mengapa ada saran luas untuk menyemprotkan kekacauan kode tambahan di sekitar peristiwa penembakan, ketika itu hanya menghindari pengecualian nol ketika ada kondisi ras yang sulit dideteksi, yang masih akan ada.
Daniel Earwicker
1
Nah, itu maksud saya. Mereka tidak peduli dengan kondisi balapan. Mereka hanya peduli tentang pengecualian referensi nol. Saya akan mengeditnya menjadi jawaban saya.
dss539
Dan poin saya adalah: mengapa masuk akal untuk peduli dengan pengecualian referensi nol namun tidak peduli dengan kondisi balapan?
Daniel Earwicker
4
Penangan acara yang ditulis dengan benar harus siap untuk menangani fakta bahwa setiap permintaan khusus untuk meningkatkan suatu peristiwa yang pemrosesannya mungkin tumpang tindih dengan permintaan untuk menambah atau menghapusnya, mungkin atau mungkin tidak menaikkan acara yang ditambahkan atau dihapus. Alasan programmer tidak peduli dengan kondisi balapan adalah bahwa dalam kode yang ditulis dengan benar tidak masalah siapa yang menang .
supercat
2
@ dss539: Meskipun orang dapat merancang kerangka acara yang akan memblokir permintaan berhenti berlangganan hingga setelah undangan acara yang tertunda selesai, desain semacam itu akan membuat tidak mungkin bagi peristiwa apa pun (bahkan sesuatu seperti Unloadacara) untuk secara aman membatalkan langganan suatu objek ke acara lain. Menjijikan. Lebih baik adalah dengan mengatakan bahwa permintaan berhenti berlangganan acara akan menyebabkan acara berhenti berlangganan "pada akhirnya", dan bahwa pelanggan acara harus memeriksa kapan mereka dipanggil apakah ada sesuatu yang berguna untuk mereka lakukan.
supercat
1

Jadi saya agak terlambat ke pesta di sini. :)

Adapun penggunaan null daripada pola objek nol untuk mewakili peristiwa tanpa pelanggan, pertimbangkan skenario ini. Anda perlu memohon suatu peristiwa, tetapi membuat objek (EventArgs) adalah non-sepele, dan dalam kasus umum acara Anda tidak memiliki pelanggan. Akan bermanfaat bagi Anda jika Anda dapat mengoptimalkan kode Anda untuk memeriksa untuk melihat apakah Anda memiliki pelanggan sama sekali sebelum Anda melakukan upaya pemrosesan untuk membangun argumen dan menjalankan acara.

Dengan pemikiran ini, solusinya adalah dengan mengatakan "baik, nol pelanggan diwakili oleh nol." Kemudian cukup lakukan pemeriksaan nol sebelum melakukan operasi mahal Anda. Saya kira cara lain untuk melakukan ini adalah dengan memiliki properti Count pada tipe Delegate, jadi Anda hanya akan melakukan operasi yang mahal jika myDelegate.Count> 0. Menggunakan properti Count adalah pola yang agak bagus yang menyelesaikan masalah asli memungkinkan pengoptimalan, dan juga memiliki properti bagus untuk dapat dipanggil tanpa menyebabkan NullReferenceException.

Perlu diingat, bahwa karena delegasi adalah tipe referensi, mereka diijinkan nol. Mungkin tidak ada cara yang baik untuk menyembunyikan fakta ini di bawah selimut dan hanya mendukung pola objek nol untuk acara, jadi alternatifnya mungkin memaksa pengembang untuk memeriksa baik nol dan nol pelanggan. Itu akan lebih jelek dari situasi saat ini.

Catatan: Ini murni spekulasi. Saya tidak terlibat dengan bahasa .NET atau CLR.

Retribusi
sumber
Saya berasumsi Anda maksudkan "penggunaan delegasi kosong daripada ..." Anda sudah dapat melakukan apa yang Anda sarankan, dengan acara diinisialisasi ke delegasi kosong. Tes (MyEvent.GetInvocationList (). Panjang == 1) akan benar jika delegasi kosong awal adalah satu-satunya hal dalam daftar. Masih tidak perlu membuat salinan terlebih dahulu. Meskipun saya pikir kasus yang Anda gambarkan akan sangat jarang.
Daniel Earwicker
Saya pikir kita sedang menyatukan ide-ide delegasi dan acara di sini. Jika saya memiliki acara Foo di kelas saya, maka ketika pengguna eksternal memanggil MyType.Foo + = / - =, mereka benar-benar memanggil metode add_Foo () dan remove_Foo (). Namun, ketika saya mereferensikan Foo dari dalam kelas yang didefinisikan, saya sebenarnya merujuk delegasi yang mendasarinya secara langsung, bukan metode add_Foo () dan remove_Foo (). Dan dengan adanya jenis-jenis seperti EventHandlerList, tidak ada yang mengamanatkan bahwa delegasi dan acara bahkan berada di tempat yang sama. Inilah yang saya maksud dengan paragraf "Ingatlah" dalam jawaban saya.
Levi
(lanjutan) Saya akui bahwa ini adalah desain yang membingungkan, tetapi alternatifnya mungkin lebih buruk. Karena pada akhirnya semua yang Anda miliki adalah delegasi - Anda dapat merujuk delegasi yang mendasarinya secara langsung, Anda mungkin mendapatkannya dari koleksi, Anda dapat membuat instantiate dengan cepat - mungkin secara teknis tidak mungkin untuk mendukung apa pun selain "periksa pola null ".
Levi
Ketika kita berbicara tentang memecat acara, saya tidak bisa melihat mengapa add / remove accessors penting di sini.
Daniel Earwicker
@Levi: Saya sangat tidak suka cara C # menangani acara. Jika saya memiliki pemabuk saya, delegasi akan diberi nama yang berbeda dari acara tersebut. Dari luar kelas, satu-satunya operasi yang diizinkan pada nama acara adalah +=dan -=. Di dalam kelas, operasi yang diizinkan juga akan mencakup pemanggilan (dengan pemeriksaan nol bawaan), pengujian terhadap null, atau pengaturan ke null. Untuk hal lain, seseorang harus menggunakan delegasi yang namanya akan menjadi nama acara dengan beberapa awalan atau akhiran tertentu.
supercat
0

untuk aplikasi threaded tunggal, Anda benar ini bukan masalah.

Namun, jika Anda membuat komponen yang mengekspos peristiwa, tidak ada jaminan bahwa konsumen komponen Anda tidak akan melakukan multithreading, dalam hal ini Anda harus bersiap untuk yang terburuk.

Menggunakan delegasi kosong tidak menyelesaikan masalah, tetapi juga menyebabkan kinerja hit pada setiap panggilan ke acara tersebut, dan mungkin bisa memiliki implikasi GC.

Anda benar bahwa konsumen tidak berhenti berlangganan agar hal ini terjadi, tetapi jika mereka berhasil melewati salinan temp, maka pertimbangkan pesan yang sudah dalam perjalanan.

Jika Anda tidak menggunakan variabel sementara, dan tidak menggunakan delegasi kosong, dan seseorang berhenti berlangganan, Anda mendapatkan pengecualian referensi nol, yang fatal, jadi saya pikir biayanya sepadan.

Jason Coyne
sumber
0

Saya tidak pernah benar-benar menganggap ini sebagai masalah karena saya umumnya hanya melindungi terhadap potensi kejahatan threading semacam ini dalam metode statis (dll) pada komponen yang dapat digunakan kembali, dan saya tidak membuat acara statis.

Apakah saya salah?

Greg D
sumber
Jika Anda mengalokasikan instance kelas dengan status yang bisa berubah (bidang yang mengubah nilai-nilainya) dan kemudian membiarkan beberapa utas mengakses instance yang sama secara bersamaan, tanpa menggunakan penguncian untuk melindungi bidang tersebut agar tidak dimodifikasi oleh dua utas pada saat yang sama, Anda mungkin salah melakukannya. Jika semua utas Anda memiliki instance terpisah (tidak berbagi apa pun) atau semua objek Anda tidak dapat diubah (sekali dialokasikan, nilai bidangnya tidak pernah berubah) maka Anda mungkin baik-baik saja.
Daniel Earwicker
Pendekatan umum saya adalah membiarkan sinkronisasi hingga ke pemanggil kecuali dalam metode statis. Jika saya penelepon, maka saya akan melakukan sinkronisasi pada tingkat yang lebih tinggi. (Dengan pengecualian objek yang satu-satunya tujuan menangani akses yang disinkronkan, tentu saja.))
Greg D
@GregD itu tergantung seberapa rumit metode ini dan data apa yang digunakannya. jika itu mempengaruhi anggota internal, dan Anda memutuskan untuk berlari dalam keadaan berulir / Ditugaskan, Anda akan mengalami banyak kesakitan
Mickey Perlstein
0

Kirim semua acara Anda di tempat konstruksi dan biarkan saja. Desain kelas Delegasi tidak mungkin menangani penggunaan lainnya dengan benar, seperti yang akan saya jelaskan pada paragraf terakhir dari posting ini.

Pertama-tama, tidak ada gunanya mencoba mencegat pemberitahuan acara ketika penangan acara Anda harus sudah membuat keputusan tersinkronisasi tentang apakah / bagaimana menanggapi pemberitahuan tersebut .

Apa pun yang dapat diberitahukan, harus diberitahukan. Jika penangan acara Anda menangani pemberitahuan dengan benar (yaitu mereka memiliki akses ke negara aplikasi yang berwenang dan merespons hanya jika perlu), maka akan baik untuk memberi tahu mereka kapan saja dan percaya mereka akan merespons dengan baik.

Satu-satunya saat seorang pawang tidak diberi tahu bahwa suatu peristiwa telah terjadi, adalah jika peristiwa tersebut tidak terjadi! Jadi, jika Anda tidak ingin pawang diberitahu, berhentilah membuat acara (mis. Nonaktifkan kontrol atau apa pun yang bertanggung jawab untuk mendeteksi dan menjadikan acara itu ada sejak awal).

Sejujurnya, saya pikir kelas Delegasi tidak dapat diselamatkan. Penggabungan / transisi ke MulticastDelegate adalah kesalahan besar, karena secara efektif mengubah definisi (berguna) dari suatu peristiwa dari sesuatu yang terjadi pada satu waktu, menjadi sesuatu yang terjadi dalam rentang waktu tertentu. Perubahan semacam itu membutuhkan mekanisme sinkronisasi yang secara logis dapat menciutkannya kembali menjadi satu instan, tetapi MulticastDelegate tidak memiliki mekanisme semacam itu. Sinkronisasi harus mencakup seluruh rentang waktu atau instan acara berlangsung, sehingga setelah aplikasi membuat keputusan yang disinkronkan untuk mulai menangani suatu acara, ia selesai menangani sepenuhnya (secara transaksi). Dengan kotak hitam yang merupakan kelas hybrid MulticastDelegate / Delegate, ini hampir mustahil, jadimematuhi penggunaan pelanggan tunggal dan / atau mengimplementasikan MulticastDelegate jenis Anda sendiri yang memiliki pegangan sinkronisasi yang dapat diambil saat rantai handler sedang digunakan / dimodifikasi . Saya merekomendasikan ini, karena alternatifnya adalah mengimplementasikan sinkronisasi / transaksional-integritas secara berlebihan di semua handler Anda, yang akan menjadi rumit / tidak perlu rumit.

Triynko
sumber
[1] Tidak ada pengendali acara yang berguna yang terjadi pada "satu instan dalam waktu". Semua operasi memiliki rentang waktu. Penangan tunggal mana pun dapat memiliki urutan langkah yang tidak sepele untuk dilakukan. Mendukung daftar penangan tidak mengubah apa pun.
Daniel Earwicker
[2] Memegang kunci saat suatu peristiwa dipecat adalah kegilaan total. Ini mengarah pada kebuntuan. Sumber mengeluarkan kunci A, peristiwa kebakaran, wastafel mengeluarkan kunci B, sekarang dua kunci ditahan. Bagaimana jika beberapa operasi di utas lainnya menghasilkan kunci yang dikeluarkan dalam urutan terbalik? Bagaimana kombinasi mematikan seperti itu dapat dikesampingkan ketika tanggung jawab untuk mengunci dibagi antara komponen yang dirancang / diuji secara terpisah (yang merupakan inti dari semua peristiwa)?
Daniel Earwicker
[3] Tidak satu pun dari masalah ini yang mengurangi kegunaan serba guna dari delegasi / peristiwa multicast normal dalam komposisi komponen berulir tunggal, terutama dalam kerangka kerja GUI. Kasus penggunaan ini mencakup sebagian besar penggunaan acara. Menggunakan acara dengan cara bebas-ulir adalah nilai yang dipertanyakan; ini sama sekali tidak membatalkan desain mereka atau kegunaannya yang jelas dalam konteks di mana mereka masuk akal.
Daniel Earwicker
[4] Utas + peristiwa sinkron pada dasarnya adalah herring merah. Komunikasi asinkron yang antri adalah jalan yang harus ditempuh.
Daniel Earwicker
[1] Saya tidak mengacu pada waktu yang diukur ... Saya berbicara tentang operasi atom, yang secara logis terjadi secara instan ... dan maksud saya, tidak ada hal lain yang melibatkan sumber daya yang sama yang dapat mereka gunakan yang dapat berubah ketika peristiwa itu terjadi karena ini bersambung dengan kunci.
Triynko
0

Silakan lihat di sini: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Ini adalah solusi yang tepat dan harus selalu digunakan sebagai pengganti semua solusi lainnya.

“Anda dapat memastikan bahwa daftar doa internal selalu memiliki setidaknya satu anggota dengan menginisialisasi dengan metode anonim apa-apa. Karena tidak ada pihak eksternal dapat memiliki referensi ke metode anonim, tidak ada pihak eksternal dapat menghapus metode, sehingga delegasi tidak akan pernah nol ”- Pemrograman. Komponen NET, Edisi 2, oleh 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);  
}  
Eli
sumber
0

Saya tidak percaya pertanyaannya terbatas pada tipe c # "event". Menghapus batasan itu, mengapa tidak menciptakan kembali roda sedikit dan melakukan sesuatu di sepanjang garis ini?

Naikkan utas acara dengan aman - praktik terbaik

  • Kemampuan untuk sub / berhenti berlangganan dari utas apa pun saat berada dalam kenaikan gaji (kondisi ras dihapus)
  • Overload operator untuk + = dan - = di tingkat kelas.
  • Delegasi yang ditentukan oleh penelepon umum
crokusek
sumber
0

Terima kasih atas diskusi yang bermanfaat. Saya sedang mengerjakan masalah ini baru-baru ini dan membuat kelas berikut yang sedikit lebih lambat, tetapi memungkinkan untuk menghindari panggilan ke objek yang dibuang.

Poin utama di sini adalah bahwa daftar doa dapat dimodifikasi bahkan acara dimunculkan.

/// <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;
        }
    }
}

Dan penggunaannya adalah:

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

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

Uji

Saya mengujinya dengan cara berikut. Saya memiliki utas yang membuat dan menghancurkan objek seperti ini:

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

Dalam Barkonstruktor (objek pendengar) saya berlangganan SomeEvent(yang diterapkan seperti yang ditunjukkan di atas) dan berhenti berlangganan di 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;
    }

Juga saya punya beberapa utas yang meningkatkan acara dalam satu lingkaran.

Semua tindakan ini dilakukan secara bersamaan: banyak pendengar diciptakan dan dihancurkan dan acara dipecat pada saat yang sama.

Jika ada kondisi lomba saya harus melihat pesan di konsol, tetapi kosong. Tetapi jika saya menggunakan acara CLR seperti biasa saya melihatnya penuh dengan pesan peringatan. Jadi, saya dapat menyimpulkan bahwa adalah mungkin untuk mengimplementasikan acara yang aman untuk thread di c #.

Bagaimana menurut anda?

Tony
sumber
Terlihat cukup baik untukku. Meskipun saya pikir mungkin saja (secara teori) disposed = trueterjadi sebelum foo.SomeEvent -= Handlerdalam aplikasi pengujian Anda, menghasilkan false positive. Namun terlepas dari itu, ada beberapa hal yang mungkin ingin Anda ubah. Anda benar-benar ingin menggunakan try ... finallyuntuk kunci - ini akan membantu Anda membuat ini tidak hanya aman, tetapi juga aman dibatalkan. Belum lagi Anda bisa menyingkirkan itu konyol try catch. Dan Anda tidak memeriksa delegasi yang lewat Add/ Remove- bisa jadi null(Anda harus langsung membuangnya di Add/ Remove).
Luaan