Bagikan melalui


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);

Diagram penjadwalan reentransi.

Alur eksekusi adalah sebagai berikut:

  1. Panggilan tiba di A, yang mencatat "1" lalu mengeluarkan panggilan ke B.
  2. B segera kembali dari Ping() kembali ke A.
  3. 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

Diagram penjadwalan ulang tanpa kebuntuan.

Dalam contoh ini:

  1. Panggilan Ping() dari A tiba di B sebelum CallOther(a) panggilan tiba di B.
  2. Oleh karena itu, B memproses Ping() panggilan sebelum CallOther(a) panggilan.
  3. Karena B memproses Ping() panggilan, A dapat kembali ke pemanggil.
  4. Ketika B mengeluarkan panggilannya Ping() ke A, A masih sibuk mencatat pesannya ("2"), jadi panggilan harus menunggu durasi singkat, tetapi segera dapat diproses.
  5. 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

Diagram penjadwalan ulang dengan kebuntuan.

Dalam contoh ini:

  1. Panggilan CallOther tiba di butir masing-masing dan diproses secara bersamaan.
  2. Kedua log biji-bijian "1" dan lanjutkan ke await other.Ping().
  3. Karena kedua butir masih sibuk (memproses CallOther permintaan, yang belum selesai), permintaan Ping() menunggu.
  4. Setelah beberapa saat, Orleans menentukan bahwa panggilan telah habis dan setiap Ping() panggilan menghasilkan pengecualian yang dilemparkan.
  5. 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:

Dengan masuknya kembali, kasus berikut menjadi eksekusi yang valid dan kemungkinan kebuntuan di atas dihapus.

Kasus 3: butir atau metode masuk kembali

Diagram penjadwalan ulang reentri dengan butir atau metode yang bersifat reentri.

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.
    }
}