This article is currently in the process of being translated into Turkish (~96% done).
Multi-threading with the BackgroundWorker
Varsayılan olarak, uygulamanız yazdığınız bir kod parçasını işlettiğinde bu kod parçası uygulamanızın kendi kod çevrimi (thread) içerisinde çalıştırılır. Bunun anlamı yazdığınız bu kod parçası çalışırken uygulamanız içerisinde UI'ınızın güncellenmesi de dahil başka hiç bir şey yapılmaz.
Bu durum Windows programlamaya yeni başlayıp bir saniyeden daha uzun işlem yapan bir program yazdıklarında, uygulamalarının bu sırada başka hiç bir iş yapamamadığını gördüklerinde gerçekten de onlara sürpriz olmaktadır. Uzun süren işlemler sırasında bir proses barını da güncellemeye çalışanlar proses bar güncelleme işleminin proses tamemen bitmeden gerçekleşmediğini gördüklerinde hayal kırıklığı içerisinde çeşitli forumlara yazı yazmaya başlarlar.
Tüm bu sorunların çözümü çoklu kod çevrimi (multiple thread) kullanmaktır. Her ne kadar C# çoklu kod çevriminin kullanımını gerçekten de kolay hale getirse de çoklu kod çevrimi pek çok gizli tuzak ile gelmekte ve konu kullanıcılar açısında anlaşılmaz bir hal almaktadır. Tüm bu zorlukların çözümü BackgroundWorker sınıfını kullanmaktır. Bu sayede uygulamanıza basit ve hızlı bir şekilde fazladan bir kod çevrimi ekleyebilirsiniz.
BackgroundWorker nasıl çalışır?
Windows uygulamalarında Çoklu Kod Çevriminin en zor kısmı kullanıcı arayüzünüzün (UI) diğer kod çevrimi içerisinden güncellenmesine izin verilmemesidir. Eğer bunu yaparsanız yani UI kod çevrimi dışındaki bir kod çevrimi içerisinden UI kontrollerini güncellemeye çalışmanız durumunda uygulamanız muhtemelen çökecektir. Bu işlemi güvenli bir şekilde yapmanın yolu UI ana kod çevriminiz içerisinden istenilen değişikliklerin yapılabilmesi için bir yordam çağırmaktır. Bu BackgroundWorker sınıfının kullanmamanız durumunda oldukça karmaşık bir hal alabilir.
Bir işlem farklı bir kod çevrimi üzerinde çalıştırıldığında genel olarak uygulamanızın geri kalan kısmı ile iki durumda haberleşmek istersiniz. İlk olarak yürüttüğünüz işlemin durumu hakkında gerekli güncellemeyi kullanıcı ekranında yapmak (örneğin işlemin yüzde kaçının tamamlandığını bir ProcessBar üzerinde göstermek), ikinci olarak ise yürüttüğünüz işlem bittiğinde elde ettiğiniz sonuçları kullanıcıya göstermek istediğinizde. BackgroundWorker sınıfı bu fikir üzerine inşa edilmiştir ve dolayısıyla iki olay (event) ile birlikte gelmektedir : ProgressChanged ve RunWorkerComplated
Sınıfın üçüncü olayı DoWork olayıdır ve genel kural olarak bu olay içerisinde kullanıcı arayüzündeki (UI) hiç bir kontrole dokunmamanız gerekir. Eğer bu olay içerisinde UI üzerinde bir güncelleme yapmak isterseniz ReportProgress yöntemini çağırmanız gerekir. Bu yordam ise UI ile aynı kod çevrimi içerisinde yer alan ve güvenli bir şekilde UI kontrollerinin güncellenmesine izin veren ProgressChanged yordamını çağırmaktadır. DoWork yordamının sonuna geldiğinizde ise RunWorkerCompleted yordamı otomatik olarak çağrılır.
Özetle, DoWork olayı tüm uzun süren işlemlerin yapıldığı yordamdır. Yordam içerisindeki yazılan kodun tamamı ayrı bir kod çevrimi içerisinde çalıştırıldığından bu yordam içerisinde doğrudan UI kontrollerine erişime (örneğin Label nesnesinin Content özelliğine yeni bir değer atanması gibi) izin verilmez. Eğer ilgili yordama UI veya başka bir yerden başlangıçta bir veriyi parametre olarak geçirmek isterseniz veriyi RunWorkerAsync() yordamına parametre olarak geçebilirsiniz. Elde ettiğiniz sonuçları ise e.Result özelliğine aktararak UI arayüzüne geri gönderebilirsiniz.
ProgressChanged ve RunWorkerCompleted olayları , genellikle ana/UI kod çevrimi tarafından oluşturulan BackgroundWorker kod çevrimi ile aynı çevrim içerisinde çalıştırılırlar ve UI kontrollerinin doğrudan bu olaylar içerisinde güncellenmesine izin verirler. Bu sebeple arka plan kod çevrimi ile UI kod çevrimi arasında haberleşme ReportProgress() yordamı aracılığı ile gerçekleştirilir.
Buraya kadar işin teori kısmıydı. BackgroundWorker kullanımı her ne kadar kolay olsa bile neyi nasıl yaptığı en önemli anlaşılması gereken kısımdır. Aksi halde daha önce de belirtildiği gibi çoklu kod çevriminde kazara yapılacak bir hata uygulamanızda ciddi sorunlara neden olacaktır.
BackgroundWorker örneği
Sanırım bu kadar teori yeter. Biraz kod pek çok şeyi daha hızlı anlamamızı sağlayacaktır. İlk örneğimizde oldukça basit ancak çok fazla zaman gerektiren bir uygulama yapacağız. Uygulamamızda 0 ile 10000 arasındaki tüm sayıların 7 ile tam bölünüp bölünemediğini test edeceğiz. Gerçekte günümüz çok hızlı bilgisayarları için bu işlem çocuk oyuncağıdır. Bu nedenle olayı biraz daha yavaşlatmak adına her bir iterasyona bir milisaniye (saniyenin binde biri) gecikme de ekledim.
Örnek uygulamamız iki buton içermektedir. Butonlardan biri görevi eş zamanlı olarak (aynı kod çevrimini kullanarak) diğeri ise BackgroundWorker sınıfını kullanarak yani çoklu kod çevrimi ile tamamlayacaktır. Bu aynı zamanda uzun zaman gerektiren işlemler için neden çoklu kod çevrimi kullanılması gerektiğini de bize gösterecektir. Kod aşağıdaki gibi gözükmektedir:
<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BackgroundWorkerSample" Height="300" Width="375">
<DockPanel Margin="10">
<DockPanel DockPanel.Dock="Top">
<Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
<Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
</DockPanel>
<ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />
<ListBox Name="lbResults" Margin="0,10" />
</DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
namespace WpfTutorialSamples.Misc
{
public partial class BackgroundWorkerSample : Window
{
public BackgroundWorkerSample()
{
InitializeComponent();
}
private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
int max = 10000;
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
int result = 0;
for(int i = 0; i < max; i++)
{
if(i % 42 == 0)
{
lbResults.Items.Add(i);
result++;
}
System.Threading.Thread.Sleep(1);
pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
}
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
}
private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
worker.RunWorkerAsync(10000);
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
int max = (int)e.Argument;
int result = 0;
for(int i = 0; i < max; i++)
{
int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
if(i % 42 == 0)
{
result++;
(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
}
else
(sender as BackgroundWorker).ReportProgress(progressPercentage);
System.Threading.Thread.Sleep(1);
}
e.Result = result;
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbCalculationProgress.Value = e.ProgressPercentage;
if(e.UserState != null)
lbResults.Items.Add(e.UserState);
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
}
}
}
Uygulamanın XAML kodu, biri hesaplama sürecinin aynı kod çevriminde (UI kod çevrimi) diğeri ise çoklu kod çevrimi üzerinde koşturulmasını sağlayan iki buton, tüm hesaplanan numaraların gösterildiği bir ListBox ve ekranın en altında işlem sürecini gösteren ProgressBar'dan oluşmaktadır.
Kaynak kod dosyasında ise eş zamanlı olarak olay işleci başlatıyoruz. Daha önce de bahsedildiği üzere hesaplama çevrimi 0 dan 10000'e kadar küçük gecikmelere de eklenerek çalışmaktadır. Eğer sayı 7 ile tam bölünebiliyorsa onu göstermek üzere listeye ekliyoruz. Her hesap çevrimide ProgressBar'ın değerini de güncelliyoruz. Hesaplamayı bitirdiğimizde de kullanıcıya kaç tane sayı bulduğumuza ilişkin bir mesaj gösteriyoruz.
Eğer uygulamayı çalıştırır ve ilk butona basarsanız prosesin ne kadarının tamamlandığı bilgisini proses tamamlanana kadar göremezsiniz.
Fareniz ile butona bastığınız anda Liste kutusunda tek bir liste elemanının olmaması, ProgressBar'da herhangi bir değişimi görülmemesi hatta bastığınız butonun geri gelmemesi programın kullanıcı arayüzünde (UI) tek bir güncellemenin yapılmadığını göstermesi bakımından önemlidir.
İkinci butona bastığınızda ise BackgroundWorker yaklaşımı devreye girer. Koddan da anlayacağınız üzere aynı şeyi biraz farklı bir yaklaşımla gerçekleştiriyoruz. Tüm uzun süren işlemler şimdi RunWorkerAsync() metodunu çalıştırdıktan sonra çağrılan DoWork olayının içerisinde yer almaktadır. Birazdan üzerinde konuşacağımız üzere bu yordam çalışan kod çevrimi tarafından kullanılacak giriş parametrelerini programınızdan alır.
Daha önce de bahsedildiği üzere DoWork olayı içerisinden UI güncellemesi yapılmasına izin verilmemektedir. UI güncellemesi yapılması gereken durumlarda DoWork olayı içerisinden ReportProgress metodunun çağrılması gerekir. Eğer güncel sayı 7 ile tam bölünebilir ise onu listeye ekleyerek aksi halde sadece güncel tamamlanma yüzdesi ReportProgress metodu çağrılarak UI kod çevrimine gerekli güncellemelerin yapılabilmesi için bildirilir.
Tüm sayıların testi tamamlandığında sonucu e.Result özelliğine (property) atarız. Bu özellik sonrasında kullanıcıya sonuçları gösterdiğimiz RunWorkerCompleted metodunun içerisine aktarılır. Sonucun ancak tüm işlem tamamlandıktan sonra kullanıcıya gösterilmesi başta biraz karmaşık gözükse de tekrar etmek gerekirse bu sayede DoWork içerisinden zaten izin verilmeyen UI ile haberleşme yapılmadığında emin olmaktayız.
Sonuç sizin de göreceğiniz gibi daha kullanıcı dostudur.
Ana program pencesi kilitlenmemiş durumdadır. Buton basılı durumdadır fakat kilitlenmemiştir. 7 ile tam bölünebilen sayılar listede ard arda gösterilmektedir. ProgressBar sürekli artarak işlemin ne kadarının kaldığını doğru bir şekilde gösterebilmektedir. Kısaca program arayüzü çok daha kullanıcı isteklerine cevap verebilir hale gelmiştir.
Giriş ve Çıkış
Dikkat edilirse DoWork olayı metonuna, gerek RunWorkerAsync metodu aracılığı ile geçilen değer gerekse de e.Result parametresi aracılığı ile çıkan sonuç nesne (object) türündendir. Bunun anlamı giriş ve çıkış değerleri için herhangi tipte değerleri serbestçe atayabilirsiniz. Bizim basit örneğimizde giriş ve çıkış değerleri tamsayı (integer) türünden olsa da elbette daha karmaşık değerler kullanılabilir.
Pek çok durumda daha karmaşık yapıdaki kendi sınıf (class) veya yapınızı (struct) DoWork metoduna geçebilir veya geri alabilirsiniz. Bu yöntemle yapılabileceklerin nerede ise sınırsızdır ve BackgroundWorker ile uygulama arayüzü arasında istediğiniz karmaşıklıkta verileri aktarabilirsiniz.
Aynı şey ReportProgress metodu için de geçerlidir. Metodun ikinci parametresi yine nesne türünden userState isimli parametredir. Bunun anlamı ProgressChanged metoduna istediğiniz değeri geçebilirsin.
Özetle...
BackgroundWorker, uygulamanıza çoklu kod çevrimi eklemek istediğinizde son derece kolay kullanılabilen harika bir yardımcıdır. Bu bölümde BackgroundWorker sınıfının proses raporlamayı son derece kolay hale getiren özelliklerine göz attık. Sınıf bir sonraki bölümde görüleceği üzere çalışan kod çevrimini gerektiğinde durdurabileme özelliğine de sahiptir.