C# async await异步编程
背景:做NX二次开发的时候收集了NX操作的操作日志,然后用winform做了个应用读取分析这些日志。每天会积累超过5000多份日志,所以想到了异步编程来解决速度和卡死的问题。
一、异步编程和多线程的异同
硬件里有个概念叫DMA,也就是直接访问内存不经过CPU处理,这正是异步操作的硬件基础。异步编程无需额外的线程负担,死锁的情况也少,但是和自然人的思维方式有些不一样。而多线程中的各个线程的代码还是顺序执行的,自然人理解起来简单一些,但是线程的滥用反而会造成很多问题。
所以适用场合分别为:
异步:直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用
多线程:需要长时间CPU运算的场合
二、异步函数简介
直接用<C# in Depth>的介绍:
async Task<int> AsyncFunction()
{
var value = await SomeLongTimeFunction();
DoUpdateOrSomething();
}
用了async修饰符声明了的函数AsyncFunction中,用await修饰某个耗时操作的表达式SomeLongTimeFunction().如果SomeLongTimeFunction没有完成那么这个异步函数AsyncFunction会立即返回。当SomeLongTimeFunction执行完毕后,异步函数将在合适的线程上继续执行下面的语句
三、使用任务的异步编程
msdn说:async 和 await 关键字不会导致创建其他线程。 因为异步方法不会在其自身线程上运行,因此它不需要多线程。 只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。 可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。
总而言之,Task能达到类似多线程的效果,但具体怎么分配线程由.net底层控制,程序员只需要专注业务逻辑即可。 同时具备以前线程不具备的返回值的功能(可以通过回调事件来处理),而在Task中只是简单的使用await来使用, await的task相当于同步的调用task, 同时会有返回值。
示例代码
ps:用了很多LINQ,看起来不太直观
ps2:现在看来其实写的复杂了,套了好几层。其实没必要把文件分组
首先是UI线程上按钮点击函数,目的是读取分析日志文件,并将结果显示在界面上:
async void ComputeClick(object sender, EventArgs e)
{
Func<DateTime, DateTime, bool, Task<int>> taskFunc = (b, end, chk) =>
{
return Task.Run(() => ReadDataAsync(b, end, chk));
};
var days = await taskFunc(date1.Value.Date, date2.Value.Date, checkBox1.Checked);
///await完毕后才会执行以下语句
///更新界面表格和图形
ShowChart();
}
显而易见,这个函数中使用了Taks.Run了一个委托taskFunc,当taskFunc执行完毕后才会更新界面。在执行过程中并没有阻塞UI线程,表现为用户仍然可以点击、浏览界面。
在ReadDataAsync中,将日志文件分为了N个组,每读取一个组就创建一个Task,这里使用了Task.WhenAll,也就是当所有任务都执行完毕以后才会继续执行
async Task<int> ReadDataAsync(DateTime begin, DateTime end, bool onlyWorkDays = true)
{
var lists = GetFilesGroup();
var tasks = lists.Select(list => ReadLogsAsync(list, begin, end, onlyWorkDays)).ToList();
var t3 = Task.WhenAll(tasks);
var results = await t3;
foreach (var objs in results.SelectMany(list => list))
{
_table.Rows.Add(objs);
}
return _table.Rows.Count;
}
继续看单个任务的异步处理方法,这里又用到了Task.Run,原理是一样的。
async Task<List<object[]>> ReadLogsAsync(IEnumerable<FileInfo> files, DateTime begin, DateTime end, bool onlyWorkDays)
{
var results = new List<object[]>();
foreach (var file in files)
{
//操作UI线程上的控件
SetProgress(1, 0, $"读取数据{file.Name}");
string user, ticks;
DateTime date;
ReadFileName(file.Name, out user, out date, out ticks);
if (DateTime.Compare(date, begin) < 0)
continue;
if (DateTime.Compare(date, end) > 0)
continue;
if (onlyWorkDays && (date.DayOfWeek == DayOfWeek.Sunday || date.DayOfWeek == DayOfWeek.Saturday))
continue;
Func<string, List<MenuButton>, Task<Dictionary<MenuButton, int>>> taskFunc = (path, buttons) =>
{
return Task.Run(() => ReadLog(path, buttons));
};
var dics = await taskFunc(file.FullName, _buttons);
results.AddRange(dics.Select(dic => new object[]
{
date, user, dic.Key.Name, dic.Key.Chs, dic.Key.CasCadeChs, dic.Value, ticks
}));
}
return results;
}
跨线程访问UI控件
刚刚肯定注意到了再ReadLogAsync中调用了控件,这里参考了这篇文章:<C# 跨线程调用控件>
private delegate void DelegateSetProgress(int value, int max, string text);
private void SetProgress(int value, int max, string text)
{
if (statusStrip1.InvokeRequired)
{
Invoke(new DelegateSetProgress(SetProgress), value, max,text);
}
else
{
progressBar.Maximum += max;
progressBar.Value += value;
var precent = (progressBar.Value*1.0/progressBar.Maximum*100.0).ToString("0.#");
progressText.Text = $"{precent}% {text}";
}
}