一、背景
最近用.Net WinForm做一个桌面应用程序,刚开始有一些问题,后面总结出做WinForm最常见的两个问题,就是多线程的使用和多线程中的控件操作。
其实挺简单,但是没做过的话也会耽误一点时间。
二、技术
1、使用多线程
为什么要开多线程?因为不开多线程,界面会出现卡死的情况,体验很不好。
先看一下线程要做的事情(for循环内部代码):
Action action = () => { while (true) { try { var stop = false; lock (lockObj_stop) { stop = IsStop; } if (stop) { OperationLog.AddLog(new OperationLogModel() { CreateTime = DateTime.Now, Remark = $"线程:{Thread.CurrentThread.ManagedThreadId},用户主动停止!线程任务结束!" }); break;//用户主动停止,本线程结束 } var otherSuccess = false; lock (lockObj_success) { otherSuccess = IsSuccess;//AddLog里有lock操作,lock嵌套容易死锁,所以找个临时中间变量 } if (otherSuccess) { OperationLog.AddLog(new OperationLogModel() { CreateTime = DateTime.Now, Remark = $"线程:{Thread.CurrentThread.ManagedThreadId},其他线程预约成功!线程任务结束!" }); break;//其他线程成功预约到,本线程结束 } var ret = requestOne(model);//请求网络,model是具体数据,for循环列表创建线程,每次model数据不同 if (ret) { lock (lockObj_success) { IsSuccess = true; } OperationLog.AddLog(new OperationLogModel() { CreateTime = DateTime.Now, Remark = $"线程:{Thread.CurrentThread.ManagedThreadId},预约成功!线程任务结束!" }); break;//结束 } } catch (Exception ex) { Log4netHelper.Error("出错!", ex); } } }
怎么开多线程?
方式1:线程池线程
(线程池把线程加入队列,不一定会立即执行,可能要排队)
var task = System.Threading.Tasks.Task.Factory.StartNew(action);
方式2:直接开新线程
(立即执行):
var thread = CommonFun.StartNewThread(action);
CommonFun.StartNewThread方法:
/// <summary> /// 开启新线程并返回线程对象 /// </summary> /// <param name="action"></param> /// <returns></returns> public static Thread StartNewThread(Action action) { Thread thread = new Thread(delegate () { action(); }); thread.IsBackground = true; thread.Start(); return thread; }
上面是两种开启多线程的方法,自己封装的方法StartNewThread跟系统提供的方法参数一样,可以方便切换,如果要响应快,电脑资源充足的话就用方式2,如果可以慢点、线程池满了允许慢慢排队执行则可以使用方式1。
2、多线程中操作控件问题
一般情况下,多线程中直接操作控件(比如:把一个按钮变成Enable状态、给textbox赋值),程序一运行就会报错,报错信息如下:
解决方法1:设置不检查跨线程调用属性
如果只是操作极少量的控件,这里有一个方法,在Form构造函数中,加入以下语句就不会报错,但要自己保证代码中不会有多个线程同时调用它,是一种非线程安全的操作,不推荐使用:
属性说明:
解决方法2:调用控件Invoke委托
调用控件的Invoke方法(同步)或BeginInvoke方法操作控件,这种方法是线程安全的,推荐使用
定义专用委托示例:
//建立个委托 private delegate void CanbookcountAppendTextDelegate(string strshow); /// <summary> /// RichTextBox追加文字,并自动滚动到最下方 /// </summary> /// <param name="texttoappend"></param> public void CanbookcountAppendText(string texttoappend) { if (this.txt_canbookcount.InvokeRequired) { this.txt_canbookcount.Invoke(new CanbookcountAppendTextDelegate(CanbookcountAppendText), texttoappend); } else { this.txt_canbookcount.AppendText(texttoappend); txt_canbookcount.SelectionStart = txt_canbookcount.Text.Length; txt_canbookcount.ScrollToCaret();//滑动到最下方 } }
或
使用匿名委托示例:
/// <summary> /// 查询按钮设置为有效或失效 /// </summary> /// <param name="enabled"></param> public void QuerycanbookcountBtnEnabled(bool enabled) { if (btn_querycanbookcount.InvokeRequired) { this.btn_querycanbookcount.Invoke(new MethodInvoker(delegate () { QuerycanbookcountBtnEnabled(enabled); })); } else { btn_querycanbookcount.Enabled = enabled; } }
上面这两个示例调用方法可以在主线程和其他线程中被调用,里面有判断 Control.InvokeRequired属性,看下面说明,
在窗口线程中,InvokeRequired值为false,会直接调用赋值语句:
btn_querycanbookcount.Enabled = enabled;
而在多线程中,InvokeRequired值为true,会调用控件的Invoke方法。
这里,比较难理解的东西重点在于QuerycanbookcountBtnEnabled内部的Invoke方法的委托还是方法本身 QuerycanbookcountBtnEnabled(enabled),不会造成死循环?
当然不会,Invoke方法的作用,就是把整个方法委托给窗口线程(主线程)去处理,调用这一步它自己不执行委托方法里面的逻辑,然而,委托方法交给窗口线程(主线程)后,窗口线程(主线程)执行时就会判断InvokeRequired值为false,执行:btn_querycanbookcount.Enabled = enabled,然后就完事了。
上面只是封装了一下方法,使得这个控件操作方法可以在所有线程内被调用。
当然了,如果明确知道自己在主线程以外的线程调用控件,也可以直接使用Invoke方法,不用那么多判断,例如:
btn.Invoke(new MethodInvoker(delegate () { btn.Enabled = true; }));
好了,这两个常见问题的处理方法就是以上内容了~~~
最后,贴一下做的程序界面:
题外话:之前开了200多个线程去请求网络,放在本地电脑执行,CPU只占2%~3%,放到单核的某云服务器上运行时cpu就到了99%,CPU资源也算是“充分利用“了,好在还没卡死,还可以关掉程序~笑哭~
评论列表: