Jump to content
Xtreme .Net Talk

Recommended Posts

Posted

I've been doing some research on Threading and am trying to give it a go. In my sample code, i have everything working except the return values are not getting passed back to the parent thread to be displayed in the list box. the debug.writeline tells me I'm getting the database back.. but can't get it to the parent thread. what am I doing wrong.

 

Option Strict Off
Imports System.Threading
Imports System.Data
Imports System.Data.SqlClient

Public Class Form3
   Inherits System.Windows.Forms.Form

#Region " Windows Form Designer generated code "

   Private Sub Form3_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

   End Sub
   Dim t1 As Thread
   Dim t2 As Thread
   Dim WithEvents oSquare As SquareClass

   Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
       Dim oSquare1 As New SquareClass
       Dim oSquare2 As New SquareClass

       t1 = New Thread(AddressOf oSquare1.CalcSquare)
       t1.Name = "Data"
       oSquare1.vcTableName = "tblPMData"


       t2 = New Thread(AddressOf oSquare2.CalcSquare)
       t2.Name = "Company"
       oSquare2.vcTableName = "tblPMCompany"

       t1.Start()
       t2.Start()

       If t1.Join(500) Then
           Me.ListBox1.Items.Add(oSquare1.Result)
       End If

       If t2.Join(500) Then
           Me.ListBox2.Items.Add(oSquare2.Result)
       End If
   End Sub
   Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
       t1.Abort()
   End Sub
   Public Class SquareClass
       Private mvalue As Integer
       Private msquare As Double
       Private mvcTableName As String
       Private mresult As Integer

       Public Event threadcomplete(ByVal Result As Integer)
       Public Property vcTableName() As String
           Get
               Return mvcTableName
           End Get
           Set(ByVal pValue As String)
               mvcTableName = pValue
           End Set
       End Property
       Public Property Result() As Integer
           Get
               Return mresult
           End Get
           Set(ByVal pValue As Integer)
               mresult = pValue
           End Set
       End Property

       Public Property value1() As Integer
           Get
               Return mvalue
           End Get
           Set(ByVal pValue As Integer)
               mvalue = pValue
           End Set
       End Property
       Public Property square() As Double
           Get
               Return (msquare)
           End Get
           Set(ByVal pSquare As Double)
               msquare = pSquare
           End Set
       End Property
       Public Sub CalcSquare()
           SyncLock GetType(SquareClass)
               Dim cn As SqlConnection
               Dim cm As New SqlCommand
               Dim result As Integer


               cn = New SqlConnection("pwd=idontusesa; UID=sa; server=Ntdts1; database=Monitor")
               cm = New SqlCommand

               With cm
                   .Connection = cn
                   .CommandType = CommandType.Text
                   .CommandText = "select count(1) from " & vcTableName
               End With

               cn.Open()
               Try
                   result = cm.ExecuteScalar
               Catch ex As Exception
                   Throw ex
               End Try

               cn.Close()

               Debug.WriteLine(result)
               'MsgBox(vcTableName & " had " & CType(result, String) & " rows")

               RaiseEvent threadcomplete(result)
           End SyncLock
       End Sub
   End Class
   Private Sub oSquare_threadcomplete(ByVal Result As Integer) Handles oSquare.threadcomplete
       MsgBox(Result)
   End Sub

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

cant you do smthng like this:

 

Class Square
Public Event Done(Sender as Square)
Dim T as System.Threading.Thread
'
'
'
'
'
'Your properties and whatever else
'
'
Public Sub CalculateSquare()

       T=New System.Threading.Thread(Addressof CalcSquare)
       T.Start

end sub

Private CalcSquare()

'           Do your Stuff

Raiseevent Done(Me)
End Sub

End Class

Posted
I think you should be setting mresult in your CalcSquare() code, NOT declaring a local result var (which btw has the same name as the public property, a big no-no).
Posted

Ok.. I have it working (part way). I can't get the raiseevent to fire. I tried to set a break point.. and it hits that break point but never fires off the event. Am I creating the event correctly.

 

