如何使用Observable FromEventPattern异步例程避免死锁?

时间:2019-01-03 20:27:10

标签: c# observable system.reactive

我正在使用Observable /反应性扩展来消除某些事件的反弹,例如单击按钮或在文本框中输入文本。但是,在关闭或关闭的情况下,我需要等待所有未决事件,以便保存操作可以完成,等等。

以下代码将死锁。

Button b1 = new Button();

var scheduler = new EventLoopScheduler(ts => new Thread(ts)
{
    IsBackground = false
});

var awaiter = Observable.FromEventPattern(h => b1.Click += h, h => b1.Click -= h, scheduler)                
     .Throttle(TimeSpan.FromMilliseconds(5000), scheduler)
     .FirstOrDefaultAsync();

someTaskList.add(awaiter.ToTask());

awaiter.Subscribe
(
    x =>
    {
        //do some work in response to click event
    }
);

//program continues...

然后,在应用程序的其他地方

private async Task CloseApplicationSafely()
{
    await AwaitPendingEvents();
}

private async Task AwaitPendingEvents()
{
    if(someTaskList.Count > 0)
    {
        await Task.WhenAll(someTaskList);
    }
}

然后该程序将死锁,如果从未发生按钮单击,则将永远等待。这是另一个示例,但是带有文本框。

var completedTask = Observable.FromEventPattern(h => t1.TextChanged += h, h => t1.TextChanged -= h, scheduler)
    .Select(x => ((TextBox)x.Sender).Text)
    .DistinctUntilChanged()
    .Throttle(TimeSpan.FromMilliseconds(5000), scheduler)
    .ForEachAsync(txt =>
    {
        //do some work, save the text
    });

someTaskList.Add(completedTask);

在这种情况下,是否更改文本都没有关系。如果等待,变量completedTask将永远死锁。 ForEachAsync()返回一个似乎从未被激活的任务。

我在做什么错?希望我的预期功能清楚。我正在取消事件。但是,我需要等待正在进行反跳过程中的任何未决事件,以确保它们完成。并且,如果没有任何待处理的事件,则无需等待即可继续。谢谢。

1 个答案:

答案 0 :(得分:0)

@Servy和@Enigmativity的评论帮助我确定了这一点。对于那些感兴趣的人,这是我想出的解决方案。关于我的方法的任何建议都会让我知道。

我创建了一个名为WaitableEventHelper的静态帮助器类,其中包括以下功能。

public static Task WaitableDebouncer(
    this Control c, 
    Action<EventHandler> addHandler, 
    Action<EventHandler> removeHandler, 
    IScheduler scheduler,
    CancellationToken cancelToken,
    TimeSpan limit,
    Func<Task> func)
{
    var mycts = new CancellationTokenSource();

    bool activated = false;
    bool active = false;

    Func<Task> pending = null;

    var awaiter = Observable.FromEventPattern(addHandler, removeHandler, scheduler)
        .TakeUntil(x => { return cancelToken.IsCancellationRequested; })
        .Do((x) => { activated = true; })
        .Do((x) =>
        {
            //sets pending task to last in sequence
            pending = func;
        })
        .Throttle(limit, scheduler)
        .Do((x) => { active = true; })    //done with throttle
        .ForEachAsync(async (x) =>
        {
            //get func
            var f = pending;

            //remove from list
            pending = null;

            //execute it
            await f();

            //have we been cancelled?
            if (cancelToken.IsCancellationRequested)
            {
                mycts.Cancel();
            }

            //not active
            active = false;

        }, mycts.Token);

    //if cancelled 
    cancelToken.Register(() => 
    {
        //never activated, force cancel
        if (!activated)
        {
            mycts.Cancel();
        }

        //activated in the past but not currently active
        if (activated && !active)
        {
            mycts.Cancel();
        }
    });

    //return new awaiter based on conditions
    return Task.Run(async () =>
    {
        try
        {
            //until awaiter finishes or is cancelled, this will block
            await awaiter;
        }
        catch (Exception)
        {
            //cancelled, don't care
        }

        //if pending isn't null, that means we terminated before ForEachAsync reached it
        //execute it
        if (pending != null)
        {
            await pending();
        }
    });
}

然后我像这样使用它。这是一个单击按钮的示例,b1是System.Windows.Forms.Button对象。这可以是任何东西。对于我的测试应用程序,我正在更改主窗体上某些面板的颜色。根据OP中的先前代码,任务只是Task类型的列表。

var awaiter1 = b1.WaitableDebouncer(h => b1.Click += h, h => b1.Click -= h, 
    scheduler, 
    canceller.Token, 
    TimeSpan.FromMilliseconds(5000), 
    async () =>
    {
        Invoke(new Action(() =>
        {
            if (p1.BackColor == Color.Red)
            {
                p1.BackColor = Color.Orange;
            }
            else if (p1.BackColor == Color.Orange)
            {
                p1.BackColor = Color.Yellow;
            }
            else if (p1.BackColor == Color.Yellow)
            {
                p1.BackColor = Color.HotPink;
            }
            else
            {
                p1.BackColor = Color.Red;
            }
        }));
    });

tasks.Add(awaiter1);

另一个用于文本框中的TextChanged的方法。 t1是System.Windows.Forms.TextBox。同样,这可以是任何东西,我只是设置一个静态的someValue字符串变量并在UI上更新标签。

var awaiter2 = t1.WaitableDebouncer(h => t1.TextChanged += h, h => t1.TextChanged -= h, 
    scheduler, 
    canceller.Token, 
    TimeSpan.FromMilliseconds(5000), 
    async () =>
    {
        savedValue = t1.Text;

        Invoke(new Action(() => l1.Text = savedValue));
    });

tasks.Add(awaiter2);  

这就是终止或关闭的样子。这可能是应用程序关闭,也可能是文件关闭。只是某些事件,我们需要取消绑定这些事件,但要保存用户在此之前启动的所有未决工作。想象一下,用户在文本框中输入内容,然后快速按X来关闭应用程序。 5秒还没有用完。

private async Task AwaitPendingEvents()
{
    if (tasks.Count > 0)
    {
        await Task.WhenAll(tasks);
    }            
}

我们有一个应用程序范围内的等待例程。在结束时,我们这样做。

//main cancel signal
canceller.Cancel();

await AwaitPendingEvents();

到目前为止,对于我的测试来说,它似乎可以正常工作。如果没有事件发生,它将取消。如果已生成事件,那么我们将查看是否有任何尚未通过节流的未完成工作。如果是这样,我们取消可观察对象并自己执行待处理的工作,因此我们不必等待计时器。如果有待处理的工作,而我们已经通过节流,那么我们只需等待并让可观察的订阅完成执行。如果请求取消,则订阅将在执行后自行取消。