Skip to content

10. Çalıştırma iş parçacıkları

10.1. Thread sınıfı

Bir uygulama başlatıldığında, bu uygulama iş parçacığı olarak adlandırılan bir yürütme akışında çalışır. Bir iş parçacığını modelleyen .NET sınıfı, System.Threading.Thread sınıfıdır ve aşağıdaki gibi tanımlanmıştır:

Oluşturucular

Aşağıdaki örneklerde yalnızca [1,3] yapıcılarını kullanacağız. [1] yapıcı, [2] ve c.a.d imza biçimine sahip bir yöntemi parametre olarak kabul eder; bu yöntem, object türünde bir parametreye sahiptir ve sonuç döndürmez. [3] oluşturucu, [4], c.a.d imzalı, parametre içermeyen ve sonuç döndürmeyen bir yöntemi parametre olarak kabul eder.

Özellikler

Bazı yararlı özellikler:

  • CurrentThread iş parçacığı: Bu özelliği talep eden kodun bulunduğu iş parçacığına bir referans veren statik özellik
  • string Name: iş parçacığının adı
  • bool IsAlive: iş parçacığının çalışıp çalışmadığını gösterir.

Yöntemler

En sık kullanılan yöntemler şunlardır:

  • Start(), Start(object obj): iş parçacığının asenkron yürütülmesini başlatır; isterse object türünde bilgi aktararak.
  • Abort(), Abort(object obj): bir iş parçacığını zorla sonlandırmak için kullanılır
  • Join(): T2.Join'i çalıştıran T1 iş parçacığı, T2 iş parçacığı tamamlanana kadar bloke edilir. Belirli bir süre sonunda beklemeyi sonlandırmak için çeşitli seçenekler mevcuttur.
  • Sleep(int n): statik yöntem – bu yöntemi çalıştıran iş parçacığı, n milisaniye boyunca askıya alınır. Bu sırada işlemciyi kaybeder ve işlemci başka bir iş parçacığına verilir.

Bir sınıfın Main işlevinin yürütüldüğü ana yürütme iş parçacığının varlığını vurgulayan ilk uygulamaya bakalım:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        static void Main(string[] args) {
            // mevcut iş parçacığının başlatılması
            Thread main = Thread.CurrentThread;
            // görüntüleme
            Console.WriteLine("Thread courant : {0}", main.Name);
            // adın değiştirilmesi
            main.Name = "main";
            // doğrulama
            Console.WriteLine("Thread courant : {0}", main.Name);

            // sonsuz döngü
            while (true) {
                // görüntüleme
                Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
                // geçici durdurma
                Thread.Sleep(1000);
            }//while        
        }
    }
}
  • 8. satır: [main] yönteminin yürütüldüğü iş parçacığına bir referans alınır
  • 10-14. satırlar: iş parçacığının adı görüntülenir ve değiştirilir
  • 17-22. satırlar: Her saniye bir görüntüleme yapan bir döngü
  • 21. satır: [main] yönteminin yürütüldüğü iş parçacığı 1 saniye boyunca askıya alınır

Ekran çıktısı şu şekildedir:

1
2
3
4
5
6
7
8
Thread courant :
Thread courant : main
main : 04:19:00
main : 04:19:01
main : 04:19:02
main : 04:19:03
main : 04:19:04
^CAppuyez sur une touche pour continuer...
  • 1. satır: mevcut iş parçacığının adı yoktu
  • 2. satır: Artık bir adı var
  • 3-7. satırlar: Her saniye görüntülenen mesaj
  • 8. satır: Program, Ctrl-C tuşlarıyla sonlandırılır.

10.2. Çalıştırma iş parçacıklarının oluşturulması

Farklı yürütme iş parçacıklarında kod parçalarının "eşzamanlı" olarak yürütüldüğü uygulamalar olabilir. thread'lerin eşzamanlı olarak yürütüldüğü söylendiğinde, genellikle yanlış bir ifade kullanılmış olur. Hala sıklıkla olduğu gibi, makinede tek bir işlemci varsa, thread'ler bu işlemciyi paylaşır: her biri sırayla kısa bir süre (birkaç milisaniye) boyunca işlemciyi kullanır. Bu durum, yürütme paralelliği yanılsamasını yaratır. Bir thread’e ayrılan süre, varsayılan bir değere sahip olan ancak programlama yoluyla da belirlenebilen önceliği de dahil olmak üzere çeşitli faktörlere bağlıdır. Bir thread işlemciye sahip olduğunda, normalde kendisine ayrılan sürenin tamamı boyunca işlemciyi kullanır. Ancak, süre dolmadan önce işlemciyi serbest bırakabilir:

  • bir olayı bekleyerek (Wait, Join)
  • belirli bir süre boyunca uyku moduna geçerek (Sleep)
  1. Bir T iş parçacığı, öncelikle yukarıda sunulan yapıcılarından biri tarafından oluşturulur; örneğin:
Thread thread=new Thread(Start);

burada Start, aşağıdaki iki imzadan birine sahip bir yöntemdir:

void Start();
void Start(object obj);

Bir iş parçacığının oluşturulması, iş parçacığının çalıştırılmasına yol açmaz.

  1. T iş parçacığının yürütülmesi, T.Start() tarafından başlatılır: T'nin yapıcıya geçirilen Start yöntemi, daha sonra T iş parçacığı tarafından yürütülür. T.Start() komutunu yürüten program, T görevinin bitmesini beklemez: hemen bir sonraki komuta geçer. Böylece paralel olarak çalışan iki görev ortaya çıkar. Bu görevler, gerçekleştirilmesi gereken ortak işin ne aşamada olduğunu öğrenmek için genellikle birbirleriyle iletişim kurabilmelidir. Bu, iş parçacıklarının senkronizasyonu sorunudur.
  2. Başlatıldıktan sonra, T iş parçacığı bağımsız olarak çalışır. Yürütmekte olduğu Start yöntemi işini bitirdiğinde durur.
  3. T iş parçacığının sonlandırılmasını zorlayabiliriz:
    1. T.Abort(), T iş parçacığından sonlanmasını ister.
  4. Ayrıca T.Join() ile iş parçacığının çalışmasının bitmesini bekleyebiliriz. Burada bir engelleyici komut söz konusudur: bu komutu çalıştıran program, T iş parçacığı işini bitirene kadar engellenir. Bu bir senkronizasyon yöntemidir.

Aşağıdaki programı inceleyelim:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        public static void Main() {
            // mevcut iş parçacığını başlat
            Thread main = Thread.CurrentThread;
            // iş parçacığına ad atama
            main.Name = "Main";

            // çalışma iş parçacıkları oluşturma
            Thread[] tâches = new Thread[5];
            for (int i = 0; i < tâches.Length; i++) {
                // i iş parçacığı oluşturuluyor
                tâches[i] = new Thread(Affiche);
                // iş parçacığının adı belirleniyor
                tâches[i].Name =  i.ToString();
                // i iş parçacığının yürütülmesini başlatma
                tâches[i].Start();
            }

            // ana iş akışının sonu
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
        }

        public static void Affiche() {
            // çalışmanın başladığı gösterilir
            Console.WriteLine("Début d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now);
            // 1 saniye boyunca uyku moduna geçme
            Thread.Sleep(1000);
            // çalışma sonu gösteriliyor
            Console.WriteLine("Fin d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now);
        }
    }
}
  • 8-10. satırlar: [Main] yöntemini çalıştıran iş parçacığına bir ad verilir
  • 13-21. satırlar: 5 iş parçacığı oluşturulur ve çalıştırılır. İş parçacıklarının referansları, daha sonra geri alınabilmeleri için bir dizide saklanır. Her iş parçacığı, 27-35. satırlardaki Affiche yöntemini yürütür.
  • 20. satır: i numaralı iş parçacığı başlatılır. Bu işlem engelleyici değildir. i numaralı iş parçacığı, kendisini başlatan [Main] yönteminin iş parçacığıyla paralel olarak çalışacaktır.
  • 24. satır: [Main] yöntemini çalıştıran iş parçacığı sona erer.
  • 27-35. satırlar: [Affiche] yöntemi çıktı verir. Kendisini çalıştıran iş parçacığının adını ve yürütmenin başlangıç ve bitiş saatlerini görüntüler.
  • 31. satır: [Affiche] yöntemini çalıştıran tüm iş parçacıkları 1 saniye boyunca durur. İşlemci daha sonra işlemci bekleyen başka bir iş parçacığına verilir. Durma süresinin sonunda, durdurulan iş parçacığı işlemci için aday olur. Sırası geldiğinde işlemciyi alır. Bu, işlemci bekleyen diğer iş parçacıklarının önceliği gibi çeşitli faktörlere bağlıdır.

Sonuçlar şöyledir:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:30:44
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:30:44
Fin du thread Main à 10:30:44
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:30:45
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:30:45

Bu sonuçlar oldukça aydınlatıcıdır:

  • öncelikle, bir iş parçacığının yürütülmesinin başlatılmasının engelleyici olmadığı görülüyor. Main yöntemi, 5 iş parçacığının yürütülmesini paralel olarak başlattı ve onlardan önce kendi yürütülmesini tamamladı.
                // i iş parçacığının yürütülmesi başlatılıyor
                tâches[i].Start();

, tâches[i] iş parçacığının yürütülmesini başlatır, ancak bu işlem tamamlandıktan sonra, iş parçacığının yürütülmesinin bitmesini beklemeden yürütme hemen bir sonraki komutla devam eder.

  • Oluşturulan tüm iş parçacıkları Affiche yöntemini yürütmelidir. Yürütme sırası öngörülemez. Örnekte yürütme sırası, yürütme isteklerinin sırasını takip ediyor gibi görünse de, bundan genel bir sonuç çıkarılamaz. İşletim sisteminde burada 6 iş parçacığı ve bir işlemci bulunmaktadır. İşletim sistemi, kendi kurallarına göre işlemciyi bu 6 iş parçacığına dağıtacaktır.
  • Sonuçlarda Sleep yönteminin bir sonucu görülmektedir. Örnekte, Affiche yöntemini ilk olarak 0 numaralı iş parçacığı yürütmektedir. Çalıştırma başlangıç mesajı görüntülenir, ardından Sleep yöntemini çalıştırır ve bu yöntem iş parçacığını 1 saniye boyunca askıya alır. Böylece işlemciyi kaybeder ve işlemci başka bir iş parçacığı için kullanılabilir hale gelir. Örnekte, işlemciyi 1 numaralı iş parçacığının alacağı görülmektedir. İş parçacığı 1, diğer iş parçacıklarıyla aynı yolu izleyecektir. İş parçacığı 0’ın 1 saniyelik bekleme süresi sona erdiğinde, yürütmesi devam edebilir. Sistem ona işlemciyi tahsis eder ve Affiche yönteminin yürütmesini tamamlayabilir.

Main yöntemini şu komutlarla sonlandırmak üzere programımızı değiştirelim:


            // işlem sonu
            Console.WriteLine("Fin du thread " + main.Name);
            // tüm iş parçacıkları durdurulur
Environment.Exit(0);

Yeni programın çalıştırılması şu sonuçları vermektedir:

1
2
3
4
5
6
Début d'exécution de la méthode Affiche dans le Thread 0 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:33:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:33:18
Fin du thread Main à 10:33:18
  • 1-5. satırlar: Main işlevi tarafından oluşturulan iş parçacıkları çalışmaya başlar ve 1 saniye boyunca durdurulur
  • 6. satır: [Main] iş parçacığı işlemciyi geri alır ve şu komutu yürütür:
        Environment.Exit(0);

Bu komut, yalnızca Main iş parçacığını değil, uygulamanın tüm iş parçacıklarını durdurur.

