【问题标题】:Cancel thread and restart it取消线程并重新启动它
【发布时间】:2014-02-06 18:42:06
【问题描述】:

当用户调整窗口大小时,应该更新一些长文本,但如果线程已经在运行,它应该停止并使用新的宽度参数重新开始。

int myWidth;
private CancellationTokenSource tokenSource2 = new CancellationTokenSource();
private CancellationToken ct = new CancellationToken();

void container_Loaded(object sender, RoutedEventArgs e)
{
  ct = tokenSource2.Token;
  MyFunction();
}

        void container_SizeChanged(object sender, SizeChangedEventArgs e)
        {
          if (tokenSource2.Token.IsCancellationRequested)
            MyFunction();
          else
            tokenSource2.Cancel();
        }

        void MyFunction()            
        {
           myWidth = GetWidth();
           Task.Factory.StartNew(() =>
           {  
              string s;    
              for (int i=0;i<1000,i++){
                  s=s+Functionx(myWidth);
                  ct.ThrowIfCancellationRequested();
              }
              this.Dispatcher.BeginInvoke(new Action(() => { 
                   ShowText(s); 
              }));
           },tokenSource2.Token)
           .ContinueWith(t => {
              if (t.IsCanceled)
              {
                tokenSource2 = new CancellationTokenSource(); //reset token
                MyFunction(); //restart
              };
           });
        }

现在发生的情况是,当我调整窗口大小时,我看到文本在接下来的几秒钟内迭代更新,就好像旧线程没有被取消一样。我做错了什么?

【问题讨论】:

  • 你从来没有真正取消线程。每个调整大小增量都会启动另一个任务。
  • 你是对的。在我看来,我唯一能做的就是让这些对象中的每一个都有一个全局任务,如果正在运行,我可以检查调整大小,然后 task=null, task = new Task.Factory ... 你怎么看@汉斯帕桑特
  • if (tokenSource2.Token.IsCancellationRequested) tokenSource2.Cancel(); - 仅当IsCancellationRequested 已经是true 时才调用Cancel(),这是没有意义的。你是说if (!tokenSource2.Token.IsCancellationRequested) tokenSource2.Cancel(); 吗?
  • 没有理由写:if (ct.IsCancellationRequested) ct.ThrowIfCancellationRequested(); ThrowIfCancellationRequested 的全部意义在于它会检查它是否被取消。只需写ct.ThrowIfCancellationRequested(); 并省略if
  • @Noseratio 是的,我切换了这两行,但它仍然是错误的。拥有一个全局 Width 变量是否有意义,Task 会检查它是否不同然后重新启动?如果在没有重新启动的情况下完成,它将更改全局变量 isFinished=true。如果同时从两个线程访问会不会有问题?

标签: c# multithreading


【解决方案1】:

