19 Nisan 2012 Perşembe

C#' da BackgroundWorker Kullanımı

Merhaba arkadaşlar..
Bu yazımızda "BackgroundWorker" kontrolünü inceleyeceğiz. Windows uygulamalarında bazı işlerin ilerlemesi çok zaman alır. Örneğin ağ üzerinde bir dosya transferi yapan bir program yaptınız. Transfer olayı 5 dakika sürüyor diyelim. Program bu işlemi yaparken de kullanıcının etkisine karşı bir tepki verebilmelidir. Bizim yapmamız gereken iş, transfer işlemi yürütülürken, programın kullanıcıya tepki vermesini sağlamak için, arka planda yeni bir thread açmaktır. Thread 'ın, "geçir(fiil), iplik(isim),en küçük yürütme birimi(isim)" gibi kelime anlamları vardır. Programlamadaki anlamı ise, işlemciye veri aktarımı yapan küçük veri yollarıdır. Bu açıklamalardan sonra  BackgroundWorker kontrolü ile ilişkisini anlatmak gerekirse, BackgroundWorker arka planda thread 'lerle çalışır, thread 'leri kontrol eder. Bu açtığım thread sayesinde programım kilitlenmeyecektir. Bu işi yapan komponente "BackgroundWorker" denir. Kısacası BackgroundWorker, windows uygulamalarında "multi thread" uygulama geliştirmenize olanak sağlar.

Şimdi BackgroundWorker kontrolünün özelliklerini inceleyelim:
  • WorkerReportsProgress, özelliği "true" yapılırsa, BackgroundWorker çalışırken yapılan işlemlerle ilgili, dışarıya anlık, güncel bilgi gönderir. Bu bilgi  BackgroundWorker 'ın ProgressChanged event 'inde yakalanır. Default değeri "false" 'tur.
  • WorkerSupportsCancellation, true ise kullanıcı, BackgroundWorker kontrolünün arka planda yaptığı işi sonladırabilir. Default değeri "false" 'tur. Örneğin programcı olarak ben, arka planda yapılan iş bitmeden, işlemin sonlandırılmasını istemeyebilirim. Bu özelliğin bu opsiyonu sunmasının nedeni de budur.
Bir de bu kontrolümüzün event 'lerine bir göz atalım. 3 tane event 'e sahiptir. Bunlar:
  • DoWork, event 'inde BackgroundWorker 'ın yapılacağı işlemler tanımlanır. BackgroundWorker çalıştırıldığında bu metot çalışmaya başlar. Peki BackgroundWorker ne zaman çalışır? BackgroundWorker 'ın "RunWorkAsync()" adında bir metodu vardır.  "RunWorkAsync" metodu bir defa çağırıldığında "DoWork" event 'i de tetiklenmiş olur ve iki iş birbirinden bağımsız olarak çalışmaya başlar. "RunWorkAsync" 'ın, "run work asynchron" kısaltmasıdır. Kelime anlamı da "bağımsız olarak çalış" demektir.
  • ProgressChanged, BackgroundWorker 'ın WorkerReportsProgress özelliği true ise, BackgroundWorker 'ın yaptığı her işlem değişikliğinde bu event tetiklenir. Bu event 'te genellikle bir "ProgressBar" kontrolü üzerinde, BackgroundWorker 'ın işlemlerinin durumu izlenir.   
  • RunWorkerComplated,  BackgroundWorker 'ın işlemi bittiğinde bu event tetiklenir. Eğer programda ayrı thread 'deki yapılan işin bittiğinden haberdar olmak isteniyorsa bu event kullanılır. Böylece kullanıcı dostu bir uygulama geliştirmeye olanak sağlar. Bu event, bir kere çalışır.
Hemen bir örnek üzerinde bu kontrolümüzü daha yakından tanıyalım. "IsYap" adında bir metodum olsun. Bir tane de butonum olsun. Metodum 1'den 100000'e kadar olan sayıları, formun textine yazsın ve bu sayıların toplamını çalışması bittikten sonra MessageBox 'da göstersin. Metodumuzu butonun "Click" event 'inde çağıralım.
        private void IsYap()
        {
            long toplam = 0;
            for (int i = 0; i < 100000; i++)
            {
                toplam += i;
                this.Text = i.ToString();
            }
            MessageBox.Show(toplam.ToString());
        }
        private void btnIsYap_Click(object sender, EventArgs e)
        {
            IsYap();
        }
Programı bu haliyle çalıştırdığımda, kullanıcıya tepki vermiyor, formumu sürükleyemiyorum. Formun textinde "not responding" (yanıt vermiyor) ibaresi görmek mümkün. Bir kilitlenme söz konusu. Çünkü thread 'im meşgul. Bu olayı bir örnekle anlamaya çalışalım.

