基于任务并行库实现多线程下载示例
任务并行库 (TPL) 是 .NET Framework 4 版的 System.Threading 和 System.Threading.Tasks 命名空间中的一组公共类型和 API。 TPL 的目的在于简化向应用程序中添加并行性和并发性的过程,从而提高开发人员的工作效率。TPL 会动态地按比例调节并发程度,以便最有效地使用所有可用的处理器。 此外,TPL 还处理工作分区、ThreadPool 上的线程调度、取消支持、状态管理以及其他低级别的细节操作。 通过使用 TPL,您可以在将精力集中于程序要完成的工作,同时最大程度地提高代码的性能。从 .NET Framework 4 开始,TPL 是编写多线程代码和并行代码的首选方法。
本示例以下载chromium win32版本作为目标,将任务并行库运用其中。
首先简单看一下界面,放置一个进度条用于显示下载进度,两个Label分别显示当前已下载文件大小和文件总大小。
再来看一下请求辅助的代码。首先是http请求方法,一个用于请求整个页面,另一个用于请求指定内容范围。相信做过分段下载的朋友应该不会陌生。
private HttpWebResponse Requst(string url, string param, long from, long to, HttpMethod method = HttpMethod.Get) { HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest; req.UserAgent = USER_AGENT; req.Accept = ACCEPT; req.ContentType = CONTENT_TYPE; req.KeepAlive = true; req.Method = method.ToString(); req.AllowAutoRedirect = true; req.AddRange(from, to); //req.Proxy = new WebProxy("127.0.0.1", 8888) return req.GetResponse() as HttpWebResponse; } private HttpWebResponse Requst(string url, string param, HttpMethod method = HttpMethod.Get) { HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest; req.UserAgent = USER_AGENT; req.Accept = ACCEPT; req.ContentType = CONTENT_TYPE; req.KeepAlive = true; req.Method = method.ToString(); req.AllowAutoRedirect = true; //req.Proxy = new WebProxy("127.0.0.1", 8888) return req.GetResponse() as HttpWebResponse; }
其次来看一下使用任务对象完成下载的主要代码,逻辑顺序是这样:
1.用一个简单的分段函数处理文件原始大小,得到若干分段对象
2.循环生成任务对象,得到分段下载的http响应对象
3.完成并行写文件,并更新界面显示
IList<Range> list = InitRange((int)total, taskCount); IList<Task> tasks = new List<Task>(); File.Delete(ZIP_FILE); foreach (var item in list) { var task = new Task((o) => { var item2 = (Range)o; HttpWebResponse subRes = Requst(string.Format(CHROME_DOWNLOAD, version), string.Empty, item2.from, item2.to); using (var sw = subRes.GetResponseStream()) { using (var fs = new FileStream(ZIP_FILE, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) { fs.Seek(item2.from, SeekOrigin.Begin); int length = 0; byte[] byt = new byte[1000]; while ((length = sw.Read(byt, 0, byt.Length)) > 0) { fs.Write(byt, 0, length); Interlocked.Add(ref globalCurrent, length); //globalCurrent += length; Current.Text = string.Format("{0}M", (globalCurrent / 1024.00 / 1024.00).ToString("##0.00")); DownLoadProgress.Value = (int)globalCurrent; } } } subRes.Close(); }, item); tasks.Add(task); task.Start(); }
请特别注意一下任务对象执行时传参的形式,之前因为参数传得不对导致下载功能错误。
最后介绍一下剩下的代码,另外启动一个任务等待下载线程的结束,弹出提示。
var taskClose = new Task((o) => { var t = (IList<Task>)o; Task.WaitAll(t.ToArray()); if (globalCurrent == total) { MessageBox.Show("Download finished !", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); } this.Close(); }, tasks); taskClose.Start();
这个示例还有许多不足,比如没有对http请求异常进行处理,没有对写文件异常进行处理并给出提示,界面更新是在非主线程中完成,虽然可运行但并不符合winform的要求,窗口关闭没有检查任务线程状态并中止。这些细节足够花点时间优化了。作为示例,仅突出了主要目标。
给个窗口类文件全景
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Windows.Forms; using System.Threading; using System.Threading.Tasks; namespace ChromiumHelper { enum HttpMethod { Post, Get } public partial class Form1 : Form { const string USER_AGENT = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1212.0 Safari/537.2"; const string ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; const string ACCEPT_CHARSET = "GBK,utf-8;q=0.7,*;q=0.3"; const string ACCEPT_ENCODING = "gzip,deflate,sdch"; const string ACCEPT_LANGUAGE = "zh-CN,zh;q=0.8"; const string CONTENT_TYPE = "application/x-www-form-urlencoded"; const string CHROME_VERSION = "http://commondatastorage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE"; const string CHROME_DIRECT = "http://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?path=Win/{0}/"; const string CHROME_DOWNLOAD = "http://commondatastorage.googleapis.com/chromium-browser-snapshots/Win/{0}/chrome-win32.zip"; const string ZIP_FILE = "chrome-win32.zip"; long total = 0;//文件总长度 int taskCount = 3;//线程数 long globalCurrent = 0;//累积下载长度 public Form1() { InitializeComponent(); } private void Form1_Shown(object sender, EventArgs e) { //取得最新的版本号 HttpWebResponse res = Requst(CHROME_VERSION, string.Empty); string version = string.Empty; using (var sw = new StreamReader(res.GetResponseStream())) { version = sw.ReadToEnd(); } res.Close(); //请求下载文件,初始化界面内容 res = Requst(string.Format(CHROME_DOWNLOAD, version), string.Empty); total = res.ContentLength; res.Close(); DownLoadProgress.Maximum = (int)total; DownLoadProgress.Minimum = 0; Total.Text = string.Format("{0}M", (total / 1024.00 / 1024.00).ToString("##0.00")); IList<Range> list = InitRange((int)total, taskCount); IList<Task> tasks = new List<Task>(); File.Delete(ZIP_FILE); foreach (var item in list) { var task = new Task((o) => { var item2 = (Range)o; HttpWebResponse subRes = Requst(string.Format(CHROME_DOWNLOAD, version), string.Empty, item2.from, item2.to); using (var sw = subRes.GetResponseStream()) { using (var fs = new FileStream(ZIP_FILE, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) { fs.Seek(item2.from, SeekOrigin.Begin); int length = 0; byte[] byt = new byte[1000]; while ((length = sw.Read(byt, 0, byt.Length)) > 0) { fs.Write(byt, 0, length); Interlocked.Add(ref globalCurrent, length); //globalCurrent += length; Current.Text = string.Format("{0}M", (globalCurrent / 1024.00 / 1024.00).ToString("##0.00")); DownLoadProgress.Value = (int)globalCurrent; } } } subRes.Close(); }, item); tasks.Add(task); task.Start(); } //另起一个任务,等待下载任务结束后弹出提示并主动关闭应用 var taskClose = new Task((o) => { var t = (IList<Task>)o; Task.WaitAll(t.ToArray()); if (globalCurrent == total) { MessageBox.Show("Download finished !", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); } this.Close(); }, tasks); taskClose.Start(); } //private void Update(object current) //{ // int c = (int)current; // Current.Text = ((int)current).ToString(); // DownLoadProgress.Value = c; //} /// <summary> /// 根据下载线程数计算每个线程下载大小,获取分段集合 /// 取近似值,先均分然后把多余的加到最后一个分段上 /// </summary> /// <param name="total"></param> /// <param name="part"></param> /// <returns></returns> private IList<Range> InitRange(long total, int part) { IList<Range> list = new List<Range>(); long max = total; while (max % part > 1000000) { max++; } long division = max / part; long last = -1; for (int i = 0; i < part; i++) { Range range = new Range { from = last + 1, to = (i + 1) * division }; list.Add(range); last = range.to; } Range r = list.Last<Range>(); r.to = total; list.RemoveAt(list.Count - 1); list.Add(r); return list; } private HttpWebResponse Requst(string url, string param, long from, long to, HttpMethod method = HttpMethod.Get) { HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest; req.UserAgent = USER_AGENT; req.Accept = ACCEPT; req.ContentType = CONTENT_TYPE; req.KeepAlive = true; req.Method = method.ToString(); req.AllowAutoRedirect = true; req.AddRange(from, to); //req.Proxy = new WebProxy("127.0.0.1", 8888) return req.GetResponse() as HttpWebResponse; } private HttpWebResponse Requst(string url, string param, HttpMethod method = HttpMethod.Get) { HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest; req.UserAgent = USER_AGENT; req.Accept = ACCEPT; req.ContentType = CONTENT_TYPE; req.KeepAlive = true; req.Method = method.ToString(); req.AllowAutoRedirect = true; //req.Proxy = new WebProxy("127.0.0.1", 8888) return req.GetResponse() as HttpWebResponse; } } struct Range { public long from; public long to; } }
示例源文件:下载