隐藏

C#多线程学习笔记 Task启动方式、Task阻塞、Task.Delay()、多线程异常处理、任务取消、多线程的临时变量、共享数据的lock、Task返回值、定时器

发布:2021/3/6 16:00:43作者:管理员 来源:本站 浏览次数:1181

1.进程、线程、多线程、计算机概念

【进程】:一个运行程序       【线程】:依托于进程,一个进程可以包含多个线程

【多线程】:多个执行流同时运行,主要分为两种:其一CPU运算太快啦,分时间片——上下文切换(加载环境—计算—保存环境)  微观角度讲,一个核同一时刻只能执行一个线程;宏观的讲是多线程并发。其二,多CPU多核,可以独立工作,4核8线程——核指物理的核,线程指虚拟核

多线程三大特点:不卡主线程、速度快、无序性     【Thread】:C#语言对线程对象的封装

2.同步与异步      【同步】:完成计算之后,再进入下一行,会阻塞     【异步】:不会等待方法的完成,直接进入下一行,不会阻塞

3.不同C#版本线程的进化升级

C#1.0 Thread 线程等待、回调、前后台线程,可以扩展Thread封装回调,各种API函数很多,可设置线程优先级,各种API包括:Start(),Suspend(),Resume(),Abort(),Thread.ResetAbort(),Join(),
C#2.0 ThreadPool 线程池使用,设置线程池,等待可用ManualResetEvent,砍掉了很多API,可以设置线程池的最大最小数量
C#3.0 Task  

4.并发编程

并发编程包括【多线程】、【并行处理】、【异步编程】、【响应式编程】

5.Task启动

5.1为什么要有task

Task  = Thread + ThreadPool

Thread:容易造成时间+空间开销,而且使用不当,容易造成线程过多,导致时间片切换...

ThreadPool:控制能力比较弱,做thread的延续、阻塞、取消、超市等功能较弱,ThreadPool的控制权在CLR而不在编程者...

Task看起来更像是一个Thread...但是在ThreadPool的基础上进行的封装

.net4.0之后微软极力推荐使用task进行异步运算

引用using System.Threading  C#3.0才有、基于ThreadPool

WinDbg: .loadby sos clr       !threads

复制代码
        private void DoSomethingLong(string name)
        {
            Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1000000000; i++)
            {
                lResult += i;
            }
            Console.WriteLine($"****************DoSomethingLong {name}   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
        }
复制代码
1 Task.Run(() => this.DoSomethingLong("btnTask_Click1")); 2 Task.Run(() => this.DoSomethingLong("btnTask_Click2"));
TaskFactory taskFactory = Task.Factory;//4.0 taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click3"));
new Task(() => this.DoSomethingLong("btnTask_Click4")).Start();

以上三种启动方式并无区别,都是返回一个Task

Run(Action)不具有参数,没有返回值

复制代码
            Task.Run(() => { string name = "无名方法";
                Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1000000000; i++)
                {
                    lResult += i;
                }
                Console.WriteLine($"****************DoSomethingLong {name}   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
            });
复制代码
复制代码
            Console.WriteLine($"这是Task1单击事件Start       当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
            Task.Run(()=> {
                Console.WriteLine($"这是Tsak.Run(Action)无名方法Start      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
                Random random = new Random();
                Thread.Sleep(random.Next(1000,5000));
                Console.WriteLine($"这是Tsak.Run(Action)无名方法End      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
            });
            Console.WriteLine($"这是Task1单击事件End      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
复制代码


 6.多线程阻塞-----Task.WaitAll

设置一场景,睡觉之后要吃饭

6.1不阻塞多线程,就会出现还没睡醒就吃饭的情况

            List<Task> listTask = new List<Task>();
            listTask.Add(Task.Run(()=> SleepMethod("杨三少")));
            listTask.Add(Task.Run(() => SleepMethod("牛大帅")));
            listTask.Add(Task.Run(() => SleepMethod("猪宝宝")));
            Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】    时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); 


    6.2阻塞多线程,使用Task.WaitAll(Task[]),会阻塞当前线程,等着全部任务都完成后,才进入下一行

            List<Task> listTask = new List<Task>();
            listTask.Add(Task.Run(()=> SleepMethod("杨三少")));
            listTask.Add(Task.Run(() => SleepMethod("牛大帅")));
            listTask.Add(Task.Run(() => SleepMethod("猪宝宝")));
            Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】    时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");


6.2.1使用Task.WaitAll如果想不阻塞线程,可以使用Task套用Task

复制代码
            Task.Run(() => {//Task套Task,可以不阻塞 List<Task> listTask = new List<Task>();
                listTask.Add(Task.Run(() => DoSomethingLong("杨三少")));
                listTask.Add(Task.Run(() => DoSomethingLong("牛大帅")));
                listTask.Add(Task.Run(() => DoSomethingLong("猪宝宝")));
                Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 Console.WriteLine($"全部都醒了     当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】  时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
            });
复制代码

   6.3设置最多等待时长,设置最长等待时间,可以看到,杨三少还未睡醒就开吃,谁让杨三少睡的太久了呢,哈哈

复制代码
            List<Task> listTask = new List<Task>();
            listTask.Add(Task.Run(()=> SleepMethod("杨三少")));
            listTask.Add(Task.Run(() => SleepMethod("牛大帅")));
            listTask.Add(Task.Run(() => SleepMethod("猪宝宝"))); //Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 Task.WaitAll(listTask.ToArray(),2500);//最多等待2.5秒,超过2.5秒就不再进行等待 Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】    时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
复制代码


   6.4等待汇总


 7.多线程阻塞-----Task.WaitAny

复制代码
            List<Task> listTask = new List<Task>();
            listTask.Add(Task.Run(()=> SleepMethod("杨三少")));
            listTask.Add(Task.Run(() => SleepMethod("牛大帅")));
            listTask.Add(Task.Run(() => SleepMethod("猪宝宝"))); //Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 //Task.WaitAll(listTask.ToArray(),2500);//最多等待2.5秒,超过2.5秒就不再进行等待 Task.WaitAny(listTask.ToArray());//阻塞线程,只要只要有一个宝宝睡醒就开吃,“早起的宝宝有饭吃” Console.WriteLine($"宝宝们,睡眠结束,开始进餐啦      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】    时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
复制代码

“这次是杨三少醒的最早,所以他就可以先吃辣”


 8.多线程不阻塞----Task.WhenAll(Task[]).ContinueWith()     Task.WhenAny(Task[]).ContinueWith() 

复制代码
            List<Task> listTask = new List<Task>();
            listTask.Add(Task.Run(()=> DoSomethingLong("杨三少")));
            listTask.Add(Task.Run(() => DoSomethingLong("牛大帅")));
            listTask.Add(Task.Run(() => DoSomethingLong("猪宝宝"))); //Task.WaitAll(listTask.ToArray());//【阻塞在此!!!】 //Task.WaitAll(listTask.ToArray(),2500);//最多等待2.5秒,超过2.5秒就不再进行等待 //Task.WaitAny(listTask.ToArray());//阻塞线程,只要只要有一个宝宝睡醒就开吃,“早起的宝宝有饭吃” Task.WhenAny(listTask.ToArray()).ContinueWith(t=> { 
                Console.WriteLine($"第一个已经醒了     当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】    时间:{DateTime.Now.ToString("HH:mm:ss.fff")}"); 
            });
            Task.WhenAll(listTask.ToArray()).ContinueWith(t=> {
                Console.WriteLine($"全部都醒了     当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】  时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
            });
复制代码

   ContinueWith类似于一个回调式的,ContinueWith里面的t就是前面的Task,也就是使用ContinueWith相当于一个回调,当执行了这个Task之后会自动执行ContinueWith里的后续方法。

9.仅用11个线程完成10000个任务

复制代码
                List<int> list = new List<int>(); for (int i = 0; i < 10000; i++)
                {
                    list.Add(i);
                } //完成10000个任务  但是只要11个线程  Action<int> action = i => {
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                    Thread.Sleep(new Random(i).Next(100, 300));
                };
                List<Task> taskList = new List<Task>(); foreach (var i in list)
                { int k = i;
                    taskList.Add(Task.Run(() => action.Invoke(k))); if (taskList.Count > 10)
                    {
                        Task.WaitAny(taskList.ToArray());//只要有一个完成,重新整理集合 taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
                    }
                }
                Task.WhenAll(taskList.ToArray());
复制代码

 10.使用TaskFactory可以知道已完成线程的标识

复制代码
                TaskFactory taskFactory = new TaskFactory();
                List<Task> taskList = new List<Task>();
                taskList.Add(taskFactory.StartNew(o => DoSomethingLong("一一一"), "一一一"));
                taskList.Add(taskFactory.StartNew(o => DoSomethingLong("二二二"), "二二二"));
                taskList.Add(taskFactory.StartNew(o => DoSomethingLong("三三三"), "三三三"));
                taskFactory.ContinueWhenAny(taskList.ToArray(), t => {
                     Console.WriteLine($"【{t.AsyncState}】任务已完成  当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                 });
                taskFactory.ContinueWhenAll(taskList.ToArray(),t=> {
                    Console.WriteLine($"所有任务已完成,第一个是{t[0].AsyncState}   当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                });
复制代码


   如果不用TaskFactory,直接用Task,可以通过做个子类的方式来搞。

11.Task中两种常见的枚举

11.1 Task生成时的枚举  public Task(Action action, TaskCreationOptions creationOptions);

11.1.1 AttachedToParent  :建立了父子关系,父任务想要继续执行,必须等待子任务执行完毕


复制代码
/* task是父task,task1和task2是子task
 * 如果不加AttachedToParent,三个线程的执行没有先后顺序
 * 如果加AttachedToParent,必须等到task1和task2执行完毕后再执行task */ Task task = new Task(() => {
    Task task1 = new Task(() => {
        Thread.Sleep(100);
        Debug.WriteLine("task1");
    }, TaskCreationOptions.AttachedToParent);

    Task task2 = new Task(() => {
        Thread.Sleep(10);
        Debug.WriteLine("task2");
    }, TaskCreationOptions.AttachedToParent);

    task1.Start();
    task2.Start();
});

task.Start();
task.Wait(); //task.WaitAll(task1,task2);  Debug.WriteLine("我是主线程!!!!");
复制代码

11.1.2 DenyChildAttach: 不让子任务附加到父任务上去,即使子任务加了AttachedToParent ,父任务如果加上DenyChildAttach的话也无济于事,还是各自执行各自的,没有先后顺序


复制代码
/* task是父任务,task1和task2是子任务
 * 如果不加AttachedToParent,三个线程的执行没有先后顺序
 * 如果加AttachedToParent,必须等到task1和task2执行完毕后再执行task
 * 如果父任务加了DenyChildAttach,还是各自执行各自的 */ Task task = new Task(() => {
    Task task1 = new Task(() => {
        Thread.Sleep(100);
        Debug.WriteLine("task1");
    }, TaskCreationOptions.AttachedToParent);

    Task task2 = new Task(() => {
        Thread.Sleep(10);
        Debug.WriteLine("task2");
    }, TaskCreationOptions.AttachedToParent);

    task1.Start();
    task2.Start();
},TaskCreationOptions.DenyChildAttach);

task.Start();
task.Wait(); //task.WaitAll(task1,task2);  Debug.WriteLine("我是主线程!!!!");
复制代码

11.1.3 HideScheduler: 子任务默认不使用父类的Task的Scheduler,而是使用默认的

11.1.4 LongRunning:如果你明知道是长时间运行的任务,建议你使用此选项,建议使用 “Thread” 而不是“threadPool(租赁公司)"

如果长期租用不还给threadPool,threadPool为了满足市场需求,会新开一些线程,满足当前使用,如果此时租用线程被归还,这会导致ThreadPool的线程过多,销毁和调度都是一个很大的麻烦


复制代码
        if ((task.Options & TaskCreationOptions.LongRunning) != 0)
        {
            Thread thread = new Thread(s_longRunningThreadWork);
            thread.IsBackground = true;
            thread.Start(task);
        } else { bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) != TaskCreationOptions.None;
            ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal);
        }
复制代码

11.1.5 PreferFairness: 给你的感觉就是一个”queue队列“的感觉,如果指定PreferFairness会将Task放入到ThreadPool的全局队列中,让work thread进行争抢,默认情况会放到task的一个本地队列中

11.2 任务延续中使用的枚举  public Task ContinueWith(Action<Task> continuationAction, TaskContinuationOptions continuationOptions);  用在ContinuWith

11.2.1 不使用取消和任务延续枚举,任务依次执行,分别是task1,task2,task3


复制代码
Task task1 = new Task(() => {
    Thread.Sleep(1000);
    Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
}); var task2 = task1.ContinueWith(t => {
    Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
}); var task3 = task2.ContinueWith(t => {
    Debug.WriteLine("task3 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
});

task1.Start(); /* 输出
task1 tid=10, dt=02/26/2021 10:11:09
task2 tid=11, dt=02/26/2021 10:11:09
task3 tid=10, dt=02/26/2021 10:11:09 */
复制代码

11.2.2 添加取消,取消task2,未指定任务延续枚举

 添加取消,不加枚举的任务延续

11.2.3 TaskContinuationOptions.LazyCancellation,需要等待task1执行完成之后再判断source.token的状态


复制代码
CancellationTokenSource source = new CancellationTokenSource(); //定义一个任务取消对象 source.Cancel(); //执行任务取消  Task task1 = new Task(() => {
    Thread.Sleep(1000);
    Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
}); var task2 = task1.ContinueWith(t => {
    Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
},source.Token,TaskContinuationOptions.LazyCancellation,TaskScheduler.Current); //即使task1被cancel,延续链依旧没断,只是task2不会执行,先执行task1,再执行task3 var task3 = task2.ContinueWith(t => {
    Thread.Sleep(100);
    Debug.WriteLine("task3 tid={0}, dt={1}  task2状态:{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"),task2.Status);
});

task1.Start(); /* 输出
task1 tid=10, dt=10:36:12.363
task3 tid=11, dt=10:36:12.468  task2状态:Canceled */ /* 说明
需要等待task1执行完成之后再判断source.token的状态
这样的话,就形成了一条链: task1 -> task2 -> task3... */
复制代码

11.2.4 TaskContinuationOptions.ExecuteSynchronously,这个枚举就是希望执行前面那个task的thread也在执行本延续任务,可以防止线程切换


复制代码
Task task1 = new Task(() => {
    Thread.Sleep(1000);
    Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
}); var task2 = task1.ContinueWith(t => {
    Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
},TaskContinuationOptions.ExecuteSynchronously); //task2用task1的线程去执行  task1.Start(); /* 输出
 task1 tid=6, dt=10:47:25.189
 task2 tid=6, dt=10:47:25.192 */ /* 说明
 * task2 也希望使用 task1的线程去执行,这样可以防止线程切换。。。 */
复制代码

11.2.5 TaskContinuationOptions.OnlyOnRanToCompletion 表示延续任务必须在前面task完成状态才能执行


复制代码
Task task1 = new Task(() => {
    Thread.Sleep(1000);
    Debug.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff")); throw new Exception("抛出异常");
}); var task2 = task1.ContinueWith(t => {
    Debug.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("HH:mm:ss.fff"));
},TaskContinuationOptions.OnlyOnRanToCompletion); //仅在task1正常完成时执行task2,如果task1异常了,task2就不会运行  task1.Start();
复制代码

11.2.6 TaskContinuationOptions.NotOnRanToCompletion 表示延续任务必须在前面task非完成状态才能执行,其余类似

NotOnRanToCompletion 延续任务必须在前面task【非完成状态】才能执行
NotOnFaulted 延续任务必须在前面task【非报错状态】才能执行
OnlyOnCanceled 延续任务必须在前面task【已取消状态】才能执行
NotOnCanceled 延续任务必须在前面task【没有取消状态】才能执行
OnlyOnFaulted 延续任务必须在前面task【报错状态】才能执行
OnlyOnRanToCompletion 延续任务必须在前面task【已完成状态】才能执行

12.Task有返回值 

12.1 Task<TResult>  继承于Task


复制代码
Task<int> task1 = Task.Factory.StartNew(() => { //... Thread.Sleep(1000); return 1;
  });

Debug.WriteLine(task1.Result);
复制代码

12.2 ContinueWith<TResult>


复制代码
Task<int> task1 = Task.Factory.StartNew(() => { //做一些逻辑运算 return 1;
}); var task2 = task1.ContinueWith<string>(t => { int num = t.Result; var sum = num + 10; return sum.ToString();
});

Debug.WriteLine(task2.Result);
复制代码

12.3 WhenAll<TResult>  WhenAny<TResult>


复制代码
Task<int> task1 = Task.Factory.StartNew(() => { //做一些逻辑运算 return 1;
});

Task<int> task2 = Task.Factory.StartNew(() => { //做一些逻辑运算 return 2;
});

Task.WhenAll<int>(new Task<int>[2] { task1, task2 }).ContinueWith<int>(t => { int sum = 0; foreach (var item in t.Result)
   {
       sum += item;
   }
   Debug.WriteLine(sum); return sum;
}); /* 输出  3
 * 同时用到了Continue<TResult>和WhenAll<TResult> */
复制代码

11.小结

Task.WaitAll(Task[]) 阻塞,等待Task[]数组里的全部线程任务都完成后方可运行到下一步
Task.WaitAny(Task[]) 阻塞,等待Task[]数组里的某一线程任务完成,方可运行到下一步
Task.WhenAll(Task[]).Continue 不阻塞,等待Task[]数组里的全部线程任务都完成后执行回调
Task.WhenAny(Task[]).Continue 不阻塞,等待Task[]数组里的某一线程任务完成后执行回调

12.Thread.Sleep(2000)与Task.Delay(2000)

Thread.Sleep(2000);//等待2s    Task.Delay(2000);//延迟2s

12.1Thread.Sleep(2000)

复制代码
                Console.WriteLine($"这是Task1单击事件Start       当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                Thread.Sleep(2000);//等待  stopwatch.Stop();
                Console.WriteLine(stopwatch.ElapsedMilliseconds);
                Console.WriteLine($"这是Task1单击事件End      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
复制代码


   12.2Task.Delay(2000)直接替换Thread.Sleep(2000)

复制代码
                Console.WriteLine($"这是Task1单击事件Start       当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                Task.Delay(2000);//延迟  stopwatch.Stop();
                Console.WriteLine(stopwatch.ElapsedMilliseconds);
                Console.WriteLine($"这是Task1单击事件End      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
复制代码


   12.3Task.Delay(2000)的正确用法

复制代码
                Console.WriteLine($"这是Task1单击事件Start       当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                Task.Delay(2000).ContinueWith(t => {//Task.Delay()的返回值仍然是Task,  Task.ContinueWith()  stopwatch.Stop();
                    Console.WriteLine(stopwatch.ElapsedMilliseconds);
                });
                Console.WriteLine($"这是Task1单击事件End      当前线程:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】 时间:{DateTime.Now.ToString("HH:mm:ss.fff")}");
复制代码


 13.多线程中的异常处理

13.1 主线程的异常捕捉不到子线程的异常信息。所以新开辟的子线程要写属于自己的try...catch...

子线程如果不阻塞,主线程是捕捉不到。但最最好还是在子线程里书写自己的异常捕捉机制。

复制代码
            Console.WriteLine($"这是主线程单击事件Start  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            List<Task> listTask = new List<Task>(); try { for (int i = 0; i < 5; i++)
                {
                    Action<string> action = t => { try {
                              Thread.Sleep(100); if (t.Equals("for2"))
                              {
                                  Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  执行异常"); throw new Exception($"这是{t},刨出异常");
                              } if (t.Equals("for3"))
                              {
                                  Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  执行异常"); throw new Exception($"这是{t},刨出异常");
                              }
                              Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  执行成功");
                          } catch (Exception ex)
                          {
                              Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}     异常信息:{ex.Message}");
                          }
                      }; string k = $"for{i}";
                    listTask.Add(Task.Run(() => action.Invoke(k)));
                }
                Task.WaitAll(listTask.ToArray());
            } catch (Exception ex)
            {
                Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}     异常信息:{ex.Message}");
            }
            Console.WriteLine($"这是主线程单击事件End  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
复制代码


13.2 

  14.线程取消

14.1 应用场景是多个线程并发执行,某个失败后,希望通知别的线程,都停下来;Task是外部无法中止的,Thread.Abort不要考虑因为不靠谱(原因是线程是OS的资源,无法掌控啥时候取消),在这里需要线程自己停止自己,需要用到公共的访问变量,所有的线程都会不断的去访问它,当某个线程执行异常时修改此公共变量(当然这中间有一定的延迟)。

CancellationTokenSource变量去标志任务是否取消,Cancel()方法用于取消,IsCancellationRequested属性作为每个线程执行的条件;其Token在启动Task的时候传入,如果某一时刻执行Cancel()方法,这个任务会自动放弃,并抛出一个异常。

复制代码
            Console.WriteLine($"这是主线程单击事件Start  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            List<Task> listTask = new List<Task>();
            CancellationTokenSource cts = new CancellationTokenSource(); try { for (int i = 0; i < 5; i++)
                {
                    Action<string> action = t => { try {
                              Thread.Sleep(100); if (t.Equals("for2"))
                              {
                                  Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  执行失败"); throw new Exception($"这是{t},刨出异常");
                              } if (t.Equals("for3"))
                              {
                                  Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  执行失败"); throw new Exception($"这是{t},刨出异常");
                              } if (cts.IsCancellationRequested)
                              {
                                  Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  放弃执行");
                              } else {
                                  Console.WriteLine($"这是{t},当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  执行成功");
                              }
                          } catch (Exception ex)
                          {
                              cts.Cancel();
                              Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}     异常信息:{ex.Message}");
                          }
                      }; string k = $"for{i}";
                    listTask.Add(Task.Run(() => action.Invoke(k),cts.Token));
                }
                Task.WaitAll(listTask.ToArray());
            } catch (Exception ex)
            {
                Console.WriteLine($"当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}  当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}     异常信息:{ex.Message}");
            }
            Console.WriteLine($"这是主线程单击事件End  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
复制代码


14.2 Thread中的取消


复制代码
var isStop = false; //定义一个公有变量 var thread = new Thread(() => { while (!isStop)
    {
        Thread.Sleep(100);
        Debug.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
    }
});
thread.Start();

Thread.Sleep(1000);
isStop = true; //多个线程操作公有变量,不用锁的话,存在Bug风险
复制代码

14.3 Task中用CancellationTokenSource取消任务

14.3.1 取消时做一些事情,希望有一个函数能够被触发,这个触发可以做一些资源的清理,又或者是更新数据库信息


复制代码
CancellationTokenSource cts = new CancellationTokenSource();

cts.Token.Register(() => { //如果当前的token被取消,此函数将会被执行 Debug.WriteLine("当前source已经被取消,现在可以做资源清理了。。。。");
}); var task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested)
      {
          Thread.Sleep(100);
          Debug.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
      }
  }, cts.Token);

