Операція перехресних потоків не вірна: керування доступ здійснюється з потоку, відмінного від потоку, на якому він створений


584

У мене є сценарій. (Форми Windows, C #, .NET)

  1. Існує основна форма, в якій розміщено деякий контроль користувачів.
  2. Управління користувачем виконує деякі важкі операції з даними, так що якщо я безпосередньо викликаю UserControl_Loadметод, інтерфейс користувача не відповідає на тривалість виконання методу навантаження.
  3. Щоб подолати це, я завантажую дані на різні потоки (намагаюся змінити існуючий код якомога менше)
  4. Я використовував фоновий робочий потік, який буде завантажувати дані, і коли це завершиться, сповістить програму, що вона виконала свою роботу.
  5. Тепер виникла справжня проблема. Весь інтерфейс користувача (основна форма та його дочірні користувальницькі контролі) був створений на основній основній темі. У методі LOAD користувачаcontcontrol я отримую дані на основі значень деякого контролю (наприклад, текстового поля) на userControl.

Псевдокод виглядатиме так:

КОД 1

UserContrl1_LoadDataMethod()
{
    if (textbox1.text == "MyName") // This gives exception
    {
        //Load data corresponding to "MyName".
        //Populate a globale variable List<string> which will be binded to grid at some later stage.
    }
}

Виняток, який він дав, був

Операція перехресних потоків не вірна: керування доступ здійснюється з потоку, відмінного від потоку, на якому він створений.

Щоб дізнатись більше про це, я зробив деякий гуглінг, і з’явилася пропозиція, якби використати наступний код

КОД 2

UserContrl1_LoadDataMethod()
{
    if (InvokeRequired) // Line #1
    {
        this.Invoke(new MethodInvoker(UserContrl1_LoadDataMethod));
        return;
    }

    if (textbox1.text == "MyName") // Now it wont give an exception
    {
    //Load data correspondin to "MyName"
        //Populate a globale variable List<string> which will be binded to grid at some later stage
    }
}

АЛЕ АЛЕ, АЛЕ ... здається, я повернувся до квадратного. Додаток знову стає безвідповідальним. Здається, це пов'язано із виконанням рядка №1, якщо це умова. Завдання з завантаження знову виконується батьківською ниткою, а не третьою, яку я породив.

Я не знаю, сприйняв я це правильно чи неправильно. Я новачок у нитці.

Як я вирішую це, а також який ефект від виконання рядка №1, якщо блокувати?

Ситуація така : я хочу завантажити дані в глобальну змінну на основі значення елемента керування. Я не хочу змінювати значення контролю з дочірньої нитки. Я не збираюся робити це ніколи з дитячої нитки.

Отже, лише доступ до значення, щоб відповідні дані можна було отримати з бази даних.


Для мого конкретного випадку цієї помилки я виявив, що вирішення полягає у використанні BackgroundWorker на формі для обробки частин коду, що потребують даних. (Тобто поставити все проблемного коду в метод backgroundWorker1_DoWork () і викликати його через backgroundWorker1.RunWorkerAsync ()) ... Ці два джерела вказав мені в правильному напрямку: stackoverflow.com/questions/4806742 / ... youtube.com/ дивитися? v = MLrrbG6V1zM
Giollia

Відповіді:


433

Відповідно до коментаря до оновлення Prerak K (оскільки видалено):

Напевно, я не представив питання належним чином.

Ситуація така: я хочу завантажити дані в глобальну змінну на основі значення елемента керування. Я не хочу змінювати значення контролю з дочірньої нитки. Я не збираюся робити це ніколи з дитячої нитки.

Отже, лише доступ до значення, щоб відповідні дані можна було отримати з бази даних.

Рішення, яке ви хочете тоді, має виглядати так:

UserContrl1_LOadDataMethod()
{
    string name = "";
    if(textbox1.InvokeRequired)
    {
        textbox1.Invoke(new MethodInvoker(delegate { name = textbox1.text; }));
    }
    if(name == "MyName")
    {
        // do whatever
    }
}

Виконайте серйозну обробку в окремому потоці перед тим, як спробувати перейти назад до потоку управління. Наприклад:

UserContrl1_LOadDataMethod()
{
    if(textbox1.text=="MyName") //<<======Now it wont give exception**
    {
        //Load data correspondin to "MyName"
        //Populate a globale variable List<string> which will be
        //bound to grid at some later stage
        if(InvokeRequired)
        {
            // after we've done all the processing, 
            this.Invoke(new MethodInvoker(delegate {
                // load the control with the appropriate data
            }));
            return;
        }
    }
}

1
З часу, коли я займався програмуванням на C #, минуло певний час, але на основі статті MSDN та моїх відокремлених знань це виглядає так.
Джефф Хаббард

1
Різниця полягає в тому, що BeginInvoke () є асинхронним, тоді як Invoke () працює синхронно. stackoverflow.com/questions/229554 / ...
frzsombor

178

Модель потоків в інтерфейсі

Будь ласка, прочитайте модель потоку в додатках інтерфейсу, щоб зрозуміти основні поняття. Посилання переходить на сторінку, яка описує модель внизу WPF. Однак Windows Forms використовує ту саму ідею.

Нитка інтерфейсу користувача

  • Є лише один потік (потік інтерфейсу користувача), якому дозволений доступ до System.Windows.Forms.Control та його членів підкласів.
  • Спроба отримати доступ до члена System.Windows.Forms.Control з іншого потоку, ніж потік інтерфейсу, спричинить виключення крос-потоку.
  • Оскільки є лише один потік, усі операції з користувальницьким інтерфейсом ставляться в чергу як робочі елементи в цей потік:

введіть тут опис зображення

  • Якщо для потоку користувальницького інтерфейсу немає роботи, то існують пробіли в режимі очікування, які можна використовувати для обчислень, що не належать до інтерфейсу користувача.
  • Для використання зазначених прогалин використовуйте System.Windows.Forms.Control.Invoke або System.Windows.Forms.Control.BeginInvoke :

введіть тут опис зображення

Методи BeginInvoke та Invoke

  • Витрати на обчислення методу, що викликається, повинні бути невеликими, а також обчислення накладних методів обробника подій, оскільки там використовується нитка інтерфейсу користувача - та сама, що відповідає за обробку вводу користувача. Незалежно, чи це System.Windows.Forms.Control.Invoke або System.Windows.Forms.Control.BeginInvoke .
  • Для виконання обчислювальної дорогої операції завжди використовуйте окрему нитку. Оскільки .NET 2.0 BackgroundWorker присвячений виконанню обчислень дорогих операцій у Windows Forms. Однак у нових рішеннях слід використовувати шаблон async-wait, як описано тут .
  • Використовуйте методи System.Windows.Forms.Control.Invoke або System.Windows.Forms.Control.BeginInvoke лише для оновлення інтерфейсу користувача. Якщо ви використовуєте їх для великих обчислень, ваша програма заблокує:

введіть тут опис зображення

Викликати

введіть тут опис зображення

ПочатокЗаклик

введіть тут опис зображення

Рішення коду

Прочитайте відповіді на запитання Як оновити графічний інтерфейс з іншого потоку в C #? . Для C # 5.0 і .NET 4.5 рекомендованим рішення тут .



72

Ви хочете використовувати Invokeабо BeginInvokeдля мінімальної роботи, необхідної для зміни інтерфейсу користувача. Ваш "важкий" метод повинен виконуватися в іншому потоці (наприклад, через BackgroundWorker), але потім використовувати Control.Invoke/ Control.BeginInvokeпросто для оновлення інтерфейсу користувача. Таким чином, ваш потік інтерфейсу може вільно обробляти події інтерфейсу тощо.

Дивіться мою статтю про нарізку на прикладі WinForms - хоча стаття була написана ще BackgroundWorkerдо того, як приїхала на сцену, і, боюся, я її не оновлював у цьому відношенні. BackgroundWorkerпросто трохи спрощує зворотний виклик.


ось у цьому моєму стані. я навіть не змінюючи інтерфейс користувача. Я просто отримую доступ до його поточних значень з дочірньої нитки. будь-яка пропозиція hw здійснити
Prerak K

1
Вам все одно потрібно перейти на потік інтерфейсу користувача, навіть лише для доступу до властивостей. Якщо ваш метод не може продовжуватись до отримання доступу до значення, ви можете використовувати делегат, який повертає значення. Але так, перейдіть через потік інтерфейсу користувача.
Джон Скіт

Привіт Джоне, я вірю, що ти спрямовуєш мене в потрібне русло. Так, мені потрібне значення без нього, я не можу продовжувати далі. Будь ласка, чи можете ви докладніше розробити тему "Використання делегата, який повертає значення". Спасибі
Prerak K

1
Використовуйте такий делегат, як Func <string>: string text = textbox1.Invoke ((Func <string>) () => textbox1.Text); (Це припущення, що ви використовуєте C # 3.0 - ви можете використовувати анонімний метод інакше.)
Джон Скіт,

45

Зараз я знаю, що це занадто пізно. Однак навіть сьогодні, якщо у вас виникають проблеми з доступом до елементів керування поперечними потоками? Це найкоротша відповідь на сьогодні: P

Invoke(new Action(() =>
                {
                    label1.Text = "WooHoo!!!";
                }));

Ось як я отримую доступ до будь-якого контролю форми з потоку.


1
Це дає мені Invoke or BeginInvoke cannot be called on a control until the window handle has been created. Я вирішив це тут
rupweb

42

У мене була ця проблема з FileSystemWatcherі виявив, що наступний код вирішив проблему:

fsw.SynchronizingObject = this

Потім елемент керування використовує об'єкт поточної форми для обробки подій, і тому буде в одній темі.


2
Це врятувало моє бекон. У VB.NET я використовував.SynchronizingObject = Me
кодування кодування

20

Я знаходжу код перевірки та виклику, який потрібно засвоїти у всіх методах, пов’язаних із формами, щоб бути занадто багатослівним та непотрібним. Ось простий метод розширення, який дозволяє повністю усунути це:

public static class Extensions
{
    public static void Invoke<TControlType>(this TControlType control, Action<TControlType> del) 
        where TControlType : Control
        {
            if (control.InvokeRequired)
                control.Invoke(new Action(() => del(control)));
            else
                del(control);
    }
}

І тоді ви можете просто зробити це:

textbox1.Invoke(t => t.Text = "A");

Більше не возитися - просто.


що тут 't'
Рават

@Rawat tв цьому випадку буде textbox1- це передається як аргумент
Роб

17

Елементи управління в .NET, як правило, не є безпечними для потоків. Це означає, що ви не повинні отримувати доступ до елемента керування з потоку, відмінного від того, де він живе. Щоб обійти це, вам потрібно зателефонувати елемент керування, саме це намагається ваш другий зразок.

Однак у вашому випадку все, що ви зробили, - це передати тривалий метод назад до основної нитки. Звичайно, це насправді не те, що ти хочеш робити. Вам потрібно переосмислити це, щоб усе, що ви робите на головній нитці, - це налаштування швидкої властивості тут і там.



10

Новий вигляд з використанням Async / Await та зворотних дзвінків. Вам потрібен лише один рядок коду, якщо ви зберігаєте метод розширення у своєму проекті.

/// <summary>
/// A new way to use Tasks for Asynchronous calls
/// </summary>
public class Example
{
    /// <summary>
    /// No more delegates, background workers etc. just one line of code as shown below
    /// Note it is dependent on the XTask class shown next.
    /// </summary>
    public async void ExampleMethod()
    {
        //Still on GUI/Original Thread here
        //Do your updates before the next line of code
        await XTask.RunAsync(() =>
        {
            //Running an asynchronous task here
            //Cannot update GUI Thread here, but can do lots of work
        });
        //Can update GUI/Original thread on this line
    }
}

/// <summary>
/// A class containing extension methods for the Task class 
/// Put this file in folder named Extensions
/// Use prefix of X for the class it Extends
/// </summary>
public static class XTask
{
    /// <summary>
    /// RunAsync is an extension method that encapsulates the Task.Run using a callback
    /// </summary>
    /// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
    /// <returns></returns>
    public async static Task RunAsync(Action Code)
    {
        await Task.Run(() =>
        {
            Code();
        });
        return;
    }
}

До методу розширення ви можете додати інші речі, такі як загортання його в оператор Try / Catch, що дозволяє абоненту повідомити йому, який тип повернути після завершення, зворотний виклик виклику абоненту:

Додавання спробуйте Catch, Автоматичний журнал виключень та CallBack

    /// <summary>
    /// Run Async
    /// </summary>
    /// <typeparam name="T">The type to return</typeparam>
    /// <param name="Code">The callback to the code</param>
    /// <param name="Error">The handled and logged exception if one occurs</param>
    /// <returns>The type expected as a competed task</returns>

    public async static Task<T> RunAsync<T>(Func<string,T> Code, Action<Exception> Error)
    {
       var done =  await Task<T>.Run(() =>
        {
            T result = default(T);
            try
            {
               result = Code("Code Here");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Unhandled Exception: " + ex.Message);
                Console.WriteLine(ex.StackTrace);
                Error(ex);
            }
            return result;

        });
        return done;
    }
    public async void HowToUse()
    {
       //We now inject the type we want the async routine to return!
       var result =  await RunAsync<bool>((code) => {
           //write code here, all exceptions are logged via the wrapped try catch.
           //return what is needed
           return someBoolValue;
       }, 
       error => {

          //exceptions are already handled but are sent back here for further processing
       });
        if (result)
        {
            //we can now process the result because the code above awaited for the completion before
            //moving to this statement
        }
    }

10

Це не рекомендований спосіб вирішити цю помилку, але ви можете її швидко придушити, вона зробить роботу. Я вважаю за краще це для прототипів або демонстрацій. додати

CheckForIllegalCrossThreadCalls = false

в Form1()конструкторі.


9

Дотримуйтесь найпростішого (на мою думку) способу модифікації об'єктів з іншої теми:

using System.Threading.Tasks;
using System.Threading;

namespace TESTE
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Action<string> DelegateTeste_ModifyText = THREAD_MOD;
            Invoke(DelegateTeste_ModifyText, "MODIFY BY THREAD");
        }

        private void THREAD_MOD(string teste)
        {
            textBox1.Text = teste;
        }
    }
}

