C++11 - std::async, std::packaged_task std::future ve std::promise

c11

#1

Daha önceki std::thread yazımızda yeni gelen standartlarla beraber C++ dilinin artık doğal olarak multi-threadingi desteklediğinden bahsetmiştik. Bu yazıda ise C++11 ile gelen multi-threading desteğinin sadece std::thread’ten ibaret olmadığından bahsedeceğim.

std::async

std::thread’ e benzer şekilde çalışır. Argüman olarak geçilen çağırılabilir (callable) nesneleri veya fonksiyonları asenkron olarak işletir ve bize std::future türünden bir nesne döner. std::future şablon sınıfı bize başlattığımız fonksiyonun veya nesnenin akıbeti hakkında bilgi sahibi olmamızı sağlar.

std::async şablon sınıfının ilk argümanı oluşturulan ‘görevin’ işleyişi (launch policy) hakkında bilgi verilmesi içindir. Şöyle ki;

  • std::launch::async
    Bu parametre oluşturulan görevin asenkron olarak (async execution) oluşturulmasını garanti eder. Yani verilen fonksiyon kesinlikle başka ve yeni bir threadde işleyecektir.

  • std::launch::deferred
    Deferred ile oluşturulan görev asenkron olarak başlamayıp ana threadde future objesinin get() fonksiyonu çağırıldığı zamanda (lazy evaluation) işlemesini sağlar. Kısacası ana thread sonuç isteyene kadar herhangi bir değerlendirme yapılmaz.

  • std::launch::async | std::launch::deferred
    Varsayılan davranıştır. Operasyonun yeni threadde mi yoksa ana threadde daha sonra mı değerlendirileceği implementasyon bağımlıdır. std::async’ in implementasyonu buna karar verir.
    Örneğin;

#include <iostream>
#include <thread>
#include <future>


using namespace std;



int fact (int i)
{
    int ret = 1;
    while (i > 0) {
        ret = ret * i;
        i--;
    }

    cout << "async fact" << endl;
    return ret;
}

int doSomething()
{

    std::this_thread::sleep_for(std::chrono::seconds(3));

    cout << "main thread do something" << endl;

    return 0;
}


int main()
{
    std::future<int> ret = std::async(std::launch::async, fact, 5);

    doSomething();

    cout << ret.get() << endl;

    return 0;
}

Kodu çalıştırdığınızda çok büyük bir olasılıkla aşağıdaki gibi bir çıktıya sahip olursunuz.

async fact
main thread do something
120

Görüldüğü gibi fact() fonksiyonu işini asenkron olarak çalışarak daha önce bitirmiş, ana threadde ihtiyacı olduğu anda fact() fonksiyonun geri dönüş değerini future şablon sınıfı sayesinde elde etmiş.

std::promise < T >

Peki bu örneğin sonucunu geri dönüş değerinden değil de bir parametre olarak elde etseydik. Tıpkı modern c++ öncesi zamanlarda olduğu gibi pointer, mutex ve condition_variable üçlüsünü kullanmamız gerekirdi. İşte bu noktada std::promise bize bu imkanı sağlıyor.

#include <iostream>
#include <thread>
#include <future>


using namespace std;



void fact (int i, std::promise<int> *p)
{
    int ret = 1;
    while (i > 0) {
        ret = ret * i;
        i--;
    }
    
    p->set_value(ret);
}


int main()
{
    std::promise<int>  pobj;

    std::future<int> fut = pobj.get_future();

    std::thread th(fact, 5, &pobj);


    cout << "fact " << fut.get()  << endl;

    th.join();
    return 0;
}

std::promise < T > den okuma yapmak istediğimiz zaman kendi içerisinde barındırdığı future nesnesini elde etmemiz gerekiyor ve yine future < T > nesnesi ile değerimize ulaşabiliriz.

Hemen akla bir neden iki nesne kullanılıyor tek bir nesne kullanılsa hem std::promise hem de std::future aynı işi görmüyor mu diye bir soru gelebilir. Standartlar gereği threadler arasındaki durum veya veri paylaşımı half-duplex bir yapıda. Yani bir threade hem okuma hem yazma hakkı verilmiyor, nesnelerin yaşama sürelerini garanti altına almak için. Bu açıdan bakıldığında std::future consumer/reader iken std::promise producer/writer durumlarını yerine getirmekle mükellef.

std::packaged_task < T >

Aslında packaged_task std::thread ile std::async özelliklerinin daha özel bir hali. Normal bir fonksiyona std::future özelliğini ekleyerek std::thread ile asenkron çalıştırmanız denilebilir std::package < T > için.

#include <iostream>
#include <thread>
#include <future>


using namespace std;



int fact (int i)
{
    int ret = 1;
    while (i > 0) {
        ret = ret * i;
        i--;
    }

    cout << "ready for return" << endl;
    return ret;
}

int doSomething()
{

    std::this_thread::sleep_for(std::chrono::seconds(3));

    cout << "main thread do something" << endl;


}


int main()
{
    std::packaged_task<int (int)> pt(fact);

    auto fut = pt.get_future();

    std::thread th(std::move(pt), 5);

    doSomething();

    cout << fut.get() << endl;

    th.join();

    return 0;
}

Unutmadan şunu da eklemekte fayda var, std::package_task < T > sadece movable’dır herhangi bir şekilde kopyalanamaz o nedenden örnek kodda threade argüman olarak verilirken std::move şablon sınıfını kullandık.