简体   繁体   中英

Lock hangs when called from UI to System.Threading.Thread

EDIT: please see question history, for unchanged question in order not to invalidate comments.

I am clicking button that executes certain codes and it creates a thread (System.Threading.Thread). When I reclick button which starts process it hangs and freezes ui. What could be the reason?

public partial class ucLoader : UserControl
{
    //lock object for whole instance of class ucLoader
    private object lockUcLoader = new object();

    //bringing info from ui
    private void btnBringInfo_Click(object sender, EventArgs e)
    {
        lock (lockUcLoader)
        {
            btnBringInfo_PerformClick(false);
        }
    }

    //using this method because it could be called when even button not visible
    internal void btnBringInfo_PerformClick(bool calledFromBandInit)
    {
        lock (lockUcLoader) //HANGS HERE when called multiple times and ui freeze as well
        //by the way I am using (repetitive) lock, because this method also called independently from btnBringInfo_Click
        {
            //...
            this.btnLoad_PerformClick();
        }
    }

    //Another button perform click that could be triggered elsewhere when even button not visible
    private void btnLoad_PerformClick()
    {
        lock (lockUcLoader) //I am using (repetitive) lock, because this method also called independently from btnBringInfo_PerformClick
        {
            //...
            Run();  
        }
    }

    //method for creating thread which System.Threading.Thread  
    private void Run()
    {
       lock (lockUcLoader)  //Maybe this lock is NOT REQUIRED, as it is called by only btnLoad_PerformClick(), could you please confirm?
        {
            //some code that thread can be killed when available,  you can ingore this two lines as they are irrelevant to subject, I think
            Source = new CancellationTokenSource();
            Token = Source.Token;
            var shell = new WindowsShell();
            Thread = new Thread((object o) =>
            {
                //...
                var tokenInThread = (CancellationToken)o;
                exitCode =TaskExtractBatchFiles(cls, shell, exitCode);


                 using (var logEnt = new logEntities())
                {

                        //Do some db operation
                        //...
                        this.Invoke((MethodInvoker)delegate
                        {
                            //do some ui update operation
                            //...
                        });
                    }
            }
            Thread.Start(Token);
        }      
    } 

    public void Progress(string message)
    {
        Invoke((MethodInvoker)delegate  //ATTENTION HERE see below picture Wait occurs here
        {
            if (message != null && message.Trim() != string.Empty)
            {
                this.txtStatus.AppendText(message + Environment.NewLine);
            }
        });
    }       
}       

In order to avoid get closed question, what my question is how can I prevent below method can be accesses with out lock from background thread and ui thread

public void Progress(string message)
        {
            Invoke((MethodInvoker)delegate  //ATTENTION HERE see below picture Wait occurs here
            {
                if (message != null && message.Trim() != string.Empty)
                {
                    this.txtStatus.AppendText(message + Environment.NewLine);
                }
            });
        }   

在此输入图像描述

在此输入图像描述

   Invoke((MethodInvoker)delegate ...

Whenever you use the lock statement in your code then you always run the risk of inducing deadlock . One of the classic threading bugs. You generally need at least two locks to get there, acquiring them in the wrong order. And yes, there are two in your program. One you declared yourself. And one you cannot see because it is buried inside the plumbing that makes Control.Invoke() work. Not being able to see a lock is what makes deadlock a difficult problem to debug.

You can reason it out, the lock inside Control.Invoke is necessary to ensure that the worker thread is blocked until the UI thread executed the delegate target. Probably also helps to reason out why the program deadlocked. You started the worker thread, it acquired the lockUcLoader lock and starts doing its job, calling Control.Invoke while doing so. Now you click the button before the worker is done, it necessarily blocks. But that makes the UI thread go catatonic and no longer capable of executing the Control.Invoke code. So the worker thread hangs on the Invoke call and it won't release the lock. And the UI thread hangs forever on the lock since the worker can't complete, deadlock city.

Control.Invoke dates from .NET 1.0, a version of the framework that has several serious design mistakes in code related to threading. While meant to be helpful, they just set death-traps for programmers to blunder into. What is unique about Control.Invoke is that it is never correct to use it.

Distinguish Control.Invoke and Control.BeginInvoke. You only ever need Invoke when you need its return value. Note how you don't, using BeginInvoke instead is good enough and instantly solves the deadlock. You'd consider Invoke to obtain a value from the UI so you can use it in the worker thread. But that induces other major threading issue, a threading race bug, the worker has no idea what state the UI is in. Say, the user might be busy interacting with it, typing a new value. You can't know what value you obtain, it will easily be the stale old value. Inevitably producing a mismatch between the UI and the work being done. The only way to avoid that mishap is to prevent the user from typing a new value, easily done with Enable = false. But now it no longer makes sense to use Invoke, you might as well pass the value when you start the thread.

So using BeginInvoke is already good enough to solve the problem. But that is not where you should stop. There is no point to those locks in the Click event handlers, all they do is make the UI unresponsive, greatly confuzzling the user. What you must do instead is set the Enable properties of those buttons to false . Set them back to true when the worker is done. Now it can't go wrong anymore, you don't need the locks and the user gets good feedback.

There is another serious problem you haven't run into yet but you must address. A UserControl has no control over its lifetime, it gets disposed when the user closes the form on which it is hosted. But that is completely out of sync with the worker thread execution, it keeps calling BeginInvoke even though the control is dead as a doornail. That will make your program bomb, hopefully on an ObjectDisposedException. A threading race bug that a lock cannot solve. The form has to help, it must actively prevent the user from closing it. Some notes about this bug in this Q+A .

For completeness I should mention the third most common threading bug that code like this is likely to suffer from. It doesn't have an official name, I call it a "firehose bug". It occurs when the worker thread calls BeginInvoke too often, giving the UI thread too much work to do. Happens easily, calling it more than about thousand times per second tends to be enough. The UI thread starts burning 100% core, trying to keep up with the invoke requests and never being able to catch up. Easy to see, it stops painting itself and responding to input, duties that are performed with a lower priority. That needs to be fixed the logical way, updating UI more than 25 times per second just produces a blur that the human eye can't observe and is therefore pointless.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM