Unable to append RichTextBox text in Cross-Thread operation VB.NET - vb.net

I'am coding a Multi-Threaded TCP echo server, but the problem is when i call UpdateMessage from ClientHandler class, the RichTextBox's text is not appending.
This is the Form1 class that containts the RichTextBox1
Public Class Form1
Const PORT As Integer = 1234 'The port number on which the server will listen for connection.
Dim ServerSocket As New TcpListener(PORT) 'The Server Socket that will listen for connections on specified port number.
Dim Link As TcpClient 'The Socket that will handle the client.
Dim NumberOfClients As Integer = 0 'The total number of clients connected to the server.
Dim myThread As Thread 'The thread on which the server will handle the client
Dim sc As SynchronizationContext = SynchronizationContext.Current
Public Clienthandler As ClientHandler
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles btnStart.Click
ServerSocket.Start()
' UpdateMessage("Server started..")
myThread = New Thread(AddressOf AcceptClients) 'Handle the client in a different thread
myThread.Start()
btnStart.Text = "Started!"
btnStart.Enabled = False
End Sub
Private Sub AcceptClients()
'Keep accepting and handling the clients.
While (True)
Link = ServerSocket.AcceptTcpClient() 'Accept the client and let the Socket deal with it.
NumberOfClients = NumberOfClients + 1 'update the total number of clients
Dim ClientAddress As String = Link.Client.RemoteEndPoint.ToString
' UpdateMessage("Client number " & NumberOfClients & " connected from " & ClientAddress)
Clienthandler = New ClientHandler
Clienthandler.StartClientThread(Link, NumberOfClients)
Me.sc.Post(AddressOf Clienthandler.UpdateMessage, "Client number " & NumberOfClients & " connected from " & ClientAddress)
End While
'close the sockets
Link.Close() 'close the Socket.
ServerSocket.Stop() 'Stop the Server Socket.
End Sub
End Class
And here is the ClientHandler class:
Public Class ClientHandler
Private Link As TcpClient
Private ClientNumber As Integer
Dim NumberOfMessages As Integer = 0 'The number of messages that client has sent.
Dim Stream As NetworkStream 'The stream being used for sending/receiving data over Socket.
Dim scHandler As SynchronizationContext = SynchronizationContext.Current
Public Sub StartClientThread(Link As TcpClient, ClientNumber As Integer)
'Initialize the class variables with the arguments passed in StartClient
Me.Link = Link
Me.ClientNumber = ClientNumber
'Start the thread
Dim ClientThread As New Thread(AddressOf Chat)
ClientThread.Start()
UpdateMessage("Thread for client " & ClientNumber & " started!")
End Sub
Public Sub UpdateMessage2(Message As String)
Me.scHandler.Post(AddressOf UpdateMessage2, Message)
End Sub
Public Sub UpdateMessage(Message As String)
Message = vbNewLine & Trim(Message)
If Message.Length < 1 Then
Message = "empty"
End If
MessageBox.Show("Updating message!" & vbNewLine & Message)
Form1.RichTextBox1.AppendText(Message)
End Sub
The same problem still exits, the RichTextBox is being updated from UI Thread only.

To be a bit more specific than Hans Passant, using the form name where an object is expected makes use of the default instance of the form, which is an instance that the system creates and manages for you. The default instance is thread-specific though, which means that there is one default instance per thread. If you use the default instance in a secondary thread then you are using a different object to the one that you displayed on the UI thread. You might read a bit more about default instances here:
http://jmcilhinney.blogspot.com.au/2009/07/vbnet-default-form-instances.html
You need to somehow marshal a method call back to the UI thread and then update the existing form instance on that thread. There are a couple of ways that can be done:
Have access to the existing form object and call its Invoke method.
Use the SynchronizationContext class to marshal the method call to the UI thread and then use the default instance.

Class myClass
Friend TB As TextBox
Class myThread
' ...
End Class
End Class
Class Form1
Overrides Sub OnLoad
_tcpClient = New myClass
Controls.Add(_tcpClient.TB)
_tcpClient.doSomething()
End Sub
End Class
like that, as per my comments.

Related

How to share a resouce (a serial port) with several threads

