#c_sharp #winforms #многопоточность
Есть основной поток, в котором я создаю все Controls в winforms. И есть второй поток, который вызывается через срабатывание события. В результате этого события мне нужно изменить значение DataSource у DataGridView. В результате возникает ошибка, что доступ к контролу пытается получить не основной поток, в котором был создан контрол. Решением является использование методов Invoke\BeginInvoke. Но саму суть я не понимаю этих методов и как их реализовать в коде. В создаваемом втором потоке используется функция доступа к dgv private void RefreshTables() { try { if(con.State == ConnectionState.Closed) { con.Open(); } sql = "select rowid, * from OpenPos"; adapOpenPos = new SQLiteDataAdapter(sql, con); dsOpenPos = new DataSet(); adapOpenPos.Fill(dsOpenPos); dataGridView1.DataSource = dsOpenPos.Tables[0]; dataGridView1.Columns[0].Visible = false; dataGridView1.Columns[15].Visible = false; dataGridView1.Update(); con.Close(); if (con.State == ConnectionState.Closed) { con.Open(); } sql = "select rowid, * from ClosePos"; adapClosePos = new SQLiteDataAdapter(sql, con); dsClosePos = new DataSet(); adapClosePos.Fill(dsClosePos); dataGridView2.DataSource = dsClosePos.Tables[0]; dataGridView2.Columns[0].Visible = false; dataGridView2.Columns[14].Visible = false; dataGridView2.Update(); con.Close(); } catch (Exception e) { MessageBox.Show(e.Message); } }
Ответы
Ответ 1
Суть метода Invoke довольно проста - он принимает делегат и выполняет его в том потоке, в котором был создан элемент управления, у которого вызывается Invoke. Как вы могли заметить, если обращаться к контролам в WinForms не из того потока, в котором они были созданы, будет выброшено исключение. Соответственно, метод Invoke полезен в случаях, когда необходимо работать с контролом из других потоков. Метод BeginInvoke делает то же самое, но асинхронно. Небольшой пример использования Invoke: private void ButtonInvoke_Click(object sender, EventArgs e) { var myThread = new Thread(ThreadFunction); myThread.Start(); //метод выполняется в другом потоке } private void ThreadFunction() { Thread.Sleep(1000); Action action = () => listBox1.Items.Add("value"); // Свойство InvokeRequired указывает, нeжно ли обращаться к контролу с помощью Invoke if (InvokeRequired) Invoke(action); else action(); } Стоит также отметить, что async/await, добавленные в C# 5, позволяют обойтись без Invoke: private async void ButtonAsync_Click(object sender, EventArgs e) { listBox1.Items.Add("first"); await Task.Run(async () => { await Task.Delay(1000); }); // этот код будет продолжен в UI потоке, // и здесь нет необходимости использовать Invoke listBox1.Items.Add("second"); }Ответ 2
В оконных приложениях Windows существует так называемый поток пользовательского интерфейса (UI thread), по сути — основной поток приложения, создаваемый самым первым. Он выполняет обработку оконных сообщений (Windows messages), вызывая в цикле функции GetMessage или PeekMessage. Если вы хотите что-то сделать с пользовательским интерфейсом, например, изменить строку в поле ввода, вы посылаете ему оконное сообщение. В WinForms изменение текста в поле ввода выполняется с помощью присваивания: textBox1.Text = "Новый текст"; но на низком уровне этот код приводит к отсылке оконного сообщения WM_SETTEXT окну с дескриптором textbox1.Handle при помощи функции SendMessage. Тонкость заключается в том, что отправив сообщение, вы можете захотеть получить результат. Это неочевидно для WM_SETTEXT, но для WM_GETTEXT вы точно захотите узнать ответ. И это означает, что если вы вызвали SendMessage, вы обязаны дождаться завершения функции. Это не представляет большой проблемы, если вы вызываете SendMessage, когда у вас всего один поток, потому что SendMessage вызывает обработчик, который, как правило, обрабатывает оконное сообщение достаточно быстро. Но что, если вы вызовете SendMessage из другого потока? Вопреки расхожему мнению, это не приведёт к ошибке. Вы можете это сделать, но вызывающий поток будет остановлен до тех пор, пока SendMessage не завершится. Если в программе работают несколько потоков, и они захотят что-то изменить в пользовательском интерфейсе, они будут приостановлены, и каждый их SendMessage будет выполняться основным потоком последовательно. В современной оконной программе вы почти наверняка не работаете с потоками напрямую, а используете абстракцию более высокого уровня: асинхронные функции обратного вызова. В .NET вы можете использовать удобную обёртку, которая называется TPL и предоставляет вам классы Task и Task, чтобы не приходилось разбираться с асинхронными функциями. Асинхронные функции работают поверх потоков. Для их работы .NET создаёт несколько потоков в пуле, каждый из которых висит в бесконечном цикле и ждёт сигналов от вашего приложения. Как только возникает сигнал, какой-то поток из пула выполняет вашу функцию обратного вызова и снова засыпает. И всё бы хорошо, но часто такие небольшие функции что-то хотят сделать с вашим пользовательским интерфейсом. Например, вы обрабатываете большой файл и просите Windows вызывать вашу функцию, когда будут прочитаны очередные 64Кб. Блок прочитан, Windows будит поток и передаёт ему вашу функцию на выполнение. Функция делает полезную работу, а потом хочет увеличить индикатор процесса. Она вызывает SendMessage, который может конфликтовать с другими асинхронными функциями. А если асинхронные функции начинают ждать друг друга, соответствующие потоки оказываются заняты, и программе, которая заведует пулом потоков, приходится создавать новые. Это проблема, так что Windows запрещает вызывать SendMessage из других потоков при асинхронных вызовах. Возникает вопрос, как эту проблему решить. Разработчики Windows предлагают разбить вашу асинхронную функцию на две. Первая делает полезную работу и потом говорит пулу потоков: вызови вот эту вот вторую асинхронную функцию, но сделай это в основном потоке. Вторая функция обновляет индикатор процесса, и, поскольку пул потоков вызывает её в основном потоке, никаких конфликтов не происходит. Да, обновление интерфейса всё ещё происходит последовательно, но все остальные асинхронные функции совершенно не мешают друг другу. Для того, чтобы выполнить функцию в потоке пользовательского интерфейса разработчики WinForms предоставили методы Invoke и BeginInvoke. Использовать их можно, например, так: if(con.State == ConnectionState.Closed) { con.Open(); } sql = "select rowid, * from OpenPos"; adapOpenPos = new SQLiteDataAdapter(sql, con); dsOpenPos = new DataSet(); adapOpenPos.Fill(dsOpenPos); // здесь соединение можно закрыть, потому что метод `adapOpenPos.Fill` // уже загрузил данные con.Close(); if (InvokeRequired) { Invoke((MethodInvoker) delegate { dataGridView1.DataSource = dsOpenPos.Tables[0]; dataGridView1.Columns[0].Visible = false; dataGridView1.Columns[15].Visible = false; dataGridView1.Update(); }); } else { dataGridView1.DataSource = dsOpenPos.Tables[0]; dataGridView1.Columns[0].Visible = false; dataGridView1.Columns[15].Visible = false; dataGridView1.Update(); } Здесь параметр Invoke это анонимный делегат, то есть в действительности функция, которая будет вызвана из потока пользовательского интерфейса. Есть несколько способов упростить этот код (ищите на Stack Overflow). Отмечу основную идею: код располагается внутри одного из методов вашей формы, соответственно, InvokeRequired и Invoke это свойство и метод формы. Вызывать изменение свойств через Invoke нужно только в том случае, когда свойство InvokeRequired истинно. Перед вызовом полезно подготовить все данные, которые вам потребуется использовать, чтобы не тормозить поток пользовательского интерфейса. Сначала готовите данные, а потом присваиваете их через свойства элементов управления внутри Invoke/BeginInvoke — таков основной паттерн асинхронного программирования десктопных приложений. Ответ 3
Я попробую ответить как можно проще для понимания, так сказать в дополнение к другим ответам. Создавая новый поток Вы ему передаете некий код, который должен будет выполниться в этом новом потоке. Для примера при создании через Task это выглядело бы так. new Task(() => { //тут код, который выполнится в новом потоке }).Start(); Метод Invoke же служит наоборот окошком в основной поток, в остальном он объявляется почти точно так же. new Task(() => { //побочный поток this.Invoke(new Action(() => { //это окошко обратно в главный поток //код написанный тут выполнится в главном потоке не вызывая ошибок })); //дальше идет снова побочный поток }).Start(); Изменение свойств и вызов методов контролов должен проходить в главном потоке и оборачиваться Invoke. Есть еще один способ, который иногда использую я, это касается изменений после основных операций. Может пригодится когда-нибудь и Вам, я его использую для блокировки/разблокировки интерфейса на время операций. Task task = new Task(() => { //основные операции }); task.ContinueWith(_ => { //обновление контролов, будет вызвано после выполнения предыдущего кода }, TaskScheduler.FromCurrentSynchronizationContext()); task.Start();
Комментариев нет:
Отправить комментарий