On 2007-11-28 11:00:17 -0800, Peter Duniho <Np*********@NnOwSlPiAnMk.comsaid:
[...]
For fun, I'm thinking about working on a simple download simulation
that uses a queue to manage the downloads. If and when it's finished,
I'll post the code here in case you or anyone else is interested.
Might not be done today, as I've got a busy day, but maybe tomorrow.
Hi. I finished the simulation. It effectively demonstrates what I'm
talking about with respect to using a queue. You'll see that the queue
class itself is _very_ simple, and yet it contains everything you need
to implement the basic functionality you've described.
It doesn't provide dynamic resizing of the number of allowed concurrent
operations, nor of cancelling an existing operation (whether it's
started working or not). Those things would not be difficult to add
though. The basic logic is easily extended to handle those scenarios.
The application itself is a GUI application. It wasn't strictly
required for the demonstration, but it makes for an easy-to-control
mechanism for adding new work items and providing user feedback for how
those work items are processed.
It's funny...even though I know the program isn't doing any actual
work, there is something oddly satisfying about watching all those
progress bars work their way toward completion. I keep wanting to add
more work items, so I can see more progress bars finish. :)
Anyway, I'm copying the code here (see below). You can create a new,
empty project, add a new source file to the project and copy all of
this verbatim into that single file. You'll need to add references to
System, System.Drawing, and System.Windows.Forms to the project. Then
it should just compile and run.
Enjoy! I tried to provide sufficient comments for the classes and code
to explain the details, bu please feel free to post specific questions
if it's not all clear.
Pete
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;
namespace TestMultiDownloadQueue
{
// DummyAsync is probably the most complicated class here, and the
// least interesting. It only exists to provide a class with an
// async API similar to those available from classes like
// HttpWebRequest, Socket, Stream, and TcpClient but without
// requiring any actual work to be done.
//
// It has the basic "Begin" and "End" methods to start and complete
// a simulated async operation. The constructor is passed a TimeSpan
// that indicates the total time that should be consumed by the
// simulated operation. The DummyAsync class decides how to divide the
// total time into partial operations, and each call to the "Begin"
// method will consume one interval of the total time. In that way,
// it simulates an async operation that takes a certain amount of time
// to complete and which requires multiple async calls to the same
// instance to finish all of the work.
class DummyAsync
{
// Total duration of this simulated operation
private TimeSpan _tsDuration;
// Duration of a single simulated processing unit
// (i.e. one call to BeginDummy()
private TimeSpan _tsInterval;
// A list of all of the current async operations for
// this instance
private List<AsyncInfo_rgai = new List<AsyncInfo>();
// The callback delegate that the client of this class must
// implement.
public delegate void DummyCallback(IAsyncInfo iai);
// Could've used IAsyncResult, but didn't want to bother with
// supporting the full IAsyncResult model (with a wait handle, etc.)
public interface IAsyncInfo
{
object AsyncState { get; }
}
// An actual async state class. Contains the context information
// for the client, as well as this class's own internal state
information
private class AsyncInfo : IAsyncInfo
{
// I don't normally make public fields, but for this very simple
// class in a very specific test harness, I decided to not bother
// wrapping them in properties.
public DummyCallback callback;
public object context;
public TimeSpan ts;
// Stores a one-shot timer that will call us back when the
// simulated processing is done
private System.Threading.Timer _timer;
public AsyncInfo(DummyCallback callback, object context)
{
this.callback = callback;
this.context = context;
}
public void Start(TimeSpan ts, TimerCallback callback)
{
this.ts = ts;
_timer = new System.Threading.Timer(callback, this,
this.ts, TimeSpan.Zero);
}
#region IAsyncInfo Members
public object AsyncState
{
get { return context; }
}
#endregion
}
public DummyAsync(TimeSpan tsDuration)
{
_tsDuration = tsDuration;
_tsInterval = new TimeSpan(_tsDuration.Ticks / 100);
// No need to abuse our timers
if (_tsInterval < new TimeSpan(0, 0, 0, 0, 100))
{
_tsInterval = new TimeSpan(0, 0, 0, 0, 100);
}
}
// When the client calls this method, we create a new state object,
// tell it to start a timer, update our duration to indicate how
// much additional processing might be required, and save the new
// state object to our list of current async operations,
public void BeginDummy(DummyCallback callback, object context)
{
AsyncInfo ai = new AsyncInfo(callback, context);
ai.Start(new TimeSpan(Math.Min(_tsDuration.Ticks,
_tsInterval.Ticks)), _TimerCallback);
_tsDuration = new TimeSpan(Math.Max(0, _tsDuration.Ticks -
_tsInterval.Ticks));
_rgai.Add(ai);
}
// The client must call this method after being notified of the
// completion of our simulated async operation via the callback.
// This method removes the state object from our list of current
// async operations and returns to the user the time spent on
// the simulated processing, representing some amount of progress
// toward completion of the simulated operaton.
public TimeSpan EndDummy(IAsyncInfo iai)
{
AsyncInfo ai = (AsyncInfo)iai;
_rgai.Remove(ai);
return ai.ts;
}
// Our _own_ callback method, used for the timer that is used to
// simulate the work. Not to be confused with the client's callback,
// though the client's callback is in fact called from here.
private void _TimerCallback(object context)
{
AsyncInfo ai = (AsyncInfo)context;
ai.callback(ai);
}
}
// DownloadItem represents a single work item that the controller
// class managing the downloads knows about. For the manager's
// benefit, it implements a specific interface that the manager
// requires. The rest of the class is specific to our simulated work,
// but it would trivial to replace that code with code that uses
// some class that does actual work via async methods.
public class DownloadItem : DownloadManager.IDownloadItem
{
// The ProgressChanged event is not required for the demonstration
// of the queue, but is useful for providing user feedback regarding
// the progress of the operation. A real-world download object
// would likely have something similar, whether explicitly in its
// own class or implemented already by a class it uses (like
// BackgroundWorker).
//
// This ProgressChanged event, unlike the one in BackgroundWorker,
// provides a real number as the percentage information, allowing
// finer granularity than 1% intervals. The float ranges from 0.0
// to 1.0, so strictly speaking it's not a "percent" value. But
// the name is still reasonably suggestive as to the relationship
// between the value and the progress.
#region ProgressChanged event members
public class ProgressChangedEventArgs : EventArgs
{
private float _percentDone;
public ProgressChangedEventArgs(float percentDone)
{
_percentDone = percentDone;
}
public float PercentDone
{
get { return _percentDone; }
}
}
public delegate void ProgressChangedEventHandler(object sender,
ProgressChangedEventArgs e);
public event ProgressChangedEventHandler ProgressChanged;
#endregion
public DownloadItem(TimeSpan tsDuration)
{
_tsDuration = tsDuration;
}
#region IDownloadItem Members
// Even in a real-world async operation object, the Process() method
// could be nearly as simple as this. This class takes a duration in
// construction, whereas a real-world class might take some kind of web
// address object or a string that represents one. This class
// creates an instance of our DummyAsync class, whereas a real-world
// class might create an HttpWebRequest instance. Finally, this class
// calls the "Begin" method for the async API of the DummyAsync class,
// whereas a real-world class would call the "Begin" method of whatever
// async-capable class it's using.
public void Process()
{
_da = new DummyAsync(_tsDuration);
_da.BeginDummy(_AsyncCallback, null);
}
// This event must be implemented and raised when the actual
work is done,
// otherwise the queue has no way to know that it's okay to
start another
// work item.
//
// Note that while this event is _required_ by the queue manager, it
// is also used by the user-interface. A nice example of taking
// advantage of the multi-cast characteristics of events/delegates. :)
public event EventHandler Done;
#endregion
// Total time consumed by this simulated work item
private TimeSpan _tsDuration;
// Time consumed so far by this simulated work item
private TimeSpan _tsDone;
// Instance of our aync operation simulation class; equivalent to
// an HttpWebRequest, Stream, Socket, TcpClient, etc.
private DummyAsync _da;
// Callback for use with async operation class. Note that in spite
// of the fact that the async operation class is itself a simulation,
// this method is very similar to those you might find in a real-world
// async-using class. Using the parameter of the callback, the "End"
// method is called, which returns whatever results the async operation
// has accomplished. If there is still work left to be done,
the "Begin"
// is called again.
private void _AsyncCallback(DummyAsync.IAsyncInfo iai)
{
_tsDone += _da.EndDummy(iai);
if (_tsDone < _tsDuration)
{
_da.BeginDummy(_AsyncCallback, null);
}
else
{
_RaiseDoneEvent();
}
_RaiseProgressChangedEvent();
}
// A couple of standard event-raising methods
private void _RaiseProgressChangedEvent()
{
ProgressChangedEventHandler handler = ProgressChanged;
if (handler != null)
{
handler(this, new
ProgressChangedEventArgs((float)_tsDone.Ticks / _tsDuration.Ticks));
}
}
private void _RaiseDoneEvent()
{
EventHandler handler = Done;
if (handler != null)
{
handler(this, new EventArgs());
}
}
}
// The DownloadManager class is the most interesting and relevant class
// to this thread. It also happens to be the simplest. :)
//
// This class defines a simple interface, IDownloadItem, that
represents the
// bare minimum required to deal with the async work. A class must have
// a method that starts the processing, and it must implement an event
// that will be raised when the work is done.
//
// There's no real requirement that the class implementing that
// interface do its work in an async manner, but if it doesn't then
// the thread that added a work item to the queue could wind up
// blocked until the work is done. Additionally a thread completing
// some work could wind up blocked until the next dequeued work item
// has been completed (assuming any are in the queue). And of course,
// this chain of blocking could continue until all the items have
// been finally processed.
//
// Obviously then, this implementation really works best with tasks
// that can be represented with an async API, so that the call to a
// method that initializes the processing will complete immediately.
// If you want to use a class that implements IDownloadItem by doing
// all of its actual work in the Process() method, then this class
// should be changed so that it creates (for example) a BackgroundWorker
// instance for each IDownloadItem, and call the IDownloadItem.Process()
// method from that BackgroundWorker's DoWork event handler.
//
// Note that the IDownloadItem could implement Process() by using
// BackgroundWorker or similar itself. In that case, the IDownloadItem
// would basically be using an async API and would work just fine with
// this manager code.
class DownloadManager
{
// This interface is what any item that might be managed by
// this manager must implement. Note that while I called this
// class "DownloadManager" there's not really anything here
// that is specific to downloading. You could use this manager
// to control a queue of any sort of operation that can be
// encapsulated in this interface (which could be all sorts of
// things).
public interface IDownloadItem
{
void Process();
event EventHandler Done;
}
// Object instance to use for locking the queue
private object _objLock = new object();
// The queue itself
private Queue<IDownloadItem_queue = new Queue<IDownloadItem>();
// The maximum number of things that should be working
// at once.
private int _citemProcessMax;
// The number of things that are currently working.
private int _citemProcessCur;
public DownloadManager(int citemProcessMax)
{
_citemProcessMax = citemProcessMax;
}
// The client calls this with a reference to an IDownloadItem
// it wants processed.
public void NewItem(IDownloadItem item)
{
lock (_objLock)
{
// As long as we haven't hit our maximum number of
// items to concurrently process yet, just count the
// item. Otherwise, queue it up.
if (_citemProcessCur < _citemProcessMax)
{
_citemProcessCur++;
}
else
{
_queue.Enqueue(item);
item = null;
}
}
// If we get this far with "item" still being non-null,
// then it's okay to let it start working. Add ourselves
// to monitor when it's done, and start it going.
//
// Note that this is outside of the lock. This means that if
// the queue manager _is_ used with non-async type operations,
// it could still effectively be used by threaded code that is
// managing multiple operations at a higher level. It's not
// really how this code was designed to be used, but it would
// at least work.
if (item != null)
{
item.Done += _DoneEventHandler;
item.Process();
}
}
// Here's the method that's actually does the work when the
// Done event is raised.
private void _DoneItem()
{
IDownloadItem itemNext = null;
lock (_objLock)
{
// Checks our queue. If there's something in it, get the
// next item. Otherwise, we've got one fewer items working
// than we did before.
if (_queue.Count 0)
{
itemNext = _queue.Dequeue();
}
else
{
_citemProcessCur--;
}
}
// If we dequeued an item above, go ahead and start it just like we
// did in the NewItem() method. (And again, it's outside the lock)
if (itemNext != null)
{
itemNext.Done += _DoneEventHandler;
itemNext.Process();
}
}
private void _DoneEventHandler(object sender, EventArgs e)
{
_DoneItem();
}
}
// A simple form that allows the user to specify some parameters to
// control the range of random durations used to create work items, as
// well as the number of work items that should be created with each click
// of the "Start" button.
//
// In this simulation, clicking the "Start" button while some operations
// are still going would be equivalent to starting new downloads before the
// others had finished. Clicking it while no operations are still going
// would be equivalent to starting new downloads after all the previous
// ones had completed. Note that there isn't any code to specifically deal
// with the two different scenarios. Because of the way the queue works,
// the same logic handles both scenarios without any difficulty.
public class Form1 : Form
{
private const int kcitemsDefault = 1;
private TimeSpan ktsMinDefault = new TimeSpan(0, 0, 5);
private TimeSpan ktsMaxDefault = new TimeSpan(0, 0, 20);
public Form1()
{
InitializeComponent();
tbxOperations.Text = kcitemsDefault.ToString();
tbxTimeMin.Text = ktsMinDefault.ToString();
tbxTimeMax.Text = ktsMaxDefault.ToString();
}
private Random _rnd = new Random();
private int _citemSequence;
private DownloadManager _dm = new DownloadManager(5);
private void button1_Click(object sender, EventArgs e)
{
int citems;
TimeSpan tsMin, tsMax;
if (!int.TryParse(tbxOperations.Text, out citems))
{
citems = kcitemsDefault;
tbxOperations.Text = kcitemsDefault.ToString();
}
if (!TimeSpan.TryParse(tbxTimeMin.Text, out tsMin))
{
tsMin = ktsMinDefault;
tbxTimeMin.Text = ktsMinDefault.ToString();
}
if (!TimeSpan.TryParse(tbxTimeMax.Text, out tsMax))
{
tsMax = ktsMaxDefault;
tbxTimeMax.Text = ktsMaxDefault.ToString();
}
for (int iitem = 0; iitem < citems; iitem++)
{
TimeSpan tsItem = new TimeSpan((int)(_rnd.NextDouble()
* (tsMax.Ticks - tsMin.Ticks)) + tsMin.Ticks);
DownloadItem item = new DownloadItem(tsItem);
ItemStatus status = new ItemStatus(item, "Download #" +
(_citemSequence++).ToString() + ", " + tsItem.ToString());
tableLayoutPanel1.Controls.Add(status);
_dm.NewItem(item);
}
}
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be
disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.tbxOperations = new System.Windows.Forms.TextBox();
this.tbxTimeMin = new System.Windows.Forms.TextBox();
this.tbxTimeMax = new System.Windows.Forms.TextBox();
this.tableLayoutPanel1 = new
System.Windows.Forms.TableLayoutPanel();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(13, 13);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.TabIndex = 0;
this.button1.Text = "Start";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(13, 43);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(61, 13);
this.label1.TabIndex = 1;
this.label1.Text = "Operations:";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(13, 63);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(106, 13);
this.label2.TabIndex = 2;
this.label2.Text = "Duration Parameters:";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(24, 80);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(51, 13);
this.label3.TabIndex = 3;
this.label3.Text = "Minimum:";
//
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(24, 105);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(54, 13);
this.label4.TabIndex = 4;
this.label4.Text = "Maximum:";
//
// tbxOperations
//
this.tbxOperations.Location = new System.Drawing.Point(80, 40);
this.tbxOperations.Name = "tbxOperations";
this.tbxOperations.Size = new System.Drawing.Size(60, 20);
this.tbxOperations.TabIndex = 5;
//
// tbxTimeMin
//
this.tbxTimeMin.Location = new System.Drawing.Point(80, 77);
this.tbxTimeMin.Name = "tbxTimeMin";
this.tbxTimeMin.Size = new System.Drawing.Size(60, 20);
this.tbxTimeMin.TabIndex = 5;
//
// tbxTimeMax
//
this.tbxTimeMax.Location = new System.Drawing.Point(80, 102);
this.tbxTimeMax.Name = "tbxTimeMax";
this.tbxTimeMax.Size = new System.Drawing.Size(60, 20);
this.tbxTimeMax.TabIndex = 5;
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.Anchor =
((System.Windows.Forms.AnchorStyles)((((System.Win dows.Forms.AnchorStyles.Top
| System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tableLayoutPanel1.AutoScroll = true;
this.tableLayoutPanel1.ColumnCount = 1;
this.tableLayoutPanel1.ColumnStyles.Add(new
System.Windows.Forms.ColumnStyle(System.Windows.Fo rms.SizeType.Percent,
100F));
this.tableLayoutPanel1.Location = new
System.Drawing.Point(146, 12);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.RowCount = 1;
this.tableLayoutPanel1.RowStyles.Add(new
System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.Size = new System.Drawing.Size(452, 407);
this.tableLayoutPanel1.TabIndex = 6;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(610, 431);
this.Controls.Add(this.tableLayoutPanel1);
this.Controls.Add(this.tbxTimeMax);
this.Controls.Add(this.tbxTimeMin);
this.Controls.Add(this.tbxOperations);
this.Controls.Add(this.label4);
this.Controls.Add(this.label3);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TextBox tbxOperations;
private System.Windows.Forms.TextBox tbxTimeMin;
private System.Windows.Forms.TextBox tbxTimeMax;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
}
// A convenience control, containing a label and an actual progress bar.
// By wrapping those in a single UserControl, they can be managed
as a single
// unit to the TableLayoutPanel in the main form.
public class ItemStatus : UserControl
{
// This class is designed to be a viewer of a DownloadItem instance.
// You need one before creating an instance of this class.
public ItemStatus(DownloadItem item, string strName)
{
InitializeComponent();
_item = item;
lblItemName.Text = strName;
_item.ProgressChanged += _ProgressChangedHandler;
_item.Done += _DoneHandler;
}
private DownloadItem _item;
// Event handlers for the DownloadItem events. Very simple:
when progress
// has changed, update the ProgressBar instance in this
control, and when
// the processing is done, remove this control instance from
its container.
private void _ProgressChangedHandler(object sender,
DownloadItem.ProgressChangedEventArgs e)
{
this.BeginInvoke((MethodInvoker)delegate()
{
progressBar.Value =
progressBar.Minimum +
(int)(e.PercentDone * (progressBar.Maximum -
progressBar.Minimum));
});
}
private void _DoneHandler(object sender, EventArgs e)
{
Control ctlParent = this.Parent;
DownloadItem item = (DownloadItem)sender;
item.ProgressChanged -= _ProgressChangedHandler;
this.BeginInvoke((MethodInvoker)delegate()
{
ctlParent.Controls.Remove(this);
this.Dispose();
});
}
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be
disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.lblItemName = new System.Windows.Forms.Label();
this.progressBar = new System.Windows.Forms.ProgressBar();
this.SuspendLayout();
//
// lblItemName
//
this.lblItemName.AutoSize = true;
this.lblItemName.Location = new System.Drawing.Point(4, 4);
this.lblItemName.Name = "lblItemName";
this.lblItemName.Size = new System.Drawing.Size(35, 13);
this.lblItemName.TabIndex = 0;
this.lblItemName.Text = "label1";
//
// progressBar
//
this.progressBar.Anchor =
((System.Windows.Forms.AnchorStyles)((((System.Win dows.Forms.AnchorStyles.Top
| System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar.Location = new System.Drawing.Point(7, 21);
this.progressBar.Name = "progressBar";
this.progressBar.Size = new System.Drawing.Size(354, 23);
this.progressBar.Style =
System.Windows.Forms.ProgressBarStyle.Continuous;
this.progressBar.TabIndex = 1;
//
// ItemStatus
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.progressBar);
this.Controls.Add(this.lblItemName);
this.Name = "ItemStatus";
this.Size = new System.Drawing.Size(368, 52);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label lblItemName;
private System.Windows.Forms.ProgressBar progressBar;
}
// And of course the main entry point.
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(fals e);
Application.Run(new Form1());
}
}
}