Main yöntemi, kendi oluşturduğu iş parçacıklarının yürütülmesinin tamamlanmasını beklemek istiyorsa, Thread sınıfındaki Join yöntemini kullanabilir:


        public static void Main() {
...
            // tüm iş parçacıklarının tamamlanmasını bekleme
            for (int i = 0; i < tâches.Length; i++) {
                // i iş parçacığının yürütülmesinin bitmesini bekleme
                tâches[i].Join();
            }
            // ana iş akışının sonu
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
  • 6. satır: [Main] iş parçacığı, her bir iş parçacığını bekler. Önce 1 numaralı iş parçacığını, ardından 2 numaralı iş parçacığını vb. beklerken bloke olur. Sonunda, 2-5. satırlardaki döngüden çıktığında, başlattığı 5 iş parçacığı da tamamlanmış olur.

Böylece şu sonuçlar elde edilir:

Début d'exécution de la méthode Affiche dans le Thread 0 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 1 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 2 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 3 : 10:35:18
Début d'exécution de la méthode Affiche dans le Thread 4 : 10:35:18
Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:35:19
Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:35:19
Fin du thread Main à 10:35:19
  • 11. satır: [Main] iş parçacığı, başlattığı iş parçacıkları tamamlandıktan sonra sona ermiştir.

10.3. İş parçacıklarının önemi

Artık varsayılan bir iş parçacığının varlığını ortaya çıkardığımıza göre (Main yöntemini çalıştıran iş parçacığı) ve başka iş parçacıkları nasıl oluşturulacağını öğrendiğimize göre, iş parçacıklarının bizim için ne kadar önemli olduğuna ve bunları burada neden ele aldığımıza değinelim. İş parçacıklarının kullanımına çok uygun bir uygulama türü vardır: İnternet’teki istemci-sunucu uygulamaları. Bunları bir sonraki bölümde ele alacağız. Bir internet istemci-sunucu uygulamasında, S1 makinesinde bulunan bir sunucu, C1, C2, ..., Cn gibi uzak makinelerde bulunan istemcilerin isteklerine yanıt verir.

Her gün bu şemaya uyan internet uygulamalarını kullanıyoruz: web hizmetleri, e-posta, forumlara erişim, dosya aktarımı... Yukarıdaki şemada, S1 sunucusu Ci istemcilerine aynı anda hizmet vermelidir. Müşterilerine dosya ileten bir FTP (Dosya Aktarım Protokolü) sunucusunu örnek alırsak, bir dosya aktarımının bazen birkaç dakika sürebileceğini biliriz. Elbette bir müşterinin bu süre boyunca sunucuyu tek başına işgal etmesi söz konusu olamaz. Genellikle yapılan şey, sunucunun müşteri sayısı kadar yürütme iş parçacığı oluşturmasıdır. Her iş parçacığı, belirli bir müşteriyle ilgilenmekle görevlendirilir. İşlemci, makinedeki tüm aktif iş parçacıkları arasında döngüsel olarak paylaşıldığından, sunucu her bir müşteriyle biraz zaman geçirir ve böylece hizmetin eşzamanlılığını sağlar.

Uygulamada, sunucu sınırlı sayıda iş parçacığı içeren bir iş parçacığı havuzu kullanır; örneğin 50 adet. 51. müşteri ise beklemek zorunda kalır.

10.4. İş parçacıkları arasında bilgi alışverişi

Önceki örneklerde, bir iş parçacığı şu şekilde başlatılıyordu:

Thread t=new Thread(Run);

burada Run, aşağıdaki imzaya sahip bir yöntemdi:

void Run();

Aşağıdaki imza da kullanılabilir:

void Run(object obj);

Bu, başlatılan iş parçacığına bilgi aktarılmasını sağlar. Dolayısıyla

t.Start(obj1);

komutu, t iş parçacığını başlatır; bu iş parçacığı da, yapı gereği kendisine atanan Run yöntemini, obj1 etkin parametresini aktararak yürütür. İşte bir örnek:


using System;
using System.Threading;

namespace Chap8 {
    class Program4 {
        public static void Main() {
            // geçerli iş parçacığını başlat
            Thread main = Thread.CurrentThread;
            // İş parçacığına bir ad veriliyor
            main.Name = "Main";

            // çalışma iş parçacıkları oluşturuluyor
            Thread[] tâches = new Thread[5];
            Data[] data = new Data[5];
            for (int i = 0; i < tâches.Length; i++) {
                // i iş parçacığı oluşturuluyor
                tâches[i] = new Thread(Sleep);
                // iş parçacığının adı belirleniyor
                tâches[i].Name = i.ToString();
                // i iş parçacığının yürütülmesini başlatma
                tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
            }
            // tüm iş parçacıklarının tamamlanmasını bekleme
            for (int i = 0; i < tâches.Length; i++) {
                // i iş parçacığının yürütülmesinin bitmesini bekleme
                tâches[i].Join();
                // sonuç görüntülenir
                Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
                    tâches[i].Name,data[i].Début,data[i].Durée,data[i].Fin,(data[i].Fin-data[i].Début));
            }        
            // işlem sonu
            Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
        }

        public static void Sleep(object infos) {
            // parametre alınır
            Data data = (Data)infos;
            // Durée saniye boyunca uyku moduna geçme
            Thread.Sleep(data.Durée*1000);
            // çalışma sonu
            data.Fin = DateTime.Now;
        }
    }

    internal class Data {
        // çeşitli bilgiler
        public DateTime Début { get; set; }
        public int Durée { get; set; }
        public DateTime Fin { get; set; }
    }
}
  • 45-50. satırlar: iş parçacıklarına aktarılan [Data] türündeki bilgi:
    • Début: iş parçacığının yürütülmeye başladığı saat – başlatıcı iş parçacığı tarafından belirlenir
    • Durée: başlatılan iş parçacığı tarafından yürütülen Sleep işleminin süresi (saniye cinsinden) – başlatıcı iş parçacığı tarafından belirlenir
    • Fin: iş parçacığının yürütülmeye başladığı zaman - başlatılan iş parçacığı tarafından belirlenir
  • satır 35-43: İş parçacıkları tarafından yürütülen Sleep yöntemi, void Sleep(object obj) imzasına sahiptir. obj etki parametresi, 45. satırda tanımlanan [Data] türünde olacaktır.
  • 15-22. satırlar: 5 iş parçacığının oluşturulması
  • 17. satır: Her iş parçacığı, 35. satırdaki Sleep yöntemiyle ilişkilendirilir
  • 21. satır: [Data] türünde bir nesne, iş parçacığını başlatan Start yöntemine aktarılır. Bu nesneye, iş parçacığının yürütülmeye başladığı saat ile uyku süresinin saniye cinsinden değeri kaydedilmiştir. Bu nesne, 14. satırdaki dizide saklanır.
  • 24-30. satırlar: [Main] iş parçacığı, başlattığı tüm iş parçacıklarının sona ermesini bekler.
  • 28-29. satırlar: [Main] iş parçacığı, i numaralı iş parçacığından data[i] nesnesini alır ve içeriğini görüntüler.
  • satır 35-42: iş parçacıkları tarafından yürütülen Sleep yöntemi
  • 37. satır: [Data] türündeki parametre alınır
  • 39. satır: Parametrenin Durée alanı, Sleep'in süresini belirlemek için kullanılır
  • 41. satır: Parametrenin Fin alanı başlatıldı

Çalıştırma sonuçları şöyledir:

1
2
3
4
5
6
Thread 0 terminé : début 11:18:50, durée programmée 1 s, fin 11:18:51, durée effective 00:00:01.0156250
Thread 1 terminé : début 11:18:50, durée programmée 2 s, fin 11:18:52, durée effective 00:00:02
Thread 2 terminé : début 11:18:50, durée programmée 3 s, fin 11:18:53, durée effective 00:00:03
Thread 3 terminé : début 11:18:50, durée programmée 4 s, fin 11:18:54, durée effective 00:00:04
Thread 4 terminé : début 11:18:50, durée programmée 5 s, fin 11:18:55, durée effective 00:00:05
Fin du thread Main à 11:18:55

Bu örnek, iki iş parçacığının birbirleriyle bilgi alışverişinde bulunabileceğini göstermektedir:

  • başlatan iş parçacığı, başlatılan iş parçacığına bilgi vererek onun yürütülmesini kontrol edebilir
  • başlatılan iş parçacığı, sonuçları başlatıcı iş parçacığına geri gönderebilir.

Başlatılan iş parçacığının beklediği sonuçların ne zaman hazır olduğunu bilmesi için, başlatılan iş parçacığının sona erdiği konusunda bilgilendirilmesi gerekir. Burada, Join yöntemini kullanarak iş parçacığının sona ermesini beklemiştir. Aynı şeyi yapmanın başka yolları da vardır. Bunlara daha sonra değineceğiz.

10.5. Paylaşılan kaynaklara eşzamanlı erişim

10.5.1. Senkronize edilmemiş eşzamanlı erişim

İş parçacıkları arasında bilgi alışverişi ile ilgili bölümde, bilgi alışverişi yalnızca iki iş parçacığı arasında ve çok belirli zamanlarda gerçekleşiyordu. Burada klasik bir parametre aktarımı söz konusuydu. Bir bilginin, aynı anda okumak veya güncellemek isteyebilecek birden fazla iş parçacığı tarafından paylaşıldığı başka durumlar da vardır. Bu durumda, bu bilginin bütünlüğü sorunu ortaya çıkar. Paylaşılan bilginin, I1, I2, ... In gibi çeşitli bilgileri içeren bir S yapısı olduğunu varsayalım.

  • T1 iş parçacığı S yapısını güncellemeye başlar: I1 alanını değiştirir ve S yapısının güncellemesini tamamlayamadan kesintiye uğrar
  • İşlemciyi devralan T2 iş parçacığı, karar vermek için S yapısını okur. Yapıyı kararsız bir durumda okur: bazı alanlar güncellenmiştir, bazıları ise değildir.

Bu duruma, paylaşılan bir kaynağa (burada S yapısı) erişim denir ve genellikle yönetilmesi oldukça zordur. Ortaya çıkabilecek sorunları açıklamak için şu örneği ele alalım:

  • Bir uygulama n adet iş parçacığı oluşturacak; burada n, parametre olarak verilir
  • paylaşılan kaynak, oluşturulan her iş parçacığı tarafından artırılması gereken bir sayacıdır
  • uygulamanın sonunda sayaçın değeri görüntülenir. Dolayısıyla sonuçta n değeri bulunmalıdır.

Program şu şekildedir:


using System;
using System.Threading;

namespace Chap8 {
    class Program {

        // sınıf değişkenleri
        static int cptrThreads = 0;    // iş parçacığı sayacı

        //ana iş parçacığı
        public static void Main(string[] args) {
            // kullanım kılavuzu
            const string syntaxe = "pg nbThreads";
            const int nbMaxThreads = 100;

            // argüman sayısı kontrolü
            if (args.Length != 1) {
                // hata
                Console.WriteLine(syntaxe);
                // durdurma
                Environment.Exit(1);
            }
            // argüman kalitesini kontrol et
            int nbThreads = 0;
            bool erreur = false;
            try {
                nbThreads = int.Parse(args[0]);
                if (nbThreads < 1 || nbThreads > nbMaxThreads)
                    erreur = true;
            } catch {
                // hata
                erreur = true;
            }
            // hata mı?
            if (erreur) {
                // hata
                Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
                // son
                Environment.Exit(2);
            }
            // iş parçacıklarının oluşturulması ve üretilmesi
            Thread[] threads = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                threads[i] = new Thread(Incrémente);
                // adlandırma
                threads[i].Name = "" + i;
                // başlatma
                threads[i].Start();
            }//for
            // iş parçacıklarının bitmesini bekleme
            for (int i = 0; i < nbThreads; i++) {
                threads[i].Join();
            }
            // sayaç görüntüleme
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }

        public static void Incrémente() {
            // iş parçacığı sayacını artırır
            // sayaç okuma
            int valeur = cptrThreads;
            // izleme
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
            // bekleme
            Thread.Sleep(1000);
            // sayaç artırma
            cptrThreads = valeur + 1;
            // takip
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
        }
    }
}

Daha önce incelediğimiz iş parçacığı oluşturma kısmına fazla değinmeyeceğiz. Bunun yerine, her iş parçacığı tarafından 8. satırdaki statik sayaç cptrThreads'i artırmak için kullanılan 59. satırdaki Incrémente yöntemine odaklanalım.

  1. 62. satır: sayaç okunur
  2. 66. satır: iş parçacığı 1 saniye durur. Dolayısıyla işlemciyi kaybeder
  3. 68. satır: Sayaç artırılır
  1. adım, iş parçacığını işlemciyi kaybetmeye zorlamak için vardır. İşlemci, başka bir iş parçacığına verilecektir. Uygulamada, bir iş parçacığının sayacı okuyacağı an ile sayacı artıracağı an arasında kesintiye uğramayacağına dair hiçbir garanti yoktur. cptrThreads++ yazılsa ve böylece tek bir komut izlenimi verse bile, sayacın değerini okuduğumuz an ile 1 artırılmış değerini yazdığımız an arasında işlemciyi kaybetme riski mevcuttur. Nitekim, üst düzey cptrThreads++ işlemi, işlemci düzeyinde birkaç temel komutun gerçekleştirilmesini gerektirir. Dolayısıyla, bir saniyelik uyku aşaması (2. adım), bu riski sistematik hale getirmek için vardır.

5 iş parçacığıyla elde edilen sonuçlar şunlardır:

A 12:00:56, le thread 3  a lu la valeur du compteur : 0
A 12:00:56, le thread 2  a lu la valeur du compteur : 0
A 12:00:56, le thread 1  a lu la valeur du compteur : 0
A 12:00:56, le thread 0  a lu la valeur du compteur : 0
A 12:00:56, le thread 4  a lu la valeur du compteur : 0
A 12:00:57, le thread 3  a écrit la valeur du compteur : 1
A 12:00:57, le thread 2  a écrit la valeur du compteur : 1
A 12:00:57, le thread 1  a écrit la valeur du compteur : 1
A 12:00:57, le thread 0  a écrit la valeur du compteur : 1
A 12:00:57, le thread 4  a écrit la valeur du compteur : 1
Nombre de threads générés : 1

Bu sonuçlara bakıldığında neler olduğu açıkça görülmektedir:

  • 1. satır: İlk iş parçacığı sayacı okur. Değerin 0 olduğunu görür. 1 saniye durur ve bu nedenle işlemciyi kaybeder
  • 2. satır: İkinci bir iş parçacığı işlemciyi devralır ve o da sayaç değerini okur. Önceki iş parçacığı henüz değeri artırmadığı için değer hâlâ 0'dır. O da 1 saniye durur ve sırayla işlemciyi kaybeder.
  • 1-5. satırlar: 1 saniye içinde 5 iş parçacığının hepsi sırayla geçip 0 değerini okuma fırsatı bulur.
  • 6-10. satırlar: Birbiri ardına uyanacakları zaman, okudukları 0 değerini artıracak ve sayaca 1 değerini yazacaklardır; bu durum, 11. satırdaki ana program (Main) tarafından da doğrulanmaktadır.

Sorun nereden kaynaklanıyor? İkinci iş parçacığı, birinci iş parçacığının penceredeki sayacı güncelleme işini tamamlamadan kesintiye uğraması nedeniyle yanlış bir değer okudu. Bu da bizi bir programdaki kritik kaynak ve kritik bölüm kavramlarına götürür:

  • kritik kaynak, aynı anda yalnızca bir iş parçacığı tarafından tutulabilen bir kaynaktır. Burada kritik kaynak, sayacıdır.
  • Bir programın kritik bölümü, bir iş parçacığının yürütme akışında kritik bir kaynağa eriştiği komut dizisidir. Bu kritik bölüm sırasında kaynağa erişimin sadece o iş parçacığına ait olduğundan emin olunmalıdır.

Örneğimizde, kritik bölüm, sayacın okunması ile yeni değerinin yazılması arasında yer alan koddur:


            // sayaç okuma
            int valeur = cptrThreads;
            // bekleme
            Thread.Sleep(1000);
            // sayaç artırma
cptrThreads = valeur + 1;

Bu kodu çalıştırmak için, bir iş parçacığının tek başına olduğundan emin olunmalıdır. İş parçacığı kesintiye uğrayabilir, ancak bu kesinti sırasında başka bir iş parçacığının aynı kodu çalıştırmasına izin verilmemelidir. .NET platformu, kodun kritik bölümlerine tekil erişimi sağlamak için çeşitli araçlar sunar. Şimdi bunlardan birkaçına göz atalım.

10.5.2. lock yorumu

lock deyimi, kritik bir bölümü şu şekilde sınırlandırmaya olanak tanır:

lock(obj){section critique}

obj, kritik bölümü çalıştıran tüm iş parçacıkları tarafından görülebilen bir nesne referansı olmalıdır. lock cümlesi, kritik bölümün aynı anda yalnızca bir iş parçacığı tarafından çalıştırılmasını sağlar. Önceki örnek şu şekilde yeniden yazılır:


using System;
using System.Threading;

namespace Chap8 {
    class Program2 {

        // sınıf değişkenleri
        static int cptrThreads = 0;    // iş parçacığı sayacı
        static object synchro = new object(); // senkronizasyon nesnesi