我认为在这种情况下使用全局变量不是一个好主意。下面是我如何通过从a related question 向我的AsyncOp 类添加取消逻辑来做到这一点。此代码还实现了the IProgress pattern 并限制了 ViewModel 更新。

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace Wpf_21611292
{
    /// <summary>
    /// Cancel and restarts an asynchronous operation
    /// </summary>
    public class AsyncOp<T>
    {
        readonly object _lock = new object();
        Task<T> _pendingTask = null;
        CancellationTokenSource _pendingCts = null;

        public Task<T> CurrentTask
        {
            get { lock (_lock) return _pendingTask; }
        }

        public bool IsPending
        {
            get { lock (_lock) return _pendingTask != null && !_pendingTask.IsCompleted; }
        }

        public bool IsCancellationRequested
        {
            get { lock (_lock) return _pendingCts != null && _pendingCts.IsCancellationRequested; }
        }

        public void Cancel()
        {
            lock (_lock)
            {
                if (_pendingTask != null && !_pendingTask.IsCompleted && !_pendingCts.IsCancellationRequested)
                    _pendingCts.Cancel();
            }
        }

        public Task<T> Run(
            Func<CancellationToken, Task<T>> routine,
            CancellationToken token = default,
            bool startAsync = false,
            bool continueAsync = false,
            TaskScheduler taskScheduler = null)
        {
            Task<T> previousTask = null;
            CancellationTokenSource previousCts = null;

            Task<T> thisTask = null;
            CancellationTokenSource thisCts = null;

            async Task<T> routineWrapper()
            {
                // await the old task
                if (previousTask != null)
                {
                    if (!previousTask.IsCompleted && !previousCts.IsCancellationRequested)
                    {
                        previousCts.Cancel();
                    }
                    try
                    {
                        await previousTask;
                    }
                    catch (Exception ex)
                    {
                        if (!(previousTask.IsCanceled || ex is OperationCanceledException))
                            throw;
                    }
                }

                // run and await this task
                return await routine(thisCts.Token);
            };

            Task<Task<T>> outerTask;

            lock (_lock)
            {
                previousTask = _pendingTask;
                previousCts = _pendingCts;

                thisCts = CancellationTokenSource.CreateLinkedTokenSource(token);

                outerTask = new Task<Task<T>>(
                    routineWrapper,
                    thisCts.Token,
                    continueAsync ?
                        TaskCreationOptions.RunContinuationsAsynchronously :
                        TaskCreationOptions.None);

                thisTask = outerTask.Unwrap();

                _pendingTask = thisTask;
                _pendingCts = thisCts;
            }

            var scheduler = taskScheduler;
            if (scheduler == null)
            {
                scheduler = SynchronizationContext.Current != null ?
                    TaskScheduler.FromCurrentSynchronizationContext() :
                    TaskScheduler.Default;
            }

            if (startAsync)
                outerTask.Start(scheduler);
            else
                outerTask.RunSynchronously(scheduler);

            return thisTask;
        }
    }

    /// <summary>
    /// ViewModel
    /// </summary>
    public class ViewModel : INotifyPropertyChanged
    {
        string _width;

        string _text;

        public string Width
        {
            get
            {
                return _width;
            }
            set
            {
                if (_width != value)
                {
                    _width = value;
                    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Width)));
                }
            }
        }

        public string Text
        {
            get
            {
                return _text;
            }
            set
            {
                if (_text != value)
                {
                    _text = value;
                    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    /// <summary>
    /// MainWindow
    /// </summary>
    public partial class MainWindow : Window
    {
        ViewModel _model = new ViewModel { Text = "Starting..." };

        AsyncOp<DBNull> _asyncOp = new AsyncOp<DBNull>();

        CancellationTokenSource _workCts = new CancellationTokenSource();

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = _model;

            this.Loaded += MainWindow_Loaded;
            this.SizeChanged += MainWindow_SizeChanged;
        }

        void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            _asyncOp.Run(WorkAsync, _workCts.Token);
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            _asyncOp.Run(WorkAsync, _workCts.Token);
        }

        async Task<DBNull> WorkAsync(CancellationToken token)
        {
            const int limit = 200000000;
            var throttle = TimeSpan.FromMilliseconds(200);

            // update ViewModel's Width
            _model.Width = $"Width: {this.Width:#.##}";

            // update ViewModel's Text using IProgress pattern 
            // and throttling updates
            IProgress<int> progress = new Progress<int>(i =>
            {
                _model.Text = $"{(double)i / (limit - 1)* 100:0.}%";
            });

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            // do some CPU-intensive work
            await Task.Run(() =>
            {
                int i;
                for (i = 0; i < limit; i++)
                {
                    if (stopwatch.Elapsed > throttle)
                    {
                        progress.Report(i);
                        stopwatch.Restart();
                    }
                    if (token.IsCancellationRequested)
                        break;
                }
                progress.Report(i);
            }, token);

            return DBNull.Value;
        }
    }
}

XAML:

