dimanche 25 mai 2014

c# - opération inter-threads non valide : contrôle accédé à partir d'un autre thread que le thread, il a été créé sur - Stack Overflow


I have a scenario. (Windows Forms, C#, .NET)



  1. There is a main form which hosts some user control.

  2. The user control does some heavy data operation, such that if I directly call the UserControl_Load method the UI become nonresponsive for the duration for load method execution.

  3. To overcome this I load data on different thread (trying to change existing code as little as I can)

  4. I used a background worker thread which will be loading the data and when done will notify the application that it has done its work.

  5. Now came a real problem. All the UI (main form and its child usercontrols) was created on the primary main thread. In the LOAD method of the usercontrol I'm fetching data based on the values of some control (like textbox) on userControl.


The pseudocode would look like this:


CODE 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.
}
}

The Exception it gave was



Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on.



To know more about this I did some googling and a suggestion came up like using the following code


CODE 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
}
}

BUT BUT BUT... it seems I'm back to square one. The Application again become nonresponsive. It seems to be due to the execution of line #1 if condition. The loading task is again done by the parent thread and not the third that I spawned.


I don't know whether I perceived this right or wrong. I'm new to threading.


How do I resolve this and also what is the effect of execution of Line#1 if block?


The situation is this: I want to load data into a global variable based on the value of a control. I don't want to change the value of a control from the child thread. I'm not going to do it ever from a child thread.


So only accessing the value so that the corresponding data can be fetched from the database.




[EDIT] As per SilverHorse's update comment, the solution you want then should look like:


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

Do your serious processing in the separate thread before you attempt to switch back to the control's thread. For example:


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;
}
}
}



You only want to use Invoke or BeginInvoke for the bare minimum piece of work required to change the UI. Your "heavy" method should execute on another thread (e.g. via BackgroundWorker) but then using Control.Invoke/Control.BeginInvoke just to update the UI. That way your UI thread will be free to handle UI events etc.


See my threading article for a WinForms example - although the article was written before BackgroundWorker arrived on the scene, and I'm afraid I haven't updated it in that respect. BackgroundWorker merely simplifies the callback a bit.




I have had this problem with the FileSystemWatcher and found that the following code solved the problem:


fsw.SynchronizingObject = this


The control then uses the current form object to deal with the events, and will therefore be on the same thread.




Use the code found here on StackOverflow to eliminate the need to write a new delegate every time you want to call Invoke.




Threading Model in UI


Read Threading Model in UI applications in order to understand basic concepts. The link navigates to page that describes WPF threading model. However, Windows Forms utilizes the same idea.


The UI Thread



  • There is only one thread (UI thread), that is allowed to access System.Windows.Forms.Control and its subclasses members.

  • Attempt to access member of System.Windows.Forms.Control from different thread than UI thread will cause cross-thread exception.

  • Since there is only one thread, all UI operations are queued as work items into that thread:


enter image description here



enter image description here


BeginInvoke and Invoke methods



enter image description here


Invoke



enter image description here


BeginInvoke



enter image description here


Code solution


Read answers on question How to update the GUI from another thread in C#?. For C# 5.0 and .NET 4.5 the recommended solution is here.




Controls in .NET are not generally thread safe. That means you shouldn't access a control from a thread other than the one where it lives. To get around this, you need to invoke the control, which is what your 2nd sample is attempting.


However, in your case all you've done is pass the long-running method back to the main thread. Of course, that's not really what you want to do. You need to rethink this a little so that all you're doing on the main thread is setting a quick property here and there.




The cleanest (and proper) solution for UI cross-threading issues is to use SynchronizationContext, see Synchronizing calls to the UI in a multi-threaded application article, it explains it very nicely.




You need to look at the Backgroundworker example:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx Especially how it interacts with the UI layer. Based on your posting, this seems to answer your issues.




For anyone else that is dealing with this issue, take a look at this article.


I found it really interesting and it works quite well.




Here is an alternative way if the object you are working with doesn't have


(InvokeRequired)

This is useful if you are working with the main form in a class other than the main form with an object that is in the main form, but doesn't have 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;
}

It works the same as above, but it is a different approach if you don't have an object with invokerequired, but do have access to the MainForm




I found a need for this while programming an iOS-Phone monotouch app controller in a visual studio winforms prototype project outside of xamarin stuidio. Preferring to program in VS over xamarin studio as much as possible, I wanted the controller to be completely decoupled from the phone framework. This way implementing this for other frameworks like Android and Windows Phone would be much easier for future uses.


I wanted a solution where the GUI could respond to events without the burden of dealing with the cross threading switching code behind every button click. Basically let the class controller handle that to keep the client code simple. You could possibly have many events on the GUI where as if you could handle it in one place in the class would be cleaner. I am not a multi theading expert, let me know if this is flawed.


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");
}
}

The GUI form is unaware the controller is running asynchronous tasks.


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);
}
}
}
}



