Проблема с многопоточностью, когда моно зависает, а MS.Net - нет - PullRequest
6 голосов
/ 27 апреля 2011

Я тестирую свое приложение с моно в предвидении порта Linux, и у меня проблема с многопоточностью.Первоначально я рассматривал вставку 3000 строк кода здесь, но, наконец, я придумал небольшой минимальный пример;)

У вас есть форма с кнопкой (поэтично названная Button1, и метка (которая несет, без удивления), имя Label1)).Весь мир живет счастливой жизнью в форме под названием Form1.Нажатие Button1 запускает бесконечный цикл, который увеличивает локальный счетчик и обновляет Label1 (используя Invoke), чтобы отразить его значение.

Теперь в Mono, если вы измените размер формы, метка перестает обновляться,никогда не перезапускать.Этого не происходит с реализацией MS.BeginInvoke не работает лучше;хуже того, в обоих случаях пользовательский интерфейс зависает.

Знаете ли вы, откуда возникает это несоответствие?Как бы вы решили это?И, наконец, почему BeginInvoke не работает здесь?Я, должно быть, делаю огромную ошибку ... но что?


РЕДАКТИРОВАТЬ : Некоторый прогресс до сих пор:
  • Вызов BeginInvoke действительно работает;только пользовательский интерфейс не обновляется достаточно быстро, поэтому он, кажется, останавливается.
  • В моно происходит то, что весь поток зависает при вставке сообщения в очередь пользовательского интерфейса (например, путем изменения размера формы).На самом деле синхронный вызов Invoke никогда не возвращается.Я пытаюсь понять, почему.
  • Интересно: даже при использовании BeginInvoke асинхронные вызовы не выполняются до завершения операции изменения размера.В MS.Net они продолжают работать при изменении размера.

