簡體   English   中英

反應性擴展-在可觀察范圍內將每個onNext呼叫解耦

[英]Reactive Extensions - De-coupling each onNext call within an observable

請和我一起放松,我也在學習這個主題,並且真的很喜歡它。 所以這里...

我從流中創建一個可觀察的讀數,當有足夠的數據輸入並代表我需要觸發的“ IMyEvent”類型時,我將構建該類型並調用observer.OnNext。

此流是來自服務器的響應流,命令從我本人或從外部發送到該服務器,因此我可以讀取此流並根據此作出反應。

對於我發送的每個命令,我都訂閱了由此流發出的可觀察對象。 我可以查看我的命令是否成功完成。 我還從外部訂閱此流,並對可能發生的其他事件做出反應。

讓我們舉個例子,假設有人從外部加入該服務器的會議 ,服務器將沿着該流發送數據,然后我的Observable捕獲此事件並調用觀察者的onNext。 我想據此作出反應並鎖定會議 編輯這里...鎖定環境的意思是我向服務器發送了一個Lock命令,並且服務器知道“鎖定自己”不會允許任何其他人加入會議

我有一個客戶端,在執行此訂閱中的onNext時,總是訂閱“ SomeoneJoinsEvent”(它們訂閱了可觀察的流),我還觸發了鎖定會議的命令。 然后,該命令還臨時訂閱(我使用超時操作符)同一可觀察流,但是我注意到我在這里被阻止/鎖定。

我可以看到,當我執行onNext時,它不會繼續讀取流以監視更多IMyEvent。

因此,僅遵循我的想法/想法/腦筋...是否有一種特殊的方法可以使該Observable流脫鈎,因此它會連續向其所有訂閱者讀取並觸發OnNext,而不必等待它們完成? 或暴露中間與主Observable流安全分離的內容。

  1. 我本以為可以在內部訂閱此Observable流並創建2個新的Observable,但是當我訂閱新的Observable時,我仍會進一步傳播這個問題。 也許我在這里錯了,請參閱Brainfart 2和3。
  2. 讀完這篇文章之后,我還認為也許我應該在一個單獨的線程上訂閱此Observable流,並將每個IMyEvent添加到2個隊列中,也許然后將其解耦並允許我分別訂閱每個隊列,一個隊列訂閱用於等待我的內部調用對於要完成的命令以及對我的外部調用的訂閱,以某種方式仍然感覺不對,如果我有很多此Observable流的外部訂閱者,並且其中一個阻止,該怎么辦?
  3. 我覺得我有點困惑,現在我想我的主要Observable流是.Publish.RefCount,我要做的就是每次我想接收IMyEvent時都創建一個新的Observable(這與創建隊列,例如腦放屁2),訂閱了可連接的主要可觀察對象? 因此,內部命令訂閱了1個可觀察到的等待正確的事件,而1個可觀察到的暴露給外部,我還沒有考慮或嘗試過這一點。

有人可以在這里幫助我嗎? 我希望這是我想要做的事情。

這是我的代碼的示例...

//This is the observable that I create from the stream of events
public IObservable<IMyEvent> MyEvents()
{
    if (_myEventObservable != null)
    {
        return _myEventObservable;
    }

    // Here is one observable that reads data as it comes in off the asynchronous stream.
    _myEventObservable = Observable.Create<IMyEvent>(async observer =>
    {
        var myServerEventStream = await GetStreamFromMyServer(_myAuthenticationConfiguration, _token, _httpClientWrapper);
        var streamReaderAsyncState = _streamReaderAsyncStateFactory.CreateStreamReadAsyncState(myServerEventStream, 255);

        var currentStateDisposable = new SerialDisposable();
        var iterator = new Action<IStreamReaderAsyncState, Action<IStreamReaderAsyncState>>((state, self) =>
        {
            try
            {
                currentStateDisposable.Disposable = state.StreamObservable().Subscribe(
                    //OnNext
                    bytesRead =>
                    {
                        if (bytesRead == 0)
                        {
                            observer.OnCompleted();
                        }
                        else
                        {
                            //In here it does all the magic to put all the data together and only call OnNext on the observer when there is a complete IMyEvent.
                            //It is just a plain observer.OnNext(IMyEvent whatever event we have build up)
                            _messageParser.Parse(new DataChunk(state.Buffer, bytesRead), _token, observer);
                            self(state);
                        }
                    });
            }
            catch (Exception e)
            {
                observer.OnError(e);
            }
        });

        var schedulerDisposable = TaskPoolScheduler.Default.Schedule(streamReaderAsyncState, iterator);
        return new CompositeDisposable(myServerEventStream, schedulerDisposable, currentStateDisposable);
    }).Publish().RefCount();

    return _myEventObservable;
}

