簡體   English   中英

如何同步Observable和卸載UI線程

[英]How to synchronize Observables and offload UI Thread

我有兩個簡單的觀察處理程序,訂閱相同的源。 但是,兩個訂閱都在不同類型上運行。 我希望它們保持可觀察源(Subject())的順序。 我嘗試使用Synchronize()擴展,但我找不到按預期方式工作的方法。

這是我的單元測試代碼:

[Test]
public void TestObserveOn()
{
    Console.WriteLine("Starting on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
    var source = new Subject<object>();
    var are = new AutoResetEvent(false);

    using (source.ObserveOn(TaskPoolScheduler.Default).Synchronize(source).OfType<int>().Subscribe(
        o =>
            {
                Console.WriteLine("Received {1} on threadId:{0}", Thread.CurrentThread.ManagedThreadId, o);
                int sleep = 3000 / o; // just to simulate longer processing
                Thread.Sleep(sleep);
                Console.WriteLine("Handled  {1} on threadId: {0}", Thread.CurrentThread.ManagedThreadId, o);
            },
        () =>
            {
                Console.WriteLine("OnCompleted on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                are.Set();
            }))
    using (source.ObserveOn(TaskPoolScheduler.Default).Synchronize(source).OfType<double>().Subscribe(
                    o =>
                    {
                        Console.WriteLine("Received {1} on threadId:{0}", Thread.CurrentThread.ManagedThreadId, o);
                        Console.WriteLine("Handled  {1} on threadId: {0}", Thread.CurrentThread.ManagedThreadId, o);
                    },
                    () =>
                    {
                        Console.WriteLine("OnCompleted on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                    }))
    {
        Console.WriteLine("Subscribed on threadId:{0}", Thread.CurrentThread.ManagedThreadId);

        source.OnNext(1);
        source.OnNext(1.1);
        source.OnNext(2);
        source.OnNext(2.1);
        source.OnNext(3);
        source.OnNext(3.1);
        source.OnCompleted();

        Console.WriteLine("Finished on threadId:{0}", Thread.CurrentThread.ManagedThreadId);

        are.WaitOne();
    }
}

得到的測試代碼輸出:

Starting on threadId:10
Subscribed on threadId:10
Finished on threadId:10
Received 1 on threadId:11
Handled  1 on threadId: 11
Received 1,1 on threadId:12
Handled  1,1 on threadId: 12
Received 2,1 on threadId:12
Handled  2,1 on threadId: 12
Received 3,1 on threadId:12
Handled  3,1 on threadId: 12
Received 2 on threadId:11
Handled  2 on threadId: 11
OnCompleted on threadId:12
Received 3 on threadId:11
Handled  3 on threadId: 11
OnCompleted on threadId:11

如您所見,訂單與輸入不同。 我想同步兩個訂閱,以便順序與輸入相同。

輸出應該是

Starting on threadId:10
Subscribed on threadId:10
Finished on threadId:10
Received 1 on threadId:11
Handled  1 on threadId: 11
Received 1,1 on threadId:12
Handled  1,1 on threadId: 12
Received 2 on threadId:11
Handled  2 on threadId: 11
Received 2,1 on threadId:12
Handled  2,1 on threadId: 12
Received 3 on threadId:11
Handled  3 on threadId: 11
Received 3,1 on threadId:12
Handled  3,1 on threadId: 12
OnCompleted on threadId:11
OnCompleted on threadId:12

(完成順序對我來說並不重要)。

編輯:

我也嘗試過以下方法:

[Test]
public void TestObserveOn()
{
    Console.WriteLine("Starting on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
    var source = new Subject<object>();
    var taskSchedulerPair = new ConcurrentExclusiveSchedulerPair();
    var exclusiveTaskFactory = new TaskFactory(taskSchedulerPair.ExclusiveScheduler);
    var exclusiveScheduler = new TaskPoolScheduler(exclusiveTaskFactory);
    var are = new AutoResetEvent(false);

    using (source.ObserveOn(exclusiveScheduler).OfType<int>().Subscribe(
        o =>
            {
                Console.WriteLine("Received {1} on threadId:{0}", Thread.CurrentThread.ManagedThreadId, o);
                int sleep = 3000 / o;
                Thread.Sleep(sleep);
                Console.WriteLine("Handled  {1} on threadId: {0}", Thread.CurrentThread.ManagedThreadId, o);
            },
        () =>
            {
                Console.WriteLine("OnCompleted on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                are.Set();
            }))
    using (source.ObserveOn(exclusiveScheduler).OfType<double>().Subscribe(
                    o =>
                    {
                        Console.WriteLine("Received {1} on threadId:{0}", Thread.CurrentThread.ManagedThreadId, o);
                        Console.WriteLine("Handled  {1} on threadId: {0}", Thread.CurrentThread.ManagedThreadId, o);
                    },
                    () =>
                    {
                        Console.WriteLine("OnCompleted on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                        are.Set();
                    }))
    {
        Console.WriteLine("Subscribed on threadId:{0}", Thread.CurrentThread.ManagedThreadId);

        source.OnNext(1);
        source.OnNext(1.1);
        source.OnNext(2);
        source.OnNext(2.1);
        source.OnNext(3);
        source.OnNext(3.1);
        source.OnCompleted();

        Console.WriteLine("Finished on threadId:{0}", Thread.CurrentThread.ManagedThreadId);

        are.WaitOne();
        are.WaitOne();
    }
}

但輸出仍然是錯誤的:

Starting on threadId:10
Subscribed on threadId:10
Finished on threadId:10
Received 1 on threadId:4
Handled  1 on threadId: 4
Received 2 on threadId:4
Handled  2 on threadId: 4
Received 3 on threadId:4
Handled  3 on threadId: 4
OnCompleted on threadId:4
Received 1,1 on threadId:4
Handled  1,1 on threadId: 4
Received 2,1 on threadId:4
Handled  2,1 on threadId: 4
Received 3,1 on threadId:4
Handled  3,1 on threadId: 4
OnCompleted on threadId:4

...因為你可以看到它不是OnNext()調用的順序。

當使用具有類似create的含義的類型然后執行多個更新時,這一點尤其重要...如果更新在創建之前怎么辦? 如果訂單無法保證您可能遇到問題或需要將“未來”事件排隊,直到其前任與要更改的狀態同步。 你需要像增加版本/訂單號這樣的東西來使用它作為訂購標准並找到“漏洞”並對后繼者進行排隊,直到它們再次排成一行。

第二次編輯 ......更接近我的問題並退出測試案例理論:

我想要一個易於使用RX過濾功能的簡單界面:

public interface ICommandBus // or to say Aggregator pattern
{
    void Send<T>(T command) where T : ICommand; // might be something like Task<Result> Send<T>(T command) to know the system has accepted the command

    IObservable<T> Stream<T>() where T : ICommand;
}

public class CommandBus : ICommandBus, IDisposable
{
    private static readonly ILog Log = LogManager.GetLogger<CommandBus>();

    private readonly HashSet<Type> registrations = new HashSet<Type>();

    private readonly Subject<ICommand> stream = new Subject<ICommand>();

    private readonly IObservable<ICommand> notifications;

    private bool disposed;

    public CommandBus()
    {
        // hmm, this is a problem!? how to sync?
        this.notifications = this.stream.SubscribeOn(TaskPoolScheduler.Default);

    }

    public IObservable<T> Stream<T>() where T : ICommand
    {
        var observable = this.notifications.OfType<T>();
        return new ExclusiveObservableWrapper<T>(
            observable,
            t => this.registrations.Add(t),
            t => this.registrations.Remove(t));
    }

    public void Send<T>(T command) where T : ICommand
    {
        if (command == null)
        {
            throw new ArgumentNullException("command");
        }

        if (!this.registrations.Contains(typeof(T)))
        {
            throw new NoCommandHandlerSubscribedException();
        }

        Log.Debug(logm => logm("Sending command of type {0}.", typeof(T).Name));

        this.stream.OnNext(command);
    }

    //public async Task SendAsync<T>(T command) where T : ICommand
    //{
    //    if (command == null)
    //    {
    //        throw new ArgumentNullException("command");
    //    }

    //    if (!this.registrations.Contains(typeof(T)))
    //    {
    //        throw new NoCommandHandlerSubscribedException();
    //    }

    //    Log.Debug(logm => logm("Sending command of type {0}.", typeof(T)));

    //    this.stream.OnNext(command);

    //    await this.stream.Where(item => ReferenceEquals(item, command));
    //}

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                this.stream.Dispose();
            }
        }

        this.disposed = true;
    }

    [Serializable]
    public class CommandAlreadySubscribedException : Exception
    {
        internal CommandAlreadySubscribedException(Type type)
            : base(string.Format("Tried to subscribe handler for command of type {0} but there was already a subscribtion. More than one handler at time is not allowed.", type))
        {
        }

        protected CommandAlreadySubscribedException(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }
    }

    [Serializable]
    public class NoCommandHandlerSubscribedException : Exception
    {
        public NoCommandHandlerSubscribedException()
        {
        }

        public NoCommandHandlerSubscribedException(string message)
            : base(message)
        {
        }

        public NoCommandHandlerSubscribedException(string message, Exception innerException)
            : base(message, innerException)
        {
        }

        protected NoCommandHandlerSubscribedException(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }
    }

    private class ExclusiveObservableWrapper<T> : IObservable<T> where T : ICommand
    {
        private readonly IObservable<T> observable;

        private readonly Func<Type, bool> register;

        private readonly Action<Type> unregister;

        internal ExclusiveObservableWrapper(IObservable<T> observable, Func<Type, bool> register, Action<Type> unregister)
        {
            this.observable = observable;
            this.register = register;
            this.unregister = unregister;
        }

        public IDisposable Subscribe(IObserver<T> observer)
        {
            var subscription = this.observable.Subscribe(observer);
            var type = typeof(T);

            if (!this.register(type))
            {
                observer.OnError(new CommandAlreadySubscribedException(type));
            }

            return Disposable.Create(
                () =>
                {
                    subscription.Dispose();
                    this.unregister(type);
                });
        }
    }
}

如果我不能保證命令在順序中(如給定的那樣)那么它們(可能)沒有任何意義。 (創建前更新)

ICommandBus用於想要為命令調用相應處理程序的UI / Presentation層(無需知道處理程序)。

我想簡單地將鏈卸載到一個單獨的線程。

命令 - >總線 - >命令處理程序 - >域模型 - >事件 - >事件處理程序 - >讀取模型

這需要按順序保持命令。

我認為RX能夠通過一些“魔術線”來做到這一點。 但據我所知,現在我必須用自己的線程處理再做一次。 :-(

您似乎對.Synchronize()作用有錯誤的理解。 它的唯一目的是獲取一個生成重疊或OnCompleted消息的可觀察對象(即在OnNext或多個OnError之前的OnCompleted )並確保它們遵循OnNext*(OnError|OnCompleted)行為契約。 這是為了使流氓可觀察的游戲變得更好。

現在,因為我們可以忽略它,因為您的示例輸入是一個表現良好的可觀察對象,那么您可以通過調用.ObserveOn(TaskPoolScheduler.Default)來看到您正在制作可觀察的跳轉線程 - 這很容易導致在不同的情況下消耗可觀察量利率 - 這就是這里發生的事情。

您已經訂閱了兩次source ,因此您無法根據引入並發性的方式停止您所看到的行為。

鑒於你之前的問題( 如何等待完成的IObserver調用,包括觀察用戶調用? ),你似乎一心想用Rx來增加並發性,但是然后強迫它以某種方式刪除它。 你真的應該放開釋放Rx的心態來做它的事情而不是扼殺它。

編輯@Beachwalker:

在對這個答案的評論中, Enigmativity給出了我的問題的正確答案

我必須使用EventLoopScheduler 所以我接受這個作為正確答案。

為了完整。 這是有效的代碼:

[Test]
public void TestObserveOn()
{
    Console.WriteLine("Starting on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
    var source = new Subject<object>();
    var exclusiveScheduler = new EventLoopScheduler();
    var are = new AutoResetEvent(false);

    using (source.ObserveOn(exclusiveScheduler).OfType<int>().Subscribe(
        o =>
            {
                Console.WriteLine("Received {1} on threadId:{0}", Thread.CurrentThread.ManagedThreadId, o);
                int sleep = 3000 / o;
                Thread.Sleep(sleep);
                Console.WriteLine("Handled  {1} on threadId: {0}", Thread.CurrentThread.ManagedThreadId, o);
            },
        () =>
            {
                Console.WriteLine("OnCompleted on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                are.Set();
            }))
    using (source.ObserveOn(exclusiveScheduler).OfType<double>().Subscribe(
                    o =>
                        {
                            Console.WriteLine(
                                "Received {1} on threadId:{0}",
                                Thread.CurrentThread.ManagedThreadId,
                                o);
                            Console.WriteLine(
                                "Handled  {1} on threadId: {0}",
                                Thread.CurrentThread.ManagedThreadId,
                                o);
                        },
                    () =>
                    {
                        Console.WriteLine("OnCompleted on threadId:{0}", Thread.CurrentThread.ManagedThreadId);
                        are.Set();
                    }))
    {
        Console.WriteLine("Subscribed on threadId:{0}", Thread.CurrentThread.ManagedThreadId);

        source.OnNext(1);
        source.OnNext(1.1);
        source.OnNext(2);
        source.OnNext(2.1);
        source.OnNext(3);
        source.OnNext(3.1);
        source.OnCompleted();

        Console.WriteLine("Finished on threadId:{0}", Thread.CurrentThread.ManagedThreadId);

        are.WaitOne();
        are.WaitOne();
    }
}

您可以根據源下一個成員的類型過濾為source創建兩個不同的任務。

您可以從線程ID中看到並行處理消息。 這為您提供了更好的性能,但不保證您處理source的順序。 因此,如果您需要對象的順序句柄,則必須重寫代碼以進行順序執行(這會降低性能)或使用其他調度程序進行測試。

目前您正在使用TaskPoolScheduler.DefaultTaskPoolScheduler.Default使用默認線程池。 所以你可以提供一個新的調度程序 您可以自己提供一個新的實現,但我認為最簡單的方法是使用ConcurrentExclusiveSchedulerPair類來提供獨占調度程序,以按照提供值的相同順序處理source

你的代碼可能是這樣的:

var taskSchedulerPair = new ConcurrentExclusiveSchedulerPair();
var exclusiveTaskFactory = new TaskFactory(taskSchedulerPair.ExclusiveScheduler );
var exclusiveScheduler = new TaskPoolScheduler(exclusiveTaskFactory);
using (source.ObserveOn(exclusiveScheduler)...

更新

正如在其他人的帖子中所說,處理類似事件的正確方法是EventLoopScheduler類。

暫無
暫無

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

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