        //ana iş parçacığı
        public static void Main(string[] args) {
    ...
            // iş parçacıklarının bitmesini bekleme
            Thread.CurrentThread.Name = "Main";
            for (int i = nbThreads - 1; i >= 0; i--) {
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
                threads[i].Join();
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a été prévenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
            }
            // sayaç görüntüleme
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }

        public static void Incrémente() {
            // iş parçacığı sayacını artırır
            // sayaça özel erişim talep ediliyor
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name);
            lock (synchro) {
                // sayaç okuma
                int valeur = cptrThreads;
                // izleme
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
                // bekleme
                Thread.Sleep(1000);
                // sayaç artırma
                cptrThreads = valeur + 1;
                // izleme
                Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
            }
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitté la section critique", DateTime.Now, Thread.CurrentThread.Name);
        }
    }
}
  • 9. satır: synchro, tüm iş parçacıklarının senkronizasyonunu sağlayacak nesnedir.
  • 16-23. satırlar: [Main] yöntemi, iş parçacıklarını oluşturulma sırasının tersi sırayla bekler.
  • 29-40. satırlar: Incrémente yönteminin kritik bölümü, lock cümlesi ile sınırlandırılmıştır.

3 iş parçacığıyla elde edilen sonuçlar şunlardır:

A 09:37:09, le thread 0 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 0 a lu la valeur du compteur : 0
A 09:37:09, le thread 1 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread 2 attend l'autorisation d'entrer dans la section critique
A 09:37:09, le thread Main attend la fin du thread 2
A 09:37:10, le thread 0 a écrit la valeur du compteur : 1
A 09:37:10, le thread 1 a lu la valeur du compteur : 1
A 09:37:10, le thread 0 a quitté la section critique
A 09:37:11, le thread 1 a écrit la valeur du compteur : 2
A 09:37:11, le thread 1 a quitté la section critique
A 09:37:11, le thread 2 a lu la valeur du compteur : 2
A 09:37:12, le thread 2 a écrit la valeur du compteur : 3
A 09:37:12, le thread 2 a quitté la section critique
A 09:37:12, le thread Main a été prévenu de la fin du thread 2
A 09:37:12, le thread Main attend la fin du thread 1
A 09:37:12, le thread Main a été prévenu de la fin du thread 1
A 09:37:12, le thread Main attend la fin du thread 0
A 09:37:12, le thread Main a été prévenu de la fin du thread 0
Nombre de threads générés : 3
  • 0 numaralı iş parçacığı kritik bölüme ilk giren iş parçacığıdır: 1., 2., 6. ve 8. satırlar
  • diğer iki iş parçacığı, iş parçacığı 0 kritik bölümden çıkana kadar bloke edilecek: satır 3 ve 4
  • ardından 1 numaralı iş parçacığı devreye girer: 7, 9, 10. satırlar
  • ardından iş parçacığı 2 devreye girer: satır 11, 12, 13
  • 14. satır: İş parçacığı 2'nin bitmesini bekleyen Main iş parçacığı bilgilendirilir
  • 15. satır: Main iş parçacığı artık iş parçacığı 1'in bitmesini bekliyor. İş parçacığı 1 çoktan tamamlandı. Main iş parçacığı hemen bilgilendirilir, 16. satır.
  • 17-18. satırlar: Aynı süreç, 0 numaralı iş parçacığı için de geçerlidir
  • 19. satır: İş parçacığı sayısı doğrudur

10.5.3. Mutex sınıfı

System.Threading.Mutex sınıfı da kritik bölümleri sınırlamaya olanak tanır. Görünürlük açısından lock cümlesinden farklıdır:

  • lock yordamı, aynı uygulamadaki iş parçacıklarını senkronize etmeye yarar
  • Mutex sınıfı ise farklı uygulamalardaki iş parçacıklarını senkronize etmeye olanak tanır.

Aşağıdaki oluşturucu ve yöntemleri kullanacağız:

public Mutex()
bir Mutex M oluşturur
public bool WaitOne()
T1 iş parçacığı, M.WaitOne() işlemini yürütürken M senkronizasyon nesnesinin sahipliğini talep eder. Eğer Mutex M nesnesi hiçbir iş parçacığı tarafından tutulmuyorsa (başlangıçta durum böyledir), bu nesne, talep eden T1 iş parçacığına "verilir". Biraz sonra, T2 iş parçacığı aynı işlemi yaparsa, bu iş parçacığı bloke olur. Zira bir Mutex nesnesi yalnızca bir iş parçacığına ait olabilir. T1 iş parçacığı, elinde tuttuğu Mutex M'yi serbest bıraktığında, bu iş parçacığının engeli kaldırılacaktır. Böylece, Mutex M'yi bekleyen birden fazla iş parçacığı engellenebilir.
public void ReleaseMutex()
T1 iş parçacığı, M.ReleaseMutex() işlemini gerçekleştirirken Mutex M'nin sahipliğini bırakır. T1 iş parçacığı işlemciyi kaybettiğinde, sistem, işlemciyi Mutex M’yi bekleyen iş parçacıklarından birine verebilir. Bunlardan sadece biri sırayla işlemciyi alabilir; M’yi bekleyen diğerleri ise bloke kalır

Bir Mutex M, paylaşılan bir kaynak R'ye erişimi yönetir. Bir iş parçacığı, M.WaitOne() ile R kaynağını talep eder ve M.ReleaseMutex() ile geri verir. Aynı anda yalnızca tek bir iş parçacığı tarafından yürütülmesi gereken kritik bir kod bölümü, paylaşılan bir kaynaktır. Kritik bölümün yürütülmesinin senkronizasyonu şu şekilde yapılabilir:

M.WaitOne();
// buraya yalnızca iş parçacığı girebilir
// kritik bölüm
....
M.ReleaseMutex();

burada M, bir Mutex nesnesidir. Artık gereksiz hale gelen bir Mutex nesnesini serbest bırakmayı unutmamak gerekir; aksi takdirde, serbest bırakılmayan Mutex nesnesini bekleyen iş parçacıkları işlemciye asla erişemeyecektir.

Az önce gördüklerimizi önceki örnekte uygulamaya koyarsak, uygulamamız şu şekilde olur:


using System;
using System.Threading;

namespace Chap8 {
    class Program3 {

        // sınıf değişkenleri
        static int cptrThreads = 0;    // iş parçacığı sayacı
        static Mutex synchro = new Mutex(); // senkronizasyon nesnesi

        //ana iş parçacığı
        public static void Main(string[] args) {
    ...
        }

        public static void Incrémente() {
....
            synchro.WaitOne();
            try {
...
            } finally {
...
                synchro.ReleaseMutex();
            }
        }
    }
}
  • 9. satır: İş parçacıklarının senkronizasyon nesnesi artık bir Mutex'tir.
  • 18. satır: kritik bölümün başlangıcı – buraya yalnızca bir iş parçacığı girmelidir. Mutex synchro serbest kalana kadar bekleriz.
  • satır 33: istisna olsun ya da olmasın, bir Mutex'in her zaman serbest bırakılması gerektiğinden, kritik bölümü bir try / finally ile yöneterek Mutex'i finally içinde serbest bırakıyoruz.
  • 23. satır: Kritik bölüm geçildikten sonra Mutex serbest bırakılır.

Elde edilen sonuçlar öncekilerle aynıdır.

10.5.4. AutoResetEvent sınıfı

Bir AutoResetEvent nesnesi, önceki iki araç olan lock ve Mutex gibi, aynı anda yalnızca bir iş parçacığının geçişine izin veren bir bariyerdir. Bir AutoResetEvent nesnesi şu şekilde oluşturulur:

AutoResetEvent barrière=new AutoresetEvent(bool état);

état Boole değeri, bariyerin kapalı (false) veya açık (true) durumunu gösterir. Bariyeri geçmek isteyen bir iş parçacığı bunu şu şekilde belirtir:

barrière.WaitOne();
  • Bariyer açıksa, iş parçacığı geçer ve bariyer arkasında yeniden kapatılır. Bekleyen birden fazla iş parçacığı varsa, sadece bir tanesinin geçeceği garanti edilir.
  • Bariyer kapalıysa, iş parçacığı engellenir. Zamanı geldiğinde başka bir iş parçacığı bariyeri açacaktır. Bu zaman tamamen ele alınan soruna bağlıdır. Bariyer şu işlemle açılacaktır:
barrière.Set(); 

Bazen bir iş parçacığı bir bariyeri kapatmak isteyebilir. Bunu şu işlemle yapabilir:

barrière.Reset(); 

Önceki örnekte, Mutex nesnesini AutoResetEvent türünde bir nesneyle değiştirirsek, kod şu şekilde olur:


using System;
using System.Threading;

namespace Chap8 {
    class Program4 {

        // sınıf değişkenleri
        static int cptrThreads = 0;    // iş parçacığı sayacı
        static EventWaitHandle synchro = new AutoResetEvent(false); // senkronizasyon nesnesi

        //ana iş parçacığı
        public static void Main(string[] args) {
....
            // kritik bölümün bariyeri açılıyor
            Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrière de la section critique", DateTime.Now, Thread.CurrentThread.Name);
            synchro.Set();
            // iş parçacıklarının bitmesini bekleme
...
            // sayaç görüntüleme
            Console.WriteLine("Nombre de threads générés : " + cptrThreads);
        }

        public static void Incrémente() {
            // iş parçacığı sayacını artırma
            // sayaç için özel erişim talep ediliyor
...
            synchro.WaitOne();
            try {
...
            } finally {
                // kaynak serbest bırakılıyor
...
                synchro.Set();
            }
        }
    }
}
  • 9. satır: bariyer kapalı olarak oluşturulur. Bariyer, 16. satırdaki Main iş parçacığı tarafından açılacaktır.
  • 27. satır: İş parçacığı sayacını artırmakla görevli iş parçacığı, kritik bölüme girmek için izin ister. Farklı iş parçacıkları kapalı bariyerin önünde birikecektir. Main iş parçacığı bariyeri açtığında, bekleyen iş parçacıklarından biri geçecektir.
  • 33. satır: İşini bitirdiğinde, bariyeri yeniden açarak başka bir iş parçacığının girmesine izin verir.

Öncekilerle benzer sonuçlar elde edilir.

10.5.5. Interlocked sınıfı

Interlocked sınıfı, bir işlem grubunu atomik hale getirmeye olanak tanır. atomique işlem grubunda, ya tüm işlemler grubu yürüten iş parçacığı tarafından yürütülür ya da hiçbiri yürütülmez. Bazı işlemlerin yürütülüp diğerlerinin yürütülmediği bir durum söz konusu olmaz. lock, Mutex, AutoResetEvent senkronizasyon nesnelerinin hepsinin amacı, atomique'i bir işlem grubu haline getirmektir. Bu sonuç, iş parçacıklarının bloke edilmesi pahasına elde edilir. Interlocked sınıfı, basit ancak oldukça sık tekrarlanan işlemlerde iş parçacıklarının kilitlenmesini önlemeye olanak tanır. Interlocked sınıfı aşağıdaki statik yöntemleri sunar:

Image

Increment yönteminin imzası şöyledir:

public static int Increment(ref int location);

Bu yöntem, location parametresini 1 artırır. İşlemin atomique garantisi vardır.

İş parçacığı sayma programımız şu şekilde olabilir:


using System;
using System.Threading;

namespace Chap8 {
    class Program5 {

        // sınıf değişkenleri
        static int cptrThreads = 0;    // iş parçacığı sayacı

        //ana iş parçacığı
        public static void Main(string[] args) {
...
        }

        public static void Incrémente() {
            // iş parçacığı sayacını artırır
            Interlocked.Increment(ref cptrThreads);
        }
    }
}
  • 17. satır: İş parçacığı sayacı atomik bir şekilde artırılır.

10.6. Birden çok paylaşılan kaynağa eşzamanlı erişim

10.6.1. Bir örnek

Önceki örneklerimizde, farklı iş parçacıkları tarafından tek bir kaynak paylaşılıyordu. Kaynak sayısı artarsa ve bunlar birbirine bağımlı hale gelirse durum karmaşıklaşabilir. Özellikle bir kilitlenme durumu ortaya çıkabilir. deadlock olarak da adlandırılan bu durum, iki iş parçacığının birbirini beklediği durumdur. Zaman içinde birbirini izleyen aşağıdaki eylemleri ele alalım:

  • bir T1 iş parçacığı, R1 adlı paylaşılan kaynağa erişmek için M1 adlı bir Mutex'in mülkiyetini alır
  • T2 iş parçacığı, R2 paylaşımlı kaynağına erişmek için M2 Mutex'inin sahipliğini alır
  • T1 iş parçacığı, M2 Mutex'ini talep eder. Engellenir.
  • T2 iş parçacığı, M1 Mutex'ini talep eder. Engellenir.

Burada, T1 ve T2 iş parçacıkları birbirlerini bekliyor. Bu durum, iş parçacıklarının iki paylaşımlı kaynağa ihtiyaç duyması halinde ortaya çıkar: M1 Mutex'i tarafından kontrol edilen R1 kaynağı ve M2 Mutex'i tarafından kontrol edilen R2 kaynağı. Olası bir çözüm, tek bir Mutex (M) kullanarak her iki kaynağa aynı anda erişim talep etmektir. Ancak bu, örneğin pahalı bir kaynağın uzun süreli işgaline yol açıyorsa her zaman mümkün olmayabilir. Başka bir çözüm ise, M1'e sahip olan ve M2'i alamayan bir iş parçacığının, karşılıklı kilitlenmeyi önlemek için M1'i serbest bırakmasıdır.

  1. Elimizde, bazı iş parçacıklarının veri yazdığı (yazarlar) ve diğerlerinin bu verileri okuduğu (okuyucular) bir dizi vardır.
  2. Yazarlar birbirleriyle eşittir ancak birbirlerini dışlarlar: Bir seferde yalnızca bir yazar verilerini diziye yazabilir.
  3. Okuyucular birbirine eşittir ancak birbirini dışlar: Bir seferde yalnızca bir okuyucu diziye yerleştirilen verileri okuyabilir.
  4. Bir okuyucu, ancak bir yazıcı diziye veri eklediğinde dizideki verileri okuyabilir; bir yazıcı ise, dizideki veriler bir okuyucu tarafından okunduktan sonra diziye yeni veri ekleyebilir.

İki tür paylaşılan kaynak ayırt edilebilir:

  • yazma tablosu: aynı anda yalnızca bir yazar erişebilir.
  • okunabilir tablo: aynı anda yalnızca bir okuyucu erişebilir.