Просто! Дякую .
Алі Есмаїлі


7

Я виявив потребу в цьому під час програмування контролера додатків для монохроматики iOS-Phone у візуальному проектному проекті, який формує winforms поза xamarin stuidio. Віддаючи перевагу програмі в VS над студією xamarin, я хотів, щоб контролер був повністю відірваний від рамки телефону. Таким чином реалізація цього для інших рамок, таких як Android та Windows Phone, була б набагато простішою для подальшого використання.

Мені хотілося рішення, де графічний інтерфейс міг реагувати на події без тягаря поводження з кодом перемикання перехресних ниток за кожним натисканням кнопки. По суті, дозвольте контролеру класу впоратися з цим, щоб простий код клієнта. Ви можете, можливо, мати багато подій на графічному інтерфейсі, де, як якщо б ви могли обробити це в одному місці в класі, було б чистіше. Я не є експертом із багаторазових досліджень, дайте мені знати, чи є це недоліком.

public partial class Form1 : Form
{
    private ExampleController.MyController controller;

    public Form1()
    {          
        InitializeComponent();
        controller = new ExampleController.MyController((ISynchronizeInvoke) this);
        controller.Finished += controller_Finished;
    }

    void controller_Finished(string returnValue)
    {
        label1.Text = returnValue; 
    }

