Отметка DispatcherTimer не работает при запуске в методе ThreadPool.QueueUserWorkItem()

Я создал класс TimerManager для своего приложения WPF. Этот класс обрабатывает запуск и остановку таймера диспетчера. Вот класс:

public static class TimerManager
{
    static DispatcherTimer disTimer;
    static Model m = Model.GetInstance();
    static TimerManager()
    {
        disTimer = new DispatcherTimer();
        disTimer.Tick += disTimer_tick;
        disTimer.Interval = new TimeSpan(0, 0, 1);
    }

    public static void StartTimer()
    {
        disTimer.Start();

    }

    public static void StopTimer()
    {
        disTimer.Stop();
    }

    private static void disTimer_tick(object sender, EventArgs e)
    {
        m.Tick++;
    }
}

И я создал класс Model, который представляет галочку в пользовательском интерфейсе. (Привязка в MainWindow.xaml -> текстовое поле xy textbox "{Binding Tick}").

class Model : INotifyPropertyChanged
{
    private Model()
    {

    }

    static Model instance;
    public static Model GetInstance()
    {
        if (instance == null)
        {
            instance = new Model();
        }
        return instance;
    }

    int tick;

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnNotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public int Tick
    {
        get
        {
            return tick;
        }

        set
        {
            tick = value;
            OnNotifyPropertyChanged();
        }
    }
}

А вот и класс MainWindow:

Model m;
public MainWindow()
{
    InitializeComponent();
    m = Model.GetInstance();
    this.DataContext = m;
}

private void startButton_Click(object sender, RoutedEventArgs e)
{
    ThreadPool.QueueUserWorkItem(o =>
    {
        TimerManager.StartTimer();
    });
    //TimerManager.StartTimer();
}

private void stopButton_Click(object sender, RoutedEventArgs e)
{
    TimerManager.StopTimer();
}

Когда я нажимаю кнопку «Пуск», я использую метод ThreadPool.QueueUserWorkItem(). В этом методе я запускаю таймер, но тик таймера не запускается каждую секунду.

Когда я не использую ThreadPool, это работает. Но это решение мне не подходит; ThreadPool важен для меня, потому что я использую веб-сервер HTTP (локально).

Мой вопрос: почему галочка не работает, если я использую ThreadPool?


person Dancs Berci    schedule 31.07.2017    source источник
comment
зачем использовать ThreadPool для почти мгновенной операции TimerManager.StartTimer();?   -  person ASh    schedule 31.07.2017
comment
Потому что это простой пример. Но в моем приложении (на заказ) не используйте Instant, потому что я использую httplistener (с этой страницы ссылка) метод запуска веб-сервера. Я использую TimerManager.StartTimer() перед возвратом ответа.   -  person Dancs Berci    schedule 31.07.2017


Ответы (1)


Объект DispatcherTimer имеет привязку к потоку. То есть он привязан к определенной нити. В частности, он разработан специально для вызова своего события Tick в потоке, в котором он был создан, используя Dispatcher для этого потока.

Статический конструктор вашего класса ThreadManager будет вызываться при первом использовании типа. В вашем нерабочем примере это происходит в методе рабочего элемента в очереди, в результате чего статический конструктор выполняется в потоке пула потоков, используемом для выполнения этого метода рабочего элемента. Это, в свою очередь, приводит к тому, что объект DispatcherTimer, который вы создаете, принадлежит этому потоку, и его событие Tick вызывается в этом потоке Dispatcher для этого потока.

За исключением того, что потоки пула потоков не имеют Dispatchers. Таким образом, нет Dispatcher, чтобы вызвать событие Tick для объекта DispatcherTimer. Даже если бы это было так, без вызова Application.Run() для выполнения цикла диспетчера Dispatcher на самом деле ничего не отправит, включая событие Tick.

Что вам нужно, так это убедиться, что при создании объекта DispatcherTimer код, который создает этот объект, выполняется в потоке диспетчера, который является вашим основным потоком пользовательского интерфейса.

Есть несколько способов сделать это. ИМХО, лучший способ - сделать ваш класс ThreadManager не классом static и создать его экземпляр в конструкторе MainWindow. Например:

class TimerManager
{
    DispatcherTimer disTimer;
    Model m = Model.GetInstance();

    public TimerManager()
    {
        disTimer = new DispatcherTimer();
        disTimer.Tick += disTimer_tick;
        disTimer.Interval = new TimeSpan(0, 0, 1);
    }

