Thread senkronizasyonu, aynı anda çalışan threadlerin birbirleriyle etkileşimini düzenleyen ve senkronize eden yöntemleri ifade eder. Bu, veri paylaşımı, kritik bölgelerin korunması, senkronizasyon araçlarının kullanımı gibi çeşitli tekniklerle gerçekleştirilebilir.

Temel olarak, thread senkronizasyonu, threadlerin eşzamanlı çalışmasını düzenleyerek, programın doğru ve tutarlı bir şekilde çalışmasını sağlar ve veri bütünlüğünü korur. Bu, çakışan threadlerin istenmeyen sonuçlara yol açmasını engeller.

Qt.io tanımı:
Thread’lerin amacı kodun paralel olarak çalışmasını sağlamak olsa da, thread’lerin durması ve diğer thread’leri beklemesi gereken zamanlar vardır. Örneğin, iki thread aynı anda aynı değişkene yazmaya çalışırsa sonuç tanımsız olur. Thread’leri birbirlerini beklemeye zorlama prensibine karşılıklı dışlama (mutual exclusion) denir. Veri gibi paylaşılan kaynakları korumak için yaygın bir tekniktir.
Qt, thread’leri senkronize etmek için düşük seviyeli ilkellerin yanı sıra yüksek seviyeli mekanizmalar da sağlar.

Low Level Senkronizasyon İlkeleri

Qt’de thread senkronizasyonu için genellikle dört temel low-level ilke bulunur:

İçerik Başlıkları

  1. Low Level Senkronizasyon İlkeleri
    1. QMutex
      1. Örnek Kod:
    2. QReadWriteLock
      1. Örnek Kod:
    3. QSemaphore
      1. Örnek Kod:
    4. QWaitCondition

QMutex

QMutex, karşılıklı dışlamayı (mutual exclusion) uygulamak için temel sınıftır. Bir thread, paylaşılan bir kaynağa erişim sağlamak için bir mutex’i kilitler. Eğer ikinci bir thread mutex zaten kilitliyken onu kilitlemeye çalışırsa, ilk thread görevini tamamlayıp mutex’in kilidini açana kadar ikinci thread uykuya alınır.

QT docs

Bir QMutex’in amacı, bir nesneyi, veri yapısını ya da kod bölümünü, aynı anda sadece bir iş parçacığının erişebileceği şekilde korumaktır (bu Java synchronized anahtar kelimesine benzer). Genellikle bir muteksi bir QMutexLocker ile kullanmak en iyisidir, çünkü bu kilitleme ve kilit açma işlemlerinin tutarlı bir şekilde yapılmasını kolaylaştırır.

QT docs

Bir mutex, bir threadin bir kaynağı ele geçirdiğini belirtir. Diğer threadler, o kaynağı kullanmak istediklerinde, önce ilk threadin görevini tamamlamasını beklerler ve uykuya alınırlar, ilk thread görevini tamamlayıp mutex kilidini açtıktan sonra sıradaki thread mutex’i kilitleyip (lock) görevine başlar. Bu şekilde, sadece bir iş parçacığı kaynağı aynı anda kullanabilir ve böylece veri bütünlüğü sağlanır. Veri yarışları gibi problemleri önlemek için kullanılır     

Örnek Kod:

Bu kodda, commonVariable adında bir static değişken tanımlıyoruz ve tüm thread’ler arasında paylaşılmasını sağlıyoruz. Mutex’i static olarak tanımlayarak tüm thread’lerin aynı mutex’i paylaşmasını sağlıyoruz. Her bir thread, mutex’i kilitleyip ortak değişkeni arttırıyor ve değeri ekrana yazdırıyor.

myThread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <QDebug>
#include <QMutex>
#include <QString>

class MyThread : public QThread
{
public:
    MyThread(const QString &name);

protected:
    void run() override;

private:
    QString name;
    static int commonVariable;
    static QMutex mutex;
};

#endif // MYTHREAD_H

myThread.cpp

#include "mythread.h"

int MyThread::commonVariable = 0;
QMutex MyThread::mutex;

MyThread::MyThread(const QString &name) : name(name) {}

