Forum: D Programlama Dili RSS
std.thread ile işletim dizileri
acehreli (Moderatör) #1
Kullanıcı başlığı: Ali Çehreli
Üye Haz 2009 tarihinden beri · 4527 mesaj
Grup üyelikleri: Genel Moderatörler, Üyeler
Profili göster · Bu konuya bağlantı
Konu adı: std.thread ile işletim dizileri
(Düzeltme: Yazım hataları ve daha açık ifadeler.)

Önce sözlük notu: Ben "thread" karşılığı olarak bir kaç senedir "işletim dizisi"ni kabul etmiştim ve "multi threaded" yerine de "çoklu işletim dizileri" duyuyordum ve kullanıyordum. Aşağıda daha kısa olarak "işletici" de diyeceğim.

D, çoklu işletim dizileri konusunda üstün. Özellikle bellek modeli (memory model), paylaşımı kısıtlayacak şekilde tasarlanmış. Örneğin evrensel (global) değişkenler, C ve C++ gibi başka dillerin tersine, erişime kapalı oluyorlar. Paylaşılmaları istenen veri 'shared' belirteci ile paylaşıma açılıyor. Evrensel değişkenler __gshared anahtar sözcüğü ile paylaşıma açılıyorlar. std.concurrency modülü işletim dizilerinin "mesajlaşmaları" üzerine kurulu... Aşağıdaki tam olarak bu konu ile ilgili değil...

Bu sabah std.thread modülüyle ve 'synchronized' anahtar sözcüğüyle oynamaya karar verdim.

Önce temel bir örnek:

import std.stdio;
import core.thread;
 
class SayıYazıcı : Thread
{
    int sayı;
 
    this(int sayı)
    {
        /* Bu işleticinin hangi üye işlevle başladığının üst sınıfa
         * bildirilmesi */
        super(&işlet);
 
        this.sayı = sayı;
    }
 
    void işlet()
    {
        writeln(sayı, " başla");
 
        /* Biraz bekle. Bu değer 100 nano saniye birimindedir; 10_000_000,
         * 1 saniye anlamına gelir. (Nanoyu 1 yapmak için 9 sıfır gerektiği
         * için, 100 nano için 7 sıfır yetiyor.)
         */
        sleep(10_000_000);
 
        writeln(sayı, " bitir");
    }
}
 
void main()
{
    Thread[] işleticiler;
 
    /* İşletim dizisi nesneleri oluştur */
    foreach (sayı; 0 .. 5) {
        işleticiler ~= new SayıYazıcı(sayı);
    }
 
    /* Hepsini başlat */
    foreach (işletici; işleticiler) {
        işletici.start();
    }
 
    // Sonlanmadan önce hepsinin işlerini bitirmelerini bekle
    thread_joinAll();
}

İşletim dizilerinin gücü, bir işlem beklenirken başka işlemlerin yapılabilmesidir. Bir işletici beklerken, başka işleticiler devam edebilirler.

İşletim dizileri aynı veri üzerinde çalışırlarken önemli bir konu, belirli bir veriye aynı anda ancak tek işleticinin erişmesidir. Yoksa birbirlerinin işlerine karışmış olurlar. Örneğin veriyi belirli bir anda tek işletici değiştirmelidir.

Bunun örneği olarak bir dizginin sonuna karakter ekleme gibi bir işimiz olsun. Ama eklenecek karakterlerin hesabı zaman alıyor olsun. Örneğin uzun bir hesap olabilir, internetteki yavaş bir sunucudaki bir bilgi kullanılıyor olabilir, yerel ama yavaş bir diskten okunuyor olabilir, bir insanın yanıtı bekleniyor olabilir, vs. Verinin hesaplanması için böyle bekleme kavramını, işletim dizisini durduran sleep() işleviyle temsil edeceğim.

Aşağıdaki program, 'synchronized' deyimi kullanılmadan çalıştırılsa, veriler tutarsız olur. Programın içine açıklamalar yerleştirdim.

Bu programı önce olduğu gibi, yani 'synchronized' deyimini etkinleştirmeden çalıştırın:

import std.stdio;
import core.thread;
import std.string;
 
/* İşletim dizilerinin sonuna ekledikleri toplam veri adedi. Bu değer kadar
 * işletim dizisi grubumuz olacak. */