Here's another nice & clean solution: http://blog.stephencleary.com/2010/06/reporting-progress-from-tasks.html.




For example to get the text from a Control of the UI thread:


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


I have a scenario. (Windows Forms, C#, .NET)



  1. There is a main form which hosts some user control.

  2. The user control does some heavy data operation, such that if I directly call the UserControl_Load method the UI become nonresponsive for the duration for load method execution.

  3. To overcome this I load data on different thread (trying to change existing code as little as I can)

  4. I used a background worker thread which will be loading the data and when done will notify the application that it has done its work.

  5. Now came a real problem. All the UI (main form and its child usercontrols) was created on the primary main thread. In the LOAD method of the usercontrol I'm fetching data based on the values of some control (like textbox) on userControl.


The pseudocode would look like this:


CODE 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.
}
}

The Exception it gave was



Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on.



To know more about this I did some googling and a suggestion came up like using the following code


CODE 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
}
}

BUT BUT BUT... it seems I'm back to square one. The Application again become nonresponsive. It seems to be due to the execution of line #1 if condition. The loading task is again done by the parent thread and not the third that I spawned.


I don't know whether I perceived this right or wrong. I'm new to threading.


How do I resolve this and also what is the effect of execution of Line#1 if block?


The situation is this: I want to load data into a global variable based on the value of a control. I don't want to change the value of a control from the child thread. I'm not going to do it ever from a child thread.


So only accessing the value so that the corresponding data can be fetched from the database.



[EDIT] As per SilverHorse's update comment, the solution you want then should look like:


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

Do your serious processing in the separate thread before you attempt to switch back to the control's thread. For example:


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;
}
}
}


You only want to use Invoke or BeginInvoke for the bare minimum piece of work required to change the UI. Your "heavy" method should execute on another thread (e.g. via BackgroundWorker) but then using Control.Invoke/Control.BeginInvoke just to update the UI. That way your UI thread will be free to handle UI events etc.


See my threading article for a WinForms example - although the article was written before BackgroundWorker arrived on the scene, and I'm afraid I haven't updated it in that respect. BackgroundWorker merely simplifies the callback a bit.



I have had this problem with the FileSystemWatcher and found that the following code solved the problem:


fsw.SynchronizingObject = this


The control then uses the current form object to deal with the events, and will therefore be on the same thread.



Use the code found here on StackOverflow to eliminate the need to write a new delegate every time you want to call Invoke.



Threading Model in UI


Read Threading Model in UI applications in order to understand basic concepts. The link navigates to page that describes WPF threading model. However, Windows Forms utilizes the same idea.


The UI Thread



  • There is only one thread (UI thread), that is allowed to access System.Windows.Forms.Control and its subclasses members.

  • Attempt to access member of System.Windows.Forms.Control from different thread than UI thread will cause cross-thread exception.

  • Since there is only one thread, all UI operations are queued as work items into that thread:


enter image description here



enter image description here


BeginInvoke and Invoke methods



enter image description here


Invoke



enter image description here


BeginInvoke



enter image description here


Code solution


Read answers on question How to update the GUI from another thread in C#?. For C# 5.0 and .NET 4.5 the recommended solution is here.



Controls in .NET are not generally thread safe. That means you shouldn't access a control from a thread other than the one where it lives. To get around this, you need to invoke the control, which is what your 2nd sample is attempting.


However, in your case all you've done is pass the long-running method back to the main thread. Of course, that's not really what you want to do. You need to rethink this a little so that all you're doing on the main thread is setting a quick property here and there.



The cleanest (and proper) solution for UI cross-threading issues is to use SynchronizationContext, see Synchronizing calls to the UI in a multi-threaded application article, it explains it very nicely.



You need to look at the Backgroundworker example:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx Especially how it interacts with the UI layer. Based on your posting, this seems to answer your issues.



For anyone else that is dealing with this issue, take a look at this article.


I found it really interesting and it works quite well.



Here is an alternative way if the object you are working with doesn't have


(InvokeRequired)

This is useful if you are working with the main form in a class other than the main form with an object that is in the main form, but doesn't have 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;
}

It works the same as above, but it is a different approach if you don't have an object with invokerequired, but do have access to the MainForm



I found a need for this while programming an iOS-Phone monotouch app controller in a visual studio winforms prototype project outside of xamarin stuidio. Preferring to program in VS over xamarin studio as much as possible, I wanted the controller to be completely decoupled from the phone framework. This way implementing this for other frameworks like Android and Windows Phone would be much easier for future uses.


I wanted a solution where the GUI could respond to events without the burden of dealing with the cross threading switching code behind every button click. Basically let the class controller handle that to keep the client code simple. You could possibly have many events on the GUI where as if you could handle it in one place in the class would be cleaner. I am not a multi theading expert, let me know if this is flawed.


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");
}
}

The GUI form is unaware the controller is running asynchronous tasks.


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);
}
}
}
}


For example to get the text from a Control of the UI thread:


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

0 commentaires:

Enregistrer un commentaire