Skip to content

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

8.1. Giriş

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

Image

Bu sınıfın özelliklerinden ve yöntemlerinden yalnızca bazılarını kullanacağız:

CurrentThread - propriété statique
şu anda yürütülmekte olan iş parçacığını verir
Name - propriété d'objet
iş parçacığının adı
isAlive - propriété d'objet
iş parçacığının aktif olup olmadığını (true) veya olmadığını (false) gösterir
Start - méthode d'objet
bir iş parçacığının yürütülmesini başlatır
Abort - méthode d'objet
bir iş parçacığının çalışmasını kalıcı olarak durdurur
Sleep(n) - méthode statique
bir iş parçacığının çalışmasını n milisaniye süreyle durdurur
Suspend() - méthode d'objet
bir iş parçacığının yürütülmesini geçici olarak askıya alır
Resume() - méthode d'objet
askıya alınmış bir iş parçacığının yürütülmesini yeniden başlatır
Join() - méthode d'objet
engelleyici işlem - bir sonraki komuta geçmek için iş parçacığının bitmesini bekler

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:


' iş parçacığı kullanımı
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' mevcut iş parçacığını başlatma
        Dim main As Thread = Thread.CurrentThread
        ' görüntüleme
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' ad değiştiriliyor
        main.Name = "main"
        ' doğrulama
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' sonsuz döngü
        While True
            ' görüntüleme
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' geçici durdurma
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Ekran çıktısı:

dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59

Yukarıdaki örnek şu noktaları göstermektedir:

  • Main işlevi bir iş parçacığında sorunsuz bir şekilde çalışır
  • Thread.CurrentThread aracılığıyla bu iş parçacığının özelliklerine erişilebilir
  • Sleep yönteminin rolü. Burada, Main'i çalıştıran iş parçacığı, iki görüntüleme arasında düzenli olarak 1 saniye süreyle uyku moduna geçmektedir.

8.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üğünü söylediğimizde, genellikle yanlış bir ifade kullanmış oluruz. 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, suspend)
  • belirli bir süre boyunca uyku moduna geçerek (sleep)
  1. Bir T iş parçacığı ilk olarak oluşturucusu tarafından oluşturulur
Public Sub New(ByVal start As ThreadStart)

ThreadStart, delegate türündedir ve parametresiz bir fonksiyonun prototipini tanımlar:

Public Delegate Sub ThreadStart()

Klasik bir yapı şöyledir:

dim T as Thread=new Thread(new ThreadStart(run));

Parametre olarak geçirilen run işlevi, iş parçacığı başlatıldığında yürütülecektir.

  1. T iş parçacığının yürütülmesi, T.Start() tarafından başlatılır: T'nin oluşturucusuna geçirilen [run] işlevi, daha sonra T iş parçacığı tarafından çalıştırılacaktır. T.start() komutunu çalıştıran 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.
  1. thread, başlatıldıktan sonra bağımsız olarak çalışır. Yürütmekte olduğu start işlevi işini bitirdiğinde durur.
  1. T görevine belirli sinyaller gönderilebilir:
    1. T.Suspend() işine geçici olarak durmasını söyler
    2. T.Resume() işine devam etmesini söyler
    3. T.Abort() komutu, göreve kalıcı olarak durmasını söyler
  1. Ayrıca T.join() ile görevin tamamlanmasını bekleyebiliriz. Bu, engelleyici bir komuttur: bu komutu çalıştıran program, T görevi işini bitirene kadar engellenir. Bu, bir senkronizasyon yöntemidir.

Şu programı inceleyelim:


' seçenekler
Option Strict On
Option Explicit On 

' ad alanları
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' geçerli iş parçacığını başlat
        Dim main As Thread = Thread.CurrentThread
        ' İş parçacığına ad atama
        main.Name = "main"

        ' çalışma iş parçacıkları oluşturma
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' i iş parçacığı oluşturulur
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' iş parçacığının adı belirleniyor
            tâches(i).Name = "tache_" & i
            ' i iş parçacığının yürütülmesini başlatma
            tâches(i).Start()
        Next i
        ' ana işlevin sonu
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub

    Public Sub affiche()
        ' çalışmanın başladığı gösterilir
        Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
        ' 1 saniye boyunca uyku moduna geçme
        Thread.Sleep(1000)
        ' çalışma sonu gösteriliyor
        Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