void MyThread::run() {
    for (int i = 0; i < 5; ++i) {
        mutex.lock();
        ++commonVariable;
        qDebug() << name << ": Common Variable değeri" << commonVariable;
        mutex.unlock();
        sleep(1);
    }
}

main.cpp

#include <QCoreApplication>
#include <QDebug>
#include "mythread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    MyThread thread1("beyaz");
    MyThread thread2("siyah");

    thread1.start();
    thread2.start();

    thread1.wait();
    thread2.wait();

    return a.exec();
}

Yukarıdaki kodlar, iki adet thread oluşturarak bu thread’lerin aynı anda çalışmasını ve ortak bir değişkeni sırasıyla arttırmasını sağlar.

main.cpp dosyası, uygulamanın ana iş mantığını içerir. İki adet MyThread nesnesi oluşturur ve bunları başlatır. Daha sonra bu thread’lerin tamamlanmasını bekler.

mythread.h dosyası, MyThread sınıfının header dosyasıdır. MyThread sınıfı QThread sınıfından kalıtım alır. Her bir thread’in bir ismi ve ortak bir değişkeni arttırmak için kullanacağı bir mutex’i vardır.

mythread.cpp dosyası, MyThread sınıfının implementasyonunu içerir. Her bir thread, run fonksiyonunu override ederek, bu fonksiyon içinde bir döngü ile ortak değişkeni sırasıyla arttırır. Her bir adımda, mutex kilidi alınır, ortak değişken artırılır, değeri ekrana yazdırılır ve mutex kilidi serbest bırakılır. Bu işlem her bir thread için beş kez tekrarlanır.

Sonuç olarak, her iki thread, ortak değişkeni sırasıyla arttırırken, birbirlerini beklemeden işlemlerini gerçekleştirirler. Bu sayede, mutex kullanarak thread’ler arasında senkronizasyon sağlanmış olur ve veri bütünlüğü korunur.

Mutex kullanmadan nasıl bir çıktı aldığımız görmek için aşağıdaki çıktıyı kontrol edebilirsiniz, çıktıda da görüldüğü üzere ortak değişkene threadler rastgele ve farklı zamanlarda ulaştığı için değişkenimizin değerini takip etmek zor.

"siyah" : Common Variable değeri 2
"beyaz" : Common Variable değeri 1
"beyaz" : Common Variable değeri 3
"siyah" : Common Variable değeri 4
"siyah" : Common Variable değeri 6
"beyaz" : Common Variable değeri 6
"siyah" : Common Variable değeri 8
"beyaz" : Common Variable değeri 8
"beyaz" : Common Variable değeri 10
"siyah" : Common Variable değeri 10

Mutex ile çıktı:

"beyaz" : Common Variable değeri 1
"siyah" : Common Variable değeri 2
"beyaz" : Common Variable değeri 3
"siyah" : Common Variable değeri 4
"beyaz" : Common Variable değeri 5
"beyaz" : Common Variable değeri 6
"beyaz" : Common Variable değeri 7
"siyah" : Common Variable değeri 8
"siyah" : Common Variable değeri 9
"siyah" : Common Variable değeri 10

Bu çıktıda ise, her iki thread, ortak değişkeni mutex kontrolünde sırasıyla arttırır. Bu sayede, mutex kullanarak thread’ler arasında senkronizasyon sağlamış oluruz.

Not: mutex->lock() ve mutex->unlock() kullanmak yerine sadece QMutexLocker kullanabiliriz, scpoe dışına çıktığında mutex otomatik olarak unlock yapılır.

    mutex.lock();
    ++commonVariable;
    qDebug() << name << ": Common Variable değeri" << commonVariable;
    mutex.unlock();
    sleep(1);

QMutexLocker ile kullanım:

    QMutexLocker locker(&mutex);
    ++commonVariable;
    qDebug() << name << ": Common Variable değeri" << commonVariable;
    sleep(1);

QReadWriteLock

