Task.Run 中的奇怪行为 [英] Strange behavior in Task.Run

查看:43
本文介绍了Task.Run 中的奇怪行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图运行一个冗长的操作,结果显示在窗口中,而不阻塞 UI 线程.我拥有的是一个 View,它有一个 ListView 控件,它绑定到我的 ViewModel 中的 ObservableCollection.我还有一些其他 TextBlock 控件绑定到两个数组.在 Task 中的冗长操作之前运行的任何内容都会显示出来,而之后的任何内容都不会显示.这是我的代码,可帮助您理解我的意思:

I am trying to run a lengthy operation with the results being displayed in the window, without blocking the UI thread. What I have is a View that has a ListView control, which binds to an ObservableCollection in my ViewModel. I also have a few other TextBlock controls that bind to two arrays. Anything that runs before the lengthy operation within the Task gets displayed and anything after does not. Here's my code to help you understand what I mean:

四年:

<TextBlock TextWrapping="Wrap" Grid.Row="1" 
           Style="{DynamicResource SectionBodyTextStyle}">
    <Run Text="{Binding Years[1]}"/>
    <Run Text=":"/>
</TextBlock>
<TextBlock TextWrapping="Wrap" Grid.Row="2" 
           Style="{DynamicResource SectionBodyTextStyle}">
    <Run Text="{Binding Years[2]}"/>
    <Run Text=":"/>
</TextBlock>
<TextBlock TextWrapping="Wrap" Grid.Row="3" 
           Style="{DynamicResource SectionBodyTextStyle}">
    <Run Text="{Binding Years[3]}"/>
    <Run Text=":"/>
</TextBlock>

保存每年计数的四个字段.

Four fields that hold the counts for each year.

<TextBlock Text="{Binding YearCounts[0]}" TextWrapping="Wrap" 
           Grid.Column="1" Margin="10,0,0,0"/>
<TextBlock Text="{Binding YearCounts[1]}" TextWrapping="Wrap" 
           Grid.Column="1" Grid.Row="1" Margin="10,0,0,0"/>
<TextBlock Text="{Binding YearCounts[2]}" TextWrapping="Wrap" 
           Grid.Column="1" Grid.Row="2" Margin="10,0,0,0"/>
<TextBlock Text="{Binding YearCounts[3]}" TextWrapping="Wrap" 
           Grid.Column="1" Grid.Row="3" Margin="10,0,0,0"/>

ListView 保存每条记录的信息:

ListView to hold the information for each record:

<ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Report Number" 
                            DisplayMemberBinding="{Binding RepNum}"/>
            <GridViewColumn Header="Employee ID" 
                            DisplayMemberBinding="{Binding EmployeeId}"/>
            <GridViewColumn Header="Date" 
                            DisplayMemberBinding="{Binding Date}"/>
            <GridViewColumn Header="Time" 
                            DisplayMemberBinding="{Binding Time}"/>
        </GridView>
    </ListView.View>
</ListView>

到目前为止,没有任何异常.但是,这是我与它们相关的代码.

So far, nothing out of ordinary. However, here's my code related to them.

属性:

    private string[] _years;
    public string[] Years
    {
        get { return _years; }
        private set
        {
            if (_years == value)
            {
                return;
            }

            _years = value;
            OnPropertyChanged("Years");
        }
    }

    private int[] _yearCounts;
    public int[] YearCounts
    {
        get { return _yearCounts; }
        private set
        {
            if (_yearCounts == value)
            {
                return;
            }

            _yearCounts = value;
            OnPropertyChanged("YearCounts");
        }
    }

    private ObservableCollection<RecordModel> _missingCollection;
    public ObservableCollection<RecordModel> MissingCollection
    {
        get { return _missingCollection; }
        private set
        {
            if (_missingCollection == value)
            {
                return;
            }

            _missingCollection = value;
            OnPropertyChanged("MissingCollection");
        }
    }

构造函数:

public MissingReportsViewModel()
{
    YearCounts = new int[4];
    Years = new string[4];

    Task.Run(() =>
    {
        SetYears();
        MissingCollection = new AccessWorker().GetMissingReports();
        SetYearCounts();
    });
}

来自这个 ViewModel 的方法:

private void SetYears()
{
    for (int i = 0; i < 4; i++)
    {
        Years[i] = DateTime.Now.AddYears(-i).Year.ToString();
    }
}

private void SetYearCounts()
{
    for (int i = 0; i < 4; i++)
    {
        YearCounts[i] =  MissingCollection.Where(item => item.RepNum.Substring(0, 4).Equals(Years[i]))
                                          .ToList().Count();

    }
}

访问工作者也有方法,但代码相当冗长.它基本上连接到 Access 数据库并获取一些数据.

And there's also method from the access worker, but the code is rather lengthy. It basically connects to an Access database and gets some data.

所以,我的问题是,如果我在 MissingCollection = new AccessWorker().GetMissingReports(); 部分之前放置任何方法在 Task.Run() 或之外它,它们将显示在 UI 上.但是,如果我在该部分之后放置任何内容,它将不会显示在 UI 中.以下方法是否在 Task.Run 中无关紧要,结果相同.我已经检查过并且方法产生了正确的值,但它们从未出现在 UI 中.我只是不明白这两者如何产生如此不同的结果:

So, my problem is that if I place any methods before MissingCollection = new AccessWorker().GetMissingReports(); portion within Task.Run() or outside of it, they will get displayed on the UI. However, if I place anything after that portion, it won't get displayed in the UI. Doesn't matter if the following method is within Task.Run or not, same result. I've checked and method yield proper values, they just never make it to the UI. I simply don't understand how these two can yield such different results:

