1. Niyə çoxaxınlılıqda istisnalar “orada” yox, başqa yerdə üzə çıxır
Əgər siz konkurrent kod yazmağa yenicə başlayırsınızsa, adətən gözlənti belə olur: “Yaxşı, əgər orada xəta varsa, onu tapşırığı başladığım yerin yanında tutacağam”. Amma çoxaxınlılığın öz yumor hissi var. İstisna bir icra kontekstində yaranır, biz isə onu çox vaxt başqa bir kontekstdə müşahidə edirik. Bu, throw və catch bir-birindən uzaq olmayan və sizə “cinayət yerini tap” tipli bir kvest yaratmayan adi dünya təsəvvürünü pozur.
Klassik std::thread halında vəziyyət daha sərtdir: əgər istisna axın funksiyasından çıxıb gedirsə və axının daxilində heç kim onu tutmursa, proqram adətən std::terminate() vasitəsilə bitir (yəni “sualsız qəfil dayandırılma”). std::async/std::future daha humanist qurulub: onlar sizə istisnanın nəticəni götürdüyünüz nöqtəyə qədər çatacağı bir “kanal” verir.
future nəticəni və ya istisnanı necə saxlayır
Biz “std::future nəticəni saxlayır” deyəndə, bunu düşüncədə bir az dəqiqləşdirmək faydalıdır: future nəticənin özünü yox, ümumi vəziyyətə (shared state) istinadı saxlayır. Bu, elə bir daxili “hüceyrədir” ki, sonda orada iki variantdan yalnız biri olacaq: ya T tipli dəyər, ya da istisna. Vacib məqam budur ki, get() bu vəziyyəti götürür (istehlak edir) — standart hətta future::get() zamanı “shared state‑in azad olunması” davranışını ayrıca müzakirə edir.
Sadələşdirilmiş şəkildə təsvir edək:
flowchart LR
A["Tapşırığın işə salınması (async / promise / packaged_task)"] --> B["Ümumi vəziyyət (shared state)"]
B --> C["value: T"]
B --> D["exception: exception_ptr"]
E["İstehlakçı (future)"] -->|wait / get| B
Bu sxemdən mühazirənin əsas praktik nəticəsi yaranır: istisna görünən olur onu yarandığı yerdə deyil, sizin future.get() etdiyiniz yerdə.
2. std::async istifadə edərkən xətaları harada tutmalı
std::async istifadə etdikdə xətalar yaranma vaxtına görə iki tip olur. Bu vacibdir, çünki müxtəlif xətalar müxtəlif yerlərdə tutulur (və məhz burada yeni başlayanlar adətən ən çox çətinlik çəkirlər).
std::async çağırışı zamanı xəta
Əvvəlcə qısa bir giriş. std::async “paralel et” adlı sehrli düymə deyil, tapşırığı hazırlamalı olan adi bir funksiyadır: arqumentləri kopyalamalı/köçürməli, callable obyektini paketləməli, bəzən də axın yaratmalıdır (işə salma siyasətindən asılı olaraq). Və məhz bu mərhələdə nəsə səhv gedə bilər.
Məsələn, nəzəri olaraq axın yaratmaq alınmaya bilər (sistem xətası) və ya arqumentlərin kopyalanması istisna ata bilər (nadir hallarda, amma mümkündür). Belə xətalar dərhal, birbaşa std::async(...) sətrində yaranır və işə salmanın ətrafındakı adi try/catch ilə tutulur.
Mini‑nümunə (demonstrasiya üçündür; istisnanın real səbəbi vəziyyətdən asılıdır):
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
try {
auto f = std::async(std::launch::async, [] {
return 1;
});
std::cout << f.get() << '\n'; // 1
} catch (const std::exception& e) {
std::cout << "launch failed: " << e.what() << '\n';
}
}
Tapşırığın icrası zamanı xəta
İndi əsas hissəyə keçək. Əgər async daxilindəki callable istisna atırsa, o öz‑özünə çap olunmur və sizə dərhal çatmır. O, shared state daxilində (exception_ptr kimi) “yığılır” və future.get() zamanı yenidən atılacaq.
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
auto f = std::async(std::launch::async, []() -> int {
throw std::runtime_error("boom in async");
});
try {
std::cout << f.get() << '\n'; // bura çatmayacağıq
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n'; // caught: boom in async
}
}
“Psixoloji tələ”yə diqqət yetirin: xəta “haradasa orada” baş verdi, amma biz onu “məhz burada” tutduq.
4. Niyə wait() və wait_for() uğura zəmanət vermir
İstisnanın get() üzərində üzə çıxdığını görəndən sonra növbəti tipik fikir belə olur: “Yaxşı, onda əvvəlcə wait() edərəm, hazır olduğuna əmin olaram və hər şey yaxşı olar”. Təəssüf ki, “hazırdır” demək “uğurludur” demək deyil. Bu, sadəcə “shared state daxilində yekun artıq yerləşib” deməkdir — həmin yekun isə istisna da ola bilər.
Bunu qısa şəkildə göstərək:
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
auto f = std::async(std::launch::async, []() -> int {
throw std::runtime_error("fail");
});
f.wait(); // gözlədik, amma nə ilə bitdiyini bilmədik
try {
(void)f.get(); // istisna məhz burada üzə çıxacaq
} catch (const std::exception& e) {
std::cout << "after wait: " << e.what() << '\n'; // after wait: fail
}
}
wait_for() və taymautlarla da mənzərə eynidir. Taymaut ümumiyyətlə “tapşırığın xətası” deyil, sadəcə “gözləməyə çatmadıq” deməkdir. Bəs əgər çatdıqsa? Bu da hələ uğura söz vermir, çünki “uğur” yalnız get() zamanı üzə çıxır.
5. std::launch::deferred rejimi: istisna sinxron görünür
std::launch::deferred elə bir rejimdir ki, tapşırıq dərhal başlamır, wait()/get() anında gözləyən axında icra olunur. Və burada beyin “deməli, istisna elə burada olacaq” deyir — bəli, həqiqətən belə olur. Amma bu, “get() zamanı müşahidə edirik” qaydasını ləğv etmir: sadəcə indi “icra yeri” ilə “müşahidə yeri” üst‑üstə düşür.
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
auto f = std::async(std::launch::deferred, []() -> int {
throw std::runtime_error("deferred fail");
});
try {
std::cout << f.get() << '\n';
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n'; // caught: deferred fail
}
}
Praktik mənası budur: əgər deferred görürsünüzsə, paralellik gözləməyin və unutmayın ki, burada ümumiyyətlə “fon axını” da olmaya bilərdi.
6. Xətanın std::promise vasitəsilə çatdırılması: set_exception və exception_ptr
Bəzən std::async uyğun olmur, çünki nəticə return kimi yox, hadisə kimi yaranır: “cavab alındı”, “emal bitdi”, “xəta tutuldu”. Onda əlimizdə std::promise<T> olur və shared state daxilinə nə qoyacağımıza özümüz qərar veririk: dəyər, yoxsa istisna.
Bunun üçün std::promise<T>::set_exception(...) istifadə olunur; o, std::exception_ptr qəbul edir. exception_ptr əldə etməyin isə iki əsas yolu var.
std::make_exception_ptr(...): istisnanı açıq şəkildə yaradırıq
Kiçik bir giriş. Bəzən siz real istisnanı tutmursunuz, sadəcə şərtə görə “xəta siqnalı” vermək istəyirsiniz: giriş verilənləri pisdir, fayl tapılmadı, format yanlışdır. Onda istisnanı əl ilə yaradıb paketləmək rahat olur.
#include <exception>
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
std::promise<int> p;
auto f = p.get_future();
p.set_exception(std::make_exception_ptr(std::runtime_error("bad input")));
try {
(void)f.get();
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // bad input
}
}
std::current_exception(): tutulmuş istisnanı ötürürük
Əgər siz artıq catch (...) daxilindəsinizsə, std::current_exception() cari tutulmuş istisnaya exception_ptr verir. Bu, xətanı worker‑koddan consumer‑koda “ötürmək” üçün baza nümunəsidir.
#include <exception>
#include <future>
#include <stdexcept>
void worker(std::promise<int> p) {
try {
throw std::runtime_error("worker failed");
} catch (...) {
p.set_exception(std::current_exception());
}
}
İstisnaların icra kontekstləri arasında ötürülməsi mexanizmi kimi exception_ptr anlayışının mövcudluğu və aktiv inkişafı komitə materiallarında mütəmadi müzakirə olunur, ona görə də bu hansısa “hak” deyil, müasir C++‑da istisna modelinin normal hissəsidir.
7. Praktik nümunə: TaskBook hesabatı və xətaların emalı
Bunu “kursun vahid tətbiqi” ilə əlaqələndirək. Tutaq ki, bizdə artıq TaskBook adlı konsol proqramı var: o, tapşırıqlar siyahısını saxlayır (başlıq + vaxt qiyməti), onları çap etməyi və saxlayıb/yükləməyi bacarır (dəqiq necə olduğu vacib deyil — kursun bu mərhələsində siz həm faylları, həm JSON‑u, həm də CLI‑ni görmüsünüz).
İndi isə yeni funksiya əlavə edirik: “Tapşırıqlar üzrə hesabat yarat”. Hesabat ağır ola bilər (məsələn, analiz, sıralama, aqreqasiyalar) — və biz onu std::async vasitəsilə asinxron yerinə yetirmək istəyirik. Həmçinin istəyirik ki, əgər hesabat alınmasa, istifadəçi aydın bir mesaj görsün və proqram səssizcə ölməsin.
Məlumat modeli
#include <string>
struct Task {
std::string title;
int minutes = 0;
};
Hesabatın yaradılması funksiyası
#include <numeric>
#include <stdexcept>
#include <string>
#include <vector>
std::string buildReport(const std::vector<Task>& tasks) {
if (tasks.empty()) {
throw std::runtime_error("no tasks to report");
}
int total = 0;
for (const Task& t : tasks) total += t.minutes;
return "Tasks: " + std::to_string(tasks.size()) +
", total minutes: " + std::to_string(total);
}
Burada biz qəsdən boş siyahıda istisna atırıq. Bu, tədris nümunəsidir: real həyatda siz “boş hesabat” qaytara bilərdiniz, amma hazırda bizim üçün xətanın çatdırılma mexanikasını görmək vacibdir.
Asinxron işə salma və try/catch üçün düzgün nöqtə
#include <future>
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks; // deyək ki, hələlik boşdur
auto reportFuture = std::async(std::launch::async, buildReport, tasks);
try {
std::string report = reportFuture.get();
std::cout << report << '\n';
} catch (const std::exception& e) {
std::cout << "Cannot build report: " << e.what() << '\n';
// Cannot build report: no tasks to report
}
}
Açar fikir: get() ətrafında tuturuq. Əgər try/catch blokunu std::async ətrafına qoysanız, yalnız “işə salma xətalarını” tutacaqsınız, “icra xətalarını” yox.
8. future ilə bağlı faydalı incəliklər: broken promise və kontrakt xətaları
Bu bölmə çox vaxt “tapşırıq çökdü” kimi qəbul edilən situasiyalar haqqındadır, halbuki əslində bu ya promise/future kontraktının pozulmasıdır, ya da API‑nin az aşkar görünən bir hissəsidir.
Broken promise: “nəticə vəd edildi, amma mənbə yoxa çıxdı”
Broken promise o vəziyyətdir ki, std::promise məhv edilir, amma nə set_value, nə də set_exception çağırıb. İstehlakçı üçün (future) bu, kontraktın pozulması kimi görünür: “mən nəticəni gözləyirdim, amma təchizatçı yox oldu”. Nəticədə future.get() std::future_error tipli istisna atacaq.
Yeni başlayanlar üçün bu çox faydalı diaqnostikadır, çünki tez bir zamanda belə bir siqnal verir: “siz nəticə kanalını tamamlamağı unutmusunuz”.
Mini‑demo:
#include <future>
#include <iostream>
int main() {
std::future<int> f;
{
std::promise<int> p;
f = p.get_future();
// p məhv ediləcək, amma set_value/set_exception olmayacaq
}
try {
(void)f.get(); // std::future_error atacaq (broken promise)
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n';
}
}
Real kodda broken promise çox vaxt bu üç şeydən birini göstərir: axın vaxtından əvvəl bitib, “erkən çıxış” məntiqi xətanı quraşdırmağı unudub, yaxud promise yanlış yerə/yanlış üsulla köçürülüb və “itirilib”.
get() birdəfəlikdir: ikinci get() hesablama xətası deyil
Burada kiçik, amma vacib bir giriş var. Əgər siz get() çağırmısınızsa, nəticəni və shared state‑i istehlak etmisiniz (və standart ayrıca get() zamanı shared state‑in azad olunmasını vurğulayır). İkinci get() istifadənin kontraktını pozmaqdır və bu adətən std::future_error vasitəsilə ifadə olunur.
#include <future>
#include <iostream>
int main() {
auto f = std::async(std::launch::async, [] { return 5; });
std::cout << f.get() << '\n'; // 5
try {
std::cout << f.get() << '\n'; // xəta: təkrar get()
} catch (const std::exception& e) {
std::cout << "second get: " << e.what() << '\n';
}
}
Praktik qayda çox sadədir: əgər dəyər sizə bir dəfədən çox lazımdırsa — onu dəyişəndə saxlayın (və ya başqa mexanizmlərdən istifadə edin, amma bu gün onlara toxunmuruq).
wait_for() ready qaytara bilər, amma get() yenə də istisna ata bilər
Bu, mövzunun məntiqi davamıdır. ready “yekun hazırdır” deməkdir. Yekun isə istisna da ola bilər.
#include <chrono>
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
auto f = std::async(std::launch::async, []() -> int {
throw std::runtime_error("fail after ready");
});
auto st = f.wait_for(std::chrono::seconds{1});
if (st == std::future_status::ready) {
std::cout << "ready\n"; // ready
}
try {
(void)f.get();
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // fail after ready
}
}
İntizam şablonu: shared state‑i həmişə tamamlayın
İndi əsəbləri qoruyan bir intizam qaydasını səliqə ilə formalaşdıraq. Sizdə promise/future vasitəsilə producer/consumer əlaqəsi varsa, producer tərəfdə dəmir bir vərdiş olmalıdır: producer‑koddan istənilən çıxış mütləq set_value və ya set_exception ilə nəticələnməlidir.
Təhlükəsiz şablonu göstərək (və bəli, burada bir az çox kod var, amma buna dəyər):
#include <exception>
#include <future>
#include <stdexcept>
void produceReport(std::promise<std::string> p) {
try {
// ... işi görürük
throw std::runtime_error("report failed");
// p.set_value("ok"); // "yaxşı" ssenaridə belə olardı
} catch (...) {
p.set_exception(std::current_exception());
}
}
Məna ondadır ki, consumer həmişə get() edib ya dəyəri, ya da normal bir istisnanı ala bilər; “sükut və broken promise” yox.
9. Tipik xətalar
Xəta No. 1: yerləşdirmək try/catch std::async ətrafında, amma future.get() ətrafında yox.
Belə kod yalnız “işə salma xətalarını” tutur, amma callable icra olunarkən baş verən xətaları tutmur. Buna görə proqram elə görünür ki, guya “bəzən səbəbsiz yerə çökür”. Çarəsi sadədir: nəticənin müşahidə nöqtəsi get()‑dirsə, deməli, try/catch də orada olmalıdır.
Xəta No. 2: wait() çağırışını uğur saymaq.
wait() və wait_for() “ümumi vəziyyət hazırdırmı” sualına cavab verir, “hesablama uğurludurmu” sualına yox. Nəticədə yeni başlayanlar wait() edir, sonra isə try/catch olmadan get() çağırır və istisnanın yenə də gəldiyinə təəccüblənirlər. Düzgün mental model belədir: wait — vaxt haqqındadır, get — nəticə haqqında.
Xəta No. 3: get() çağırmağı unutmaq və beləliklə istisnanı “itirmək”.
future — yekunu almaqla bağlı kontraktınızdır. Əgər siz tapşırığı başladıb, amma heç yerdə get() etməmisinizsə (və ya heç olmasa yekunu başqa cür emal etməmisinizsə), əslində nəticələri qara dəliyə atmış olursunuz. Tədris nümunələrində bu zərərsiz görünür, amma real sistemlərdə “fon tərəfdə bəzən nəsə qırılır, amma heç kim nə olduğunu bilmir” vəziyyətinə çevrilir.
Xəta No. 4: promise obyektini nə dəyər, nə də istisna qurulmadan məhv etmək (broken promise).
Bu, klassik vəziyyətdir: erkən return, producer‑kod daxilində istisna, unudulmuş set_exception — və consumer std::future_error alır. Həll yolu “catch (...) → set_exception(current_exception())” intizamı və “hər çıxış yolu shared state‑i tamamlayır” qaydasıdır.
Xəta No. 5: tapşırığın istisnasını və “future kontraktı” istisnasını qarışdırmaq.
buildReport() daxilindən gələn istisna sizin biznes məntiqinizin xətasıdır. Amma ikinci get() zamanı gələn std::future_error API istifadəsinin xətasıdır. Əgər bu iki problem sinfini ayırmasanız, hesablama tərəfini “düzəltməyə” başlaya bilərsiniz, halbuki düzəldilməli olan şey future obyektinin həyat dövrüdür (məsələn, nəticəni dəyişəndə saxlamaq).
GO TO FULL VERSION