Public Class Form4
   Inherits System.Windows.Forms.Form
   Dim WithEvents oSquare1 As SquareClass
   Dim WithEvents oSquare2 As SquareClass


 Public Class SquareClass
       Private mvalue As Double
       Private msquare As Double

       Public Event threadcomplete(ByVal square As SquareClass)

       Public Property value() As Double
           Get
               Return mvalue
           End Get
           Set(ByVal pValue As Double)
               mvalue = pValue
           End Set
       End Property
       Public Property square() As Double
           Get
               Return (msquare)
           End Get
           Set(ByVal pSquare As Double)
               msquare = pSquare
           End Set
       End Property
       Public Sub CalcSquare()
           SyncLock GetType(SquareClass)
               square = value * value
               RaiseEvent threadcomplete(Me)

           End SyncLock
       End Sub
   End Class

   Sub SquareEventHandler(ByVal square As SquareClass) Handles oSquare1.threadcomplete
       MsgBox("Went to the handler")
   End Sub

 

hope this is all the info needed to help me past the current obstacle.

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

It's been a good day..

 

I have the threading working.. it also is returing the data back from the thread through an event... The question I have now.. if I start 2 threads.. and the first thread I start takes longer to run then the second thread.. should the second thread have to wait for the first thread to come back before it completes thread 2.

 

I am passing in a value to each thread.. the first threads value = 3000. in the class that is called it is doing a thread.currentthread.sleep(value). The second thread has a value of 10. it looks like thread 1 always has to complete before thread 2.

 

If I change the values around.. thread 2 = 3000 and thread 1 = 10, then thread 1 comes back.. than then thread 2 3 seconds later.

 

one other thing.. do you have to clean up after the thread completes

 

thanks

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

here you go.. there isn't much code.. so i grabbed it all.

Dim t1 As Thread
   Dim t2 As Thread
   Dim WithEvents oSquare1 As SquareClass
   Private Sub Form4_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
       Me.Label1.Text = Nothing
       Me.Label2.Text = Nothing
   End Sub

   Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
       oSquare1 = New SquareClass

       t1 = New Thread(AddressOf oSquare1.CalcSquare)
       t1.Name = "tblPMData"
       oSquare1.value = 3000
       Me.Label1.Text = "Thread 1 started Processing"
       t1.Start()
       t1.Sleep(10)

       t2 = New Thread(AddressOf oSquare1.CalcSquare)
       t2.Name = "tblPMCompany"
       oSquare1.value = 10
       Me.Label2.Text = "Thread2 started Processing"
       t2.Start()

       Refresh()
       If t1.ThreadState.Stopped Then
           t1 = Nothing
       End If

       If t2.ThreadState.Stopped Then
           t2 = Nothing
       End If

   End Sub
   Public Class SquareClass
       Private mvalue As Double
       Private msquare As Double

       Public Event threadcomplete(ByVal square As Integer, ByVal thread As String)

       Public Property value() As Double
           Get
               Return mvalue
           End Get
           Set(ByVal pValue As Double)
               mvalue = pValue
           End Set
       End Property
       Public Property square() As Double
           Get
               Return (msquare)
           End Get
           Set(ByVal pSquare As Double)
               msquare = pSquare
           End Set
       End Property
       Public Sub CalcSquare()
           SyncLock GetType(SquareClass)
               square = value * value
               Debug.WriteLine(value)
               Thread.CurrentThread.Sleep(value)
               RaiseEvent threadcomplete(square, Thread.CurrentThread.Name.ToString)
           End SyncLock
       End Sub
   End Class

   Sub SquareEventHandler(ByVal square As Integer, ByVal thread As String) Handles oSquare1.threadcomplete
       Debug.WriteLine(square & " - Ran on thread " & thread)

       If thread = "tblPMData" Then
           Me.ListBox1.Items.Add(square)
           Me.Label1.Text = "Thread 1 finished Processing - " & Now.Second & " - " & Now.Millisecond
       Else
           Me.ListBox2.Items.Add(square)
           Me.Label2.Text = "Thread 2 finished processing - " & Now.Second & " - " & Now.Millisecond

       End If
       Refresh()
   End Sub

