一、线程对象Thread的创建
我们可以通过System.Thread类来创建线程对象,通常使用的构造函数有如下两种:
public Thread(ThreadStart start); //线程开始后执行的方法为无参方法
public Thread(ParameterizedThreadStart start); //线程开始后执行的方法为有参方法
关于该类有一个属性需要特殊说明:IsBackground
它指示当前线程为前台还是后台线程。对于应用程序而言,必须等待所有的前台线程执行完毕,才可以退出;而后台线程则不会阻止进程的退出。
此外,关于Thread最多可以创建多少个的问题,它受系统及栈空间的限制。如:
32位系统,单个线程占用1M空间,一个进程最多有2G空间,那么理论上可以创建2048个线程;
64位系统则不受限制,关于这一点我也做了尝试,图证如下:
(可以看出已经超过2048个线程了,不过在大约创建了2700个线程的时候,程序就自然崩了。电脑表示伤不起,伤不起=。=!)
关于Thread的其他一些属性,如获取当前线程Id、Name、State等,比较简单就不做赘述,可查阅官方文档
MSDN_Thread类
二、线程池ThreadPool的使用
当我们在程序中需要频繁创建线程来执行任务时,每次都new一个新的Thread对象则显得很不爽,且对资源的消耗也是很大的。由此一来,我们可以使用线程池来协助我们创建和管理线程。
特别说明:线程池中创建的线程都是后台线程。
线程池就是存放Thread的大池子,我们需要用到多线程时,只要把过程封装成一个方法丢进去就好了,系统会自动分配当前处于空闲状态的线程来执行该方法,如果没有空闲线程,则会在队列中等待。这一动作,通过ThreadPool.QueueUserWorkItem() 方法来实现。该方法的简单示例如下:
public static void MainProcess()
{
Console.WriteLine($"主线程ID:{Thread.CurrentThread.ManagedThreadId}");
ThreadPool.QueueUserWorkItem(new WaitCallback(Task), 233);
Console.ReadKey();
}
public static void Task(object obj)
{
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId},传入参数为:{(obj == null? "null": obj.ToString())}");
}
运行结果如下:
下面来说一说线程池对线程的管控方法。不过在此之前,先要了解一个概念。
我们常用的线程分为两种:一种是工作线程(WorkThread),也称辅助线程,用来执行一些计算性的任务,也是最常用的;另一种是I/O线程(CompletionPortThreads),专门用于执行异步I/O操作,如文件读写和网络通讯。这一节主要以工作线程为例进行各项说明。
线程池有三个参数是我们需要了解的,使用得当可以极大提高我们的资源利用率和执行效率。
1)最小空闲线程数:
我想这个参数结合实例演示能说得更清楚:
static void Main(string[] args)
{
ThreadPool.SetMinThreads(8, 8);
for (int i = 1; i <= 20; i++)
{
ThreadPool.QueueUserWorkItem(Auto, i);
}
}
static void Auto(object id)
{
// 为了输出行数按顺序执行,所以加了个锁
lock (m_LockObject)
{
Console.WriteLine($"线程{id.ToString().PadLeft(2, '0')}开启".AppendTime());
}
Thread.Sleep(3000);
}
执行结果:
由上图可以看出,前8个线程几乎同时执行了方法,也就是说它们是提前预留好的空闲线程,只要有任务分配便会立即响应。设置这个参数要注意,不得小于CPU内核数,也就是说至少给每个CPU分配一个空闲线程,以防止有人光吃饭不干活 = =
2)最大线程数
这一数值决定了可以创建的线程数量上限,很好理解,Pass!
3)当前可用线程数
它=最大线程数-当前活跃线程数,于是我们也很好得出当前活跃线程数。通过判断活跃线程数是否为0,可以得知线程池中所有的线程任务是否执行结束。
三、基于任务(Task)的异步编程
无论是Thread还是ThreadPool,都无法同时满足广大码友既想精准控制每个线程的起承转合,又不想操心线程管理的这种高(tōu)贵(lǎn)需求,于是出现了Task这种堪称ThreadPool的无敌强化版本。
下面来介绍Task的几个强大之处:
1、使用Task的任务类型,可以创建如同Thread的独立线程
在下面的示例当中,通过设置任务类型TaskCreationOptions为LongRunning,实现了创建一个不依赖于线程池的独立线程。
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(8, 8);
Task.Factory.StartNew((o) => { Tasks(o); }, 1, TaskCreationOptions.LongRunning);
Task.Factory.StartNew((o) => { Tasks(o); }, 2, TaskCreationOptions.PreferFairness);
System.Threading.Thread.Sleep(500);
GetActiveThreads();
Console.ReadKey();
}
public static void Tasks(object obj)
{
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId},传入参数为:{(obj == null ? "null" : obj.ToString())}");
Thread.Sleep(10000);
}
public static void GetActiveThreads()
{
int work, io;
int maxwork, maxio;
ThreadPool.GetAvailableThreads(out work, out io);
ThreadPool.GetMaxThreads(out maxwork, out maxio);
Console.WriteLine($"当前活跃工作线程数:{maxwork - work}");
}
输出结果:
可以看出尽管用线程工厂创建了两个线程任务,但只有一个活跃线程在线程池中。
2、Task的令牌机制
关于Task中的令牌机制,我想用一个故事来说明会比较清楚。
故事中的主角,我们姑且称他为小王吧。这天,小王接到老板的一个任务,这个任务可能比较棘手,要花费较长的时间才能完成。于是老板让小王专心做,不要考虑其他任何事情。大概快到下班时间了,小王还没有回来,老板有点急,就指派小刘去告诉小王,不管做没做完,你都可以回来了。但是为了小王能相信小刘的话,就给了他一个令牌,小王看到这个令牌,就需要立即停止手里的工作,回来向老板汇报。
下面我们来分析一下,故事中的小王,是被老板(MainThread)分配了一个任务(Task)。
这个任务比较耗时,没有办法立即返回结果,但是老板想提前知道结果(Result)。于是指派小刘为钦差(CancellationTokenSource),并给了他一块令牌(CancellationToken),去告诉小王,别干了(Cancel),赶紧回去报告。在这个事件中,我们可以推断,小王的汇报结果应该有以下几种情况:
1)干完了,自己结束的
2)没干完,老板你不让干的
3)碰壁了,干不下去了
我想通过以上事例,应该就能大致明白令牌在Task使用过程中的作用了。OK,接下来就用实际代码来演示上述三种情况:
①小王自己干完的,回去报告
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<int> task = new Task<int>(() => Tasks(cts.Token), cts.Token);
Console.WriteLine("小王开始任务");
task.Start();
Console.WriteLine("小王汇报结果为:" + task.Result);
Console.WriteLine("任务最终状态为:IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
Console.ReadKey();
}
// 为了让小王有东西可以汇报,因此使用有返回值的回调函数
private static int Tasks(CancellationToken ct)
{
int result = 0;
Thread.Sleep(1000);
result++;
return result;
}
以上结果反映,任务顺利结束,IsCompleted为True,IsCanceled和IsFaulted均为False。
②小王没做完,老板不让做了
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<int> task = new Task<int>(() => Tasks(cts.Token), cts.Token);
Console.WriteLine("小王开始任务");
task.Start();
Thread.Sleep(500);
Console.WriteLine("按下回车让小王结束任务");
Console.ReadKey();
cts.Cancel();
try
{
Console.WriteLine("小王汇报结果为:" + task.Result);
}
catch (AggregateException ex)
{
if (ex.InnerException is OperationCanceledException)
{
Console.WriteLine("老板取消了小王的任务!");
}
else
{
Console.WriteLine("小王无法完成当前任务!");
}
}
Console.WriteLine("任务最终状态为:IsCanceled={0} IsCompleted={1} IsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
Console.ReadKey();
}
private static int Tasks(CancellationToken ct)
{
int result = 0;
while (true)
{
ct.ThrowIfCancellationRequested();
Thread.Sleep(1000);
result++;
}
return result;
}
从任务的最终状态可以看出,由于是老板主动请求取消小王的任务,所以IsCanceled会标记为True,这就是系统对此中止类型的特殊处理。
在这段代码中,用到了一个方法是ThrowIfCancellationRequested,它的作用是当令牌所有者发出取消任务请求时,抛出异常OperationCanceledException,当试图获取Result时,也会引发相同异常。
我们还可以用以下写法:
// 当判断取消请求为True时,才执行ThrowIfCancellationRequested方法
if (ct.IsCancellationRequested)
{
ct.ThrowIfCancellationRequested();
}
③小王自己遇到了问题,觉得任务做不下去了,需要中止
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<int> task = new Task<int>(() => Tasks(cts.Token), cts.Token);
Console.WriteLine("小王开始任务");
task.Start();
Thread.Sleep(500);
Console.WriteLine("等待小王汇报任务");
try
{
Console.WriteLine("小王汇报结果为:" + task.Result);
}
catch (AggregateException ex)
{
if (ex.InnerException is OperationCanceledException)
{
Console.WriteLine("老板取消了小王的任务!");
}
else
{
Console.WriteLine("小王无法完成当前任务!");
}
}
Console.WriteLine("任务最终状态为:IsCanceled={0} IsCompleted={1} IsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
Console.ReadKey();
}
private static int Tasks(CancellationToken ct)
{
int result = 0;
Thread.Sleep(1000);
throw new Exception();
return result;
}
可以看出任务执行过程抛出了其他错误,导致任务中止,此时IsFaulted标记为True。
*这里有一个问题,假如收到了取消任务请求,但是依然是以抛出异常形式中止了任务,会怎么样呢?将代码修改如下:
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<int> task = new Task<int>(() => Tasks(cts.Token), cts.Token);
Console.WriteLine("小王开始任务");
task.Start();
Thread.Sleep(500);
Console.WriteLine("等待小王汇报任务");
Console.ReadKey();
cts.Cancel();
try
{
Console.WriteLine("小王汇报结果为:" + task.Result);
}
catch (AggregateException ex)
{
if (ex.InnerException is OperationCanceledException)
{
Console.WriteLine("老板取消了小王的任务!");
}
else
{
Console.WriteLine("小王无法完成当前任务!");
}
}
catch { Console.WriteLine("err"); }
Console.WriteLine("任务最终状态为:IsCanceled={0} IsCompleted={1} IsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
Console.ReadKey();
}
private static int Tasks(CancellationToken ct)
{
int result = 0;
while (true)
{
if (ct.IsCancellationRequested)
{
throw new OperationCanceledException();
}
Thread.Sleep(1000);
}
return result;
}
这里可以看出,任务是主动申请取消的,但是由于小王没有使用ThrowIfCancellationRequested方法抛出异常,而是选择throw的方式抛出异常,任务最终结果被判为IsFaulted。所以说,要想以IsCanceled方式结束任务,必须经过ThrowIfCancellationRequested方法。
3、Task的异步等待机制
在.net Framework 4.5以后,微软提供了async+await语法糖,目的是希望能够在执行某一耗时较长的任务时,仍能做些其他的事情。这一机制运用在UI交互时则显得非常好用。
这里也创建一个窗体项目来作为演示:
1)窗体界面如下
2)核心后台代码如下
private async void button1_Click(object sender, EventArgs e)
{
textBox1.AppendText("Button1按下了\r\n");
textBox1.AppendText("执行一个耗时任务\r\n");
await Task.Run(Tasks);
textBox1.AppendText("耗时任务执行完毕\r\n");
}
private void Tasks()
{
Thread.Sleep(3000);
}
private void button2_Click(object sender, EventArgs e)
{
textBox1.AppendText("Button2按下了\r\n");
}
下面执行操作如下:
①点击button1按钮
②3秒内点击button2按钮(保证在Tasks任务执行结束前)
结果显示,button1的操作并不会阻塞UI线程,button2可以正常操作。这就是async+await语法糖的强大魅力。
四、并行任务(Parallel)
Parallel的出现,是为了更好地支持并行循环任务的创建和执行。说白了,就是为了更省事。
假如我们用Task来循环创建任务,可能是下面这样:
for (int i = 0; i < 10; i++)
{
Task.Factory.StartNew((o) => { Console.WriteLine($"当前任务id:{o}"); }, i);
}
而用了Parallel,能更加简洁:
Parallel.For(0, 10, (i) => Console.WriteLine($"当前任务id:{i}"));
如果需要把数据源作为迭代变量,则可以这样:
List<int> source = new List<int> { 2, 4, 6, 8, 10 };
Parallel.ForEach(source, (i) => Console.WriteLine($"当前任务id:{i}"));
当然,Parallel的强大之处还不止于此,配合Linq语法,能够使得数据的查询更加高效,并且代码更加简洁,可读性更高。
下面我们来对Linq单线程查询和多线程查询做一个对比:
static void Main(string[] args)
{
List<int> source = new List<int> { 1, 2, 3, 4, 5 };
Stopwatch sw = new Stopwatch();
sw.Start();
Console.WriteLine("开始单线程查询");
var src = source.Where(s =>
{
Thread.Sleep(2000);
return true;
}).ToList();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");
sw.Restart();
Console.WriteLine("开始多线程查询");
src = source.AsParallel().Where(s =>
{
Thread.Sleep(2000);
return true;
}).ToList();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");
Console.ReadKey();
}
通过模拟每条查询需要2s的耗时,可以看出并行查询提供了有效的多线程执行方式,使得总查询时间不再完全受查询条数的影响。
需要注意的是,当执行条数过少且单条执行速度很快时,不建议使用并行查询方式,因为线程调度所消耗的时间会远远超过执行时间
如下对比实验可以很好得说明这个问题:
实验设计是从1W条Guid字符串集合中匹配包含"ac"的部分。
先是不做任何处理的结果:
static void Main(string[] args)
{
List<string> guidList = new List<string>();
for (int i = 0; i < 10000; i++)
{
guidList.Add(Guid.NewGuid().ToString());
}
Stopwatch sw = new Stopwatch();
sw.Start();
Console.WriteLine("Normal");
var src1 = guidList.Where(s =>
{
//Thread.Sleep(1);
return s.Contains("ac");
}).ToList();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");
sw.Restart();
Console.WriteLine("Parallel");
var src2 = guidList.AsParallel().Where(s =>
{
//Thread.Sleep(1);
return s.Contains("ac");
}).ToList();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");
Console.ReadKey();
}
可以看到并行执行结果远超过正常执行所消耗的时间。接着将Thread.Sleep(1)代码取消注释,模拟一个耗时操作,执行结果如下:
这时Parallel的优势就体现出来了+_+
上一篇:.NET 多线程开发总结(一)——并行、并发、异步、同步的概念区分
下一篇:.NET 多线程开发总结(三)——线程间的信号传递(线程交互)