Topik temu duga representatif

Temu duga Rust: Bagaimanakah cara menambah kekangan Send pada async trait future menggunakan return type notation?

PengekodanSukar
Pasukan Editorial Offer.ccDiterbitkan Dikemas kini

Soalan

Sesuatu async trait Rust mesti menyokong kedua-dua executor satu utas (single-threaded) dan executor berbilang utas (multi-threaded) Tokio. Bagaimanakah anda mengenakan kekangan pada Future yang dikembalikan oleh sesuatu kaedah dengan RTN, dan bilakah anda akan memilih varian trait atau GAT sebagai gantinya?

Prompt dan skop

Anda menyelenggara trait Service yang digunakan oleh pelbagai executor berbeza. Kaedah-kaedahnya menggunakan async fn, tetapi hanya sebilangan tapak panggilan (call site) yang menghantar Future kepada tugasan yang mungkin berpindah antara utas. Terangkan sebab mengisytiharkan setiap Future yang dikembalikan sebagai Send adalah terlalu mengehadkan, dan berikan peraturan pemilihan bagi return type notation (RTN), varian trait, dan GAT.

Soalan ini menyasarkan AFIT dan RPITIT yang distabilkan dalam Rust 1.75 serta RTN yang dicadangkan oleh RFC 3654. RTN masih merupakan eksperimen nightly; asingkan keupayaan bahasa, keperluan executor, dan kestabilan keluaran dalam jawapan anda.

Perkara yang diuji oleh penemu duga

Jawapan yang kukuh mengenal pasti setiap Future sebagai jenis pulangan tanpa nama bagi sesuatu kaedah trait. Pemanggil generik atau dyn Trait tidak dapat membuat inferens sifat Send daripadanya melalui trait itu sahaja, jadi kekangan tersebut sepatutnya berada pada tapak panggilan yang benar-benar merentasi utas.

Penemu duga juga menjangkakan skop RTN: ia mengekang nilai pulangan AFIT/RPITIT dan bukannya bertindak sebagai jenis medan biasa. Bincangkan risiko nightly, executor satu utas, executor work-stealing, dan laluan keserasian.

Soalan penjelasan

Bolehkah Future berpindah antara utas

Jika executor mengepin kerja pada satu utas, Send mungkin tidak diperlukan. Executor work-stealing memerlukan Future yang di-spawn bersifat Send, dan lazimnya turut memerlukan 'static.

Adakah kekangan itu untuk satu kaedah atau keseluruhan trait

Jika hanya call yang mesti boleh dipindahkan, RTN boleh menyatakan call(..): Send. Jika setiap kaedah memerlukan kekangan yang sama, varian trait mengelakkan pengulangan klausa peringkat kaedah.

Bolehkah persekitaran pengeluaran menggunakan nightly

Jika pustaka awam mesti menyokong Rust stabil, RTN tidak boleh menjadi satu-satunya reka bentuk. Nilaikan trait Send yang dijana atau GAT eksplisit dan rekodkan versi pengkompil dalam matriks keluaran.

Jawapan 30 saat

“Mula-mula saya menyemak sama ada executor boleh memindahkan tugasan dan kaedah mana yang memerlukan jaminan tersebut. Saya mengekalkan trait asas dengan kekangan minimum, kemudian menulis T::call(..): Send + 'static pada tapak panggilan yang melakukan spawn merentasi utas. Jika setiap kaedah memerlukan Send, saya menggunakan varian trait; jika Rust stabil dan Future bernama diperlukan, saya menggunakan GAT. RTN berada pada nightly, jadi saya akan mengujinya dalam rantai alat terasing dan mengekalkan sandaran stabil.”

Penyelesaian langkah demi langkah

Langkah 1: Terbitkan kekangan daripada executor