    private void button1_Click(object sender, EventArgs e)
    {
        controller.SubmitTask("Do It");
    }
}

Форма GUI не знає, що контролер виконує асинхронні завдання.

public delegate void FinishedTasksHandler(string returnValue);

public class MyController
{
    private ISynchronizeInvoke _syn; 
    public MyController(ISynchronizeInvoke syn) {  _syn = syn; } 
    public event FinishedTasksHandler Finished; 

    public void SubmitTask(string someValue)
    {
        System.Threading.ThreadPool.QueueUserWorkItem(state => submitTask(someValue));
    }

    private void submitTask(string someValue)
    {
        someValue = someValue + " " + DateTime.Now.ToString();
        System.Threading.Thread.Sleep(5000);
//Finished(someValue); This causes cross threading error if called like this.

        if (Finished != null)
        {
            if (_syn.InvokeRequired)
            {
                _syn.Invoke(Finished, new object[] { someValue });
            }
            else
            {
                Finished(someValue);
            }
        }
    }
}

6

Ось альтернативний спосіб, якщо об’єкт, з яким ви працюєте, не має

(InvokeRequired)

Це корисно, якщо ви працюєте з основною формою в класі, відмінному від основної форми, з об'єктом, який знаходиться в основній формі, але не має InvokeRequired

