Практическое руководство. Осуществление потокобезопасных вызовов элементов управления Windows FormsHow to: Make thread-safe calls to Windows Forms controls

Многопоточность может повысить производительность Windows Forms приложений, но доступ к элементам управления Windows Forms не является потокобезопасным.Multithreading can improve the performance of Windows Forms apps, but access to Windows Forms controls isn't inherently thread-safe. Многопоточность может представлять код для очень серьезных и сложных ошибок.Multithreading can expose your code to very serious and complex bugs. Два или более потока, управляющих элементом управления, могут привести к нестабильному состоянию и вызвать условия гонки, взаимоблокировки, зависания или фиксации.Two or more threads manipulating a control can force the control into an inconsistent state and lead to race conditions, deadlocks, and freezes or hangs. При реализации многопоточности в приложении следует обязательно вызывать элементы управления между потоками потокобезопасным образом.If you implement multithreading in your app, be sure to call cross-thread controls in a thread-safe way. Дополнительные сведения см. в разделе рекомендации по управляемому потоку.For more information, see Managed threading best practices.

Существует два способа безопасного вызова элемента управления Windows Forms из потока, который не был создан этим элементом управления.There are two ways to safely call a Windows Forms control from a thread that didn't create that control. System.Windows.Forms.Control.Invoke Метод можно использовать для вызова делегата, созданного в основном потоке, который, в свою очередь, вызывает элемент управления.You can use the System.Windows.Forms.Control.Invoke method to call a delegate created in the main thread, which in turn calls the control. Или можно реализовать System.ComponentModel.BackgroundWorker, который использует модель, управляемую событиями, для разделения работы, выполненной в фоновом потоке, от создания отчетов о результатах.Or, you can implement a System.ComponentModel.BackgroundWorker, which uses an event-driven model to separate work done in the background thread from reporting on the results.

Ненадежные вызовы между потокамиUnsafe cross-thread calls

Вызвать элемент управления напрямую из потока, который не создал его, неважно.It's unsafe to call a control directly from a thread that didn't create it. В следующем фрагменте кода показан незащищенный вызов System.Windows.Forms.TextBox элемента управления.The following code snippet illustrates an unsafe call to the System.Windows.Forms.TextBox control. Обработчик событий создает новый WriteTextUnsafe поток, который TextBox.Text устанавливает свойство основного потока напрямую. Button1_ClickThe Button1_Click event handler creates a new WriteTextUnsafe thread, which sets the main thread's TextBox.Text property directly.

private void Button1_Click(object sender, EventArgs e)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}
Private Sub Button1_Click(ByVal sender As Object, e As EventArgs) Handles Button1.Click
    Thread2 = New Thread(New ThreadStart(AddressOf WriteTextUnsafe))
    Thread2.Start()
End Sub

Private Sub WriteTextUnsafe()
    TextBox1.Text = "This text was set unsafely."
End Sub

Отладчик Visual Studio обнаруживает эти ненадежные вызовы потоков путем вызова InvalidOperationException исключения с сообщением, недопустимой операцией между потоками. Доступ к элементу управления "" осуществляется из потока, отличного от потока, в котором он был создан.The Visual Studio debugger detects these unsafe thread calls by raising an InvalidOperationException with the message, Cross-thread operation not valid. Control "" accessed from a thread other than the thread it was created on. InvalidOperationException Всегда происходит для ненадежных межпотоковых вызовов во время отладки Visual Studio и может возникнуть во время выполнения приложения.The InvalidOperationException always occurs for unsafe cross-thread calls during Visual Studio debugging, and may occur at app runtime. Проблему следует устранить, но можно отключить исключение, задав Control.CheckForIllegalCrossThreadCalls для falseсвойства значение.You should fix the issue, but you can disable the exception by setting the Control.CheckForIllegalCrossThreadCalls property to false.

Надежные вызовы между потокамиSafe cross-thread calls

В следующих примерах кода демонстрируются два способа безопасного вызова элемента управления Windows Forms из потока, который не создал его.The following code examples demonstrate two ways to safely call a Windows Forms control from a thread that didn't create it:

  1. System.Windows.Forms.Control.Invoke Метод, который вызывает делегат из основного потока для вызова элемента управления.The System.Windows.Forms.Control.Invoke method, which calls a delegate from the main thread to call the control.
  2. System.ComponentModel.BackgroundWorker Компонент, который предоставляет модель, управляемую событиями.A System.ComponentModel.BackgroundWorker component, which offers an event-driven model.

В обоих примерах фоновый поток заждет одну секунду для имитации работы, выполняемой в этом потоке.In both examples, the background thread sleeps for one second to simulate work being done in that thread.

Вы можете собрать и запустить эти примеры как .NET Framework приложения из командной C# строки или Visual Basic.You can build and run these examples as .NET Framework apps from the C# or Visual Basic command line. Дополнительные сведения см. в разделе Построение из командной строки с помощью csc. exe или Сборка из командной строки (Visual Basic).For more information, see Command-line building with csc.exe or Build from the command line (Visual Basic).

Начиная с .NET Core 3,0, можно также создавать и запускать примеры как приложения Windows .NET Core из папки, в которой имеется .NET Core Windows Forms <имя папки >. csproj -файл проекта.Starting with .NET Core 3.0, you can also build and run the examples as Windows .NET Core apps from a folder that has a .NET Core Windows Forms <folder name>.csproj project file.

