Saya mengerti situasinya sedikit lebih baik sekarang (dalam jumlah kecil karena jawaban di sini!), Jadi saya pikir saya menambahkan sedikit artikel saya sendiri.
Ada dua konsep yang berbeda, meskipun terkait, dalam C ++ 11: Asynchronous computation (fungsi yang disebut di tempat lain), dan eksekusi bersamaan ( utas , sesuatu yang bekerja secara bersamaan). Keduanya adalah konsep yang agak ortogonal. Komputasi Asynchronous hanya rasa yang berbeda dari panggilan fungsi, sedangkan utas adalah konteks eksekusi. Utas berguna dalam hak mereka sendiri, tetapi untuk tujuan diskusi ini, saya akan memperlakukan mereka sebagai detail implementasi.
Ada hierarki abstraksi untuk perhitungan asinkron. Sebagai contoh, misalkan kita memiliki fungsi yang mengambil beberapa argumen:
int foo(double, char, bool);
Pertama, kami memiliki template std::future<T>
, yang mewakili nilai tipe masa depan T
. Nilai dapat diambil melalui fungsi anggota get()
, yang secara efektif menyinkronkan program dengan menunggu hasilnya. Atau, dukungan masa depan wait_for()
, yang dapat digunakan untuk menyelidiki apakah hasilnya sudah tersedia atau tidak. Futures harus dianggap sebagai pengganti drop-in asinkron untuk tipe pengembalian biasa. Untuk fungsi contoh kita, kita mengharapkan astd::future<int>
.
Sekarang, ke hierarki, dari level tertinggi ke level terendah:
std::async
: Cara paling mudah dan mudah untuk melakukan perhitungan asinkron adalah melalui async
templat fungsi, yang segera mengembalikan pencocokan di masa depan:
auto fut = std::async(foo, 1.5, 'x', false); // is a std::future<int>
Kami memiliki sedikit kontrol atas detail. Secara khusus, kita bahkan tidak tahu apakah fungsinya dijalankan bersamaan, berseri get()
, atau dengan ilmu hitam lainnya. Namun, hasilnya mudah diperoleh saat dibutuhkan:
auto res = fut.get(); // is an int
Kita sekarang dapat mempertimbangkan bagaimana menerapkan sesuatu seperti async
, tetapi dengan cara itu kita kendalikan. Sebagai contoh, kami mungkin bersikeras bahwa fungsi dijalankan di utas terpisah. Kami sudah tahu bahwa kami dapat menyediakan utas terpisah dengan carastd::thread
kelas.
Level abstraksi selanjutnya yang lebih rendah melakukan hal itu: std::packaged_task
. Ini adalah templat yang membungkus suatu fungsi dan menyediakan masa depan untuk nilai pengembalian fungsi, tetapi objek itu sendiri dapat dipanggil, dan memanggilnya adalah atas kebijaksanaan pengguna. Kita dapat mengaturnya seperti ini:
std::packaged_task<int(double, char, bool)> tsk(foo);
auto fut = tsk.get_future(); // is a std::future<int>
Masa depan menjadi siap setelah kami memanggil tugas dan panggilan selesai. Ini adalah pekerjaan ideal untuk utas terpisah. Kami hanya harus memastikan untuk memindahkan tugas ke utas:
std::thread thr(std::move(tsk), 1.5, 'x', false);
Utas mulai berjalan segera. Kita dapat detach
melakukannya, atau memilikinya join
di akhir ruang lingkup, atau kapan saja (misalnya menggunakan scoped_thread
pembungkus Anthony Williams , yang seharusnya ada di perpustakaan standar). Namun, detail penggunaannya std::thread
tidak menjadi perhatian kami di sini; pastikan untuk bergabung atau melepaskan thr
akhirnya. Yang penting adalah bahwa setiap kali panggilan fungsi selesai, hasil kami siap:
auto res = fut.get(); // as before
Sekarang kita turun ke level terendah: Bagaimana kita mengimplementasikan tugas yang dikemas? Di sinilah yang std::promise
datang. Janji adalah blok bangunan untuk berkomunikasi dengan masa depan. Langkah-langkah utamanya adalah ini:
Utas panggilan membuat janji.
Utang panggilan mendapatkan masa depan dari janji.
Janji, bersama dengan argumen fungsi, dipindahkan ke utas terpisah.
Utas baru menjalankan fungsi dan memenuhi janji.
Utas asli mengambil hasilnya.
Sebagai contoh, inilah "tugas terpaket" kami sendiri:
template <typename> class my_task;
template <typename R, typename ...Args>
class my_task<R(Args...)>
{
std::function<R(Args...)> fn;
std::promise<R> pr; // the promise of the result
public:
template <typename ...Ts>
explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
template <typename ...Ts>
void operator()(Ts &&... ts)
{
pr.set_value(fn(std::forward<Ts>(ts)...)); // fulfill the promise
}
std::future<R> get_future() { return pr.get_future(); }
// disable copy, default move
};
Penggunaan template ini pada dasarnya sama dengan std::packaged_task
. Perhatikan bahwa memindahkan seluruh tugas berarti memindahkan janji. Dalam situasi yang lebih ad-hoc, orang juga bisa memindahkan objek janji secara eksplisit ke utas baru dan menjadikannya argumen fungsi dari fungsi utas, tetapi pembungkus tugas seperti yang di atas sepertinya merupakan solusi yang lebih fleksibel dan tidak terlalu mengganggu.
Membuat pengecualian
Janji terkait erat dengan pengecualian. Antarmuka janji saja tidak cukup untuk menyampaikan keadaannya sepenuhnya, sehingga pengecualian dilemparkan setiap kali operasi pada janji tidak masuk akal. Semua pengecualian bertipe std::future_error
, yang berasal dari std::logic_error
. Pertama, deskripsi beberapa kendala:
Janji yang dibangun secara default tidak aktif. Janji tidak aktif bisa mati tanpa konsekuensi.
Sebuah janji menjadi aktif ketika masa depan diperoleh melalui get_future()
. Namun, hanya satu masa depan yang dapat diperoleh!
Sebuah janji harus dipenuhi melalui set_value()
atau memiliki pengecualian yang ditetapkan set_exception()
sebelum masa hidupnya berakhir jika masa depannya ingin dikonsumsi. Janji yang puas dapat mati tanpa konsekuensi, dan get()
tersedia di masa depan. Sebuah janji dengan pengecualian akan memunculkan eksepsi yang tersimpan atas panggilan get()
di masa depan. Jika janji itu mati tanpa nilai atau pengecualian, menyerukan get()
masa depan akan menimbulkan pengecualian "janji rusak".
Berikut ini adalah serangkaian uji untuk menunjukkan berbagai perilaku luar biasa ini. Pertama, harness:
#include <iostream>
#include <future>
#include <exception>
#include <stdexcept>
int test();
int main()
{
try
{
return test();
}
catch (std::future_error const & e)
{
std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl;
}
catch (std::exception const & e)
{
std::cout << "Standard exception: " << e.what() << std::endl;
}
catch (...)
{
std::cout << "Unknown exception." << std::endl;
}
}
Sekarang ke tes.
Kasus 1: Janji tidak aktif
int test()
{
std::promise<int> pr;
return 0;
}
// fine, no problems
Kasus 2: Janji aktif, tidak digunakan
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
return 0;
}
// fine, no problems; fut.get() would block indefinitely
Kasus 3: Terlalu banyak masa depan
int test()
{
std::promise<int> pr;
auto fut1 = pr.get_future();
auto fut2 = pr.get_future(); // Error: "Future already retrieved"
return 0;
}
Kasus 4: Janji yang dipuaskan
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_value(10);
}
return fut.get();
}
// Fine, returns "10".
Kasus 5: Terlalu banyak kepuasan
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_value(10);
pr2.set_value(10); // Error: "Promise already satisfied"
}
return fut.get();
}
Pengecualian yang sama dilemparkan jika ada lebih dari satu baik dari set_value
atauset_exception
.
Kasus 6: Pengecualian
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo")));
}
return fut.get();
}
// throws the runtime_error exception
Kasus 7: Patah janji
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
} // Error: "broken promise"
return fut.get();
}