Dasar
Mari kita mulai dengan contoh yang disederhanakan dan memeriksa Boost. Asio yang relevan:
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
Apa itu Handler ?
Seorang penangan tidak lebih dari sebuah panggilan balik. Di kode contoh, ada 3 penangan:
- The
print
handler (1).
- The
handle_async_receive
handler (3).
- The
print
handler (4).
Meskipun print()
fungsi yang sama digunakan dua kali, setiap penggunaan dianggap membuat penangannya sendiri yang dapat diidentifikasi secara unik. Penangan bisa datang dalam berbagai bentuk dan ukuran, mulai dari fungsi dasar seperti yang di atas hingga konstruksi yang lebih kompleks seperti fungsi yang dihasilkan dari boost::bind()
dan lambda. Terlepas dari kerumitannya, handler tetap tidak lebih dari callback.
Apa Itu Pekerjaan ?
Pekerjaan adalah beberapa pemrosesan yang diminta oleh Boost.Asio untuk dilakukan atas nama kode aplikasi. Terkadang Boost.Asio dapat memulai beberapa pekerjaan segera setelah diberitahu tentangnya, dan di lain waktu mungkin menunggu untuk melakukan pekerjaan di lain waktu. Setelah menyelesaikan pekerjaan, Boost.Asio akan menginformasikan aplikasi dengan memanggil penangan yang disediakan .
Boost.Asio menjamin bahwa penangan hanya akan berjalan dalam thread yang saat menelepon run()
, run_one()
, poll()
, atau poll_one()
. Ini adalah utas yang akan berfungsi dan menangani panggilan . Oleh karena itu, dalam contoh di atas, print()
tidak dipanggil saat diposting ke io_service
(1). Sebaliknya, ini ditambahkan ke io_service
dan akan dipanggil di lain waktu. Dalam hal ini, itu dalam io_service.run()
(5).
Apa Itu Operasi Asinkron?
Sebuah operasi asynchronous menciptakan pekerjaan dan Boost.Asio akan memanggil handler untuk menginformasikan aplikasi ketika pekerjaan telah selesai. Operasi asinkron dibuat dengan memanggil fungsi yang memiliki nama dengan awalan async_
. Fungsi ini juga dikenal sebagai fungsi inisiasi .
Operasi asinkron dapat diuraikan menjadi tiga langkah unik:
- Memulai, atau menginformasikan, pekerjaan terkait
io_service
yang perlu dilakukan. The async_receive
Operasi (3) menginformasikan io_service
bahwa akan membutuhkan data asynchronous dibaca dari soket, kemudian async_receive
segera kembali.
- Melakukan pekerjaan yang sebenarnya. Dalam hal ini, ketika
socket
menerima data, byte akan dibaca dan disalin ke buffer
. Pekerjaan sebenarnya akan dilakukan di:
- Fungsi inisiasi (3), jika Boost.Asio dapat menentukan bahwa itu tidak akan diblokir.
- Ketika aplikasi secara eksplisit menjalankan
io_service
(5).
- Menyerukan
handle_async_receive
ReadHandler . Sekali lagi, penangan hanya dipanggil di dalam utas yang menjalankan io_service
. Jadi, terlepas dari kapan pekerjaan selesai (3 atau 5), dijamin handle_async_receive()
hanya akan dipanggil dalam io_service.run()
(5).
Pemisahan dalam ruang dan waktu antara ketiga langkah ini dikenal sebagai inversi aliran kontrol. Ini adalah salah satu kompleksitas yang membuat pemrograman asinkron menjadi sulit. Namun, ada beberapa teknik yang dapat membantu menguranginya, seperti dengan menggunakan coroutine .
Apa yang io_service.run()
Dilakukan?
Saat utas memanggil io_service.run()
, pekerjaan dan penangan akan dipanggil dari dalam utas ini. Dalam contoh di atas, io_service.run()
(5) akan memblokir hingga:
- Ini telah dipanggil dan dikembalikan dari kedua
print
penangan, operasi penerimaan selesai dengan keberhasilan atau kegagalan, dan handle_async_receive
penangannya telah dipanggil dan dikembalikan.
- Ini
io_service
secara eksplisit dihentikan melalui io_service::stop()
.
- Pengecualian dilemparkan dari dalam sebuah handler.
Salah satu aliran psuedo-ish potensial dapat dijelaskan sebagai berikut:
buat io_service
buat soket
tambahkan penangan cetak ke io_service (1)
tunggu soket untuk terhubung (2)
tambahkan permintaan kerja baca asinkron ke io_service (3)
tambahkan penangan cetak ke io_service (4)
jalankan io_service (5)
apakah ada pekerjaan atau penangan?
ya, ada 1 pekerjaan dan 2 penangan
apakah soket memiliki data? tidak, jangan lakukan apa-apa
jalankan penangan cetak (1)
apakah ada pekerjaan atau penangan?
ya, ada 1 pekerjaan dan 1 penangan
apakah soket memiliki data? tidak, jangan lakukan apa-apa
jalankan penangan cetak (4)
apakah ada pekerjaan atau penangan?
ya, ada 1 pekerjaan
apakah soket memiliki data? tidak, lanjutkan menunggu
- soket menerima data -
socket memiliki data, membacanya menjadi buffer
tambahkan handler handle_async_receive ke io_service
apakah ada pekerjaan atau penangan?
ya, ada 1 pawang
jalankan handle_async_receive handler (3)
apakah ada pekerjaan atau penangan?
tidak, setel io_service sebagai berhenti dan kembali
Perhatikan bagaimana ketika pembacaan selesai, itu menambahkan penangan lain ke io_service
. Detail halus ini adalah fitur penting dari pemrograman asinkron. Ini memungkinkan pawang untuk dirantai bersama. Misalnya, jika handle_async_receive
tidak mendapatkan semua data yang diharapkan, implementasinya dapat memposting operasi pembacaan asinkron lainnya, yang menghasilkan io_service
lebih banyak pekerjaan, dan dengan demikian tidak kembali dari io_service.run()
.
Apakah dicatat bahwa ketika io_service
memiliki berlari keluar dari pekerjaan, aplikasi harus reset()
yang io_service
sebelum menjalankan lagi.
Contoh Soal dan Contoh kode 3a
Sekarang, mari kita periksa dua bagian kode yang dirujuk dalam pertanyaan itu.
Kode Pertanyaan
socket->async_receive
menambahkan pekerjaan ke io_service
. Dengan demikian, io_service->run()
akan memblokir hingga operasi baca selesai dengan keberhasilan atau kesalahan, dan ClientReceiveEvent
telah selesai berjalan atau melontarkan pengecualian.
Dengan harapan membuatnya lebih mudah untuk dipahami, berikut adalah Contoh 3a beranotasi yang lebih kecil:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
Pada level tinggi, program akan membuat 2 thread yang akan memproses io_service
event loop (2). Ini menghasilkan kumpulan utas sederhana yang akan menghitung angka Fibonacci (3).
Satu perbedaan utama antara Kode Pertanyaan dan kode ini adalah bahwa kode ini memanggil io_service::run()
(2) sebelum pekerjaan aktual dan penangan ditambahkan ke io_service
(3). Untuk mencegah agar tidak io_service::run()
segera kembali, sebuah io_service::work
objek dibuat (1). Objek ini mencegah io_service
dari kehabisan pekerjaan; oleh karena itu, io_service::run()
tidak akan kembali karena tidak ada pekerjaan.
Alur keseluruhan adalah sebagai berikut:
- Buat dan tambahkan
io_service::work
objek yang ditambahkan ke io_service
.
- Kumpulan benang dibuat yang memanggil
io_service::run()
. Untaian pekerja ini tidak akan kembali dari io_service
karena io_service::work
objeknya.
- Tambahkan 3 penangan yang menghitung angka Fibonacci ke
io_service
, dan segera kembali. Rangkaian pekerja, bukan utas utama, mungkin mulai menjalankan penangan ini dengan segera.
- Hapus
io_service::work
objek tersebut.
- Tunggu hingga thread pekerja selesai dijalankan. Ini hanya akan terjadi setelah ketiga penangan menyelesaikan eksekusi, karena
io_service
tidak ada penangan atau pun pekerjaan.
Kode dapat ditulis berbeda, dengan cara yang sama seperti Kode Asli, di mana penangan ditambahkan ke io_service
, dan kemudian io_service
perulangan kejadian diproses. Ini menghilangkan kebutuhan untuk menggunakan io_service::work
, dan menghasilkan kode berikut:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
Sinkron vs. Asinkron
Meskipun kode dalam pertanyaan menggunakan operasi asinkron, kode ini berfungsi secara efektif secara sinkron, karena sedang menunggu operasi asinkron selesai:
socket.async_receive(buffer, handler)
io_service.run();
setara dengan:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
Sebagai pedoman umum, cobalah untuk menghindari pencampuran operasi sinkron dan asinkron. Seringkali, ini dapat mengubah sistem yang kompleks menjadi sistem yang rumit. Jawaban ini menyoroti keunggulan pemrograman asinkron, beberapa di antaranya juga tercakup dalam dokumentasi Boost.Asio .