Пример Использование метода Invoke с делегатомExample: Use the Invoke method with a delegate

В следующем примере показан шаблон для обеспечения потокобезопасных вызовов элемента управления Windows Forms.The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. Он запрашивает System.Windows.Forms.Control.InvokeRequired свойство, которое сравнивает идентификатор потока создаваемого элемента управления с идентификатором вызывающего потока.It queries the System.Windows.Forms.Control.InvokeRequired property, which compares the control's creating thread ID to the calling thread ID. Если идентификаторы потоков совпадают, он вызывает элемент управления напрямую.If the thread IDs are the same, it calls the control directly. Если идентификаторы потоков отличаются, он вызывает Control.Invoke метод с делегатом из основного потока, который выполняет фактический вызов элемента управления.If the thread IDs are different, it calls the Control.Invoke method with a delegate from the main thread, which makes the actual call to the control.

Позволяет задать свойствоTextэлементауправления. TextBox SafeCallDelegateThe SafeCallDelegate enables setting the TextBox control's Text property. WriteTextSafe Метод запрашивает InvokeRequired.The WriteTextSafe method queries InvokeRequired. Если InvokeRequired возвращает true ,WriteTextSafe передает вметод,чтобывыполнитьфактическийвызовэлементауправления.SafeCallDelegate InvokeIf InvokeRequired returns true, WriteTextSafe passes the SafeCallDelegate to the Invoke method to make the actual call to the control. Если InvokeRequired возвращает false, WriteTextSafe задает непосредственно.TextBox.TextIf InvokeRequired returns false, WriteTextSafe sets the TextBox.Text directly. Обработчик событий создает новый поток и WriteTextSafe выполняет метод. Button1_ClickThe Button1_Click event handler creates the new thread and runs the WriteTextSafe method.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }
}
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class InvokeThreadSafeForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New InvokeThreadSafeForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox
    Dim Thread2 as Thread = Nothing

    Delegate Sub SafeCallDelegate(text As String)

    Private Sub New()
        Button1 = New Button()
        With Button1
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
            .Text = "Set text safely"
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Thread2 = New Thread(New ThreadStart(AddressOf SetText))
        Thread2.Start()
        Thread.Sleep(1000)
    End Sub

    Private Sub WriteTextSafe(text As String)
        If TextBox1.InvokeRequired Then
            Dim d As New SafeCallDelegate(AddressOf SetText)
            TextBox1.Invoke(d, New Object() {text})
        Else
            TextBox1.Text = text
        End If
    End Sub

    Private Sub SetText()
        WriteTextSafe("This text was set safely.")
    End Sub
End Class

Пример Использование обработчика событий BackgroundWorkerExample: Use a BackgroundWorker event handler

Простой способ реализации многопоточности заключается в System.ComponentModel.BackgroundWorker использовании компонента, использующего модель, управляемую событиями.An easy way to implement multithreading is with the System.ComponentModel.BackgroundWorker component, which uses an event-driven model. Фоновый поток запускает BackgroundWorker.DoWork событие, которое не взаимодействует с основным потоком.The background thread runs the BackgroundWorker.DoWork event, which doesn't interact with the main thread. Главный поток запускает BackgroundWorker.ProgressChanged обработчики событий BackgroundWorker.RunWorkerCompleted и, которые могут вызывать элементы управления основного потока.The main thread runs the BackgroundWorker.ProgressChanged and BackgroundWorker.RunWorkerCompleted event handlers, which can call the main thread's controls.

Чтобы сделать потокобезопасный вызов с помощью BackgroundWorker, создайте метод в фоновом потоке, чтобы выполнить работу, и привяжите его DoWork к событию.To make a thread-safe call by using BackgroundWorker, create a method in the background thread to do the work, and bind it to the DoWork event. Создайте другой метод в основном потоке, чтобы сообщить результаты фоновой работы и привязать его к ProgressChanged событию или. RunWorkerCompletedCreate another method in the main thread to report the results of the background work, and bind it to the ProgressChanged or RunWorkerCompleted event. Чтобы запустить фоновый поток, вызовите BackgroundWorker.RunWorkerAsync.To start the background thread, call BackgroundWorker.RunWorkerAsync.

В примере RunWorkerCompleted с помощью обработчика событий TextBox задается Text свойство элемента управления.The example uses the RunWorkerCompleted event handler to set the TextBox control's Text property. Пример использования события см. ProgressChanged в разделе. BackgroundWorkerFor an example using the ProgressChanged event, see BackgroundWorker.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely with BackgroundWorker"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000);
        e.Result = "This text was set safely by BackgroundWorker.";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}
Imports System.ComponentModel
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class BackgroundWorkerForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New BackgroundWorkerForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents BackgroundWorker1 As BackgroundWorker
    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox

    Private Sub New()
        BackgroundWorker1 = New BackgroundWorker()
        Button1 = New Button()
        With Button1
            .Text = "Set text safely with BackgroundWorker"
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) _
     Handles BackgroundWorker1.DoWork
        ' Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000)
        e.Result = "This text was set safely by BackgroundWorker."
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) _
     Handles BackgroundWorker1.RunWorkerCompleted
        textBox1.Text = e.Result.ToString()
    End Sub
End Class

См. такжеSee also