Программирование PC C#: System.InvalidOperationException: Недопустимая операция в нескольких потоках Thu, November 21 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


C#: System.InvalidOperationException: Недопустимая операция в нескольких потоках Печать
Добавил(а) microsin   

Решение проблемы с ошибкой "System.InvalidOperationException: Недопустимая операция в нескольких потоках: попытка доступа к элементу управления 'имя_элемента_GUI' не из того потока, в котором он был создан." заключается в создании и вызове методов-делегатов. Статья написана на основе перевода статьи Rüdiger Klaehn, оригинал см. в [1].

Проблема обычно возникает, когда необходимо получить доступ на запись к одному и тому же ресурсу асинхронно из нескольких потоков. Например, Вы имеете на форме ListBox, в который задумали выводить сообщения из обработчика события, который обрабатывается в другом потоке. У меня такое произошло, когда понадобилось отображение данных в ListBox, принятых в обработчике события поступления данных по USB.

MultiThreadingIssue01.png

Почему такая проблема возникает: ListBox создается в основном потоке (thread), запускающем GUI окна программы и обрабатывающим сообщения главного окна формы. Другие потоки могут быть созданы для поддержки обмена данными с внешними устройствами (сеть Ethernet, COM-порт, USB и т. п.). Эти другие потоки могут обрабатывать события приема данных асинхронно с выполнением основного потока. Поэтому попытка разными потоками что-то записать в ListBox может привести к конфликту с записью в ListBox данных из основного потока GUI интерфейса программы.

Как проблему можно решить: для ListBox создаются подпрограммы (методы), через которые выводятся данные в ListBox. Данные выводятся в ListBox только через эти подпрограммы, больше никак. Эти подпрограммы делаются защищенными (thread-safe) для вызова из любых потоков через delegate, InvokeRequired, Invoke.

[Введение]

Программирование интерфейса пользователя через Windows Forms довольно простое, пока вы не используете несколько потоков. Однако всякий раз, когда Вашему приложению нужно выполнить какую-нибудь фактическую работу, становится необходимым использовать поточную обработку (threading), т. е. создать дополнительный поток (thread) или потоки, чтобы обеспечить работоспособность пользовательского интерфейса (скорость отклика UI). В этом случае программирование Windows Forms может стать довольно сложным.

[Описание проблемы]

Вы наверное уже знаете, что Windows Forms главным образом не являются потокозащищенными (not thread safe). Например, Вы не можете безопасно прочитать (get) или установить (set) свойство объекта управления (control) Windows.Forms из любого потока, за исключением того потока, который обрабатывает очередь сообщений формы (message queue). Это чрезвычайно важно, когда Вы делаете модификацию своих элементов управления (controls) Ваших Windows Forms из потока очереди сообщений. Могут быть и другие случаи, когда разные потоки вступают в конфликт при доступе к одному и тому же общему ресурсу – например, обработчик события получения данных от внешнего устройства (USB, сокет Ethernet и проч.) должен выводить данные в ListBox, который был создан в главном окне программы. В этом случае выполнение программы может вызывать исключение «System.InvalidOperationException: Недопустимая операция в нескольких потоках: попытка доступа к элементу управления 'имя_экземпляра_ListBox' не из того потока, в котором он был создан.».

[Стандартное решение]

Само собой, имеется стандартный механизм для решения этой проблемы. Каждый элемент управления (control) Windows Forms имеет свойство InvokeRequired, которое вернет false, если текущий поток является «родным» для контрола, т. е. является потоком очереди сообщений. Также имеется метод Invoke, который делает возможным поставить выполнение делегата (delegate) с параметрами в очередь сообщений управления.

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

[Пример: случайные строки (Random Strings)]

Чтобы проиллюстрировать этот метод, я написал маленькую программу на Windows Forms, которая генерирует случайные строки. Вот кусок кода, который показывает, как сделана синхронизация между рабочим потоком (worker thread) и потоком цикла очереди сообщений (message loop thread). 