delegate void updateMainFormObject(FormObjectType objectWithoutInvoke, string text);

private void updateFormObjectType(FormObjectType objectWithoutInvoke, string text)
{
    MainForm.Invoke(new updateMainFormObject(UpdateObject), objectWithoutInvoke, text);
}

public void UpdateObject(ToolStripStatusLabel objectWithoutInvoke, string text)
{
    objectWithoutInvoke.Text = text;
}

Він працює так само, як вище, але це інший підхід, якщо у вас немає об'єкта з потрібними посиланнями, але у вас є доступ до MainForm


5

З тієї ж лінії, що і попередні відповіді, але дуже коротке доповнення, що дозволяє використовувати всі властивості Control, не маючи винятку виклику поперечних потоків.

Хелперний метод

/// <summary>
/// Helper method to determin if invoke required, if so will rerun method on correct thread.
/// if not do nothing.
/// </summary>
/// <param name="c">Control that might require invoking</param>
/// <param name="a">action to preform on control thread if so.</param>
/// <returns>true if invoke required</returns>
public bool ControlInvokeRequired(Control c, Action a)
{
    if (c.InvokeRequired) c.Invoke(new MethodInvoker(delegate
    {
        a();
    }));
    else return false;

    return true;
}

Використання зразка

// usage on textbox
public void UpdateTextBox1(String text)
{
    //Check if invoke requied if so return - as i will be recalled in correct thread
    if (ControlInvokeRequired(textBox1, () => UpdateTextBox1(text))) return;
    textBox1.Text = ellapsed;
}

