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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

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