Reputation: 1595
I have a console application which uses multiple threads to work trough a list of items. All these items need to access an API and I don't want to make more then 1 request a second to that API.
So I have a class which does the API request
Public Class apiRequest
Property lastRequest as date = now()
function doRequest()
'check if we can make a request
if Now.Substract(lastRequest).Seconds > 1 then
lastRequest = now()
'Do request [...]
end if
end function
End Class
And I have the main program
property apiRequester as new apiRequest
sub main
'start multiple threads (addressOf threadFunction)[...]
end sub
sub threadFunction()
dim data as string = apirequester.doRequest() 'Call the api request
end sub
But what happens if two threads do a request exactly at the same time, they both come past de seconds check because they execute both this line first
if Now.Substract(lastRequest).Seconds > 1 then
before any of them get to this line
lastRequest = now()
How can I make the apiRequest function so there is only 1 request a second?
Solution
I found the following solution using SyncLock
.
Private locker As New Object
Private lastRequest as date = now()
function doRequest()
SyncLock locker
if Now.Substract(lastRequest).Seconds > 1 then
lastRequest = now()
end if
End Synclock
end function
This works almost the same as the Monitor.Enter
, Monitor.Exit
answer from dbasnett except that lock and SyncLock wrap the Exit method in a try…finally block (Try…Finally in Visual Basic) to ensure that the monitor is released (as Alex B pointed out in the comments)
Upvotes: 0
Views: 1414
Reputation: 25013
You could add items to a ConcurrentQueue and de-queue them on a timer tick. That way you can create a request at any time but they will be submitted at the rate you set and there is no locking of anything required.
For example,
Option Infer On
Imports System.Collections.Concurrent
Imports System.Timers
Module Module1
Class ApiRequest
Property SerialNo As Integer
End Class
Dim q As ConcurrentQueue(Of ApiRequest)
Dim tim As Timer
Sub Dequeue(sender As Object, e As ElapsedEventArgs)
Dim ar As ApiRequest = Nothing
If q.TryDequeue(ar) Then
Console.ForegroundColor = ConsoleColor.Green
Console.WriteLine($"Processed: {ar.SerialNo}")
End If
End Sub
Sub Init()
q = New ConcurrentQueue(Of ApiRequest)
tim = New Timer With {.AutoReset = True, .Interval = 1000}
AddHandler tim.Elapsed, AddressOf Dequeue
tim.Start()
End Sub
Sub Main()
Init()
For i = 1 To 20
Dim ar = New ApiRequest With {.SerialNo = i}
q.Enqueue(ar)
Console.ForegroundColor = ConsoleColor.Red
Console.WriteLine($"Created: {ar.SerialNo}")
Threading.Thread.Sleep(200)
Next
Console.ForegroundColor = ConsoleColor.Yellow
Console.WriteLine("<press any key to exit>")
' wait for user
Console.Read()
' clean up
tim.Stop()
RemoveHandler tim.Elapsed, AddressOf Dequeue
tim.Dispose()
End Sub
End Module
Sample output which shows items being processed even as more items are beining enqueued:
Created: 1
Created: 2
Created: 3
Created: 4
Created: 5
Created: 6
Processed: 1
Created: 7
Created: 8
Created: 9
Created: 10
Processed: 2
Created: 11
Created: 12
Created: 13
Created: 14
Created: 15
Processed: 3
Created: 16
Created: 17
Created: 18
Created: 19
Created: 20
Processed: 4
<press any key to exit>
Processed: 5
Processed: 6
Processed: 7
Processed: 8
Processed: 9
Processed: 10
Processed: 11
Processed: 12
Processed: 13
Processed: 14
Processed: 15
Processed: 16
Processed: 17
Processed: 18
Processed: 19
Processed: 20
Sometimes the colours of the text (not illustratable here) are wrong because the thread that sets the console colour is interrupted before the text is written. Writing the text to the console would also be a good candidate for a concurrentqueue, but it would hinder the clarity this example code.
Upvotes: 0
Reputation: 11773
This will prohibit more than one request from happening per second. It assumes that this
dim data as string = apirequester.doRequest() 'Call the api request
wants to wait for other requests. Your class modified.
Public Class apiRequest
Private APILock As New Object
Private stpw As Stopwatch = Stopwatch.StartNew
Private Shared ReadOnly ReqEvery As New TimeSpan(0, 0, 1)
Function doRequest() As Boolean
'check if we can make a request
Threading.Monitor.Enter(Me.APILock) 'only one request past this point at a time
Do While Me.stpw.Elapsed < ReqEvery 'loop until one second has passed
Threading.Thread.Sleep(10)
Loop
'Do request [...]
Me.stpw.Restart() 'restart the clock
Threading.Monitor.Exit(Me.APILock) 'allow other requests
End Function
End Class
Upvotes: 1