End Class

thanks.. shannon

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

What advantages does the adhandler give over using the raiseevent like I was doing in test code... which one should I be using??? From what little I've learned since you asked the question.. it looks like they do the same thing.. but what is the reason for 2 different ways of doing the same thing.. I'm not getting something am I.

thanks

shannon

JvCoach23

VB.Net newbie

MS Sql Vet

Posted
One is declarative, one is imperative syntax. The advantage of using AddHandler is that you can easily have one method handle multiple events (I may be wrong, but I don't think this is possible with the declarative syntax).
Posted

Actually, with declarative syntax, you can handle multiple events by using a comma-delimited list of those events.

 

Something like:

 

Private Sub button_Click(.. blah blah args ..) Handles Button1.Click, Button2.Click, Button3.Click

Posted (edited)
Actually, with declarative syntax, you can handle multiple events by using a comma-delimited list of those events.

 

Something like:

 

Private Sub button_Click(.. blah blah args ..) Handles Button1.Click, Button2.Click, Button3.Click

 

Thats true but with the addhandler keyword you can managed controls created on run-time better...

 

Plus if an object goes out of scope for the function that created it but there is still a referrence to it, the event will still fire.

like if a function in a class creates an object by declaring it only in the scope of that function and then passes it to another class if addhandler was used in order to add an event handler in the first class, when the event fires the handler will be used.

 

What is interesting is what happens if an event has multiple handlers? With what sequence will the handlers be called?

Edited by IcingDeath
Posted
What advantages does the adhandler give over using the raiseevent like I was doing in test code... which one should I be using??? From what little I've learned since you asked the question.. it looks like they do the same thing.. but what is the reason for 2 different ways of doing the same thing.. I'm not getting something am I.

thanks

shannon

 

!!!

U still need to use raiseevent with addhandler....

The difference is in the declaration of the object...

 

 

Public Class MyClass

Public Event SomeEvent()

Public Sub MySub
RaiseEvent SomeEvent()
End Sub

End Class

 

AddHandler Style

 


Private Sub Form1_Load(Blah)
Dim MC as new MyClass
addhandler MC.SomeEvent,addressof EventHandler
end sub

Private Sub EventHandler
'Blah
End Sub

 

 

With events style

 


Dim WithEvents MC as new MyClass

Private Sub EventHandler Handles MC.SomeEvent

End Sub

Posted
was playing around some more... with something different within the threads... it appears that if I do a while loop in a thread i start, that the first thread works... going through this loop. However, the second thread I start doesn't appear to even run. So I'm doing to starts, but the threads only work if I take a the while loop inside the class that I call when I create the new thread. can you not do a while loop inside a thread

JvCoach23

VB.Net newbie

MS Sql Vet

Posted
you can definately do a while loop inside a thread. Post your code and show us why you think the second thread isn't being executed.

the reasons I was thinking it was not firing is the debug.writeline I have in each thread. when I take the while loop out.. i see both debug statements come up.. if I have the while loop.. only one debug.writeline every shows up. The dataset that is being used to put information in for the threads has 2 rows in it.

 

Public Class PerfmonClass
       Private mvcServer As String
       Private mvcCompany As String
       Private mvcCategoryName As String
       Private mvcCounterName As String
       Private mvcInstance As String
       Private mintTblPMInstanceId As Integer

       Public Event PerfmonThread()

       Dim WithEvents oCounter As New PerformanceCounter
       Dim Counter As Integer


       Public Property vcServer() As String
       Public Property vcCompany() As String
       Public Property vcCategoryName() As String
       Public Property vcCounterName() As String
       Public Property vcInstance() As String
       Public Property intTblPMInstanceId() As Integer

       Public Sub GetPerfmon()
           SyncLock GetType(PerfmonClass)

               'setup the counter 
               With oCounter
                   .CategoryName = vcCategoryName
                   .CounterName = vcCounterName
                   .InstanceName = vcInstance
                   .MachineName = vcServer
                   .BeginInit()
               End With

               While 1 = 1
                   'poll the counter for the info
                   Counter = oCounter.NextValue
                   Debug.WriteLine(Counter & " - " & Thread.CurrentThread.Name.ToString)
                   Thread.CurrentThread.Sleep(5000)
               End While
           End SyncLock
       End Sub
   End Class

Public Sub GetServerCounters()
       oPerfmon = New PerfmonClass

       Dim ds As DataSet
       ds = wsPerfmon.spPMCategoryInfoForOneServer("home", "s011038home")

       Dim dr As DataRow

       For Each dr In ds.Tables(0).Rows
           Dim perfThread = New Thread(AddressOf oPerfmon.GetPerfmon)
           oPerfmon.intTblPMInstanceId = dr.Item("intTblPMInstanceId")
           oPerfmon.vcCategoryName = dr.Item("vcCategoryName")
           oPerfmon.vcCompany = "home"
           oPerfmon.vcCounterName = dr.Item("vcCounterName")
           oPerfmon.vcInstance = dr.Item("vcInstance")
           oPerfmon.vcServer = dr.Item("vcServer")

           perfThread.name = dr.Item("vcCategoryName")
           Debug.WriteLine(dr.Item("vcCounterName"))
           perfThread.start()
           Thread.Sleep(5)
       Next
   End Sub

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
       GetServerCounters()
   End Sub

 

right now I wasn't trying to raise any events... in the end.. I'll want this loop to write to a database each time it loops through...

 

thanks for the help

Shannon

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

Multithreading to my knowledge can be easily mistaken to mean two processes running on the CPU concurrently. This is not the case. The two threads take it in turn to use processor time similar to two separate programs. If the processor is not told to free itself up for the other thread surely you will get one thread hogging all the process time. Having not used the currentthread.sleep I could not say if this frees the processor or not. Maybe an application.doevents in there would free up the processor for the other thread?

 

Dill

Posted
If the processor is not told to free itself up for the other thread surely you will get one thread hogging all the process time. Having not used the currentthread.sleep I could not say if this frees the processor or not. Maybe an application.doevents in there would free up the processor for the other thread?

 

Dill

 

I believe you are wrong on that...

The OS will automaticly pre-empt threads that are running and give other threads with the same priority a time slice. Only win3.1 had the cooperative multitasking where tasks could claim 100% cpu without being any change of being preempted. That mechanism has been fully abandoned in favor of pre-emptive multitasking (in both winNT and win95, with different levels of succes ;) ) where the OS can give time to other threads of the same priority after some time (20 milliseconds or so, i dont know how much) even though the original thread isnt finished yet.

