首页 新闻 会员 周边 捐助

WPF Task有时候会延迟进入,或者进入后到切换UI线程时会延迟进入。

0
悬赏园豆:100 [已解决问题] 解决于 2019-12-27 17:28

场景,定时器6S一次调用以下方法:代码大概如下:

我另外的业务需要暂停这个定时器,当重新启动定时器时,因为需要即时,所以没用定时器启动方法,而直接调用这个方法,方法内部运行完再启动这个定时器,有时候很慢才进入UI线程里面,保存数据完成后开始计算UI线程进入事件,大概30秒以上。这是线程在排队吗?我应该怎么优化我的代码?或者说我能否指定一个线程专门负责我的定时器,而不需要每次都开启一个线程?

public void GetShipLocation(int min)
{
if (StopTimer)
{
return;
}
ThreadPool.QueueUserWorkItem((o) =>
{
Task.Run(async () =>
{
getShipPositionTimer.Stop();
try
{
Console.WriteLine("Testing method: GetShipLocation " + DateTime.Now);
//读取数据
//保存数据
await App.MyDatabase.SaveVesselRecordListAsync(App.vesselRecordList);
await App.MyDatabase.SaveListWarningMessAsync(newWarnList);
Application.Current.Dispatcher.Invoke(() =>
{
//一些UI线程对象操作
});

                }
                catch (Exception ex)
                {


                }
                getShipPositionTimer.Start();
            });
        });
    }
DotNet8023的主页 DotNet8023 | 初学一级 | 园豆:112
提问于:2019-12-27 12:04

最好做一个可运行的demo发上来

会长 4年前
< >
分享
最佳答案
0

没看懂描述,
定时器6S一次调用GetShipLocation,
然后GetShipLocation里面又有对于定时器的Stop和Start?

收获园豆:50
猝不及防 | 老鸟四级 |园豆:2878 | 2019-12-27 13:27

时调用,但可能存在网络不好,或者说有时候线程进入慢的情况,所以把timer.stop和Start放在里面。

DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 14:09

@DotNet8023
1 不要再timer的执行方法里在调用: timer.stop和start,不存在什么线程进入慢,是由于其他原因而导致的你认为的现象
个人认为是定时器触发方法的时候,你又手动触发了GetShipLocation方法,造成了SaveVesselRecordListAsync多次,而这个数据库连接是独占模式?
2 timmer即时停止,他的方法应该也会走完的

你的目的应该是
1 定时刷新界面
2 做了一些操作后,修改数据库,在即时刷新界面

现在的矛盾点在于定时刷新和即时刷新的冲突,用锁就能解决啊,

void RefreshUI()
{
      Lock(object)
        {
       //UI操作
        }
}

timmer:事件+GetShipLocation,执行就好,不需要停

另外的业务需要暂停这个定时器,重新启动定时器:

void  DoSomethingAndRefreshUI()
{
 await App.MyDatabase.SaveVesselRecordListAsync(App.vesselRecordList);
 await App.MyDatabase.SaveListWarningMessAsync(newWarnList);
 RefreshUI()
 }
猝不及防 | 园豆:2878 (老鸟四级) | 2019-12-27 15:45

@猝不及防: 数据库是SQLite,是UI操作的时候加锁吗?这样?

void DoSomethingAndRefreshUI()
{
await App.MyDatabase.SaveVesselRecordListAsync(App.vesselRecordList);
await App.MyDatabase.SaveListWarningMessAsync(newWarnList);
Application.Current.Dispatcher.Invoke(() =>
{
Lock(object)
{
//UI操作
}
});
}

DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 15:49

@DotNet8023:
对啊,但这不是重点
重点是你要把Timer.Start和Stop从Timer的执行方法分离出来,防止这任务开的越来越多

猝不及防 | 园豆:2878 (老鸟四级) | 2019-12-27 16:03

@DotNet8023: 修改了一下方法名,你应该好理解了

猝不及防 | 园豆:2878 (老鸟四级) | 2019-12-27 16:05

@DotNet8023:
其实按照wpf的思想 数据驱动界面 你是不需要手动更新ui的,只要你的数据变了界面就变了,
这样的话你只需定时从数据库中刷新绑定的数据就行,当然如果你还是一个刷新数据timmer,一个立即修改timmer的话绑定的属性也要加锁

猝不及防 | 园豆:2878 (老鸟四级) | 2019-12-27 16:09

@猝不及防: 说的UI操作是UI线程的对象操作。我也是说尽量按照 数据驱动界面 这样去做的,但有时候,比如一个用户控件只有一个Path,后台逻辑判断绑定什么形状的Data,判断填充什么颜色,我需要new几百个用户控件,当我操作这些path的数据时,比如原来的值是1,那就是红色,我修改了数据,改为2,应该是黄色,有时候不能即时在界面响应,或者说不会再次进入逻辑判断。

