Introduction
The AsyncVar
class is a simple class that asynchronously executes a worker function and stores the return value. If the worker function finishes before the value is needed, the value is held in the AsyncVar
instance. If the value is requested before the worker function finishes, the main thread will join the worker function's thread, blocking until the value is available.
Background
Developers are always looking for ways to make their applications a little faster. Trimming a second or two off a long operation is always a good thing, even if it takes an hour or two of programming and debugging to make it work. I was recently working on a project that required four long-running data operations to occur; each of these operations returned a value, and I needed all four values to continue processing.
At first, I set up each of the four operations to run serially. The program took slightly under a minute to perform all four operations. I, being a programmer, didn't find this acceptable, and decided I needed to look into threading these operations to make my program go much faster. After putting around with things like the ThreadPool
and the BackgroundWorker
component, I decided I wanted an easier way to perform background work that returned a value necessary for further processing. Hence, the AsyncVar
was born.
Building the AsyncVar class
To start building the AsyncVar
class, we need to first create the class. Looking at the problem requirements, we can see that the AsyncVar
class could potentially be needed for any sort of data type; as such, we need to create it using Generics.
C#:
namespace AsyncVarArticle
{
using System;
using System.Threading;
public class AsyncVar<TResult>
{
Visual Basic:
Imports System
Imports System.Threading
Namespace AsyncVarArticle
Public Class AsyncVar(Of TResult)
The TResult
generic parameter will determine the return type of the function used to provide the value, as well as the data type of the value to store in the AsyncVar
.
Fields
C#:
private Func<TResult> workerFunction;
private Thread workerThread;
private TResult value;
private Exception error;
Visual Basic:
Private _workerFunction As Func(Of TResult)
Private _workerThread As Thread
Private _value As TResult
Private _error As Exception
The first field is a delegate reference; this delegate will point to the worker function that is run asynchronously and provides us with the value.
The next field is a Thread
object. We'll use this object to run the asynchronous operation, as well as use its properties to determine what state the thread is in (so we can see if the thread needs to be joined when the value is requested.
The value
field, of the generic type TResult
, will contain the return value of the worker function, once it's complete.
The error
field will contain any unhandled exception that occurs within the worker function.
Constructor and destructor
C#:
public AsyncVar(Func<TResult> worker)
{
this.workerFunction = worker;
this.workerThread = new Thread(this.ThreadStart);
this.workerThread.Start();
}
~AsyncVar()
{
if (this.workerThread.IsAlive)
{
this.workerThread.Abort();
}
}
Visual Basic:
Public Sub New(ByVal worker As Func(Of TResult))
Me._workerFunction = worker
Me._workerThread = New Thread(AddressOf Me.ThreadStart)
Me._workerThread.Start()
End Sub
Protected Overrides Sub Finalize()
If Me._workerThread.IsAlive Then
Me._workerThread.Abort()
End If
End Sub
The constructor stores the passed worker function, and instantiates and starts the worker thread. The worker thread executes the class' ThreadStart
function, which is the next member we'll add to the class. The destructor simply kills the thread if it's still running. Typically, the thread shouldn't still be running when the AsyncVar
instance is destructed, but it's possible. If the instance goes out of scope or is garbage collected, there's no reason for the thread to continue.
ThreadStart
The next member we need to add to the class is a ThreadStart
method. The Thread
member will be initialized using this method; it will call the worker function and handle any errors that occur.
C#:
private void ThreadStart()
{
TResult returnValue = default(TResult);
try
{
returnValue = this.workerFunction();
}
catch (Exception ex)
{
this.error = ex;
}
if (this.error == null)
{
this.value = returnValue;
}
}
Visual Basic:
Private Sub ThreadStart()
Dim returnValue As TResult
Try
returnValue = Me._workerFunction()
Catch ex As Exception
Me._error = ex
End Try
If Me._error Is Nothing Then
Me._value = returnValue
End If
End Sub
The ThreadStart
method is simple; within a try
-catch
block, it executes the worker function. If an exception is caught, it's assigned to the error
field. Otherwise, the return value of the worker function is assigned to the value
field.
Property Accessors
The last two members of the AsyncVar
class are the property accessors for the value
and error
fields.
C#:
public TResult Value
{
get
{
if (this.workerThread.IsAlive)
{
this.workerThread.Join();
}
if (this.error != null)
{
throw new InvalidOperationException("Thread encountered " +
"an exception during execution.", this.error);
}
else
{
return this.value;
}
}
}
Visual Basic:
Public ReadOnly Property Value() As TResult
Get
If Me._workerThread.IsAlive Then
Me._workerThread.Join()
End If
If Me._error IsNot Nothing Then
Throw New InvalidOperationException("Thread encountered " & _
"an exception during execution.", Me._error)
Else
Return Me._value
End If
End Get
End Property
The accessor first checks to see if the thread is still executing; if it is, the current thread simply blocks until the worker thread is finished. Once the worker thread is done (or if it has already finished before the accessor was called), it checks to see if an exception was caught inside the worker thread. If there was, an InvalidOperationException
is thrown (similar to the BackgroundWorker
class) if someone tries to access the value. If there was no error, the value is simply returned.
C#:
public Exception Error
{
get
{
if (this.workerThread.IsAlive)
{
this.workerThread.Join();
}
return this.error;
}
}
}
}
Visual Basic:
Public ReadOnly Property [Error]() As Exception
Get
If Me._workerThread.IsAlive Then
Me._workerThread.Join()
End If
Return Me._error
End Get
End Property
End Class
End Namespace
The Error
accessor does essentially the same thing; if the thread is still running, it waits for it to finish. Then, if an error was encountered, it returns the exception.
The complete class
Here's the full listing of the class, including the XML documents. You can simply copy and paste this into a code file (and change the namespace, if necessary) and start using the class without any further work.
C#:
namespace AsyncVarArticle
{
using System;
using System.Threading;
public class AsyncVar<TResult>
{
private Func<TResult> workerFunction;
private Thread workerThread;
private TResult value;
private Exception error;
public AsyncVar(Func<TResult> worker)
{
this.workerFunction = worker;
this.workerThread = new Thread(this.ThreadStart);
this.workerThread.Start();
}
~AsyncVar()
{
if (this.workerThread.IsAlive)
{
this.workerThread.Abort();
}
}
private void ThreadStart()
{
TResult returnValue = default(TResult);
try
{
returnValue = this.workerFunction();
}
catch (Exception ex)
{
this.error = ex;
}
if (this.error == null)
{
this.value = returnValue;
}
}
public TResult Value
{
get
{
if (this.workerThread.IsAlive)
{
this.workerThread.Join();
}
if (this.error != null)
{
throw new InvalidOperationException("Thread encountered " +
"an exception during execution.", this.error);
}
else
{
return this.value;
}
}
}
public Exception Error
{
get
{
if (this.workerThread.IsAlive)
{
this.workerThread.Join();
}
return this.error;
}
}
}
}
Visual Basic:
Imports System
Imports System.Threading
Namespace AsyncVarArticle
Public Class AsyncVar(Of TResult)
Private _workerFunction As Func(Of TResult)
Private _workerThread As Thread
Private _value As TResult
Private _error As Exception
Public Sub New(ByVal worker As Func(Of TResult))
Me._workerFunction = worker
Me._workerThread = New Thread(AddressOf Me.ThreadStart)
Me._workerThread.Start()
End Sub
Protected Overrides Sub Finalize()
If Me._workerThread.IsAlive Then
Me._workerThread.Abort()
End If
End Sub
Private Sub ThreadStart()
Dim returnValue As TResult
Try
returnValue = Me._workerFunction()
Catch ex As Exception
Me._error = ex
End Try
If Me._error Is Nothing Then
Me._value = returnValue
End If
End Sub
Public ReadOnly Property Value() As TResult
Get
If Me._workerThread.IsAlive Then
Me._workerThread.Join()
End If
If Me._error IsNot Nothing Then
Throw New InvalidOperationException("Thread encountered" & _
" an exception during execution.", Me._error)
Else
Return Me._value
End If
End Get
End Property
Public ReadOnly Property [Error]() As Exception
Get
If Me._workerThread.IsAlive Then
Me._workerThread.Join()
End If
Return Me._error
End Get
End Property
End Class
End Namespace
Using the AsyncVar class
The AsyncVar
class is designed to be very easy to use. To run an operation asynchronously, simply construct a new instance of AsyncVar
with a worker function as the only constructor parameter. As soon as you construct the AsyncVar
, the operation will begin on its own thread.
C#:
private int SomeOperation()
{
System.Threading.Thread.Sleep(1000);
return 1;
}
private int SomeOtherOperation()
{
System.Threading.Thread.Sleep(3000);
return 3;
}
public void DoLotsOfWork()
{
AsyncVar<int> asyncVar1 = new AsyncVar<int>(SomeOperation);
AsyncVar<int> asyncVar2 = new AsyncVar<int>(delegate
{
System.Threading.Thread.Sleep(2000);
return 2;
});
AsyncVar<int> asyncVar3 = new AsyncVar<int>(() => SomeOtherOperation());
Console.WriteLine("Return values: {0}, {1}, {2}",
asyncVar1.Value, asyncVar2.Value, asyncVar3.Value);
}
Visual Basic:
Private Function SomeOperation() As Integer
System.Threading.Thread.Sleep(1000)
Return 1
End Function
Private Function SomeOtherOperation() As Integer
System.Threading.Thread.Sleep(3000)
Return 3
End Function
Public Sub DoLotsOfWork()
Dim asyncVar1 As New AsyncVar(Of Integer)(AddressOf SomeOperation)
Dim asyncVar3 As New AsyncVar(Of Integer)(Function() SomeOtherOperation())
Console.WriteLine("Return values: {0}, {1}, {2}", _
asyncVar1.Value, asyncVar2.Value, asyncVar3.Value)
End Sub
If you were to run this code using regular synchronous calls, it would take six seconds to run (the first function taking 1000ms, the second taking 2000ms, and the third 3000ms), but using the AsyncVar
s, it will only take three seconds to run (the length of the longest operation.) Now, Thread.Sleep
, of course, doesn't take CPU time or resources, so your own asynchronous operations may run slightly slower than if they were run synchronously, as they're sharing time and resources with the other threads.
Also, remember that each AsyncVar
instance has its own thread; use AsyncVar
s with discretion; having a few thousand of them running simultaneously probably isn't the greatest idea.