Uygulamada "main thread" denilen bir thread vardır. Bu thread, program çalıştığında formun görsellerini cpu da işleyip ekrana getirmekten sorumludur. Biz zaman gerektiren bir iş parçacığı yollarsak bu thread 'de bir trafik meydana gelir. Bu thread 'de oluşan bir trafik görselliği olumsuz yönde etkiler. Çünkü bir yandan iş yapmaya çalışırken diğer yandan programın anlık görselliğini de taşımak zorundadır. Bunları aynı anda yapamaz. Aynı anda yapamaz derken arkadaşlar: Tek şeritli bir yol düşünün. Aynı anda iki araba geçebilir mi? Ancak sırayla geçebilirler. Bu örnekte thread bizim tek şeritli yolumuz, araba ise işin tamamı değil, işin bir parçası olsun. Eğer thread 'in üzerinden iki iş aynı anda geçmeye çalışırsa, thread bunu bir işten bir parça, diğer işten bir parça alarak yapar. Böylece görsellikte bir aksaklık meydana gelir.
Bu sorun gidermek için, en az bir adet daha thread 'e ihtiyaç duyulur. Bu thread 'e de "cross thread" denir. Böylelikle "main thraed" im rahatlıkla görsellikle ilgilenirken, yeni açtığım thread de benim yapmak istediğimiz iş ile ilgilenir.

Projeme "ToolBox" 'dan bir adet BackgroundWorker atalım. İşlemimi butonun "Click" event 'inde değil de BackgroundWorker 'ın "DoWork" event 'inde tanımlayalım. Ve butonun "Click" event 'inde BackgroundWorker 'ın çalışmasını başlatalım.
        private void btnIsYap_Click(object sender, EventArgs e)
        {
            //IsYap();
            backgroundWorker1.RunWorkerAsync();
        }
        private void IsYap()
        {
            long toplam = 0;
            for (int i = 0; i < 100000; i++)
            {
                toplam += i;
                this.Text = i.ToString();
            }
            MessageBox.Show(toplam.ToString());
        }
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            IsYap();
        }
O da ne? Bir hata mesajı ile karşılaştık.
Bu bir "cross-thread" hatasıdır arkadaşlar. C# uygulamalarında, uygulama yanlış yönlendirilmemesi amacıyla, "multi-threading" özelliği default olarak yasaktır. Bu özelliği kullanacağımı belirtmeliyim. Programa, ben "birden fazla thread kullanmak istiyorum" demeliyim. Bunu da ya formun "Load" event 'inde ya da formun "constructor" 'da (formun yapıcısında) belirtebilirim. Fakat formun yapıcısının ilk önce çalışmasından dolayı bu kontrolü formun yapıcısında, formun komponentleri yüklenmeden yapmam daha mantıklıdır. 
        public Form1()
        {
            CheckForIllegalCrossThreadCalls = false;
            InitializeComponent();
        }
CheckForIllegalCrossThreadCalls = false diyerek, "yasal olmayan thread çağrılarını kontrol etme" demiş bulunuyorum.

Şimdi programın sorunsuz bir şekilde çalışacaktır. BackgroundWorker 'ın  yaptığı işi bir "ProgressBar" üzerinde görmek istersek, formumuza bir ProgressBar kontrolü atalım, WorkerReportsProgress özelliğini "true" yapalım. BackgroundWorker 'ın  yaptığı işteki her değişiklikte "ProgressChanged" event 'ini tetikletmek için "IsYap" metodunda  backgroundWorker1.ReportProgress metodunu çalıştıralım. Bu metot, int tipinde bir değer ister, bu da işin yüzde kaçının bittiğini gösterir. Bu yüzden buraya girilen değer en fazla 100 olabilir. i 'yi 1000 'e bölmemizin sebebi de budur. Bu sorunu ProgessBar 'ın özelliklerinden en büyük değerini "Maximum= 100000" yaparak da çözebilirdik. Son olarak "RunWorkerCompleted" event 'inde de "sonuc" değişkenini göstermesini isteyelim. Şimdi programımızın son haline bir göz atalım.
        public Form1()
        {
            CheckForIllegalCrossThreadCalls = false;
            InitializeComponent();
        }
        private void btnIsYap_Click(object sender, EventArgs e)
        {
            //IsYap();
            backgroundWorker1.RunWorkerAsync();
        }

        long toplam = 0;
        private void IsYap()
        {
            for (int i = 0; i <= 100000; i++)
            {
                toplam += i;
                this.Text = i.ToString();
                backgroundWorker1.ReportProgress(i);
            }
        }
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            IsYap();
        }
        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar1.Value = e.ProgressPercentage;
        }
        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            MessageBox.Show(toplam.ToString());
        }

