在执行较为耗时的处理时,很容易出现用户界面“卡顿”现象,用异步编程模型,将耗时处理的代码放到另一个线程上执行,不会阻止用户界面线程的继续执行,应用程序 就不再出现“卡顿”现象。
本例子提供同步加载和异步加载两种实现。
例子:打开图片
同步、异步区别 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading.Tasks; using System.IO; namespace MyApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void AddItemToListView(string filename, int width, int height, float dpix, float dpiy) { ListViewItem item = new ListViewItem(); item.Text = filename; item.SubItems.Add(width.ToString()); item.SubItems.Add(height.ToString()); item.SubItems.Add(dpix.ToString("N1")); item.SubItems.Add(dpiy.ToString("N1")); this.listView1.Items.Add(item); } private void btnSyn_Click(object sender, EventArgs e)//同步 { if (this.folderBrowserDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK) { this.listView1.Items.Clear(); this.listView1.BeginUpdate(); //开始更新 // 取得目录路径 string dir = this.folderBrowserDialog1.SelectedPath; // 搜索目录下的所有Jpg图片 string[] jpgFiles = null; try { jpgFiles = Directory.GetFiles(dir, "*.jpg", SearchOption.AllDirectories); } catch { } if (jpgFiles != null) { foreach (string file in jpgFiles) { // 从文件中加载图像 string fileName = Path.GetFileName(file); int width = 0, height = 0; float dpiX = 0f, dpiY = 0f; // 读取图像数据 using (Bitmap bmp = (Bitmap)Bitmap.FromFile(file)) { width = bmp.Width; height = bmp.Height; dpiX = bmp.HorizontalResolution; dpiY = bmp.VerticalResolution; } // 向ListView中添加项 AddItemToListView(fileName, width, height, dpiX, dpiY); } } this.listView1.EndUpdate(); //结束更新 } } private void btnAsync_Click(object sender, EventArgs e)//异步 { if (this.folderBrowserDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK) { // 取得用户选择的路径 string dir = this.folderBrowserDialog1.SelectedPath; // 清空ListView中的项 this.listView1.Items.Clear(); this.listView1.BeginUpdate(); this.btnAsync.Enabled = btnSyn.Enabled = false; Task.Run(() => { // 获取目录下的所有Jpg文件 string[] files = Directory.GetFiles(dir, "*.jpg", SearchOption.AllDirectories); foreach (string imgFile in files) { string imageFileName; //文件名 int width, height; //宽度和高度 float dpiX, dpiY; //水平和垂直分辨率 imageFileName = Path.GetFileName(imgFile); // 加载图像 using (Bitmap bmp = (Bitmap)Bitmap.FromFile(imgFile)) { width = bmp.Width; height = bmp.Height; dpiX = bmp.HorizontalResolution; dpiY = bmp.VerticalResolution; } // 向ListView添加项 listView1.BeginInvoke((Action<string, int, int, float, float>)AddItemToListView, imageFileName, width, height, dpiX, dpiY); } // 完成更新 listView1.BeginInvoke((Action)delegate() { listView1.EndUpdate(); btnSyn.Enabled = btnAsync.Enabled = true; }); }); } } } }
TASK
Task为.NET提供了基于任务的异步模式,它不是线程,它运行在线程池的线程上。
创建 Task
创建Task有两种方式,一种是使用构造函数创建,另一种是使用 Task.Factory.StartNew 进行创建。如下代码所示
一.使用构造函数创建Task
Task t1 = new Task(MyMethod);
二.使用Task.Factory.StartNew 进行创建Task
Task t1 = Task.Factory.StartNew(MyMethod);
其实方法一和方法二这两种方式都是一样的,Task.Factory 是对Task进行管理,调度管理这一类的。好学的伙伴们,可以深入研究。这不是本文的范畴,也许会在后面的文章细说。
Task 类还提供了构造函数对任务进行初始化,但的未计划的执行。 出于性能原因, Task.Run 或 TaskFactory.StartNew(工厂创建) 方法是用于创建和计划计算的任务的首选的机制,但对于创建和计划必须分开的方案,您可以使用的构造函数(new一个出来),然后调用 Task.Start 方法来计划任务,以在稍后某个时间执行。
//第一种创建方式,直接实例化:必须手动去Start var task1 = new Task(() => { //TODO you code }); task1.Start(); //第二种创建方式,工厂创建,直接执行 var task2 = Task.Factory.StartNew(() => { //TODO you code });
在实际编程中,我们用的较多的是Task、Task.Factory.StarNew、Task.Run,接下来简单的表述下我的理解。
新手浅谈C#Task异步编程
Task与Task<TResult>类
Task类和Task<TResult>类,后者是前者的泛型版本。TResult类型为Task所调用方法的返回值。
主要区别在于Task构造函数接受的参数是Action委托,而Task<TResult>接受的是Func<TResult>委托。
- Task(Action)
- Task<TResult>(Func<TResult>)
启动一个任务
- static void Main(string[] args)
- {
- Task Task1 = new Task(() => Console.WriteLine("Task1"));
- Task1.Start();
- Console.ReadLine();
- }
通过实例化一个Task对象,然后Start,这种方式中规中矩,但是实践中,通常采用更方便快捷的方式
Task.Run(() => Console.WriteLine("Foo"));
这种方式直接运行了Task,不像上面的方法还需要调用Start();
Task.Run方法是Task类中的静态方法,接受的参数是委托。返回值是为该Task对象。
Task.Run(Action)
Task.Run<TResult>(Func<Task<TResult>>)
下面例子计算阶乘,并返回结果
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace MyApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { uint baseNum = 0; if (!uint.TryParse(textBox1.Text, out baseNum)) { MessageBox.Show("请输入一个整数作为基数。"); return; } // 更新进度条的组件 IProgress<int> progress = new Progress<int>((p) => progressBar1.Value = p); // 声明后台任务 Task<long> task = new Task<long>(() => { long res = 0L; for (int n = 1; n <= baseNum; n++) { // 累加 res += n; // 计算进度 double pr = Convert.ToDouble(n) / Convert.ToDouble(baseNum) * 100d; // 报告进度 progress.Report(Convert.ToInt32(pr)); } return res; }); textBox2.Text = "正在计算……"; // 启动任务 task.Start(); // 等待任务完成 button1.Enabled = false; while (!task.Wait(50)) { // 在等待过程中允许程序处理其他消息 Application.DoEvents(); } button1.Enabled = true; // 获取计算结果 textBox2.Text = "计算结果:" + task.Result.ToString(); } } }
因为要返回最终计算结果,所以本例子使用的是Task<TResult>类,在实例 化的时候 需要提供一个委托实例,该委托定义异步任务需要执行哪些操作。在调用start方法后,任务就被启动了,随后可以调用wait方法来等待任务完成。
BeginInvoke方法
C# 委托的三种调用示例(同步调用、异步调用、异步回调)
由于线程安全和保护用户界面完整性的考虑,一般来说,不能跨线程去修改用户界面,因此,需要调用BeginInvoke方法并通过一个委托在用户界面所在的线程上去更新用户界面。
传递给Thread构造函数的委托有两种:一种表示不带参数方法的委托;另一种表示 带一个object类型 参数方法的委托。
BeginInvoke与Invoke的区别?????
Thread类 多线程
C#thread线程类
Thread类位于System.Threading下,如果 要用,需引用。
thread.IsBackground=true;可以设置为后台线程
thread.Start();开始执行线程。
Thread.Sleep方法是静态方法,表示让当前线暂停一段时间后,再往下执行,通常以毫秒为计算单位。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Windows.Forms; namespace MyApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Thread newThread = new Thread(DoWork); // 禁用按钮 button1.Enabled = false; // 启动新线程 newThread.Start(); } /// <summary> /// 该方法将在新线程上执行 /// </summary> private void DoWork() { int n = 0; while (n < 100) { n++; Thread.Sleep(100); // 更新进度条 this.BeginInvoke(new Action(() => { this.progressBar1.Value = n; })); } this.BeginInvoke(new Action(delegate() { // 恢复按钮的可用状态 button1.Enabled = true; // 恢复进度条的值为0 progressBar1.Value = 0; // 显示提示信息 MessageBox.Show("操作完成。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); })); } } }
线程锁
由于线程是抢占式使用处理器的时间片,当多个线程同时访问某个资源时就会出现不一致的问题。
为解决一个问题,c#语言中提供 了lock关键字,该关键字可以对多个线程都 要访问的资源 进行锁定,哪个线程首先占有资源就把该资源锁定,其他想要访问该资源的线程只能等待改线程访问完成并解锁后才能访问资源,也就说,同一时刻只允许一个线程访问资源 。
将要锁定的代码放到紧跟lock关键字的语句块中,lock关键字要使用一个引用类型的实例来完成锁定。
lock(obj)
{
}
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace MyApp { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void OnStart(object sender, RoutedEventArgs e) { TicketNum = 10; txtDisplay.Clear(); // 创建15个线程并启动 Thread[] ths = new Thread[15]; for (int x = 0; x < 15; x++) { ths[x] = new Thread(Sell); } foreach (Thread t in ths) { t.Start(); } } // 表示所剩飞机票的数量 private static int TicketNum = 10; // 用于加锁时使用的对象 private object lockObject = new object(); private void Sell() { lock (lockObject) //锁定 { if (TicketNum > 0) { Thread.Sleep(300); // 数量减1 TicketNum--; // 显示剩余的票数 string msg = string.Format("还剩余{0}张机票。 ", TicketNum.ToString()); this.Dispatcher.BeginInvoke((Action)(delegate()//WPF应用程序中也不允许跨线程直接访问用户界面元素,必须通过Dispatcher.BeginInvoke方法来访问用户界面元素。 { txtDisplay.AppendText(msg); })); } } } } }
通过委托执行异步操作
使用IProgress实现异步编程的进程通知
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Numerics; namespace MyApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnStart_Click(object sender, EventArgs e) { int baseNum = default(int); if (!int.TryParse(txtBaseNum.Text, out baseNum)) { MessageBox.Show("请输入一个整数。"); return; } txtResult.Clear(); // 用于报告进度的对象 IProgress<int> progressReporter = new Progress<int>((p) => { this.progressBar1.Value = p; }); // 计算阶乘的委托实例 Func<int, BigInteger> ComputeAction = (bsNum) => { BigInteger bi = new BigInteger(1); for (int i = 1; i <= bsNum; i++) { bi *= i; //相乘 // 计算当前进度 double ps = Convert.ToDouble(i) / Convert.ToDouble(bsNum) * 100d;// 进度条值 progressReporter.Report(Convert.ToInt32(ps)); } return bi; }; // 开始调用 btnStart.Enabled = false; ComputeAction.BeginInvoke(baseNum, new AsyncCallback(FinishCallback), ComputeAction); } private void FinishCallback(IAsyncResult ar) { // 取出委托变量 Func<int, BigInteger> action = (Func<int, BigInteger>)ar.AsyncState; // 得到计算结果 BigInteger res = action.EndInvoke(ar); this.BeginInvoke(new Action(() => { btnStart.Enabled = true; // 显示计算结果 txtResult.Text = string.Format("计算结果:{0}", res); })); } } }
Progress<T> 实现了IProgress<T>接口,可以通过 Report方法报告与操作进度相关的数据,该对象在进度更新后允许代码直接操作用户界面。
Func<int,BigInteger>委托表示带有一个int类型的参数,并且返回BigInteger实现的方法。为什么以要用BigInteger结构来保存计算结果呢?因为阶乘的计算结果可能 很大,超出long值 的有效范围也是正常现象。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; namespace MyApp { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void OnClick(object sender, RoutedEventArgs e) { int num1 = default(int); if (!int.TryParse(txtInput1.Text, out num1)) { MessageBox.Show("请输入第一个基数。"); return; } int num2 = default(int); if (!int.TryParse(txtInput2.Text, out num2)) { MessageBox.Show("请输入第二个基数。"); return; } int num3 = default(int); if (!int.TryParse(txtInput3.Text, out num3)) { MessageBox.Show("请输入第三个基数。"); return; } // 声明用于并行操作的委托实例 Action act1 = () => { int sum = 0; for (int x = 1; x <= num1; x++) { sum += x; } // 显示结果 this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => run1.Text = sum.ToString())); }; Action act2 = () => { int sum = 0; for (int x = 1; x <= num2; x++) { sum += x; } // 显示结果 this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => run2.Text = sum.ToString())); }; Action act3 = () => { int sum = 0; for (int x = 1; x <= num3; x++) { sum += x; } // 显示结果 this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => run3.Text = sum.ToString())); }; // 开始执行并行任务 Parallel.Invoke(act1, act2, act3); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.IO; namespace MyApp { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void OnClick(object sender, RoutedEventArgs e) { if (string.IsNullOrWhiteSpace(txtDir.Text)) { MessageBox.Show("请输入有效的目录名。"); return; } // 如果目录不存在,先创建目录 if (!Directory.Exists(txtDir.Text)) { Directory.CreateDirectory(txtDir.Text); } int fileNum = 0; if (!int.TryParse(txtFileNum.Text, out fileNum)) { MessageBox.Show("请输入文件数量。"); return; } long fileSize = 0L; if (long.TryParse(txtSize.Text, out fileSize) == false) { MessageBox.Show("请输入正确的文件大小。"); return; } // 产生名件名列表 List<string> fileNames = new List<string>(); for (int n = 0; n < fileNum; n++) { // 文件名 string _filename = "file_" + (n + 1).ToString(); // 目录的绝对路径 string _dirpath = System.IO.Path.GetFullPath(txtDir.Text); // 组建新文件的全路径 string fullPath = System.IO.Path.Combine(_dirpath, _filename); fileNames.Add(fullPath); } txtDisplay.Clear(); // 用于产生随机字节 Random rand = new Random(); // 用于执行任务的委托实例 Action<string> act = (file) => { FileInfo fileInfo = new FileInfo(file); // 如果文件存在,则删除 if (fileInfo.Exists) fileInfo.Delete(); // 将数据写入文件 byte[] buffer = new byte[fileSize]; rand.NextBytes(buffer); using (FileStream fs = fileInfo.Create()) { BinaryWriter sw = new BinaryWriter(fs); sw.Write(buffer); sw.Close(); sw.Dispose(); } // 显示结果 string msg = string.Format("文件{0}已成功写入。 ", fileInfo.Name); this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => txtDisplay.AppendText(msg))); }; // 启动循环任务 Parallel.ForEach<string>(fileNames, act); } } }