Thread.Sleep(1000);
cts.Cancel();
复制代码

14.3.2 延时取消,可以用CancelAfter,也可以在构造函数中指定延时时长


复制代码
CancellationTokenSource cts = new CancellationTokenSource(); var task = Task.Factory.StartNew(() => { while (!cts.IsCancellationRequested)
      {
          Thread.Sleep(100);
          Debug.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
      }
  }, cts.Token);

cts.CancelAfter(new TimeSpan(0, 0, 0, 1)); //延时1秒取消
复制代码

14.3.3 取消组合,是and的逻辑关系


复制代码
CancellationTokenSource source1 = new CancellationTokenSource(); //现在要让source1取消 source1.Cancel();

CancellationTokenSource source2 = new CancellationTokenSource(); var combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);

Debug.WriteLine("s1={0}  s2={1}  s3={2}", source1.IsCancellationRequested,
                                         source2.IsCancellationRequested,
                                         combineSource.IsCancellationRequested); /* 输出
 s1=True  s2=False  s3=True */ /* 说明
 * s3 = s1 && s2 */
复制代码

14.3.4 ThrowIfCancellationRequested


复制代码
ThrowIfCancellationRequested 比 IsCancellationRequested 多了throw。。。

如果一个任务被取消,我希望代码抛出一个异常。。。 if(IsCancellationRequested) throw new Exception("adasdaf"); == 等价操作 == throwIfCancellationRequested();
复制代码

  15.多线程中的临时变量

