다음을 통해 공유


요청 일정

조직 활성화에는 단일 스레드 실행 모델이 있으며, 기본적으로 다음 요청이 처리를 시작하기 전에 각 요청을 처음부터 완료까지 처리합니다. 경우에 따라 한 요청이 비동기 작업이 완료되기를 기다리는 동안 다른 요청을 처리하기 위해 활성화하는 것이 바람직할 수 있습니다. 이러한 이유로 인해 Orleans는 재진입 섹션에 설명된 대로 개발자에게 요청 인터리브 동작을 일부 제어할 수 있도록 합니다. 다음은 Orleans의 기본 동작인 재진입하지 않는 요청 예약의 예입니다.

다음 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");
    }
}

예제 AB에는 PingGrain 형식의 두 조직이 포함됩니다. 호출자는 다음 호출을 호출합니다.

var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);

재진입 일정 다이어그램

실행 흐름은 다음과 같습니다.

  1. 호출은 A에 도착하여 "1"를 기록한 다음, B에 호출을 발급합니다.
  2. BPing()에서 즉시 A로 돌아갑니다.
  3. A"2"를 기록하고 원래 호출자에게 돌아갑니다.

AB에 대한 호출을 기다리는 동안에는 들어오는 요청을 처리할 수 없습니다. 결과적으로 AB가 동시에 서로 호출하는 경우 해당 호출이 완료되기를 기다리는 동안 교착 상태가 발생할 수 있습니다. 다음 호출을 실행하는 클라이언트를 기반으로 하는 예제는 다음과 같습니다.

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

사례 1: 호출이 교착 상태에 빠지지 않음

교착 상태가 없는 재진입 일정 다이어그램

이 예제에 대한 설명:

  1. APing() 호출은 CallOther(a) 호출이 B에 도착하기 전에 B에 도착합니다.
  2. 따라서 BCallOther(a) 호출 전에 Ping() 호출을 처리합니다.
  3. BPing() 호출을 처리하므로 A는 호출자에게 다시 돌아갈 수 있습니다.
  4. BA에 대한 Ping() 호출을 발행하는 경우 A는 여전히 메시지("2")를 로깅하는 중이므로 호출은 짧은 기간 동안 기다려야 하지만 곧 처리될 수 있습니다.
  5. APing() 호출을 처리하고 원래 호출자에게 반환되는 B로 돌아갑니다.

동일한 코드가 약간 다른 타이밍으로 인해 교착 상태가 발생하는 일련의 이벤트를 살펴보겠습니다.

사례 2: 호출 교착 상태

교착 상태가 있는 재진입 일정 다이어그램

이 예제에 대한 설명:

  1. CallOther 호출은 각각의 조직에 도착하고 동시에 처리됩니다.
  2. 두 조직 모두 "1"를 기록하고 await other.Ping()으로 진행합니다.
  3. 두 조직 모두 여전히 사용 중이므로(아직 완료되지 않은 CallOther 요청 처리 중) Ping() 요청이 대기합니다.
  4. 잠시 후 Orleans는 호출이 시간 초과되었음을 확인하고 각 Ping() 호출로 인해 예외가 throw됩니다.
  5. CallOther 메서드 본문은 예외를 처리하지 않으며 원래 호출자에게 전달됩니다.

다음 섹션에서는 여러 요청이 실행을 서로 인터리브하도록 허용하여 교착 상태를 방지하는 방법을 설명합니다.

다시 표시

Orleans는 기본적으로 안전한 실행 흐름, 즉 여러 요청 중에 조직의 내부 상태가 동시에 수정되지 않는 흐름을 선택합니다. 내부 상태를 동시에 수정하면 논리가 복잡해지고 개발자에게 더 큰 부담을 줍니다. 이러한 종류의 동시성 버그로부터 보호하려면 이전에 토론한 대로 주로 활성화라는 비용이 발생합니다. 특정 호출 패턴은 교착 상태로 이어질 수 있습니다. 교착 상태를 방지하는 한 가지 방법은 조직 호출로 인해 순환이 발생하지 않도록 하는 것입니다. 순환이 없고 교착 상태에 빠질 수 없는 코드를 작성하는 것이 어려운 경우가 많습니다. 다음 요청을 처리하기 전에 각 요청이 처음부터 완료될 때까지 기다리면 성능이 저하될 수 있습니다. 예를 들어 기본적으로 조직 메서드가 데이터베이스 서비스에 대한 비동기 요청을 수행하는 경우 조직은 데이터베이스의 응답이 조직에 도착할 때까지 요청 실행을 일시 중지합니다.