QReadWriteLock, ” read ” ve ” write ” erişimi arasında ayrım yapması dışında QMutex‘e benzer. Bir veri parçası yazılmadığında, birden fazla iş parçacığının aynı anda ondan okuması güvenlidir. Bir QMutex, birden fazla okuyucuyu paylaşılan verileri okumak için sırayla okumaya zorlar, ancak bir QReadWriteLock eşzamanlı okumaya izin verir, böylece paralelliği geliştirir.

QT docs

Okuma-yazma (read-write) kilidi, okuma ve yazma için erişilebilen kaynakları korumaya yönelik bir senkronizasyon aracıdır. Bu tür bir kilit, birden fazla iş parçacığının aynı anda salt okunur erişime sahip olmasına izin vermek istiyorsanız kullanışlıdır, ancak bir iş parçacığı kaynağa yazmak istediğinde, yazma işlemi tamamlanana kadar diğer tüm iş parçacıkları engellenmelidir.

Birçok durumda QReadWriteLock, QMutex‘in doğrudan rakibidir. QReadWriteLock, çok sayıda eşzamanlı okuma varsa ve yazma işlemi seyrek gerçekleşiyorsa iyi bir seçimdir.

Reader’lar tarafından writer’ların sonsuza kadar bloke edilmemesini sağlamak için, bir lock’u elde etmeye çalışan reader’lar, erişim için bekleyen bloke edilmiş bir writer varsa, lock’a şu anda sadece diğer reader’lar tarafından erişilse bile başarılı olamayacaklardır. Ayrıca, kilide bir writer tarafından erişilirse ve başka bir writer gelirse, o writer bekleyen okuyuculara göre önceliğe sahip olacaktır.

QT docs

QReadWriteLock, okuma-yazma senaryoları için optimize edilmiş bir kilitleme mekanizmasıdır. Bu, bir kaynağa aynı anda birden çok iş parçacığının okuma erişimine izin verirken, yazma işlemi yapılırken diğer iş parçacıklarının okuma ve yazma işlemlerini engeller. Yani, QReadWriteLock, veriyi sık sık okunduğu ancak nadiren yazıldığı durumlarda performansı artırabilir.

Örnek Kod:

Bu örnekte 3 adet sınıfımız bulunuyor; WorkerClass sınıfı ortak bir veriyi tutuyor, bu veri, threadler ile güncellenmek ve okunmak için tanımlandı. Ayrıca bu sınıfta iki adet fonksiyon mevcut; read ve update fonksiyonu.
ReaderThread sınıfı QThread’den miras alan bir sınıf, override edilen run fonksiyonu ile ortak değeri okumaya çalışacak. Bu sınıf ayrıca name isimli bir parametre alıyor, bu isim çıktıda hangi threadin değeri okuduğunu görmemiz için lazım.
UpdaterThread yine aynı şekildeQThread’den miras aldı, override edilen run fonksiyonu ile ortak değeri her seferinde 1 arttıracak.

workerclass.h

#ifndef WORKERCLASS_H
#define WORKERCLASS_H

#include <QString>
#include <QReadWriteLock>

class WorkerClass
{
public:
    WorkerClass();

    void read(const QString  & text);

    void update();

private:
    int m_count;
    QReadWriteLock m_read_write_lock;
};
#endif // WORKERCLASS_H

workerclass.cpp

#include "workerclass.h"
#include <QDebug>

WorkerClass::WorkerClass(){
    m_count = 0;
}

void WorkerClass::read(const QString  & text){
    QReadLocker readLocker(&m_read_write_lock);
    qDebug() << text << "Okunan Değer : " << m_count ;

}

void WorkerClass::update(){
    QWriteLocker writeLocker(&m_read_write_lock);
    m_count++;
    qDebug() << "Değer Güncellendi";
}

readerThread.h

#ifndef READERTHREAD_H
#define READERTHREAD_H

#include <QThread>
#include <QDebug>
#include <QReadWriteLock>
#include "workerclass.h"

class ReaderThread : public QThread
{
    Q_OBJECT
public:
    explicit ReaderThread(const QString & name, WorkerClass * workerClass, QObject *parent = nullptr);

signals:

public slots:
private:
    QString m_name;
    WorkerClass * m_worker_class;

