VB.Net BeginGetResponse much faster than using Await GetResponseAsync? - vb.net

I am trying to start using tasks, but I wanted to compare the speed difference when using a standard HttpWebRequest.BeginGetResponse.
From what I have found, it is taking ~600ms to send and complete 100 requests to example.com using BeginGetResponse
However, using Await GetResponseAsync is taking 5x that. Around 3000ms. In production, that really matters alot to me when scaled up. Am I doing something wrong, or is Await GetResponseAsync inherently slower than BeginGetResponse?
Imports System.Net
Public Class Form1
Private sw As New Stopwatch
Private respCounter As Integer
Private iterations As Integer = 100
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
sw.Start()
For i = 1 To iterations
Dim req As HttpWebRequest = HttpWebRequest.Create("http://example.com")
Dim state As New RequestState
state.req = req
req.BeginGetResponse(AddressOf respCallback, state)
Next
End Sub
Private Sub respCallback(ar As IAsyncResult)
Dim state As RequestState = ar.AsyncState
state.resp = state.req.EndGetResponse(ar)
state.respStream = state.resp.GetResponseStream
state.respStream.BeginRead(state.buffer, 0, 1024, AddressOf readCallback, state)
End Sub
Private Sub readCallback(ar As IAsyncResult)
Dim state As RequestState = ar.AsyncState
Dim read As Integer = state.respStream.EndRead(ar)
If read > 0 Then
state.respBody += System.Text.ASCIIEncoding.ASCII.GetString(state.buffer, 0, read)
state.respStream.BeginRead(state.buffer, 0, 1024, AddressOf readCallback, state)
Else
state.Dispose()
respCounter += 1
If respCounter = iterations Then
respCounter = 0
sw.Stop()
Debug.WriteLine(sw.ElapsedMilliseconds)
sw.Reset()
End If
End If
End Sub
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
sw.Start()
For i = 1 To iterations
Dim req As HttpWebRequest = HttpWebRequest.Create("http://example.com")
Using resp As WebResponse = Await req.GetResponseAsync
Using sr As New IO.StreamReader(resp.GetResponseStream)
Dim respBody As String = Await sr.ReadToEndAsync
End Using
End Using
respCounter += 1
If respCounter = iterations Then
respCounter = 0
sw.Stop()
Debug.WriteLine(sw.ElapsedMilliseconds)
sw.Reset()
End If
Next
MsgBox("Execution!")
End Sub
End Class
Public Class RequestState
Implements IDisposable
Public req As HttpWebRequest
Public resp As HttpWebResponse
Public respStream As IO.Stream
Public buffer(1024) As Byte
Public respBody As String
#Region "IDisposable Support"
Private disposedValue As Boolean ' To detect redundant calls
' IDisposable
Protected Overridable Sub Dispose(disposing As Boolean)
If Not disposedValue Then
If disposing Then
' TODO: dispose managed state (managed objects).
respStream.Close()
respStream.Dispose()
resp.Close()
End If
' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
' TODO: set large fields to null.
End If
disposedValue = True
End Sub
' TODO: override Finalize() only if Dispose(disposing As Boolean) above has code to free unmanaged resources.
'Protected Overrides Sub Finalize()
' ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
' Dispose(False)
' MyBase.Finalize()
'End Sub
' This code added by Visual Basic to correctly implement the disposable pattern.
Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
Dispose(True)
' TODO: uncomment the following line if Finalize() is overridden above.
' GC.SuppressFinalize(Me)
End Sub
#End Region
End Class

is Await GetResponseAsync inherently slower than BeginGetResponse?
It's difficult to address your specific performance concern without a good Minimal, Complete, and Verifiable code example. That said…
It seems to me that you're comparing apples and oranges here. First and foremost, there's a major difference in the implementations. In your BeginGetResponse() version, you initiate all of the requests concurrently, so assuming the web server will tolerate it, they complete in parallel. In your GetResponseAsync() version, you only initiate a new request after the previous one completes.
This serialization will necessarily slow everything down.
Beyond that, the BeginGetResponse() version performs all of its work in the IOCP thread pool, while the GetResponseAsync() version uses the single UI thread to handle completion of I/O events. The UI thread is a bottleneck, both because it can only do one thing at a time, and because you have to wait for it to be available from performing other tasks before it can move on to dealing with the I/O completions (a variation on the "can only do one thing at a time" issue).
In addition to that, you also have to deal with the latency involved in the message loop that dequeues the asynchronous completions for execution in the UI thread.
It wouldn't surprise me at all to find that the GetResponseAsync() approach is slower, when used in the way you're using it.
If you want better performance from it, you should probably use ConfigureAwait(false) in your async calls. Of course, this assumes you can otherwise minimize the interaction with the UI thread (e.g. the processing of the results does not actually need direct posting back to the UI thread). But doing so will tell the framework to not bother marshaling completions back to the UI thread. At least in the code you posted, this would be safe, as you don't actually interact with UI objects in the async event handler method.
All that said, when I changed your code so that it would run the GetResponseAsync() version concurrently, I found that at least with the web server I tested with, it worked just as fast as the BeginGetResponse() version. It was able to complete 100 iterations in just over 10 seconds in both cases.
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
sw.Start()
Dim tasks As List(Of Task(Of String)) = New List(Of Task(Of String))
For i = 1 To iterations
Dim req As HttpWebRequest = HttpWebRequest.Create("http://example.com/")
tasks.Add(ReadResponse(req))
Next
Await Task.WhenAll(tasks)
sw.Stop()
Debug.WriteLine(sw.ElapsedMilliseconds)
sw.Reset()
MsgBox("Execution!")
End Sub
Private Async Function ReadResponse(req As HttpWebRequest) As Task(Of String)
Using resp As WebResponse = Await req.GetResponseAsync
Using sr As New IO.StreamReader(resp.GetResponseStream)
Dim respBody As String = Await sr.ReadToEndAsync
Return respBody
End Using
End Using
End Function
It's possible with a faster web server, you might start to run into the UI-thread-as-a-bottleneck issue, but I would say the primary difference is likely just that the two implementations really aren't even logically the same.

Related

How to speed up Binance coin price parsing?

I'm trying to create a bot that place multiples orders on Binance and I need a fast coin price parsing.
I'm parsing the code using this code
Dim price as decimal
Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Timer1.Stop()
Dim downloadTasks As New List(Of Task(Of String))
Dim dogebusd = wc1.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/price?symbol=DOGEBUSD")
downloadTasks.Add(dogebusd)
Await Task.WhenAll(downloadTasks)
Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(dogebusd.Result)
Dim PREZZO As Decimal = CDec(Val((d("price")).ToString))
price = CDec((PREZZO))
Timer1.Start()
End Sub
but when the price is dumping or pumping really fast, even a 10 ms or 100ms timer tick is not that efficient.
I wonder if this is the fastest way possible or if I can improve the code.
Thanks
following Jimi suggestions:
Dim price As Decimal
Private ReadOnly _timer As New PeriodicTimer(TimeSpan.FromSeconds(1))
Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles _timer.Tick
timer1.stop
Dim dogebusdTask = HttpClient.GetStringAsync("https://api.binance.com/api/v1/ticker/price?symbol=DOGEBUSD")
Await dogebusdTask
Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(dogebusdTask.Result)
If Decimal.TryParse(d("price"), NumberStyles.Number, CultureInfo.InvariantCulture, price) Then
_timer.Start()
End If
End Sub
An example, using the .NET 6+ awaitable PeriodicTimer and System.Text.Json to parse the resulting stream returned by HttpClient's GetStreamAsync()
The PeriodicTimer can be quite efficient in contexts like this, since it tries to keep up with the interval you have provided. Since the internal procedure takes time to execute - the HttpClient.GetStreamAsync() takes an undetermined amount of time to execute and return a result - the Timer keeps track of the actual elapsed time and tries to keep ticking steadily (unless the procedure takes more than the specified Interval, then you skip a beat, of course)
Note: as usual, when dealing with Tasks that accept a CancellationToken, you should run this code without a Debugger (CTRL+F5) or configure the Debugger to not halt on TaskCanceledException or OperationCanceledException, otherwise it handles the (ignorable) exception before you do
Imports System.Globalization
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading
Private Shared ReadOnly m_Client As New HttpClient()
Private timerCts As CancellationTokenSource = Nothing
Private m_PeriodicTimer As PeriodicTimer = Nothing
Private m_PeriodicTimerInterval As Integer = 500
Private currentPrice As Decimal = 0.0D
Private Async Function StartPriceLookupTimer(token As CancellationToken, timerInterval As Integer) As Task
If token.IsCancellationRequested Then Return
Dim lookupAddress = "https://api.binance.com/api/v1/ticker/price?symbol=DOGEBUSD"
m_PeriodicTimer = New PeriodicTimer(TimeSpan.FromMilliseconds(timerInterval))
Try
While Await m_PeriodicTimer.WaitForNextTickAsync(token)
Dim jsonStream = Await m_Client.GetStreamAsync(lookupAddress, token)
Try
Dim prop = (Await JsonDocument.ParseAsync(jsonStream, Nothing, token)).RootElement.GetProperty("price")
Dim strPrice = prop.GetString()
If Decimal.TryParse(strPrice, CultureInfo.InvariantCulture, currentPrice) Then
' Do whatever you need to do with the parsed value
' E.g., assign it to a TextBox for presentation,
' using the current Culture format, since strPrice contains the original format
[Some TextBox].Text = currentPrice.ToString() ' <= UI Thread here
End If
Catch knfex As KeyNotFoundException
Debug.WriteLine("The JSON property was not found")
Catch tcex As TaskCanceledException
Debug.WriteLine("The lookup procedure was canceled")
End Try
End While
Catch tcex As TaskCanceledException
Debug.WriteLine("The lookup procedure was canceled")
End Try
End Function
Private Sub StopPriceLookupTimer()
timerCts?.Cancel()
m_PeriodicTimer?.Dispose()
timerCts?.Dispose()
timerCts = Nothing
End Sub
How to start this lookup procedure, calling the StartPriceLookupTimer() method?
You could use a Button, making its Click handler async:
Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
If timerCts Is Nothing Then
timerCts = New CancellationTokenSource()
Await StartPriceLookupTimer(timerCts.Token, m_PeriodicTimerInterval)
Else
Debug.WriteLine("Lookup Timer already started")
End If
End Sub
Or make the Form's Shown Handler / OnShown() method override async and start the procedure when the Form is first presented:
Protected Overrides Async Sub OnShown(e As EventArgs)
MyBase.OnShown(e)
timerCts = New CancellationTokenSource()
Await StartPriceLookupTimer(timerCts.Token, m_PeriodicTimerInterval)
End Sub
To stop the lookup procedure, call the StopPriceLookupTimer() method when needed, e.g., using another Button or in the Form.Closed handler / OnFormClosed() override

