多线程之旅(Task 任务)
一、task(任务)和threadpool(线程池)不同
1、线程(thread)是创建并发工具的底层类,但是在前几篇文章中我们介绍了thread的特点,和实例。可以很明显发现局限性(返回值不好获取(必须在一个作用域中)),当我们线程执行完之后不能很好的进行下一次任务的执行,需要多次销毁和创建,所以不是很容易使用在多并发的情况下。
2、线程池(threadpool) queueuserworkitem是很容易发起并发任务,也解决了上面我们的需要多次创建、销毁的性能损耗解决了,但是我们就是太简单的,我不知道线程什么时候结束,也没有获取返回值的途径,也是比较尴尬的事情。
3、任务(task)表示一个通过或不通过线程实现的并发操作,任务是可组合的,使用延续(continuation)可将它们串联在一起,它们可以使用线程池减少启动延迟,可使用回调方法避免多个线程同时等待i/o密集操作。
二、初识task(任务)
1、task(任务)是在.net 4.0引入的、task是在我们线程池threadpool上面进行进一步的优化,所以task默认还是线程池线程,并且是后台线程,当我们的主线程结束时其他线程也会结束
2、task创建任务,也和之前差不多
/// <summary> /// task 的使用 /// task 的创建还是差不多的 /// </summary> public static void show() { //实例方式 task task = new task(() => { console.writeline("无返回参数的委托"); }); //无参有返回值 task<string> task1 = new task<string>(() => { return "我是返回值"; }); //有参有返回值 task<string> task2 = new task<string>(x => { return "返回值 -- " + x.tostring(); }, "我是输入参数"); //开启线程 task2.start(); //获取返回值 result会堵塞线程获取返回值 console.writeline(task2.result); //使用线程工厂创建 无参数无返回值线程 task.factory.startnew(() => { console.writeline("这个是线程工厂创建"); }).start(); //使用线程工厂创建 有参数有返回值线程 task.factory.startnew(x => { return "返回值 -- " + x.tostring(); ; }, "我是参数"); //直接静态方法运行 task.run(() => { console.writeline("无返回参数的委托"); }); }
说明:
1、事实上task.factory类型本身就是taskfactory(任务工厂),而task.run(在.net4.5引入,4.0版本调用的是后者)是task.factory.startnew的简写法,是后者的重载版本,更灵活简单些。
2、调用静态run方法会自动创建task对象并立即调用start
3、task.run等方式启动任务并没有调用start,因为它创建的是“热”任务,相反“冷”任务的创建是通过task构造函数。
三、task(任务进阶)
1、wait 等待task线程完成才会执行后续动作
//创建一个线程使用wait堵塞线程 task.run(() => { console.writeline("wait 等待task线程完成才会执行后续动作"); }).wait();
2、waitall 等待task[] 线程数组全部执行成功之后才会执行后续动作
//创建一个装载线程的容器 list<task> list = new list<task>(); for (int i = 0; i < 10; i++) { list.add(task.run(() => { console.writeline("waitall 执行"); })); } task.waitall(list.toarray()); console.writeline("wait执行完毕");
3、waitany 等待task[] 线程数组任一执行成功之后就会执行后续动作
//创建一个装载线程的容器 list<task> list = new list<task>(); for (int i = 0; i < 10; i++) { list.add(task.run(() => { console.writeline("waitany 执行"); })); } task.waitany(list.toarray()); console.writeline("waitany 执行完毕");
4、whenall 等待task[] 线程数组全部执行成功之后才会执行后续动作、与waitall不同的是他有回调函数continuewith
//创建一个装载线程的容器 list<task> list = new list<task>(); for (int i = 0; i < 10; i++) { list.add(task.run(() => { console.writeline("whenall 执行"); })); } task.whenall(list.toarray()).continuewith(x => { return x.asyncstate; }); console.writeline("whenall 执行完毕");
5、whenany 等待task[] 线程数组任一执行成功之后就会执行后续动作、与waitany不同的是他有回调函数continuewith
//创建一个装载线程的容器 list<task> list = new list<task>(); for (int i = 0; i < 10; i++) { list.add(task.run(() => { console.writeline("whenany 执行"); })); } task.whenany(list.toarray()).continuewith(x => { return x.asyncstate; }); console.writeline("whenany 执行完毕"); console.readline();
四、parallel 并发控制
1、是在task的基础上做了封装 4.5,使用起来比较简单,如果我们执行100个任务,只能用到10个线程我们就可以使用parallel并发控制
public static void show5() { //第一种方法是 parallel.invoke(() => { console.writeline("我是线程一号"); }, () => { console.writeline("我是线程二号"); }, () => { console.writeline("我是线程三号"); }); //for 方式创建多线程 parallel.for(0, 5, x => { console.writeline("这个看名字就知道是for了哈哈 i=" + x); }); //foreach 方式创建多线程 parallel.foreach(new string[] { "0", "1", "2", "3", "4" }, x => console.writeline("这个看名字就知道是foreach了哈哈 i=" + x)); //这个我们包一层,就不会卡主界面了 task.run(() => { //创建线程选项 paralleloptions paralleloptions = new paralleloptions() { maxdegreeofparallelism = 3 }; //创建一个并发线程 parallel.for(0, 5, paralleloptions, x => { console.writeline("限制执行的次数"); }); }).wait(); console.writeline("**************************************"); //break stop 都不推荐用 paralleloptions paralleloptions = new paralleloptions(); paralleloptions.maxdegreeofparallelism = 3; parallel.for(0, 40, paralleloptions, (i, state) => { if (i == 20) { console.writeline("线程break,parallel结束"); state.break();//结束parallel //return;//必须带上 } if (i == 2) { console.writeline("线程stop,当前任务结束"); state.stop();//当前这次结束 //return;//必须带上 } console.writeline("我是线程i=" + i); }); }
五、多线程实例
1、代码异常我信息大家都不陌生,比如我刚刚写代码经常会报 =>对象未定义null 的真的是让我心痛了一地,那我们的多线程中怎么去处理代码异常呢? 和我们经常写的同步方法不一样,同步方法遇到错误会直接抛出,当是如果我们的多线程中出现代码异常,那么这个异常会自动传递调用wait 或者 task<tresult> 的result属性上面。任务的异常会将自动捕获并且抛给调用者,为了确保报告所有的异常,clr会将异常封装到aggregateexcepiton容器中,这容器是公开了innerexceptions属性中包含所有捕获的异常,但是如果我们的线程没有等待结束不会获取到异常。
class program { static void main(string[] args) { try { task.run(() => { throw new exception("错误"); }).wait(); } catch (aggregateexception axe) { foreach (var item in axe.innerexceptions) { console.writeline(item.message); } } console.readkey(); } }
/// <summary> /// 多线程捕获异常 /// 多线程会将我们的异常吞了,因为我们的线程执行会直接执行完代码,不会去等待你捕获到我的异常。 /// 我们的线程中最好是不要出现异常,自己处理好。 /// </summary> public static void show() { //创建一个多线程工厂 taskfactory taskfactory = new taskfactory(); //创建一个多线程容器 list<task> tasks = new list<task>(); //创建委托 action action = () => { try { string str = "sad"; int num = int.parse(str); } catch (aggregateexception ax) { console.writeline("我是aggregateexception 我抓到了异常啦 ax:" + ax); } catch (exception) { console.writeline("我是线程我已经报错了"); } }; //这个是我们经常需要做的捕获异常 try { //创建10个多线程 for (int i = 0; i < 10; i++) { tasks.add(taskfactory.startnew(action)); } task.waitall(tasks.toarray()); } catch (exception ex) { console.writeline("异常啦"); } console.writeline("我已经执行完了"); }
2、多线程取消机制,我们的task在外部无法进行暂停 thread().abort() 无法很好控制,上上篇中thread我们也讲到了thread().abort() 的不足之处。有问题就有解决方案。如果我们使用一个全局的变量控制,就需要不断的监控我们的变量取消线程。那么说当然有对应的方法啦。cancellationtokensource (取消标记源)我们可以创建一个取消标记源,我们在创建线程的时候传入我们取消标记源token。cancel()方法 取消线程,iscancellationrequested 返回一个bool值,判断是不是取消了线程了。
/// <summary> /// 多线程取消机制 我们的task在外部无法进行暂停 thread().abort() 无法很好控制,我们的线程。 /// 如果我们使用一个全局的变量控制,就需要不断的监控我们的变量取消线程。 /// 我们可以创建一个取消标记源,我们在创建线程的时候传入我们取消标记源token /// cancel() 取消线程,iscancellationrequested 返回一个bool值,判断是不是取消了线程了 /// </summary> public static void show1() { //创建一个取消标记源 cancellationtokensource cancellationtokensource = new cancellationtokensource(); //创建一个多线程工厂 taskfactory taskfactory = new taskfactory(); //创建一个多线程容器 list<task> tasks = new list<task>(); //创建委托 action<object> action = x => { try { //每个线程我等待2秒钟,不然 thread.sleep(2000); //判断是不是取消线程了 if (cancellationtokensource.iscancellationrequested) { console.writeline("放弃执行后面线程"); return; } if (convert.touint32(x) == 20) { throw new exception(string.format("{0} 执行失败", x)); } console.writeline("我是正常的我在执行"); } catch (aggregateexception ax) { console.writeline("我是aggregateexception 我抓到了异常啦 ax:" + ax); } catch (exception ex) { //异常出现取消后面执行的所有线程 cancellationtokensource.cancel(); console.writeline("我是线程我已经报错了"); } }; //这个是我们经常需要做的捕获异常 try { //创建10个多线程 for (int i = 0; i < 50; i++) { int k = i; tasks.add(taskfactory.startnew(action, k, cancellationtokensource.token)); } task.waitall(tasks.toarray()); } catch (exception ex) { console.writeline("异常啦"); } console.writeline("我已经执行完了"); }
3、多线程创建临时变量,当我们启动线程之后他们执行没有先后快慢之分,正常的循环中的变量也没有作用。这个时候就要创建一个临时变量存储信息,解决不访问一个数据源。
/// <summary> /// 线程临时变量 /// </summary> public static void show2() { //创建一个线程工厂 taskfactory taskfactory = new taskfactory(); cancellationtokensource cancellationtokensource = new cancellationtokensource(); //创建一个委托 action<object> action = x => { console.writeline("传入参数 x:" + x); }; for (int i = 0; i < 20; i++) { //这最主要的就是会创建20个k的临时变量 int k = i; taskfactory.startnew(action, k); } console.readline(); }
4、多线程锁,之前我们有提到过我们的多线程可以同时公共资源,如果我们有个变量需要加一,但是和这个时候我们有10个线程同时操作这个会怎么样呢?
public static list<int> list = new list<int>(); public static int count = 0; public static void show3() { //创建线程容器 list<task> tasks = new list<task>(); for (int i = 0; i < 10000; i++) { //添加线程 tasks.add(task.run(() => { list.add(i); count++; })); } task.waitall(tasks.toarray()); console.writeline("list 行数:" + list.count + " count 总数:" + count); console.readline(); }
我们上面的代码本来是count++到10000,但是我们看到结果的时候,我们是不是傻了呀,怎么是不是说好的10000呢,其实的数据让狗吃了?真的是小朋友有很多问号??????
5、那么我们要怎么去解决这个问题呢?方法还是有的今天我们要将到一个语法糖lock、它能做什么呢?它相当于一个代码块锁,它主要锁的是一个对象,当它锁住对象的时候会当其他线程发生堵塞,因为当它锁住代码时候也是锁住了对象的访问链,是其他的线程不能访问。必须等待对象访问链被释放之后才能被一个线程访问。我们的使用lock锁代码块的时候,尽量减少锁入代码块范围,因为我们锁代码之后会导致只有一个线程可以拿到数据,尽量只要必须使用lock的地方使用。
6、lock使用要注意的地方
1、lock只能锁引用类型的对象.
2、不能锁空对象null某一对象可以指向null,但null是不需要被释放的。(请参考:)。
3、lock 尽量不要去锁string 类型虽然它是引用类型,但是string是享元模式,字符串类型被clr“暂留”
这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
4、lock就避免锁定public 类型或不受程序控制的对象。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。
/// <summary> /// 创建一个静态对象,主要是用于锁代码块,如果是静态的就会全局锁,如果要锁实例类,就不使用静态就好了 /// </summary> private readonly static object obj = new object(); public static list<int> list = new list<int>(); public static int count = 0; /// <summary> /// lock 多线程锁 /// 当我们的线程访问同一个全局变量、同时访问同一个局部变量、同一个文件夹,就会出现线程不安全 /// 我们的使用lock锁代码块的时候,尽量减少锁入代码块范围,因为我们锁代码之后会导致只有一个线程可以 /// 访问到我们代码块了 /// </summary> public static void show3() { //创建线程容器 list<task> tasks = new list<task>(); //锁代码 for (int i = 0; i < 10000; i++) { //添加线程 tasks.add(task.run(() => { //锁代码 lock (obj) { //这个里面就只会出现一个线程访问,资源。 list.add(i); count++; } //lock 是一个语法糖,就是下面的代码 monitor.enter(obj); monitor.exit(obj); })); } task.waitall(tasks.toarray()); console.writeline("list 行数:" + list.count + " count 总数:" + count); console.readline(); }
7、总结实例篇,双色球实例。
1、双色球:投注号码由6个红色球号码和1个蓝色球号码组成。红色球号码从01--33中选择(不重复)蓝色球号码从01--16中选择(可以跟红球重复),代码我已经实现了大家可以下载源码。只有自己多多倒腾才能让自己的技术成长。 下一次我们async和await这两个关键字下篇记录