    public void StartTimer()
    {
        disTimer.Start();
    }

    public void StopTimer()
    {
        disTimer.Stop();
    }

    private void disTimer_tick(object sender, EventArgs e)
    {
        m.Tick++;
    }
}

и:

public partial class MainWindow : Window
{
    TimerManager _timerManager = new TimerManager();

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = Model.GetInstance();
    }

    private void startButton_Click(object sender, RoutedEventArgs e)
    {
        ThreadPool.QueueUserWorkItem(o =>
        {
            _timerManager.StartTimer();
        });
    }

    private void stopButton_Click(object sender, RoutedEventArgs e)
    {
        _timerManager.StopTimer();
    }
}

Поскольку вы знаете, что ваш объект MainWindow должен быть создан в потоке диспетчера, и вы знаете, что инициализация нестатического поля происходит одновременно с вызовом конструктора в том же потоке диспетчера, приведенное выше гарантирует, что ваш объект TimerManager будет создан в поток диспетчера.

Это дает вам полный контроль над временем жизни объекта TimerManager, особенно когда он создается, но, конечно, и когда его можно удалить. Учитывая природу самого объекта DispatcherTimer, я считаю, что это лучше, чем поддерживать статически удерживаемый экземпляр.

Этот подход также дает вам возможность иметь объект-менеджер для каждого потока диспетчера (в редких случаях программа может иметь более одного — вам следует очень постараться, чтобы не попасть в такую ​​ситуацию, но может быть полезно, чтобы типы, по крайней мере, были совместим с такой ситуацией).

Тем не менее, если вы действительно хотите сохранить реализацию static, вы можете сделать это, предоставив метод, который можно вызывать явно, когда вы хотите инициализировать класс, чтобы вы могли убедиться, что инициализация происходит в правой ветке:

static class TimerManager
{
    static DispatcherTimer disTimer;
    static Model m = Model.GetInstance();

    public static void Initialize()
    {
        disTimer = new DispatcherTimer();
        disTimer.Tick += disTimer_tick;
        disTimer.Interval = new TimeSpan(0, 0, 1);
    }

    public static void StartTimer()
    {
        disTimer.Start();
    }

    public static void StopTimer()
    {
        disTimer.Stop();
    }

    private static void disTimer_tick(object sender, EventArgs e)
    {
        m.Tick++;
    }
}

Затем в вашем классе MainWindow:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = Model.GetInstance();
        StaticTimerManager.Initialize();
    }

    private void startButton_Click(object sender, RoutedEventArgs e)
    {
        ThreadPool.QueueUserWorkItem(o =>
        {
            StaticTimerManager.StartTimer();
        });
    }

    private void stopButton_Click(object sender, RoutedEventArgs e)
    {
        StaticTimerManager.StopTimer();
    }
}

Все, что вам нужно сделать здесь, это убедиться, что вы вызываете метод Initialize() из основного потока пользовательского интерфейса, где у вас фактически есть работающий диспетчер, прежде чем вы попытаетесь вызвать любой из двух других методов static в классе.

Этот подход также можно использовать для работы с несколькими потоками (например, если у вас более одного потока диспетчера), но это будет сложнее, особенно если вы хотите иметь возможность вызывать метод StartTimer() из другого потока, который на самом деле владеет объектом таймера. Я бы рекомендовал против подхода класса static, если вы действительно оказались в такой ситуации.

person Peter Duniho    schedule 31.07.2017
comment
Спасибо :) Я создаю одноэлементный класс TimerManager, но если я инициализирую экземпляр в threadpool.queueuserworkitem, он не работает, таймер тикает, но если я создаю экземпляр в конструкторе главного окна, он работает :) И я создаю класс StaticTimerManager (я предпочел это решение) с методом инициализации и запустить этот метод в пуле потоков ... не работает тиканье, но я запускаю этот метод (инициализировать) в главном окне (или другом), тик конструктора работает :) Спасибо! :) Но почему ? Могу ли я инициализировать в пуле потоков с рабочим тиканием? :\ - person Dancs Berci; 01.08.2017
comment
Могу ли я инициализироваться в пуле потоков с работающим тиканием? -- нет, вы не можете, и по причине, которую я объяснил в самом начале опубликованного ответа выше. Если вы собираетесь использовать класс DispatcherTimer, вы ДОЛЖНЫ создать объект DispatcherTimer в потоке пользовательского интерфейса, которому он будет принадлежать. - person Peter Duniho; 01.08.2017