immutable üretilenVeriAdedi = 2;
 
/* Her veri için kaç tane işletim dizisinin çalışacağı. */
immutable veriBaşınaİşleticiAdedi = 10;
 
/* Her işletim dizisi verinin sonuna bu kadar sayıda ek yapacak. */
immutable işleticiDöngüTekrarı = 5;
 
/*
 * İşletim dizisinin veriye eklerken karşılaşacağı yavaşlık. Ekin baş
 * tarafını yaptıktan sonra bu kadar süre yavaşlayacak, ekin geri kalanı bu
 * kadar süre sonra eline geçecek.
 *
 * Bu değer 100 nano saniye birimindedir; 10_000_000, 1 saniye anlamına
 * gelir.
 */
immutable beklemeSüresi = 1_000_000;
 
/* synchronized deyimine verilecek olan ve aynı veri üzerinde çalışan
 * işletim dizilerinin birbirlerine karışmamak için paylaştıkları nesne.
 *
 * Bu, 'mutex' kavramı gibi; ama D'de bu iş çok daha kolay: bunun bir sınıf
 * olması yeterli; hiçbir üye gerekmiyor.  */
class Paylaşım
{}
 
/* İşletim dizileri Thread'den türerler. */
class İşletici : Thread
{
    /* Bu işleticinin hangi veri üzerinde çalıştığı */
    int veriNumarası;
 
    /* Bu işleticinin numarası */
    int işleticiNumarası;
 
    /* 'ref' türünde üye olamadığı için gösterge kullanmak zorundayız. Ama
     * kavram olarak 'ref' ile aynı şey. */
    string * veri;
 
    /* Bu işleticinin kendi grubundaki diğer işleticilerle karışmaması için
     * 'synchronized' deyimine verilecek olan nesne. */
    Paylaşım paylaşım;
 
    this(int veriNumarası,
         int işleticiNumarası,
         ref string veri,
         Paylaşım paylaşım)
    {
        /* Bu işleticinin hangi üye işlevle başladığının üst sınıfa
         * bildirilmesi */
        super(&işlet);
 
        this.veriNumarası = veriNumarası;
        this.işleticiNumarası = işleticiNumarası;
        this.veri = &veri;
        this.paylaşım = paylaşım;
 
        writeln(kimlik, " oluşturuldu");
    }
 
    void işlet()
    {
        writeln(kimlik, " başladı");
 
        foreach (döngü; 0 .. işleticiDöngüTekrarı) {
            writeln(kimlik, ": döngü: ", döngü);
 
            /*
             * BURASI!
             *
             * Bu synchronized satırlarından birisini etkinleştirin. İfade
             * kullanmayan deyim, bütün işleticilerin birbirlerini
             * beklemelerine neden olur ve programı çok yavaşlatır.
             *
             * 'paylaşım' ifadesi kullanan deyim ise, yalnızca aynı
             * 'paylaşım' nesnesini kullanan, yani yalnızca aynı veri
             * üzerinde çalışan işletim dizilerinin birbirlerini
             * beklemelerini sağlar.
             */
 
//          synchronized
//          synchronized (paylaşım)
 
            {
                /* Veriye ek yapıyoruz. Amacımız, verinin sonuna yapılan
                 * ekin tutarlı olması
                 *
                 * Yapılan ek, (4.0.0:4.0.0) düzeninde olacak. Tutarlı
                 * kabul edilmesi için ':' karakterinden önceki ve sonraki
                 * damganın aynı olması gerekiyor. Bu paragrafta kullanılan
                 * ek tutarlı, çünkü ':' karakterinden önceki ve sonraki
                 * damgalar aynı.
                 *
                 * Bozuk bir ek örneği: (1.1.4:1.2.3)
                 */
                *veri ~= format("(%s:", damga(döngü));
                sleep(beklemeSüresi);
                *veri ~= format("%s) ", damga(döngü));
            }
        }
    }
 
    @property string kimlik() const
    {
        return format("%s.%s", veriNumarası, işleticiNumarası);
    }
 
    string damga(int döngü) const
    {
        return format("%s.%s", kimlik, döngü);
    }
}
 
