Saya ingin memberi tahu Anda secara langsung bahwa artikel ini tidak akan membahas utas secara khusus, tetapi peristiwa dalam konteks utas di .NET. Jadi, saya tidak akan mencoba mengatur utas dengan benar (dengan semua blok, panggilan balik, pembatalan, dll.) Ada banyak artikel tentang hal ini.
Semua contoh ditulis dalam C# untuk kerangka kerja versi 4.0 (di 4.6, semuanya sedikit lebih mudah, tapi tetap saja, ada banyak proyek di 4.0). Saya juga akan mencoba untuk tetap menggunakan C# versi 5.0.
Pertama, saya ingin mencatat bahwa ada delegasi yang siap untuk sistem acara .Net yang sangat saya rekomendasikan untuk digunakan daripada menciptakan sesuatu yang baru. Misalnya, saya sering menghadapi 2 metode berikut untuk mengatur acara.
Metode pertama:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Saya akan merekomendasikan menggunakan metode ini dengan hati-hati. Jika Anda tidak menguniversalkannya, pada akhirnya Anda dapat menulis lebih banyak kode dari yang diharapkan. Dengan demikian, itu tidak akan menetapkan struktur yang lebih tepat jika dibandingkan dengan metode di bawah ini.
Dari pengalaman saya, saya dapat mengatakan bahwa saya menggunakannya ketika saya mulai bekerja dengan acara dan akibatnya membodohi diri sendiri. Sekarang, saya tidak akan pernah mewujudkannya.
Metode kedua:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Metode ini cukup valid, tetapi bagus untuk kasus-kasus tertentu ketika metode di bawah ini tidak berfungsi karena beberapa alasan. Jika tidak, Anda mungkin mendapatkan banyak pekerjaan yang monoton.
Dan sekarang, mari kita lihat apa yang telah dibuat untuk acara tersebut.
Metode universal:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Seperti yang Anda lihat, di sini kami menggunakan kelas EventHandler universal. Artinya, tidak perlu mendefinisikan handler Anda sendiri.
Contoh selanjutnya menampilkan metode universal.
Mari kita lihat contoh paling sederhana dari generator peristiwa.
class EventRaiser { int _counter; public event EventHandler<EventRaiserCounterChangedEventArgs> CounterChanged; public int Counter { get { return _counter; } set { if (_counter != value) { var old = _counter; _counter = value; OnCounterChanged(old, value); } } } public void DoWork() { new Thread(new ThreadStart(() => { for (var i = 0; i < 10; i++) Counter = i; })).Start(); } void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged != null) CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); } } class EventRaiserCounterChangedEventArgs : EventArgs { public int NewValue { get; set; } public int OldValue { get; set; } public EventRaiserCounterChangedEventArgs(int oldValue, int newValue) { NewValue = newValue; OldValue = oldValue; } }
Di sini kita memiliki kelas dengan properti Counter yang dapat diubah dari 0 hingga 10. Pada saat itu, logika yang mengubah Counter diproses di thread terpisah.
Dan inilah titik masuk kami:
class Program
{
static void Main(string[] args)
{
var raiser = new EventRaiser();
raiser.CounterChanged += Raiser_CounterChanged;
raiser.DoWork();
Console.ReadLine();
}
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
{
Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
}
}
Artinya, kita membuat instance generator kita, berlangganan counter change dan, dalam event handler, mengeluarkan nilai ke console.
Inilah yang kami dapatkan sebagai hasilnya:
Sejauh ini baik. Tapi coba kita pikirkan, di thread mana event handler dieksekusi?
Sebagian besar rekan saya menjawab pertanyaan ini "Secara umum". Artinya tidak ada satupun dari mereka yang tidak mengerti bagaimana susunan delegasi. Saya akan mencoba menjelaskannya.
Kelas Delegate berisi informasi tentang suatu metode.
Ada juga turunannya, MulticastDelegate, yang memiliki lebih dari satu elemen.
Jadi, saat Anda berlangganan suatu acara, turunan MulticastDelegate dibuat. Setiap pelanggan berikutnya menambahkan metode baru (event handler) ke dalam instance MulticastDelegate yang sudah dibuat.
Saat Anda memanggil metode Invoke, penangan semua pelanggan dipanggil satu per satu untuk acara Anda. Pada saat itu, utas di mana Anda memanggil penangan ini tidak tahu apa-apa tentang utas di mana mereka ditentukan dan, karenanya, tidak dapat memasukkan apa pun ke dalam utas itu.
Secara umum, event handler pada contoh di atas dieksekusi di thread yang dihasilkan dalam metode DoWork(). Artinya, selama pembuatan acara, utas yang membuatnya sedemikian rupa, sedang menunggu eksekusi semua penangan. Saya akan menunjukkan ini kepada Anda tanpa menarik utas Id. Untuk ini, saya mengubah beberapa baris kode pada contoh di atas.
Bukti bahwa semua penangan dalam contoh di atas dieksekusi di utas yang disebut acara
Metode tempat acara dibuat
void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged != null) { CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue)); } }
Penangan
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
Di handler, kami mengirim utas saat ini ke mode tidur selama setengah detik. Jika penangan bekerja di utas utama, waktu ini akan cukup untuk utas yang dihasilkan di DoWork() untuk menyelesaikan tugasnya dan mengeluarkan hasilnya.
Namun, inilah yang benar-benar kami lihat:
Saya tidak tahu siapa dan bagaimana seharusnya menangani peristiwa yang dihasilkan oleh kelas yang saya tulis, tetapi saya tidak benar-benar ingin penangan ini memperlambat pekerjaan kelas saya. Itu sebabnya, saya akan menggunakan metode BeginInvoke daripada Invoke. BeginInvoke menghasilkan utas baru.
Catatan:Keduanya, metode Invoke dan BeginInvoke bukan anggota kelas Delegate atau MulticastDelegate. Mereka adalah anggota dari kelas yang dihasilkan (atau kelas universal yang dijelaskan di atas).
Sekarang, jika kita mengubah metode di mana acara tersebut dihasilkan, kita akan mendapatkan yang berikut:
Pembuatan acara multi-utas:
void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged != null) { var delegates = CounterChanged.GetInvocationList(); for (var i = 0; i < delegates.Length; i++) ((EventHandler<EventRaiserCounterChangedEventArgs>)delegates[i]).BeginInvoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue), null, null); Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue)); } }
Dua parameter terakhir sama dengan nol. Yang pertama adalah panggilan balik, yang kedua adalah parameter tertentu. Saya tidak menggunakan panggilan balik dalam contoh ini, karena contohnya adalah perantara. Mungkin berguna untuk umpan balik. Misalnya, ini dapat membantu kelas yang menghasilkan peristiwa untuk menentukan, apakah suatu peristiwa ditangani dan/atau diperlukan untuk mendapatkan hasil dari penanganan ini. Itu juga dapat membebaskan sumber daya yang terkait dengan operasi asinkron.
Jika kita menjalankan programnya, kita akan mendapatkan hasil sebagai berikut.
Saya kira, cukup jelas bahwa sekarang event handler dieksekusi di thread terpisah, yaitu event generator tidak peduli siapa, bagaimana dan berapa lama akan menangani event-nya.
Dan di sini muncul pertanyaan:bagaimana dengan penanganan berurutan? Kami memiliki Counter, setelah semua. Bagaimana jika itu akan menjadi serangkaian perubahan status? Tapi saya tidak akan menjawab pertanyaan ini, itu bukan subjek artikel ini. Saya hanya bisa mengatakan bahwa ada beberapa cara.
Dan satu hal lagi. Agar tidak mengulangi tindakan yang sama berulang kali, saya sarankan untuk membuat kelas terpisah untuk mereka.
Kelas untuk membuat peristiwa asinkron
static class AsyncEventsHelper { public static void RaiseEventAsync<T>(EventHandler<T> h, object sender, T e) where T : EventArgs { if (h != null) { var delegates = h.GetInvocationList(); for (var i = 0; i < delegates.Length; i++) ((EventHandler<T>)delegates[i]).BeginInvoke(sender, e, h.EndInvoke, null); } } }
Dalam hal ini, kami menggunakan panggilan balik. Itu dieksekusi di utas yang sama dengan pawang. Yaitu, setelah metode handler selesai, delegasi memanggil h.EndInvoke berikutnya.
Berikut adalah bagaimana seharusnya digunakan
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Saya kira, sekarang sudah jelas mengapa metode universal diperlukan. Jika kami menjelaskan peristiwa dengan metode 2, trik ini tidak akan berhasil. Jika tidak, Anda harus menciptakan universalitas untuk delegasi Anda sendiri.
Catatan :Untuk proyek nyata, saya sarankan mengubah arsitektur acara dalam konteks utas. Contoh yang dijelaskan dapat menyebabkan kerusakan pada pekerjaan aplikasi dengan utas, dan disediakan hanya untuk tujuan informatif.
Kesimpulan
Harapan, saya berhasil menggambarkan cara kerja acara dan di mana penangan bekerja. Di artikel berikutnya, saya berencana mempelajari lebih dalam untuk mendapatkan hasil penanganan peristiwa saat panggilan asinkron dilakukan.
Saya menantikan komentar dan saran Anda.