Main işlevini çalıştıran ana iş parçacığı, affiche statik yöntemini çalıştırmakla görevli 5 iş parçacığı daha oluşturur. Sonuçlar şöyledir:

dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54

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()

işlemi, 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.

Programımızı değiştirerek Main yöntemini şu komutlarla sonlandıralım:

         ' işlem sonu
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Yeni programın çalıştırılması sonucunda şu çıktı elde edilir:

fin du thread main

Main işlevi tarafından oluşturulan iş parçacıkları çalıştırılmıyor. Bunun nedeni şu komuttur:

        Environment.Exit(0)

komutu bunu yapmaktadır: sadece Main iş parçacığını değil, uygulamanın tüm iş parçacıklarını silmektedir. Bu sorunun çözümü, Main yönteminin, kendi çalışmasını sonlandırmadan önce oluşturduğu iş parçacıklarının çalışmasının bitmesini beklemesidir. Bu, Thread sınıfındaki Join yöntemi ile yapılabilir:


         ' tüm iş parçacıklarının yürütülmesinin bitmesini bekleme
        For i = 0 To tâches.Length - 1
            ' i iş parçacığının yürütülmesinin bitmesini bekleme
            tâches(i).Join()
        Next i        'for
         ' işlem sonu
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Böylece şu sonuçlar elde edilir:

Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main

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

Artık varsayılan bir iş parçacığının varlığını ortaya çıkardık; bu iş parçacığı Main yöntemini yürütür ve başka iş parçacıkları nasıl oluşturulacağını da öğrendik. Şimdi 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 son derece uygun bir uygulama türü vardır: İnternet’teki istemci-sunucu uygulamaları. Böyle bir uygulamada, 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 göz atma, 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ç saat sürebileceğini biliriz. Elbette bir müşterinin bu kadar uzun 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.

8.4. Paylaşılan kaynaklara erişim

Yukarıda bahsedilen istemci-sunucu örneğinde, her iş parçacığı bir istemciye büyük ölçüde bağımsız bir şekilde hizmet eder. Bununla birlikte, iş parçacıkları, özellikle paylaşılan kaynaklara erişim için istemcilerine talep edilen hizmeti sunmak üzere işbirliği yapmak zorunda kalabilirler. Yukarıdaki şema, örneğin her gişede bir memurun bir müşteriye hizmet verdiği büyük bir idari kurumun gişelerini andırır. Zaman zaman bu memurların, müşterileri tarafından getirilen belgelerin fotokopilerini çekmeleri gerektiğini ve sadece bir fotokopi makinesi olduğunu varsayalım. İki görevli fotokopi makinesini aynı anda kullanamaz. Görevli i, fotokopi makinesinin görevli j tarafından kullanıldığını görürse beklemek zorunda kalır. Bu duruma “paylaşılan kaynağa erişim” denir ve bilgisayar bilimlerinde yönetimi oldukça zordur. Ş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ç değeri görüntülenir. Dolayısıyla n değerini bulmamız gerekir.

Program şu şekildedir:


' seçenekler
Option Explicit On 
Option Strict On

' iş parçacıklarının kullanımı
Imports System
Imports System.Threading

Public Class thread3
    ' sınıf değişkenleri
    Private Shared cptrThreads As Integer = 0

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' kullanım kılavuzu
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' argüman sayısının kontrolü
        If args.Length <> 1 Then
            ' hata
            Console.Error.WriteLine(syntaxe)
            ' durdurma
            Environment.Exit(1)
        End If
        ' argüman kalitesi denetimi
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' hata
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' son
            Environment.Exit(2)
        End Try
        ' iş parçacıklarının oluşturulması ve üretilmesi
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' oluşturma
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' adlandırma
            threads(i).Name = "tache_" & i
            ' başlatma
            threads(i).Start()
        Next i
        ' iş parçacıklarının bitmesini bekleme
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' affichage compteur
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' iş parçacığı sayacını artırır
        ' sayaç okuma
        Dim valeur As Integer = cptrThreads
        ' izleme
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' bekleme
        Thread.Sleep(1000)
        ' sayaç artırma
        cptrThreads = valeur + 1
        ' izleme
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
    End Sub
