Вопрос по .net, c#, multithreading – Как вызвать метод пользовательского интерфейса из другого потока

21

Играя с таймерами. Контекст: winforms с двумя метками.

Я хотел бы увидеть, какSystem.Timers.Timer работает, поэтому я не использовал таймер форм. Я понимаю, что форма и myTimer теперь будут работать в разных потоках. Есть ли простой способ представить прошедшее время наlblValue в следующем виде?

Я посмотрел здесьMSDN но есть ли более простой способ!

Вот код winforms:

<code>using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {
    //instance variables of the form
    System.Timers.Timer myTimer;
    int ElapsedCounter = 0;

    int MaxTime = 5000;
    int elapsedTime = 0;
    static int tickLength = 100;

    public AirportParking()
    {
        InitializeComponent();
        keepingTime();
        lblValue.Text = "hello";
    }

    //method for keeping time
    public void keepingTime() {

        myTimer = new System.Timers.Timer(tickLength); 
        myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);

        myTimer.AutoReset = true;
        myTimer.Enabled = true;

        myTimer.Start();
    }


    void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        myTimer.Stop();
        ElapsedCounter += 1;
        elapsedTime += tickLength; 

        if (elapsedTime < MaxTime)
        {
            this.lblElapsedTime.Text = elapsedTime.ToString();

            if (ElapsedCounter % 2 == 0)
                this.lblValue.Text = "hello world";
            else
                this.lblValue.Text = "hello";

            myTimer.Start(); 

        }
        else
        { myTimer.Start(); }

    }
  }
}
</code>
Нужно ли останавливать таймер каждый раз, когда он проходит по истечении указанного времени? Я, вероятно, условно остановлю его - не остановлю, а затем условно начну. SkonJeet
Я думаю, что способ подсчета истекшего времени будет довольно неточным. Вам было бы гораздо лучше использовать объект Секундомер для фактического расчета истекшего времени (но все равно использовать таймер для периодического обновления элемента управления). Конечно, это вообще не имеет значения, если все, что вы делаете, это изучаете таймеры. Matthew Watson
Работает ли этот код на самом деле, или исключено межпотоковое исключение в обработчике? Я не пробовал этот код, но я бы подумал, что, поскольку обработчик может работать в любом потоке из пула потоков, обновление метки в графическом интерфейсе в большинстве случаев приведет к исключению между потоками. Jodrell
@ Джодрелл - точно! но я посмотрел вВо и кажется, что добавление следующей строки помогаетmyTimer.SynchronizingObject = this; ..... я должен ответить на свой вопрос?! whytheq
@ whytheq, который заставил бы таймер выполнять перекрестные потоки для вас, не видел этого в примере. Кроме того, вы делаете проверку самостоятельно с помощьюControl.InvokeRequired Msdn.microsoft.com / EN-US / библиотека / ... Jodrell

Ваш Ответ

5   ответов
2

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

РЕДАКТИРОВАТ исправленоBeginInvoke вызов. Я сделал перекрестный вызов, используя общийAction, Это позволяет отправителю и eventargs быть переданным. Если они не используются (как они здесь), более эффективно использоватьMethodInvoker но я подозреваю, что обработку нужно будет перенести в метод без параметров.

public partial class AirportParking : Form
{
    private Timer myTimer = new Timer(100);
    private int elapsedCounter = 0;
    private readonly DateTime startTime = DateTime.Now;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    public AirportParking()
    {
        lblValue.Text = EvenText;
        myTimer.Elapsed += MyTimerElapsed;
        myTimer.AutoReset = true;
        myTimer.Enabled = true;
        myTimer.Start();
    }

    private void MyTimerElapsed(object sender,EventArgs myEventArgs)
    {
        If (lblValue.InvokeRequired)
        {
            var self = new Action<object, EventArgs>(MyTimerElapsed);
            this.BeginInvoke(self, new [] {sender, myEventArgs});
            return;   
        }

        lock (this)
        {
            lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString();
            elapesedCounter++;
            if(elapsedCounter % 2 == 0)
            {
                lblValue.Text = EvenText;
            }
            else
            {
                lblValue.Text = OddText;
            }
        }
    }
}
почему я использовалElapsedCounter += 1; скорее, чемelapesedCounter++; !! whytheq
put EvenText / OddText в качестве переменных экземпляра кажется лучшей практикой, так как я должен избегать оставления жестко закодированных строк в середине моего кода whytheq
отличный; использованиеDateTime упрощает whytheq
не уверен, почему вы явно указали большинство переменныхprivate ... это не по умолчанию? Также почемуstartTime неstatic ? whytheq
7

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

