C++11 – std::mutex

Multi-process programlamada her bir processin kendine ait bir bellek alanı (memory space) vardır. Her bir process kendisine lazım olan veriyi kendisi elde eder. Herhangi bir veri paylaşımı gerektiğinde ise inter-process communication araçlarından birini (pipe, shared memory v.s) tercih ederek gerekli veri processler arasında dağıtılmış olur.

Multi-thread programlamada ise tüm threadlar aynı bellek alanını paylaşırlar. Bu paylaşım verinin değiştirilmesi veya okunması sırasından bir senkronizasyonu zorunlu kılar. Bunun temel amacı yarış durumundan (race condition) korunmaktır. Bu noktada programcıların en sık başvurduğu metod ise mutual exclusion yani mutexlerdir.

Şimdi aşağıdaki programa bir göz atalım.

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>


std::vector<int> ivec;
 
void addToVector(int i)
{
    ivec.push_back(i);
}

int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

Kod parçası 10 thread oluşturuyor ve bu threadi sırasıyla kendisine geçilen parametre değerleriyle beraber çağırıyor. Bu programın çıktı olarak ne vermesini beklersiniz ?

Herhangi bir çıktıya sahip olabilir misiniz, hatta programınız seg fault alarak da sonlanabilir veya hiçbir şey olmadan ilk bakista beklenildiği gibi,

0
1
2
3
4
5
6
7
8
9

şeklinde bir çıktı da alabilirsiniz. Kısacası bu durum bilinmez veya kestirilemez (undefined behaviour). Peki burdaki yarış durumu nerde oluşmaktadır, kısacası kodun hangi bölümünde threadler için kritik ve mutex ile koruma altına alınması gerekir ?

Cevap aslında threadler nerede ortak alana müdahale ediyorlarsa orada mutex kullanmamız lazımç Hepimizin az çok tahmin ettiği gibi threadlerin oluşturulup ivec vectorune threadler veri eklerken yarış durumu oluşur. Eğer bu bölgede mutexler vasıtasıyla koruma sağlanırsa bilinmezlik durumu ortadan kalkar ve threadlerin yarış durumuna girmeden herbirinin güvenli bir şekilde çağrılması garanti altına alınmış olur. Aşağıdaki örnek kodu inceleyelim;

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>

std::vector<int> ivec;
std::mutex mtx;
 
void addToVector(int i)
{
    mtx.lock();
    ivec.push_back(i);
    mtx.unlock();
}

int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

lock() ve unlock() üye fonksiyonları, aralarında kalan bloğun thread-safe olmasını garanti altına almaktadır. Yeri gelmişken bunu da belirtmeden geçmeyelim, kilitli (locked) durumundaki bir mutexi tekrar kilitlemeye (lock) çalışmak yine belirsizlik durumuna (undefined behaviour) sebep olur.
Dalgınlıktan dolayı unlock() fonksiyonunu çağırmayı unutursa programcı işte o zaman program bloklanır. Bu tarz hatalardan korunmak için yine akıllı göstericilerde kullanıldığı gibi RAII semantiği yardımımıza koşuyor.

lock_guard :

lock_guard ctor’ına geçilen mutex nesnesi, o aşamada kilitleniyor. Daha sonra lock_guard türündeki değişken dtor edildiginde sahip olunan mutex nesnesi ‘unlock’ ediliyor. Yani geliştirici unutsa bile iş otomatik olarak RAII makenizmasına emanet edildiği için başını yastığa rahat koyabilecek. 🙂

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>

std::vector<int> ivec;
std::mutex mtx;
 
void addToVector(int i)
{
    std::lock_guard<std::mutex> grd(mtx);
    ivec.push_back(i);
}

int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

Yukarıdaki kod örneğinde görüldüğü üzere yaptığım tek değişiklik std::mutex sınıfının lock() unlock() fonksiyonları yerine işi direkt lock_guard şablon sınıfına havale ettim.

unique_lock :

unique_lock şablon sınıfı aslında lock_guard sınıfının daha esnek ve seçenek sunan halidir. Örneğin,

std::unique_lock<std::mutex> ulock (mtx, std::defer_lock);

unique_lock sınıfının ctor ının ikinci parametresine std::defer_lock parametresi geçilerek inşaa aşamasında (construction) lock işleminin yapılmamasını sağlayabilirsin. Daha sonra bu işlemi sağlamak için unique_lock sınıfı lock() ve unlock() fonksiyonlarını hizmet olarak sunmaktadır. Ayrıca unique_lock sınıfı nesneleri kopyalanamazlar (non-copyable) ama taşınabilirlerdir (movable).

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>

std::vector<int> ivec;
std::mutex mtx;
 
void addToVector(int i)
{
    std::unique_lock<std::mutex> grd(mtx, std::defer_lock);
    grd.lock();
    ivec.push_back(i);
    grd.unlock(); //1
}


int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

1 nolu satırda mutexi unlock etmeyi unutsanız bile bunu sizin yerinize unique_lock sınıfının dtor ı yapacaktır. Bu yüzden direkt mutex sınıfının lock ve unlock fonksiyonlarını kullanmak yerine ihtiyacınıza göre kesinlikle lock_guard ve unique_lock gibi yardımcı sınıfların kullanımlarını alışkanlık haline getirmekte fayda var.

Genel olarak yeni gelen standartlarda mutexler bu şekilde ele alınmış. Burda ekstra deadlock nedir, nasıl korunulur gibi konulara girmedim. Bu konular üzerine ayrıca uzun uzun yazılar yazılabilir. Ama multi-thread programlama yapma niyeti olan arkadaşların kesinlikle deadlock gibi durumlardan korunacak disiplinleri kazanması ve bu konuda okumalar yapması şiddetle tavsiye edilir.

Advertisements