ve bu kaynakların kullanım sırası:

  • bir okuyucu her zaman bir yazıcıdan sonra gelmelidir.
  • Bir yazar, ilk sefer hariç, her zaman bir okuyucudan sonra gelmelidir.

Bu iki kaynağa erişim, iki adet AutoResetEvent tipi bariyerle kontrol edilebilir:

  • peutEcrire bariyeri, yazıcıların tabloya erişimini kontrol edecektir.
  • peutLire bariyeri, okuyucuların panoya erişimini kontrol edecektir.
  • peutEcrire bariyeri başlangıçta açık olarak oluşturulacak ve böylece ilk okuyucunun geçişine izin verirken diğerlerini engelleyecektir.
  • peutLire bariyeri başlangıçta kapalı olarak oluşturulacak ve tüm okuyucuları engelleyecektir.
  • Bir yazar işini bitirdiğinde, bir okuyucunun girmesine izin vermek için peutLire bariyerini açacaktır.
  • Bir okuyucu işini bitirdiğinde, peutEcrire bariyerini açarak bir yazarın girmesine izin verecektir.

Bu olay tabanlı senkronizasyonu gösteren program şöyledir:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        // okuma ve yazma iş parçacıklarının kullanımı
        // senkronizasyon olaylarının kullanımını gösterir


        // sınıf değişkenleri
        static int[] data = new int[3];    // okuma iş parçacıkları ile yazma iş parçacıkları arasında paylaşılan kaynak
        static Random objRandom = new Random(DateTime.Now.Second);    // bir rasgele sayı üreteci
        static AutoResetEvent peutLire;    // data'nın içeriğinin okunabileceğini belirtir
        static AutoResetEvent peutEcrire;    // data içeriğinin yazılabileceğini belirtir

        //ana iş parçacığı
        public static void Main(string[] args) {

            // oluşturulacak iş parçacığı sayısı
            const int nbThreads = 2;

            // bayrakların başlatılması
            peutLire = new AutoResetEvent(false);    // henüz okunamaz
            peutEcrire = new AutoResetEvent(true);    // zaten yazılabilir

            // okuma iş parçacıklarının oluşturulması
            Thread[] lecteurs = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                lecteurs[i] = new Thread(Lire);
                lecteurs[i].Name = "L" + i.ToString();
                // başlatma
                lecteurs[i].Start();
            }

            // yazma iş parçacıklarının oluşturulması
            Thread[] écrivains = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                écrivains[i] = new Thread(Ecrire);
                écrivains[i].Name = "E" + i.ToString();
                // başlatma
                écrivains[i].Start();
            }

            //işin sonu
            Console.WriteLine("Fin de Main...");
        }

        // tablonun içeriğini okuma
        public static void Lire() {
...
        }

        // tabloya yazma
        public static void Ecrire() {
....
        }
    }
}
  • 11. satır: data dizisi, okuyucu ve yazıcı iş parçacıkları arasında paylaşılan kaynaktır. Bu dizi, okuyucu iş parçacıkları tarafından okuma, yazıcı iş parçacıkları tarafından yazma amacıyla paylaşılır.
  • 13. satır: peutLire nesnesi, okuyucu iş parçacıklarına data dizisini okuyabileceklerini bildirmek için kullanılır. Bu nesne, data dizisini dolduran yazıcı iş parçacığı tarafından true olarak ayarlanır. Bu nesne, 23. satırda false olarak başlatılır. Bir yazma iş parçacığının, peutLire olayını vrai'e aktarmadan önce diziyi doldurması gerekir.
  • 14. satır: peutEcrire nesnesi, yazma iş parçacıklarına data dizisine yazabileceklerini bildirmek için kullanılır. Bu nesne, data dizisinin tamamını kullanan okuma iş parçacığı tarafından true olarak ayarlanır. 24. satırda true olarak başlatılır. Nitekim, data dizisi yazma için boştur.
  • 27-34. satırlar: okuma iş parçacıklarının oluşturulması ve başlatılması
  • 37-44. satırlar: yazma iş parçacıklarının oluşturulması ve başlatılması

Okuma iş parçacıkları tarafından yürütülen Lire yöntemi şöyledir:


public static void Lire() {
            // izleme
            Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            // okuma izni beklenmelidir
            peutLire.WaitOne();
            // tablo okuma
            for (int i = 0; i < data.Length; i++) {
                //1 saniye bekleme
                Thread.Sleep(1000);
                // görüntüleme
                Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
            }
            // yazma izni var
            peutEcrire.Set();
            // izleme
            Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
        }
  • 5. satır: Bir yazma iş parçacığının dizinin doldurulduğunu bildirmesi beklenir. Bu sinyal alındığında, bu sinyali bekleyen okuma iş parçacıklarından yalnızca biri geçiş yapabilir.
  • 7-12. satırlar: data dizisinin, ortasında Sleep bulunan bir şekilde kullanılmasıyla iş parçacığının işlemciyi kaybetmesi sağlanır.
  • 14. satır: Yazma iş parçacıklarına, dizinin okunduğunu ve yeniden doldurulabileceğini bildirir.

Yazma iş parçacıkları tarafından yürütülen Ecrire yöntemi şöyledir:


public static void Ecrire() {
            // takip
            Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            // yazma izni beklenmelidir
            peutEcrire.WaitOne();
            // tabloya yazma
            for (int i = 0; i < data.Length; i++) {
                //1 saniye bekleme
                Thread.Sleep(1000);
                // görüntüleme
                data[i] = objRandom.Next(0, 1000);
                Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
            }
            // okunabilir
            peutLire.Set();
            // izleme
            Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
        }
  • 5. satır: Bir okuma iş parçacığının dizinin okunduğunu bildirmesi beklenir. Bu sinyal alındığında, bu sinyali bekleyen yazma iş parçacıklarından yalnızca biri geçiş yapabilir.
  • 7-13. satırlar: data dizisinin, ortasında Sleep bulunan bir şekilde işlenmesi; bu, iş parçacığının işlemciyi kaybetmesini zorlar.
  • 15. satır: Okuma iş parçacıklarına dizinin doldurulduğunu ve yeniden okunabileceğini bildirir.

Çalıştırma sonucunda şu sonuçlar elde edilir:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L1
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
Fin de Main...
02:29:18 : L'écrivain E0 a écrit le nombre 607
02:29:19 : L'écrivain E0 a écrit le nombre 805
02:29:20 : L'écrivain E0 a écrit le nombre 650
Méthode [Ecrire] terminée par le thread n° E0
02:29:21 : Le lecteur L0 a lu le nombre 607
02:29:22 : Le lecteur L0 a lu le nombre 805
02:29:23 : Le lecteur L0 a lu le nombre 650
Méthode [Lire] terminée par le thread n° L0
02:29:24 : L'écrivain E1 a écrit le nombre 186
02:29:25 : L'écrivain E1 a écrit le nombre 881
02:29:26 : L'écrivain E1 a écrit le nombre 415
Méthode [Ecrire] terminée par le thread n° E1
02:29:27 : Le lecteur L1 a lu le nombre 186
02:29:28 : Le lecteur L1 a lu le nombre 881
02:29:29 : Le lecteur L1 a lu le nombre 415
Méthode [Lire] terminée par le thread n° L1

Aşağıdaki noktalara dikkat edilebilir:

  • Her seferinde gerçekten sadece 1 okuyucu vardır, ancak bu okuyucu kritik bölümde işlemciyi kaybeder: Lire
  • Aynı anda yalnızca bir yazıcı vardır, ancak bu yazıcı kritik bölüm Ecrire'te işlemciyi kaybeder
  • bir okuyucu, tabloda okunacak bir şey olduğunda okur
  • bir yazıcı, tablo tamamen okunduktan sonra yazmaya başlar

10.6.2. Monitor sınıfı

Önceki örnekte:

  • yönetilmesi gereken iki paylaşımlı kaynak vardır
  • belirli bir kaynak için iş parçacıkları eşittir.

Yazma iş parçacıkları peutEcrire.WaitOne komutunda bloke olduğunda, bunlardan biri (hangisi olursa olsun) peutEcrire.Set işlemi tarafından serbest bırakılır. Eğer önceki işlem belirli bir yazma iş parçacığına bariyeri açmak zorundaysa, işler daha karmaşık hale gelir.

Bunu, her gişenin farklı bir alanda uzmanlaştığı, halka açık bir kurumla karşılaştırabiliriz. Müşteri geldiğinde, bilet dağıtıcıdan X numaralı gişe için bir bilet alır ve sonra oturur. Her bilet numaralandırılmıştır ve müşteriler hoparlör aracılığıyla numaralarına göre çağrılır. Bekleme süresince müşteri istediğini yapar. Kitap okuyabilir ya da uyuklayabilir. Her seferinde, hoparlörden “X numaralı gişeye Y numaralı kişi çağrılıyor” anonsu geldiğinde uyanır. Eğer çağrılan kişi kendisiyse, ayağa kalkar ve X numaralı gişeye gider; değilse, yaptığı şeye devam eder.

Burada da benzer bir şekilde hareket edilebilir. Yazarları örnek olarak alalım:

plusieurs écrivains attendent pour un même guichet
konu başlıkları kilitli
le guichet se libère et le n° de l'écrivain suivant est appelé
Diziyi okuma işleminde kullanan iş parçacığı, yazma işlemiyle uğraşan iş parçacıklarına dizinin hazır olduğunu bildirir. Bu iş parçacığı ya da başka bir iş parçacığı, bariyeri geçmesi gereken yazma iş parçacığını belirlemiştir.
chaque écrivain regarde son n° et seul celui qui a le n° appelé
va au guichet. Les autres se remettent en attente.
Her iş parçacığı, seçilen iş parçacığı olup olmadığını kontrol eder. Eğer öyleyse, bariyeri geçer. Değilse, tekrar bekleme moduna geçer.

Monitor sınıfı, bu senaryoyu uygulamaya koymaya olanak tanır.

Image

Şimdi, bu belgenin girişinde atıfta bulunulan C# 3.0 kitabının Threading bölümünde önerilen ve giriş koşulu olan bariyer sorunlarını çözebilen standart bir yapı (pattern) açıklayacağız.

  • Öncelikle, bir kaynağı (gişe, ...) paylaşan iş parçacıkları, bu kaynağa “jeton” olarak adlandıracağımız bir nesne aracılığıyla erişir. Gişeye açılan bariyeri açmak için jetona sahip olmak gerekir ve sadece tek bir jeton vardır. Dolayısıyla iş parçacıkları jetonu birbirleri arasında aktarmalıdır.
object jeton=new object();
  • Gişeye gitmek için iş parçacıkları öncelikle jetonu talep eder:
Monitor.Enter(jeton);

Jeton boşsa, önceki işlemi gerçekleştiren iş parçacığına verilir; aksi takdirde iş parçacığı jeton için beklemeye alınır.

  • Gişeye erişim sırasız bir şekilde gerçekleşiyorsa, c.a.d. Giren kişinin kim olduğunun önemi olmadığı durumlarda, önceki işlem yeterlidir. Jetonu elinde bulunduran iş parçacığı gişeye gider. Erişim sıralı bir şekilde gerçekleşiyorsa, jetonu elinde bulunduran iş parçacığı gişeye gitmek için gerekli koşulu karşılayıp karşılamadığını kontrol eder:
while (! jeNeSuisPasCeluiQuiEstAttendu) {Monitor.Wait(jeton);}

Eğer iş parçacığı gişede beklenen iş parçacığı değilse, jetonu geri vererek sırasını bırakır. Bloklanmış bir duruma geçer. Jeton kendisi için tekrar kullanılabilir hale gelir gelmez uyandırılır. Ardından gişeye gitme koşulunu karşılayıp karşılamadığını yeniden kontrol eder. Jetonu serbest bırakan Monitor.Wait(jeton) işlemi, yalnızca iş parçacığı jetonun sahibi ise gerçekleştirilebilir. Aksi takdirde bir istisna atılır.

  • Gişeye gitme koşulunu kontrol eden iş parçacığı gişeye gider:
  1. // gişede çalışma
  2. ....

Gişeden ayrılmadan önce iş parçacığı jetonunu iade etmelidir; aksi takdirde, jetonu bekleyen bloke edilmiş iş parçacıkları süresiz olarak bloke kalır. İki farklı durum söz konusudur:

  • İlk durum, jetona sahip olan iş parçacığının aynı zamanda jetonu bekleyen iş parçacıklarına jetonun boş olduğunu bildiren iş parçacığı olmasıdır. Bunu şu şekilde yapacaktır:
1
2
3
4
5
6
7
8
// gişede işlem
....
// gişeye erişim koşulunun değiştirilmesi
...
// jetonu bekleyen iş parçacıklarının uyandırılması
Monitor.PulseAll(jeton);
// jetonun serbest bırakılması
Monitor.Exit(jeton);
  1. satırda, jetonu bekleyen iş parçacıklarını uyandırır. Bu uyandırma, iş parçacıklarının jetonu almaya uygun hale geldiği anlamına gelir. Ancak bu, jetonu hemen alacakları anlamına gelmez. 8. satırda jeton serbest bırakılır. Uygun tüm iş parçacıkları, belirsiz bir sırayla sırayla jetonu alacaklardır. Bu, onlara erişim koşulunu yeniden kontrol etme fırsatı verecektir. Jetonu serbest bırakan iş parçacığı, yeni bir iş parçacığının girmesine izin vermek için 4. satırda bu koşulu değiştirmiştir. Koşulu ilk karşılayan iş parçacığı jetonu elinde tutar ve sırası geldiğinde gişeye gider.
  • İkinci durum ise, jetona sahip olan iş parçacığının, jetonu bekleyen iş parçacıklarına jetonun boşaldığını bildirmekle yükümlü olmadığı durumdur. Yine de jetonu serbest bırakması gerekir, çünkü bu sinyali göndermekle görevli iş parçacığı jetona sahip olmalıdır. Bunu şu işlemle yapacaktır:
Monitor.Exit(jeton);

Jeton artık kullanılabilir durumdadır, ancak onu bekleyen iş parçacıkları (Wait(jeton) işlemini gerçekleştirmiş olanlar) bu durumdan haberdar edilmez. Bu görev, belirli bir anda aşağıdakine benzer bir kodu çalıştıracak başka bir iş parçacığına verilir:

1
2
3
4
5
6
7
8
// jeton edinme
Monitor.Enter(jeton);
// işlem penceresine erişim koşulunun değiştirilmesi
....
// jetonu bekleyen iş parçacıklarının uyandırılması
Monitor.PulseAll(jeton);
// jetonun serbest bırakılması
Monitor.Exit(jeton);