char PickRandomChar(string digits)
{
    Thread.Sleep(100);
    return digits[random.Next(digits.Length)];
}
delegate void SetBoolDelegate(bool parameter);
void SetInputEnabled(bool enabled)
{
    if(!InvokeRequired)
    {
        button1.Enabled=enabled;
        comboBoxDigits.Enabled=enabled;
        numericUpDownDigits.Enabled=enabled;
    }
    else
        Invoke(new SetBoolDelegate(SetInputEnabled),new object[] {enabled});
}
delegate void SetStringDelegate(string parameter);
void SetStatus(string status) {
    if(!InvokeRequired)
        labelStatus.Text=status;
    else
        Invoke(new SetStringDelegate(SetStatus),new object[] {status});
}
void SetResult(string result) {
    if(!InvokeRequired)
        textBoxResult.Text=result;
    else
        Invoke(new SetStringDelegate(SetResult),new object[] {result});
}
delegate int GetIntDelegate();
int GetNumberOfDigits()
{
    if(!InvokeRequired)
        return (int)numericUpDownDigits.Value;
    else
        return (int)Invoke(new GetIntDelegate(GetNumberOfDigits),null);
}
delegate string GetStringDelegate();
string GetDigits()
{
    if(!InvokeRequired)
        return comboBoxDigits.Text;
    else
        return (string)Invoke(new GetStringDelegate(GetDigits),null);
}
void Work()
{
    try
    {
        SetInputEnabled(false);
        SetStatus("Working");        
        int n=GetNumberOfDigits();
        string digits=GetDigits();
        StringBuilder text=new StringBuilder();
        for(int i=0;i!=n;i++)
        {
            text.Append(PickRandomChar(digits));
            SetResult(text.ToString());
        }
        SetStatus("Ready");
    }
    catch(ThreadAbortException)
    {
        SetResult("");
        SetStatus("Error");
    }
    finally
    {
        SetInputEnabled(true);
    }
}
void Start()
{
    Stop();
    thread=new Thread(new ThreadStart(Work));
    thread.Start();
}
void Stop()
{
    if(thread!=null)
    {
        thread.Abort();
        thread=null;
    }
}

