C# 如何使用Task并行库中的Task.Run()启动冷可观测?

C# 如何使用Task并行库中的Task.Run()启动冷可观测?,c#,task-parallel-library,system.reactive,C#,Task Parallel Library,System.reactive,在这种情况下,我们需要在C#应用程序中启动后台“轮询”操作,该应用程序使用反应式扩展定期返回值。我们希望实施的流程如下: 调用方调用类似于Poll()的方法,该方法返回一个IObservable 调用者订阅所述可观察对象,并启动后台线程/任务,该线程/任务与硬件交互以在某个时间间隔检索值 调用方完成后,将处理订阅,并自动停止后台线程/任务 尝试#1 为了证明这一点,我编写了以下控制台应用程序,但这并不是我所期望的: public class OutputParameters { publ

在这种情况下,我们需要在C#应用程序中启动后台“轮询”操作,该应用程序使用反应式扩展定期返回值。我们希望实施的流程如下:

  • 调用方调用类似于
    Poll()
    的方法,该方法返回一个
    IObservable
  • 调用者订阅所述可观察对象,并启动后台线程/任务,该线程/任务与硬件交互以在某个时间间隔检索值
  • 调用方完成后,将处理订阅,并自动停止后台线程/任务
  • 尝试#1 为了证明这一点,我编写了以下控制台应用程序,但这并不是我所期望的:

    public class OutputParameters
    {
        public Guid Id { get; set; }
        public int Value { get; set; }
    }
    
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Requesting the polling operation");
            var worker1 = Poll();
    
            Console.WriteLine("Subscribing to start the polling operation");
    
            var sub1 = worker1.Subscribe(
                value => { Console.WriteLine($"Thread {value.Id} emitted {value.Value}"); },
                ex => { Console.WriteLine($"Thread threw an exception: {ex.Message}"); },
                () => { Console.WriteLine("Thread has completed"); });
    
    
            Thread.Sleep(5000);
    
            sub1.Dispose();
    
            Console.ReadLine();
        }
    
    
        private static IObservable<OutputParameters> Poll()
        {
            return Observable.DeferAsync(Worker);
        }
    
    
        private static Task<IObservable<OutputParameters>> Worker(CancellationToken token)
        {
            var subject = new Subject<OutputParameters>();
    
            Task.Run(async () =>
            {
                var id = Guid.NewGuid();
                const int steps = 10;
    
                try
                {
                    for (var i = 1; i <= steps || token.IsCancellationRequested; i++)
                    {
                        Console.WriteLine($"[IN THREAD] Thread {id}: Step {i} of {steps}");
                        subject.OnNext(new OutputParameters { Id = id, Value = i });
    
                        // This will actually throw an exception if it's the active call when
                        //  the token is cancelled.
                        //
                        await Task.Delay(1000, token);
                    }
                }
                catch (TaskCanceledException ex)
                {
                    // Interestingly, if this is triggered because the caller unsibscribed then
                    //  this is unneeded...the caller isn't listening for this error anymore
                    //
                    subject.OnError(ex);
                }
    
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine($"[IN THREAD] Thread {id} was cancelled");
                }
                else
                {
                    Console.WriteLine($"[IN THREAD] Thread {id} exiting normally");
                    subject.OnCompleted();
                }
            }, token);
    
            return Task.FromResult(subject.AsObservable());
        }
    }
    
    尝试#2 然后,我尝试对
    Worker
    方法进行一个小更改,使其异步,并等待
    任务。按如下方式运行
    调用:

        private static async Task<IObservable<OutputParameters>> Worker(CancellationToken token)
        {
            var subject = new Subject<OutputParameters>();
    
            await Task.Run(async () =>
            {
                ...what happens in here is unchanged...
            }, token);
    
            return subject.AsObservable();
        }
    
    我的问题 所以很明显,我不完全理解这里发生了什么,或者在这种情况下,使用
    DeferAsync
    是创建可观察对象的正确方法


    有没有合适的方法来实施这种方法?

    如果只有RX的解决方案就足够了,就可以了。如果你问我的话会干净很多

    static IObservable<OutputParameters> Poll()
    {
        const int steps = 10;
        return Observable.Defer<Guid>(() => Observable.Return(Guid.NewGuid()))
            .SelectMany(id => 
                Observable.Generate(1, i => i <= steps, i => i + 1, i => i, _ => TimeSpan.FromMilliseconds(1000))
                    .ObserveOn(new EventLoopScheduler())
                    .Do(i => Console.WriteLine($"[IN THREAD] Thread {id}: Step {i} of {steps}"))
                    .Select(i => new OutputParameters { Id = id, Value = i })
            );
    }
    

    如果只有RX的解决方案就足够了,这就可以了。如果你问我的话会干净很多

    static IObservable<OutputParameters> Poll()
    {
        const int steps = 10;
        return Observable.Defer<Guid>(() => Observable.Return(Guid.NewGuid()))
            .SelectMany(id => 
                Observable.Generate(1, i => i <= steps, i => i + 1, i => i, _ => TimeSpan.FromMilliseconds(1000))
                    .ObserveOn(new EventLoopScheduler())
                    .Do(i => Console.WriteLine($"[IN THREAD] Thread {id}: Step {i} of {steps}"))
                    .Select(i => new OutputParameters { Id = id, Value = i })
            );
    }
    


    Subject
    必须在有人订阅后立即启动异步轮询工作。研究如何跟踪c#中的订户。您可以使用事件,也可以使用观察者模式,我只会使用事件。然后,一旦所有订户都取消订阅,发送取消令牌以停止轮询。大部分代码应该在
    Subject
    中,因此它是封装的,不在
    程序
    类中。此外,首先使用windows窗体应用程序尝试它,一旦它正常工作,在控制台应用程序中搜索
    任务
    ,并研究它,因为控制台有点棘手。出于好奇,投反对票有什么原因吗?如果你问我的话,我自己也在想。我认为你的问题措辞很好,也很清楚。@SamStorie-不要像你的问题中那样使用
    主题。或者用一个可观察的
    来包装它。延迟
    或者尝试完全避免它。将其作为变量放在方法中会导致它被捕获到您的observable中,创建一次可观察的运行-发送的任何错误或完成信号都会一直杀死您的observable。
    主题必须在有人订阅它后立即启动异步轮询工作。研究如何跟踪c#中的订户。您可以使用事件,也可以使用观察者模式,我只会使用事件。然后,一旦所有订户都取消订阅,发送取消令牌以停止轮询。大部分代码应该在
    Subject
    中,因此它是封装的,不在
    程序
    类中。此外,首先使用windows窗体应用程序尝试它,一旦它正常工作,在控制台应用程序中搜索
    任务
    ,并研究它,因为控制台有点棘手。出于好奇,投反对票有什么原因吗?如果你问我的话,我自己也在想。我认为你的问题措辞很好,也很清楚。@SamStorie-不要像你的问题中那样使用
    主题。或者用一个可观察的
    来包装它。延迟
    或者尝试完全避免它。将其作为变量放在方法中会导致它被捕获到您的observable中,从而创建一次可观察的运行-并且发送的任何错误或完成信号都会一直杀死您的observable。您应该将observable包装在一个
    observable中。延迟
    以启用捕获
    id
    ,否则将有多个订阅者获取相同的
    id
    @SamStorie-
    EventLoopScheduler
    确保每个可观察对象和在该可观察对象上调度的每个操作都使用一个且仅使用一个线程,当然,还确保没有执行并发访问-只有一个线程,因此两件事情不能同时运行。这是允许多线程代码在非线程安全代码上运行的一种非常简洁的方法。@Enigmativity,谢谢。更新的答案包括在
    延迟
    中初始化Guid,以及
    事件循环调度程序
    优先于
    新线程调度程序
    @Shlomo-非常抱歉编辑您的答案。我想我应该把你答案的变体作为对你答案的编辑,而不是发布我自己的答案,让一切变得混乱。请随意使用或删除它。我发布它的原因是
    Return
    /
    SelectMany
    /
    ObserveOn
    组合将首先使用线程池中的线程,然后再编组到
    EventLoopScheduler
    。我写这篇文章的方式将避免所有这些,只使用一个线程。再次感谢你们两位的帮助。我们已经将其应用到实际的应用程序中,这种方法非常有效。我做的一个小改动是让我通过调度程序,这样我就可以使用TestScheduler更轻松地进行测试……这些答案也帮助我探索了一些东西。荣誉您应该将可观察对象包装在一个
    可观察对象中。延迟
    以启用捕获
    id
    ,否则多个订阅者将获得相同的
    id
    @SamStorie-
    EventLoopScheduler
    确保每个可观察对象和在此可观察对象上调度的每个操作都使用一个且仅使用一个线程,并确保:,当然,没有执行并发访问——只有一个线程,所以不能同时运行两个线程。这是允许多线程代码在非线程安全代码上运行的一种非常简洁的方法。@Enigmativity,谢谢。更新的答案包括在
    延迟
    中初始化Guid,以及
    事件循环调度程序
    优先于
    新线程调度程序
    @Shlomo-非常抱歉编辑您的答案。我想我应该把你的answ的变体
    static IObservable<OutputParameters> Poll()
    {
        const int steps = 10;
        return Observable.Defer<Guid>(() => Observable.Return(Guid.NewGuid()))
            .SelectMany(id => 
                Observable.Generate(1, i => i <= steps, i => i + 1, i => i, _ => TimeSpan.FromMilliseconds(1000))
                    .ObserveOn(new EventLoopScheduler())
                    .Do(i => Console.WriteLine($"[IN THREAD] Thread {id}: Step {i} of {steps}"))
                    .Select(i => new OutputParameters { Id = id, Value = i })
            );
    }
    
    static IObservable<OutputParameters> Poll()
    {
        const int steps = 10;
        return Observable.Defer<OutputParameters>(() =>
        {
            var id = Guid.NewGuid();
            return Observable.Generate(1, i => i <= steps, i => i + 1, i => i,
                    _ => TimeSpan.FromMilliseconds(1000), new EventLoopScheduler())
                .Do(i => Console.WriteLine($"[IN THREAD] Thread {id}: Step {i} of {steps}"))
                .Select(i => new OutputParameters { Id = id, Value = i });
        });
    }