Sonuç olarak, C# 3.0 kitabının Threading bölümünde önerilen standart yapı şöyledir:

  • giriş jetonunu tanımlayın:
object jeton=new object();
  • gişeye erişim talebinde bulunmak:
lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
}
// işlem penceresine geçiş
...
lock(jeton){...} 

şuna eşdeğerdir

Monitor.Enter(jeton);
try{...} finally{Monitor.Exit(jeton);}

Bu şemada, bariyer geçilir geçilmez jetonun hemen serbest bırakıldığına dikkat edilmelidir. Böylece başka bir iş parçacığı erişim koşulunu test edebilir. Dolayısıyla önceki yapı, erişim koşulunu kontrol eden tüm iş parçacıklarının geçişine izin verir. Eğer istenen bu değilse, şu şekilde yazılabilir:

lock(jeton){
    while (! jeNeSuisPasCeluiQuiEstAttendu) 
        Monitor.Wait(jeton);
     // gişeye geçiş
    ...
}

burada jeton, gişeden geçildikten sonra serbest bırakılır.

  • gişeye erişim koşulunu değiştirmek ve diğer iş parçacıklarını bilgilendirmek
lock(jeton){
     // gişeye erişim koşulunun değiştirilmesi
    ...
     // jetonu bekleyen iş parçacıklarını bilgilendirme
    Monitor.PulseAll(jeton);
}

Yukarıda, erişim koşulu yalnızca jetona sahip iş parçacığı tarafından değiştirilebilir. Şu şekilde de yazılabilir:

     // işlem noktasına erişim koşulunu değiştir
    ...
     // jetonu bekleyen iş parçacıklarını bilgilendir
    Monitor.PulseAll(jeton);
     // jetonu serbest bırak
    Monitor.Exit(jeton);

eğer iş parçacığı zaten jetona sahipse.

Bu bilgiler ışığında, okuyucular ve yazıcılar için kendi kanallarına erişim sırasını belirleyerek okuyucu/yazıcı uygulamasını yeniden yazabiliriz. Kod şu şekildedir:


using System;
using System.Threading;

namespace Chap8 {
    class Program2 {
        // okuma ve yazma iş parçacıklarının kullanımı
        // senkronizasyon olaylarının kullanımını gösterir


        // sınıf değişkenleri
        static int[] data = new int[3];            // okuma ve yazma iş parçacıkları arasında paylaşılan kaynak
        static Random objRandom = new Random(DateTime.Now.Second);    // bir rasgele sayı üreteci
        static object peutLire = new object();        // data içeriğinin okunabileceğini belirtir
        static object peutEcrire = new object();    // data içeriğinin yazılabileceğini belirtir
        static bool lectureAutorisée = false;    // dizinin okunmasına izin vermek için
        static bool écritureAutorisée = false;    // tabloya yazma izni vermek için
        static string[] ordreLecture;    // okuyucuların sırasını belirler
        static string[] ordreEcriture;    // yazarların sırasını belirler
        static int lecteurSuivant = 0;    // bir sonraki okuyucunun numarasını belirtir
        static int écrivainSuivant = 0;    // bir sonraki yazıcının numarasını belirtir

        //ana
        public static void Main(string[] args) {

            // oluşturulacak iş parçacığı sayısı
            const int nbThreads = 5;

            // okuyucu iş parçacıklarının oluşturulması
            Thread[] lecteurs = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                lecteurs[i] = new Thread(Lire);
                lecteurs[i].Name = "L" + i.ToString();
                // başlatma
                lecteurs[i].Start();
            }

            // okuma sırasının oluşturulması
            ordreLecture = new string[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                ordreLecture[i] = lecteurs[nbThreads - i - 1].Name;
                Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i);
            }

            // yazma iş parçacıklarının oluşturulması
            Thread[] écrivains = new Thread[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                écrivains[i] = new Thread(Ecrire);
                écrivains[i].Name = "E" + i.ToString();
                // başlatma
                écrivains[i].Start();
            }

            // yazma sırasının oluşturulması
            ordreEcriture = new string[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                ordreEcriture[i] = écrivains[i].Name;
                Console.WriteLine("L'écrivain {0} est en position {1}", ordreEcriture[i], i);
            }

             // yazma izni
            lock (peutEcrire) {
               écritureAutorisée = true;
                Monitor.Pulse(peutEcrire);
            }


            //işlemin sonu
            Console.WriteLine("Fin de Main...");
        }

        // tablonun içeriğini okuma
        public static void Lire() {
...
        }

        // tabloya yazma
        public static void Ecrire() {
...
        }
    }
}

Okuma kanalı erişimi aşağıdaki koşullara bağlıdır:

  • 13. satır: peutLire jetonu
  • 15. satır: lectureAutorisée boole değeri
  • satır 17: okuyucuların sıralı dizisi. Okuyucular, adlarının yer aldığı bu dizideki sıraya göre okuma gişesine giderler.
  • satır 19: lecteurSuivant, gişeye gitmeye yetkili bir sonraki okuyucunun numarasını gösterir.

Yazma gişesine erişim aşağıdaki unsurlara bağlıdır:

  • 14. satır: peutEcrire jetonu
  • satır 16: écritureAutorisée boole değeri
  • satır 18: yazıcıların sıralı tablosu. Yazıcılar, adlarının yer aldığı bu tablodaki sıraya göre yazma gişesine giderler.
  • 20. satır: écrivainSuivant, gişeye gitmeye yetkili bir sonraki yazarın numarasını belirtir.

Kodun diğer unsurları şunlardır:

  • 29-36. satırlar: okuma iş parçacıklarının oluşturulması ve başlatılması. Okuma izni olmadığı için (15. satır) hepsi bloke edilecektir.
  • satır 39-43: Gişeye geçiş sıraları, oluşturulma sıralarının tersi olacaktır.
  • satır 46-53: yazar iş parçacıklarının oluşturulması ve başlatılması. Yazma izni olmadığı için (satır 16) hepsi bloke edilecektir.
  • satır 56-60: Gişeye geçiş sırası, oluşturulma sırasına göre belirlenecektir.
  • 64. satır: Yazma izni verilir
  • satır 65: yazıcılara bir değişiklik olduğu bildirilir.

Lire yöntemi şu şekildedir:


        public static void Lire() {
            // izleme
            Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            // okuma izni beklenmelidir
            lock (peutLire) {
                while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
                    Monitor.Wait(peutLire);
                }
                // tablo okuma
                for (int i = 0; i < data.Length; i++) {
                    //1 saniye bekleme
                    Thread.Sleep(1000);
                    // görüntüleme
                    Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
                }
                 // sonraki okuyucu
                lectureAutorisée = false;
                lecteurSuivant++;
                // yazıcılara yazabilecekleri konusunda bildirimde bulunulur
                lock (peutEcrire) {
                    écritureAutorisée = true;
                    Monitor.PulseAll(peutEcrire);
                }

                // takip
                Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
            }
}
  • Gişeye erişim süreci, 5-27. satırlardaki lock tarafından kontrol edilir. Jetonu alan okuyucu, gişede kaldığı süre boyunca jetonu elinde tutar
  • 6-8. satırlar: 5. satırdaki jetonu alan okuyucu, okuma izni yoksa veya sıra kendisinde değilse jetonu serbest bırakır.
  • satır 10-15: gişeye geçiş (tablonun işlenmesi)
  • 17-18. satırlar: İş parçacığı, okuma gişesine erişim koşullarını değiştirir. İş parçacığının hâlâ okuma jetonuna sahip olduğu ve bu değişikliklerin henüz bir okuyucunun geçişine izin vermediği unutulmamalıdır.
  • 20-23. satırlar: İş parçacığı, yazma gişesine erişim koşullarını değiştirir ve bekleyen tüm yazarlara bir değişiklik olduğunu bildirir.
  • 27. satır: lock iş parçacığı sona erer, peutLire jetonu serbest bırakılır. Bu durumda bir okuma iş parçacığı 5. satırdaki jetonu alabilir, ancak lectureAutorisée boole değeri "yanlış" olduğu için erişim koşulunu geçemez. Ayrıca, peutLire jetonunu bekleyen tüm iş parçacıkları, PulseAll(peutLire) işlemi henüz gerçekleşmediği için beklemeye devam eder.

Ecrire yöntemi şu şekildedir:


        public static void Ecrire() {
            // takip
            Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
            // yazma izni beklenmelidir
            lock (peutEcrire) {
                while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
                    Monitor.Wait(peutEcrire);
                }
                // tabloya yazma
                for (int i = 0; i < data.Length; i++) {
                    //1 saniye bekleme
                    Thread.Sleep(1000);
                    // görüntüleme
                    data[i] = objRandom.Next(0, 1000);
                    Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
                }
                // sonraki yazar
                écritureAutorisée = false;
                écrivainSuivant++;
                // jetonu bekleyen okuyucuları uyandırıyoruz peutLire
                lock (peutLire) {
                    lectureAutorisée = true;
                    Monitor.PulseAll(peutLire);
                }
                // izleme
                Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
            }
}
  • Yazma penceresine erişim, 5-27. satırlardaki lock tarafından kontrol edilir. Jetonu alan yazar, pencereye kaldığı süre boyunca jetonu elinde tutar
  • 6-8. satırlar: 5. satırdaki jetonu alan yazar, yazma işlemi izin verilmezse veya sıra kendisinde değilse jetonu serbest bırakır.
  • satır 10-16: gişede işlem (tablonun işlenmesi)
  • satır 18-19: iş parçacığı, yazma gişesine erişim koşullarını değiştirir. İş parçacığının hâlâ yazma jetonuna sahip olduğu ve bu değişikliklerin henüz bir yazıcının geçişine izin vermediği unutulmamalıdır.
  • 21-24. satırlar: İş parçacığı, okuma gişesine erişim koşullarını değiştirir ve bekleyen tüm okuyuculara bir değişiklik olduğunu bildirir.
  • 27. satır: lock sona erer, peutEcrire jetonu serbest bırakılır. Bu durumda bir yazma iş parçacığı 5. satırdaki jetonu alabilir, ancak écritureAutorisée boole değeri "yanlış" olduğu için erişim koşulunu geçemez. Ayrıca, peutEcrire jetonunu bekleyen tüm iş parçacıkları, yeni bir PulseAll(peutEcrire) işlemi bekleyerek bekleme durumunda kalır.

Çalıştırma örneği şu şekildedir:

Méthode [Lire] démarrée par le thread n° L0
Méthode [Lire] démarrée par le thread n° L2
Méthode [Lire] démarrée par le thread n° L1
Le lecteur L2 est en position 0
Le lecteur L1 est en position 1
Le lecteur L0 est en position 2
Méthode [Ecrire] démarrée par le thread n° E0
Méthode [Ecrire] démarrée par le thread n° E1
L'écrivain E0 est en position 0
L'écrivain E1 est en position 1
L'écrivain E2 est en position 2
Fin de Main...
Méthode [Ecrire] démarrée par le thread n° E2
12:09:05 : L'écrivain E0 a écrit le nombre 815
12:09:06 : L'écrivain E0 a écrit le nombre 990
12:09:07 : L'écrivain E0 a écrit le nombre 563
Méthode [Ecrire] terminée par le thread n° E0
12:09:08 : Le lecteur L2 a lu le nombre 815
12:09:09 : Le lecteur L2 a lu le nombre 990
12:09:10 : Le lecteur L2 a lu le nombre 563
Méthode [Lire] terminée par le thread n° L2
12:09:11 : L'écrivain E1 a écrit le nombre 411
12:09:12 : L'écrivain E1 a écrit le nombre 11
12:09:13 : L'écrivain E1 a écrit le nombre 54
Méthode [Ecrire] terminée par le thread n° E1
12:09:14 : Le lecteur L1 a lu le nombre 411
12:09:15 : Le lecteur L1 a lu le nombre 11
12:09:16 : Le lecteur L1 a lu le nombre 54
Méthode [Lire] terminée par le thread n° L1
12:09:17 : L'écrivain E2 a écrit le nombre 698
12:09:18 : L'écrivain E2 a écrit le nombre 448
12:09:19 : L'écrivain E2 a écrit le nombre 472
Méthode [Ecrire] terminée par le thread n° E2
12:09:20 : Le lecteur L0 a lu le nombre 698
12:09:21 : Le lecteur L0 a lu le nombre 448
12:09:22 : Le lecteur L0 a lu le nombre 472
Méthode [Lire] terminée par le thread n° L0

10.7. İş parçacığı havuzları

Şimdiye kadar, iş parçacıklarını yönetmek için:

  • Thread T = new Thread(...) komutuyla oluşturduk
  • ve T.Start() ile çalıştırıyorduk

"Veritabanları" bölümünde, bazı SGBD ile açık bağlantı havuzlarına sahip olmanın mümkün olduğunu görmüştük:

  • n havuzun başlatılmasıyla bağlantılar açılır
  • bir iş parçacığı bağlantı talep ettiğinde, havuzdaki açık bağlantılardan biri ona verilir
  • iş parçacığı bağlantıyı kapattığında, bağlantı kapatılmaz, havuza geri verilir

Bağlantı havuzunun kullanımı kod düzeyinde şeffaftır. Bunun avantajı, performansın artırılmasıdır: bir bağlantı açmak maliyetlidir. Burada açık olan 10 bağlantı, yüzlerce isteği karşılayabilir.

Benzer bir sistem iş parçacıkları için de mevcuttur:

  • Havuzun başlatılması sırasında min adet iş parçacığı oluşturulur. min değeri, ThreadPool.SetMinThreads(min1,min2) yöntemi ile belirlenir. Bir iş parçacığı havuzu, engelleyici veya engelleyici olmayan (asenkron olarak adlandırılan) görevleri yürütmek için kullanılabilir. İlk parametre min1, engelleyici iş parçacıklarının sayısını belirler; ikinci parametre min2 ise asenkron iş parçacıklarının sayısını belirler. Bu iki değerin güncel durumları, ThreadPool.GetMinThreads(out min1,out min2) ile elde edilebilir.
  • Bu sayı yeterli değilse, havuz, max iş parçacığı sınırına kadar talepleri karşılamak için başka iş parçacıkları oluşturacaktır. max değeri, ThreadPool.SetMaxThreads(max1,max2) yöntemi ile belirlenir. Her iki parametrenin anlamı, SetMinThreads yöntemindekiyle aynıdır. Bu iki değerin güncel durumları, ThreadPool.GetMaxThreads(out max1,out max2) ile elde edilebilir. max1 iş parçacığı sınırına ulaşıldığında, engelleyici görevler için iş parçacığı talepleri, havuzda boş bir iş parçacığı bulunana kadar beklemeye alınır.

