Go Back  Xtreme Visual Basic Talk > General Discussion > Tech Discussions > Accessing controls from worker threads in .NET


Reply
 
Thread Tools Display Modes
  #1  
Old 12-04-2008, 12:36 PM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default Accessing controls from worker threads in .NET


Side note to interested moderators: This is a question that's asked frequently in the .NET forums, and it is going to be of increasing importance as more applications become multithreaded. I was disappointed that I couldn't find anything about it in Tutors' Corner. If this article seems good enough, could it get moved to the .NET Tutors' Corner?

You're probably here because you've got a long-running task that needs to be performed on a worker thread, and the thread interacts with some controls. You wrote the code in the most straightforward fashion, but when it runs either weird things happen (VS .NET 2003 or earlier) or you get an InvalidOperationException that's telling you the control is accessed from a thread other than the thread it was created on. What's this mean? How do you fix it?

Windows controls display a property called thread affinity. This means that they have a special connection to the thread that creates them, and they can only be safely accessed from that thread. Generally, there is a single thread created at application startup on which all of the controls are created; this thread is known as the UI thread, but it might have been better if people called it the "main thread". If you are on a different thread and want to access the control, you'll have to somehow move your code onto the control's thread. This is called marshalling the call, and I'm going to demonstrate two methods for marshalling calls across threads in the .NET framework: one that works in all versions of the framework and one that requires .NET 2.0.

I'm going to start by stating if you are using the Thread class, this is likely your first mistake. The reasons why could fill another topic, but for now there's three things I'm interested in:
  • A thread is an expensive kernel object that takes time and resources to create.
  • The ThreadPool class maintains a pool of threads that are instantly available and don't require additional resources.
  • Enhancements to the .NET framework 4.0 that allow you to use multiple cores more efficiently use an API similar to the ThreadPool; if you start using it now you'll be more ready to parallelize your work.

Using the ThreadPool is not harder than using Thread. Instead of your thread "work" method using the ThreadStart delegate, now you need to use the WaitCallback delegate:
Public Delegate Sub WaitCallback(ByVal state As Object)

You don't have to worry about the state parameter; it's just a way to pass parameters to the method. If you ever used ParameterizedThreadStart, it will look familiar. If you don't have information to pass to the worker thread, just use Nothing.

So here's a quick example of using ThreadPool to start a thread:

Code:
Sub StartThread()
    ThreadPool.QueueUserWorkItem(AddressOf ThreadDoWork, Nothing)
End Sub

Sub ThreadDoWork(ByVal state As Object)
    ' do whatever