Note that this is the reason why using multithreading becomes a very hard job. You NEVER KNOWN WHEN your executing thead is pre-empted and another is activated. Making it very VERY important to use the correct thread safety mechanisms like ReaderWriterLock. Many crashes relating to multithreading can never be reproduced because the exact timeing can't be recreated. I can still get Word to crash using background printing on large documents, due to incorrect multithreading in Word (at least Word 2000, havent tried with latest version, but I dont have high hopes).

 

Only if all other threads have lower priority can a thread keep the cpu occupied (even for such a configuration there are ways that the low prioirty threads do get some cpu time).

The sleep does allow the OS to schedule a different thread. I believe even a sleep time of 0 allows the OS to pre-empt the executing thread and start running another thread. Returning to the origal thread when it has gone through its list of threads.

Nothing is as illusive as 'the last bug'.
Posted

With what Wile has said.. and it sounds like it makes sense, why does this code

               While 1 = 1
                   'poll the counter for the info
                   Counter = oCounter.NextValue
                   Debug.WriteLine(Counter & " - " & Thread.CurrentThread.Name.ToString)
                   Thread.CurrentThread.Sleep(5000)
               End While

seem to no allow the second thrid thread to be started. Based on what you are saying.. the first thread.. the one the intial form is loaded with is running, on the button click it loops through and created thread #2 and that runs taking a slight pause when it hits the sleep command... but thread #3 never does fire.. it sounds like it should fire and start #3 regardlesss of the thread sleep command.. seeing how is it suppose to only sleep on the current thread...#2

JvCoach23

