Страницы

Поиск по вопросам

понедельник, 9 декабря 2019 г.

Почему делегат из отдельного потока добавляет неправильные данные в UI-поток?

#c_sharp #winforms #многопоточность


В отдельных потоках происходит вычисление, результат которых надо вывести в  форму.
Для передачи данных в UI-поток надо вызывать Control.BeginInvoke

using System.Windows.Forms;
using System.Threading.Tasks;

void addText(Form f, int res) { 
  f.Controls.Add(new TextBox() { Text = res.ToString(), Dock = DockStyle.Top });
}

var f = new Form();
f.Load += (s, e) => {
  Task.Run(() => {
     for(var result=0; result < 5; result++)
        f.BeginInvoke(new Action(() => addText(f, result) ));
  });      
};

f.ShowDialog();


Но в форме выводятся неправильные значения. Во всех TextBox'ах выводится 5.   



Вызываю BeginInvoke из UI-потока

f.Load += (s, e) => {
  var result = "a";
  f.BeginInvoke(new Action(() => addText(f, result) ));
  result = "b";
  f.BeginInvoke(new Action(() => addText(f, result) ));


};

Но в форме также выводятся неправильные значения. В TextBox'ах выводится "b".
Почему так и как вывести правильный результат в форму?
Возможно ли избавиться от вызовов BeginInvoke в разных частях кода?  
    


Ответы

Ответ 1



Альтернативное решение той же проблемы - не использовать одну и ту же переменную для хранения результатов двух разных операций: f.Load += (s, e) => { var resultA = "a"; f.BeginInvoke(new Action(() => addText(f, resultA) )); var resultB = "b"; f.BeginInvoke(new Action(() => addText(f, resultB) )); };

Ответ 2



Вызов BeginInvoke ставит делегат в специальную очередь, которая обрабатывается в UI-потоке. К моменту вызова делегата, в result оказывается новое значение, а не то, которое было в момент вызова BeginInvoke. Если в addText надо передать result, существующий на момент вызова BeginInvoke, то строку f.BeginInvoke(new Action(() => addText(f, result) )); надо заменить на следующую f.BeginInvoke( new Action(v => addText(f, v)), result.ToString() // передается в v в момент вызова делегата ); Т.к. f это ссылка на Form, и мы знаем, что она не изменится, то передавать ее в BeginInvoke не надо. При работе с замыканиями надо соблюдать правило: переменные, которые могут измениться к моменту вызова делегата, должны быть переданы в метод BeginInvoke при его вызове. Вместо того, чтобы вызывать BeginInvoke в разных частях кода можно перенести BeginInvoke в метод addText и вызывать его после проверки InvokeRequired. void addText(Form frm, int res) { if (frm.InvokeRequired) // true - если вызван не из UI-потока frm.BeginInvoke( new Action((f, r) => addText(f, r)), frm, res); // передается в (f, r) else frm.Controls.Add(new TextBox() { Text = res.ToString(), Dock = DockStyle.Top }); } var f = new Form(); f.Load += (s, e) => { Task.Run(() => { for (var result = 0; result< 5; result++) addText(f, result); }); }; f.ShowDialog(); Хотя в методе addText определен вызов addText, но это не рекурсия, т.к. BeginInvoke добавляет делегат вместе с данными в специальную очередь, которая обрабатывается в UI-потоке.

Ответ 3



§Цикл for Наверное, это самый классический пример, который приводят все: public void Run() { var actions = new List(); for (int i = 0; i < 3; i++) actions.Add(() => Console.WriteLine(i)); foreach (var action in actions) action(); } В этом примере сделана типичная ошибка. Начинающие программисты думаю, что этот код выведет "0 1 2", но на самом деле он выведет "3 3 3". Такое странное поведение легко понять, если взглянуть на развёрнутую версию этого метода: public void Run() { var actions = new List(); DisplayClass c = new DisplayClass(); for (c.i = 0; c.i < 3; c.i++) list.Add(c.Action); foreach (Action action in list) action(); } private sealed class DisplayClass { public int i; public void Action() { Console.WriteLine(i); } } В таком случае часто говорят, что переменная замыкается по ссылке, а не по значению. Эту особенность замыканий многие осуждают, как непонятную, хотя она является достаточно логичной для тех, кто хорошо представляет, что скрыто под капотом замыканий. Эту тему очень подробно обсуждает Эрик Липперт в постах О вреде замыканий на переменных цикла и Замыкания на переменных цикла. Часть 2. §Неочевидности в использовании C#-замыканий Замыкания на переменных цикла в C# 5

Комментариев нет:

Отправить комментарий