15.1直接引用for循环中的i

复制代码
            Console.WriteLine($"这是主线程单击事件Start  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); for (int i = 0; i < 5; i++)
            {
                Task.Run(() => {
                    Console.WriteLine($"这是for循环,i={i}");
                });
            }
            Console.WriteLine($"这是主线程单击事件End  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
复制代码


   为什么都等于5,线程并没有阻塞,所以5次循环下来之后i就是5啦。

15.2直接引用循环外的局部变量

复制代码
            Console.WriteLine($"这是主线程单击事件Start  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); int k = 0; for (int i = 0; i < 5; i++)
            {
                k = i; new Action(() => {
                    Console.WriteLine($"这是for循环,i={k}");
                }).BeginInvoke(null, null);
            }
            Console.WriteLine($"这是主线程单击事件End  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
复制代码


    为什么都等于4,线程没有阻塞,其实最主要的原因是声明了一次k,全程只有一个k。

15.3在循环内每次声明赋值

复制代码
            Console.WriteLine($"这是主线程单击事件Start  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); for (int i = 0; i < 5; i++)
            { int k = i; new Action(() => {
                    Console.WriteLine($"这是for循环,i={k}");
                }).BeginInvoke(null, null);
            }
            Console.WriteLine($"这是主线程单击事件End  当前线程{Thread.CurrentThread.ManagedThreadId.ToString("00")}   当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
复制代码


  16.线程安全

共有变量指:都能访问的局部变量、全局变量、数据库中的一个值、磁盘文件等,如果多个线程都去操作一个共有变量,可能会出现意外的结果,这就是多线程并不安全

16.1多线程并不安全

复制代码
            List<Task> listTask = new List<Task>(); int sum = 0; for (int i = 0; i < 10000; i++)
            {
                listTask.Add(Task.Run(() => {
                    sum += 1;
                }));
            }
            Task.WhenAll(listTask.ToArray()).ContinueWith((t) => {
                Console.WriteLine($"最终结果:{sum}");
            });
复制代码

可见结果并不是意料之中的10000.

16.2加lock

复制代码
        //private:定义为私有,防止外界也去lock    static:全场唯一   readonly:不要随意赋值  object表示引用类型 private static readonly object objLock = new object(); private void button1_Click(object sender, EventArgs e)
        {
            List<Task> listTask = new List<Task>(); int sum = 0; for (int i = 0; i < 10000; i++)
            {
                listTask.Add(Task.Run(() => { lock (objLock)
                    { //lock后的方法块,任意时刻只有一个线程可以进入 //只能锁引用类型,相当于占用这个引用链接 //string虽然也是引用类型,但不能用string,因为什么享元模式 sum += 1;
                    }
                }));
            }
            Task.WhenAll(listTask.ToArray()).ContinueWith((t) => {
                Console.WriteLine($"最终结果:{sum}");
            });
        }
复制代码

可见结果是意料之中的10000

16.3有关lock的深思

lock虽然能解决线程安全的问题,同一时刻只能有一个线程可以运行lock后的程序块不并发,但是牺牲了性能,所以有两个建议:

其一、尽可能得缩小lock的范围;其二、尽量不要定义共有变量,可以通过数据拆分来避免冲突

 17.await/async

await与async通常成对出现,async放在方法名前(private后面),await放在task对象前。如果只在方法名前加一个async,没有任何意义;如果只在方法内task前面加一个await,会报错。在await task后面的语句,类似于一个回调,方法运行到await task时会自动返回返回主线程继续运行,要等待子线程运行结束后才会继续运行这个回调,并且这个回调的线程是不确定的,可能是主线程,可能是子线程,也可能是其他线程。

不用用void作为async方法的返回类型,如果没有返回值,也要Task,如果有返回值,要返回Task<T>;仅限于编写事件处理程序需要返回void

   17.1类似于用一种同步的方法写异步。举例说在主线程中运行了一个task,并且希望在这个子线程task执行之后再运行另一个事件task1,那么就需要task.ContinueWith(task1),在这里task.ContinueWith(task1)就等于await task;task1,其实说白了就是ContinueWith=await。

复制代码
        /* 使用swait/async可以把异步当成同步来使用
         * 这个示例演示一个没有返回值的多线程任务,模拟人一天的动作
         * 其执行结果的先后顺序是【按钮单击开始】【NoReturn方法Start】【开始吃饭】【吃饭结束】【开始工作】【工作结束】【工作结束】【开始睡觉】【NoReturn方法End】【按钮单击结束】
         * 这个示例可能并无实际的意义,但只是用来模拟这种await/async的使用方法
         * 不过请注意使用async的方法虽说没有返回值(并没有具体的return task),但其结果返回的是Task */ private async void button1_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"ClickEventStart     【按钮单击开始】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); await NoReturn_AwaitAsync();
            Console.WriteLine($"ClickEventEnd       【按钮单击结束】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        } private async static Task NoReturn_AwaitAsync()
        {
            Console.WriteLine($"【NoReturn方法Start】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}"); await Task.Run(()=> {
                Console.WriteLine($"NoReturn方法await之前   【开始吃饭】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                Thread.Sleep(500);
                Console.WriteLine($"NoReturn方法await之后   【吃饭结束】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            }); await Task.Run(() => {
                Console.WriteLine($"NoReturn方法await之前   【开始工作】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                Thread.Sleep(500);
                Console.WriteLine($"NoReturn方法await之后   【工作结束】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            });
            Console.WriteLine($"NoReturn方法await之后   【开始睡觉】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            Console.WriteLine($"【NoReturn方法End】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }
复制代码


   同样的方法从await/async换成Task.ContinueWith来一步步回调则显得代码很冗余

复制代码
        private static void NoReturn_Task_ContinueWith()
        {
            Console.WriteLine($"【NoReturn方法Start】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            Task.Run(() => {
                Console.WriteLine($"NoReturn方法await之前   【开始吃饭】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                Thread.Sleep(500);
                Console.WriteLine($"NoReturn方法await之后   【吃饭结束】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            }).ContinueWith(t=> {
                Task.Run(() => {
                    Console.WriteLine($"NoReturn方法await之前   【开始工作】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                    Thread.Sleep(500);
                    Console.WriteLine($"NoReturn方法await之后   【工作结束】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                }).ContinueWith(t1=> {
                    Console.WriteLine($"NoReturn方法await之后   【开始睡觉】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                    Console.WriteLine($"【NoReturn方法End】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                });
            });
        }
复制代码

17.2 task有返回值的场景

复制代码
        private void button1_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"ClickEventStart     【按钮单击开始】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            Task<int> task = Task.Run(() => DoSomething("杨三少")); int result = task.Result;//这一步子线程会阻塞主线程,因为主线程需要调用子线程的执行结果 Console.WriteLine($"子线程执行结果为{result}");
            Console.WriteLine($"ClickEventEnd       【按钮单击结束】    当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        } private int DoSomething(string name)
        {
            Console.WriteLine($"Task【{name}】 线程ID:{Thread.CurrentThread.ManagedThreadId} 是否在线程池中:{Thread.CurrentThread.IsThreadPoolThread}"); return 22;
        }
复制代码


18.定时器

18.1 ThreadPool的定时器


复制代码
Debug.WriteLine("main, datetime={0}", DateTime.Now);
ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(false), new WaitOrTimerCallback((obj, b) => {//为false第一次不会马上执行,为true第一次会马上执行 //做逻辑判断,判断是否在否以时刻执行。。。 Debug.WriteLine("obj={0},tid={1}, datetime={2}", obj, Thread.CurrentThread.ManagedThreadId,
                                                             DateTime.Now);
}), "hello world", 1000, false);
复制代码

18.2 Timer

System.Threading(优先使用这个Timer,最底层的,其他的都是对它的封装)    System.Timers    System.Windows.Forms    System.Web.UI