    // QThread interface
protected:
    void run() override;
};

#endif // READERTHREAD_H

readerThread.cpp

#include "readerThread.h"

ReaderThread::ReaderThread(const QString & name, WorkerClass * workerClass,
                         QObject *parent) : QThread(parent),
    m_name(name),
    m_worker_class(workerClass){}

void ReaderThread::run(){
    for (int i = 0; i < 5; ++i) {
        m_worker_class->read(m_name);
        sleep(1);
    }
}

updaterThread.h

#ifndef UPDATERTHREAD_H
#define UPDATERTHREAD_H

#include <QThread>
#include <QDebug>
#include <QReadWriteLock>
#include "workerclass.h"

class UpdaterThread : public QThread
{
    Q_OBJECT
public:
    explicit UpdaterThread(WorkerClass * workerClass, QObject *parent = nullptr);

signals:

public slots:

private:
    WorkerClass * m_worker_class;

    // QThread interface
protected:
    void run() override;
};

#endif // UPDATERTHREAD_H

updaterThread.cpp

#include "updaterthread.h"

UpdaterThread::UpdaterThread(WorkerClass * workerClass,
                                 QObject *parent) : QThread(parent),
    m_worker_class(workerClass){}

void UpdaterThread::run(){
    for (int i = 0; i < 5; ++i) {
        m_worker_class->update();
        sleep(1);
    }
}

main.cpp

#include <QCoreApplication>
#include "readerthread.h"
#include "updaterthread.h"
#include "workerclass.h"

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    WorkerClass workerClass;

    ReaderThread white("Beyaz Okuyucu", &workerClass), black("Siyah Okuyucu", &workerClass);
    UpdaterThread updater(&workerClass);

    white.start();
    black.start();
    updater.start();

    white.wait();
    black.wait();
    updater.wait();

    return a.exec();
}

Main.cpp dosyamızda iki adet okuyucu ve bir adet güncelleyici thread bulunmakta. Okuyucu threadlere “beyaz okuyucu” ve “siyah okuyucu” isimlerini vererek hangi okuyucunun veriye eriştiğini ekrana bastırabiliriz. Hem reader hem de updater threadlerine ortak bir workerClass nesnesini ilettik her üç thread’de ortak nesne üzerinden işlem gerçekleştirerek veriyi okuma ve yazma çalışmaları yapacaktır.

void WorkerClass::read(const QString  & text){
    QReadLocker readLocker(&m_read_write_lock);
    qDebug() << text << "Okunan Değer : " << m_count ;

}

void WorkerClass::update(){
    QWriteLocker writeLocker(&m_read_write_lock);
    m_count++;
    qDebug() << "Değer Güncellendi";
}

WorkerClass içerisinde yer alan ve veri üzerine işlem yapan bu fonksiyonların kendilerine erişen threadler tarafından senkronize çalışması için QWriteLocker sınıfını kullandık. Farklı bir kullanım şekli için:

    QReadWriteLock lock;    
    lock.lockForRead();
    read_file();
    lock.unlock();
    ...
    lock.lockForWrite();
    write_file();
    lock.unlock();
    ...

Uygulamamızın çıktısı:

QWriteLock kullanmadan kodumuzu çalıştırdığımızda birkaç denemeden sonra aşağıdaki hatayı alabiliriz, çoğu zaman kod düzgün çalışsada aslında sağlıklı bir yapı yoktur ve ne zaman hata alacağımızı kestiremeyiz dolayısıyla programın akışında çökmelerle karşılaşabiliriz. Aynı anda birden fazla thread’in aynı değişkene erişmeye çalışması durumunda bu yazma hatasını alabilirsiniz. Çünkü bu durumda birden fazla thread aynı anda değişkeni güncellemeye çalışabilir ve bu da veri bütünlüğünü bozabilir.

Bir thread bir değişkeni okurken, aynı anda başka bir thread bu değişkeni değiştirmeye çalışırsa, veri bütünlüğü bozulabilir ve access violation veya race conditions gibi hatalar oluşabilir

QSemaphore