Код выглядит следующим образом (версия C # ниже):

Public Class Form1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim T As New Threading.Thread(AddressOf Increment)
        T.Start()
    End Sub

    Sub UpdateLabel(ByVal Text As String)
        Label1.Text = Text
    End Sub

    Delegate Sub UpdateLabelHandler(ByVal Text As String)
    Sub Increment()
        Dim i As Long = 0
        Dim UpdateLabelDelegate As New UpdateLabelHandler(AddressOf UpdateLabel)
        Try
            While True
                i = (i + 1) Mod (Long.MaxValue - 1)
                Me.Invoke(UpdateLabelDelegate, New Object() {i.ToString})
            End While
        Catch Ex As ObjectDisposedException
        End Try
    End Sub
End Class

Или, в C #,

public class Form1
{
    private void Button1_Click(System.Object sender, System.EventArgs e)
    {
        System.Threading.Thread T = new System.Threading.Thread(Increment);
        T.Start();
    }

    public void UpdateLabel(string Text)
    {
        Label1.Text = Text;
    }

    public delegate void UpdateLabelHandler(string Text);
    public void Increment()
    {
        long i = 0;
        UpdateLabelHandler UpdateLabelDelegate = new UpdateLabelHandler(UpdateLabel);
        try {
            while (true) {
                i = (i + 1) % (long.MaxValue - 1);
                this.Invoke(UpdateLabelDelegate, new object[] { i.ToString() });
            }
        } catch (ObjectDisposedException Ex) {
        }
    }
}

Ответы [ 3 ]

5 голосов
/ 28 апреля 2011

Это ошибка во время выполнения моно, по крайней мере, я так думаю. Код не может быть хорошей практикой (я не специалист по многопоточности), но есть одна вещь, которая предполагает ошибку, это то, что поведение в Windows и Linux отличается.

В Linux mono работает точно так же, как MS.Net в Windows. Нет зависаний, постоянные обновления даже при изменении размера.

В Windows mono отображает все вышеупомянутые проблемы. Я опубликовал сообщение об ошибке на https://bugzilla.novell.com/show_bug.cgi?id=690400.

1 голос
/ 27 апреля 2011

Знаете ли вы, откуда это расхождение?Как бы вы решили это?

Я не уверен.Я не вижу в вашем коде ничего очевидного, что могло бы вызвать разницу между Mono и .NET.Если бы мне пришлось делать дикие предположения, я бы сказал, что есть вероятность, что вы наткнулись на неясную ошибку в Mono.Хотя, я полагаю, возможно, что Mono использует достаточно другой механизм для обработки сообщений WM_PAINT, которые приводят к обновлению формы.Постоянный стук потока пользовательского интерфейса от повторных вызовов к Invoke может нарушать способность Моно обновлять форму.

И, наконец, почему здесь не работает BeginInvoke?

Вызов Invoke в узком цикле достаточно плох, но BeginInvoke будет еще хуже.Рабочий поток загружает насос сообщений пользовательского интерфейса.BeginInvoke не ожидает, пока поток пользовательского интерфейса завершит выполнение делегата.Он просто отправляет запросы и быстро возвращается.Вот почему, кажется, висит.Сообщения, которые BeginInvoke отправляет в очередь сообщений пользовательского интерфейса, продолжают накапливаться, так как рабочий поток, вероятно, серьезно перебивает способность потока пользовательского интерфейса обрабатывать их.

Другие комментарии

Я должен также упомянуть, что рабочий поток почти бесполезен в коде.Причина в том, что у вас есть вызов Invoke на каждой итерации.Invoke блокируется до тех пор, пока пользовательский интерфейс не завершит выполнение делегата.Это означает, что ваш рабочий поток и поток пользовательского интерфейса, по сути, находятся в состоянии блокировки друг с другом.Другими словами, работник тратит большую часть своего времени на ожидание пользовательского интерфейса и наоборот.

Решение

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

Любой, даже лучший, подход состоит в том, чтобы вообще не использовать Invoke или BeginInvoke.Лично я думаю, что эти механизмы для обновления пользовательского интерфейса слишком перегружены.Почти всегда лучше позволить потоку пользовательского интерфейса регулировать собственную частоту обновления, особенно когда рабочий поток выполняет непрерывную обработку.Это означает, что вам нужно будет поместить таймер в форму и установить желаемую частоту обновления.Из события Tick вы будете проверять общую структуру данных, которую обновляет рабочий поток, и использовать эту информацию для обновления элементов управления в форме.Это имеет несколько преимуществ.

  • Это нарушает тесную связь между пользовательским интерфейсом и рабочими потоками, которые навязывает Control.Invoke.
  • Это возлагает ответственность за обновление потока пользовательского интерфейса на поток пользовательского интерфейса.где он должен находиться в любом случае.
  • Поток пользовательского интерфейса должен определять, когда и как часто должно происходить обновление.
  • Нет риска переполнения насоса сообщений пользовательского интерфейса, как в случаес методами маршалинга, инициированными рабочим потоком.
  • Рабочему потоку не нужно ждать подтверждения того, что обновление было выполнено, прежде чем переходить к следующим шагам (т. е. вы получаете большую пропускную способность как в пользовательском интерфейсе, так и врабочие темы).
0 голосов
/ 27 апреля 2011

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

private void Button1_Click(System.Object sender, System.EventArgs e)
{
    Thread t = new Thread(Increment);
    t.IsBackground = true;
    t.Start();
}

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

Третье: если вы обновляете пользовательский интерфейс, проверьте свойство InvokeRequired и вызовите BeginInvoke:

public void UpdateLabel(string Text)
{

    if (InvokeRequired)
    {
        BeginInvoke(new UpdateLabelDelegate(UpdateLabel), Text);
    }
    else
    {
        Label1.Text = Text;
    }
}

public void Increment()
{
    int i = 0;
    while(true)
    {
        i++; // just incrementing i??
        UpdateLabel(i.ToString());

        Thread.Sleep(1000);// slow down a bit so you can see the updates
    }
}

Вы также можете «автоматизировать» шаблон требуемого Invoke: Автоматизировать шаблон кода InvokeRequired

А теперь посмотрим, возникла ли у вас такая же проблема.

Я попробовал его на своей машине, и он работает как шарм:

public partial class Form1 : Form
{
    private delegate void UpdateLabelDelegate(string text);
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Thread t = new Thread(Increment);
        t.IsBackground = true;
        t.Start();
    }

    private void UpdateLabel(string text)
    {
        if (label1.InvokeRequired)
        {
            BeginInvoke(new UpdateLabelDelegate(UpdateLabel), text);
        }
        else
        {
            label1.Text = text;
        }

    }

    private void Increment()
    {
        int i = 0;
        while (true)
        {
            i++;
            UpdateLabel(i.ToString());
            Thread.Sleep(1000);
        }
    }
}
...