HttpClient GetAsync working in a VB.Net Console application but not in Windows Forms

Works as expected in the console application (I converted it from a C# YouTube tutorial for reasons I won't bore you with), but hangs with no exception thrown in the desktop app when calling GetAsync.
`Imports System
Imports System.Net.Http
Module Moduke1
Sub Main()
Dim strContent As Task(Of String) = GetRequest("http://www.google.com.pk")
Console.WriteLine(strContent.Result)
Console.ReadKey()
End Sub
Async Function GetRequest(url As String) As Task(Of String)
Using client As New HttpClient()
Using response As HttpResponseMessage = Await client.GetAsync(url)
Using content As HttpContent = response.Content
Dim myContent As String = Await content.ReadAsStringAsync()
Return myContent
End Using
End Using
End Using
End Function
End Module`
That works, but the following does not. Probably a rookie error, although I'm not really a rookie - never used System.Net.Http until now and I've been all round the houses with this one.
The following hangs at the call to GetAsync...
`Imports System
Imports System.Net.Http
Public Class HTTP_Test_One
Public Sub HTTP_Test_One_Load(sender As Object, e As EventArgs) Handles MyBase.Load
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim strContent As Task(Of String) = GetRequest("http://www.google.com.pk")
txtResults.Text = strContent.Result
End Sub
Async Function GetRequest(url As String) As Task(Of String)
Using client As New HttpClient()
Using response As HttpResponseMessage = Await client.GetAsync(url)
Using content As HttpContent = response.Content
Dim myContent As String = Await content.ReadAsStringAsync()
Return myContent
End Using
End Using
End Using
End Function
End Class`
I'm not 100% on this, and suspect I may get corrected by people more in the know than I, maybe even the why. Looks to me that awaiting the Result of your task is jamming up, why not in the Console app, my guess is because it doesn't have the overhead of UI thread, or being triggered by an event. Just by reworking your button event handler, I at least get the desired response.
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim strContent As String = Await GetRequest("http://www.google.com.pk")
txtResults.Text = strContent
End Sub
Note I've changed the Event to Async, which means I can just await the response from GetRequest() rather than looking at the task result
Blocking on async code in the UI thread is likely to lead to a deadlock. This has been discussed in a number of async-related resources; my go-to is the series of posts by Stephen Cleary, and he discusses this specific issue in https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html with some additional detail on why blocking can lead to a deadlock.
The right way to do this is to make your event handler Async and then Await the result within it, i.e.
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim content = Await GetRequest("http://www.google.com.pk")
txtResults.Text = content
End Sub
Note that Async Sub is generally recommended against due to issues with processing errors, but event handlers are the exception---Async Sub is designed specifically for UI event handlers.

How to let the code run smoothly using timers and different threads

I'm trying to prevent the GUI from freezing, because of a low timer interval and too much to process in the Timer.Tick event handler.
I've been googling a while and I understood that I cannot update UI from any other thread other than the UI thread.
So, what about if you are using lots of controls under Timer1.Tick?
How can I update a Label when the data is downloaded with WebClient with a timer, you don't want to lower the interval too much and keep the UI responsive at the same time?
I receiver Cross Thread violation exceptions when I access UI elements, a ListBox1 and a RichTextBox.
What is the correct way to update the UI with a timer and/or a Thread without causing cross threat exceptions?
You have different ways to update UI elements from a Thread other than the UI Thread.
You can use the InvokeRequired/Invoke() pattern (meh), call the asynchronous BeginInvoke() method, Post() to the SynchronizationContext, maybe mixed with an AsyncOperation + AsyncOperationManager (solid BackGroundWorker style), use an async callback etc.
There's also the Progress<T> class and its IProgress<T> interface.
This class provides a quite simplified way to capture the SynchronizationContext where the class object is created and Post() back to the captured execution context.
The Progress<T> delegate created in the UI Thread is called in that context. We just need to pass the Progress<T> delegate and handle the notifications we receive.
You're downloading and handling a string, so your Progress<T> object will be a Progress(Of String): so, it will return a string to you.
The Timer is replaced by a Task that executes your code and also delays its actions by a Interval that you can specify, as with a Timer, here using Task.Delay([Interval]) between each action. There's a StopWatch that measures the time a download actually takes and adjusts the Delay based on the Interval specified (it's not a precision thing, anyway).
▶ In the sample code, the download Task can be started and stopped using the StartDownload() and StopDownload() methods of a helper class.
The StopDownload() method is awaitable, it executes the cancellation of the current tasks and disposes of the disposable objects used.
▶ I've replaced WebClient with HttpClient, it's still quite simple to use, it provides async methods that support a CancellationToken (though a download in progress requires some time to cancel, but it's handled here).
▶ A Button click initializes and starts the timed downloads and another one stops it (but you can call the StopDownload() method when the Form closes, or, well, whenever you need to).
▶ The Progress<T> delegate is just a Lambda here: there's not much to do, just fill a ListBox and scroll a RichTextBox.
You can initialize the helper class object (it's named MyDownloader: of course you will pick another name, this one is ridiculous) and call its StartDownload() method, passing the Progress<T> object, the Uri and the Interval between each download.
Private downloader As MyDownloader = Nothing
Private Sub btnStartDownload_Click(sender As Object, e As EventArgs) Handles btnStartDownload.Click
Dim progress = New Progress(Of String)(
Sub(data)
' We're on the UI Thread here
ListBox1.Items.Clear()
ListBox1.Items.AddRange(Split(data, vbLf))
RichTextBox1.SelectionStart = RichTextBox1.TextLength
End Sub)
Dim url As Uri = New Uri("https://SomeAddress.com")
downloader = New MyDownloader()
' Download from url every 1 second and report back to the progress delegate
downloader.StartDownload(progress, url, 1)
Private Async Sub btnStopDownload_Click(sender As Object, e As EventArgs) Handles btnStopDownload.Click
Await downloader.StopDownload()
End Sub
The helper class:
Imports System.Diagnostics
Imports System.Net
Imports System.Net.Http
Imports System.Text.RegularExpressions
Public Class MyDownloader
Implements IDisposable
Private Shared client As New HttpClient()
Private cts As CancellationTokenSource = Nothing
Private interval As Integer = 0
Private disposed As Boolean
Public Sub StartDownload(progress As IProgress(Of String), url As Uri, intervalSeconds As Integer)
interval = intervalSeconds * 1000
Task.Run(Function() DownloadAsync(progress, url, cts.Token))
End Sub
Private Async Function DownloadAsync(progress As IProgress(Of String), url As Uri, token As CancellationToken) As Task
token.ThrowIfCancellationRequested()
Dim responseData As String = String.Empty
Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
Dim downloadTimeWatch As Stopwatch = New Stopwatch()
downloadTimeWatch.Start()
Do
Try
Using response = Await client.GetAsync(url, HttpCompletionOption.ResponseContentRead, token)
responseData = Await response.Content.ReadAsStringAsync()
responseData = WebUtility.HtmlDecode(Regex.Replace(responseData, pattern, ""))
End Using
progress.Report(responseData)
Dim delay = interval - CInt(downloadTimeWatch.ElapsedMilliseconds)
Await Task.Delay(If(delay <= 0, 10, delay), token)
downloadTimeWatch.Restart()
Catch tcEx As TaskCanceledException
' Don't care - catch a cancellation request
Debug.Print(tcEx.Message)
Catch wEx As WebException
' Internet connection failed? Internal server error? See what to do
Debug.Print(wEx.Message)
End Try
Loop
End Function
Public Async Function StopDownload() As Task
Try
cts.Cancel()
client?.CancelPendingRequests()
Await Task.Delay(interval)
Finally
client?.Dispose()
cts?.Dispose()
End Try
End Function
Protected Overridable Sub Dispose(disposing As Boolean)
If Not disposed AndAlso disposing Then
client?.Dispose()
client = Nothing
End If
disposed = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
End Sub
End Class
Your listbox and richtextbox accesses must run on the UI thread. The easiest way to do it is like this.
Me.Invoke(Sub()
ListBox1.Items.Clear()
ListBox1.Items.AddRange(Split(clientdecode, vbLf))
RichTextBox1.SelectionStart() = RichTextBox1.TextLength
RichTextBox1.ScrollToCaret()
End Sub)

Using ThreadPool.QueueUserWorkItem for Millions of threads

I have the need to process millions of files. Currently, I use a custom thread manager to do the work by using a DataGridView to keep track of the threads and a timer to check if more threads can start; kind of like (sudo):
Private Sub ThreadManager()
If AVailableThreads > 0 then
Dim t as Threading.Thread = New Thread(AddressOf MyThread)
t.Start()
AvailableThreads = AvailableThreads - 1
ThreadManager()
End If
End Sub
This has many drawbacks, the main ones being that the CPU and memory usage is high as each of the above threads process a full directory instead of each file independently.
So I have rewritten the process. Now I have a class that will perform the process at the file level and report back to the main thread the results; like so:
Imports System.IO
Public Class ImportFile
Public Class ImportFile_state
Public ID as Long = Nothing
Public FilePath as String = Nothing
Public Result as Boolean = False
End Class
Public Event ReportState(ByVal state as ImportFile_state)
Dim _state as ImportFile_state = New ImportFile_State
Public Sub New(ByVal ID as Long, ByVal FilePath as String)
MyBase.New()
_state.ID = ID
_state.FilePath = FilePath
End Sub
Public Sub GetInfo()
'Do the work here, but just return the result for this demonstration
Try
_state.Result = True
Catch ex As Exception
_state.Result = False
Finally
RaiseEvent ReportState(_state)
End Try
End Sub
End Class
The above class works like a charm and is very fast, uses almost no memory and next to nothing of the CPU. Albeit that I have only been able to test this with a few hundred threads using the Threading.Thread process.
Now I would like to use the ThreadPool.QueueUserWorkItem to execute the above class for each file allowing the system to control the number of threads to have running at any given time. However, I know I cannot just dump several million threads into the ThreadPool without locking up my server. I have done a lot of research on this and I have only been able to find examples/discussions on using the ThreadPool.QueueUserWorkItem for a few threads. What I need is to fire off millions of these threads.
So, I have two questions: 1) Should I even be trying to use the ThreadPool.QueueUserWorkItem to run this many threads, and 2) is the code below sufficient to perform this process without locking up my server?
Here is my code so far:
For Each subdir As String In Directory.GetDirectories(DirPath)
For Each fl In Directory.GetFiles(subdir)
'MsgBox(fl)
Dim f As ImportFile = New ImportFile(0, fl)
AddHandler f.ReportState, AddressOf GetResult
ThreadPool.QueueUserWorkItem(New Threading.WaitCallback(AddressOf f.GetInfo))
ThreadPool.GetAvailableThreads(worker, io)
Do While (worker) <= 0
Thread.Sleep(5000)
ThreadPool.GetAvailableThreads(worker, io)
Loop
Next
Next

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!