//Or any control
public void UpdateControl(Color c, String s)
{
    //Check if invoke requied if so return - as i will be recalled in correct thread
    if (ControlInvokeRequired(myControl, () => UpdateControl(c, s))) return;
    myControl.Text = s;
    myControl.BackColor = c;
}


5

Наприклад, щоб отримати текст з управління потоком інтерфейсу користувача:

Private Delegate Function GetControlTextInvoker(ByVal ctl As Control) As String

Private Function GetControlText(ByVal ctl As Control) As String
    Dim text As String

    If ctl.InvokeRequired Then
        text = CStr(ctl.Invoke(
            New GetControlTextInvoker(AddressOf GetControlText), ctl))
    Else
        text = ctl.Text
    End If

    Return text
End Function

3

Те саме питання: як оновити-the-gui-from-another-thread-in-c

Два способи:

  1. Повернути значення в e.result і використовувати його для встановлення значення текстового поля у фоновому режиміWorker_RunWorkerCompleted подія

  2. Зазначте деяку змінну, щоб містити ці значення в окремому класі (який буде працювати власником даних). Створіть статичний екземпляр цього класу adn, ви можете отримати доступ до нього через будь-яку нитку.

Приклад:

public  class data_holder_for_controls
{
    //it will hold value for your label
    public  string status = string.Empty;
}

class Demo
{
    public static  data_holder_for_controls d1 = new data_holder_for_controls();
    static void Main(string[] args)
    {
        ThreadStart ts = new ThreadStart(perform_logic);
        Thread t1 = new Thread(ts);
        t1.Start();
        t1.Join();
        //your_label.Text=d1.status; --- can access it from any thread 
    }

    public static void perform_logic()
    {
        //put some code here in this function
        for (int i = 0; i < 10; i++)
        {
            //statements here
        }
        //set result in status variable
        d1.status = "Task done";
    }
}



0

Простий і повторно використаний спосіб подолати цю проблему.

Спосіб розширення

public static class FormExts
{
    public static void LoadOnUI(this Form frm, Action action)
    {
        if (frm.InvokeRequired) frm.Invoke(action);
        else action.Invoke();
    }
}

Використання зразка

private void OnAnyEvent(object sender, EventArgs args)
{
    this.LoadOnUI(() =>
    {
        label1.Text = "";
        button1.Text = "";
    });
}

-3

Існує два варіанти операцій з поперечними потоками.

Control.InvokeRequired Property 

друге - використовувати

SynchronizationContext Post Method

Control.InvokeRequired корисний лише тоді, коли робочі елементи управління, успадковані від класу Control, тоді як SynchronizationContext можна використовувати в будь-якому місці. Деякі корисні відомості - це наступні посилання

Інтерфейс оновлення поперечної нитки | .Нет

Інтерфейс оновлення перехресної нитки за допомогою SynchronizationContext | .Нет

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.