async / await不切换回UI线程

时间:2016-05-31 07:13:24

标签: c# winforms async-await

我有调用存储操作列表的计时器。我希望异步调用这些操作。所以我将CPU绑定操作包装到任务中,然后使async / await运行。但是,它没有更新组合框。显然,上下文不会切换回UI,但我不明白为什么以及我应该做些什么来解决它。

主要形式的代码:

public FormMain()
{
    InitializeComponent();

    pt = new PeriodicTask(() => Execute());
    pt.Start();

    actions = new List<ActionWrapper>();
    actions.Add(new ActionWrapper() { Periodic = false, MyAction = async () => {
        bool b = await NetworkOperation();
        comboBoxPairs.DataSource = pairs; // this doesn't update combo box
        comboBoxPairs.DisplayMember = "Name";
        //comboBoxPairs.Refresh(); // this is even crashing app
    }});
}

private Task<bool> NetworkOperation()
{
    return Task.Run(() => {

        // CPU bound activity goes here
        return true;
    });
}

private void Execute()
{
    Parallel.ForEach(actions,
        new ParallelOptions { MaxDegreeOfParallelism = 10 },
        x => {
            x.MyAction();
            if (!x.Periodic)
                actions.Remove(x);
        });
}

计时器类:

public class PeriodicTask
{
    private System.Threading.Timer timer;
    private int dueTime;
    private int periodTime;
    private Action callBack;

    public PeriodicTask(Action cb)
    {
        callBack = cb;
        timer = new System.Threading.Timer(Task, null, Timeout.Infinite, Timeout.Infinite);
        dueTime = 100;
        periodTime = 5000;
    }

    public void Start()
    {
        timer.Change(dueTime, periodTime);
    }

    public void Stop()
    {
        timer.Change(Timeout.Infinite, Timeout.Infinite);
    }

    private void Task(object parameter)
    {
        callBack();
    }
}

这是我用来执行操作的包装类:

public class ActionWrapper
{
    public bool Periodic { get; set; }
    public Func<Task> MyAction { get; set; }
}

1 个答案:

答案 0 :(得分:1)

并不是它没有切换回来,它首先没有在UI线程上启动,因为你正在使用System.Threading.Timer来处理WPF中的线程池线程上的滴答上下文。

您可以将其替换为DispatcherTimer

  

如果在WPF应用程序中使用System.Timers.Timer,则值得注意的是System.Timers.Timer在与用户界面(UI)线程不同的线程上运行。为了访问用户界面(UI)线程上的对象,必须使用Invoke或BeginInvoke将操作发布到用户界面(UI)线程的Dispatcher上。使用与System.Timers.Timer相对的DispatcherTimer的原因是 DispatcherTimer在与Dispatcher 相同的线程上运行,并且可以在DispatcherTimer上设置DispatcherPriority。

此外,您需要处理重新入口,与System.Threading.Timer相同,因为Cpu绑定操作仍然可以处理上一个滴答。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        DispatcherTimer timer = new DispatcherTimer();

        long currentlyRunningTasksCount;

        public MainWindow()
        {
            InitializeComponent();

            Loaded += MainWindow_Loaded;

            timer.Interval = TimeSpan.FromSeconds(1);

            timer.Tick += async (s, e) =>
            {
                // Prevent re-entrance.
                // Skip the current tick if a previous one is already in processing.
                if (Interlocked.CompareExchange(ref currentlyRunningTasksCount, 1, 0) != 0)
                {
                    return;
                }
                try
                {
                    await ProcessTasks();
                }
                finally
                {
                    Interlocked.Decrement(ref currentlyRunningTasksCount);
                }
            };
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            // This one would crash, ItemsSource requires to be invoked from the UI thread.
            // ThreadPool.QueueUserWorkItem(o => { listView.Items.Add("started"); });

            listView.Items.Add("started");

            timer.Start();
        }

        async Task ProcessTasks()
        {
            var computed = await Task.Run(() => CpuBoundComputation());

            listView.Items.Add(string.Format("tick processed on {0} threads", computed.ToString()));
        }

        /// <summary>
        /// Computes Cpu-bound tasks. From here and downstream, don't try to interact with the UI.
        /// </summary>
        /// <returns>Returns the degree of parallelism achieved.</returns>
        int CpuBoundComputation()
        {
            long concurrentWorkers = 0;

            return
                Enumerable.Range(0, 1000)
                .AsParallel()
                .WithDegreeOfParallelism(Math.Max(1, Environment.ProcessorCount - 1))
                .Select(i =>
                {
                    var cur = Interlocked.Increment(ref concurrentWorkers);

                    SimulateExpensiveOne();

                    Interlocked.Decrement(ref concurrentWorkers);
                    return (int)cur;
                })
                .Max();
        }

        /// <summary>
        /// Simulate expensive computation.
        /// </summary>
        void SimulateExpensiveOne()
        {
            // Prevent from optimizing out the unneeded result with GC.KeepAlive().
            GC.KeepAlive(Enumerable.Range(0, 1000000).Select(i => (long)i).Sum());
        }
    }
}

如果您需要precise控制发生的事情,最好排队事件并独立于处理显示事件:

using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication2
{
    public partial class MainWindow : Window
    {
        DispatcherTimer fastTimer = new DispatcherTimer();

        BackgroundProcessing processing = new BackgroundProcessing();

        public MainWindow()
        {
            InitializeComponent();

            processing.Start();

            fastTimer.Interval = TimeSpan.FromMilliseconds(10);
            fastTimer.Tick += Timer_Tick;

            fastTimer.Start();
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            Notification notification;
            while ((notification = processing.TryDequeue()) != null)
            {
                listView.Items.Add(new { notification.What, notification.HappenedAt, notification.AttributedToATickOf });
            }        
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);
            processing.Stop();
        }
    }

    public class Notification
    {
        public string What { get; private set; }

        public DateTime AttributedToATickOf { get; private set; }

        public DateTime HappenedAt { get; private set; }

        public Notification(string what, DateTime happenedAt, DateTime attributedToATickOf)
        {
            What = what;
            HappenedAt = happenedAt;
            AttributedToATickOf = attributedToATickOf;
        }
    }

    public class BackgroundProcessing
    {
        /// <summary>
        /// Different kind of timer, <see cref="System.Threading.Timer"/>
        /// </summary>
        Timer preciseTimer;

        ConcurrentQueue<Notification> notifications = new ConcurrentQueue<Notification>();

        public Notification TryDequeue()
        {
            Notification token;
            notifications.TryDequeue(out token);
            return token;
        }

        public void Start()
        {
            preciseTimer = new Timer(o =>
            {
                var attributedToATickOf = DateTime.Now;

                var r = new Random();

                Parallel.ForEach(Enumerable.Range(0, 2), i => {

                    Thread.Sleep(r.Next(10, 5000));

                    var happenedAt = DateTime.Now;

                    notifications.Enqueue(
                        new Notification("Successfully loaded cpu 100%", happenedAt, attributedToATickOf));
                });

            }, null, 0, 1000);
        }

        public void Stop()
        {
            preciseTimer.Change(0, 0);
        }
    }
}

更新: 对于Windows窗体,您可以使用第二个代码示例中的System.Windows.Forms.Timer替换DispatcherTimer。