C#多线程应用

在上一个任务成功给团队demo之后,大家同意了我进一步的考虑——让下载数据这个进程可控,能够根据用户的需要关闭下载进程,进行其他任务。

我的想法来自于之前学Android中的AsyncTask:

AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.

如定义所述,AsyncTask能够让我们在Android程序编写中无需使用多线程,即可使后台任务独立运行,并将结果反馈给UI进程。

这里将老师的课件对比案例引用如下:


Using Async Task in Android

与一般的Sync相比为什么要使用Async呢,相信大家看完上面的栗子已经有所了解,我们再现来看看Microsoft给出的解释,注意一些关键词:

Async Improves Responsiveness
Asynchrony is essential for activities that are potentially blocking, such as when your application accesses the web. Access to a web resource sometimes is slow or delayed. If such an activity is blocked within a synchronous process, the entire application must wait. In an asynchronous process, the application can continue with other work that doesn't depend on the web resource until the potentially blocking task finishes.

由于需要改进的项目是C#编写的Window Form Application,在网上找到,在.NET的Toolbox中,有一个BackgroundWorker控件也能够让我们“proper and easy use of the UI thread"

BackgroundWorker makes threads easy to implement in Windows Forms. Intensive tasks need to be done on another thread so the UI does not freeze. It is necessary to post messages and update the user interface when the task is done.

是不是和AsyncTask的思想是一致的~~
BackgroundWorker通过DoWork,ProgressChanged,RunWorkerCompleted这三个EventHandler,分别控制程序的后台运行,进程的更新和结束后,我们只需要把任务分类到这三个EventHandler中,然后在UI线程中调用即可。

为了控制在程序在后台的运行状况和是否可被取消,需要设置它的两个属性True/False:WorkerReportsProgress, WorkerSupportsCancellation。

关于BackgroundWorker就不赘述了,在学习使用的时候,这两篇教程相见恨晚:

  1. https://www.dotnetperls.com/backgroundworker-introduction
  2. http://omegacoder.com/?p=642

BackgroundWorker作为Asynchronous Programming的基础,可以为设计结构较为简单的程序实现后台任务的运行,和Thread相比能够更方便地和UI进程进行信息交互。然而它们都比较繁琐。

在C# 4.0之后,Microsoft推出了Task。
O'REILLY出版的《C# 5.0 IN A NUTSHELL》 中指出Task弥补了Thread的不足:

A thread is a low-level tool for creating concurrency, and as such it has limitations. In particular:

  1. While it's easy to pass data into a thread that you start, there's no easy way to get a "return value" back from a thread that you Join. You have to set up some kind of shared field. And if the operation throws an exception, catching and propagating that exception is equally painful
  2. You can't tell a thread to start something else when it's finished; instead you must Join it (blocking your own thread in the process)

C#5.0之后推出了async和await关键词:

These keywords let you write asynchronous code that has the same structure and simplicity as synchronous code, as well as eliminating the "plumbing" of asynchronous programming

Task与async/await关键词两者的结合使用,让Asynchronous Programming能够在Synchronous代码的基础快速改写完成,换言之,就是简单易用。

那么剩下最后一个核心问题,如何取消与数据库连接下载数据的这个进程?

方案一:像关闭一个asynchronous method一样,将控制数据下载的进程关闭。
根据这个思路,在《Entity Framework Core Cookbook》中指出:

All asynchronous methods take an optional CancellationToken parameter. This parameter, when supplied, provides a way for the caller method to cancel the asynchronous execution.

var source = new CancelationTokenSource();
var cancel = source.Token;
cancel.Register(()=>{
  //cancelled
});
ctx.MyEntities.TolistAsync(cancel);
if(!cancel.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))){
  source.Cancel();
}