VB.Net newbie

MS Sql Vet

Posted
With what Wile has said.. and it sounds like it makes sense, why does this code

...

seem to no allow the second thrid thread to be started. Based on what you are saying.. the first thread.. the one the intial form is loaded with is running, on the button click it loops through and created thread #2 and that runs taking a slight pause when it hits the sleep command... but thread #3 never does fire.. it sounds like it should fire and start #3 regardlesss of the thread sleep command.. seeing how is it suppose to only sleep on the current thread...#2

 

I don't know how exactly the vb SyncLock works, but my guess is that it works the same as the lock statement in C#. The trick is that you pass it something (in your case you pass it the result of GetType(PerfmonClass)), and that every other SyncLock statement that is passed the same argument is halted, until the first one to lock on that particular object, unlocks it.

Now in your case, the first thing you do is get a SyncLock in the GetPerfMon method, and you dont release it until the method is completely done, including the while loop. This means that once the first thread gets the SyncLock, all the other threads, will be waiting until the first thread unlocks, which is never as you loop forever.

 

You can test this by putting a counter increment just before the synclock (Use the System.Threading.Interlocked.Increment method for this to make sure it is thread safe) and that counter should increase. You can even put a break point on that counter and step through the code, you will see that the 2nd and next threads will wait for the SyncLock method.

 

The idea of the SyncLock is correct, you want to protect the member variables of the class from simultanious access by multiple threads, however, in the while loop, during the sleep, there is no need to keep the SyncLock. I suggest you take a good look at that loop and decide where the SyncLock is really needed to protect the member variables, and where it isn't needed, e.g. the sleep. If the lock is not needed, unlock it so other threads can enter.

 

You might want to take a look at the ReaderWriterLock as it allows more control over how member variables are used (allowing multiple reads at the same time, but makes sure that only 1 thread can write at a time, without any thread reading).

Nothing is as illusive as 'the last bug'.
Posted

thanks a million for the lesson.. I'll have to read it a couple times and try to get it goign tonight... thanks again for the explinations.

 

Shannon

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

I'll add a tip that can be usefull while debugging. While running your application, under the Debug -> Windows a lot of items are available. One of them is Threads (default key combo: ctrl + alt + H).

 

Open this window.

 

When your program is paused (e.g. on a breakpoint or just stepping through some code), you can view all the active threads in your application in that window. If the Location of a thread is somewhere in your code, you can double click on that line, and the debugger will show the exact location of where that thread is at that moment, that way you can view what all the different threads are doing.

In most cases this is the easiest way to spot a deadlock or a situation like it seems to be in the code you posted above. When debugging the current code you should see 1 thread inside the loop, and other threads in the same function, but waiting for the SyncLock.

 

Good luck ;).

Nothing is as illusive as 'the last bug'.
Posted (edited)

thanks for the info.. I went in and did a ctrl + alt + H and the threads window came up.. but I'm trying to go through the menu's and don't see where you open it up from if you want to see what all else is there to view.. can you point me in the right direction

 

I also took out the synclock.. and that got the looping to work.. got a lot to play with and learn about.. so once I do.. I'll have more questions.. hopefully you'll still be around to explain.

thanks

shannon

Edited by jvcoach23

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

The menu is in under Debug->Windows (top item in the debug menu). But for some reason the list at development time has only 2 items (breakpoints and immediate) , but at run time there are about 10 of them, including the watches (1-4 and auto), call stack, threads, modules, memory, registers, all the fun things you really wish you'd knew how they work ;).

 

Be carefull though. If you only took out the synclock, you can have two threads calling the Counter = oCounter.NextValue at the same time, giving strange results (e.g. you skip 1 value of counter, and use another one by 2 threads at the same time). Especially as Counter is a member variable of the class and can be accessed by all the threads running.

If you only use that counter inside the while loop, it is saver to declare the counter inside the GetPerfmon method. Variables declared inside a method are always thread safe as another thread executing the same method, has its own instance of that variable. Member variables of classes however will always need protection as they can be accessed by all threads.

 