QSemaphore, QMutex ‘in belirli sayıda aynı kaynağı koruyan bir genellemesidir. Buna karşılık, bir QMutex tam olarak bir kaynağı korur. Semaforları kullanan Üretici ve Tüketici örneği, semaforların tipik bir uygulamasını gösterir: bir üretici ve tüketici arasında dairesel bir tampona erişimi senkronize etmek.

Qt docs

Semafor, muteksin genelleştirilmiş halidir. Bir muteks yalnızca bir kez kilitlenebilirken, bir semaforu birden çok kez edinmek mümkündür. Semaforlar genellikle belirli sayıda aynı kaynağı korumak için kullanılır.

Semaforlar acquire() ve release() olmak üzere iki temel işlemi destekler:

  • acquire(n) n adet kaynak edinmeye çalışır. Eğer o kadar kaynak mevcut değilse, çağrı bu durum gerçekleşene kadar bloke olur.
  • release(n) n kaynağı serbest bırakır.
Semaforların tipik bir uygulaması, bir üretici iş parçacığı ve bir tüketici iş parçacığı tarafından paylaşılan dairesel bir tampona erişimi kontrol etmek içindir. Semafor Kullanan Üretici ve Tüketici örneği, bu sorunu çözmek için QSemaphore’un nasıl kullanılacağını gösterir.

Bilgisayar dışı bir semafor örneği, bir restoranda yemek yemek olabilir. Bir semafor, restorandaki sandalye sayısı ile başlatılır. İnsanlar geldikçe, bir koltuk isterler. Koltuklar dolduğunda, available() değeri azaltılır. İnsanlar ayrıldıkça, available() artar ve daha fazla insanın girmesine izin verilir. 10 kişilik bir grup oturmak isterse, ancak yalnızca 9 koltuk varsa, bu 10 kişi bekleyecek, ancak 4 kişilik bir grup oturacaktır (mevcut koltukları 5’e çıkararak 10 kişilik grubun daha uzun süre beklemesine neden olur).

Qt docs

Semaforlar, çoklu iş parçacığı programlamada senkronizasyonu sağlamak için kullanılan bir mekanizmadır. Temelde bir sayaçtır ve birden çok iş parçacığının bir kaynağa (örneğin, bir bellek bölgesi veya bir dosya) eş zamanlı erişimini kontrol etmek için kullanılır. Semaforlar, özellikle kaynak paylaşımı, iş parçacığı koordinasyonu ve kritik bölge kontrolü gibi senaryolarda kullanışlıdır.

QSemaphore ise, Qt framework’ünde semaforları uygulamak için sağlanan bir sınıftır. Mutexler gibi, QSemaphore da çoklu iş parçacığı senkronizasyonunu sağlar. Ancak, mutexler sadece tek bir iş parçacığına ait bir kaynağa erişimi sınırlandırırken, semaforlar belirli bir kaynak miktarına izin verir ve bu kaynağın tükenmesi durumunda diğer iş parçacıklarının beklemesini sağlar.

QSemaphore’un mutexlerden farklılıkları şunlardır:

  1. Sayaç Değerine İzin Verme: QSemaphore, başlangıçta belirlenen bir sayaç değerine sahiptir. Bu sayaç, aynı anda belirli bir sayıda iş parçacığının belirli bir kaynağa erişmesine izin verirken, mutex sadece tek bir iş parçacığına izin verir.
  2. Kullanım Esnekliği: Semaforlar, bir kaynağın birden çok örneğine izin vermek veya birden çok farklı kaynağa erişimi kontrol etmek gibi durumlarda daha esnek bir seçenek sunar. Mutexler genellikle tek bir kaynağın erişimini kontrol etmek için kullanılır.

Örnek kullanım senaryoları şunlar olabilir:

  1. Kuyruk Yönetimi: Bir kuyruğa erişimi kontrol etmek için semaforlar kullanılabilir. Örneğin, bir iş parçacığından gelen verileri kuyruğa yazarken, diğer iş parçacıklarının aynı kuyruğa erişimini engellemek için bir semafor kullanılabilir.
  2. Kaynak Havuzu Yönetimi: Belirli bir kaynağa erişimi sınırlamak için semaforlar kullanılabilir. Örneğin, aynı anda belirli sayıda ağ bağlantısı oluşturmak istediğinizde, bir semafor kullanarak bu sayıyı kontrol edebilirsiniz.
  3. Veri Yapısı Erişimi: Çoklu iş parçacıklarının aynı veri yapısına erişimini kontrol etmek için semaforlar kullanılabilir. Örneğin, bir dizi veya listeye erişimi senkronize etmek için semaforlar kullanılabilir.

