关于C#:如何同步Observable和卸载UI线程

How to synchronize Observables and offload UI Thread

我有两个简单的观察处理程序,它们订阅了相同的源。但是,两个订阅都在不同类型上运行。我希望他们保持可观察源(Subject())的顺序。我使用Synchronize()扩展进行了尝试,但没有找到一种按预期方式进行这项工作的方法。

这是我的单元测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[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();
    }
}

测试代码的结果输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

如您所见,顺序与输入不同。我想同步两个订阅,以便顺序与输入相同。

输出应为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

(完成顺序对我来说并不重要)。

编辑:

我还尝试了以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[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();
    }
}

但是输出仍然是错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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过滤功能的简单界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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()的功能有错误的了解。它的唯一目的是采用可观察到的,产生重叠或不适当的消息(即在OnNext或多个OnError之前的OnCompleted)并确保它们遵循OnNext*(OnError|OnCompleted)行为协定。这是关于使流氓可观察的游戏变得更好。

现在,由于我们可以忽略这一点,因为示例输入的行为是可观察的,所以您可以看到,通过调用.ObserveOn(TaskPoolScheduler.Default),您正在创建可观察的跳转线程-这很容易导致以不同的速率消耗可观察的对象-这就是这里发生的事情。

您已经预订了source两次,因此,按照引入并发的方式,您就无法停止所看到的行为。

考虑到您先前的问题(如何等待包括观察订户调用的IObserver调用?),您似乎对使用Rx添加并发一事无成,但是以某种方式强制将其删除。您确实应该以释放Rx的心态来做它的事情而不是轻视它。

@Beachwalker编辑:

谜题在对这个答案的评论中为我的问题提供了正确的答案。

我必须使用EventLoopScheduler。因此,我认为这是正确的答案。

出于完整性考虑。这是有效的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[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.Default,它仅使用默认线程池。因此,您可以提供一个新的调度程序。您可以自己为它提供一个新的实现,但是我认为最简单的方法是使用ConcurrentExclusiveSchedulerPair类提供排它的调度程序,以按提供值的顺序处理source

您的代码可能是这样的:

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

更新:

就像在其他人的帖子中所说的那样,处理此类事件的正确方法是EventLoopScheduler类。