각 사례는 다음 섹션에서 토론됩니다. 이러한 이유로 Orleans는 개발자에게 일부 또는 모든 요청을 동시에 실행할 수 있는 옵션을 제공하여 실행을 서로 인터리빙합니다. Orleans에서는 이러한 문제를 재진입 또는 인터리빙이라고 합니다. 요청을 동시에 실행함으로써 비동기 작업을 수행하는 조직은 더 짧은 기간에 더 많은 요청을 처리할 수 있습니다.

다음과 같은 경우 여러 요청이 인터리브될 수 있습니다.

재진입을 사용하면 다음 사례가 유효한 실행이 되고 위의 교착 상태가 될 가능성이 제거됩니다.

사례 3: 조직 또는 메서드가 재진입됨

재진입 조직 또는 메서드를 사용하는 재진입 일정 다이어그램

이 예제에서 조직 AB는 둘 다 재진입되기 때문에 요청 일정 교착 상태에 대한 가능성 없이 동시에 서로 호출할 수 있습니다. 다음 섹션에서는 재진입에 대한 자세한 내용을 제공합니다.

재진입 조직

Grain 구현 클래스는 다른 요청이 자유롭게 인터리브될 수 있음을 나타내기 위해 ReentrantAttribute로 표시될 수 있습니다.

즉, 재진입 활성화는 이전 요청의 처리가 완료되지 않은 동안 다른 요청 실행을 시작할 수 있습니다. 실행은 여전히 단일 스레드로 제한되므로 활성화는 한 번에 한 번씩 실행되고 각 턴은 활성화 요청 중 하나만 대신하여 실행됩니다.

재진입 조직 코드는 여러 조각의 조직 코드를 병렬로 실행하지 않지만(조직 코드 실행은 항상 단일 스레드임) 재진입 조직은 인터리빙된 다양한 요청에 대한 코드 실행을 볼 수 있습니다. 즉, 다른 요청에서 연속이 전환되면 인터리브할 수 있습니다.

예를 들어, 다음 의사 코드에 표시된 것처럼 FooBar는 동일한 조직 클래스의 두 메서드라고 생각합니다.

Task Foo()
{
    await task1;    // line 1
    return Do2();   // line 2
}

Task Bar()
{
    await task2;   // line 3
    return Do2();  // line 4
}

이 조직이 ReentrantAttribute로 표시된 경우 FooBar의 실행이 인터리브될 수 있습니다.

예를 들어 다음 실행 순서가 가능합니다.

줄 1, 줄 3, 줄 2 및 줄 4. 즉, 서로 다른 요청 인터리브의 회전입니다.

조직이 재진입되지 않은 경우 가능한 유일한 실행은 줄 1, 줄 2, 줄 3, 줄 4 또는 줄 3, 줄 4, 줄 1, 줄 2입니다(이전 요청이 완료되기 전에 새 요청을 시작할 수 없음).

재진입 및 비재진입 조직을 선택할 때의 주요 트레이드오프는 인터리빙이 올바르게 작동하도록 만드는 코드의 복잡성과 이에 대해 추론하기가 어렵다는 점입니다.

조직이 상태 비저장이고 논리가 단순한 경우에는 저 적은 수(그러나 너무 적지는 않아 모든 하드웨어 스레드가 사용됨)의 재진입 조직이 일반적으로 약간 더 효율적입니다.

코드가 더 복잡하다면, 전체적으로 효율성이 약간 떨어지더라도 더 많은 수의 재진입하지 않는 조직을 사용하면 명확하지 않은 인터리브 문제를 파악하는 데 드는 어려움을 덜 수 있습니다.

결국 대답은 애플리케이션의 세부 사항에 따라 다릅니다.

인터리빙 메서드

AlwaysInterleaveAttribute로 표시된 조직 인터페이스 메서드는 항상 다른 요청을 인터리브하고 항상 다른 요청([AlwaysInterleave]가 아닌 메서드에 대한 요청 포함)과 인터리브할 수 있습니다.

다음 예제를 참조하세요.

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

다음 클라이언트 요청에 의해 시작된 호출 흐름을 고려합니다.

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