Bu senaryoları daha iyi anlamak için basit bir örnek verelim:

Örnek Kod:

#include <QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <QDebug>

const int DataSize = 5;
int buffer[DataSize];
QSemaphore freeSpace(DataSize); // Bufferdeki boş alanı belirtir
QSemaphore usedSpace(0); // Bufferdeki dolu alanı belirtir

class Producer : public QThread {
public:
    void run() override {
        for (int i = 0; i < DataSize; ++i) {
            freeSpace.acquire(); // Boş alana erişim sağla
            buffer[i] = i;
            qDebug() << "Produced:" << i << "Buffer Size:" << freeSpace.available();
            usedSpace.release(); // Dolu alana erişim sağla
        }
    }
};

class Consumer : public QThread {
public:
    void run() override {
        for (int i = 0; i < DataSize; ++i) {
            usedSpace.acquire(); // Dolu alana erişim sağla
            int data = buffer[i];
            qDebug() << "Consumed:" << data << "Buffer Size:" << freeSpace.available();
            freeSpace.release(); // Boş alana erişim sağla
        }
    }
};

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    Producer producer;
    Consumer consumer;

    producer.start();
    consumer.start();

    producer.wait();
    consumer.wait();

    return a.exec();
}

Bu kod, üretici ve tüketicinin buffer üzerinde işbirliği yaparak veri üretmesini ve tüketmesini sağlayan bir senkronizasyon mekanizması kullanır. Semaforlar, bufferdeki boş ve dolu alanları izleyerek üreticinin buffer’a veri eklemesini ve tüketicinin veriyi almasını kontrol eder.

Producer ve Consumer sınıflarını tanımlıyoruz. Her ikisi de QThread sınıfından kalıtım alır. Bu sınıflar, üretici ve tüketici işlemlerini temsil eder.

Producer sınıfının run() işlevinde, buffer’a veri eklemek için döngü oluşturuyoruz. Her adımda, freeSpace semaforunu kullanarak bufferdaki boş alanın olduğunu kontrol ediyoruz. Boş alan varsa, veriyi buffer’a ekliyoruz ve usedSpace semaforunu artırıyoruz.

Consumer sınıfının run() işlevinde de benzer bir döngü oluşturuyoruz. Ancak bu sefer, buffer’daki dolu alanı kontrol ediyoruz. Dolu alan varsa, veriyi buffer’dan alıyoruz ve freeSpace semaforunu artırıyoruz.

QWaitCondition

QWaitCondition, thread’leri karşılıklı dışlamayı zorlayarak değil, bir koşul değişkeni sağlayarak senkronize eder. Diğer ilkeller iş parçacıklarını bir kaynağın kilidi açılana kadar bekletirken, QWaitCondition threadleri belirli bir koşul karşılanana kadar bekletir. Bekleyen thread’lerin devam etmesine izin vermek için, rastgele seçilen bir thread’i uyandırmak üzere wakeOne() veya hepsini aynı anda uyandırmak üzere wakeAll() çağrısı yapın. Bekleme Koşullarını Kullanan Üretici ve Tüketici örneği, QSemaphore yerine QWaitCondition kullanarak üretici-tüketici probleminin nasıl çözüleceğini gösterir.

Qt docs

Örnek Kod: https://code.qt.io/cgit/qt/qtbase.git/tree/examples/corelib/threads/waitconditions/waitconditions.cpp?h=6.6

Buraya kadar okuduğunuz için teşekkürler, serinin devamında Reentrancy ve Thread-Safety konularını işleyeceğiz, iyi çalışmalar.

Yorum bırakın

Trend

WordPress.com’da Blog Oluşturun.