void main()
{
    /* İşletim dizilerinin sonlarına ek yaparak ürettikleri veriler */
    string[üretilenVeriAdedi] ortakVeriler;
 
    /* Aynı veri üzerinde çalışan işletim dizilerinin ek yapmadan önce
     * birbirlerini beklemeleri için kullanılan nesneler */
    Paylaşım[üretilenVeriAdedi] paylaşımNesneleri;
 
    /* Bütün işletim dizileri */
    Thread[] işleticiler;
 
    foreach (veriNumarası, ref veri; ortakVeriler) {
        /* Her veri için bir tane de paylaşım nesnesi oluşturuyoruz.
         *
         * Aslında veriyi salt 'string' yapmak yerine bir string ve bir
         * Paylaşım nesnesinden oluşan bir yapı (struct) olarak tanımlamak
         * daha mantıklı olurdu. Burada farklı diziler olarak tanımlamışız.
         */
        paylaşımNesneleri[veriNumarası] = new Paylaşım;
 
        /* Her veri için birden fazla işletim dizisi çalışacak */
        foreach (işleticiNumarası; 0 .. veriBaşınaİşleticiAdedi) {
            işleticiler ~= new İşletici(veriNumarası,
                                        işleticiNumarası,
                                        veri,
                                        paylaşımNesneleri[veriNumarası]);
 
            /* İşleticiyi başlat */
            işleticiler[$-1].start();
        }
    }
 
    /* Devam etmeden önce bütün işletim dizilerinin sonlanmalarını bekle */
    thread_joinAll();
 
    /* Üretilen bütün verileri göster */
    foreach (i, veri; ortakVeriler) {
        writefln("\nVeri %s:\n%s", i, veri);
    }
}

Programı o şekilde çalıştırırsanız, bütün program yarım saniye sürer. Bunun nedeni, her işleticinin döngüsünün 5 kere tekrarlanması ve her tekrardaki bekleme (sleep) süresinin 0.1 saniye olmasıdır. Her işletim dizisi diğerini beklemeden aynı anda çalışır ve hepsi aynı yarım saniyede tamamlanır.

Yalnız, üretilen veriler bozuk olur (tutarlı verinin nasıl olduğunu kod içinde açıklamıştım):