I currently have a program with 4 threads.
4 Threads are "Worker Threads" that each have a dedicated serial port that monitors a dedicated device. So Worker Thread 1 monitors Com port 1, Thread 2 monitors Com Port 2 etc.
All this all working fine. No conflicts.
However, the 4 worker threads all have to send commands to a 5th Comm Port as well, which is a communication link to a device that can powercycle the other devices.
I.E. they all have to share a specific resource, the 5th com port.
When they do send a command to this 5th shared each thread has to wait until the command has finished before continuing.
I've followed the coding example from Dan (thanks!) and tried to form a prototype test code.
This SEEMS to work.
I would appreciate a critical review of the code to see if I'm going in the right direction.
Apologies if I'm not explaining this very well as whilst I've used threads before. Handling a shared resource is new to me. Also I'm just getting to grips with how Stackoverflow works!!
Many thanks
A simplified solution using a shared instance of a resource and a lock.
Public Class Resource
Public Function Read() As String
Return "result"
End Function
End Class
Public Class ResourceUser
Private Shared resourceLock As New Object
Private Shared r As New Resource()
Public Function Read()
Dim res As String
SyncLock resourceLock
res = r.Read()
End SyncLock
Return res
End Function
End Class
Example usage:
Sub Main()
Dim t1 As New Threading.Thread(AddressOf DoSomethingWithResourceUser)
Dim t2 As New Threading.Thread(AddressOf DoSomethingWithResourceUser)
t1.Start()
t2.Start()
End Sub
Private Sub DoSomethingWithResourceUser()
Dim ru As New ResourceUser()
ru.Read()
End Sub
Here's another example more specific to serial comms. It uses a dictionary to keep track of physical comm resources and their relevant locks, so that you can do asynchronous access to different comm ports, but synchronize access to each single comm port.
Sub Main()
Dim c1 As New CommPortThreadSafe("COM1")
Dim c2 As New CommPortThreadSafe("COM2")
Dim c3 As New CommPortThreadSafe("COM1")
Dim t1 As New Threading.Thread(Sub() c1.Read())
Dim t2 As New Threading.Thread(Sub() c2.Read())
Dim t3 As New Threading.Thread(Sub() c3.Read())
' t1 and t3 can't be in critical region at same time
' t2 will be able to run through critical region
t1.Start()
t2.Start()
t3.Start()
End Sub
Public Class CommPort
Public Property Name As String
Public Function Read() As String
Return "result"
End Function
End Class
Public Class CommPortThreadSafe
Private Shared resourceLocks As New Dictionary(Of String, Object)()
Private Shared comms As New Dictionary(Of String, CommPort)()
Private Shared collectionLock As New Object()
Private commPortName As String
' constructor takes the comm port name
' so the appropriate dictionaries can be set up
Public Sub New(commPortName As String)
SyncLock collectionLock
Me.commPortName = commPortName
If Not comms.ContainsKey(commPortName) Then
Dim c As New CommPort()
Dim o As New Object()
c.Name = commPortName
' configure comm port further etc.
comms.Add(commPortName, c)
resourceLocks.Add(commPortName, o)
End If
End SyncLock
End Sub
Public Function Read()
Dim res As String
SyncLock resourceLocks(Me.commPortName)
res = comms(Me.commPortName).Read()
End SyncLock
Return res
End Function
End Class
To address your recent edits:
Threads A would all declare the comm port in the same way. Actually this is a benefit of this pattern (similar to multiton pattern) which works like a singleton when only one comm port is used. This code could be used in all threads:
Dim myCommPort As New CommPortThreadSafe("COM1")
The lock inside the read is going to synchronize access to COM1 because "COM1" (the name of the comm port) is actually the key to the Dictionary<string, object> used for locking. So when any thread reaches this code, keying with the same key, that region will be accessible only to a single thread because they all use the same key.
SyncLock resourceLocks(Me.commPortName)
res = comms(Me.commPortName).Read()
End SyncLock
As you saw, that string is set up in the constructor so as long as all the threads create their object passing the same string to the constructor, they will all have underlying indirect references to the same CommPort. The constructor can only create the instance if the name doesn't already exist in its dictionary:
If Not comms.ContainsKey(commPortName) Then
Dim c As New CommPort()
Here's another example usage with just one comm port:
Sub Main()
Dim ts As New ThreadStart(
Sub()
Dim c As New CommPortThreadSafe("COM1")
For i As Integer = 0 To 99
c.Read()
Next
End Sub)
Dim t1 As New Threading.Thread(ts)
Dim t2 As New Threading.Thread(ts)
Dim t3 As New Threading.Thread(ts)
Dim t4 As New Threading.Thread(ts)
t1.Start()
t2.Start()
t3.Start()
t4.Start()
End Sub
In this example we start 4 threads which each perform the code in the threadstart. There is a loop reading the comm port. If you test this out, you will see that it is thread-safe as long as the entire read takes place inside Read(), which you will need to develop of course. You may have another layer in which you are sending custom commands and waiting for a response. These two actions should both be inside a single SyncLock in each custom function. Thread B should use the same class if it's doing a similar thing.
FORM CODE FOR TESTING
Imports System.Threading
Public Class Form1
Private Worker(4) As jWorker '4 Worker Object
Public myWorkerThread(4) As Threading.Thread '4 runtime threads
Private Checker As jChecker '1 Checking Object
Public myCheckerThread As Threading.Thread ' Thread to check status of Resource
Dim MainThreadResouce As jResourceUser
'Assume the actual serial port is opened here
'so its available for access by the jResource Object.
'Pressing button 1 will start up all the threads
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'setup a another ResourceUser object here so can see one of the shared values from the form.
MainThreadResouce = New jResourceUser
'Setup and start worker threads - basically these do work and regularly
'send commands to a shared resource
For I = 1 To 4
Worker(I) = New jWorker
Worker(I).id = I
myWorkerThread(I) = New Threading.Thread(AddressOf Worker(I).doWork)
myWorkerThread(I).Start()
Next I
'Start Checking thread - regularly checks something in the resource
'please ignore this for now!
Checker = New jChecker
myCheckerThread = New Threading.Thread(AddressOf Checker.dochecking)
myCheckerThread.Start()
End Sub
End Class
========================
WORKER THREAD, MODELLING THE MONITORING THREADS
Imports System.Threading
Public Class jWorker
Private _id As Integer = 0
Private _workCount As Integer = 0
Private resourceUser As jResourceUser
Public workerStatus As String = ""
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
Me._id = Value
End Set
End Property
Public ReadOnly Property count() As Integer
Get
Return _workCount
End Get
End Property
Public Sub New()
resourceUser = New jResourceUser
End Sub
Sub doWork()
workerStatus = "Started"
Do Until False
Thread.Sleep(1000)
_workCount += 1
If _workCount Mod 5 = 0 Then
'THe line below would cause a bottleneck
'As it prevents the system trying to powercycle any other
'modems whilst its doing just one.
'resourceUser.PowerCycle(_id)
Debug.Print("W" & _id & ", Worker - Requesting Power OFF +++++++++++++")
resourceUser.Poweroff(_id)
Debug.Print("W" & _id & ", Worker - Waiting 10 secs for modem to settle")
Thread.Sleep(10000)
Debug.Print("W" & _id & ", Worker - Requesting Power ON")
resourceUser.PowerOn(_id)
Debug.Print("W" & _id & ", Worker - Finished Power cycle ------------")
End If
Loop
End Sub
End Class
===================
RESOURCE USER - TO CONTROL ACCESS TO SHARED RESOURCE
Public Class jResourceUser
'Variables for handling resouce locking
Private Shared resourceLock As New Object
Private Shared _resource As New jResource("Com1", "ON")
'keeps a status of which workers have signalled an OFF or ON via the Resource
'in the form of a string 11213141 (device number (1-4) - 1 for on, 0 for off
Private Shared _powerStatus As String
Public ReadOnly Property PowerStatus As String
Get
Return _powerStatus
End Get
End Property
Public Sub New()
_powerStatus = _resource.PowerState
End Sub
Sub PowerOn(ByVal WorkerID As Integer)
Debug.Print("W" & WorkerID & ", ResouceUser - requesting Lock for Power ON [" & _powerStatus & "]")
SyncLock resourceLock
_resource.TurnOn(WorkerID)
_powerStatus = _resource.PowerState
Debug.Print("W" & WorkerID & ", ResouceUser - Turned On, Device statuses " & _powerStatus & "]")
End SyncLock
End Sub
Sub Poweroff(ByVal WorkerID As Integer)
Debug.Print("W" & WorkerID & ", ResouceUser requesting Lock for Power OFF [" & _powerStatus & "]")
SyncLock resourceLock
_resource.TurnOff(WorkerID)
_powerStatus = _resource.PowerState
Debug.Print("W" & WorkerID & ", ResouceUser - Turned Off, Device statuses [" & _powerStatus & "]")
End SyncLock
End Sub
'Not going to work as it blocks the whole system when it could be
'reseting other modems.
Sub PowerCycle(ByVal WorkerID As Integer)
SyncLock resourceLock
Debug.Print("W" & WorkerID & ", ResourceUser - Requesting Power CYcle LockPower")
_resource.PowerCycle(WorkerID)
_powerStatus = _resource.PowerState
Debug.Print("W" & WorkerID & ", ResourceUser - Power Cycled")
End SyncLock
End Sub
Function CheckState() As String
SyncLock resourceLock
Return _resource.CheckState
_powerStatus = _resource.PowerState
End SyncLock
End Function
End Class
============
RESOURCE - FOR ACTUAL WORK ON SHARED RESOURCE
'THis code would directly handle interactions
'with one specific com port that has already
'been configured and opened on the main thread.
Public Class jResource
Private _ComPort As String
Private _state As String
Private _PowerState As String
Private _CheckState As String
'Record the com port used for this resource
Public Property ComPort() As String
Get
Return _ComPort
End Get
Set(ByVal Value As String)
Me._ComPort = Value
End Set
End Property
'Returns the a particular status of the resouce
Public ReadOnly Property CheckState As String
Get
'here I'd send a few command to the comm port
'pick u the response and return it
Return _state
End Get
End Property
'The connected serial port is used to power cycle serveral devices
'this property returns the state of all those devices.
Public ReadOnly Property PowerState() As String
Get
Return _PowerState
End Get
End Property
Public Sub New(ByVal name, ByRef state)
Me._ComPort = name
Me._state = state
Me._PowerState = "11213141"
Me._CheckState = "ON"
End Sub
'Simulate a off command sent by a worker
'via its resourceUser object
Public Sub TurnOn(ByVal intWorker As Integer)
'simulate some work with the com port
Dim myTimeOut As DateTime
myTimeOut = Now.AddMilliseconds(500)
Do Until Now > myTimeOut
Loop
'Set the status to show that Device is on.
_PowerState = _PowerState.Replace(intWorker & "0", intWorker & "1")
Debug.Print("W" & intWorker & ", Resource - issued TurnON, Device Statuses [" & _PowerState & "]")
End Sub
Public Sub TurnOff(ByVal intWorker As Integer)
'simulate some work
Dim myTimeOut As DateTime
myTimeOut = Now.AddMilliseconds(500)
Do Until Now > myTimeOut
Loop
'Here would send command to Com port
'Set the status to show that Device is Off.
_PowerState = _PowerState.Replace(intWorker & "1", intWorker & "0")
Debug.Print("W" & intWorker & ", Resource - issued TurnOFF, Device Statuses [" & _PowerState & "]")
End Sub
Public Sub PowerCycle(ByVal intWorker As Integer)
Debug.Print("W" & intWorker & ", Resource - issued PowerCycle, Device Statuses [" & _PowerState & "]")
'Here would send command to Com port
'Set the status to show that Device is Off.
_PowerState = _PowerState.Replace(intWorker & "1", intWorker & "0")
Debug.Print("W" & intWorker & ", Resource - issued TurnOFF, Device Statuses [" & _PowerState & "]")
'simulate some work - takes a while for device to turn off
Dim myTimeOut As DateTime
myTimeOut = Now.AddMilliseconds(10000)
Do Until Now > myTimeOut
Loop
'Here would send command to Com port
'Set the status to show that Device is Off.
_PowerState = _PowerState.Replace(intWorker & "1", intWorker & "1")
Debug.Print("W" & intWorker & ", Resource - issued TurnON, Device Statuses [" & _PowerState & "]")
'simulate some work
myTimeOut = Now.AddMilliseconds(10000)
Do Until Now > myTimeOut
Loop
'Here would send command to Com port
End Sub
End Class

Is there away to switch from a Worker Thread to the Main (UI) thread?

I apologize in advance if my question is too long-winded. I looked at the question “How to update data in GUI with messages that are being received by a thread of another class?” and it is very close to what I am trying to do but the answer was not detailed enough to be helpful.
I have converted a VB6 app to VB.NET (VS2013). The main function of the app is to send queries to a Linux server and display the results on the calling form. Since the WinSock control no longer exists, I’ve created a class to handle the functions associated with the TcpClient class. I can successfully connect to the server and send and receive data.
The problem is that I have multiple forms that use this class to send query messages to the server. The server responds with data to be displayed on the calling form. When I try to update a control on a form, I get the error "Cross-thread operation not valid: Control x accessed from a thread other than the thread it was created on." I know I’m supposed to use Control.InvokeRequired along with Control.Invoke in order to update controls on the Main/UI thread, but I can’t find a good, complete example in VB. Also, I have over 50 forms with a variety of controls on each form, I really don’t want to write a delegate handler for each control. I should also mention that the concept of threads and delegates is very new to me. I have been reading everything I can find on this subject for the past week or two, but I’m still stuck!
Is there some way to just switch back to the Main Thread? If not, is there a way I can use Control.Invoke just once to cover a multitude of controls?
I tried starting a thread just after connecting before I start sending and receiving data, but netStream.BeginRead starts its own thread once the callback function fires. I also tried using Read instead of BeginRead. It did not work well if there was a large amount of data in the response, BeginRead handled things better. I feel like Dorothy stuck in Oz, I just want to get home to the main thread!
Thanks in advance for any help you can provide.
Option Explicit On
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
Friend Class ATISTcpClient
Public Event Receive(ByVal data As String)
Private Shared WithEvents oRlogin As TcpClient
Private netStream As NetworkStream
Private BUFFER_SIZE As Integer = 8192
Private DataBuffer(BUFFER_SIZE) As Byte
Public Sub Connect()
Try
oRlogin = New Net.Sockets.TcpClient
Dim localIP As IPAddress = IPAddress.Parse(myIPAddress)
Dim localPrt As Int16 = myLocalPort
Dim ipLocalEndPoint As New IPEndPoint(localIP, localPrt)
oRlogin = New TcpClient(ipLocalEndPoint)
oRlogin.NoDelay = True
oRlogin.Connect(RemoteHost, RemotePort)
Catch e As ArgumentNullException
Debug.Print("ArgumentNullException: {0}", e)
Catch e As Net.Sockets.SocketException
Debug.Print("SocketException: {0}", e)
End Try
If oRlogin.Connected() Then
netStream = oRlogin.GetStream
If netStream.CanRead Then
netStream.BeginRead(DataBuffer, 0, BUFFER_SIZE, _
AddressOf DataArrival, DataBuffer)
End If
Send(vbNullChar)
Send(User & vbNullChar)
Send(User & vbNullChar)
Send(Term & vbNullChar)
End If
End Sub
Public Sub Send(newData As String)
On Error GoTo send_err
If netStream.CanWrite Then
Dim sendBytes As [Byte]() = Encoding.UTF8.GetBytes(newData)
netStream.Write(sendBytes, 0, sendBytes.Length)
End If
Exit Sub
send_err:
Debug.Print("Error in Send: " & Err.Number & " " & Err.Description)
End Sub
Private Sub DataArrival(ByVal dr As IAsyncResult)
'This is where it switches to a WorkerThread. It never switches back!
On Error GoTo dataArrival_err
Dim myReadBuffer(BUFFER_SIZE) As Byte
Dim myData As String = ""
Dim numberOfBytesRead As Integer = 0
numberOfBytesRead = netStream.EndRead(dr)
myReadBuffer = DataBuffer
myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
Do While netStream.DataAvailable
numberOfBytesRead = netStream.Read(myReadBuffer, 0, myReadBuffer.Length)
myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
Loop
'Send data back to calling form
RaiseEvent Receive(myData)
'Start reading again in case we don‘t have the entire response yet
If netStream.CanRead Then
netStream.BeginRead(DataBuffer, 0,BUFFER_SIZE,AddressOf DataArrival,DataBuffer)
End If
Exit Sub
dataArrival_err:
Debug.Print("Error in DataArrival: " & err.Number & err.Description)
End Sub
Instead of using delegates one could use anonymous methods.
Singleline:
uicontrol.Window.Invoke(Sub() ...)
Multiline:
uicontrol.Window.Invoke(
Sub()
...
End Sub
)
If you don't want to pass an UI control every time you need to invoke, create a custom application startup object.
Friend NotInheritable Class Program
Private Sub New()
End Sub
Public Shared ReadOnly Property Window() As Form
Get
Return Program.m_window
End Get
End Property
<STAThread()> _
Friend Shared Sub Main()
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Dim window As New Form1()
Program.m_window = window
Application.Run(window)
End Sub
Private Shared m_window As Form
End Class
Now, you'll always have access to the main form of the UI thread.
Friend Class Test
Public Event Message(text As String)
Public Sub Run()
Program.Window.Invoke(Sub() RaiseEvent Message("Hello!"))
End Sub
End Class
In the following sample code, notice that the Asynchronous - Unsafe run will throw a Cross-thread exception.
Imports System.Threading
Imports System.Threading.Tasks
Public Class Form1
Public Sub New()
Me.InitializeComponent()
Me.cbOptions = New ComboBox() With {.TabIndex = 0, .Dock = DockStyle.Top, .DropDownStyle = ComboBoxStyle.DropDownList} : Me.cbOptions.Items.AddRange({"Asynchronous", "Synchronous"}) : Me.cbOptions.SelectedItem = "Asynchronous"
Me.btnRunSafe = New Button() With {.TabIndex = 1, .Dock = DockStyle.Top, .Text = "Run safe!", .Height = 30}
Me.btnRunUnsafe = New Button() With {.TabIndex = 2, .Dock = DockStyle.Top, .Text = "Run unsafe!", .Height = 30}
Me.tbOutput = New RichTextBox() With {.TabIndex = 3, .Dock = DockStyle.Fill}
Me.Controls.AddRange({Me.tbOutput, Me.btnRunUnsafe, Me.btnRunSafe, Me.cbOptions})
Me.testInstance = New Test()
End Sub
Private Sub _ButtonRunSafeClicked(s As Object, e As EventArgs) Handles btnRunSafe.Click
Dim mode As String = CStr(Me.cbOptions.SelectedItem)
If (mode = "Synchronous") Then
Me.testInstance.RunSafe(mode)
Else 'If (mode = "Asynchronous") Then
Task.Factory.StartNew(Sub() Me.testInstance.RunSafe(mode))
End If
End Sub
Private Sub _ButtonRunUnsafeClicked(s As Object, e As EventArgs) Handles btnRunUnsafe.Click
Dim mode As String = CStr(Me.cbOptions.SelectedItem)
If (mode = "Synchronous") Then
Me.testInstance.RunUnsafe(mode)
Else 'If (mode = "Asynchronous") Then
Task.Factory.StartNew(Sub() Me.testInstance.RunUnsafe(mode))
End If
End Sub
Private Sub TestMessageReceived(text As String) Handles testInstance.Message
Me.tbOutput.Text = (text & Environment.NewLine & Me.tbOutput.Text)
End Sub
Private WithEvents btnRunSafe As Button
Private WithEvents btnRunUnsafe As Button
Private WithEvents tbOutput As RichTextBox
Private WithEvents cbOptions As ComboBox
Private WithEvents testInstance As Test
Friend Class Test
Public Event Message(text As String)
Public Sub RunSafe(mode As String)
'Do some work:
Thread.Sleep(2000)
'Notify any listeners:
Program.Window.Invoke(Sub() RaiseEvent Message(String.Format("Safe ({0}) # {1}", mode, Date.Now)))
End Sub
Public Sub RunUnsafe(mode As String)
'Do some work:
Thread.Sleep(2000)
'Notify any listeners:
RaiseEvent Message(String.Format("Unsafe ({0}) # {1}", mode, Date.Now))
End Sub
End Class
End Class
Thank you to those who took the time to make suggestions. I found a solution. Though it may not be the preferred solution, it works beautifully. I simply added MSWINSCK.OCX to my toolbar, and use it as a COM/ActiveX component. The AxMSWinsockLib.AxWinsock control includes a DataArrival event, and it stays in the Main thread when the data arrives.
The most interesting thing is, if you right click on AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent and choose Go To Definition, the object browser shows the functions and delegate subs to handle the asynchronous read and the necessary delegates to handle BeginInvoke, EndInvoke, etc. It appears MicroSoft has already done the hard stuff that I did not have the time or experience to figure out on my own!