<Window x:Class="Wpf_21611292.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <TextBox Width="200" Height="30" Text="{Binding Path=Width}"/>
        <TextBox Width="200" Height="30" Text="{Binding Path=Text}"/>
    </StackPanel>
</Window>

它使用async/await,所以如果你的目标是.NET 4.0,你需要Microsoft.Bcl.Async和VS2012+。或者,您可以将 async/await 转换为 ContinueWith,这有点乏味,但总是可以的(这或多或少是 C# 5.0 编译器在幕后所做的)。

【讨论】:

  • 你能解释一下它是如何工作的吗?我想如果你在启动任务时发送令牌,它根本不会启动。我不明白它在哪里取消以前的任务并开始新的任务。此外,它应该一直迭代到 1000,只是将宽度传递给另一个函数。
  • @MiloS,请注意MyFunctionAsync 内的for 循环,这是它迭代的地方,您可以在此处调用MyFunctionX。它通过取消先前的任务实例并异步等待取消完成来工作。查看它是如何发生的最好方法是在调试器中单步执行。
  • 安装 Microsoft.Bcl.Async 它说 .net 4.5 不需要/不支持它,也不支持 AsyncOp。顺便问一下,您的解决方案是否为每个控件创建了两个任务?
  • @MiloS,如果您以 .NET 4.5 为目标,则不需要 Microsoft.Bcl.Async,我说过您在 .NET 4.0 中需要它。复制AsyncOp 代码from here顺便问一下,您的解决方案是否为每个控件创建了两个任务? 不确定您在问什么。 AsyncOp 用于每个 operaton (如您的 MyFunction)。您可以根据需要为每个控件使用任意数量的控件。
  • 非常感谢您的时间和帮助。它在您的测试应用程序中工作得很好!我将在下一条评论中分享为什么它在我的应用程序中不起作用。干杯!
【解决方案2】:

您应该使用 Microsoft 的响应式框架(又名 Rx) - NuGet System.Reactive.Windows.Threading(用于 WPF)并添加 using System.Reactive.Linq; - 然后您可以这样做:

    public MainWindow()
    {
        InitializeComponent();

        _subscription =
            Observable
                .FromEventPattern<SizeChangedEventHandler, SizeChangedEventArgs>(
                    h => container.SizeChanged += h,
                    h => container.SizeChanged -= h)
                .Select(e => GetWidth())
                .Select(w => Observable.Start(
                        () => String.Concat(Enumerable.Range(0, 1000).Select(n => Functionx(w)))))
                .Switch()
                .ObserveOnDispatcher()
                .Subscribe(t => ShowText(t));
    }

    private IDisposable _subscription = null;

这就是所有需要的代码。

这会响应SizeChanged 事件,调用GetWidth,然后将Functionx 推送到另一个线程。它使用Switch() 始终切换到最新的SizeChanged,然后忽略任何正在进行的代码。它将结果推送到调度程序,然后调用ShowText

如果您需要关闭表单或停止订阅运行,只需致电_subscription.Dispose()

简单。

【讨论】:

  • 真的很漂亮很简洁。 Rx.NET 是我一直想在生产项目中使用的东西,但还没有实现。
  • @noseratio - 它美观、简洁且功能强大。对于几乎所有事情,我更喜欢它而不是任务。 Observables 也是可等待的。他们太棒了!
  • ? 我希望我能在未来的项目中更多地使用它,尤其是现在微软正在为基于 XAML 的 UI 采用 MVU 模式。
  • @noseratio - 模型/视图/更新?这与 MVC 有何不同?
  • 基本上这是在 WPF 或未来的 MAUI .NET 中完成的 React 模式的变体(即单向绑定和不可变状态),请参阅Introducing .NET Multi-platform App UI,您可以向下滚动到“MVU”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-12
  • 1970-01-01
  • 2019-06-29
  • 1970-01-01
  • 2015-10-20
相关资源
最近更新 更多