(1.1.0:(1.2.0:(1.4.0:(1.5.0: ...

Şimdi iki şey yapın:

1) Kod içinde BURASI diye işaretlenen yerdeki 'synchronized' satırını ektinleştirin. 'synchronized' bloğu, belirli bir anda tek işletim dizisi tarafından kullanılabilir. O zaman veriler tutarlı olur:

(0.1.0:0.1.0) (0.1.1:0.1.1) (0.1.2:0.1.2) ...

Ama, 'synchronized' bölümü sırasında tek işletim dizisi çalışabildiği için bütün programın çalışma süresi 10 saniyeye çıkar:

üretilenVeriAdedi * veriBaşınaİşleticiAdedi * işleticiDöngüTekrarı * beklemeSüresi = 2 * 10 * 5 * 0.1 saniye = 10 saniye

2) Yine kod içinde BURASI denen yere gidin ve bu sefer synchronized (paylaşım) satırını etkinleştirin. Her işletim dizisi bu sefer yalnızca kendisi ile aynı veri üzerinde çalışan işletim dizilerini beklediği için, her şey 5 saniyede tamamlanır. Yani iki adet veri olsa bile, her iki veri aynı anda üretildikleri için 10 saniye yerine 5 saniye beklenir. İki verinin üretimi aynı anda olur.

İşin güzel tarafı, üretilen veri adedi ne kadar artarsa artsın, bilgisayarın gücü yettiği sürece, işlemler hep 5 saniye sürer! :) Örneğin üretilenVeriAdedi'ni 2 yerine 10 yapın... :) On tane tutarlı veri elde edilir; ama yine 5 saniye sürer... (Ben 100 yaptığımda program hatayla sonlandı; nedenini bilmiyorum.)

Ali
Bu mesaj acehreli tarafından değiştirildi; zaman: 2010-06-29, 15:02.
canalpay (Moderatör) #2
Kullanıcı başlığı: Can Alpay Çiftçi
Üye Tem 2009 tarihinden beri · 1133 mesaj · Konum: İzmir
Grup üyelikleri: Genel Moderatörler, Üyeler
Profili göster · Bu konuya bağlantı
Mesajın daha tamamını okuyamadım. Aslında ilk okuduğumda tamamını okumuştum ancak yinede tam olarak Thread ne demek anlamamıştım. Thread ne olduğunu araştırıp ikinci defa okudum (İlk örnek koda kadar.). Şimdi bu Threadleri önemsemeye başladım.

Çok güzel şeyler yapılabiliyor ki ben hep böyle şeylerin nasıl yapılacağını merak ederdim.

Örneğin bir stateji oyununun temeli şöyle olsa gerek :

import std.stdio;
import core.thread;
 
class GeriSay:Thread
{
    int kaçS;
    char[] yapı;
    this(int kaçS,char[] yapı) 
    {
        super(&geriyeSay);
        this.kaçS=kaçS;
        this.yapı=yapı;
    }
    void geriyeSay()
    {
        while(kaçS>-1){
            sleep(10_000_000);
            --kaçS;
            if(kaçS==0)
            writeln(yapı," kuruldu.");
        }
    }
}
 
void main()
{
    writeln("2 hakkınız var, ne kurulsun ? Kışla kurulması için 'kışla',\n Gelişigüzel bir şey kurmak için rastgele bir şey yazın.
    \n (Kışla: 10 saniye diğer: 7 saniye)");
    for(int i=1;i<3;i++)
    {
        char[] kurulacak = readln().dup;
        if(kurulacak=="kışla")
        {
           Thread kışla = new GeriSay(10,kurulacak);
           kışla.start();
        } else {
           Thread diğer = new GeriSay(7,kurulacak);
           diğer.start();
       }
       
       thread_joinAll();
   }
}
acehreli (Moderatör) #3
Kullanıcı başlığı: Ali Çehreli
Üye Haz 2009 tarihinden beri · 4527 mesaj
Grup üyelikleri: Genel Moderatörler, Üyeler
Profili göster · Bu konuya bağlantı
İşletim dizileri çok zor konudur. Kodun hangi köşesini tutsanız elinizde kalır. :)

Daha bitirmediğim bir ACCU makalesi bu konuyu irdeliyor, ve çok bilinen şu kuralları yineliyor:

- kesinlikle gerekmiyorsa thread kullanmayın :) (ben işe alınmak için bir yerle mülakat yaparken konu thread'lere geldiğinde bunu söylemiştim; karşımdaki çok bilmiş kişi de gözlerini açarak çok şaşırmış ve kesin bana eksi puan vermişti; umarım o zamandan sonra deneyim kazanmıştır ;))

- gerektiği zaman, kodun olabildiğince küçük bir bölümlerinin paylaşılmalarına dikkat edin; yoksa üstesinden gelinemez

Şimdi çalıştığım yerde benim katkıda bulunduğum projede thread kullanmak yasak. Zaten bu noktadan sonra eklenemez de. Ama şirkette thread'lerden yararlanan projeler de var.

Ali
canalpay (Moderatör) #4
Kullanıcı başlığı: Can Alpay Çiftçi
Üye Tem 2009 tarihinden beri · 1133 mesaj · Konum: İzmir
Grup üyelikleri: Genel Moderatörler, Üyeler
Profili göster · Bu konuya bağlantı
2 tane aynı anda geri sayım yaptırırken Thread yerine daha güvenli kullanabileceğimiz bir şey var mı ?
acehreli (Moderatör) #5
Kullanıcı başlığı: Ali Çehreli
Üye Haz 2009 tarihinden beri · 4527 mesaj
Grup üyelikleri: Genel Moderatörler, Üyeler
Profili göster · Bu konuya bağlantı
Benim çalıştığım projede 'event loop' ("olay döngüsü"?) kullanılıyor.

Program bütün hazırlıklarını yaptıktan sonra bir kütüphane tarafından sağlanan olay döngüsüne giriyor. Bütün olaylar bu döngünün zamanı gelen işlevleri çağırması ile hallediliyore.

Çok küçük ve olayın özünü gösteren bir örnek:

import std.string;
import std.stdio;
import std.array;
import std.date;
import std.conv;
import std.random;
import core.thread// bu  yalnızca sleep için; iş parçacığı kullanmıyoruz
 
class OlayDöngüsü
{
    alias void delegate() İşlem;
 
    struct Olay
    {
        string isim;
        int tekrarSaniyesi;
        d_time başlamaZamanı;
        İşlem işlem;
 
        this (string isim, int tekrarSaniyesi, İşlem işlem)
        {
            this.isim = isim;
            this.tekrarSaniyesi = tekrarSaniyesi;
            this.başlamaZamanı = getUTCtime() + tekrarSaniyesi * ticksPerSecond;
            this.işlem = işlem;
        }
 
        int opCmp(Olay sağdaki) const
        {
            return cast(int)(başlamaZamanı - sağdaki.başlamaZamanı);
        }
    }
 
    Olay[] olaylar;
    bool bitti_mi;
 
    void ekle(string isim, int saniye, İşlem işlem)
    {
        olaylar ~= Olay(isim, saniye, işlem);
    }
 
    void bitsin()
    {
        bitti_mi = true;
    }
 
    bool devam()
    {
        foreach (ref olay; olaylar) {
            if (olay.başlamaZamanı <= getUTCtime()) {
                // zamanı gelmiş
                olay.işlem();
 
                // Tekrar kuralım
                olay = Olay(olay.isim, olay.tekrarSaniyesi, olay.işlem);
            }
        }
 
        return bitti_mi;
    }
};
 
// Parametrenin double olması daha kullanışlı
void sleep(double saniye)
{
    core.thread.Thread.sleep(cast(long)(saniye * 100_000_000));
}
 
void main()
{
    auto olayDöngüsü = new OlayDöngüsü;
 
    olayDöngüsü.ekle("birli", 1, { writeln("birli"); });
    olayDöngüsü.ekle("üçlü", 3, { writeln("üçlü"); });
    olayDöngüsü.ekle("beşli", 5, { writeln("beşli"); });
 
    olayDöngüsü.ekle("sonlan", 15, { writeln("bitiyor");
                                     olayDöngüsü.bitsin(); });
 
    // Olay döngüsüne giriliyor
    while (true) {
        bool bitti_mi = olayDöngüsü.devam();
        if (bitti_mi) {
            break;
        }
 
        // Bu program belirli bir süre uyur ve sonra döngüye devam eder
        // Böyle sabit bir bekleme değeri yerine, olayDöngüsü'nün bir
        // sonraki olayın ne zaman olduğunu bildirmesinden de
        // yararlanılabilir
        sleep(0.1);
 
        // Burada kullanıcıyla da etkileşilebilir. Zamanı gelen işlemler bu
        // işin bitmesini beklemek zorundadırlar:
        if (uniform (0, 20) == 0) {
            write("Lütfen Enter'a basın: ");
            readln();
        }
 
        // Veya başka işlemler de yapılabilir; yine, olay döngüsünün
        // bunları beklemesi gerekir
        // ...
    }
}

(TDK Bilgisayar Terimleri Karşılıklar Kılavuzu 'iş parçacığı' demiş; ben de beğeniyorum.)

O programda hiç iş parçacığı yok (daha doğrusu, ana programı oluşturan tek iş parçacığı var). O yüzden, bir işlev çalışırken diğerinin beklenmesi şarttır. Örneğin bir olay kullanıcı ile etkileşiyorsa, ve kullanıcı çay koymaya gitmişse, her beş saniyede bir çağrılması gereken başka bir işlem beklemek zorundadır.

İş parçacıklarının önemsiz oldukları veya yanlış oldukları sanılmasın. Ancak kesinlikle gereken durumlarda kullanılmalıdır.

Ali
Doğrulama Kodu: VeriCode Lütfen resimde gördüğünüz doğrulama kodunu girin:
İfadeler: :-) ;-) :-D :-p :blush: :cool: :rolleyes: :huh: :-/ <_< :-( :'( :#: :scared: 8-( :nuts: :-O
Özel Karakterler:
Bağlı değilsiniz. · Şifremi unuttum · ÜYELİK
This board is powered by the Unclassified NewsBoard software, 20100516-dev, © 2003-10 by Yves Goergen
Şu an: 2017-11-19, 00:33:37 (UTC -08:00)