How can I properly implement threaded downloads using WebClient with a BlockingCollection?

I'm attempting to make a multi-threaded download manager that has a limit of 4 concurrent downloads. In my research, I came across the following: C# Downloader: should I use Threads, BackgroundWorker or ThreadPool?
[edit] updated code:
Imports System.Net
Imports System.Collections.Concurrent
Imports System.ComponentModel
Imports System.Threading
Public Class Form1
Const MaxClients As Integer = 4
' create a queue that allows the max items
Dim ClientQueue As New BlockingCollection(Of WebClient)(MaxClients)
' queue of urls to be downloaded (unbounded)
Dim UrlQueue As New Queue(Of String)()
Dim downloadThread As Thread
'Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' create four WebClient instances and put them into the queue
For i As Integer = 0 To MaxClients - 1
Dim cli = New WebClient()
AddHandler cli.DownloadFileCompleted, AddressOf DownloadFileCompleted
AddHandler cli.DownloadProgressChanged, AddressOf DownloadProgressChanged
ClientQueue.Add(cli)
Next
' Fill the UrlQueue here
UrlQueue.Enqueue("http://www.gnu.org/licenses/gpl-1.0.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/gpl-2.0.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/gpl-3.0.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/lgpl-2.1.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/lgpl-3.0.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/fdl-1.1.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/fdl-1.2.txt")
UrlQueue.Enqueue("http://www.gnu.org/licenses/fdl-1.3.txt")
downloadThread = New Thread(AddressOf downloadQueue)
downloadThread.IsBackground = True
downloadThread.Start()
End Sub
Private Sub downloadQueue()
' Now go until the UrlQueue is empty
While UrlQueue.Count > 0
Dim cli As WebClient = ClientQueue.Take() ' blocks if there is no client available
Dim url As String = UrlQueue.Dequeue()
Dim fname As String = CreateOutputFilename(url)
cli.DownloadFileAsync(New Uri(url), fname, New DownloadArgs(url, fname, cli))
AppendText(url & " started" & vbCrLf)
End While
End Sub
Private Sub DownloadProgressChanged(sender As Object, e As DownloadProgressChangedEventArgs)
Dim args As DownloadArgs = DirectCast(e.UserState, DownloadArgs)
' Do status updates for this download
End Sub
Private Sub DownloadFileCompleted(sender As Object, e As AsyncCompletedEventArgs)
Dim args As DownloadArgs = DirectCast(e.UserState, DownloadArgs)
' do whatever UI updates
Dim url As String = "Filename" '<============I'd like to be able to pass the filename or URL but can't figure this out
AppendText(url & " completed" & vbCrLf)
' now put this client back into the queue
ClientQueue.Add(args.Client)
End Sub
Public Function CreateOutputFilename(ByVal url As String) As String
Try
Return url.Substring(url.LastIndexOf("/") + 1)
Catch ex As Exception
Return url
End Try
End Function
Private Delegate Sub SetTextCallback(text As String)
Private Sub AppendText(text As String)
If Me.TextBox1.InvokeRequired Then
TextBox1.Invoke(New Action(Of String)(AddressOf AppendText), text)
Return
End If
Me.TextBox1.AppendText(text)
Me.TextBox1.SelectionStart = TextBox1.TextLength
Me.TextBox1.ScrollToCaret()
End Sub
End Class
Class DownloadArgs
Public ReadOnly Url As String
Public ReadOnly Filename As String
Public ReadOnly Client As WebClient
Public Sub New(u As String, f As String, c As WebClient)
Url = u
Filename = f
Client = c
End Sub
End Class
This will successfully download the first 4 files in the UrlQueue, but it then seems to freeze and no further files download. I'd imagine the problem lies in something minor I missed in the process of converting from C# to vb.net, but I can't seem to figure this out.
ClientQueue.Take() blocks the UI thread. Also, WebClient will want to raise the DownloadFileCompleted event on the UI thread - but it is already blocked by ClientQueue.Take(). You have a deadlock.
To resolve this, you got to move your blocking loop to another background thread.
You are blocking the ability for your async queue to process. Not sure this is the "Correct" way to do this but the changes here make it work:
While UrlQueue.Count > 0
Do While ClientQueue.Count = 0
Application.DoEvents()
Loop
Dim cli As WebClient = ClientQueue.Take() ' blocks if there is no client available
Dim url As String = UrlQueue.Dequeue()
Dim fname As String = CreateOutputFilename(url) ' or however you get the output file name
cli.DownloadFileAsync(New Uri(url), fname, New DownloadArgs(url, fname, cli))
End While