//Just for fuller visibility on the stream reader async state, here is the class that is returned from the "_streamReaderAsyncStateFactory.CreateStreamReadAsyncState(myServerEventStream, 255)" call   
internal class StreamReaderAsyncState : IStreamReaderAsyncState
{
    private readonly IObservable<int> _readAsyncObservable;

    public StreamReaderAsyncState(Stream stream, int bufferSize)
    {
        Buffer = new byte[bufferSize];
        _readAsyncObservable = Observable.FromAsync(() => stream.ReadAsync(Buffer, 0, bufferSize));
    }

    public byte[] Buffer { get; private set; }

    public IObservable<int> StreamObservable()
    {
        return _readAsyncObservable;
    }
}


//Externally I subscribe to this like so...
MyEvents.OfType<SomoneJoinsEvent>
.Subscribe(
    //I read somewhere that I shouldn't be making this async and using a selectmany with the async, but I am unsure.
    async myEvent => {
        await LockEnvironment()
    }
)

//The LockEnvironment call
//The myCommandPost is the LockEnvironment Command that is passed in.
private async Task<CommandResponse<TCommandResponseDto>> PostCommandAndWaitForEvent<TEventToWaitFor, TCommandResponseDto>(IMyCommandPost myCommandPost)
    where TEventToWaitFor : IMyEvent
{
    //So my intention here is to zip together the result of the post command with the TEventToWaitFor and return the first one. Otherwise if it takes too long it will return the result of the Timeout operator.
    return await MyEvents()
                    .OfType<TEventToWaitFor>()
                    //This myCommandPost.PostCommand... returns a Task<CommandResponse<TCommandResponseDto>>
                    .Zip(myCommandPost.PostCommand<TCommandResponseDto>().ToObservable(), (myEvent, myCommandResponse) => myCommandResponse)
                    .Timeout(new TimeSpan(0, 0, _myAuthenticationConfiguration.TimeoutToWaitForCommands), TimedOutLookingForMyEvent<TCommandResponseDto>())
                    .FirstAsync();
}

//The timeout observable
private IObservable<CommandResponse<TCommandResponseDto>> TimedOutLookingForMyEvent<TCommandResponseDto>()
{
    return Observable.Create<CommandResponse<TCommandResponseDto>>(observable =>
    {
        observable.OnNext(new CommandResponse<TCommandResponseDto>());
        return Disposable.Empty;
    });
}

在這里也進行了編輯,添加了我對事件解析器所做的...

internal class MyEventParser : IMessageParser
{
    private readonly ILogService _logService;
    private readonly IMyEventFactory _MyEventFactory;
    private readonly StringBuilder _data = new StringBuilder();
    private const string EventDelimiter = "\n\n";
    private readonly Regex _eventDelimiterRegex = new Regex("\n{3,}");

    public MyEventParser(ILogService logService, IMyEventFactory myEventFactory)
    {
        _logService = logService;
        _myEventFactory = myEventFactory;
    }

