Skip to main content
 首页 » 编程设计

c#之为什么在这个特定的 WinForms 应用程序中会发生死锁

2024年02月13日33zlslch

我创建了一个带有文本框、设置按钮和切换按钮的简单 Windows 窗体。当我单击“切换”按钮时,会创建一个线程,将文本重复设置到文本框。当我再次单击按钮时,线程停止。当我单击“设置”按钮时,文本一次设置到文本框。如果我执行以下操作,则会发生死锁:

  1. 运行应用(在 Debug模式下)。
  2. 单击切换按钮让文本在文本框中运行。
  3. 点击设置按钮。 -> 这一步出现死锁。

您能解释一下为什么以及如何在这种情况下发生死锁吗?如何避免?

代码如下:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Windows.Forms; 
 
namespace DeadLockTest 
{ 
    static class Program 
    { 
        /// <summary> 
        /// The main entry point for the application. 
        /// </summary> 
        [STAThread] 
        static void Main() 
        { 
            Application.EnableVisualStyles(); 
            Application.SetCompatibleTextRenderingDefault(false); 
            Application.Run(new Form1()); 
        } 
    } 
} 

using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Threading; 
using System.Windows.Forms; 
 
namespace DeadLockTest 
{ 
    public class Form1 : Form 
    { 
        private System.Windows.Forms.TextBox textBox1; 
        private System.Windows.Forms.Button button1; 
        private System.Windows.Forms.Button button2; 
 
        private int counter; 
        private Thread thread; 
        private bool cancelRequested; 
        private string content; 
        private object lockKey = new object(); 
 
        public Form1() 
        { 
            InitializeComponent(); 
        } 
 
        protected void UpdateContent() 
        { 
            this.textBox1.Text = this.content; 
        } 
 
        protected void InvokeUpdateContent() 
        { 
            lock (this.lockKey) 
            { 
                if (InvokeRequired) 
                { 
                    Invoke(new Action(UpdateContent)); 
                } 
                else 
                { 
                    UpdateContent(); 
                } 
            } 
        } 
 
        protected void SetText(string text) 
        { 
            this.content = text; 
            InvokeUpdateContent(); 
        } 
 
        protected void StressTest() 
        { 
            int localCounter = 0; 
 
            while (!this.cancelRequested) 
            { 
                SetText(string.Format("{0}", localCounter++)); 
            } 
 
            this.cancelRequested = false; 
            this.thread = null; 
        } 
 
        private void InitializeComponent() 
        { 
            this.textBox1 = new System.Windows.Forms.TextBox(); 
            this.button1 = new System.Windows.Forms.Button(); 
            this.button2 = new System.Windows.Forms.Button(); 
            this.SuspendLayout(); 
            //  
            // textBox1 
            //  
            this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
                        | System.Windows.Forms.AnchorStyles.Right))); 
            this.textBox1.Location = new System.Drawing.Point(12, 12); 
            this.textBox1.Name = "textBox1"; 
            this.textBox1.ReadOnly = true; 
            this.textBox1.Size = new System.Drawing.Size(260, 20); 
            this.textBox1.TabIndex = 0; 
            //  
            // button1 
            //  
            this.button1.Location = new System.Drawing.Point(12, 38); 
            this.button1.Name = "button1"; 
            this.button1.Size = new System.Drawing.Size(75, 23); 
            this.button1.TabIndex = 1; 
            this.button1.Text = "Set"; 
            this.button1.UseVisualStyleBackColor = true; 
            this.button1.Click += new System.EventHandler(this.button1_Click); 
            //  
            // button2 
            //  
            this.button2.Location = new System.Drawing.Point(93, 38); 
            this.button2.Name = "button2"; 
            this.button2.Size = new System.Drawing.Size(75, 23); 
            this.button2.TabIndex = 2; 
            this.button2.Text = "Toggle"; 
            this.button2.UseVisualStyleBackColor = true; 
            this.button2.Click += new System.EventHandler(this.button2_Click); 
            //  
            // Form1 
            //  
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 
            this.ClientSize = new System.Drawing.Size(284, 262); 
            this.Controls.Add(this.button2); 
            this.Controls.Add(this.button1); 
            this.Controls.Add(this.textBox1); 
            this.Name = "Form1"; 
            this.Text = "Form1"; 
            this.ResumeLayout(false); 
            this.PerformLayout(); 
 
        } 
 
        private void button1_Click(object sender, EventArgs e) 
        { 
            SetText(string.Format("{0}", this.counter++)); 
        } 
 
        private void button2_Click(object sender, EventArgs e) 
        { 
            if (this.thread == null) 
            { 
                this.thread = new Thread(new ThreadStart(StressTest)); 
 
                thread.Start(); 
            } 
            else 
            { 
                this.cancelRequested = true; 
            } 
        } 
    } 
} 

请您参考如下方法:

Can you explain why and how deadlock occurs in this situation?

当然...死锁的发生是因为 lock()/Invoke() 组合。

副线程在运行和更新的同时,获得了对象的锁。然后辅助线程调用 Invoke(),这是一个同步调用。关键是要认识到辅助线程实际上是在等待主 UI 线程在继续之前更新 TextBox。

当您单击“设置”按钮时,它会尝试更新但必须等待辅助线程释放锁。此时,主 UI 实际上停止并卡住在 lock() 行,等待辅助线程释放锁。在等待锁释放期间,主 UI 线程无法处理任何消息。

但是辅助线程在做什么呢?它当前拥有锁并正在等待主 UI 线程为其同步 Invoke() 调用提供服务。但是,由于主 UI 线程正在等待释放锁,因此它无法处理任何请求(包括 Invoke() 请求)并且 bam...DEADLOCK!他们都在等待对方。

How to avoid it?

切勿在主 UI 线程中使用 lock()。在某些情况下,从 Invoke() 切换到 BeginInvoke() 可以解决问题,因为 BeginInvoke() 是异步的。