Jadwal permintaan
Aktivasi grain memiliki model eksekusi satu alur dan, secara default, memproses setiap permintaan dari awal hingga selesai sebelum permintaan berikutnya dapat mulai diproses. Dalam beberapa keadaan, mungkin diinginkan agar aktivasi memproses permintaan lain sementara satu permintaan menunggu operasi asinkron selesai. Untuk alasan ini dan lainnya, Orleans memberi pengembang sebagian kontrol atas perilaku pengurutan permintaan, seperti yang dijelaskan di bagian Reentrancy. Berikut ini adalah contoh penjadwalan permintaan non-reentrant, yang merupakan perilaku default di Orleans.
Pertimbangkan definisi berikut PingGrain
:
public interface IPingGrain : IGrainWithStringKey
{
Task Ping();
Task CallOther(IPingGrain other);
}
public class PingGrain : Grain, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(ILogger<PingGrain> logger) => _logger = logger;
public Task Ping() => Task.CompletedTask;
public async Task CallOther(IPingGrain other)
{
_logger.LogInformation("1");
await other.Ping();
_logger.LogInformation("2");
}
}
Dua butir jenis PingGrain
terlibat dalam contoh kami, A dan B. Pemanggil melakukan panggilan berikut:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);
Alur eksekusi adalah sebagai berikut:
- Panggilan tiba di A, yang mencatat
"1"
lalu mengeluarkan panggilan ke B. -
B segera kembali dari
Ping()
kembali ke A. -
A mencatat
"2"
dan mengembalikan ke pemanggil asli.
Saat A menunggu panggilan ke B, A tidak dapat memproses permintaan masuk apa pun. Akibatnya, jika A dan B saling memanggil secara bersamaan, mereka mungkin mengalami kebuntuan sambil menunggu panggilan tersebut selesai. Berikut adalah contohnya, berdasarkan panggilan yang dikeluarkan klien berikut:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
// A calls B at the same time as B calls A.
// This might deadlock, depending on the non-deterministic timing of events.
await Task.WhenAll(a.CallOther(b), b.CallOther(a));
Kasus 1: panggilan tidak kebuntuan
Dalam contoh ini:
- Panggilan
Ping()
dari A tiba di B sebelumCallOther(a)
panggilan tiba di B. - Oleh karena itu, B memproses
Ping()
panggilan sebelumCallOther(a)
panggilan. - Karena B memproses
Ping()
panggilan, A dapat kembali ke pemanggil. - Ketika B mengeluarkan panggilannya
Ping()
ke A, A masih sibuk mencatat pesannya ("2"
), jadi panggilan harus menunggu durasi singkat, tetapi segera dapat diproses. -
A memproses
Ping()
panggilan dan mengembalikannya ke B, yang kemudian kembali ke pemanggil asli.
Pertimbangkan serangkaian peristiwa yang kurang beruntung: satu di mana kode yang sama mengakibatkan kebuntuan karena waktu yang sedikit berbeda.
Kasus 2: panggilan mengalami deadlock
Dalam contoh ini:
- Panggilan
CallOther
tiba di butir masing-masing dan diproses secara bersamaan. - Kedua log biji-bijian
"1"
dan lanjutkan keawait other.Ping()
. - Karena kedua butir masih sibuk (memproses
CallOther
permintaan, yang belum selesai), permintaanPing()
menunggu. - Setelah beberapa saat, Orleans menentukan bahwa panggilan telah habis dan setiap
Ping()
panggilan menghasilkan pengecualian yang dilemparkan. - Isi metode
CallOther
tidak menangani pengecualian dan diteruskan ke pemanggil asli.
Bagian berikut menjelaskan cara mencegah deadlock dengan memungkinkan beberapa permintaan untuk menyela eksekusi mereka satu sama lain.
Masuknya kembali
Orleans secara default memilih alur pelaksanaan yang aman: di mana status internal grain tidak dimodifikasi secara bersamaan selama permintaan simultan. Modifikasi bersamaan dari status internal mempersulit logika dan menempatkan beban yang lebih besar pada pengembang. Perlindungan terhadap jenis bug konkurensi ini memiliki biaya, yang sebelumnya telah dibahas, terutama terkait dengan kelangsungan: pola panggilan tertentu dapat menyebabkan kebuntuan. Salah satu cara untuk menghindari kebuntuan adalah dengan memastikan bahwa panggilan biji-bijian tidak pernah menghasilkan siklus. Seringkali, sulit untuk menulis kode yang bebas siklus dan tidak dapat kebuntuan. Menunggu setiap permintaan berjalan dari awal hingga selesai sebelum memproses permintaan berikutnya juga dapat merusak performa. Misalnya, secara default, jika metode grain melakukan beberapa permintaan asinkron ke layanan database, maka grain menjeda eksekusi permintaan sampai respons dari database tiba di grain.
Masing-masing kasus tersebut dibahas di bagian berikut. Untuk alasan ini, Orleans memberi pengembang opsi untuk memungkinkan beberapa atau semua permintaan dijalankan secara bersamaan, dengan cara saling tumpang tindih dalam eksekusi mereka. Dalam Orleans, kekhawatiran tersebut disebut sebagai reentrancy atau interleaving. Dengan menjalankan permintaan secara bersamaan, biji-bijian yang melakukan operasi asinkron dapat memproses lebih banyak permintaan dalam waktu yang lebih singkat.
Beberapa permintaan dapat diselingi dalam kasus berikut:
- Kelas biji-bijian ditandai dengan ReentrantAttribute.
- Metode antarmuka ditandai dengan AlwaysInterleaveAttribute.
- Predikat dari biji-bijian MayInterleaveAttribute mengembalikan
true
.
Dengan masuknya kembali, kasus berikut menjadi eksekusi yang valid dan kemungkinan kebuntuan di atas dihapus.
Kasus 3: butir atau metode masuk kembali
Dalam contoh ini, unit kerja A dan B dapat saling memanggil secara bersamaan tanpa potensi deadlock jadwal permintaan karena kedua unit kerja tersebut bersifat re-entran. Bagian berikut memberikan penjelasan lebih detail tentang reentransi.
Melarutkan kembali butiran
Kelas Grain implementasi dapat ditandai dengan ReentrantAttribute untuk menunjukkan bahwa permintaan yang berbeda dapat disusun secara bebas.
Dengan kata lain, aktivasi re-entran dapat mulai menjalankan permintaan lain saat permintaan sebelumnya belum selesai diproses. Eksekusi masih terbatas pada satu utas, sehingga aktivasi masih menjalankan satu giliran pada satu waktu, dan setiap giliran dijalankan atas nama hanya salah satu permintaan aktivasi.
Kode grain re-entrant tidak pernah menjalankan beberapa potongan kode grain secara paralel (eksekusi kode grain selalu single-threaded), tetapi grain re-entrant mungkin melihat eksekusi kode untuk permintaan yang berbeda saling bersilangan. Artinya, urutan kelanjutan dari permintaan yang berbeda dapat saling berselang-seling.
Misalnya, seperti yang ditunjukkan dalam kode pseudo berikut, pertimbangkan bahwa Foo
dan Bar
merupakan dua metode dari kelas biji-bijian yang sama:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Jika butir ini ditandai ReentrantAttribute, eksekusi Foo
dan Bar
dapat saling bersilangan.
Misalnya, urutan eksekusi berikut dimungkinkan:
Baris 1, baris 3, baris 2 dan baris 4. Artinya, giliran dari permintaan yang berbeda saling bergantian.
Jika proses tidak dapat dimulai ulang, satu-satunya urutan eksekusi yang mungkin adalah: baris 1, baris 2, baris 3, baris 4 ATAU: baris 3, baris 4, baris 1, baris 2 (permintaan baru tidak dapat dimulai sebelum yang sebelumnya selesai).
Tradeoff utama dalam memilih antara reentrant dan nonreentrant grains adalah kompleksitas kode dalam mengimplementasikan interleaving dengan benar, dan kesulitan untuk memahaminya secara logis.
Dalam kasus sederhana ketika gandum tanpa status dan logikanya yang sederhana, lebih sedikit (tetapi tidak terlalu sedikit, sehingga semua utas perangkat keras digunakan) gandum re-entran harus, secara umum, sedikit lebih efisien.
Jika kode lebih kompleks, maka sejumlah besar komponen non-reentrant, bahkan jika sedikit kurang efisien secara keseluruhan, harus menghindarkan Anda dari kesulitan dalam mencari tahu masalah interleaving yang tidak mudah dipahami.
Pada akhirnya, jawabannya tergantung pada spesifikasi aplikasi.
Metode interleaving
Metode antarmuka grain yang ditandai dengan AlwaysInterleaveAttribute, selalu menginterleave permintaan lain dan dapat selalu diinterleave dengan permintaan lain, bahkan permintaan untuk metode yang tidak ditandai sebagai [AlwaysInterleave].
Pertimbangkan contoh berikut:
public interface ISlowpokeGrain : IGrainWithIntegerKey
{
Task GoSlow();
[AlwaysInterleave]
Task GoFast();
}
public class SlowpokeGrain : Grain, ISlowpokeGrain
{
public async Task GoSlow()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
public async Task GoFast()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
}
Pertimbangkan alur panggilan yang dimulai oleh permintaan klien berikut:
var slowpoke = client.GetGrain<ISlowpokeGrain>(0);
// A. This will take around 20 seconds.
await Task.WhenAll(slowpoke.GoSlow(), slowpoke.GoSlow());
// B. This will take around 10 seconds.
await Task.WhenAll(slowpoke.GoFast(), slowpoke.GoFast(), slowpoke.GoFast());
Panggilan ke GoSlow
tidak dilakukan secara bergantian, sehingga total waktu eksekusi untuk dua panggilan GoSlow
memakan waktu sekitar 20 detik. Di sisi lain, GoFast
ditandai AlwaysInterleaveAttribute, dan tiga panggilan untuk itu dijalankan secara bersamaan, selesai dalam total sekitar 10 detik alih-alih membutuhkan setidaknya 30 detik untuk diselesaikan.
Metode hanya baca
Ketika metode grain tidak mengubah keadaan grain, aman untuk dijalankan bersamaan dengan permintaan lain.
ReadOnlyAttribute menunjukkan bahwa metode tidak memodifikasi keadaan grain. Menandai metode sebagai ReadOnly
memungkinkan Orleans untuk memproses permintaan Anda secara bersamaan dengan permintaan lain ReadOnly
, yang dapat secara signifikan meningkatkan performa aplikasi Anda. Pertimbangkan contoh berikut:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
Metode GetCount
tidak memodifikasi kondisi butir, sehingga ditandai dengan ReadOnly
. Pemanggil yang menunggu pemanggilan metode ini tidak diblokir oleh permintaan lain ReadOnly
ke grain, dan metode ini mengembalikan hasilnya segera.
Reentri pada rantai panggilan
Jika sebuah grain memanggil metode pada grain lain yang kemudian memanggil kembali ke grain asli, panggilan tersebut akan mengakibatkan kebuntuan kecuali panggilan tersebut bersifat reentrant. Reentrancy dapat diaktifkan berdasarkan per situs panggilan dengan menggunakan reentrancy rantai panggilan. Untuk mengaktifkan reentransi rantai panggilan, panggil metode AllowCallChainReentrancy(), yang mengembalikan nilai yang memungkinkan reentry dari penelepon mana pun lebih jauh dalam rantai panggilan sampai dibersihkan. Ini termasuk masuknya kembali dari biji-bijian yang memanggil metode itu sendiri. Pertimbangkan contoh berikut:
public interface IChatRoomGrain : IGrainWithStringKey
{
ValueTask OnJoinRoom(IUserGrain user);
}
public interface IUserGrain : IGrainWithStringKey
{
ValueTask JoinRoom(string roomName);
ValueTask<string> GetDisplayName();
}
public class ChatRoomGrain : Grain<List<(string DisplayName, IUserGrain User)>>, IChatRoomGrain
{
public async ValueTask OnJoinRoom(IUserGrain user)
{
var displayName = await user.GetDisplayName();
State.Add((displayName, user));
await WriteStateAsync();
}
}
public class UserGrain : Grain, IUserGrain
{
public ValueTask<string> GetDisplayName() => new(this.GetPrimaryKeyString());
public async ValueTask JoinRoom(string roomName)
{
// This prevents the call below from triggering a deadlock.
using var scope = RequestContext.AllowCallChainReentrancy();
var roomGrain = GrainFactory.GetGrain<IChatRoomGrain>(roomName);
await roomGrain.OnJoinRoom(this.AsReference<IUserGrain>());
}
}
Dalam contoh sebelumnya, UserGrain.JoinRoom(roomName)
memanggil ke ChatRoomGrain.OnJoinRoom(user)
, yang berusaha untuk dipanggil kembali oleh UserGrain.GetDisplayName()
untuk mendapatkan nama tampilan pengguna. Karena rantai panggilan ini melibatkan siklus, ini akan mengakibatkan kebuntuan jika UserGrain
tidak mengizinkan masuknya kembali menggunakan salah satu mekanisme yang didukung yang dibahas dalam artikel ini. Dalam hal ini, kita menggunakan AllowCallChainReentrancy(), yang hanya roomGrain
memungkinkan untuk memanggil kembali ke UserGrain
. Ini memberi Anda kontrol yang sangat rinci atas tempat dan bagaimana reentrancy diaktifkan.
Jika Anda sebaliknya mencegah deadlock dengan memberi anotasi pada deklarasi metode GetDisplayName()
di IUserGrain
dengan [AlwaysInterleave]
, Anda akan mengizinkan unit apa pun untuk menggabungkan panggilan GetDisplayName
dengan metode lainnya. Sebaliknya, Anda mengizinkan hanya untuk memanggil metode pada grain kami dan hanya sampai scope
dibuang.
Menonaktifkan reentransi rantai panggilan
Rantai panggilan yang berulang juga dapat ditekan dengan menggunakan metode SuppressCallChainReentrancy(). Ini memiliki kegunaan terbatas bagi pengembang akhir, tetapi penting untuk penggunaan internal oleh pustaka yang memperluas Orleans fungsionalitas grain, seperti streaming dan saluran siaran untuk memastikan bahwa pengembang mempertahankan kontrol penuh ketika reentransi rantai panggilan diaktifkan.
Reentrancy menggunakan predikat
Kelas biji-bijian dapat menentukan predikat untuk menentukan interleaving berdasarkan panggilan demi panggilan dengan memeriksa permintaan. Atribut [MayInterleave(string methodName)]
ini menyediakan fungsionalitas ini. Argumen ke atribut adalah nama metode statis dalam kelas grain yang menerima objek InvokeMethodRequest dan mengembalikan bool
yang menentukan apakah permintaan harus diselingi atau tidak.
Berikut adalah contoh yang memungkinkan interleaving jika jenis argumen permintaan memiliki [Interleave]
atribut :
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class InterleaveAttribute : Attribute { }
// Specify the may-interleave predicate.
[MayInterleave(nameof(ArgHasInterleaveAttribute))]
public class MyGrain : Grain, IMyGrain
{
public static bool ArgHasInterleaveAttribute(IInvokable req)
{
// Returning true indicates that this call should be interleaved with other calls.
// Returning false indicates the opposite.
return req.Arguments.Length == 1
&& req.Arguments[0]?.GetType()
.GetCustomAttribute<InterleaveAttribute>() != null;
}
public Task Process(object payload)
{
// Process the object.
}
}