Here is an experiment you might want to try (I'm assuming the current code, and at least 2 threads running the while loop).

Put a breakpoint on the Counter = oCounter.NextValue and run the code. When the breakpoint hits, let the code proceed one line so the Counter value is updated, but don't execute the Debug.WriteLine yet (Use F10 to execute a single line). Do look up the current value of the Counter and write it down somewhere.

Now go into the thread window, and Freeze (right mouse menu) the current thread. Press F5 to let the program run again (the frozen thread is still paused and won't run yet).

What should happen now is that the thread remains frozen, and another thread will hit the breakpoin on the Counter = oCounter.NextValue line. Let this line execute, and let this thread execute the debug.writeline. Now go back into the thread window and Thaw (right mouse menu again ;) ) the thread you frozen earlier. Switch to this thread (also right mouse menu), and let it execute the debug.writeline statement.

Instead of the original value of Counter the frozen/thawed threat got when it did Counter = oCounter.NextValue (you did write it down I hope ;) ), it now has the same value as the other thread.

It is a bloody pain to reproduce, but race-conditions like these create the weirdest bugs you'll ever find as they are almost impossible to reproduce without spending hours on all possible timing scenario's.

 

One nasty thing about debugging multithreaded applications, if threads are running at the same time, the debugger can switch over from one thread to another without telling you, putting you in a different location of the code without warning, keep a close eye on what thread you are looking in when debugging like that, it is very easy to get confused. Especially when using a sleep command there is a high chance of a thread switch (with sleep it is 100% chance I believe ;) )

Nothing is as illusive as 'the last bug'.
Posted
so if the sleep command is a high threat for thread switch.. and I want the thread to wait for several seconds before it runs again.. what are my options that would make it thread safe.

JvCoach23

VB.Net newbie

MS Sql Vet

Posted

The sleep itself is perfectly thread save, however there is a threat switch when you sleep. A threat switch in itself is perfectly OK, the OS does them and you can't prevent it even if you wanted to, maybe I wasn't clear about that.

 

What must be done is making the rest of the code safe to correctly handle that all member variables can be accessed from two different threads at (virtually, see bellow) the same time if they arent protected. That means that yes, you have limit the actual reading/writing of member variables and make sure only 1 thread at a time can do that. However, make the blocking as small as possible so you block as little as possible or you might run into the problem you had before.

 

Note that when a thread encounters a lock that it can't pass yet (another thread has it), the OS automatically switches to another thread, leaving the original thread at the lock statement.

 

 

You said you removed the SyncLock. What I ment in my reply is: if you don't implement a locking mechanism at all, you will run into a lot of problems.

Sadly multithreading isnt something you can trial and error until everything is fixed, as you will keep running into problems, some only appearing after 4 hours of execution or worse, after the customer started using it.

You really have to look at how all the variables that can be shared between threads (like the member variables in a class) are accessed, and how to protect access to it, with the smallest scope possible. You need to use a locking mechanism (like SyncLock, or a critical section like the ReaderWriterLock) to make sure no 2 threads have access at the same time, but you also need to use it as little as possible to prevent other problems.

 

I think nobody ever claimed multithreading was easier than very difficult. The concepts of multithreading (having 2 or more threads do the same thing at the same time and how to protect everything) are very hard to get a grip on. I've had quite some experience with multithreading in c++, and those concepts of threads, locks, etc still apply to .NET, but in c++ it took me about a year to get a good grip on the basic principles of having your code execute in two locations at the same time and how to protect your application against problems resulting from this.

The .NET framework might make using threads and everything easier, however, the concepts behind them are still just as difficult. Don't try using multithreading just because you want a checkmark on a feature list. The amount of work it takes to get it right is rarely worth the extra feature. Only use it when you have no other option.

 

 

On the virtually remark I made above:

With virtually I mean that with a (old fashioned, pre-Intel hyperthreading) single CPU, the CPU can execute only 1 assembly statement at a time, but assigning a value to a parameter (single line of code in the editor) can be several lines of assembly. So dont assume that 1 line of code is always run without a thread switch half way the line of code.

Nothing is as illusive as 'the last bug'.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...