private void InvokeUI(Action a)
{
    this.BeginInvoke(new MethodInvoker(a));
}

Когда я делаю асинхронный вызов в другом потоке, я всегда могу использовать обратный вызов.

InvokeUI(() => { 
   Label1.Text = "Super Cool";
});

Просто и чисто.

35

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

БольшинствоControlоступ к методам и свойствам @ возможен только из потока пользовательского интерфейса (в действительности к ним можно получить доступ только из потока, в котором вы их создали, но это уже другая история). Это потому, что каждый поток должен иметь свой собственный цикл сообщений GetMessage() отфильтровывает сообщения по цепочке), а затем сделать что-то сControl вы должны отправить сообщение из своей ветки наглавны нить. В .NET это легко, потому что каждыйControl наследует несколько методов для этой цели:Invoke/BeginInvoke/EndInvoke. Чтобы узнать, должен ли выполняющийся поток вызывать эти методы, у вас есть свойствоInvokeRequired. Просто измените код, чтобы он работал:

if (elapsedTime < MaxTime)
{
    this.BeginInvoke(new MethodInvoker(delegate 
    {
        this.lblElapsedTime.Text = elapsedTime.ToString();

        if (ElapsedCounter % 2 == 0)
            this.lblValue.Text = "hello world";
        else
            this.lblValue.Text = "hello";
    }));
}

Пожалуйста, проверьте MSDN для списка методов, которые вы можете вызвать из любого потока, так же, как ссылку, которую вы всегда можете вызватьInvalidate, BeginInvoke, EndInvoke, Invoke методы и читатьInvokeRequired свойство. В общем, это распространенный шаблон использования (при условииthis - это объект, полученный изControl):