End Class

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

  1. sayaç okunduğunda
  2. iş parçacığı 1 saniye durur. Dolayısıyla işlemciyi kaybeder
  3. 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. Sayaç değerinin okunması ile 1 artırılmış değerin yazılması arasındaki sürede işlemciyi kaybetme riski mevcuttur. Nitekim, artırma işlemi işlemci düzeyinde kesintiye uğrayabilecek birkaç temel komutun gerçekleştirilmesini gerektirir. Dolayısıyla, 1 saniyelik bekleme süresini içeren 2. adım, bu riski sistematik hale getirmek için vardır. Elde edilen sonuçlar şunlardır:
dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_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ülüyor:

  • İlk iş parçacığı sayacı okur. 0 değerini bulur.
  • 1 saniye durur ve böylece işlemciyi serbest bırakır
  • ardından ikinci bir iş parçacığı işlemciyi devralır ve o da sayaç değerini okur. Önceki iş parçacığı henüz sayacı artırmadığı için değer hâlâ 0'dır. O da 1 saniye durur.
  • 1 saniye içinde 5 iş parçacığının hepsi sırayla geçip 0 değerini okuma fırsatı bulur.
  • Birbiri ardına uyanmaya başladıklarında, okudukları 0 değerini artırıp sayaca 1 değerini yazacaklar; bu durum ana program (Main) tarafından da doğrulanır.

Sorun nereden kaynaklanıyor? İkinci iş parçacığı, penceredeki sayacı güncelleme görevini tamamlamadan kesintiye uğrayan ilk iş parçacığı 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.

8.5. Paylaşılan bir kaynağa özel erişim

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


         ' sayaç okuma
        Dim valeur As Integer = cptrThreads
        ' bekleme
        Thread.Sleep(1000)
        ' sayaç artışı
        cptrThreads = valeur + 1

Bu kodu çalıştırmak için, bir iş parçacığının tek başına çalıştığından 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ırmaması gerekir. .NET platformu, kodun kritik bölümlerine tekil erişimi sağlamak için çeşitli araçlar sunar. Mutex sınıfını kullanacağız:

Image

Burada yalnızca aşağıdaki yapıcıları ve yöntemleri kullanacağız:

public Mutex()
bir M senkronizasyon nesnesi oluşturur
public bool WaitOne()
T1 iş parçacığı, M.WaitOne() işlemini yürütürken M senkronizasyon nesnesinin mülkiyetini talep eder. Mutex M hiçbir iş parçacığı tarafından tutulmuyorsa (başlangıçta durum böyledir), bu Mutex, talep eden T1 iş parçacığına "verilir". Biraz sonra, T2 iş parçacığı aynı işlemi yaparsa, bu iş parçacığı bloke olur. Çünkü bir Mutex 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ığı engeli kalkacaktı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 M.Lorsque mutex'inin sahipliğini bırakır; T1 iş parçacığı işlemciyi kaybeder, sistem ise Mutex M'yi bekleyen iş parçacıklarından birine işlemciyi verebilir. Bunlardan sadece biri sırayla işlemciyi alacaktır; Mutex M'yi bekleyen diğer iş parçacıkları ise bloke kalacaktır

Bir Mutex M, paylaşılan bir kaynak R'ye erişimi yönetir. Bir iş parçacığı, M.WaitOne() ile kaynak R'yi talep eder ve M.ReleaseMutex() ile geri verir. Bir seferde 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 bu iş parçacığı girer
' kritik bölüm
....
M.ReleaseMutex()

burada M, bir Mutex nesnesidir. Elbette, başka bir iş parçacığının kritik bölüme girebilmesi için artık gereksiz hale gelen bir Mutex nesnesini serbest bırakmayı asla unutmamak gerekir; aksi takdirde, serbest bırakılmayan bir Mutex'i bekleyen iş parçacıkları işlemciye asla erişemeyecektir. Ayrıca, iki iş parçacığının birbirini beklediği kilitlenme durumu (deadlock) da önlenmelidir. Zaman içinde birbirini izleyen aşağıdaki eylemleri ele alalım:

  • bir T1 iş parçacığı, M1 Mutex'inin sahipliğini alarak R1 paylaşımlı kaynağına erişim sağlar
  • T2 iş parçacığı, R2 adlı paylaşılan kaynağa erişmek için M2 adlı Mutex'in sahipliğini alır
  • T1 iş parçacığı, M2 Mutex'ini talep eder. Engellenir.
  • T2 iş parçacığı, M1 Mutex'ini talep ediyor. İş parçacığı kilitlenmiş durumda.

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. Az önce gördüklerimizi önceki örnekte uygulamaya koyarsak, uygulamamız şu şekilde olur:


' seçenekler
Option Explicit On 
Option Strict On

' iş parçacığı kullanımı
Imports System
Imports System.Threading

Public Class thread4
    ' sınıf değişkenleri
    Private Shared cptrThreads As Integer = 0    ' compteur de threads
    Private Shared autorisation As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' kullanım kılavuzu
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' argüman sayısının kontrolü
        If args.Length <> 1 Then
            ' hata
            Console.Error.WriteLine(syntaxe)
            ' durdurma
            Environment.Exit(1)
        End If
        ' argüman kalitesi denetimi
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
        End Try

        ' kritik bölüme erişim yetkisinin başlatılması
        autorisation = New Mutex

        ' iş parçacıklarının oluşturulması ve üretilmesi
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' oluşturma
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' adlandırma
            threads(i).Name = "tache_" & i
            ' başlatma
            threads(i).Start()
        Next i
        ' iş parçacıklarının bitmesini bekleme
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' sayaç görüntüleme
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' iş parçacığı sayacını artırır
        ' kritik bölüme girme izni isteniyor
        autorisation.WaitOne()
        ' sayaç okuma
        Dim valeur As Integer = cptrThreads
        ' izleme
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' bekleme
        Thread.Sleep(1000)
        ' sayaç artırma
        cptrThreads = valeur + 1
        ' takip
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' erişim izni verilir
        autorisation.ReleaseMutex()
    End Sub
End Class

Elde edilen sonuçlar beklenenle uyumludur:

dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5

8.6. Olaylara dayalı senkronizasyon

Bazen üretici-tüketici durumu olarak da adlandırılan aşağıdaki durumu ele alalım.

  1. Elimizde, bazı süreçlerin veri kaydettiği (üreticiler) ve diğerlerinin bu verileri okuduğu (tüketiciler) bir tablo var.
  2. Üreticiler birbirine eşittir ancak birbirini dışlar: Her seferinde yalnızca bir üretici verilerini tabloya kaydedebilir.
  3. Tüketiciler birbirine eşittir ancak birbirini dışlar: bir seferde yalnızca bir okuyucu tabloya yerleştirilen verileri okuyabilir.
  4. Bir tüketici, ancak bir üretici tabloya veri eklediğinde verileri okuyabilir ve bir üretici, tablodaki veriler tüketildiğinde yeni verileri tabloya ekleyebilir.

Bu açıklamada iki paylaşılan kaynak ayırt edilebilir:

    1. yazma yetkisi olan tablo
    2. okunur tablo

Bu iki paylaşımlı kaynağa erişim, daha önce görüldüğü gibi her kaynak için birer tane olmak üzere Mutex'ler aracılığıyla kontrol edilebilir. Bir tüketici okuma tablosunu elde ettikten sonra, içinde veri olup olmadığını kontrol etmelidir. Bunu bildirmek için bir olay kullanılacaktır. Benzer şekilde, yazma tablosunu elde eden bir üretici, bir tüketicinin tabloyu boşaltmasını beklemelidir. Burada da yine bir olay kullanılacaktır.

Kullanılacak olaylar, AutoResetEvent sınıfının bir parçası olacaktır:

Image

Bu tür bir olay, bir boole değerine benzer ancak aktif veya yarı aktif beklemeleri önler. Dolayısıyla, yazma hakkı bir peutEcrire boole değeri ile kontrol ediliyorsa, bir üretici yazma işleminden önce şu tür bir kod çalıştıracaktır:

while(peutEcrire==false)        ' attente active

veya

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

İlk yöntemde, iş parçacığı işlemciyi gereksiz yere meşgul eder. İkinci yöntemde ise, her 100 ms'de bir peutEcrire boole değişkeninin durumunu kontrol eder. AutoResetEvent sınıfı, durumu daha da iyileştirir: iş parçacığı, beklediği olay gerçekleştiğinde uyandırılmasını talep eder:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