Bir iş parçacığı havuzu çeşitli avantajlar sunar:

  • bağlantı havuzunda olduğu gibi, iş parçacığı oluşturma süresinden tasarruf edilir: 10 iş parçacığı yüzlerce isteği karşılayabilir.
  • uygulamanın güvenliği sağlanır: maksimum iş parçacığı sayısı belirlenerek, çok fazla istek nedeniyle uygulamanın tıkanması önlenir. Bu istekler kuyruğa alınır.

Havuzdaki bir iş parçacığına görev vermek için şu iki yöntemden biri kullanılır:

  1. ThreadPool.QueueWorkItem(WaitCallBack)
  2. ThreadPool.QueueWorkItem(WaitCallBack,object)

burada WaitCallBack, void WaitCallBack(object) imzasına sahip herhangi bir yöntemdir. 1. yöntem, bir iş parçacığından WaitCallBack yöntemini hiçbir parametre aktarmadan yürütmesini ister. 2. yöntem ise aynı şeyi yapar, ancak object türünde bir parametreyi WaitCallBack yöntemine aktarır.

İşte bu kavramları gösteren bir program:


using System;
using System.Threading;

namespace Chap8 {
    class Program {
        public static void Main() {
            // mevcut iş parçacığını başlat
            Thread main = Thread.CurrentThread;
            // İş parçacığına bir ad veriliyor
            main.Name = "Main";

            // bir iş parçacığı havuzu kullanılıyor
            int min1, min2;
            // blokajlı iş parçacıklarının minimum sayısı belirleniyor
            ThreadPool.GetMinThreads(out min1, out min2);
            Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
            Console.WriteLine("Nombre minimum de tâches asynchrones dans le pool : {0}", min2);
            ThreadPool.SetMinThreads(3, min2);
            ThreadPool.GetMinThreads(out min1, out min2);
            Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool après changement : {0}", min1);
            // bloke eden iş parçacıklarının maksimum sayısı belirleniyor
            int max1, max2;
            ThreadPool.GetMaxThreads(out max1, out max2);
            Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
            Console.WriteLine("Nombre maximum de tâches asynchrones dans le pool : {0}", max2);
            ThreadPool.SetMaxThreads(5, max2);
            ThreadPool.GetMaxThreads(out max1, out max2);
            Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool après changement : {0}", max1);
            // 7 iş parçacığı çalıştırılır
            for (int i = 0; i < 7; i++) {
                // i iş parçacığının bir havuzda çalıştırılmasına başlanır
                ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 10 });
            }
            // işlem sonu
            Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss:FF}", main.Name, DateTime.Now);
            // bekleme
            Console.ReadLine();
        }

        public static void Sleep(object infos) {
            // parametre alınır
            Data2 data = infos as Data2;
            Console.WriteLine("A {2:hh:mm:ss:FF}, le thread n° {0} va dormir pendant {1} seconde(s)", data.Numéro, data.Durée,DateTime.Now);
            // havuzun durumu
            int cpt1, cpt2;
            ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
            Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
            // Durée saniye boyunca askıya alınma
            Thread.Sleep(data.Durée * 1000);
            // çalışma sonu
            data.Fin = DateTime.Now;
            Console.WriteLine("A {3:hh:mm:ss:FF}, le thread n° {0} se termine. Il était programmé pour durer {1} seconde(s). Il a duré {2} seconde(s)", data.Numéro, data.Durée, data.Fin - data.Début,DateTime.Now);
        }
    }

    internal class Data2 {
        // çeşitli bilgiler
        public string Numéro { get; set; }
        public DateTime Début { get; set; }
        public int Durée { get; set; }
        public DateTime Fin { get; set; }
    }
}
  • 15-17. satırlar: İş parçacığı havuzundaki her iki iş parçacığı türünün mevcut minimum sayısını sorgulayıp görüntüler
  • 18. satır: Bloke edici görevler için minimum iş parçacığı sayısı 2 olarak değiştirilir
  • 19-21. satırlar: Yeni minimum değerler görüntülenir
  • 22-28. satırlar: Engelleyici görevler için maksimum iş parçacığı sayısını da aynı şekilde 5 olarak ayarlıyoruz
  • satır 30-33: 5 iş parçacığından oluşan bir havuzda 7 görev çalıştırılır. 5 görev 1 iş parçacığı almalıdır; ilk 2 görev, 2 iş parçacığı her zaman mevcut olduğu için hızlı bir şekilde, diğer 3 görev ise 0,5 saniyelik bir bekleme süresiyle. 2 görev, bir iş parçacığının boşalmasını beklemelidir.
  • satır 32: Görevler, 40-54. satırlarda yer alan Sleep yöntemini, 56-62. satırlarda tanımlanan Data2 türünde bir parametre aktararak yürütür.
  • 40. satır: Görevler tarafından yürütülen Sleep yöntemi
  • 42. satır: Sleep yöntemine aktarılan parametre alınır.
  • 43. satır: Görev, konsolda kendini tanıtıyor
  • 45-47. satırlar: Şu anda kullanılabilir olan iş parçacığı sayısı görüntülenir. Bu sayının nasıl değiştiğini görmek istiyoruz.
  • 49. satır: Görev birkaç saniye durur (engelleyici görev).
  • 52. satır: Görev yeniden etkinleştiğinde, hesapla ilgili bazı bilgiler görüntülenir.

Elde edilen sonuçlar şunlardır.

Havuzdaki min ve max iş parçacığı numaraları için:

1
2
3
4
5
6
Nombre minimum de tâches bloquantes dans le pool : 2
Nombre minimum de tâches asynchrones dans le pool : 2
Nombre minimum de tâches bloquantes dans le pool après changement : 3
Nombre maximum de tâches bloquantes dans le pool : 500
Nombre maximum de tâches asynchrones dans le pool : 1000
Nombre maximum de tâches bloquantes dans le pool après changement : 5

7 iş parçacığının yürütülmesi için:

A 03:07:37:04, le thread n° 0 va dormir pendant 10 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 3
A 03:07:37:04, le thread n° 2 va dormir pendant 12 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:37:04, le thread n° 1 va dormir pendant 11 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
A 03:07:38:04, le thread n° 3 va dormir pendant 13 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 1
A 03:07:38:54, le thread n° 4 va dormir pendant 14 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:47:04, le thread n° 0 se termine. Il était programmé pour durer 10 seconde(s). Il a duré 00:00:10 seconde(s)
A 03:07:47:04, le thread n° 5 va dormir pendant 15 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:48:04, le thread n° 1 se termine. Il était programmé pour durer 11 seconde(s). Il a duré 00:00:11 seconde(s)
A 03:07:48:04, le thread n° 6 va dormir pendant 16 seconde(s)
Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
A 03:07:49:04, le thread n° 2 se termine. Il était programmé pour durer 12 seconde(s). Il a duré 00:00:12 seconde(s)
A 03:07:51:04, le thread n° 3 se termine. Il était programmé pour durer 13 seconde(s). Il a duré 00:00:14 seconde(s)
A 03:07:52:54, le thread n° 4 se termine. Il était programmé pour durer 14 seconde(s). Il a duré 00:00:15.5000000 seconde(s)
A 03:08:02:04, le thread n° 5 se termine. Il était programmé pour durer 15 seconde(s). Il a duré 00:00:25 seconde(s)
A 03:08:04:04, le thread n° 6 se termine. Il était programmé pour durer 16 seconde(s). Il a duré 00:00:27 seconde(s)
  • 1-6. satırlar: İlk 3 görev sırayla yürütülür. Hemen 1 kullanılabilir iş parçacığı bulurlar (MinThreads=3) ve ardından bekleme moduna geçerler.
  • 7-9. satırlar: 3 ve 4 numaralı görevler için süreç biraz daha uzun sürer. Her biri için boş iş parçacığı yoktu. Bir iş parçacığı oluşturulması gerekti. Bu mekanizma 5'e kadar mümkündür (MaxThreads=5).
  • 10. satır: Artık kullanılabilir iş parçacığı kalmamıştır: 5 ve 6 numaralı görevler beklemek zorunda kalacaktır.
  • 11-12. satırlar: 0 numaralı görev sona eriyor. 5 numaralı görev kendi iş parçacığını alıyor.
  • 13-14. satırlar: 1 numaralı görev sona eriyor. 6 numaralı görev kendi iş parçacığını alıyor.
  • satır 17-21: görevler birbiri ardına sona eriyor.

10.8. BackgroundWorker sınıfı

10.8.1. Örnek 1

BackgroundWorker sınıfı, [System.ComponentModel] ad alanına aittir. Bir iş parçacığı gibi kullanılır, ancak bazı durumlarda onu [Thread] sınıfından daha ilgi çekici hale getirebilecek bazı özelliklere sahiptir:

  • Aşağıdaki olayları yayar:
  • DoWork: Bir iş parçacığı, BackgroundWorker'in yürütülmesini talep etti
  • ProgressChanged: BackgroundWorker nesnesi, ReportProgress yöntemini çalıştırdı. Bu yöntem, yürütme yüzdesini belirtmek için kullanılır.
  • RunWorkerCompleted: BackgroundWorker nesnesi işini tamamladı. İş, normal şekilde, iptal edilerek veya bir istisna nedeniyle tamamlanmış olabilir.

Bu olaylar, BackgroundWorker'i grafik arayüzlerde kullanışlı hale getirir: uzun süren bir görev, BackgroundWorker'e verilir; bu nesne, ProgressChanged olayıyla ilerleme durumunu ve RunWorkerCompleted olayıyla tamamlanma durumunu bildirebilir. BackgroundWorker tarafından gerçekleştirilecek iş, DoWork olayına atanmış bir yöntemle yapılacaktır.

  • Bu işin iptal edilmesi talep edilebilir. Bir grafik arayüzde, uzun süren bir görev bu şekilde kullanıcı tarafından iptal edilebilir.
  • BackgroundWorker nesneleri bir havuza aittir ve ihtiyaç duyulduğunda yeniden kullanılır. Bir BackgroundWorker nesnesine ihtiyaç duyan bir uygulama, bu nesneyi havuzdan alır ve havuz, ona halihazırda mevcut ancak kullanılmayan bir iş parçacığı verir. Her seferinde yeni bir iş parçacığı oluşturmak yerine iş parçacıklarını bu şekilde yeniden kullanmak, performansı artırır.

Girişin kontrolsüz olduğu durumlarda, önceki uygulamada bu aracı kullanıyoruz:


using System;
using System.Threading;
using System.ComponentModel;

namespace Chap8 {
    class Program2 {
        // okuma ve yazma iş parçacıklarının kullanımı
        // paylaşılan kaynakların ve senkronizasyonun eşzamanlı kullanımını gösterir

        // sınıf değişkenleri
        const int nbThreads = 2;                    // toplam iş parçacığı sayısı
        static int nbLecteursTerminés = 0;        // tamamlanan iş parçacığı sayısı
        static int[] data = new int[5];            // okuma iş parçacıkları ile yazma iş parçacıkları arasında paylaşılan dizi
        static object appli;                            // tamamlanan iş parçacığı sayısına erişimi senkronize eder
        static Random objRandom = new Random(DateTime.Now.Second);    // bir rasgele sayı üreteci
        static AutoResetEvent peutLire;        // dizinin içeriğinin okunabileceğini bildirir
        static AutoResetEvent peutEcrire;        // diziye yazılabileceğini bildirir
        static AutoResetEvent finLecteurs;    // okuyucuların sonunu bildirir

        //ana iş parçacığı
        public static void Main(string[] args) {

            // iş parçacığına bir ad verilir
            Thread.CurrentThread.Name = "Main";

            // bayrakların başlatılması
            peutLire = new AutoResetEvent(false);        // henüz okuma yapılamaz
            peutEcrire = new AutoResetEvent(true);    // yazma işlemi yapılabilir
            finLecteurs = new AutoResetEvent(false);    // uygulama henüz tamamlanmadı

            // tamamlanan iş parçacıkları sayacına erişimi senkronize ediyor
            appli = new object();                

            // okuma iş parçacıkları oluşturuluyor
            MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                lecteurs[i] = new MyBackgroundWorker();
                lecteurs[i].Numéro = "L" + i;
                lecteurs[i].DoWork += Lire;
                lecteurs[i].RunWorkerCompleted += EndLecteur;
                // başlatma
                lecteurs[i].RunWorkerAsync();
            }

            // yazma iş parçacıklarının oluşturulması
            MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
            for (int i = 0; i < nbThreads; i++) {
                // oluşturma
                écrivains[i] = new MyBackgroundWorker();
                écrivains[i].Numéro = "E" + i;
                écrivains[i].DoWork += Ecrire;
                // başlatma
                écrivains[i].RunWorkerAsync();
            }

            // tüm iş parçacıklarının bitmesini bekleme
            finLecteurs.WaitOne();
            //işlemin sonu
            Console.WriteLine("Fin de Main...");
        }

        public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
...
        }

        // tablonun içeriğini okuma
        public static void Lire(object sender, DoWorkEventArgs infos) {
...
        }

        // tabloya yazma
        public static void Ecrire(object sender, DoWorkEventArgs infos) {
...
        }
    }

    // konu
    internal class MyBackgroundWorker : BackgroundWorker {
        // çeşitli bilgiler
        public string Numéro { get; set; }
    }

}