案例中的TolistAsync()方法称为API Async Methods,标志是以“Async”后缀结尾,它们的返回类型是Task(参见API Async Methods

我的项目使用的是Entity Framework,于是引入CancellationToken,调用.ToListAsync(),将SqlQuery改写如下:

IList<print_pack_list_ext>query = 
SysVariable.Database.SqlQuery<print_pack_list_ext>(sql.ToString(), parameters).ToListAsync(token);

应用这个方法后,并没有成功终止。网站上也有人遇到的了同样的问题,听说微软团队针对Entity Framework尚未解决这个问题。无奈之下,当时也有几分自豪,居然被我找到了Bug~~

方案二:是否存在一个终止的方法,直接作用在SqlQuery上面呢?
有,他就是:SqlCommand.Cancel Method (),正如他的使命:

Tries to cancel the execution of a SqlCommand.

啊,终于找到了,他就是我的韩信。

那什么时候调用这名大将呢?应该在用户取消下载任务,使CancellationToken值为False之后被Invoke。正如CancellationToken.Register Method (Action)的使命:

Registers a delegate that will be called when this CancellationToken is canceled.

所以,将SqlCommand.Cancel注册在CancelToken中即可:

using (CancellationTokenRegistration ctr = token.Register(() => cmd.Cancel()))
{
  ...
}

至此,所有的疑惑都找到了答案。

从底层向上走,首先DAO改写如下:

public async Task<IList<print_pack_list_ext>> GetPackByDate(DateTime datefrom, DateTime dateto, CancellationToken token)
{
    IList<print_pack_list_ext> list = new List<print_pack_list_ext>();
    try
    {
        await Task<IList<print_pack_list_ext>>.Run(() =>
        {
            using (SqlConnection conn = new SqlConnection(getConnectionstring()))
            {
                conn.Open();
                var cmd = conn.CreateCommand();
                using (CancellationTokenRegistration ctr = token.Register(() => cmd.Cancel()))
                {
                    #region sql string
                    string sqlString = "select a.pps_number,a.created_by from pack_list a where convert(datetime, a.created_datetime, 120) > convert(datetime, @dateFrom0, 120) and convert(datetime, a.created_datetime, 120) < convert(datetime, @dateFrom1, 120)"
                    #endregion
                    cmd.Parameters.AddWithValue("dateFrom0", datefrom);
                    cmd.Parameters.AddWithValue("dateFrom1", dateto);
                    cmd.CommandTimeout = 0;
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = sqlString;
                    
                    DataSet ds = new DataSet();
                    DataTable table = new DataTable();
                    table.Load(cmd.ExecuteReader());
                    ds.Tables.Add(table);
                    
                    #region fill model
                    list = ds.Tables[0]
                        .AsEnumerable()
                        .Select(dataRow =>
                            new print_pack_list_ext
                            {
                                pps_number = dataRow.Field<string>("pps_number"),
                                created_by = dataRow.Field<string>("created_by")
                            }).ToList();
                    #endregion
                }
            }
        }, token);
        return list;
    }
    catch (SqlException ex)
    {
        return list;
    } 
}

在Controller中调用:

public async Task<IList<print_pack_list_ext>> GetPackByDate(DateTime datefrom, DateTime dateto, CancellationToken token)
{
    return await _printPackListDAO.GetPackByDate(datefrom, dateto, token);
}

在View中实现:

using System.Threading.Tasks;
using Solution.BusinessLayer;
using Solution.ExtendedEntity;

namespace Solution.Forms
{
    public partial class FormLabelExportLog : Form
    {
        #region property
        CancellationTokenSource tokenSource;        
        CancellationToken token;       
        LogController _logController = new LogController();
        #endregion
        
        #region event_button
        //Code behind 'Generate Button'
        private async void myBtnGenReport_Click(object sender, EventArgs e)
        {
            setProgressBarStyle_start();
            myLabelInfo.Text = "Loading...";
            string selectedItem = this.myListBox1.SelectedItem.ToString();

            tokenSource = new CancellationTokenSource();
            token = tokenSource.Token;

            DataTable taskGetData = await Task.Factory.StartNew(() => loadData(selectedItem, token), token);

            if (token.IsCancellationRequested)
                MessageBox.Show("Cancelled Successfully!");
            else
                processData(taskGetData);
            tokenSource.Dispose();
            
            this.myLabelInfo.Text = "";
        }
        
        //Code behind 'Cancel' Button
        private void myBtnCancelReport_Click(object sender, EventArgs e)
        {
            tokenSource.Cancel();
            setProgressBarStyle_end();
        }
        #endregion
        
        #region method_data
        private DataTable loadData(string selectedItem, CancellationToken token)
        {
            #region initialization
            DataTable dt = new DataTable();
            DataRow dr;
            ...
            #endregion
            #region _PackListLog
            if (selectedItem == _PackListLog)
            {
                IList<print_pack_list_ext> real = new List<print_pack_list_ext>();
                real = _logController.GetPackByDate(datefrom, dateto, token).Result;

                if (!token.IsCancellationRequested)
                {
                    ...
                }
            }
            #endregion
            return dt
        }
        
        private void processData(DataTable dt)
        {
            if (dt != null)
            {
                ExportToExcel(dt);
            }
        }
        #endregion
    }       
}

就这样愉快地完成了。虽然是个小功能,牵扯到的很多知识点都没有学过,翻阅了很多资料,虽然耗时较长,但也是实习期间收获最多,最有意义的时间。


P.S.
所有O'RELLY的书在SafariOnline都有,邮箱注册无需信用卡绑定免费使用10天!!真是良心~~

P.P.S
整理一下上面没有提到的学习资料:

就用StackOverflow上的关于delegate,event,callback的经典回答结束这一篇啦,下期再见~~

I just met you
And this is crazy
But here's my number (delegate)
So if something happens (event)
Call me (callback)


Idea Matters

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • 赠言 03年8月 金榜题名沐春风 最是人生得意时 欲知前途风光好 登上广寒折桂枝
    天行健君马甲阅读 157评论 0 4
  • 终于 阳光洒下 抚摸着我 如丝绸般的触感 我微笑着 这苍白的脸庞 在我们的 那个寒冷的世界 —— 世界里 你踏着飞...
    三月烟霞阅读 218评论 0 0
  • 【天天棒棒】20171117学习力七期践行D31 阅读《小猪唏哩呼噜》20分钟。今天又换了一本是唏哩呼噜和猪八戒。...
    gxl水月亮阅读 100评论 0 0
  • 独自一个人吃饭就喜欢琢磨别人,尤其是在这种不太好找的小酒馆里。 我来的时候1层有个摄影师在拍摄食物,那些刺身拼盘被...
    辛默默阅读 406评论 2 3