<Path Data="{Binding Vessel.Vessel, Converter={StaticResource CVTVesselModelTOPathData}}" x:Name="VesselPath"
Fill="{Binding Vessel , Converter={StaticResource CVTBoolTOSelectionFillRed}}"
Stroke="{Binding IsSelected, Converter={StaticResource CVTBoolTOSelectionForeColor}}"
StrokeThickness="{Binding IsMouseOver, Converter={StaticResource CVTIsMouseOverTOThickness}}" Cursor="Hand"
Opacity="{Binding Vessel.IsVisible, Converter={StaticResource CVTIsVisibleTOOpacity}}">
<Path.RenderTransform>
<TransformGroup>
<RotateTransform Angle="{Binding Vessel.Vessel.Heading}" />
<TranslateTransform X="{Binding PositionX}" Y="{Binding PositionY}" />
</TransformGroup>
</Path.RenderTransform>
</Path>

DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 17:22

@猝不及防: 我已经修改了我的代码,第一个定时器异步操作数据库,读取数据,第二个方法同步操作ui对象。没有用Task,延迟问题解决了。还是谢谢你。

DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 17:25
其他回答(2)
0

QueueUserWorkItem 里面的代码必要再用Task了。可能连QueueUserWorkItem都不用因为看这代码读数据和保存数据的方法都是异步的。方法在保存完数据后才会执行更新UI的代码啊,你可以在读数据前和更新UI前记录下时间看看间隔时间

左眼水星 | 园豆:123 (初学一级) | 2019-12-27 13:40

是操作完数据才会更新UI,没理解你说的

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 14:09
0

有一些问题

  1. 既然已经用了Task为什么用ThreadPool,Task.Run方法默认就是放到线程池上执行的,在ThreadPool中调用Task.Run好像没有什么意义。
  2. System.Threading.Timer 或者 System.Timers.Timer 本就是在单独的后台线程上执行的。
  3. 那两个Save方法有没有可能抛出异常,如果有,操作UI对象的代码可能无法执行。应该吧这段代码放到trycatch外面。
  4. 这个方法是定时器定期调用吗,如果是,在方法内部调用timer.start()好像有些不妥,应该放到外面去。
收获园豆:50
拓拓 | 园豆:1055 (小虾三级) | 2019-12-27 13:42

没异常,定时调用,但可能存在网络不好,或者说有时候线程进入慢的情况,所以把timer.stop和Start放在里面。

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 14:07

@DotNet8023: 个人建议

  1. 把 ThreadPool 和 Task.Run 全删掉,把方法改为异步方法
  2. 测量一下从进入方法到进入操作UI方法的运行时间
public async void GetShipLocation(int min)
{
    if (StopTimer)
    {
        return;
    }
    getShipPositionTimer.Stop();
    try
    {
        Console.WriteLine("Testing method: GetShipLocation " + DateTime.Now);
        //读取数据
        //保存数据
        await App.MyDatabase.SaveVesselRecordListAsync(App.vesselRecordList);
        await App.MyDatabase.SaveListWarningMessAsync(newWarnList);
        Application.Current.Dispatcher.Invoke(() =>
        {
            //一些UI线程对象操作
        });
    }
    catch (Exception ex)
    {
    }
    getShipPositionTimer.Start();
}
支持(0) 反对(0) 拓拓 | 园豆:1055 (小虾三级) | 2019-12-27 14:33

@拓拓: 正常1S左右完成全部

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 14:57

@拓拓: 比如我这个方法正在运行到一半,我在别的地方暂停这个定时器,这个方法还是会继续往下走,尽管我在几个地方加了StopTimer判断,有时候还是会往下走。我应该如何阻止他继续往下走呢?

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 15:16

@DotNet8023: 如果1s左右,单凭这段代码不可能阻塞30秒时间,我看不出来有什么问题了。
一般来说,这种不怎么耗时的东西都是假装已经取消了。。。你想支持取消的话,那两个Save方法需要支持取消,用CancellationToken 和 CancellationTokenSource 来做

支持(0) 反对(0) 拓拓 | 园豆:1055 (小虾三级) | 2019-12-27 15:27

@拓拓: 感觉就像是线程排队

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 15:28

@DotNet8023: 问题是你要开多少后台任务才能排队30秒啊

支持(0) 反对(0) 拓拓 | 园豆:1055 (小虾三级) | 2019-12-27 15:32

@拓拓: 我是有两个开启线程的定时器,都是5/6秒一次,一个是调用接口获取数据的,一个是刚刚那个。如果运行半个小时以上就会出现延迟进入的问题

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 15:33

@DotNet8023: 5/6秒开启一个新线程吗? new Thread() ?

支持(0) 反对(0) 拓拓 | 园豆:1055 (小虾三级) | 2019-12-27 15:37

@拓拓: Task.Run(async () =>

支持(0) 反对(0) DotNet8023 | 园豆:112 (初学一级) | 2019-12-27 15:41

@DotNet8023: Task.run 不会真的开启线程,开销不大,不是主要原因

支持(0) 反对(0) 拓拓 | 园豆:1055 (小虾三级) | 2019-12-27 15:48
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册