Introduction
What made me think of writing this article is a project that I worked on 8 years ago. It was a stock trading web application, where each page contained three grids and sometimes more. To make the grids update with real time data we used AJAX polling: every two seconds each grid will send a request to the server to ask for new changes, and then update itself with the new data. Now I can think of many things that can be done in similar scenarios to boost performance and decrease the amount of data packets that will be sent across the network, and also decrease the pressure on the Web Server by eliminating the Polled requests coming from the clients and replacing them by a server broadcast.
Background
This article consists of two sections, one for showing how to use Task Factory
to split the work done at the server side between different threads, and thus decreasing the amount of time needed to fetch data for the clients. The other section will show how to build a real time stock application that will receive updates from server broadcasts and render them to the client.
For the second part, I will be using the ASP.net SignalR
Library, which allows the servers to push data to clients rather than waiting for the clients to request data. Now why websockets
is mentioned in the title? This is because SignalR will use the new Webscocket transport if the conditions of Websocket are available ( IIS version, Web server OS, browser compatibility...), else it will revert back to the old methods of data transfer. Microsoft has built a sample that shows how to use the new SignalR, it is also a Stock application, and there is a very nice and detailed article about this subject at Introduction to SignalR. I used this article to learn all what I need, and my sample is built by using snippets from the code provided. However I added new features and rewrote the application in a simpler style. You must have Visual Studio 2012 or later.
Part 1:
In this part we will be building a small web application with one page that has three grids. These grids will be reading from methods that will simulate a 2 seconds delay. Begin by creating a new ASP.net Web forms application, name it WebParallelProgramming.
From solution explorer, add a new project class library, name it DataLayer.
Inside this library, create a new class and name it Stock. copy and paste the below code inside this class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataLayer
{
public class Stock
{
public string Name { get; set; }
public string Symbol { get; set; }
public double LastValue { get; set; }
public double Change { get; set; }
public string Currency { get; set; }
public string ImageName { get; set; }
public Stock(string name, string symbol, double lastValue, double change, string currency, string imageName)
{
this.Name = name;
this.Symbol = symbol;
this.LastValue = lastValue;
this.Change = change;
this.Currency = currency;
this.ImageName = imageName;
}
public static List<Stock> GetPreciousMetals()
{
System.Threading.Thread.Sleep(2000);
List<Stock> preciousMetals = new List<Stock>();
preciousMetals.Add(new Stock("First Majestic Silver", "TSX FR", 5.23, 12.72, "CAD", "CA"));
preciousMetals.Add(new Stock("Newmont", "NYSE NEM", 20.01, 4.87, "USD", "US"));
preciousMetals.Add(new Stock("Endeavour Silver", "TSX EDR", 2.88, 2.86, "CAD", "CA"));
preciousMetals.Add(new Stock("Freeport-McMoRan", "NYSE FCX", 25.03, 0, "USD", "US"));
preciousMetals.Add(new Stock("Petaquilla Minerals", "TSX PTQ", 0.04, 33.33, "CAD", "CA"));
return preciousMetals;
}
public static List<Stock> GetStocks()
{
System.Threading.Thread.Sleep(2000);
List<Stock> stocks = new List<Stock>();
stocks.Add(new Stock("Bank Audi GDR", "BAG Bank", 15.12, 11, "LBP", "LB"));
stocks.Add(new Stock("National American Bank", "NAM Bank", 22.1, 3.21, "USD", "US"));
stocks.Add(new Stock("Mono Software", "MS", 2.3, 7.1, "EURO", "GE"));
stocks.Add(new Stock("Funds Trusts", "FT", 14.04, 2.9, "USD", "US"));
stocks.Add(new Stock("Food and Beverages CO", "FB CO", 22.17, 22.12, "CAD", "CA"));
return stocks;
}
public static List<Stock> GetMoneyStocks()
{
System.Threading.Thread.Sleep(2000);
List<Stock> moneyStocks = new List<Stock>();
moneyStocks.Add(new Stock("European Euro", "EURO", 1.2395, 0.1, "USD", "EU"));
moneyStocks.Add(new Stock("United Kingdom Pound", "Pound", 1.5709, 3.21, "USD", "GB"));
moneyStocks.Add(new Stock("Japanese Yen", "Yen", 0.0084, 1.2, "USD", "JA"));
moneyStocks.Add(new Stock("Canadian Dollar", "CAD", 0.87, 1.2, "USD", "CA"));
return moneyStocks;
}
}
}
Here we just created a simple stock class with some properties and data retrieval methods. Notice also the Thread.Sleep(2000)
statement that will force the method to wait for 2 seconds before returning data.
Now download and extract this zip file: flags.zip. It contains images of some countries flags, copy these images into the Images folder of you web application. Now right click you Web application and select Add-> Skin File. Accept the default name of Skin1 suggested by Visual Studio and click ok. Click Yes when prompted to place this file inside the App-Themes
folder. Copy and paste the below inside the skin file:
<asp:GridView runat="server" AutoGenerateColumns="false" HeaderStyle-HorizontalAlign="Center" AlternatingRowStyle-BackColor="LightSteelBlue" > </asp:GridView>
Replace the code inside your Default.aspx page with the below:
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" Theme="Skin1" CodeBehind="Default.aspx.cs" Inherits="WebParallelProgramming._Default" %>
<asp:Content runat="server" ID="FeaturedContent" ContentPlaceHolderID="FeaturedContent">
</asp:Content>
<asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
<asp:Label ID="lblServerResponseTime" runat="server" />
<h2>Market
</h2>
<asp:GridView ID="gvStocks" runat="server">
<Columns>
<asp:ImageField DataImageUrlField="ImageName"
DataImageUrlFormatString="~\Images\{0}.gif"
AlternateText="Country Photo"
NullDisplayText="No image on file."
HeaderText=""
ReadOnly="true" />
<asp:BoundField DataField="Name" HeaderText="Company" />
<asp:BoundField DataField="Symbol" HeaderText="Exchange Symbol" />
<asp:BoundField DataField="LastValue" HeaderText="Price" />
<asp:BoundField DataField="Change" HeaderText="Change" />
<asp:BoundField DataField="Currency" HeaderText="Currency" />
</Columns>
</asp:GridView>
<br />
<h2>Precious Metals</h2>
<asp:GridView ID="gvMetals" runat="server">
<Columns>
<asp:ImageField DataImageUrlField="ImageName"
DataImageUrlFormatString="~\Images\{0}.gif"
AlternateText="Country Photo"
NullDisplayText="No image on file."
HeaderText=""
ReadOnly="true" />
<asp:BoundField DataField="Name" HeaderText="Company" />
<asp:BoundField DataField="Symbol" HeaderText="Exchange Symbol" />
<asp:BoundField DataField="LastValue" HeaderText="Price" />
<asp:BoundField DataField="Change" HeaderText="Change" />
<asp:BoundField DataField="Currency" HeaderText="Currency" />
</Columns>
</asp:GridView>
<br />
<h2>Currenct Stock Exchange</h2>
<asp:GridView ID="gvMoney" runat="server">
<Columns>
<asp:ImageField DataImageUrlField="ImageName"
DataImageUrlFormatString="~\Images\{0}.gif"
AlternateText="Country Photo"
NullDisplayText="No image on file."
HeaderText=""
ReadOnly="true" />
<asp:BoundField DataField="Name" HeaderText="Company" />
<asp:BoundField DataField="Symbol" HeaderText="Exchange Symbol" />
<asp:BoundField DataField="LastValue" HeaderText="Price" />
<asp:BoundField DataField="Change" HeaderText="Change" />
<asp:BoundField DataField="Currency" HeaderText="Currency" />
</Columns>
</asp:GridView>
</asp:Content>
Add a reference to the DataLayer library from your project by right clicking your webapp and selecting Add Reference:
replace the code inside your Default.aspx.cs file with the following:
using DataLayer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace WebParallelProgramming
{
public partial class _Default : Page
{
protected void Page_Load(object sender, EventArgs e)
{
DateTime startDate = DateTime.Now;
gvStocks.DataSource = Stock.GetStocks();
gvStocks.DataBind();
gvMetals.DataSource = Stock.GetPreciousMetals();
gvMetals.DataBind();
gvMoney.DataSource = Stock.GetMoneyStocks();
gvMoney.DataBind();
TimeSpan span = (DateTime.Now - startDate);
lblServerResponseTime.Text = string.Format("Server reponser time was: {0} seconds", span.Seconds);
}
}
}
Here we are just binding the grids to their data sources, we are also populating a label with the time required by the server for the response. Of course this is not the correct way to calculate the server response time, but only for simplicity we are writing it like this. Run your application, you should see something like the below, notice that the server response time is 6 seconds.
Now we will use the Task parallel programming to split the data retrieval for these three grids among three different threads. Replace the code inside your Load method with the below:
DateTime startDate = DateTime.Now;
List<Stock> stocks = new List<Stock>();
List<Stock> metals = new List<Stock>();
List<Stock> money = new List<Stock>();
Task t1 = new Task
(
() =>
{
stocks = Stock.GetStocks();
}
);
Task t2 = new Task
(
() =>
{
metals = Stock.GetPreciousMetals();
}
);
Task t3 = new Task
(
() =>
{
money = Stock.GetMoneyStocks();
}
);
t1.Start();
t2.Start();
t3.Start();
Task.WaitAll(t1, t2, t3);
gvStocks.DataSource = stocks;
gvStocks.DataBind();
gvMetals.DataSource = metals;
gvMetals.DataBind();
gvMoney.DataSource = money;
gvMoney.DataBind();
TimeSpan span = (DateTime.Now - startDate);
lblServerResponseTime.Text = string.Format("Server reponser time was: {0} seconds", span.Seconds);
Here we declared three new tasks, assigned each task to go and fetch the data for a specific grid. Then we waited for the three tasks to finish before binding the returned data to the grids. Run your application and notice the response time, it is now down to two seconds.
Part 2:
In this part, we will use the SignalR
to broadcast stock updates from the server to all the clients. To learn more about SignalR, please check this link: Introduction to SignalR. SignalR will use websockets whenever possible, and will use the old methods if the conditions for Websocket
are not available.
Now in our application, instead of clients polling the server every specified time for updates, the server will broadcast the updates to the clients. This will significantly reduce the load on the server, and also on the client side. It will also reduce the network traffic between the server side and client side.
Just to understand a little bit the power of websockets, wbesocket.org did an experiment, and for one use case, there were 100, 000 clients polling through http requests every one second, the total network throughput was 665 Mb per second. When the same number of clients was receiving one message from the server every second through websockets, the total network throughput was 1.526 Mb per second! Pretty amazing, you can find the link here: http://www.websocket.org/quantum.html. Following is a figure from the same link representing a "comparison of the unnecessary network throughput overhead between the polling and the WebSocket applications":
where
- Use case A: 1,000 clients receive 1 message per second.
- Use case B: 10,000 clients receive 1 message per second.
- Use case C: 100,000 clients receive 1 message per second.
In the sample built by Microsoft, the server will broadcast the updates of each stock one at a time, we will do it in a list, that is the server will broadcast the whole list of stocks updated, and the clients will digest these updates. We will also change the color of the stocks updated according to their change.
Begin by adding SignalR to your project by opening the Tools->Library Package Manager->Package Manager Console and running the command: install-package Microsoft.AspNet.SignalR
.
Now as we will be using the SignalR
Hub API to handle client-server communications, we need to create a class that will derive from the SignalR
Hub class and will be responsible for receiving connections and method calls from clients. Right click your project and add a class named "StockBroadcasterHub.cs". Replace the code inside the class with the following:
using DataLayer;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace WebParallelProgramming
{
[HubName("stockBroadcaster")]
public class StockBroadcasterHub : Hub {
private readonly StockBroadcaster _stockBroadcaster;
public StockBroadcasterHub() : this(StockBroadcaster.Instance) { }
public StockBroadcasterHub(StockBroadcaster stockBroadcaster)
{
_stockBroadcaster = stockBroadcaster;
}
public IEnumerable<Stock> GetAllStocks()
{
return _stockBroadcaster.GetAllStocks();
}
}
}
Note: you can also right click the project and select Add->SignalR Hub Class (v2), this will add the necessary attributes for you.
This class is used to define the methods that the clients can call on the server, like the GetAllStocks() method. In our case we will not be implementing calls from the clients, only server broadcasts, however it is good to know. Also this hub is used by the clients to open connections to the server.
The HubName
attribute is used to specify how the Hub will be referenced on the client side. We will see how to use it in the JavaScript code later on. If you don't use this attribute then the default name on the client will be a camel-cased version of the class name, which in this case would be stockBroadcaster.
Now since a new Hub class instance will be created on each connection or call from the client, we will need to add a class that will store stock data, perform random additions or subtractions to the stock prices and broadcast the updates to the clients. Right click your project and add a class named: "StockBroadcaster.cs". Replace the class template code with the following:
using DataLayer;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
namespace WebParallelProgramming
{
public class StockBroadcaster
{
private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(2000);
private readonly Random _willUpdate = new Random();
private readonly Timer _timer;
private volatile bool _updatingStockPrices = false;
private readonly object _updateStockPricesLock = new object();
private IHubConnectionContext<dynamic> Clients
{
get;
set;
}
private readonly static Lazy<StockBroadcaster> _instance = new Lazy<StockBroadcaster>(() => new StockBroadcaster(GlobalHost.ConnectionManager.GetHubContext<StockBroadcasterHub>().Clients));
private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
private StockBroadcaster(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
_stocks.Clear(); var stocks = new List<Stock>();
stocks = Stock.GetStocks();
stocks.AddRange(Stock.GetMoneyStocks());
stocks.AddRange(Stock.GetPreciousMetals());
stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
_timer = new Timer(UpdateStocks, null, _refreshRate, _refreshRate);
}
public static StockBroadcaster Instance
{
get
{
return _instance.Value;
}
}
public IEnumerable<Stock> GetAllStocks()
{
return _stocks.Values; }
private void UpdateStocks(object state)
{
lock (_updateStockPricesLock)
{
if (!_updatingStockPrices)
{
List<Stock> stocks = new List<Stock>();
_updatingStockPrices = true;
foreach (var stock in _stocks.Values)
{
if (TryUpdateStockPrice(stock))
{
stocks.Add(stock);
}
}
BroadcastAllStocksPrices(stocks);
_updatingStockPrices = false;
}
}
}
private bool TryUpdateStockPrice(Stock stock)
{
var randomUpdate = _willUpdate.NextDouble();
if (randomUpdate > 0.3) {
return false;
}
var random = new Random((int)Math.Floor(stock.LastValue));
double percentChange = random.NextDouble() * 0.1;
bool isChangePostivie = random.NextDouble() > .51; double changeValue = Math.Round(stock.LastValue * percentChange, 2);
changeValue = isChangePostivie ? changeValue : -changeValue;
double newValue = stock.LastValue + changeValue;
stock.Change = newValue - stock.LastValue;
stock.LastValue = newValue;
return true;
}
private void BroadcastStockPrice(Stock stock)
{
Clients.All.updateStockPrice(stock); }
private void BroadcastAllStocksPrices(List<Stock> stocks)
{
Clients.All.updateAllStocksPrices(stocks); }
}
}
First we created a TimeSpan
that will be the refresh rate for our server broadcasts, initialize this to 2000 MS. Then we will create a Random
variable and name it _willUpdate. This will decide whether to update a given stock or not.
Next we will declare a timer that will be used to update and broadcast the stock data every 2 seconds. Now since multiple threads will be running on the same instance of the class, we will be using locks to ensure thread safety. Why use only one instance? Because this is a Web application, for all the users to be able to see the same data, a single instance must exist, or we will have to read data from a database or other datasources (Which is the real life scenario). The _updateStockPricesLock will be the object used for the locking mechanism.
Now we will create an IHubConnectionContext<dynamic>
object and name it Clients. This will be used to encapsulate all information about a SignalR
connection for a Hub.
Below that we will initialize the static Singleton instance that will be providing and updating our data. Since this is the only instance that will be allowed to be created, we will declare the Constructer as private. Lazy initializatio
n is used to ensure that the instance creation is thread safe. Also declare a dictionary to store our stocks. We also used a special type of Dictionary called ConcurrentDictionary, this is used to create a "thread-safe collection of key/value pairs that can be accessed by multiple threads concurrently."
private readonly static Lazy<StockBroadcaster> _instance = new Lazy<StockBroadcaster>(() => new StockBroadcaster(GlobalHost.ConnectionManager.GetHubContext<StockBroadcasterHub>().Clients));
private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
private StockBroadcaster(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
_stocks.Clear(); var stocks = new List<Stock>();
stocks = Stock.GetStocks();
stocks.AddRange(Stock.GetMoneyStocks());
stocks.AddRange(Stock.GetPreciousMetals());
stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
_timer = new Timer(UpdateStocks, null, _refreshRate, _refreshRate);
}
The method UpdateStocks will update all the stocks in the dictionary, put the updated stocks in a generic list, and then broadcast this list to the clients.
private void UpdateStocks(object state)
{
lock (_updateStockPricesLock)
{
if (!_updatingStockPrices)
{
List<Stock> stocks = new List<Stock>();
_updatingStockPrices = true;
foreach (var stock in _stocks.Values)
{
if (TryUpdateStockPrice(stock))
{
stocks.Add(stock);
}
}
BroadcastAllStocksPrices(stocks);
_updatingStockPrices = false;
}
}
}
The next method is the TryUpdateStockPrice. Based on the boolean called randomUpdate, it will decide whether to update the stock or not. If the stock is to be updated, its last price will be modified based on the variable called changeValue. We will also update the Stock.Change property.
private bool TryUpdateStockPrice(Stock stock)
{
var randomUpdate = _willUpdate.NextDouble();
if (randomUpdate > 0.3) {
return false;
}
var random = new Random((int)Math.Floor(stock.LastValue));
double percentChange = random.NextDouble() * 0.1;
bool isChangePostivie = random.NextDouble() > .51; double changeValue = Math.Round(stock.LastValue * percentChange, 2);
changeValue = isChangePostivie ? changeValue : -changeValue;
double newValue = stock.LastValue + changeValue;
stock.Change = newValue - stock.LastValue;
stock.LastValue = newValue;
return true;
}
Finally the broadcast method, this method will call a client side function, that is a JavaScript method residing on the client's machine. It will also pass the stocks parameter to this method, these stocks will then be parsed at the client side to update the necessary cells. Note that I also kept a method to update only one stock if you wish to use it, it is called BroadcastStockPrice.
private void BroadcastAllStocksPrices(List<Stock> stocks)
{
Clients.All.updateAllStocksPrices(stocks); }
Before we begin writing our client code, we need to register the SignalR route in our web application, add a new class and call it "Startup.cs", or you can right click your project and select Add->OWIN Startup Class. Replace the class code with the below:
using Microsoft.Owin;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
[assembly: OwinStartup(typeof(WebParallelProgramming.Startup))]
namespace WebParallelProgramming
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
Open your default.aspx page and add the following JavaScript links, be sure that they match your jQuery version and paths. The SignalR
proxies script file, which is specified in the "/signalr/hubs" URL, is dynamically generated and defines proxy methods for the methods on our Hub class, which in our case is a single method called StockBroadcasterHub.GetAllStocks().
<!---->
<!---->
<script src="/Scripts/jquery-1.7.1.min.js"></script>
-->
<script src="/Scripts/jquery.signalR-2.1.2.js"></script>
-->
<script src="/signalr/hubs"></script>
-->
<script src="StockManager.js"></script>
Now we will create the client side engine that will be receiving the broadcasts from the server and updating our grid views with the new data. Right click your project and add a new JavaScript file, name it "StockManager.js". Inside it, copy and paste the below code:
$(function () {
var stockBroadcaster = $.connection.stockBroadcaster;
function init() {
}
stockBroadcaster.client.updateStockPrice = function (stock) {
}
stockBroadcaster.client.updateAllStocksPrices = function (stocks) {
$("td.updatedStockP").removeClass("updatedStockP");
$("td.updatedStockN").removeClass("updatedStockN");
$.each(stocks, function (index, stock) { var stockSymbol = stock.Symbol; $('td').filter(function () {
return $(this).text() === stockSymbol; }).next().html(stock.LastValue.toFixed(2)).addClass(stock.Change > 0 ? "updatedStockP" : "updatedStockN").next().html(stock.Change.toFixed(2)).addClass(stock.Change > 0 ? "updatedStockP" : "updatedStockN");
});
}
$.connection.hub.start().done(init);
});
Notice the stockBroadcaster hub name, which should match the name that is used in the StockBroadCasterHub HubName
attribute. First we will use the connection object to get the hub proxy, then we will open the connection using the command:
$.connection.hub.start().done(init);
After that we will define our updateAllStockPrices method, remember this method? The one that we called in our StockBroadcaster.BroadcastAllStocksPrices() server method.
Inside this JavaScript function, we will first clear all the table cells that are marked as updated stocks cells (we will create the CSS classes shortly). Then we will loop through the stocks array that we received from the server, this is the stock parameter that we passed in our StockBroadcaster.BroadcastAllStocksPrices() method:
Clients.All.updateAllStocksPrices(stocks);
This is an array of JSON objects, this means we can call the properties of the Stock object on the items of this array. The first challenge is to find for each stock its corresponding row in the three grid views, for this we will assume that the stock symbol is unique, and we will use it to capture the row specific for that stock. Then we will use the Jquery .next()
method to get the next sibling of this cell, which is the LastValue column. After that we will use the .html()
method to write the new value. The .toFixed(2)
is used to round the number to the nearest 2 decimals. After this we use the .addClass()
method to style the cell, either with green if the change is positive, or salmon red if the change is negative. Then we will do the same thing for the change column.
Finally we will add the CSS classes to our .css file, copy and paste the below to the file Site.css found inside the Contents folder
.updatedStockP {
background-color:lightgreen;
}
.updatedStockN {
background-color:lightsalmon;
}
Also add the following to the Site.Master page to link it to the CSS file:
<link href="Content/Site.css" rel="stylesheet" type="text/css" />
Run your project and check the data updating, if the colors were not changing, press ctrl + F5 to refresh the page and load up the new CSS file. The stocks with positive change will appear in green, those with negative change will appear in red. It will take some time for the data to begin updating because of the Thread.Sleep() operations we are doing in the DataLayer class. You can download the source code of the application from the link at the top of the article.
Thanks for following up :).