c# 多线程的四种打开方式
c# 发展到今天,多线程的写法也经历了几代演变。你不需要全都精通,但至少要知道它们是什么、什么时候用。
方式一:thread — 最原始的一把锤子
thread 是 .net 最底层的线程类,给你最大的控制权,也意味着你要自己管好一切。
thread thread = new thread(dowork); thread.isbackground = true; // 设为后台线程,程序退出时自动带走 thread.start(parameter);
适合什么场景?长时间运行的独立任务,比如一个持续监听硬件数据的采集循环。
缺点也很明显:线程创建和销毁开销大,数量一多系统调度压力就上来了。
方式二:threadpool — 线程池,省着点用
既然创建线程贵,那我提前建好一批、用完放回去行不行?这就是线程池的思路。
threadpool.queueuserworkitem(state =>
{
// 你的后台任务
});适合短而频繁的小任务。但在上位机开发里用得不算多——因为我们的任务往往是"长驻"的。
方式三:task — 现代 c# 的标配
task 是 .net 4.0 推出的 tpl(任务并行库)的核心,也是目前最主流的写法。
task.run(() =>
{
// 后台干活
return result;
}).continuewith(t =>
{
// 干完了通知 ui
}, taskscheduler.fromcurrentsynchronizationcontext());它比 thread 轻量,比 threadpool 灵活。绝大多数上位机场景,用 task 就对了。
方式四:async/await — 最优雅的写法
如果说 task 是标配,那 async/await 就是"顶配"。它让异步代码写起来跟同步代码一样顺。
private async void btnstart_click(object sender, eventargs e)
{
btnstart.enabled = false;
try
{
// 后台去读数据,ui 线程不阻塞
var data = await task.run(() => readdatafromdevice());
// await 之后自动回到 ui 线程,直接更新控件
txtresult.text = data;
}
finally
{
btnstart.enabled = true;
}
}注意看,这里没有 invoke,没有跨线程异常——await 帮你把后续代码自动封送回了 ui 线程。
一句话总结:能写 async/await 就别写别的,代码干净、bug 少。
跨线程更新 ui,这道坎必须过
做上位机,第一个会撞上的墙就是——"线程间操作无效: 从不是创建控件的线程访问它"。
这条规则你得刻在脑子里:只有创建控件的线程,才能修改控件。
winforms 怎么搞
最朴素的写法,用 invokerequired 判断一下:
private void updatetextbox(string text)
{
if (txtlog.invokerequired)
{
txtlog.invoke(new action<string>(updatetextbox), text);
return;
}
txtlog.appendtext(text + environment.newline);
}每个控件都写一遍太麻烦?封装个扩展方法一劳永逸:
public static class controlextensions
{
public static void invokeifrequired(this control c, action action)
{
if (c.invokerequired)
c.invoke(action);
else
action();
}
}
// 用起来就一行
txtlog.invokeifrequired(() => txtlog.appendtext("收到数据啦"));wpf 怎么搞
wpf 用 dispatcher,思路是一样的:
application.current.dispatcher.invoke(() =>
{
txtresult.text = "数据已更新";
});如果用了 mvvm 模式,那就更省心了——inotifypropertychanged 的绑定机制会自动处理线程切换,你在后台线程改 viewmodel 属性就行。
三个经典场景,拿来就能用
理论说了一堆,不来点实际的怎么行。下面这三个场景,做上位机的大概率会遇到。
场景一:串口数据持续采集
最常见的需求:打开串口,不停读数据,实时显示到界面上。
private cancellationtokensource _cts;
private task _readtask;
private void btnstart_click(object sender, eventargs e)
{
_cts = new cancellationtokensource();
_readtask = task.run(() => readloop(_cts.token));
btnstart.enabled = false;
btnstop.enabled = true;
}
private void readloop(cancellationtoken token)
{
while (!token.iscancellationrequested)
{
try
{
string data = serialport.readline();
txtlog.invokeifrequired(() =>
{
txtlog.appendtext($"[{datetime.now:hh:mm:ss}] {data}\r\n");
});
}
catch (exception ex)
{
// 异常处理,别让线程崩了
}
}
}
private void btnstop_click(object sender, eventargs e)
{
_cts?.cancel();
btnstart.enabled = true;
btnstop.enabled = false;
}注意两个细节:
- • 用
cancellationtoken来停止,永远不要用thread.abort(),那是暴力手段,容易把资源搞坏 - • 循环里一定要
try-catch,不然后台线程抛个异常,程序直接就没了
场景二:多设备并行读取
手上有五台设备要轮询,一台一台读太慢?并行走起:
private async task<list<devicedata>> readalldevicesasync(){
var tasks = new list<task<devicedata>>();
foreach (var device in _devices)
{
tasks.add(task.run(() => device.readdata()));
}
// 等所有设备都读完
var results = await task.whenall(tasks);
return results.tolist();
}task.whenall 会等所有任务都完成,然后一次性给你结果。配合 async/await,代码干净得不像话。
场景三:生产者-消费者模式
数据采集速度快、处理速度慢,或者 ui 更新太频繁会卡?用队列做个缓冲。
// 线程安全的队列
private blockingcollection<dataframe> _dataqueue = new blockingcollection<dataframe>();
// 生产者:采集线程,只管往队列里塞
void producerloop()
{
while (true)
{
var frame = readframefromhardware();
_dataqueue.add(frame);
}
}
// 消费者:处理线程,慢慢从队列里取
void consumerloop()
{
foreach (var frame in _dataqueue.getconsumingenumerable())
{
processanddisplay(frame);
}
}blockingcollection 是个好东西——队列空的时候消费者自动阻塞,不占 cpu;有数据来了自动唤醒。做数据缓冲、日志写入都很好用。
到此这篇关于c#上位机多线程详解的文章就介绍到这了,更多相关c#上位机多线程内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论