    public void Parse(DataChunk dataChunk, string token, IObserver<IMyEvent> myEventObserver)
    {
        _data.Append(dataChunk);
        CleanUpEventDelimiterInData();
        var numberOfSubstrings = CountNumberOfSubstrings(EventDelimiter, _data.ToString());
        if (numberOfSubstrings == 0)
        {
            return;
        }

        var events = _data.ToString().Split(new[]{EventDelimiter}, StringSplitOptions.RemoveEmptyEntries);

        events.Take(numberOfSubstrings).Foreach(x =>
        {
            _logService.InfoFormat("MyEventParser - {0} - OnNext: \n\n{1}\n\n", token.Substring(token.Length -10), x);
            myEventObserver.OnNext(_myEventFactory.Create(x));
        });

        //Clean up data of what has already been fired.
        if (events.Count() == numberOfSubstrings)
        {
            _data.Clear();
        }
        else
        {
            _data.Clear();
            _data.Append(events.Last());
        }
    }

    private void CleanUpEventDelimiterInData()
    {
        var eventDelimitersFixed = _eventDelimiterRegex.Replace(_data.ToString(), EventDelimiter);
        _data.Clear();
        _data.Append(eventDelimitersFixed);
    }

    private int CountNumberOfSubstrings(string subString, string source)
    {
        var i = 0;
        var count = 0;
        while ((i = source.IndexOf(subString, i, StringComparison.InvariantCulture)) != -1)
        {
            i += subString.Length;
            count++;
        }

        return count;
    }
}

預先感謝您的所有幫助:-)

首先,歡迎使用Rx和反應式編程。 剛開始時可能會造成混淆,但是掌握基本權利將使其成倍地容易。

首先,我想快速瀏覽一下您的代碼