Sadece değişiklikleri detaylı olarak açıklıyoruz:

  • 79-82. satırlarda Thread sınıfı, MyBackgroundWorker sınıfıyla değiştirilmiştir. BackgroundWorker sınıfı, iş parçacığına bir numara atamak amacıyla türetilmiştir. 43. ve 54. satırlardaki RunWorkerAsync yöntemine, iş parçacığı numarasını içeren bir nesne aktararak farklı bir şekilde de ilerleyebilirdik.
  • 58. satır: Main yöntemi, tüm okuma iş parçacıkları işlerini tamamladıktan sonra sona erer. Bunun için 12. satırda, nbLecteursTerminés sayacı, işini tamamlayan okuyucu iş parçacıklarının sayısını sayar. Bu sayaç, bir okuyucu iş parçacığı her sona erdiğinde yürütülen 63-65. satırlardaki EndLecteur yöntemi tarafından artırılır. 18. satırdaki AutoResetEvent finLecteurs olayını kontrol eden bu prosedürdür; 59. satırdaki Main. yöntemi bu olayla senkronize olur
  • 16. satır: Birden fazla okuma iş parçacığı nbLecteursTerminés sayacını aynı anda artırmak isteyebileceğinden, bu sayaca özel erişim appli senkronizasyon nesnesi tarafından sağlanır. Bu durum olasılığı düşük olmakla birlikte teorik olarak mümkündür.
  • 35-44. satırlar: okuma iş parçacıklarının oluşturulması
  • 38. satır: MyBackgroundWorker türünde iş parçacığının oluşturulması
  • 39. satır: İş parçacığına bir numara verilir
  • 40. satır: Yürütülmesi gereken Lire yöntemi atanır
  • 41. satır: EndLecteur yöntemi, iş parçacığı sona erdikten sonra yürütülecektir
  • 43. satır: iş parçacığı başlatılır
  • 47-55. satırlar: yazma iş parçacıklarının oluşturulması
  • satır 50: MyBackgroundWorker türünde iş parçacığı oluşturulur
  • 51. satır: Bu iş parçacığına bir numara verilir
  • 52. satır: Yürütülmesi gereken Ecrire yöntemi atanır
  • 54. satır: iş parçacığı başlatıldı

Lire ve Ecrire yöntemleri değişmeden kalır. EndLecteur yöntemi, her okuma iş parçacığının sonunda yürütülür. Kod şöyledir:


        public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
            // tamamlanan okuyucu sayısının artırılması
            lock (appli) {
                nbLecteursTerminés++;
                if (nbLecteursTerminés == nbThreads)
                    finLecteurs.Set();
            }
}

EndLecteur yönteminin görevi, tüm okuyucuların işlerini tamamladığını Main yöntemine bildirmektir.

  • 4. satır: nbLecteursTerminés sayacı artırılır.
  • 5-6. satırlar: Eğer tüm okuyucular görevlerini yerine getirmişse, bu olayı bekleyen Main yöntemini bilgilendirmek amacıyla finLecteurs olayı “true” olarak ayarlanır.
  • EndLecteur yordamı birden fazla iş parçacığı tarafından yürütüldüğünden, yukarıdaki kritik bölüm 3. satırdaki lock koşulu ile korunur.

Çalıştırma, iş parçacıkları kullanan sürümle benzer sonuçlar verir.

10.8.2. Örnek 2

Aşağıdaki kod, BackgroundWorker sınıfının diğer özelliklerini göstermektedir:

  • görevi iptal etme imkanı
  • görevde atılan bir istisnanın üst düzeye iletilmesi
  • göreve bir G/Ç parametresinin aktarılması

using System;
using System.Threading;
using System.ComponentModel;

namespace Chap8 {
    class Program3 {

        // iş parçacıkları
        static BackgroundWorker[] tâches = new BackgroundWorker[5];

        public static void Main() {
            // geçerli iş parçacığını başlat
            Thread main = Thread.CurrentThread;
            // iş parçacığına ad atama
            main.Name = "Main";

            // iş parçacıkları oluşturma
            for (int i = 0; i < tâches.Length; i++) {
                // i numaralı iş parçacığı oluşturuluyor
                tâches[i] = new BackgroundWorker();
                // başlatılıyor
                tâches[i].DoWork += Sleep;
                tâches[i].RunWorkerCompleted += End;
                tâches[i].WorkerSupportsCancellation = true;
                // başlatılıyor
                tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
            }
            // son iş parçacığını iptal ediyoruz
            tâches[4].CancelAsync();

            // işlem sonu
            Console.WriteLine("Fin du thread {0}, tapez [entrée] pour terminer...", main.Name);
            Console.ReadLine();
            return;
        }

        public static void Sleep(object sender, DoWorkEventArgs infos) {
...
        }

        public static void End(object sender, RunWorkerCompletedEventArgs infos) {
...
        }

        internal class Data {
            // çeşitli bilgiler
            public int Numéro { get; set; }
            public DateTime Début { get; set; }
            public int Durée { get; set; }
            public DateTime Fin { get; set; }
        }
    }
}
  • 9. satır: BackgroundWorker dizisi
  • 18-27. satırlar: iş parçacıklarının oluşturulması
  • 20. satır: iş parçacığının oluşturulması
  • 22. satır: iş parçacığı, 39-41. satırlardaki Sleep yöntemini çalıştıracak
  • 23. satır: İş parçacığının sonunda 43-45. satırlardaki End yöntemi çalıştırılacaktır
  • 24. satır: iş parçacığı iptal edilebilir
  • 26. satır: İş parçacığı, 49-52. satırlarda tanımlanan [Data] türünde bir parametre ile başlatılır. Bu nesnenin aşağıdaki alanları vardır:
    • Numéro (giriş): iş parçacığı numarası
    • Début (giriş): iş parçacığının yürütülmeye başlama saati
    • Durée (giriş): Sleep'in yürütme süresi
    • Fin (çıkış): iş parçacığının yürütme sonu
  • 29. satır: 4 numaralı iş parçacığı iptal edilir

Tüm iş parçacıkları aşağıdaki Sleep yöntemini yürütür:


        public static void Sleep(object sender, DoWorkEventArgs infos) {
            // "infos" parametresini işliyoruz
            Data data = (Data)infos.Argument;
            // 3 numaralı görev için istisna
            if (data.Numéro == 3) {
                throw new Exception("test....");
            }
            // Saniye süreyle uyku moduna geçiliyor, her saniye bir durdurma
            for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
                // 1 saniye bekleme
                Thread.Sleep(1000);
            }
            // çalışma sonu
            data.Fin = DateTime.Now;
            // sonuç başlatılıyor
            infos.Result = data;
            infos.Cancel = tâches[data.Numéro].CancellationPending;
}
  • 1. satır: Sleep yöntemi, olay işleyicilerinin standart imzasına sahiptir. Bu yöntem iki parametre alır:
    • sender: olayın kaynağı; burada,
    • infos: DoWorkEventArgs türündedir ve DoWork olayı hakkında bilgi verir. Bu parametre, hem iş parçacığına bilgi iletmek hem de sonuçlarını almak için kullanılır.
  • 3. satır: Görevin RunWorkerAsync yöntemine aktarılan parametre, infos.Argument özelliğinde bulunur.
  • 5-7. satırlar: 3 numaralı görev için bir istisna atılır
  • 9-12. satırlar: İş parçacığı, 9. satırdaki iptal testinin yapılabilmesi için birer saniyelik aralıklarla Durée saniye boyunca "uykuya dalar". Bu, iş parçacığının düzenli aralıklarla bir iptal talebi olup olmadığını kontrol edeceği uzun süreli bir işi simüle eder. İptal edildiğini belirtmek için iş parçacığı, infos.Cancel özelliğini true olarak ayarlamalıdır (satır 17).
  • satır 16: İş parçacığı, kendisini başlatan iş parçacığına bir sonuç döndürebilir. Bu sonucu infos.Result'e yerleştirir.

İşlemleri tamamladıktan sonra, iş parçacıkları aşağıdaki End yöntemini yürütür:


public static void End(object sender, RunWorkerCompletedEventArgs infos) {
            // infos parametresini kullanarak yürütme sonucunu görüntüleme
            // istisna mı?
            if (infos.Error != null) {
                Console.WriteLine("Le thread {1} a rencontré l'erreur suivante : {0}", infos.Error.Message, sender);
            } else
                if (!infos.Cancelled) {
                    Data data = (Data)infos.Result;
                    Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
                    data.Numéro, data.Début, data.Durée, data.Fin, (data.Fin - data.Début));
                } else {
                    Console.WriteLine("Thread {0} annulé", sender);
                }
        }
  • 1. satır: End yöntemi, olay işleyicilerinin standart imzasına sahiptir. Bu yöntem iki parametre alır:
    • sender: olayın kaynağı; burada,
    • infos: RunWorkerCompletedEventArgs türündedir ve RunWorkerCompleted olayı hakkında bilgi verir.
  • 4. satır: Exception türündeki infos.Error alanı, yalnızca bir istisna meydana geldiğinde doldurulur.
  • 7. satır: İş parçacığı iptal edilmişse, true değerine sahip boole tipi infos.Cancelled alanı doldurulur.
  • 8. satır: Eğer bir istisna veya iptal olmamışsa, o zaman infos.Result, yürütülen iş parçacığının sonucudur. İş parçacığı iptal edilmişse veya bir istisna atmışsa bu sonucu kullanmak, bir istisna oluşturur. Dolayısıyla 5. ve 13. satırlarda, iptal edilen veya istisna atan iş parçacığının numarasını görüntüleyemiyoruz çünkü bu numara infos.Result'te yer almaktadır. Bu sorun, önceki örnekte yapıldığı gibi, çağıran iş parçacığı ile çağrılan iş parçacığı arasında paylaşılacak bilgileri içerecek şekilde BackgroundWorker sınıfını türetmek suretiyle aşılabilir. Bu durumda, infos argümanı yerine, BackgroundWorker'i temsil eden sender argümanı kullanılır.

Çalıştırma sonuçları şöyledir:

1
2
3
4
5
6
Fin du thread Main. Laissez les autres threads se terminer puis tapez [entrée] pour terminer...
Thread 0 terminé : début 05:19:46, durée programmée 1 s, fin 05:19:47, durée effective 00:00:01
Le thread System.ComponentModel.BackgroundWorker a rencontré l'erreur suivante : test....
Thread System.ComponentModel.BackgroundWorker annulé
Thread 1 terminé : début 05:19:46, durée programmée 2 s, fin 05:19:49, durée effective 00:00:03
Thread 2 terminé : début 05:19:46, durée programmée 3 s, fin 05:19:50, durée effective 00:00:04

10.9. İş parçacığına özgü veriler

10.9.1. Prensip

Üç katmanlı bir uygulamayı ele alalım:

Uygulamanın çok kullanıcılı olduğunu varsayalım; örneğin bir web uygulaması. Her kullanıcıya kendisine özel bir iş parçacığı hizmet eder. İş parçacığının yaşam döngüsü şu şekildedir:

  1. iş parçacığı, bir kullanıcının talebini karşılamak üzere oluşturulur veya bir iş parçacığı havuzundan talep edilir
  2. eğer bu istek veri gerektiriyorsa, iş parçacığı [ui] katmanındaki bir yöntemi çalıştırır; bu yöntem de [metier] katmanındaki bir yöntemi çağırır; bu yöntem de sırayla [dao] katmanındaki bir yöntemi çağırır.
  3. İş parçacığı, yanıtı kullanıcıya döndürür. Ardından ortadan kalkar veya bir iş parçacığı havuzunda yeniden kullanıma alınır.
  1. işlemde, iş parçacığının diğer iş parçacıklarıyla paylaşılmayan kendine özgü verilere (c.a.d) sahip olması yararlı olabilir. Bu veriler, örneğin iş parçacığının hizmet verdiği belirli bir kullanıcıya ait olabilir. Bu veriler daha sonra farklı katmanlarda ([ui, metier, dao]) kullanılabilir.

Thread sınıfı, anahtarların LocalDataStoreSlot türünde olduğu bir tür özel sözlük sayesinde bu senaryoyu mümkün kılar:

komutu, name anahtarı için iş parçacığının özel sözlüğünde bir giriş oluşturur.
data değerini, iş parçacığının özel sözlüğündeki name anahtarına eşler
iş parçacığının özel sözlüğündeki name anahtarına eşleştirilen değeri alır

Kullanım örneği şu şekilde olabilir:

  • geçerli iş parçacığıyla ilişkili bir çift (clé,valeur) oluşturmak için:
Thread.SetData(Thread.GetNamedDataSlot("clé"),valeur);
  • clé ile ilişkili değeri almak için:
Thread.GetData(Thread.GetNamedDataSlot("clé"));

10.9.2. İlkenin uygulanması

Aşağıdaki üç katmanlı uygulamayı ele alalım:

[dao] katmanının bir ürün veritabanını yönettiğini ve arayüzünün başlangıçta aşağıdaki gibi olduğunu varsayalım:


using System.Collections.Generic;

namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • 5. satır: veritabanına bir ürün eklemek için
  • 6. satır: veritabanındaki tüm ürünleri almak için
  • 7. satır: veritabanındaki tüm ürünleri silmek için

Daha sonra, bir işlem yardımıyla ürün tablosu eklemek için bir yönteme ihtiyaç duyulur; çünkü ya tüm ürünler eklenecek ya da hiçbiri eklenmeyecektir. Bu yeni ihtiyacı karşılamak için arayüzü şu şekilde değiştirebiliriz:


using System.Collections.Generic;

namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        void insertArticles(Article[] articles);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • 6. satır: veritabanına bir ürün tablosu eklemek için

Daha sonra, başka bir uygulama için, yine bir işlem içinde, bir listede kayıtlı olan ürün listesini silme ihtiyacı ortaya çıkar. Farklı iş ihtiyaçlarını karşılamak için [dao] katmanının genişlemesi gerekeceği görülür. Başka bir yol da izlenebilir:

  • [dao] katmanına yalnızca temel işlemler olan InsertArticle, DeleteArticle, UpdateArticle, SelectArticle, SelectArticles
  • birden fazla maddenin eşzamanlı güncelleme işlemlerini [métier] katmanına aktarabilir. Bu işlemler, [dao] katmanındaki temel işlemleri kullanır.

Bu çözümün avantajı, aynı [dao] katmanının, farklı [metier] katmanlarıyla herhangi bir değişiklik yapılmaksızın kullanılabilmesidir. Ancak bu, veritabanında atomik olarak gerçekleştirilmesi gereken güncellemeleri bir araya getiren işlemin yönetiminde bir zorluk yaratır:

  • işlem, [metier] katmanı tarafından başlatılmalıdır; ancak bu katman, [dao] katmanındaki yöntemleri çağırmadan önce
  • [dao] katmanındaki yöntemler, işlem varsa buna katılabilmek için işlemin varlığından haberdar olmalıdır
  • işlem, [métier] katmanı tarafından sonlandırılmalıdır.

[dao] katmanındaki yöntemlerin, devam eden bir işlemin varlığından haberdar olabilmesi için, bu işlemi [dao] katmanındaki her yöntemin parametresi olarak eklenebilir. Bu parametre, arayüzün yöntemlerinin imzasında yer alacak ve bu da arayüzü belirli bir veri kaynağına, yani veritabanına bağlayacaktır. İş parçacığının yerel verileri bize daha zarif bir çözüm sunar: [métier] katmanı, işlemi iş parçacığının yerel verilerine yerleştirecek ve [dao] katmanı da buradan alacaktır. Böylece, [dao] katmanındaki yöntemlerin imzalarının değiştirilmesine gerek kalmaz.