Şimdi programımız kullanıcıya çok rahat tepki verebiliyor. (Formumu sürükleyebiliyorum, butonum aktif, TextBox atsam değer girebilirim vs..) Programın çalışmasını daha yavaş incelemek istiyorsak for döngümüzün içerisine System.Threading.Thread.Sleep(100); kodunu ekleyebiliriz. Thread sınıfının "Sleep" mototu mili saniye cinsinden bir parametre ister ve programı akışını, her döngüde belirttiğim mili saniye değerinde uyutur. 

Bunun gibi ve bu uygulamadan daha karmaşık uygulamalarda çok işimize yarayabilecek bir kontrolü inceledik arkadaşlar. Umarım yardımcı olabilmişimdir. 

Başka yazılarda görüşmek dileğiyle, hoşçakalın..

25 yorum:

  1. 10 numara anlatım olmuş tebrikler.

    YanıtlaSil
  2. Tebrikler çok güzel bir yazı olmuş.
    Teşekkürler.

    YanıtlaSil
  3. Helal olsun sana ya varya o kadar elzem bi durumdan kurtardınki beni Şu mevzu daki CheckForIllegalCrossThreadCalls = false şunu varya şunu ne kadar aradım bilemezsin

    YanıtlaSil
  4. Bu yorum yazar tarafından silindi.

    YanıtlaSil
  5. Saolasın çok açıklayıcı olmuş

    YanıtlaSil
  6. şimdiye kadar okuduğum en açık anlatım. teşekkürler ve tebrikler.

    YanıtlaSil
  7. CheckForIllegalCrossThreadCalls = false için çok teşekkürler. İyi çalışmalar.

    YanıtlaSil
  8. Çok teşekkürler, okuduğum en açık yazıydı sanırım :)

    YanıtlaSil
  9. Çok güzel bir açıklama olmuş. Teşekkür ederim.

    YanıtlaSil
  10. Merhaba,
    Anlatımınız için çok teşekür ederim. Gerçekten işime yaradı.
    Fakat progress bar yarıya gelmeden işlemim bitiyor. İşlem bittiğinde aynı oranda progress bar nasıl %100 dolu olmuş olacak caba bir ayarı varmıdır?
    Şimdiden teşekkürler.

    YanıtlaSil
  11. Bu tür durumlarda formdaki controlleri editlemek için

    this.Text = i.ToString();
    yerine

    myForm.Invoke(new Action(
    delegate()
    {
    this.Text = i.ToString();;
    }));


    kullanılmalı.
    CheckForIllegalCrossThreadCalls = false; yapmak oluşan hataları gözardı etmekten başka birşey değildir.

    YanıtlaSil
  12. Süper olmuş hoca, teşekkürler

    YanıtlaSil
  13. Cok akici ve anlasilir bir anlatim olmus. Tesekkurler.

    YanıtlaSil
  14. Gaffar Bey çok yararlı bir çalışma olduğunu belirtmek istiyorum. Ellerinize sağlık çok güzel bir şekilde açıklamışsınız. Fakat burada bir döngüyü ele alarak bir çalışma gerçekleştirilmiş. Döngüdeki i değerine göre progressbarın dolabildiğini gördük. Döngünün olmadığı durumlarda bu işlemi nasıl sağlayacağız? Örneğin uzun süre çalışacak bir SQL sorgu çalıştıracağız. Burada i gibi bir değişkenimiz yok. Burada nasıl bir yol izleriz?

    YanıtlaSil
  15. Çok teşekkürler açıklayıcı olmuş

    YanıtlaSil
  16. Bu arada su linkte checkforillegalcrossthtead kullaniminin yanlis olacagi söyleniyor. Bunun yerine control.invoke kullanilmaliymis.
    https://stackoverflow.com/questions/13345091/is-it-safe-just-to-set-checkforillegalcrossthreadcalls-to-false-to-avoid-cross-t

    YanıtlaSil
  17. Net, açıklayıcı ve anlaşılır bir makale olmuş. ellerinize ve yüreğinize sağlık.

    YanıtlaSil
  18. gayet iyi anlatmışsınız uygulamasını yapacağım

    YanıtlaSil
  19. Merhaba, anlatımınız çok yararlı, bu nesneyi çok iyi özetlemişsiniz, özellikle gif oynatma ile ilgili bu yazıdan daha faydalı bir yazı bulamadım. Elinize sağlık

    YanıtlaSil
  20. Çok profesyonel bir anlatım olmuş.

    YanıtlaSil