Executor satu utas atau thread-per-core biasanya tidak memindahkan tugasan antara utas. Executor work-stealing memindahkannya, jadi Future yang dihantar kepada spawn biasanya memerlukan Send dan selalunya 'static. Oleh itu, Send ialah keperluan pengguna (consumer), bukan sifat lalai bagi setiap kaedah trait.

Langkah 2: Kekang satu kaedah dengan RTN

RTN menambah kekangan pada jenis pulangan sesuatu kaedah. Kod berikut menggunakan sintaks nightly:

rust
trait Service<Request> {
    type Response;
    async fn call(&self, request: Request) -> Self::Response;
}

async fn spawn_call<S, R>(service: S, request: R) -> S::Response
where
    S: Service<R> + Send + 'static,
    R: Send + 'static,
    S::call(..): Send + 'static,
{
    tokio::spawn(async move { service.call(request).await })
        .await
        .expect("task failed")
}

S::call(..) merujuk kepada Future yang dikembalikan oleh kaedah trait, bukan nilai yang dihasilkan selepas menunggu (await) kaedah tersebut. Ia hanya mengekang pengguna yang memerlukan sifat itu dan membiarkan pelaksanaan tempatan boleh digunakan di tempat lain.

Langkah 3: Bandingkan varian trait

Apabila setiap kaedah async memerlukan Send, kekalkan trait asas yang dikekang secara minimum dan jana varian Send:

rust
#[trait_variant::make(SendService: Send)]
trait LocalService<R> {
    async fn call(&self, request: R) -> Response;
}

Ini sesuai untuk API awam dengan laluan lazim: pelaksana menyasarkan satu trait asas dan pemanggil memilih varian tempatan atau Send. Pertukaran komprominya ialah kejituan peringkat kaedah yang lebih lemah; RTN lebih baik apabila hanya satu kaedah memerlukan kekangan tersebut.

Langkah 4: Pilih GAT apabila penamaan stabil diperlukan

Jika Rust stabil mesti menamakan Future yang dikembalikan, GAT boleh mendedahkan associated type:

rust
trait StableService {
    type Future<'a>: Future<Output = Response> + Send + 'a
    where
        Self: 'a;
    fn call(&self) -> Self::Future<'_>;
}

Setiap pelaksanaan mesti menyediakan jenis Future yang konkrit, yang meningkatkan boilerplate. Gunakan ini apabila sokongan stabil serta penyimpanan atau penggunaan semula jenis Future mengatasi ergonomik sintaks async.

Langkah 5: Sahkan batasan dan risiko penghijrahan

Pasukan Rust mendokumentasikan bahawa RTN terpakai pada associated function trait atau kaedah yang menggunakan AFIT/RPITIT; ia tidak boleh digunakan sebagai jenis medan struct pada masa ini. Uji sintaks, diagnostik, dan pengembangan makro dalam CI nightly, sambil mengekalkan laluan varian trait atau GAT untuk keluaran stabil.

Langkah 6: Tukar perbandingan menjadi peraturan

Pilih RTN untuk keperluan merentas utas peringkat kaedah dan peringkat tapak panggilan; varian trait apabila semua kaedah berkongsi keperluan yang sama; dan GAT apabila Rust stabil serta jenis pulangan yang boleh dinamakan adalah wajib. Jika executor tidak pernah memindahkan tugasan, kekalkan Future tempatan dan bukannya mengecilkan set pelaksanaan semata-mata demi keselamatan yang dirasakan.

Model jawapan berkualiti tinggi

Mula-mula saya bertanya sama ada executor boleh memindahkan tugasan. Dengan executor work-stealing seperti Tokio, outer Future yang di-spawn biasanya memerlukan Send + 'static; dengan executor satu utas, menyebarkan kekangan itu kepada setiap pelaksanaan adalah tidak perlu. Saya mengekalkan trait asas minimum dan hanya menulis S::call(..): Send + 'static dalam fungsi generik yang merentasi utas. Ini mengekalkan pelaksanaan yang mana Future call adalah untuk kegunaan tempatan sahaja.