Bu çözümü aşağıdaki Visual Studio projesiyle hayata geçiriyoruz:

  • [1]: çözümün tamamı
  • [2]: Kullanılan referanslar. [4] veritabanı bir SQL Server Compact veritabanı olduğundan, [System.Data.SqlServerCe] referansına sahip olmak gerekir.
  • [3]: Uygulamanın farklı katmanları.

[4] veritabanı, önceki bölümde, özellikle 9.3.1. paragrafında zaten kullanılan SQL Server Compact veritabanıdır.

 

Article sınıfı

Yukarıdaki [articles] tablosundaki bir satır, Article türünde bir nesne içinde kapsüllenmiştir:


namespace Chap8 {
    public class Article {
        // özellikler
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }

        // yapıcılar
        public Article() { 
        }

        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }

        // kimlik
        public override string ToString() {
            return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum);
        }
    }
}

[dao] katmanının arayüzü

[dao] katmanının IDao arayüzü şu şekilde olacaktır:


using System.Collections.Generic;

namespace Chap8 {
    public interface IDao {
        int InsertArticle(Article article);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • 5. satır: [articles] tablosuna bir öğe eklemek için
  • 6. satır: [articles] tablosundaki tüm satırları Article nesne listesine eklemek için
  • 7. satır: [articles] tablosundaki tüm satırları silmek için

[metier] katmanının arayüzü

[metier] katmanının IMetier arayüzü şu şekilde olacaktır:


using System.Collections.Generic;

namespace Chap8 {
    interface IMetier {
        void InsertArticlesInTransaction(Article[] articles);
        void InsertArticlesOutOfTransaction(Article[] articles);
        List<Article> GetAllArticles();
        void DeleteAllArticles();
    }
}
  • 5. satır: Bir işlem içinde bir ürün grubu eklemek için
  • 6. satır: aynısı, ancak işlem olmadan
  • 7. satır: tüm ürünlerin listesini almak için
  • 8. satır: tüm ürünleri silmek için

[metier] katmanının uygulanması

IMetier arayüzünün uygulaması aşağıdaki gibi olacaktır:


using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;

namespace Chap8 {
    public class Metier : IMetier {
        // katman [dao]
        public IDao Dao { get; set; }
        // bağlantı dizesi
        public string ConnectionString { get; set; }

        // bir işlem içine ürün tablosu ekleme
        public void InsertArticlesInTransaction(Article[] articles) {
            // veritabanına bağlantı kurulur
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                // bağlantı açılıyor
                connexion.Open();
                // işlem
                SqlCeTransaction transaction = null;
                try {
                    // işlemin başlangıcı
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // işlemi iş parçacığına kaydedme
                    Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
                    // ürünlerin eklenmesi
                    foreach (Article article in articles) {
                        Dao.InsertArticle(article);
                    }
                    // işlem onaylanıyor
                    transaction.Commit();
                } catch {
                    // işlem geri alınır
                    if (transaction != null)
                        transaction.Rollback();
                }
            }
        }

        // işlem olmadan ürün tablosu ekleniyor
        public void InsertArticlesOutOfTransaction(Article[] articles) {
            // ürünlerin eklenmesi
            foreach (Article article in articles) {
                Dao.InsertArticle(article);
            }
        }

        // ürün listesi
        public List<Article> GetAllArticles() {
            return Dao.GetAllArticles();
        }
        // tüm ürünleri silme
        public void DeleteAllArticles() {
            Dao.DeleteAllArticles();
        }
    }
}

Sınıfın özellikleri şunlardır:

  • 9. satır: [dao] katmanına bir referans
  • 11. satır: ürün veritabanına bağlanmayı sağlayan bağlantı dizesi

Sadece zorluk çıkaran InsertArticlesInTransaction yöntemini açıklayacağız:

  • 16. satır: veritabanına bir bağlantı kurulur
  • satır 18: bağlantı açılır
  • 23. satır: Bir işlem oluşturulur
  • 25. satır: İşlem, "transaction" anahtarıyla ilişkilendirilerek iş parçacığının yerel verilerine kaydedilir
  • 27-29. satırlar: Eklenecek her bir ürün için [dao] katmanının tekli ekleme yöntemi çağrılır
  • 21. ve 32. satırlar: Tablonun eklenme işlemi bir try / catch bloğu ile denetlenir
  • 31. satır: Buraya gelindiğine göre, herhangi bir istisna oluşmamıştır. Bu durumda işlem onaylanır.
  • satır 34-35: bir istisna oluştuğu için işlem geri alınır
  • satır 37: using cümlesinden çıkılır. Satır 18'de açılan bağlantı otomatik olarak kapatılır.

[dao] katmanının uygulaması

IDao arayüzünün DAO uygulaması şu şekilde olacaktır:


using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;

namespace Chap8 {
    public class Dao : IDao {
        // bağlantı dizesi
        public string ConnectionString { get; set; }
        // sorgular
        public string InsertText { get; set; }
        public string DeleteAllText { get; set; }
        public string GetAllText { get; set; }

        // arayüz uygulaması

        // ürün ekleme
        public int InsertArticle(Article article) {
            // devam eden bir işlem var mı?
            SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
            // bağlantıyı al veya oluştur
            SqlCeConnection connexion = null;
            if (transaction != null) {
                // bağlantıyı alma
                connexion = transaction.Connection as SqlCeConnection;
            } else {
                // bağlantıyı oluştur
                connexion = new SqlCeConnection(ConnectionString);
                connexion.Open();
            }
            try {
                // ekleme komutunu hazırla
                SqlCeCommand sqlCommand = new SqlCeCommand();
                sqlCommand.Transaction = transaction;
                sqlCommand.Connection = connexion;
                sqlCommand.CommandText = InsertText;
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                sqlCommand.Parameters["@nom"].Value = article.Nom;
                sqlCommand.Parameters["@prix"].Value = article.Prix;
                sqlCommand.Parameters["@sa"].Value = article.StockActuel;
                sqlCommand.Parameters["@sm"].Value = article.StockMinimum;
                // yürütme
                return sqlCommand.ExecuteNonQuery();
            } finally {
                // eğer bir işlem içinde değilsek, bağlantıyı kapatırız
                if (transaction == null) {
                    connexion.Close();
                }
            }
        }

        // ürün listesi
        public List<Article> GetAllArticles() {
...
        }

        // ürünlerin silinmesi
        public void DeleteAllArticles() {
...
        }
    }
}

Sınıfın özellikleri şunlardır:

  • 9. satır: Ürün veritabanına bağlanmayı sağlayan bağlantı dizesi
  • 11. satır: Bir ürün eklemek için kullanılan SQL komutu
  • 12. satır: tüm ürünleri silmek için SQL komutu
  • 13. satır: tüm ürünleri almak için SQL komutu

Bu özellikler, aşağıdaki [App.config] yapılandırma dosyasından başlatılacaktır:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
    <appSettings>
        <add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/>
        <add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/>
        <add key="deleteAllText" value="delete from articles"/>
    </appSettings>
</configuration>

InsertArticle yöntemini açıklayalım:

  • 20. satır: [metier] katmanının iş parçacığına yerleştirmiş olabileceği olası işlemi alıyoruz
  • 23-25. satırlar: İşlem varsa, bu işlemin bağlı olduğu bağlantı alınır.
  • 26-30. satırlar: Aksi takdirde, yeni bir bağlantı oluşturulur ve açılır.
  • satır 33-44: Ekleme komutu hazırlanır. Bu komut parametrelerle ayarlanır (bkz. App.config'in g satırı).
  • 33. satır: Command nesnesi oluşturulur.
  • 34. satır: Bu, geçerli işleme ilişkilidir. Eğer bu işlem mevcut değilse (işlem=null), bu durum SQL komutunun açık bir işlem olmadan yürütülmesi anlamına gelir. Bu durumda yine de örtük bir işlem olduğu unutulmamalıdır. SQL Sunucusu CE ile bu örtük işlem varsayılan olarak autocommit modundadır: SQL emri, yürütülmesinden sonra committé olur.
  • 35. satır: Command nesnesi, geçerli bağlantıyla ilişkilendirilir
  • 36. satır: Yürütülecek SQl metni belirlenir. Bu, App.config'in g satırındaki parametreli sorgudur.
  • 37-44. satırlar: Sorgunun 4 parametresi başlatılır
  • satır 46: sorgu yürütülür.
  • satır 49-51: Unutulmamalıdır ki, işlem yoksa veritabanıyla yeni bir bağlantı açılmıştır (satır 26-30). Bu durumda bağlantı kapatılmalıdır. İşlem varsa, bağlantı kapatılmamalıdır çünkü bu bağlantıyı [metier] katmanı yönetmektedir.

Diğer iki yöntem ise "Veritabanları" bölümünde ele alınan konuları tekrarlar:


        // ürün listesi
        public List<Article> GetAllArticles() {
            // ürün listesi - başlangıçta boş
            List<Article> articles = new List<Article>();
            // bağlantı kullanımı
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                // bağlantı açma
                connexion.Open();
                // select sorgusu ile sqlCommand'i çalıştırır
                SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
                using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
                    // sonuç işleme
                    while (reader.Read()) {
                        // geçerli satırı işleme
                        articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
                    }
                }
            }
            // sonuç döndürülüyor
            return articles;
        }

        // kayıtların silinmesi
        public void DeleteAllArticles() {
            using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
                // bağlantı açma
                connexion.Open();
                // sqlCommand'i güncelleme isteği ile çalıştırma
                new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
            }
}

[console] test uygulaması

[console] test uygulaması şu şekildedir:


using System;
using System.Configuration;

namespace Chap8 {
    class Program {
        static void Main(string[] args) {
            // yapılandırma dosyasının işlenmesi
            string connectionString = null;
            string insertText;
            string getAllText;
            string deleteAllText;
            try {
                // bağlantı dizesi
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
                // diğer parametreler
                insertText = ConfigurationManager.AppSettings["insertText"];
                getAllText = ConfigurationManager.AppSettings["getAllText"];
                deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
            // katman oluşturma [dao]
            Dao dao = new Dao();
            dao.ConnectionString = connectionString;
            dao.DeleteAllText = deleteAllText;
            dao.GetAllText = getAllText;
            dao.InsertText = insertText;
            // katman oluşturma [métier]
            Metier metier = new Metier();
            metier.Dao = dao;
            metier.ConnectionString = connectionString;
            // ürün tablosu oluşturuluyor
            Article[] articles = new Article[2];
            for (int i = 0; i < articles.Length; i++) {
                articles[i] = new Article(0, "article", 100, 10, 1);
            }
            // tüm ürünler siliniyor
            Console.WriteLine("Suppression de tous les articles...");
            metier.DeleteAllArticles();
            // tabloyu işlem dışı olarak eklenir
            Console.WriteLine("Insertion des articles hors transaction...");
            try {
                metier.InsertArticlesOutOfTransaction(articles);
            } catch (Exception e){
                Console.WriteLine("Exception : {0}", e.Message);
            }
            // ürünler görüntülenir
            Console.WriteLine("Liste des articles");
            AfficheArticles(metier);
            // tüm ürünleri silinir
            Console.WriteLine("Suppression de tous les articles...");
            metier.DeleteAllArticles();
            // tabloyu bir işleme eklenir
            Console.WriteLine("Insertion des articles dans une transaction...");
            metier.InsertArticlesInTransaction(articles);
            // ürünleri görüntüleme
            Console.WriteLine("Liste des articles");
            AfficheArticles(metier);
        }

        private static void AfficheArticles(IMetier metier) {
            // ürünleri görüntüler
            foreach(Article article in metier.GetAllArticles()){
                Console.WriteLine(article);
            }
        }

    }
}
  • 12-22. satırlar: [App.config] dosyası kullanılır.
  • 24-28. satırlar: [dao] katmanı örneklenir ve başlatılır
  • 30-32. satırlar: [metier] katmanı için de aynı işlem yapılır
  • satır 34-37: Aynı ada sahip 2 kalemden oluşan bir tablo oluşturulur. SQL sunucusundaki [articles] tablosunda, [dbarticles.sdf] için ad üzerinde tekillik kısıtlaması vardır. Bu nedenle, ikinci ürünün eklenmesi reddedilecektir. Tablonun eklenmesi işlem dışı olarak yapılırsa, ilk ürün önce eklenecek ve kalacaktır. Tablonun eklenmesi bir işlem içinde yapılırsa, ilk ürün önce eklenecek, ardından işlemin Rollback aşamasında silinecektir.
  • satır 39-50: 2 kalemden oluşan tablonun işlem dışı eklenmesi ve doğrulama.
  • satır 52-59: Aynı işlem, ancak bir işlem içinde

Çalıştırma sonuçları şunlardır:

1
2
3
4
5
6
7
8
9
Suppression de tous les articles...
Insertion des articles hors transaction...
Exception : A duplicate value cannot be inserted into a unique index. [ Table na
me = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ]
Liste des articles
[7,article,100,10,1]
Suppression de tous les articles...
Insertion des articles dans une transaction...
Liste des articles
  • 5-6. satırlar: İşlem dışı ekleme, 1. ürünü veritabanında bıraktı
  • 9. satır: İşlem içinde yapılan ekleme, veritabanında hiçbir ürün bırakmadı

10.9.3. Sonuç

Yukarıdaki örnek, işlem yönetimi açısından iş parçacığına özgü verilerin önemini göstermiştir. Bu örnek aynen tekrarlanmamalıdır. Spring, Nhibernate gibi çerçeveler bu tekniği kullanır ancak bunu daha da şeffaf hale getirir: [metier] katmanının, [dao] katmanının bundan haberdar olmasına gerek kalmadan işlemlerden yararlanması mümkündür. Bu durumda, [dao] katmanının kodunda hiçbir Transaction nesnesi bulunmaz. Bu, AOP (Aspects Oriented Programming) adı verilen bir proxy tekniği aracılığıyla sağlanır. Yine, okuyucuyu bu çerçeveleri kullanmaya teşvik etmekten başka bir şey yapamayız.

10.10. Daha fazla bilgi için...

İş parçacığı senkronizasyonu gibi zorlu bir konuyu daha derinlemesine incelemek için, bu belgenin giriş bölümünde referans verilen C# 3.0 kitabının Threading başlıklı bölümünü okuyabilirsiniz. Bu bölümde, farklı durumlar için çok sayıda senkronizasyon tekniği ele alınmaktadır.