// First -  Years get displayed.
// Second - After a little while data gets displayed
// Third - Counts never get displayed.
Task.Run(() =>
{
    SetYears();
    MissingCollection = new AccessWorker().GetMissingReports();
    SetYearCounts();
});


// First -  After a while, data gets displayed.
// Second - Years and counts do not get displayed.
Task.Run(() =>
{
    MissingCollection = new AccessWorker().GetMissingReports();
    SetYears();
    SetYearCounts();
});

我显然做错了什么,但我不知道是什么.我试过调用 UI 线程,但没有任何作用.

I'm obviously doing something wrong, but I can't figure out what. I've tried invoking into the UI thread, but that didn't do anything.

当我尝试为我的 YearsCount 数组绑定调用 UI 线程的更新时,我得到一个奇怪的超出范围的异常.代码如下:

When I try to invoke an update into the UI thread for my YearsCount array bindings, I get an odd out of range exception. Here's the code:

private void Initialize()
{
    SetYears();
    Task.Run(() =>
    {
        MissingCollection = new AccessWorker().GetMissingReports();
        SetYearCounts();
    });
}

private void SetYearCounts()
{
    for (int i = 0; i < 4; i++)
    {
        Application.Current.Dispatcher.BeginInvoke(
            DispatcherPriority.Background, 
            new Action(() => YearCounts[i] =  MissingCollection.Where(
                    item => item.RepNum.Substring(0, 4).Equals(Years[i])).ToList().Count()));
    }
}

当我单步执行时,它会遍历 YearCounts[i] 的每个索引,从 SetYearCounts() 跳回到 Task.Run(),然后跳回到 SetYearsCounts() 并使用最后一个 i 值,在 Years[i] 中为 4,这显然会引发超出范围的异常.

When I step through it, it'll go through each index of YearCounts[i], jump out of the SetYearCounts() back to Task.Run(), then jump back into the SetYearsCounts() and use the last i value, which is 4 in Years[i], which, obviously, throws the out of range exception.

当我在没有 Task.Run() 的情况下运行代码时,这一切都不会发生.它只是冻结 UI,直到操作完成.

None of this happens when I run the code without Task.Run(). It just freezes for UI until the operation is finished.

如果我这样做:

private void Initialize()
{
    SetYears();
    Task.Run(() =>
    {
        MissingCollection = new AccessWorker().GetMissingReports();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
            new Action(() => SetYearCounts()));
    });
}

...并写出在调试窗口中分配的每个值:

...and write out each value as it's being assigned in the Debug window:

Debug.WriteLine(YearCounts[i]);

...它会在那里写输出,但不会写在 UI 窗口上.我认为这与数组只报告它们自己的变化而不是项目本身的变化有关.当只报告数组的初始初始化时,我可以在调试窗口中看到这一点,但没有对项目进行更改.然而,奇怪的是,在可观察集合之前的任何东西都会被更新,而在可观察集合之后的任何东西都不会.这可能与在可观察集合调用更改时查看其数据上下文的视图有关.

...it'll write outputs there, but not on the UI window. I am thinking it has to do with the fact that arrays only report on their own change and not the change of the items themselves. I can see that in the debug window when only the initial initializations of the arrays get reported, but no the change to the items. However, what is odd is the fact that anything prior to the observable collection gets updated and anything after doesn't. It has probably to do with the view looking at its data context when observable collection calling a change.

每个请求,这里是 GetMissingReports() 声明部分:

Per request, here's GetMissingReports() declarative portion:

public ObservableCollection<RecordModel> GetMissingReports() {..}

推荐答案

关于下标越界错误的部分,你可以创建一个只做这个的小WPF应用...

For the part about the subscript out of range error, you can create a small WPF app that only does this...

    public MainWindow()
    {
        InitializeComponent();
        for (int i = 0; i < 10; i++)
        {
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
                new Action(() => Console.WriteLine(i.ToString())));
        }
    }

这就是你得到的......

and here's what you get...

10101010101010101010

10 10 10 10 10 10 10 10 10 10

这是由于委托关闭不当造成的.要关闭它,请编写一个仅执行此操作的小型 WPF 应用程序...

This is caused by inadequate closure of the delegate. To close it off, write a small WPF app that only does this...

    public MainWindow()
    {
        InitializeComponent();
        for (int i = 0; i < 10; i++)
        {
            int i1 = i;
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
                new Action(() => Console.WriteLine(i1.ToString())));
        }
    }

并观察结果是...

0123456789

区别在于关闭".您的 Action 委托发生在一个循环内,AND 正在访问循环控制变量.使用闭包将使异常消失.

The difference is 'closure'. Your Action delegate takes place inside a loop AND is accessing the loop control variable. Using closure will make the exception go away.

对于问题的同步部分,根据您所写的内容,它看起来像是竞争条件.如果您更改此代码块...

For the synchronization part of the question, and based upon what you wrote, it looks like a race condition. If you change this block of code...

private void Initialize()
{
    SetYears();
    Task.Run(() =>
    {
        MissingCollection = new AccessWorker().GetMissingReports();
        SetYearCounts();
    });
}

这...

    private void OtherInitialize()
    {
        SetYears();
        Task task = new Task(() => { MissingCollection = new AccessWorker().GetMissingReports(); });
        task.ContinueWith((result) => { SetYearCounts(); });
        task.Start();
    }

...那么您的年"将按预期同步,而不会造成正在发生的竞争条件.但是,您仍然需要在 UI 线程上进行调度.

...then your 'Years' will synch up as expected, and not create the race condition that was occurring. You'll still need to dispatch on the UI thread however.

这篇关于Task.Run 中的奇怪行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