GoSlow에 대한 호출은 인터리브되지 않으므로 두 개의 GoSlow 호출의 총 실행 시간은 약 20초가 걸립니다. 반면, GoFastAlwaysInterleaveAttribute로 표시되고 이에 대한 세 가지 호출이 동시에 실행되어 완료하는 데 최소 30초가 필요하지 않고 총 약 10초 만에 완료됩니다.

읽기 전용 방법

조직 메서드가 조직 상태를 수정하지 않는 경우 다른 요청과 동시에 실행하는 것이 안전합니다. ReadOnlyAttribute는 메서드가 조직 상태를 수정하지 않음을 나타냅니다. 메서드를 ReadOnly로 표시하면 Orleans가 다른 ReadOnly 요청과 동시에 요청을 처리할 수 있으므로 앱 성능이 크게 개선될 수 있습니다. 다음 예제를 참조하세요.

public interface IMyGrain : IGrainWithIntegerKey
{
    Task<int> IncrementCount(int incrementBy);

    [ReadOnly]
    Task<int> GetCount();
}

호출 체인 재진입

조직이 다른 조직에서 원래 조직을 다시 호출하는 메서드를 호출하는 경우 호출이 재진입되지 않는 한 교착 상태가 발생합니다. 호출 체인 재진입을 사용하면 호출 사이트별로 재진입을 사용하도록 설정할 수 있습니다. 호출 체인 재진입을 사용하도록 설정하려면 호출 체인이 삭제될 때까지 호출 체인 아래에 있는 호출자의 재진입을 허용하는 값을 반환하는 AllowCallChainReentrancy() 메서드를 호출합니다. 여기에는 메서드 자체를 호출하는 조직의 재진입이 포함됩니다. 다음 예제를 참조하세요.

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

앞의 예에서 UserGrain.JoinRoom(roomName)ChatRoomGrain.OnJoinRoom(user)를 호출하고, 이는 사용자의 표시 이름을 가져오기 위해 UserGrain.GetDisplayName()을 다시 호출하려고 시도합니다. 이 호출 체인에는 주기가 포함되어 있으므로 UserGrain이 이 문서에서 설명하는 지원 메커니즘을 사용하여 재진입을 허용하지 않으면 교착 상태가 발생합니다. 이 인스턴스에서는 roomGrainUserGrain을 다시 호출할 수 있도록 허용하는 AllowCallChainReentrancy()를 사용하고 있습니다. 이를 통해 재진입이 사용하도록 설정되는 위치와 방법을 세밀하게 제어할 수 있습니다.

대신 IUserGrainGetDisplayName() 메서드 선언에 [AlwaysInterleave] 주석을 달아 교착 상태를 방지하려면 모든 조직이 GetDisplayName 호출을 다른 메서드와 인터리브하도록 허용합니다. 대신 scope가 삭제될 때까지roomGrain 조직에 대한 메서드를 호출하도록 허용합니다.

호출 체인 재진입 억제

호출 체인 재진입은 SuppressCallChainReentrancy() 메서드를 사용하여 억제할 수도 있습니다. 이는 최종 개발자에게 유용성이 제한되어 있지만 스트리밍브로드캐스트 채널과 같은 Orleans 조직 기능을 확장하는 라이브러리에서 내부적으로 사용하여 개발자가 모든 제어권을 보존할 수 있도록 해야 합니다. 호출 체인 재진입이 사용하도록 설정되면 종료됩니다.

GetCount 메서드는 입자 상태를 수정하지 않으므로 ReadOnly로 표시됩니다. 이 메서드 호출을 기다리는 호출자는 조직에 대한 다른 ReadOnly 요청에 의해 차단되지 않으며 메서드가 즉시 반환됩니다.

조건자를 사용한 재진입

조직 클래스는 요청을 검사하여 호출별로 인터리빙을 결정하는 조건자를 지정할 수 있습니다. [MayInterleave(string methodName)] 특성은 이 기능을 제공합니다. 특성에 대한 인수는 InvokeMethodRequest 개체를 허용하고 요청을 인터리브해야 하는지 여부를 나타내는 bool을 반환하는 조직 클래스 내의 정적 메서드 이름입니다.

다음은 요청 인수 형식에 [Interleave] 특성이 있는 경우 인터리빙을 허용하는 예제입니다.

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