İşlem

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

, peutEcrire adlı boole değerini false olarak başlatır.

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

işlemi, bir iş parçacığı tarafından yürütüldüğünde, peutEcrire boole değeri doğruysa iş parçacığının ilerlemesini sağlar; aksi takdirde, bu değer doğru olana kadar iş parçacığı bloke olur. Başka bir iş parçacığı, peutEcrire.Set() işlemiyle bu değeri doğru olarak, ya da peutEcrire.Reset() işlemiyle yanlış olarak değiştirecektir.

Üretici-tüketici programı şöyledir:


' okuma ve yazma iş parçacıklarının kullanımı
' paylaşılan kaynakların ve senkronizasyonun eşzamanlı kullanımını gösterir

' seçenekler
Option Explicit On 
Option Strict On

' iş parçacıklarının kullanımı
Imports System
Imports System.Threading

Public Class lececr

    ' sınıf değişkenleri
    Private Shared data(5) As Integer    ' ressource partagée entre threads lecteur et threads écrivain
    Private Shared lecteur As Mutex    ' variable de synchronisation pour lire le tableau
    Private Shared écrivain As Mutex    ' variable de synchronisation pour écrire dans le tableau
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' un générateur de nombres aléatoires
    Private Shared peutLire As AutoResetEvent    ' signale qu'on peut lire le contenu de data
    Private Shared peutEcrire As AutoResetEvent

    Public Shared Sub Main(ByVal args() As [String])

        ' oluşturulacak iş parçacığı sayısı
        Const nbThreads As Integer = 3

        ' bayrakların başlatılması
        peutLire = New AutoResetEvent(False)        ' on ne peut pas encore lire
        peutEcrire = New AutoResetEvent(True)        ' on peut déjà écrire

        ' senkronizasyon değişkenlerinin başlatılması
        lecteur = New Mutex         ' synchronise les lecteurs
        écrivain = New Mutex         ' synchronise les écrivains

        ' okuma iş parçacıklarının oluşturulması
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' oluşturma
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' başlatma
            lecteurs(i).Start()
        Next i

        ' yazma iş parçacıklarının oluşturulması
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' oluşturma
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' başlatma
            écrivains(i).Start()
        Next i

        'elin sonu
        Console.Out.WriteLine("fin de Main...")
    End Sub

    ' tablonun içeriğini okuma
    Public Shared Sub lire()
        ' kritik bölüm
        lecteur.WaitOne()        ' un seul lecteur peut passer
        peutLire.WaitOne()        ' on doit pouvoir lire

        ' tablo okuma
        Dim i As Integer
        For i = 0 To data.Length - 1
            '1 saniye bekleme
            Thread.Sleep(1000)
            ' görüntüleme
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' artık okunamıyor
        peutLire.Reset()
        ' yazılabilir
        peutEcrire.Set()
        ' kritik bölüm sonu
        lecteur.ReleaseMutex()
    End Sub

    ' tabloya yazma
    Public Shared Sub écrire()
        ' kritik bölüm
        ' sadece bir yazar geçebilir
        écrivain.WaitOne()
        ' yazma izni beklenmelidir
        peutEcrire.WaitOne()

        ' tabloya yazma
        Dim i As Integer
        For i = 0 To data.Length - 1
            '1 saniye bekleme
            Thread.Sleep(1000)
            ' görüntüleme
            data(i) = objRandom.Next(0, 1000)
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
        Next i

        ' artık yazma yapılamaz
        peutEcrire.Reset()
        ' okuma yapılabilir
        peutLire.Set()
        'kritik bölüm sonu
        écrivain.ReleaseMutex()
    End Sub
End Class

Çalıştırıldığında şu sonuçlar elde edilir:

dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C

Aşağıdaki noktalara dikkat çekilebilir:

  • Kritik bölümde işlemciyi kaybetmesine rağmen, her seferinde gerçekten de yalnızca 1 okuyucu bulunmaktadır: lire
  • Aynı anda yalnızca bir yazıcı vardır, ancak bu yazıcı kritik bölüm écrire'te işlemciyi kaybeder
  • bir okuyucu, tabloda okunacak bir şey olduğunda okur
  • bir yazıcı, tablo tamamen okunduktan sonra yazmaya başlar