我正在学习Rx,并试图将以下问题转换为Rx管道。似乎应该有一个简单的Rx解决方案,但是我找不到它。这是一些简单的C#代码来演示该问题:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Item = System.Collections.Generic.KeyValuePair<int, string>;
namespace Sample
{
class Test
{
readonly object _sync = new object();
readonly List<Item> _workList = new List<Item>();
public void Update(IEnumerable<Item> items)
{
lock(_sync)
{
foreach (var item in items)
{
bool found = false;
for (int i = 0; i < _workList.Count; ++i)
{
if (_workList[i].Key == item.Key)
{
_workList[i] = item;
found = true;
break;
}
}
if (!found)
{
_workList.Add(item);
}
}
}
}
public void Run()
{
void ThreadMethod(object _)
{
while (true)
{
Item? item = null;
lock (_sync)
{
if (_workList.Any())
{
item = _workList[0];
_workList.RemoveAt(0);
}
}
if (item.HasValue)
{
var str = $"{item.Value.Key} : {item.Value.Value}";
Console.WriteLine($"Start {str}");
Thread.Sleep(5000); // simluate work
Console.WriteLine($"End {str}");
}
}
}
var thread = new Thread(ThreadMethod);
thread.Start();
}
}
}
“更新”事件由键/值对的列表组成。使用以下规则将更新与现有列表合并。 不能保证每个已知密钥都将出现在每次更新中
一个单独的线程一次处理列表一个项目。此处理需要一些时间(由Thread.sleep模拟)。处理项目时,它们将从列表的开头删除。
如您所见,在处理单个项目期间,待办事项中的项目可能会发生适当的突变。关键是,每个密钥仅会处理收到的最新值,但积压中的密钥顺序无法更改(除非处理密钥时将其从列表中删除。如果将密钥重新引入列表中,则为添加到最后)。
我对Rx的最新尝试是将更新输入到“扫描”功能中,该功能将以前未知的键转换为主题,然后在组合所有最新值之前将每个键的新值输入其对应的主题,但效果不佳。
请不要讨论非Rx解决方案。上面的简单代码可以完成这项工作,但我想了解是否有Rx解决方案。
我正在使用C#(System.Reactive)工作,但我会很乐意接受Rx其他方言中的解决方案。
答案 0 :(得分:1)
要实现目标,需要两种机制。第一个是可以为您提供已发射物品的最新值的地图。第二个是flatMap()
运算符。
Map<String, String> currentSourceValue = new HashMap<>();
我使用String
作为数据类型以及keyOf()
和valOf()
方法。
此方法将使用最新值更新地图。如果已经有当前值,请替换它并返回empty()
可观察值。
synchronized Observable<String> setLatestValue( String s ) {
String r = currentSourceValue.put( keyOf( s ), valOf( s ) );
return r == null ? Observable.just( s ) : Observable.empty();
}
如果可以发射该值,则该方法将从地图中提取该值。
synchronized Observable<String> getLatestValue( String s ) {
String r = currentSourceValue.remove( keyOf( s ) );
return r == null ? Observable.empty() : Observable.just( r );
}
这将允许发出最新值
source
.flatMap( s -> setLatestValue( s ) )
.observeOn( processingScheduler )
.flatMap( s -> getLatestValue( s ), 1 )
.subscribe( s -> process( s ) );
第一个flatMap()
运算符更新传入流的最新值。如果队列中已经有该键的项目,则返回empty()
观察值,以便在下游链中不占用任何空间。
第二个flatMap()
运算符在处理线程上工作。 flatMap()
的第二个参数表示应一次处理一项,而不能并行处理。如果地图中存在该值,它将发出一个值;如果地图中不存在该值,则将不显示任何值,并清除地图项。从理论上讲,第二个flatMap()
可以发出一个值,但是当观察者链从一个线程跳到另一个线程时,存在一些不确定性。
synchronized
关键字表示地图上的操作是原子操作,并防止将值从下游地图中删除,就像将其添加到上游地图中一样。
此解决方案的工作方式类似于groupBy()
运算符,但是可以处理您只想处理给定键的最新值的情况。
答案 1 :(得分:1)
这将起作用,尽管我不是它的最大支持者。
我认为这是生产者/消费者的情况:一个线程创建工作,另一个线程创建工作。 producer
主题代表添加工作的线程。其他所有事物都代表事物的消费者方面。如果您要class
上一堂课,producer
会上一堂课,其他都上一堂课。
completedKeys
保留已完成的键,因此该键的状态会弹出:具有该键的新项目将移至该行的末尾。 readyGate
代表何时有新的消费者来处理下一件事。将其与最新的工作内容相结合是棘手的部分。 WithLatestFrom
效果很好,直到您得到一个空列表。 .Where().FirstAsync()
很好地完成了等待的过程。
所有这些操作的关键是GroupByUntil
:对事物进行分组,它们自然会落入首先添加密钥的顺序,这就是您想要的。 Until
子句意味着我们可以关闭可观察对象,这将使带有旧键的新项目成为行尾。 DynamicCombinedLatest
将所有这些可观察对象转换为一个列表,这实际上就是您的状态。
无论如何,这是您要去的
var producer = new Subject<Item>();
var readyGate = new Subject<Unit>();
var completedKeys = new Subject<int>();
var Process = new Action<Item>(kvp =>
{
var str = $"{kvp.Key} : {kvp.Value}";
Console.WriteLine($"Start {str}");
Thread.Sleep(500); // simluate work
Console.WriteLine($"End {str}");
});
var groups = producer
.GroupByUntil(kvp => kvp.Key, kvp => kvp, go => completedKeys.Where(k => k == go.Key))
.DynamicCombineLatest();
var q = groups.Publish(_groups => readyGate
.ObserveOn(NewThreadScheduler.Default)
.WithLatestFrom(groups, (_, l) => l)
.SelectMany(l => l.Count == 0
? _groups.Where(g => g.Count > 0).FirstAsync()
: Observable.Return(l)
)
)
.Subscribe(l =>
{
var kvp = l[0];
completedKeys.OnNext(kvp.Key);
Process(kvp);
readyGate.OnNext(Unit.Default);
});
//Runner code:
producer.OnNext(new Item(1, "1-a"));
producer.OnNext(new Item(1, "1-b"));
producer.OnNext(new Item(2, "2-a"));
producer.OnNext(new Item(2, "2-b"));
readyGate.OnNext(Unit.Default);
await Task.Delay(TimeSpan.FromMilliseconds(100)); //to test if 1 gets done again and goes to the back of the line.
producer.OnNext(new Item(1, "1-c"));
DynamicCombinedLatest
是这个(使用nuget包System.Collections.Immutable
):
public static IObservable<List<T>> DynamicCombineLatest<T>(this IObservable<IObservable<T>> source)
{
return source
.SelectMany((o, i) => o.Materialize().Select(notification => (observableIndex: i, notification: notification)))
.Scan((exception: (Exception)null, dict: ImmutableDictionary<int, T>.Empty), (state, t) => t.notification.Kind == NotificationKind.OnNext
? ((Exception)null, state.dict.SetItem(t.observableIndex, t.notification.Value))
: t.notification.Kind == NotificationKind.OnCompleted
? ((Exception)null, state.dict.Remove(t.observableIndex))
: (t.notification.Exception, state.dict)
)
.Select(t => t.exception == null
? Notification.CreateOnNext(t.dict)
: Notification.CreateOnError<ImmutableDictionary<int, T>>(t.exception)
)
.Dematerialize()
.Select(dict => dict.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToList());
}