End Sub
Now that I've wasted ~1500 characters on attacking Thread, let's look at the example application that gets it wrong. The attached NoProtection.vb is a form that doesn't perform any marshalling. It's got a read-only textbox and a button (these are created in the "Unimportant Layout Code" region; you can ignore that code as it's not relevant to the topic.) When you click the button, the button is disabled and a worker thread is started. The thread counts from 0 to 9 and updates the text box as it counts; when it's finished it enables the button. If you run this in VS 2005 or later, an exception will be thrown because the worker thread is not allowed to update the control. In VS 2003 or earlier, an exception is not thrown but you'll probably see odd behavior such as the textbox not updating.

In the next post, I'll be talking about the classic marshalling method that's been available since .NET 1.0.
Attached Files
File Type: vb NoProtection.vb (1.7 KB, 24 views)
File Type: vb ClassicInvoke.vb (2.3 KB, 46 views)
File Type: vb AsyncOp.vb (2.7 KB, 33 views)
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #2  
Old 12-04-2008, 12:39 PM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

Classic Marshalling

It's obvious we need to do something, but what can we do? There's a basic marshalling pattern that we need to follow. Let's take it step-by-step. Classic marshalling is demonstrated in the file ClassicInvoke.vb

Step 1: Identify the actions you need to perform on controls.
Figure out how your thread needs to interact with each control. Write a list for everything. In the case of the example application, we have two control interactions:
  1. Update the text box.
  2. Enable the button.

Step 2: For each action you need to perform, create a method to perform the action.
This is simple. Make a method that can do what you want, and make sure it has parameters that are useful. Don't implement the methods at this point; you've got a couple more things to do first. Here's the methods used in ClassicInvoke.vb:
Code:
Private Sub EnableButton()
End Sub

Private Sub UpdateTextBox(ByVal text As String)
End Sub
Step 3: For each method defined in step 2, create a delegate.
Delegates are the key to marshalling. If you don't know what a delegate is, the simple answer is that delegates are variables that can hold methods. I explain it a little more in Observer, Delegates, and .NET Events, an unrelated article.

You declare a delegate using the Delegate keyword, then the method signature (name + parameters + return type [if it's a function].) It's discouraged to name a delegate with the pattern "XXXXDelegate", but in this case it makes things easier and the delegates should be private, so it's not a big deal. If you have multiple action methods that have the same signature, feel free to share the same delegate for all of them. Here's what the delegates for ClassicInvoke.vb look like:
Code:
Private Delegate Sub EnableButtonDelegate()
Private Delegate Sub UpdateTextBoxDelegate(ByVal text As String)
Step 4: Implement the methods from Step 2 using InvokeRequired and the delegates from step 3.
Every control has an InvokeRequired property that returns True if the current thread is not the correct thread to access the control. Every control also has an Invoke method that will invoke a delegate on the correct thread to access the control. Sound useful? It is. Here's the basic pattern of implementing the marshalling method:

Code:
Sub ActionMethod()
    If yourControl.InvokeRequired Then
        Dim del As New YourDelegate(AddressOf ActionMethod)
        yourControl.Invoke(del)
    Else
        ' Update the control -- you are on the right thread here.
    End If
End Sub
This method first checks if it is on the UI thread or a worker thread. If it's on a worker thread, it uses a delegate and the control's Invoke method to call itself on the UI thread. If it's on the UI thread, no marshalling is performed and the control is updated. It might be easier to visualize with a diagram:

Code:
Worker thread                                  |        UI Thread
-------------------------------------------------------------------------------------------
ActionMethod()                                 | (idle)
    If yourControl.InvokeRequired Then ' True  | (idle)
        Dim del As New YourDelegate(...)       | (idle)
        yourControl.Invoke(del)                | (idle)
 End Sub                                       | ActionMethod()
(idle)                                         |    If yourControl.InvokeRequired Then ' False
(idle)                                         |    Else
(idle)                                         |        ' Update the control
It's a bit of work, but once you've done it a few times it will feel like second nature. Take a peek inside ClassicInvoke.vb to see how it all works and more extensive comments.

This is a great technique, but it's tedious to define all of the delegates and methods to update the controls. The next post is going to discuss a new technique in the .NET Framework 2.0 that might be more preferable.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #3  
Old 12-04-2008, 12:41 PM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

AsyncOperation Marshalling

I'm going to be open with you: this technique is at the core of the Event-Based Asynchronous Pattern, which is probably one of my favorite things to play with. I am biased in its favor, but I really feel like it has merits that the classic marshalling method does not. In the classic marshalling, you spend a lot of time writing methods that don't know if they are called from the right thread, so they have to determine what thread they are on and marshall to the right thread if needed. With this newer method, you can guarantee your methods are only called from the appropriate thread. It's about an equal amount of effort, but I feel like in the end you produce cleaner code using this method.

At the heart of this technique is the System.ComponentModel.AsyncOperation class. You get an instance of this class by calling System.ComponentModel.AsyncOperationManager.CreateOperation. This is a smart little method that determines if you're running in Windows Forms, ASP .NET, or some other application context and returns the correct AsyncOperation for your application model. AsyncOperation has two methods I'll discuss:
  1. Post invokes a delegate on the thread that created the AsyncOperation.
  2. PostOperationCompleted indicates the task is complete, and invokes a delegate on the thread that created the AsyncOperation.

The important thing to take away from this is that if you create an AsyncOperation on the UI thread, you can always use Post or PostOperationCompleted to marshall a delegate onto the UI thread. This means that your methods don't have to worry about if they are on the right thread; if you call them via these two methods they are on the right thread.

Only one thing left before we can discuss implementation. The one inconvenience to AsyncOperation is that Post and PostOperationCompleted don't take any delegate; they expact a SendOrPostCallback:
Public Delegate Sub SendOrPostCallback(ByVal state As Object)

This means that if your control update method takes parameters, you'll end up having to write a wrapper method in order to call it. For example, consider EnableButton from the last post. To use it with Post, we need a wrapper like so:
Code:
Private Sub EnableButtonWrapper(ByVal state As Object)
    EnableButton()
End Sub
This is aggravating in this particular case, but AsyncOperation is primarily used in a different scenario where this is not a hassle so it's something we just have to live with.

Now, to implement this method, let's go over the steps. You can look at AsyncOp.vb to see the implementation.

Step 1: Identify the actions you need to perform on controls.
This is the same as in the classic technique.

Step 2: Create a method for each control action.
This is the same as in the classic technique, but you will need to implement the methods this time. For example, here's the methods in AsyncOp.vb:
Code:
Private Sub EnableButton()
    _btn.Enabled = True
End Sub

Private Sub UpdateTextBox(ByVal text As String)
    _txt.Text = text
End Sub
Step 3: Implement wrapper methods for the methods in step 2
You need a method that matches SendOrPostCallback so the AsyncOperation can marshall it. If your method takes one parameter, you can cheat if you want and make it an Object parameter. If your method takes more than one, you'll need a custom class to store the information. If your method takes 0 parameters, your wrapper can ignore its argument. For example, here's the wrapper method in AsyncOp.vb:

Code:
Private Sub UpdateTextBoxWrapper(ByVal text As Object)
    UpdateTextBox(text.ToString())
End Sub
Step 4: Use a variable to keep track of an AsyncOperation.
This is optional if you use some tricks that aren't relevant to this discussion. The simplest way to keep track of the operation is to store it in a class level variable. In AsyncOp.vb, this variable is named _operation.

Step 5: Create the operation when you start the thread.
You have to get an operation from somewhere, and the best time to do it is when you are starting the thread. Make sure that you do this from the UI thread, not the worker thread. If you create the operation from the worker thread, it will post to the worker thread, which is not what you want. In AsyncOp.vb, I create the operation in the StartThread method immediately before starting the worker thread.

Step 6: When the thread needs to update a control, Post the proper method.
Now that you have your wrapper methods, use Post to call them on the UI thread. There's a shorthand syntax for specifying delegates that you can abuse to make this less painful. For example, in AsyncOp.vb, here's the long way I could have updated the textbox followed by the short way:
Code:
_operation.Post(New SendOrPostCallback(AddressOf UpdateTextBoxWrapper), i.ToString())
' -or- 
_operation.Post(AddressOf UpdateTextBoxWrapper, i.ToString())
Do as you wish, but I prefer the short way.

Step 7: When the thread is finished, call PostOperationCompleted.
Here's the other aggravation when using this method for this simple case. I think that calling PostOperationCompleted is optional, but the documentation does not confirm this. If it is optional, you don't have to worry about it, but if it isn't, you'll have to create a "thread finished" callback whether you need it or not. In the case of AsyncOp.vb, I happened to want to enable the button when the thread is finished, so I used it for that. If I were you, I'd call PostOperationCompleted just to be safe.

Honestly this method is using AsyncOperation a little bit outside of its intended design; it was intended for use with the Event-Based Asynchronous pattern and most of the "aggravations" I've mentioned here are things that would seem natural if you were raising an event. Still, if writing all of the "If InvokeRequired Then Else" loops and delegates is a hassle to you, this is a decent alternative.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #4  
Old 01-23-2009, 09:38 AM
fixitchris's Avatar
fixitchris fixitchris is offline
Contributor
 
Join Date: Dec 2004
Posts: 418
Exclamation

Sticky
Reply With Quote
  #5  
Old 01-23-2009, 12:57 PM
Roger_Wgnr's Avatar
Roger_Wgnr Roger_Wgnr is offline
CodeASaurus Hex

Forum Leader
* Expert *
 
Join Date: Jul 2006
Location: San Antonio TX
Posts: 2,427
Default

Great Info Atma, Having not worked with Multi-Thread apps yet found it very informative. A good addition to the Tutors' corner.
__________________
Code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. ~Martin Golding
The user is a peripheral that types when you issue a read request. ~Peter Williams
MSDN Visual Basic .NET General FAQ
Reply With Quote
  #6  
Old 01-23-2009, 01:36 PM
fixitchris's Avatar
fixitchris fixitchris is offline
Contributor
 
Join Date: Dec 2004
Posts: 418
Default

AW, how about one of these articles on semaphores
Reply With Quote
  #7  
Old 01-23-2009, 02:05 PM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

I'm not really certain that it (semaphores) deserves its own article; it's basically a very small topic and it's only useful in the context of specific problems. I suppose it couldn't hurt to do a small writeup, only because I really don't feel like doing the larger threading article of which this topic should be a subsection
Reply With Quote
  #8  
Old 04-16-2009, 02:19 PM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

Does there need to be an Invoke for each control that is to be updated from a different thread?

Say from your example, you wanted to update both controls at the same time. Couldn't you do this from one Invoke call since both controls were created on the same thread?
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
  #9  
Old 04-16-2009, 08:05 PM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

Yes; your method can do whatever it pleases with any control once it's on the UI thread.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #10  
Old 04-16-2009, 08:31 PM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

Well, that is what I had thought, but I get a very intermittent ObjectDisposedException. It is not a catchable exception, so it crashes the app.

In my form I have these snippets of code:
Code:
Private Delegate Sub UpdateControlsDelegate()

Private WithEvents m_objTimer As SerialTimer

Private Sub SerialTimer_DataReceived(ByVal sender As Object, ByVal e As TimerEventArgs) Handles m_objTimer.DataReceived
  If Me.InvokeRequired Then
    Me.BeginInvoke(New UpdateControlsDelegate(AddressOf UpdateControls))
  Else
    Me.UpdateControls()
  End If
End Sub

Private Sub UpdateControls()
  ' Update various controls
End Sub
The object that the exception is complaining about is the form. This happens as soon as the UpdateControls method is called, according to the stack trace. I would think that the form would have been created in the same thread as the controls that it contains, so that is rather puzzling.
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
  #11  
Old 04-17-2009, 09:00 AM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

You wouldn't really get an ObjectDisposedException from cross threading I don't think; something tells me that something else is going on.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #12  
Old 04-17-2009, 11:13 AM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

Quote:
Originally Posted by AtmaWeapon View Post
You wouldn't really get an ObjectDisposedException from cross threading I don't think; something tells me that something else is going on.
From the docs on BeginInvoke:

Quote:
The delegate is called asynchronously, and this method returns immediately. You can call this method from any thread, even the thread that owns the control's handle. If the control's handle does not exist yet, this method searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, BeginInvoke will throw an exception. Exceptions within the delegate method are considered untrapped and will be sent to the application's untrapped exception handler.
I have tried to trap that exception, to no avail. It gets trapped in the CatchUnhandledException handler, which coincides with what is described above. It's got me rather stumped.

Fortunately, this exception is very infrequent. However, it is still very disconcerting to a user when an app crashes.

I'm using 2.0 of the framework, if it makes any difference.
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
  #13  
Old 04-17-2009, 11:22 AM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

Actually now that you mention it I'm somewhat concerned about the use of BeginInvoke; the IAsyncResult pattern requires that you match all Beginxxxx() calls with an Endxxxx() call; are you ever calling EndInvoke()? I've never tried following this pattern with BeginInvoke(); I've only used Invoke(). Something tells me this might be part of the problem.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #14  
Old 04-17-2009, 11:34 AM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

I haven't tried calling EndInvoke().

I use BeginInvoke() since Invoke() can cause a lockup situation with events raised by the serial port as described at http://social.msdn.microsoft.com/for...7-adff82b19e5e
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
  #15  
Old 04-17-2009, 12:23 PM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

Looking at EndInvoke(), calling it will wait until the asynch operation is completed before it returns. So, I guess I have to wonder why that is any better than calling Invoke()?
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
  #16  
Old 04-20-2009, 11:26 AM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

There's three "rendezvous" methods for the IAsyncResult pattern. They're detailed really well in Jeffery Richter's CLR via C#, but MSDN kind of explains them in the Asynchronous Programming Overview. Unfortunately, it looks like Control.BeginInvoke() doesn't support the callback rendezvous method, which is really what you'd want.

Looking at the documentation for BeginInvoke(), I'm not convinced this is the problem anymore. We need to figure out exactly what object the code thinks is disposed to figure out what's going on.

You might want to consider the Event-Based Asynchronous Pattern instead, but in the end I believe it would work very similar to how the current code works.
Reply With Quote
  #17  
Old 04-21-2009, 01:52 PM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

Quote:
Originally Posted by AtmaWeapon View Post
Looking at the documentation for BeginInvoke(), I'm not convinced this is the problem anymore. We need to figure out exactly what object the code thinks is disposed to figure out what's going on.
The object name in the exception is the form that kicks everything off and is awaiting the data. The form is still open at the time the data comes into the serial port, so it should be able to find the handle.

Quote:
Originally Posted by AtmaWeapon View Post
You might want to consider the Event-Based Asynchronous Pattern instead, but in the end I believe it would work very similar to how the current code works.
I'll have to revisit this at a later date. For now, I have done a quick and dirty implementation of dropping the data into a class level variable and having a timer periodically check for it. It is not elegant but it is working.
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
  #18  
Old 10-28-2010, 06:17 PM
AtmaWeapon's Avatar
AtmaWeapon AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,419
Default

The game changed today with the introduction of the Async CTP. This is beautiful. You will forget about the previous articles like the wastes of time that they are. More details coming over the next day or two as I explore this API.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #19  
Old 10-28-2010, 06:36 PM
PlausiblyDamp's Avatar
PlausiblyDamp PlausiblyDamp is offline
Ultimate Contributor

Forum Leader
* Expert *
 
Join Date: Nov 2003
Location: Wigan, UK
Posts: 1,692
Default

It looks really nice so far, seems a very clean and intuitive coding style from the examples I have looked at, just need to get in the office tomorrow where I have a better connection than a 3G dongle with poor reception and get the CTP downloaded.

I only wish I had a bit more time to spend looking at it. Then again I wish I had more time to look at some the .Net 4 stuff as well.
__________________
Intellectuals solve problems; geniuses prevent them.
-- Albert Einstein

Posting Guidelines Forum Rules Use the code tags
Reply With Quote
  #20  
Old 10-29-2010, 08:08 AM
gpraceman's Avatar
gpraceman gpraceman is offline
Contributor

* Expert *
 
Join Date: Sep 2002
Location: Highlands Ranch, CO
Posts: 556
Default

That is intriguing. Looks like there is a download in order to use CTP with VS 2010. What I am not clear on is if this is now built into the .NET Framework 2.0 or some newer version or if this needs to be an additional install during deployment.
__________________
Awana Grand Prix and Pinewood Derby racing - Where a child, an adult and a small block of wood combine for a lot of fun and memories.
Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is Off
HTML code is Off

Forum Jump

Advertisement:





Free Publications
The ASP.NET 2.0 Anthology
101 Essential Tips, Tricks & Hacks - Free 156 Page Preview. Learn the most practical features and best approaches for ASP.NET.
subscribe
Programmers Heaven C# School Book -Free 338 Page eBook
The Programmers Heaven C# School book covers the .NET framework and the C# language.
subscribe
Build Your Own ASP.NET 3.5 Web Site Using C# & VB, 3rd Edition - Free 219 Page Preview!
This comprehensive step-by-step guide will help get your database-driven ASP.NET web site up and running in no time..
subscribe
 
 
-->