Здесь используется Thread.Abort, так как это самое простое решение. Если Вы делаете что-то, что не должно быть прервано ни при каких обстоятельствах, то вместо этого Вы должны использовать флаг сигнализации для потока. Здесь приведен простой, но очень повторяющийся код. Имейте в виду, что Вы всегда должны проверить InvokeRequired, потому что вызов Invoke перед созданием очереди сообщений может привести к ошибке. Скачать исходный код проекта (Visual Studio C# 2010) можно по ссылке [7].

[Генерирование потокозащищенных оберток (thread-safe wrappers)]

В предыдущей статье (см. [2]) я показал, как можно автоматически генерировать обертки для классов, чтобы заставить их «неявно» реализовать интерфейс. Тот же метод генерации кода можно расширить, чтобы создать обертки, автоматически обеспечивающие вызов метода в правильном потоке. Я позже подробно опишу, как это работает. В первую очередь рассмотрим, как используется весь механизм. Сначала Вы предоставляете соответствующие свойства в Вашей форме (чекбоксы, листбоксы, и проч.), не заботясь о проблемах с потоками. Это именно то, что Вы хотели бы сделать, если бы совсем не использовали многопоточность. 

public bool InputEnabled
{
    set
    {
        button1.Enabled=value;
        comboBoxDigits.Enabled=value;
        numericUpDownDigits.Enabled=value;
    }
}
public string Status
{
    set { labelStatus.Text=value;}
}
public int NumberOfDigits
{
    get { return numericUpDownDigits.Value; }
}
public string Digits
{
    get { return comboBoxDigits.Text; }
}
public string Result
{
    set { textBoxResult.Text=value; }
}

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

interface IFormState
{
    int NumberOfDigits { get; }
    string Digits { get; }
    string Status { set; }
    string Result { set; }
    bool InputEnabled { set; }
}

Теперь в методе worker все, что Вы должны сделать – создать thread-safe wrapper и использовать его. Для Вас опущен весь повторяющийся код.

void Work()
{
    IFormState state=Wrapper.Create(typeof(IFormState),this);
    try
    {
        state.InputEnabled=false;
        state.Status="Working";
        int n=state.NumberOfDigits;
        string digits=state.Digits;
        StringBuilder text=new StringBuilder();

        for(int i=0;i         {
            text.Append(PickRandomChar(digits));
            state.Result=text.ToString();
        } 
        state.Status="Ready";
    }
    catch(ThreadAbortException)
    {
        state.Status="Error";
        state.Result="";
    }
    finally
    {
        state.InputEnabled=true;
    }
}

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

Генератор обертки (wrapper generator) использует System.Reflection.Emit для генерации proxy-класса, который содержит все требуемые для интерфейса методы. Это также включает методы доступа к свойствам, у которых есть специальная сигнатура.

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

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

Поскольку wrapper generator использует интерфейс для вызова синхронизации ISynchronizeInvoke, Вы можете применить это решение и для приложений, не использующих Windows-Forms. Все, что нужно сделать – самостоятельно реализовать интерфейс и возможно обработку очереди сообщений.

[Ограничения и возможные проблемы]

Важно понимать, что пока thread-safe wrapper скрывает издержки потоковой синхронизации, из него нельзя делать выход. К примеру, доступ к свойству через thread safe wrapper будет намного медленнее, чем прямой доступ к свойству, если InvokeRequired вернул true. Поэтому если Вам нужно делать многочисленные сложные изменения в Вашей форме из разных потоков, лучшим решением будет создать один отдельный метод для всех действий и вызывать его, вместо того чтобы использовать отдельные вызовы доступа к свойствам.

Также нужно иметь в виду, что не каждый объект может быть безопасно передан из одного потока в другой без синхронизации. В общем случае можно безопасно передавать только типы наподобие int, DateTime и т. п. и иммунные ссылочные типы наподобие string. Будьте особенно осторожны при передаче мутированных ссылочных типов (mutable reference types) наподобие StringBuilder из одного потока к другому. Это будет удачным только в том случае, если будет реально обеспечено, что объект не был модифицирован, пока имеется ссылка на этот объект в других потоках, или все будет удачно если объект является thread safe. Если есть сомнения, то просто передайте глубокую копию вместо ссылки.

[Whidbey]

Whidbey уменьшает проблемы многопоточности, так как он имеет дополнительную поддержку (см. [5]) для фоновой обработки в Windows Forms, и это тем более делает работу с делегатами намного проще благодаря поддержке анонимных методов (см. [4]), которые являются реально закрытыми (closure, см. [6]).

Установка свойства в Whidbey намного проще: 

Invoke(delegate { labelStatus.Text="Working"; });

Точно так же просто получить значение свойства: 

int n=(int)Invoke(delegate { return numericUpDownDigits.Value; });

К сожалению, Whidbey наверное выйдет в релиз одновременно с Duke Nukem Forever, где-нибудь в 2025 году.

[Ссылки]

1. Оригинал статьи Rüdiger Klaehn "Making Windows Forms thread safe".
2. Оригинальная статья о «неявных» интерфейсах "Autocaster - Implicit interfaces for .NET".
3. Статья о проблемах с потоками в Windows Forms "How to: Make Thread-Safe Calls to Windows Forms Controls ". Русская версия той же статьи.
4. Отличная статья об анонимных методах (anonymous methods) в системе Whidbey.
5. Статья о классе BackgroundWorker "A tryst with Whidbey: Background operations in Windows Forms".
6. Closure (computer science).
7. Демо-проект ThreadSafeWrapper (Visual Studio C# 2010).

 

Добавить комментарий


Защитный код
Обновить

Top of Page