public IObservable<IMyEvent> MyEvents()
{
    if (_myEventObservable != null)
    {
        return _myEventObservable;
    }

看起來它應該是具有private readonly后備字段的屬性。 可觀察的序列會延遲計算,因此如果沒有人訂閱,則任何代碼都不會運行。

如果我有很多此Observable流的外部訂戶,並且其中一個阻止,該怎么辦?

好吧,那么您就有一個行為不端的觀察者。 Rx Observable序列的訂閱者應盡快處理其回調。 這意味着沒有鎖定,沒有IO,沒有CPU密集型處理。 如果您需要執行此操作,則可能需要將一條消息排隊,然后在其他地方/某個時間進行。

似乎StreamReaderAsyncState做得還不夠,而MyEvents做得太多。 也許如果您有這樣的事情(從https://github.com/LeeCampbell/RxCookbook/tree/master/IO/Disk修改)

public static class ObservableStreamExtensions
{
    public static IObservable<byte[]> ToObservable(this Stream source, int bufferSize)
    {
        return Observable.Create<byte[]>(async (o, cts) =>
        {
            var buffer = new byte[bufferSize];
            var bytesRead = 0;
            do 
            {
                try
                {
                    bytesRead = await source.ReadAsync(buffer, 0, bufferSize);
                    if (bytesRead > 0)
                    {
                        var output = new byte[bytesRead];
                        Array.Copy(buffer, output, bytesRead);
                        o.OnNext(output);   
                    }
                }
                catch (Exception e)
                {
                    o.OnError(e);
                }
            }
            while (!cts.IsCancellationRequested && bytesRead > 0);

            if (!cts.IsCancellationRequested && bytesRead == 0)
            {
                o.OnCompleted();
            }
        });
    }
}

然后您的代碼被簡化為

private readonly IObservable<IMyEvent> _myEvents;
public ctor()
{
    _myEvents = return Observable.Create<IMyEvent>(async observer =>
    {
        var bufferSize = 255;
        var myServerEventStream = await GetStreamFromMyServer(_pexipAuthenticationConfiguration, _token, _httpClientWrapper);

        var subscription = myServerEventStream
            .ToObservable(bufferSize)
            .Select(buffer=>new DataChunk(buffer, _token))
            .Subscribe(observer);

        return new CompositeDisposable(myServerEventStream, subscription);
    })
    .Publish()
    .RefCount();
}

//This is the observable that I create from the stream of events
public IObservable<IMyEvent> MyData{ get { return _myEvents; } }

請注意, new DataChunk現在不使用IObserver ,對我而言,它看起來像代碼的味道。 還要注意,在您的評論顫抖中使用術語“魔術”。 這些都不是魔術。 僅僅是訂戶和轉換的組成。

接下來,我們看一下問題的根源:鎖定。 我不知道“鎖定環境”是什么意思,您似乎也沒有對此進行解釋或證明。

也許您想要做的就是擁有異步門的概念。 在這里,您只需設置一個標志來指定您所處的狀態。理想情況下,該標志也是可觀察的。 然后,可以鎖定該標志的狀態來組合其他傳入事件/命令/消息,而不是鎖定系統。

在此處觀看Matt討論異步門的視頻(轉到時間00:18:00): https ://yow.eventer.com/yow-2014-1222/event-driven-user-interfaces-by-matt-barrett -和李-坎貝爾1686

回到我認為是您的實際問題的是,我認為您已經創建了死鎖。

MyEvents.OfType<SomoneJoinsEvent>
.Subscribe(
    //I read somewhere that I shouldn't be making this async and using a selectmany with the async, but I am unsure.
    async myEvent => {
        await LockEnvironment()
    }
)

此代碼正在積極阻止生產者回叫。 Rx是一個自由線程模型,因此(在理論上)它實際上是非常簡單的。 調用OnNext時,它僅循環遍歷每個訂戶並調用其回調。 在這里您處於阻塞狀態,因此生產者不僅不能呼叫下一個訂閱者,而且不能處理下一條消息。

因此,您正在收聽MyEvents序列,就會收到一個SomoneJoinsEvent如第100條消息。 然后,您嘗試推入將在MyEvents產生事件的命令。 收到此事件后,您將繼續。 但是 ,您正在阻止該消息的接收。 因此,您陷入了僵局。

所以現在問題又回到了您身上,您希望這個LockEnvironment實際實現什么?

編輯:

查看您的代碼,它似乎過於復雜。 您似乎打了很多StringBuilder,添加,查詢了變異並替換了它的內容。

我認為您可以使用更通用的解決方案。

這是一個示例,說明如何處理流,該流被讀入緩沖區,然后投影/轉換為記錄。

var testScheduler = new TestScheduler();

//A series of bytes/chars to be treated as buffer read from a stream (10 at a time).
//  a \n\n represents a record delimiter.
var source = testScheduler.CreateColdObservable<char[]>(
    ReactiveTest.OnNext(0100, new char[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', '\n', '\n', 'h' }),
    ReactiveTest.OnNext(0200, new char[] { 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', '\n', '\n' }),
    ReactiveTest.OnNext(0300, new char[] { 'q', 'r', 's', '\n', '\n', 't', 'u', 'v', '\n', '\n' }),
    ReactiveTest.OnNext(0400, new char[] { 'w', 'x', '\n', 'y', 'z', '\n', '\n' })
);

var delimiter = '\n';
var observer = testScheduler.CreateObserver<string>();
var shared = source.SelectMany(buffer=>buffer).Publish().RefCount();
var subscription = shared
    //Where we see two '\n' values then emit the buffered values
    .Buffer(() => shared.Scan(Tuple.Create(' ',' '), (acc, cur)=>Tuple.Create(acc.Item2, cur)).Where(t => t.Item1 == delimiter && t.Item2==delimiter))
    //Remove trailing delimiters
    .Select(chunk =>
        {
            var len = chunk.Count;
            while(chunk[chunk.Count-1]==delimiter)
            {
                chunk.RemoveAt(chunk.Count-1);
            }
            return chunk;
        })
    //Filter out empty buffers
    .Where(chunk=>chunk.Any())
    //Translate the record to the desired output type
    .Select(chunk=>new string(chunk.ToArray()))
    .Subscribe(observer);

testScheduler.Start();
observer.Messages.AssertEqual(
    ReactiveTest.OnNext(0100, "abcdefg"),
    ReactiveTest.OnNext(0200, "hijklmnop"),
    ReactiveTest.OnNext(0300, "qrs"),
    ReactiveTest.OnNext(0300, "tuv"),
    ReactiveTest.OnNext(0400, "wx\nyz")
);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM