24 Ekim 2019 Perşembe

std::asynch metodu

Giriş
Yeni C++ ile gelen std::async metodunu kullanarak verilen fonksiyonu bir başka thread içinde çalıştırabilme imkanı var. Şu dosyaları dahil ederiz.
#include <future>
#include <thread>
Metodun imzası
Metod std::future döner. Parametre olarak lambda,metod artı lambda ve metodun kullanacağı parametreler geçilebiliyor.
lambda
lambda ve parametre olarak 2 ve 4 geçmek için şöyle yaparız.
std::future<int> result(std::async([](int m,int n) {return m + n;} , 2, 4));
std::bind
std::asynch nesnenin metodunu çağırmak için std::bind kullanmaya ihtiyaç duymaz. Örnekte void metod sonuç dönüyor gibi görünse de çok takılmayın. Önemli olan çağrının olabildiğini görmek.
#include <future>
void A::first_fun()
{
  auto future = std::async(&A::second_fun, this);
  return future.get();
}
Niçin Future
Metod future döndüğü için, thread'ler yaratıp, join ile beklemekten ve sonucu bir "out" parametresi ile almaya çalışmaktan çok daha kolay.
const std::size_t n_async = 4;
const std::size_t N = 150;
std::array<std::future<double>, n_async> futures;
for (auto index = 0U; index < n_async; ++i) {
  futures[i] = std::async(mycalculate, index * 10,index * 100);
}

double final_sum = 0.0;
for (auto&& future : futures) {
    final_sum += future.get();
}
Şöyle yaparız.
auto stuff = std::array<future_type, 2>
{
  {
    std::async(std::launch::async, func1),
    std::async(std::launch::async, func2)
  }
};

for (auto& f : stuff) f.wait();
Future nesnesinin destructor metodu
future nesnesi destructor'ı içinde thread'in bitmesini bekler.
The destructor of the last future object associated with the asynchronous state of the returned std::future shall block until the future is ready.
Bu yüzden bazı kodlarda dikkat etmek gerekir. Aşağıdaki kodda f future nesnesi arka planda çalışan foo metodu bitinceye kadar ana thread'i bloke eder.
{
    auto f = std::async(std::launch::async, foo);
    // dtor f::~f blocks until completion of foo()... why??
}
Benzer bir açıklama şöyle.
{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}
Eğer fire and forget tarzı bir iş yapmak istiyorsak önümüzde iki seçenek var. Ya kendimiz thread açarız ya da bir thread pool kullanırız. Kendi thread'imizi şöyle açarız.
thread([]{ f(); }).detach();
Kullanılan Policy Tipleri
Bu metoda geçilebilen 3 launch policy parametresi var. Bunlar aşağıda.
1. launch::deferred
Verilen iş, future.get() ile sonucu istenince çalıştırılır.Bu durumda sonucu almaya çalışan thread kullanılmış olur. Aslında bu kullanım için iyi bir örnek düşünemiyorum. Açıklaması şöyle
std::launch::deferred indicates that the function call is to be deferred until either wait() or get() is called on the future.
Açıklaması şöyle.
For std::launch:async the implementation supplies the thread that will preform the work while with std::launch::deferred you supply the thread (the one that reads the value).
Şöyle yaparız.
X baz(X&);

auto f7 = std::async(std::launch::deferred, baz, std::ref(x)); //run in wait() or get()
//...
f7.wait();  //invoke deferred function

2. launch::async
Örnek
Şu asynch metod çağrısı
auto future = async (launch::async, []{ f(); });
// ...
future.wait();
aslında şöyle düşünülebilir.
thread t([]{ f(); });
// ...
t.join();
Örnek
Şöyle yaparız.
int func()
{
  ...
}

int main()
{
  auto result=std::async(std::launch::async, func);
  ...
}
Örnek
Bir sürü iş verip hepsinin sonucunu almak için şöyle yaparız. Bu kod aslında C#'taki Task.WhenAll() çağrısına denk gelir.
std::vector<std::string> names = { "file1.txt", "file2.txt", "file3.txt" };
std::vector<std::future<Foo>> results;
for (const auto& name : names) {
  // load from the name file asynchronously 
  auto future = std::async(std::launch::async, &Foo::load, std::ref(name));
  results.emplace_back(future);
}
// gather result
for (auto& future : results) {
  Foo& data = future.get();
  // todo use data from the file object
}
Örnek
Bir sürü işi lambda olarak verip hepsinin sonucunu almak için şöyle yaparız.
auto lambda = [&] {
  ...
};

std::vector<decltype(std::async(std::launch::async, lambda))> results;

int threadCount = ...;

for (int i = 0; i < threadCount; i++) {
  results.emplace_back(std::move(std::async(std::launch::async, lambda)));
}
Visual C++ bu policy parametresi için yeni bir thread yaratmak yerine havuzu kullandığı için şikayet var. Açıklaması şöyle
Visual C++ uses the Windows thread pool (Vista's CreateThreadpoolWork if available and QueueUserWorkItem if not) when calling std::async with std::launch::async.

The number of threads in the pool is limited. If create several tasks that run for a long time without sleeping (including doing I/O), the upcoming tasks in the queue won't get a chance to work.
3. herhangi bir launch policy vermemek - default behavior
Bu durumda fonksiyonun nasıl çalıştırılacağına c++ kütüphanesi karar veriyor. Açıklaması şöyle
Behaves the same as async(std::launch::async | std::launch::deferred, f, args...). In other words, f may be executed in another thread or it may be run synchronously when the resulting std::future is queried for a value.
Şöyle yaparız
auto future = std::async([]{...});
std::asynch ve performans
Elimizde süre ölçen şöyle bir kod olsun.
template <class Clock = std::chrono::high_resolution_clock, class Task>
double timing(Task&& t, typename std::result_of<Task()>::type* r = nullptr)
{
  using namespace std::chrono;
  auto begin = Clock::now();
  if (r != nullptr) *r = std::forward<Task>(t)();
  auto end = Clock::now();
  return duration_cast<duration<double>>(end - begin).count();
}
İş yapan metod olsun.
template <typename Num>
double asum(const std::vector<Num>& v, const std::size_t l, const std::size_t h)
{
  ...
  return ...;
}
Çağırmak için şöyle yaparız.
std::cout << 1000 * timing([&]() -> double { return asum(...); }, &r)
          << " msec " << r << std::endl;