It all began with a post I read in the Silverlight.Net forums. A user, new to Silverlight, was frustrated with the asynchronous programming model required by Silverlight and wondered how to make all of the calls synchronous. I admit I heavily resisted the notion because I think the asynchronous programming model is one that developers should learn and be comfortable with. Forcing an asynchronous call to “look synchronous” seems to involve a lot of framework overhead just to keep the developer from having to learn something new, and would limit their ability to take advantage of the functionality available.
Then I watched Rob Eisenberg’s Build your own MVVM Framework presentation and learned what he was doing with a concept known as coroutines. That was the “ah-hah” moment: some beginning programmers truly are looking for a crutch and should take some time to embrace the asynchronous model and learn it. Other developers have embraced the model but are faced with an interesting challenge: how to keep code maintainable and readable when it is littered with asynchronous calls?
Download the source for this post Note: this requires Wintellect’s PowerThreading library. Download that here, then follow the instructions on the download link to add the reference.
Consider for a moment a typical workflow in a service-driven Silverlight application. The application must fetch information from a service, then perhaps take that data and push it to another service, then take even more data and bring it back. You might be Simplifying Asynchronous Calls using Action, but the code can start to look a little interesting.
You really end up with two solutions. One is to wire in the calls with methods, like this (consider a scenario where we get a number from a service, and the previous call result feeds into the next call):
private int n1, n2; private void FirstCall() { Service.GetRandomNumber(10, SecondCall); } private void SecondCall(int number) { n1 = number; Service.GetRandomNumber(n1, ThirdCall); } private void ThirdCall(int number) { n2 = number; // etc }
Or you can go lambda expression/delegate crazy like this:
private void FetchNumbers() { int n1, n2; Service.GetRandomNumber(10, result => { n1 = result; Service.GetRandomNumber(n1, secondResult => { n2 = secondResult; }); }); }
That may be somewhat manageable, but what if your workflow chains, say, a dozen calls together?
You don’t want your asynchronous calls to be synchronous. You just want to make them sequential.
Does that point make sense? If and when you become comfortable with asynchronous programming, you understand the issues with blocking threads and waiting for calls and everything else. That’s not the issue. You just want clean code that does the job. Something like this:
private void FetchNumbers() { int n1 = Service.GetRandomNumber(10); int n2 = Service.GetRandomNumber(n1); }
Let that run. We understand it’s asynchronous and don’t want to block everything else, but please, please, let’s just do it sequentially and make it easy to read, mmm kay?
This is where the power of coroutines comes in.
C# does not have direct support for coroutines.
Let’s make that clear up front. We have ways to build frameworks that make them happen, but it’s not an “out-of-the-box” implementation. But let’s start with the basics: what is a coroutine, anyway?
When I learned about coroutines, I understood the concept immediately. What frustrated me was understanding the mechanism to apply them in C#. Most of the posts and topics I uncovered assumed a fairly advanced base of knowledge and often jumped into a solution with a framework and didn’t describe the framework itself. I consider myself a fast learner and the fact that I was hitting my head against a brick wall and failing to find some decent “101” tutorials put me on a mission: first, build my own framework from scratch to prove to me that I truly understand coroutines in C#, and second, blog about it so that you, too, can benefit from the learning process to receive better understanding and have an easy set of references to learn about them.
Please use the comments below to let me know if I succeeded!
Note: while I did build my own framework to gain understanding, the example here uses an existing, free library that has been maintained for years. It has been thoroughly tested for stability and performance and is chock full of additional features and benefits … I’ll share more on that in a bit.
In summary, a coroutine is a subroutine or method with multiple entry and exit points that maintains state between calls. That is me paraphrasing the various definitions there. The two keys to a coroutine are that you may enter it from places other than the beginning, and that it retains state when you enter it again after exiting.
When I mentioned that C# doesn’t directly support coroutines, I wasn’t 100% correct. C# has native support for a very specialized form of a coroutine, known as an iterator. You use these all of the time in your code. Traditional thought is that a foreach loop really just takes a list, like a piece of tape, and then spins through the tape, right? We really just take an index, then increment the index to look at item[0] and item[1] and we’re on our merry way, right?
Not quite. The most common uses of iterators work that way, but behind the scenes something far more complex is happening. Whenever you use an iterator (which is really any time you use the keyword foreach
), you are invoking a state machine under the covers.
If this is news to you, stop right now and head over to my article, What’s in Your Collection Part 3 of 3. Don’t worry, parts 1 and 2 just cover basic collections. This part focuses on iterators and shows how an iterator is really a state machine.
Those of you already familiar with the yield
command know this. For an example of exploiting this knowledge, take a look at Pipeline and Yield in C#. Once you understand the iterator is a state machine, you begin to realize the possibilities. You don’t always have to have the full list, for example. You can generate it on the fly! This way you only create objects in the list when and as you need them.
So this is great, we understand a little bit about coroutines and iterators in C#, but what does that have to do with “sequential asynchronous workflows in Silverlight using coroutines?” A lot, actually.
You see, we can take advantage of the iterator state machine to build our own coroutines. The way we do that is by taking over the IEnumerator
implementation required to perform iterations.
Take a moment to look at the IEnumerator documentation. Now, stop thinking of foreach
as a simple loop, and instead, consider it a state machine. You climb into the state machine, and the rules are simple:
- We’re currently at
Current
. In fact, we’re stuck here untilMoveNext
is called. - Only a call to
MoveNext
will let us advance. If the call is true, we are now at a newCurrent
state, so back to step 1. If the call is false, we’re done. We can either go home, orReset
and start all over again.
The only caveat is that MoveNext
can really do a number of things, until it hits a yield statement. Once the yield statement is reached, the process stops until the next MoveNext
is called.
Those are some pretty simple rules, aren’t they? Armed with that knowledge, you should now be able to predict what will happen in this simple little routine:
public void PrintAll() { foreach(int x in IntegerList()) { Console.WriteLine(x); } } public IEnumerable<int> IntegerList() { yield return 1; Thread.Sleep(1000); yield return 2; yield return 3; }
In this program, you’ll see 1, then wait a second, then see 2 and 3, then hang. Yes, hang: the program will not end. Remember our rule that we keep doing “something” until we hit a yield statement. In the state machine above, we return 1 and save our state. When the loop returns, we call move next. This results in jumping into our spot right after the yield, where we sleep, then return 2. After we return 3, the foreach loop still asks for something more. The state machine hits the end of the yield statement, and returns … never finding anything. Because yield isn’t hit, the machine just sits there, waiting.
To rectify the situation, we add a “yield break” to the end. This will result in the MoveNext
call returning false and let our state machine know we’re out of states. Then, unless we reset the enumerator, we’re done.
Solving the Problem with Coroutines
Let me restate the problem: we have asynchronous calls that we want to process sequentially without having to chain dozens of methods or resort to complex lambda expressions. How do we do it?
The trick lies within our state machine. Because the state machine is linked to two key events: MoveNext
and the yield
keyword, we can hook into the enumerator and force the state machine to wait for our asynchronous actions to complete. We do this without blocking other threads: everything else still happens asynchronously, we just have a “sequential view” of our workflow.
The first step is to consider a generic contract we’ll use to navigate the workflow. This is similar to Rob’s but I chose to use a delegate instead of an event, as I don’t need multiple subscribers to the workflow. This is a very simple interface, but it’s important to note something: the Yield
method is what is called for the class to start it’s “life” in the state machine. The Yielded
delegate should be called when it’s done. If I have an instant action, then yield might be:
() => { action(); Yielded(); }
So you enter through one function, but call the other. Who sets the Yielded
action? Our enumerator, or the “state machine manager” that is taking care of everything. Here’s the interface:
public interface ICoroutine { void Yield(); Action Yielded { get; set; } }
Note: I’m using this interface to help conceptualize the model. We won’t actually use this interface in the final solution. It helps here with understanding the general flow.
Before we worry about an implementation, let’s conceptualize the engine that drives this, I’ll call the engine CoroutineManager
. This is the “master of the universe” that controls the state engine. Let me show the idea to you first, then we’ll talk about what it does:
CoroutineManager Begin: Yield() Yield: Move Iterator At End? Yes: Done No: Grab next item Set next item "Yielded" action to point to local "Yield" action Call "Yield" on item
OK, let’s break it down. This is driving a state machine. The method will encapsulate our workflow. Each item in the workflow gets passed into the coroutine engine, does it’s thing, then exits and moves to the next workflow item.
To help us enter our workflow, we have a Begin
method to start the process. This simply calls into the Yield, which is the main routine.
Now we play a game of back-and-forth using yield and yielded. The manager’s yield method moves the iterator along. If there is nothing left, it stops. Our workflow is finished. Otherwise, it does two interesting things. First, it hooks up the next instance to call back to the engine by setting the Yielded
action. While this is done sequentially, it also happens asynchronously. I prove that in the sample code by having several workflow happen simultaneously. We’ll get to that in a second.
Once the callback is wired in, it calls into the item to execute. Note that our enumerator is now “hanging” and cannot go anywhere until we come back into the Yielded
action, and then it will go to look for another yield in the code.
Conceptually, it looks something like this … here our workflow has some miscellaneous code, then yield returns an ICoroutine
instance, then has some more code, etc. Keep in mind the “hook” into yielded happens based on an asynchronous event completing, or a direct call:
So how do we use this engine, now that we have it defined?
The Wintellect PowerThreading Library
Wintellect’s Jeffrey Richter has built a PowerThreading Library that does exactly what we needed. The Silverlight version is lightweight (under 30KB) but contains some powerful functionality. I’ll let you drive through the full capabilities of the library; we’re going to focus on two key aspects.
First, the AsyncEnumerator class is what helps us implement the ICoroutine
concept. This class allows for asynchronous sequential enumeration of any process that implements the IAsyncResult interface. Next, we’ll use the EventAPMFactory
to cast event-driven processes into an asynchronous result. Because some of our code uses action callbacks instead of events, we’ll also take advantage of a simple event wrapper that allows any process to fire a completed event.
You can find the download link for the PowerThreading library, complete with documentation and examples, here.
First, the class that allows any action to become an event:
public class EventWrapper { public EventWrapper() { } public EventWrapper(Action action) { Begin = action; } public Action Begin { get; set; } public void End() { if (Completed != null) { Completed(this, EventArgs.Empty); } } public event EventHandler Completed; }
Fairly straightforward: set the start action, and make sure you call End
and it will raise the Completed
event.
For the sample program, I included several layers so you can see both the sequential workflow, but understand it is still truly asynchronous because other workflows are also executing at the same time. Here’s the premise:
First, I simulate a “random number” routine that gets passed a maximum value and returns a random number. We pretend this is a service call and even build in a random delay so it doesn’t return immediately. In my main page, I’m going to do a few workflow tasks like animating a text box, etc. Then, I make a square 400 x 400. I get a random number from the “service” with a max of 400, and draw that square. If the square has a size of 320 x 320, I then get another random number between 0 and 320. The result is subsequently smaller squares until we hit zero.
The signature of the random number routine is:
... void GenerateRandomNumber(int max, Action<int> result); ...
To make it even more interesting, the squares themselves use a workflow to get random numbers to set their fill color. This is interesting because they use the same random number generator with the delay built in, so each square may take several seconds before it gets a color. The reason I did this is so you can see the squares being drawn and colored at the same time: proof we are asynchronously stepping through each workflow, however the individual workflows themselves are firing sequentially.
Here is the XAML for a square:
<UserControl x_Class="Coroutines.SquareView" xmlns_x="http://schemas.microsoft.com/winfx/2006/xaml" x_Name="SquareHost" > <Grid x_Name="LayoutRoot" Background="Black" HorizontalAlignment="Center" VerticalAlignment="Center"> <Rectangle x_Name="Square" Width="{Binding Size}" Margin="3" Height="{Binding Size}" Fill="{Binding RectangleFill}"/> </Grid> </UserControl>
As you can see, it’s a black grid with a rectangle that is bound to a size and a fill property. Here’s the code behind:
[Export] public partial class SquareView : IPartImportsSatisfiedNotification { [Import] public RandomNumberService Service { get; set; } public SquareView() { InitializeComponent(); SetValue(NameProperty,Guid.NewGuid().ToString()); LayoutRoot.DataContext = this; } public static readonly DependencyProperty SizeProperty = DependencyProperty.Register( "Size", typeof (int), typeof (SquareView), null); public int Size { get { return (int) GetValue(SizeProperty); } set { SetValue(SizeProperty, value);} } public static readonly DependencyProperty RectangleFillProperty = DependencyProperty.Register( "RectangleFill", typeof(SolidColorBrush), typeof(SquareView), new PropertyMetadata(new SolidColorBrush(Colors.Gray))); public SolidColorBrush RectangleFill { get { return (SolidColorBrush)GetValue(RectangleFillProperty); } set { SetValue(RectangleFillProperty, value); } } private IEnumerator<Int32> ColorWorkFlow(AsyncEnumerator ae) { int alpha = 0, red = 0, green = 0, blue = 0; ae.SetOperationTag("Starting random number request."); var randomNumberWrapper = new EventWrapper(); randomNumberWrapper.Begin = () => Service.GetRandomNumber(128, result => { alpha = result + 128; randomNumberWrapper.End(); }); var eventArgsFactory = new EventApmFactory<EventArgs>(); EventHandler serviceEnded = eventArgsFactory.PrepareOperation(ae.End()).EventHandler; randomNumberWrapper.Completed += serviceEnded; randomNumberWrapper.Begin(); yield return 1; eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); ae.SetOperationTag("Starting request for color red."); randomNumberWrapper.Begin = () => Service.GetRandomNumber(255, result => { red = result; randomNumberWrapper.End(); }); randomNumberWrapper.Begin(); yield return 1; eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); ae.SetOperationTag("Starting request for color green."); randomNumberWrapper.Begin = () => Service.GetRandomNumber(255, result => { green = result; randomNumberWrapper.End(); }); randomNumberWrapper.Begin(); yield return 1; eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); ae.SetOperationTag("Starting request for color blue."); randomNumberWrapper.Begin = () => Service.GetRandomNumber(255, result => { blue = result; randomNumberWrapper.End(); }); randomNumberWrapper.Begin(); yield return 1; randomNumberWrapper.Completed -= serviceEnded; eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); RectangleFill = new SolidColorBrush(new Color { A = (byte)alpha, B = (byte)blue, G = (byte)green, R = (byte)red }); ae.SetOperationTag("End of Color Workflow."); yield break; } public void OnImportsSatisfied() { var ae = new AsyncEnumerator("Color Workflow"); ae.BeginExecute(ColorWorkFlow(ae), ae.EndExecute); } }
The user control itself is exported. We have to import the service for the random number, so we wait until that is available using IPartImportsSatisfiedNotification
. Once it is imported, we begin our workflow. You’ll notice I’m using a helper class to call to the random number service and get the alpha, red, green, and blue values for my color. When I’m done, I set it and break out of the loop. In a nutshell, here are the steps we’re taking:
- Create an
AsyncEnumerator
and kick off the process - Implement
IEnumerator<Int32>
for the workflow. The documentation for the PowerThreading library explains this. Basically, the enumerator uses the yield return statements to keep track of how many pending asynchronous calls exist. We return 1 each time we start a process because were queuing up a single call. The yield statements cause the state machine to block until the asynchronous calls are complete.
With our ICoroutine
concept, we “yield” or begin the process by kicking off the actual asynchronous call. The call’s end is hooked into the Async Enumerator’s End
method (the “yielded” concept we discussed). The yield returns are what lock the state engine to wait for the operation to complete.
If you recall the diagram above, here is what the overall process looks like:
- Kick it off by calling
BeginExecute
on the coroutine manager (ourAsyncEnumerator
) - Wire in a process so it completes by calling the coroutine manager’s
End
method - Kick off the process
- Yield
- The engine now blocks until the asynchronous call completes and hooks back into the end method
- Now we dequeue the result, and decide if we wish to continue the workflow or end it
By wrapping the random service call in the event wrapper, I can use the library’s EventAPMFactory
to convert the event to an asynchronous result that implements IAsyncResult
. You’ll notice we hook into the completed event by pointing to our enumerator, which will handle grabbing the result and implementing the interface for us. When it comes back, we dequeue the result and continue on, and unhook the event handler at the end.
The code behind does some other interesting things. You’ll notice it exposes the dependency properties for the size and color so it sets the data context for the grid to itself. Not always the best practice, but it made this example a little more simple. You’ll also notice we aren’t doing anything with the size. Where does that come from?
The size is populated by the main workflow in the main page. The main page has a text box and some story boards. It dynamically inserts the squares onto the grid surface via the workflow. Take a look at the page XAML:
<UserControl x_Class="Coroutines.MainPage" xmlns_x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns_d="http://schemas.microsoft.com/expression/blend/2008" xmlns_mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc_Ignorable="d" d_DesignWidth="640" d_DesignHeight="480"> <Grid> <Grid.Resources> <Storyboard x_Name="ScaleText"> <DoubleAnimation Storyboard.TargetName="TextScale" Storyboard.TargetProperty="ScaleX" From="0.3" To="1.0" Duration="0:0:2"/> <DoubleAnimation Storyboard.TargetName="TextScale" Storyboard.TargetProperty="ScaleY" From="0.3" To="1.0" Duration="0:0:2"/> </Storyboard> <Storyboard x_Name="SlideText"> <DoubleAnimation Storyboard.TargetName="TextSlide" Storyboard.TargetProperty="X" From="400" To="0" Duration="0:0:2"/> </Storyboard> <Storyboard x_Name="HideText"> <ObjectAnimationUsingKeyFrames Duration="0:0:2" Storyboard.TargetName="Loading" Storyboard.TargetProperty="(UIElement.Visibility)"> <DiscreteObjectKeyFrame KeyTime="0:0:0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> <DiscreteObjectKeyFrame KeyTime="0:0:2"> <DiscreteObjectKeyFrame.Value> <Visibility>Collapsed</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Storyboard.TargetName="Loading" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="0:0:2" From="1.0" To="0"/> </Storyboard> </Grid.Resources> <Grid x_Name="LayoutRoot"> </Grid> <Button x_Name="BeginButton" Content="Click to Begin" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock FontSize="30" Foreground="White" FontWeight="Bold" x_Name="Loading" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock.Effect> <DropShadowEffect/> </TextBlock.Effect> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform x_Name="TextScale"/> <TranslateTransform x_Name="TextSlide"/> </TransformGroup> </TextBlock.RenderTransform> </TextBlock> </Grid> </UserControl>
Notice the layout root is empty, and nested inside another grid. This allows us to layer the button to kick the workflow off and the text box we’ll use to show some animations on top of everything.
The fun, of course, is in the code behind:
[Export] public partial class MainPage : IPartImportsSatisfiedNotification { [Import] public ExportFactory<SquareView> ViewFactory { get; set; } [Import] public RandomNumberService Service { get; set; } public MainPage() { InitializeComponent(); } public void OnImportsSatisfied() { BeginButton.Click += (o, e) => { var asyncEnum = new AsyncEnumerator("Main Workflow"); asyncEnum.BeginExecute(Workflow(asyncEnum), asyncEnum.EndExecute); }; } public IEnumerator Workflow(AsyncEnumerator ae) { BeginButton.Visibility = Visibility.Collapsed; // get rid of the button Loading.Text = "Animating"; SlideText.Completed += (o, e) => ((Storyboard)o).Stop(); SlideText.Begin(); var eventArgsFactory = new EventApmFactory<EventArgs>(); EventHandler storyboardEnded = eventArgsFactory.PrepareOperation(ae.End()).EventHandler; ScaleText.Completed += storyboardEnded; ae.SetOperationTag("Begin ScaleText Storyboard"); ScaleText.Begin(); yield return 1; ScaleText.Completed -= storyboardEnded; ScaleText.Stop(); eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); Loading.Text = "Loading Squares"; int max = 400; while (max > 5) { var routedEventWrapper = new EventWrapper(); EventHandler eventEnded = eventArgsFactory.PrepareOperation(ae.End()).EventHandler; ae.SetOperationTag("Adding a square."); var square = ViewFactory.CreateExport().Value; square.Size = max; routedEventWrapper.Begin = () => LayoutRoot.Children.Add(square); square.Loaded += (o, e) => routedEventWrapper.End(); routedEventWrapper.Completed += eventEnded; routedEventWrapper.Begin(); yield return 1; routedEventWrapper.Completed -= eventEnded; eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); ae.SetOperationTag("Getting a new random number."); var eventWrapper = new EventWrapper(); int i = max; eventWrapper.Begin = () => { Service.GetRandomNumber(i, result => { max = result; eventWrapper.End(); }); }; EventHandler randomCompleted = eventArgsFactory.PrepareOperation(ae.End()).EventHandler; eventWrapper.Completed += randomCompleted; eventWrapper.Begin(); yield return 1; eventWrapper.Completed -= randomCompleted; eventArgsFactory.EndInvoke(ae.DequeueAsyncResult()); } Loading.Text = "Goodbye"; HideText.Begin(); ae.SetOperationTag("End of main workflow."); yield break; } }
So the first thing you’ll notice is my use of MEF’s ExportFactory
. This let’s me get the “means to create” an export, rather than a single export value. This allows me to generate as many squares as I like, all using MEF’s composition engine to inject the service so the workflow can create the random colors.
We wire the button click when the imports are ready, and that kicks off the workflow.
Notice how easy it is to read the sequential workflow. We hide the button and set the text up. I kick off one animation asynchronously (the expansion) but plug the other into the workflow. This is to show how you can mix the asynchronous and sequential calls: these are two different storyboards, but they run at the same time and only when the coroutine one ends does the text update. Our state engine allows us to have our own loops, so we continue to loop until the random value gets too small to draw a visible square. Notice I actually wait for the square to get loaded into the visual tree before I draw the next square. This means you’ll get several squares with the default (gray) color visible while the color workflow fires and then eventually colors the squares when complete.
When the last square is drawn, it changes the text and fades it out and then the workflow ends.
Here is the actual application for you to play with:
Click here to see the application in a new window
There you have it! Feel free to pull down the source and kick the tires. I hope this article has done a decent job of explaining what coroutines are, how you can implement them in C# and what they are practical for. Please share your feedback and comments below as I believe this area is very interesting for discussion.
Download the source for this post Note: this requires Wintellect’s PowerThreading library. Download that here, then follow the instructions on the download link to add the reference.
We put security at the center of everything we do.
We understand that technology is a critical part of your business and that the applications you rely can never go down.
We offer an extensive suite of capabilities to improve your Information Security (InfoSec) including meeting specific compliance requirements.
Trusted advisor with shared responsibility and liability.
We take on our customer’s infrastructure security and compliance concerns by becoming an extension of their team or their application development vendor. We share the responsibility and liability associated with maintaining a secure or compliant environment, and stand by that commitment with a guarantee based on the defined lines of responsibility.
Secure and able to meet specific compliance requirements.
Our methodology encompasses design through deployment and focuses on delivering InfoSec and Compliance solutions which are realistically implementable. Our services span the entire computing stack, from connectivity to applications, with stringent physical and logical security controls. Whether you are looking for a higher level of security and protection or need to comply with specific regualtory mandates, we have the expertise to deliver the right solution.
Get a thorough assessment.
Build the right plan.
Rely on 24x7x365 proactive services.
Stay in good standing.
We can be counted on to actively help you prepare and pass required industry compliance audits.
We always implement networks which deliver better security and usability.
All our deployments take into consideration the end-to-end solution and we secure all aspects of the network and connectivity.
- Defense in Depth – Our approach offers a flexible and customizable set of capabilities based on enterprise-grade standards for performance, availability, and response.
- WANs, MANs, LANs, and VPNs – We provide comprehensive networking and high-speed connectivity options from Wide Area Networks (WAN), Metropolitan Area Networks (MAN) and Local Area Networks (LAN), as well as managed Virtual Private Networks (VPNs) and firewalls.
- Stay current – We can be counted on to deliver proactive testing and the most secure network protocols.
- Reduce vulnerabilities – We help your team leverage advanced security features coupled with relentless vigilance from trained experts.