C # WhenAny Отмена вызова не работает, вызывая накопление потока - PullRequest
1 голос
/ 08 марта 2019

Скорее всего, это действительно глупая ошибка, но я не могу понять это, и я потратил почти 2 дня на это. У меня есть приложение, которое одним нажатием кнопки запускает 5 задач, работающих параллельно, но каждая со своей задержкой. Эта часть отлично работает. Однако, если вы нажмете ту же кнопку еще раз, она не отменяет ранее запущенные задачи и создает другой экземпляр. Так что в основном создает все больше и больше задач, которые работают параллельно. У меня, очевидно, есть код, пытающийся отменить задачи, если кнопка нажата более одного раза, но по какой-то причине она не работает. Может ли кто-нибудь указать мне на мою проблему? Или этот код не подлежит ремонту и нуждается в полной реконструкции? Спасибо! Если вы запустите это приложение WPF и нажмете на кнопку, вы увидите, что частота пинга продолжает расти.

using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace TestMultiThreadWithDiffSleeps
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        #region Binding
        private string m_output0;
        public string Output0
        {
            get { return m_output0; }
            set { m_output0 = value; OnPropertyChanged(); }
        }

        private string m_output1;
        public string Output1
        {
            get { return m_output1; }
            set { m_output1 = value; OnPropertyChanged(); }
        }

        private string m_output2;
        public string Output2
        {
            get { return m_output2; }
            set { m_output2 = value; OnPropertyChanged(); }
        }

        private string m_output3;
        public string Output3
        {
            get { return m_output3; }
            set { m_output3 = value; OnPropertyChanged(); }
        }

        private string m_output4;
        public string Output4
        {
            get { return m_output4; }
            set { m_output4 = value; OnPropertyChanged(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (PropertyChanged != null)
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        private static SemaphoreSlim ThreadSemaphore;
        private CancellationTokenSource CancellationTokenSrc;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            ThreadSemaphore = new SemaphoreSlim(1, 1);
        }

        private async void ButtonStart_Click(object sender, RoutedEventArgs e)
        {
            await StartToMonitor();
        }

        private async Task<bool> StartToMonitor()
        {
            M_Stop(); // Stop everything in case this is a restart

            CancellationTokenSrc = new CancellationTokenSource();
            bool taskResult = await M_Start();
            CancellationTokenSrc = null;

            return taskResult;
        }

        public void M_Stop()
        {
            Output0 = string.Empty;
            Output1 = string.Empty;
            Output2 = string.Empty;
            Output3 = string.Empty;
            Output4 = string.Empty;

            if (CancellationTokenSrc != null)
                CancellationTokenSrc.Cancel();
        }

        private async Task<bool> M_Start()
        {
            List<Task<bool>> tasks = new List<Task<bool>>();

            // Build task list
            for (int i = 0; i < 5; i++)
            {
                int iIdx = i; // Need to do this when multi-threading
                int sleepTime = (i + 1) * 1000;

                tasks.Add(Task.Run(async () =>
                {
                    while (!CancellationTokenSrc.Token.IsCancellationRequested)
                    {
                        if (!Ping(iIdx))
                            return false; // Ping in this example always returns 'true' but you can imagine in real life there'd be 'false' returns that shall cancel all threads

                        await Task.Delay(sleepTime); // Delay for different length of time for each thread
                    }
                    return true;
                }, CancellationTokenSrc.Token));
            }

            Task<bool> firstFinishedTask = await Task.WhenAny(tasks);
            bool result = firstFinishedTask.Result;
            CancellationTokenSrc.Cancel(); // Cancel all other threads as soon as one returns

            return result;
        }

        private bool Ping(int index)
        {
            ThreadSemaphore.Wait(); // Not needed for this app...  here only because it's in my other app I'm troubleshooting

            switch (index)
            {
                case 0: Output0 += "*"; break;
                case 1: Output1 += "*"; break;
                case 2: Output2 += "*"; break;
                case 3: Output3 += "*"; break;
                case 4: Output4 += "*"; break;
            }

            ThreadSemaphore.Release();

            return true;
        }
    }
}


<Window x:Class="TestMultiThreadWithDiffSleeps.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <StackPanel>
        <Button Content="Start" HorizontalAlignment="Left" VerticalAlignment="Top" Width="200" Margin="0,30,0,0" Click="ButtonStart_Click"/>
        <TextBox Text="{Binding Output0}"/>
        <TextBox Text="{Binding Output1}"/>
        <TextBox Text="{Binding Output2}"/>
        <TextBox Text="{Binding Output3}"/>
        <TextBox Text="{Binding Output4}"/>
    </StackPanel>
</Window>

Ответы [ 2 ]

2 голосов
/ 08 марта 2019

В этом коде есть несколько проблем.

Источник описываемой вами проблемы:

tasks.Add(Task.Run(async () =>
{
    while (!CancellationTokenSrc.Token.IsCancellationRequested)

Задачи всегда проверяют токен текущего CancellationTokenSrc.Так что, если отмена не произойдет в точный момент между Task.Delay вызовами, когда задачи проверяют IsCancellationRequested, они просто увидят новый, не отмененный CTS, который вы создадите после перезапуска.

Вы должны передать текущий CancellationToken в качестве аргумента метода или сохраните его в локальной переменной внутри M_Start вместо проверки общего поля CancellationTokenSrc, чтобы избежать этой проблемы.

Небольшое улучшение: Task.Delay также имеет перегрузку, которая принимаетCancellationToken.Он выдаст TaskCanceledException.

. Кроме того, вы обновляете свойства OutputX из параллельных потоков (Task.Run), которые затем вызывают событие PropertyChanged, которое должно обновлять пользовательский интерфейс.Этот вид межпотокового взаимодействия с пользовательским интерфейсом небезопасен, вам нужно будет задействовать Dispatcher, чтобы убедиться, что событие вызвано в потоке пользовательского интерфейса.

Наконец, владение CancellationTokenSrc довольносложный и кажется созревшим для условий гонки, так как несколько параллельных методов используют и устанавливают его.В случае перезапуска M_Start может легко вызвать NullReferenceException при попытке отменить CancellationTokenSrc, для которого уже установлено значение null в StartToMonitor.

1 голос
/ 08 марта 2019

Я думаю, вам нужно также сделать Task.Delay отменяемым:

await Task.Delay(sleepTime);

Измените его на:

await Task.Delay(sleepTime, CancellationTokenSrc.Token);

...