VB.NET TCPClient/Server Issue- Sending on a Timer

So my program starts out by counting how many processes of "XProcess" and has a timer to check every 1 second, which works great! I have the input of the count going into Setting variable. I then have a sub routine that takes that Setting variable, along with a IF statement to output that the TCPClient is sending the string ("One Process").
Well the issue is, I have no event to use with the sub routine and so I tied it in with the timer to send the message out every 1 second. The TCPClient sends it to a local address(127.0.0.1) right now, and sends it to a Textbox. Well PROBLEM!!!
It repeats ("One Process") over and over and over, which I can see why this happens.
So with the Code below, how can the TCPClient send an notification of how many processes of "XProcess" to the TCPServer and the TCPSERVER to spit out that 1 or more processes are running ? (with out the SERVER repeating the string (integer works as wel) over and over again)
The below works, but repeats how many processes are running as a string.
MainWindow.xaml.vb
Imports System.Windows.Threading
Imports System.Net.Sockets
Public Class MainWindow
Private Run_ProgramRunCheck_timer As New DispatcherTimer
Private Run_RecieveCheck_timer As New DispatcherTimer
Dim processCount As Integer
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
ServerStart()
'Check for program changes every second
' Set interval for timer
Run_ProgramRunCheck_timer.Interval = TimeSpan.FromMilliseconds(1000)
'Start timer on button click
Run_ProgramRunCheck_timer.Start()
AddHandler Run_ProgramRunCheck_timer.Tick, AddressOf __ProgramCheck
'Check for Message Recieve every second
' Set interval for timer
Run_RecieveCheck_timer.Interval = TimeSpan.FromMilliseconds(1000)
'Start timer on button click
Run_RecieveCheck_timer.Start()
AddHandler Run_RecieveCheck_timer.Tick, AddressOf _RecieveMessageConvert
TCPClientSender()
End Sub
Public Sub __ProgramCheck()
'This sub will be checked every 1 seconds for changes
'Count number of processes
processCount = Process.GetProcessesByName("tvnviewer").Count()
My.Settings.TotalProcesses = processCount
End Sub
Public Sub TCPClientSender()
My.Settings.TotalProcesses = 0
If My.Settings.TotalProcesses = 1 Then
Dim port As Int32 = 50000
Dim client As New TcpClient("127.0.0.1", port)
' Translate the passed message into ASCII and store it as a Byte array.
Dim data As [Byte]() = System.Text.Encoding.ASCII.GetBytes("One Process")
' Get a client stream for reading and writing.
' Stream stream = client.GetStream();
Dim stream As NetworkStream = client.GetStream()
' Send the message to the connected TcpServer.
stream.Write(data, 0, data.Length)
End If
End Sub
Public Sub _RecieveMessageConvert()
TextBlock1.Text = My.Settings.StoreSentMessage
End Sub
End Class
TCPServer.vb
Imports System.Net
Imports System.Threading
Imports System.Net.Sockets
Imports System.IO
Public Module TCPServer
Dim Server = New TcpListener(IPAddress.Any, 50000) ' <-- Listen on Port 50,000
Dim Client As New TcpClient
Private ServerThread As Thread = Nothing
Dim Message As String = ""
Private Threads As New List(Of Thread)
Public Sub ServerStart()
ServerThread = New Thread(AddressOf ConnectionListener)
ServerThread.IsBackground = True
ServerThread.Start()
End Sub
Private Sub ConnectionListener()
Try
Server.Start()
While True
Dim client As TcpClient = Server.AcceptTcpClient ' Blocks until Connection Request is Received
Dim Reader As New StreamReader(client.GetStream())
While Reader.Peek > -1
Message = Message + Convert.ToChar(Reader.Read()).ToString
End While
My.Settings.StoreSentMessage = Message
End While
Catch ex As Exception
End Try
End Sub
End Module
Dont you just want to do this:
Dim data As [Byte]() = System.Text.Encoding.ASCII.GetBytes(My.Settings.TotalProcesses)
I dont know how you ever get it to say "One Process" when you set the value to zero and then check if the value is 1 on the next line???
My.Settings.TotalProcesses = 0
If My.Settings.TotalProcesses = 1 Then