void DoStuff() {
    // Has been called from a "wrong" thread?
    if (InvokeRequired) {
        // Dispatch to correct thread, use BeginInvoke if you don't need
        // caller thread until operation completes
        Invoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

Обратите внимание, что текущий поток будет блокироваться до тех пор, пока поток пользовательского интерфейса не завершит выполнение метода. Это может быть проблемой, если важна синхронизация потока (не забывайте, что поток пользовательского интерфейса может быть занят или немного завис). Если вам не нужно возвращаемое значение метода, вы можете просто заменитьInvoke сBeginInvoke, для WinForms вам даже не нужен последующий вызовEndInvoke:

void DoStuff() {
    if (InvokeRequired) {
        BeginInvoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

Если вам нужно возвращаемое значение, тогда вы должны иметь дело с обычнымIAsyncResult интерфейс.

Как это работает

GUI Windows-приложение основано на оконной процедуре с ее циклами сообщений. Если вы пишете приложение на простом C, у вас есть что-то вроде этого:

MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
    TranslateMessage(&message);
    DispatchMessage(&message);
}

С этими несколькими строками кода ваше приложение ожидает сообщения и затем доставляет сообщение в оконную процедуру. Оконная процедура - это большой оператор switch / case, где вы проверяете сообщения WM_) вы знаете и обрабатываете их как-то (вы рисуете окно дляWM_PAINT, вы закрыли заявку наWM_QUIT и так далее)

А теперь представь, что у тебя есть рабочая тема, как ты можешьвызо твоя основная тема? Простейший способ - использовать эту базовую структуру, чтобы добиться цели. Я упрощаю задачу, но это шаги:

Создайте (потокобезопасную) очередь функций для вызова (некоторые примеры здесь на SO). Поместите пользовательское сообщение в оконную процедуру. Если вы сделаете эту очередь приоритетной, то вы даже можете определить приоритет этих вызовов (например, уведомление о ходе выполнения из рабочего потока может иметь более низкий приоритет, чем уведомление о тревоге). В оконной процедуре (внутри оператора switch / case) выПонима это сообщение, то вы можете просмотреть функцию для вызова из очереди и для ее вызова.

Both WPF и WinForms используют этот метод для доставки (отправки) сообщения из потока в поток пользовательского интерфейса. Посмотрите на эта статья на MSDN для получения более подробной информации о нескольких потоках и пользовательском интерфейсе, WinForms скрывает многие из этих деталей, и вам не нужно заботиться о них, но вы можете взглянуть, чтобы понять, как это работает под капотом.

0

используя следующее. Это сочетание предложенных предложений:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    //instance variables of the form
    System.Timers.Timer myTimer;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    static int tickLength = 100; 
    static int elapsedCounter;
    private int MaxTime = 5000;
    private TimeSpan elapsedTime; 
    private readonly DateTime startTime = DateTime.Now; 
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


    public AirportParking()
    {
        InitializeComponent();
        lblValue.Text = EvenText;
        keepingTime();
    }

    //method for keeping time
    public void keepingTime() {

    using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength))
    {  
           myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
           myTimer.AutoReset = true;
           myTimer.Enabled = true;
           myTimer.Start(); 
    }  

    }

    private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        elapsedCounter++;
        elapsedTime = DateTime.Now.Subtract(startTime);

        if (elapsedTime.TotalMilliseconds < MaxTime) 
        {
            this.BeginInvoke(new MethodInvoker(delegate
            {
                this.lblElapsedTime.Text = elapsedTime.ToString();

                if (elapsedCounter % 2 == 0)
                    this.lblValue.Text = EvenText;
                else
                    this.lblValue.Text = OddText;
            })); 
        } 
        else {myTimer.Stop();}
      }
  }
}
ahhh - так что даже когда я запускал приложение, и оно казалось остановленным ... оно все еще работало в своем собственном потоке. Я добавлю еще сейчас. Лучше в будущем, если я когда-нибудь буду использовать эти Таймеры, чтобы всегда думать о том, когда они остановятся. whytheq
Использование будет работать здесь, хотя обычно это хороший вариант. Как только блок использования завершен, таймер будет немедленно уничтожен, поэтому, вероятно, никогда не сработает.Form наследуетScrollableControl который реализуетIDisposable уже так, я думаю, вы должны расширить базовую реализациюIDisposable явно вызвать распоряжение наmyTimer. Jodrell
Два последних комментария, теперь, когда вы оставляете таймер включенным (что хорошо), вы, вероятно, должны иметьelse в обработчике, чтобы остановить таймер. Кроме того,Timer класс реализуетIDisposable так что вы должны избавиться от таймера / Stackoverflow.com вопросы / 475763 / ... Я оставлю дальнейшие комментарии для другого вопроса Jodrell
@ Jodrell - я добавил структуру 'using' вKeepingTim() method ... не уверен, будет ли это означать, что вышесказанное сработает при попаданииelse {myTimer.Stop();} ? whytheq
1

в Windows Forms (и большинстве фреймворков) доступ к элементу управления возможен только (если не задокументировано как «потокобезопасный») потоком пользовательского интерфейса.

Такthis.lblElapsedTime.Text = ... в вашем обратном вызове совершенно неправильно. Взгляни на Control.BeginInvoke.

Во-вторых, вы должны использовать System.DateTime а также System.TimeSpan для ваших временных расчетов.

Untested:

DateTime startTime = DateTime.Now;

void myTimer_Elapsed(...) {
  TimeSpan elapsed = DateTime.Now - startTime;
  this.lblElapsedTime.BeginInvoke(delegate() {
    this.lblElapsedTime.Text = elapsed.ToString();
  });
}
Извините, если "просто неправильно" было немного обидно. Английский такой хитрый ... Nicolas Repiquet
не беспокоится, Николас - не первый раз кто-то помещает меня в мое место новичка и не будет последним whytheq
Я осозналthis.lblElapsed... был неправильно когда компилятор сообщил мне об ошибке по поводу неправильного потока! Спасибо за совет в отношении правильного использования объектов Tim, высоко ценится (+1) whytheq

Похожие вопросы