C#和任务-UI线程挂起-异步/等待关键字之前

5

我正在尝试了解正确的代码,以异步方式获取一组数据,当我无法访问用于检索数据的客户端库时。我指定了一个端点和一个日期范围,应该检索到一个播放列表。但目前的代码在调用Start()后从未返回。注意:此代码在WinForm中运行。我正在努力理解任务(Tasks),不想仅仅使用awaits或BackgroundWorker。我知道自己在某个地方走错了。

    private void GoButtonClick(object sender, EventArgs e)
    {
        string baseUrl = "http://someserver/api";
        var startDateTime = this._startDateTimePicker.Value;
        var endDateTime = this._endDateTimePicker.Value;
        _getPlaylistsFunc = delegate()
            {
                var client = new PlaylistExportClient(baseUrl);
                return client.GetPlaylistsByDateRange(startDateTime, endDateTime).ToList();
            };
        var task = new Task<List<Playlist>>(_getPlaylistsFunc);
        task.ContinueWith((t) => DisplayPlaylists(t.Result));
        task.Start();
    }

    private void DisplayPlaylists(List<Playlist> playlists)
    {
        _queueDataGridView.DataSource = playlists;
    }

更新 我做了这些更改,但现在应用程序似乎挂起了UI线程。

    private void GoButtonClick(object sender, EventArgs e)
    {
        string baseUrl = "http://someserver/api";
        var startDateTime = this._startDateTimePicker.Value;
        var endDateTime = this._endDateTimePicker.Value;
        var token = Task.Factory.CancellationToken;

        var context = TaskScheduler.FromCurrentSynchronizationContext();
        Task.Factory.StartNew(() =>
            {
                var client = new PlaylistExportClient(baseUrl);
                _queueDataGridView.DataSource = client.GetPlaylistsByDateRange(startDateTime, endDateTime).ToList();

            },token,TaskCreationOptions.None,context);

    }

如果你在GetPlaylistsByDateRange中设置了断点,你能看到它确实被调用了吗? - user7116
2
你需要将同步上下文传递给继续函数,以便它在 UI 线程中运行,而不是在另一个线程池线程中运行。除此之外,我没有看到任何真正的错误。 - Servy
2
你的更新在UI线程上运行了所有任务。只有第二个任务应该在那里运行。 - Hans Passant
2个回答

3

我建议您使用基于任务的异步模式。这很简单:

private async void GoButtonClick(object sender, EventArgs e)
{
    string baseUrl = "http://someserver/api";
    var startDateTime = this._startDateTimePicker.Value;
    var endDateTime = this._endDateTimePicker.Value;
    var playlists = await Task.Run(() =>
    {
        var client = new PlaylistExportClient(baseUrl);
        return client.GetPlaylistsByDateRange(startDateTime, endDateTime).ToList();
    });
    _queueDataGridView.DataSource = playlists;
}

请注意,这将会阻塞线程池线程;如果您可以修改库,添加一个GetPlaylistsByDateRangeAsync方法,那么会更加高效。
编辑:如果您使用的是.NET 4.0,您可以安装Microsoft.Bcl.Async来获取完整的async/await功能。如果出于某些难以理解的原因,您仍然无法使用async/await,那么可以按照以下方式实现:
private void GoButtonClick(object sender, EventArgs e)
{
    string baseUrl = "http://someserver/api";
    var startDateTime = this._startDateTimePicker.Value;
    var endDateTime = this._endDateTimePicker.Value;
    var context = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Run(() =>
    {
        var client = new PlaylistExportClient(baseUrl);
        return client.GetPlaylistsByDateRange(startDateTime, endDateTime).ToList();
    }).ContinueWith(t =>
    {
        _queueDataGridView.DataSource = t.Result;
    }, context);
}

然而,需要注意的是,使用这种方法时,您的错误处理会更加复杂。


我肯定是在尝试学习在异步/等待之前如何完成这项任务。但这也是有用的信息。 - BuddyJoe
我基于问题中指定的“async-await”标签给出了这个答案。如果你想要一个不带async的答案,可以使用Task.ContinueWith(结合TaskScheduler.FromCurrentSynchronizationContext)或者BackgroundWorker。相比这两个选项,async代码更加简洁。 - Stephen Cleary
请查看使用“ContinueWith”方法的更新答案中的代码。 - Stephen Cleary
1
更新后的代码与帖子作者想要的完全一样,除了Task.Run需要改为Task.Factory.StartNew。在.NET 4.0中不存在Run - Cory Nelson
是的,在.NET 4.0中没有Task.Run(),即使安装了VS2010 Async CTP(版本3),它仍然不可用。如果有人使用.NET 4.0,则通常会陷入使用VS2010的困境,因此您对Microsoft.Bcl.Async的参考是无用的。 - Gennady Vanin Геннадий Ванин
实际上,有很多开发人员被困在.NET 4.0中,但被困在VS2010中的人很少。Async CTP和Microsoft.Bcl.Async都提供了TaskEx类型上的Run方法,而不是Task - Stephen Cleary

2
看起来您正在后台线程中为UI控件分配属性。这通常是个坏消息。在WPF中,当您这样做时,通常会抛出异常,不确定WinForms是否也是如此。
请在后台线程中捕获数据,但在将其分配给UI控件之前切换回主UI线程。尝试使用类似以下内容的方式将数据发布到UI线程:
    var uiSync = SynchronizationContext.Current;
    Task.Factory.StartNew(() =>
        {
            var client = new PlaylistExportClient(baseUrl);
            var list = client.GetPlaylistsByDateRange(...).ToList();
            uiSync.Post(() => _queueDataGridView.DataSource = list, null);
        },token,TaskCreationOptions.None,context);

哦,我理解了 - 我想。但是 .Post() 不是我的上下文中的方法。 - BuddyJoe
糟糕。好的,你所称之为上下文变量实际上是一个调度程序。我会更新我的答案,并提供代码来获取同步上下文。 - dthorpe
看起来非常接近。谢谢。我正在解决Post() lambda参数的问题。目前,IDE报告匿名函数签名不正确。 - BuddyJoe
仍然锁定我的WinForm。我所做的唯一更改是在Post()调用中添加了(object state)。 - BuddyJoe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接