Jika setiap kaedah async mesti boleh dipindahkan, saya menggunakan varian trait untuk menawarkan API Send. Jika projek mesti kekal pada Rust stabil dan perlu menamakan atau menyimpan Future, saya menggunakan GAT. RTN masih nightly, jadi saya mengasingkannya dalam CI dan mengekalkan reka bentuk stabil daripada menjadikan sintaks eksperimen sebagai versi minimum awam.

Kesilapan lazim

  • Gejala → Menambah Send pada setiap pulangan async dalam trait → Sebab gagal → Pelaksanaan satu utas yang sah dikecualikan → Pembetulan → Letakkan kekangan pada pengguna merentas utas.
  • Gejala → Menganggap S::call(..) sebagai jenis hasil yang ditunggu (awaited) → Sebab gagal → RTN mengekang Future yang dikembalikan, bukan S::ResponsePembetulan → Nyatakan perbezaan tersebut secara eksplisit.
  • Gejala → Mengeluarkan RTN nightly terus ke pengeluaran → Sebab gagal → Sokongan pengkompil dan sintaks mungkin berubah → Pembetulan → Pinkan nightly dalam CI dan kekalkan sandaran stabil.
  • Gejala → Menukar setiap async trait kepada GAT → Sebab gagal → Pelaksana mesti mendedahkan jenis Future yang konkrit → Pembetulan → Gunakan GAT hanya apabila penamaan stabil merupakan keperluan sebenar.
  • Gejala → Hanya menyemak sama ada Service adalah SendSebab gagal → Perkhidmatan yang boleh dipindahkan masih boleh mengembalikan Future bukan Send → Pembetulan → Kekang objek, argumen, Future yang dikembalikan, dan outer task secara berasingan.

Soalan susulan dan respons

Soalan susulan 1: Mengapakah S: Send tidak mencukupi untuk S::call(..): Send?

S: Send menyatakan bahawa nilai perkhidmatan boleh berpindah antara utas. Kaedahnya masih boleh menangkap Rc atau nilai bukan Send lain dalam Future. Spawn merentas utas menyemak outer Future dan setiap Future yang ditunggu, jadi Future yang dikembalikan memerlukan kekangannya sendiri.

Soalan susulan 2: Hanya put memerlukan Send manakala get menggunakan cache tempatan. Apakah yang anda lakukan?

Kekalkan trait asas dan kekang put(..): Send milik Backend pada tapak panggilan merentas utas. Jangan jana varian yang turut mengekang get, kerana ini akan menolak pelaksanaan cache tempatan yang sah.

Soalan susulan 3: RTN belum boleh menjadi jenis medan. Bagaimanakah anda boleh menyimpan Future tersebut?

Gunakan GAT eksplisit, Future bernama, atau padamkan jenisnya pada sempadan dengan Box::pin. Pilih berdasarkan sokongan stabil, kos peruntukan, dan keperluan keselamatan objek (object-safety); RTN tidak mencipta jenis yang boleh disimpan secara automatik.

Soalan susulan 4: Bagaimanakah anda membuktikan bahawa kekangan tersebut tidak terlalu ketat?

Tulis tiga kes kompilasi: pelaksanaan tempatan yang tidak mempunyai Send tetapi berjalan pada satu utas; pelaksanaan yang mana hanya put mengembalikan Future Send; dan satu pelaksanaan yang mana setiap kaedah boleh di-spawn merentasi utas. Jalankan setiap satu pada versi stabil dan nightly serta sahkan bahawa kegagalan berlaku pada tapak panggilan yang dimaksudkan.

Sumber awam

Soalan berkaitan

Alat temu duga berkaitan

Gunakan Tangkapan Skrin untuk gesaan pengekodan

Tangkap soalan, kemudian selesaikan kekangan, penyelesaian, kod, kes pinggir dan kerumitan mengikut urutan.

Lihat alat