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. 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)
- Bir T iş parçacığı, öncelikle yukarıda sunulan yapıcılarından biri tarafından oluşturulur; örneğin:
burada Start, aşağıdaki iki imzadan birine sahip bir yöntemdir:
Bir iş parçacığının oluşturulması, iş parçacığının çalıştırılmasına yol açmaz.
- 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.
- 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.
- T iş parçacığının sonlandırılmasını zorlayabiliriz:
- T.Abort(), T iş parçacığından sonlanmasını ister.
- 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:
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ı.
, 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-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:
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:
- 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:
burada Run, aşağıdaki imzaya sahip bir yöntemdi:
Aşağıdaki imza da kullanılabilir:
Bu, başlatılan iş parçacığına bilgi aktarılmasını sağlar. Dolayısıyla
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:
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.
- 62. satır: sayaç okunur
- 66. satır: iş parçacığı 1 saniye durur. Dolayısıyla işlemciyi kaybeder
- 68. satır: Sayaç artırılır
- 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:
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:
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:
- 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:
bir Mutex M oluşturur | |
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. | |
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:
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:
é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:
- 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:
Bazen bir iş parçacığı bir bariyeri kapatmak isteyebilir. Bunu şu işlemle yapabilir:
Ö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:

Increment yönteminin imzası şöyledir:
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.
- Elimizde, bazı iş parçacıklarının veri yazdığı (yazarlar) ve diğerlerinin bu verileri okuduğu (okuyucular) bir dizi vardır.
- Yazarlar birbirleriyle eşittir ancak birbirlerini dışlarlar: Bir seferde yalnızca bir yazar verilerini diziye yazabilir.
- Okuyucular birbirine eşittir ancak birbirini dışlar: Bir seferde yalnızca bir okuyucu diziye yerleştirilen verileri okuyabilir.
- 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:
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:
konu başlıkları kilitli | |
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. | |
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.

Ş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.
- Gişeye gitmek için iş parçacıkları öncelikle jetonu talep eder:
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:
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:
- // gişede çalışma
- ....
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:
- 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:
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:
Sonuç olarak, C# 3.0 kitabının Threading bölümünde önerilen standart yapı şöyledir:
- giriş jetonunu tanımlayın:
- gişeye erişim talebinde bulunmak:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
}
// işlem penceresine geçiş
...
şuna eşdeğerdir
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:
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:
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:
- ThreadPool.QueueWorkItem(WaitCallBack)
- 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:
7 iş parçacığının yürütülmesi için:
- 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:
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:
- iş parçacığı, bir kullanıcının talebini karşılamak üzere oluşturulur veya bir iş parçacığı havuzundan talep edilir
- 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.
- İş 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.
- 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:
- clé ile ilişkili değeri almak için:
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 iş 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:
- 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.