TCPClient disconnects after several hours

I've created a Windows service that waits for TCPClient connections and relays any messages to all connected clients (except the sender). My code is based on this example.
One client connects when an event is triggered, sends some progress updates and then disconnects. The other clients are front end applications that receive and display the update.
If these clients are left idle for several hours they seem to loose the connection without any error\warning. I cannot find any relevent timouts for idle periods, is there something I am missing?
Service Code:
Protected Overrides Sub OnStart(ByVal args() As String)
_Listener = New TcpListener(IPAddress.Any, 1314)
_Listener.Start()
ListenForClient()
_ConnectionMontior = Task.Factory.StartNew(AddressOf DoMonitorConnections, New MonitorInfo(_Listener, _Connections), TaskCreationOptions.LongRunning)
End Sub
Private Sub ListenForClient()
Dim info As New ConnectionInfo(_Listener)
_Listener.BeginAcceptTcpClient(AddressOf DoAcceptClient, info)
End Sub
Private Sub DoAcceptClient(result As IAsyncResult)
Try
Dim monitorInfo As MonitorInfo = CType(_ConnectionMontior.AsyncState, MonitorInfo)
If monitorInfo.Listener IsNot Nothing AndAlso Not monitorInfo.Cancel Then
Dim info As ConnectionInfo = CType(result.AsyncState, ConnectionInfo)
monitorInfo.Connections.Add(info)
info.AcceptClient(result)
ListenForClient()
info.AwaitData()
End If
Catch ex As Exception
WriteToEventLog("DoAcceptClient: " & ex.Message)
End Try
End Sub
Private Sub DoMonitorConnections()
Try
'Create delegate for updating output display
' Dim doAppendOutput As New Action(Of String)(AddressOf AppendOutput)
'Get MonitorInfo instance from thread-save Task instance
Dim monitorInfo As MonitorInfo = CType(_ConnectionMontior.AsyncState, MonitorInfo)
'Implement client connection processing loop
Do
'Create temporary list for recording closed connections
Dim lostConnections As New List(Of ConnectionInfo)
'Examine each connection for processing
For Each info As ConnectionInfo In monitorInfo.Connections
If info.Client.Connected Then
'Process connected client
If info.DataQueue.Count > 0 Then
'The code in this If-Block should be modified to build 'message' objects
'according to the protocol you defined for your data transmissions.
'This example simply sends all pending message bytes to the output textbox.
'Without a protocol we cannot know what constitutes a complete message, so
'with multiple active clients we could see part of client1's first message,
'then part of a message from client2, followed by the rest of client1's
'first message (assuming client1 sent more than 64 bytes).
Dim messageBytes As New List(Of Byte)
While info.DataQueue.Count > 0
messageBytes.Add(info.DataQueue.Dequeue)
End While
'Relay the message to all clients except the sender
For Each inf As ConnectionInfo In monitorInfo.Connections
If inf.Client.Connected Then
Dim msg As String = info.Client.Client.RemoteEndPoint.ToString & "|" & System.Text.Encoding.ASCII.GetString(messageBytes.ToArray)
If Not inf.Client.Client.RemoteEndPoint.ToString = msg.Split("|")(0) Then
inf.Client.Client.Send(messageBytes.ToArray)
End If
End If
Next
End If
Else
'Record clients no longer connected
lostConnections.Add(info)
End If
Next
'Clean-up any closed client connections
If lostConnections.Count > 0 Then
While lostConnections.Count > 0
monitorInfo.Connections.Remove(lostConnections(0))
lostConnections.RemoveAt(0)
End While
End If
'Throttle loop to avoid wasting CPU time
_ConnectionMontior.Wait(1)
Loop While Not monitorInfo.Cancel
'Close all connections before exiting monitor
For Each info As ConnectionInfo In monitorInfo.Connections
info.Client.Close()
Next
monitorInfo.Connections.Clear()
Catch ex As Exception
WriteToEventLog("DoMonitorConnections" & ex.Message)
End Try
End Sub
Client Code:
_ServerAddress = IPAddress.Parse(ServerIP)
_Connection = New ConnectionInfo(_ServerAddress, 1314, AddressOf InvokeAppendOutput)
_Connection.AwaitData()
ConnectionInfo Class:
Public Class ConnectionInfo
Private _AppendMethod As Action(Of String)
Public ReadOnly Property AppendMethod As Action(Of String)
Get
Return _AppendMethod
End Get
End Property
Private _Client As TcpClient
Public ReadOnly Property Client As TcpClient
Get
Return _Client
End Get
End Property
Private _Stream As NetworkStream
Public ReadOnly Property Stream As NetworkStream
Get
Return _Stream
End Get
End Property
Private _LastReadLength As Integer
Public ReadOnly Property LastReadLength As Integer
Get
Return _LastReadLength
End Get
End Property
Private _Buffer(255) As Byte
Public Sub New(address As IPAddress, port As Integer, append As Action(Of String))
_AppendMethod = append
_Client = New TcpClient
_Client.Connect(address, port)
_Stream = _Client.GetStream
End Sub
Public Sub AwaitData()
_Stream.BeginRead(_Buffer, 0, _Buffer.Length, AddressOf DoReadData, Me)
End Sub
Public Sub Close()
If _Client IsNot Nothing Then _Client.Close()
_Client = Nothing
_Stream = Nothing
End Sub
Private Const MESSAGE_DELIMITER As Char = ControlChars.Cr
Dim sBuilder As New System.Text.StringBuilder
Private Sub DoReadData(result As IAsyncResult)
Dim info As ConnectionInfo = CType(result.AsyncState, ConnectionInfo)
Try
If info._Stream IsNot Nothing AndAlso info._Stream.CanRead Then
info._LastReadLength = info._Stream.EndRead(result)
If info._LastReadLength > 0 Then
Dim message As String = System.Text.Encoding.UTF8.GetString(info._Buffer, 0, info._LastReadLength)
If (message.IndexOf(MESSAGE_DELIMITER) > -1) Then
Dim subMessages() As String = message.Split(MESSAGE_DELIMITER)
sBuilder.Append(subMessages(0))
If Not info._Client.Client.LocalEndPoint.ToString = sBuilder.ToString.Split("|")(0) Then
info._AppendMethod(sBuilder.ToString)
End If
sBuilder = New System.Text.StringBuilder
If subMessages.Length = 2 Then
sBuilder.Append(subMessages(1))
Else
For i As Integer = 1 To subMessages.GetUpperBound(0) - 1
'MessageBox.Show(subMessages(i))
info._AppendMethod(subMessages(i))
Next
sBuilder.Append(subMessages(subMessages.GetUpperBound(0)))
End If
Else
sBuilder.Append(message)
End If
End If
End If
info.AwaitData()
Catch ex As Exception
info._LastReadLength = -1
End Try
End Sub
End Class
TCP does not guarantee that a side not trying to send data can detect a loss of the connection. You should have taken this into account when you designed your application protocol.
What you are seeing is most commonly caused by NAT or stateful firewalls. As a practical matter, if you don't send data at least every ten minutes, you can expect at least some clients to get disconnected. Their NAT devices or stateful firewalls simply forget about the connection. Neither side notices until it tries to send data.
I would suggest creating some kind of dummy message that the server sends to all its clients every five minutes. Basically, this is just some small chunk of data that can be uniquely identified as serving only to keep the connection alive.
Each client responds to the dummy message by sending a dummy message back to the server. If a client doesn't receive a dummy message in ten minutes, it should consider the connection lost, close it, and try to connect again.
The mere act of trying to send the dummy message will cause the server to detect any lost connections, but you should probably also consider as dead any connection to a client that hasn't responded to a dummy message by the time you're ready to send the next one. The client will know a connection is lost when it doesn't receive the dummy message. The exchange of messages will keep the NAT/firewall entry alive.