C# 多线程总结

1.Thread

1.1 Thread生命周期

1.2 Thread本地存储

本地存储槽

Local Store Slot(本地存储槽):存储的信息只对该线程有用,这叫做线程本地化存储

            //1.给所有线程分配一个(未命名)数据槽。 存放数据
            var slot = Thread.AllocateDataSlot();
            Thread.SetData(slot, "hello world");

            //2.给所有线程分配一个(aaa)数据槽。 存放数据
            var slotaaa = Thread.AllocateNamedDataSlot("aaa");
            Thread.SetData(slotaaa, "hello world aaa");

            var slotObj = Thread.GetData(slot);
            var slotObjaaa = Thread.GetData(slotaaa);

            Console.WriteLine(slotObj);
            Console.WriteLine(slotObjaaa);


            //消除进程与槽位
            Thread.FreeNamedDataSlot("aaa");

ThreadStatic

性能提升:

      [ThreadStatic]
        static string username = string.Empty;

        static void Main(string[] args)
        {
            username = "hello world!!!";

            var t = new Thread(() =>
            {
                Console.WriteLine("当前工作线程:{0}", username);
            });

            t.Start();

            Console.WriteLine("主线程:{0}", username);

            Console.Read();
        }

ThreadLocal

            ThreadLocal<string> local = new ThreadLocal<string>();

            local.Value = "hello world!!!";

            var t = new Thread(() =>
            {
                Console.WriteLine("当前工作线程:{0}", local.Value);
            });

            t.Start();

            Console.WriteLine("主线程:{0}", local.Value);

1.3 认识MemoryBarrier

2个线程同时操作一个变量时,这个变量可能加载到Cpu Cache中。这时,就使用到MemoryBarrier

例子:因为Release中做了一些代码和缓存的优化。。。 比如说将一些数据从memory中读取到cpu高速缓存中。

static void Main(string[] args)
        {
            var isStop = false;

            var t = new Thread(() =>
            {
                var isSuccess = false;

                while (!isStop)
                {
		    Thread.MemoryBarrier(); //及时从cpu cache中更新到 memor
                    isSuccess = !isSuccess;
                }
            });

            t.Start();

            Thread.Sleep(1000);
            isStop = true;
            t.Join();

            Console.WriteLine("主线程执行结束!");
            Console.ReadLine();
        }

在此方法之前的内存写入都要及时从cpu cache中更新到 memory。
在此方法之后的内存读取都要从memory中读取,而不是cpu cache。

类似的功能还有:

 var isStop = 0;

            var t = new Thread(() =>
            {
                var isSuccess = false;

                while (isStop == 0)
                {
                    Thread.VolatileRead(ref isStop);
                    isSuccess = !isSuccess;
                }
            });

1.4ThreadPool 线程池

使用线程池的线程,要调用静态方法 ThreadPool.QueueUserWorkItem ,以指定线程要调用的方法。该静态方法有两种:

public static bool QueueUserWorkItem(WaitCallback callBack);  

public static bool QueueUserWorkItem(WaitCallback callBack, object state); 

这两个方法用于向线程池队列添加一个工作项(work item)以及一个可选的状态数据。

工作项是指一个由 callback 参数标识的委托对象,被委托对象包装的回调方法由线程池来执行。

传入的回调方法匹配 System.Threading.WaitCallback 委托类型

        private void button2_Click(object sender, EventArgs e)
        {
            Console.WriteLine("主线程ID={0}", Thread.CurrentThread.ManagedThreadId);

            ThreadPool.QueueUserWorkItem(CallbackWorkItem);

            ThreadPool.QueueUserWorkItem(CallbackWorkItem, "work");

            Thread.Sleep(3000);

            Console.WriteLine("主线程退出");
			
	    //线程池线程ID = 5
            //主线程ID = 1
            //线程池开始执行
            //线程池开始执行
            //线程池线程ID = 6
            //线程池线程ID = 3 传入的参数为 work
            //主线程退出
        }

        private static void CallbackWorkItem(object state)
        {
            Console.WriteLine("线程池开始执行");

            if (state != null)
            {
                Console.WriteLine("线程池线程ID = {0} 传入的参数为 {1}", Thread.CurrentThread.ManagedThreadId, state.ToString());

            }

            else
            {
                Console.WriteLine("线程池线程ID ={0}", Thread.CurrentThread.ManagedThreadId);
            }
        }

工作者线程与I/O线程

CLR线程池分为工作者线程(workerThreads)与I/O线程 (completionPortThreads) 两种,

工作者线程是主要用作管理CLR内部对象的运作,I/O(Input/Output) 线程顾名思义是用于与外部系统交换信息.

I/O 线程是.NET专为访问外部资源所设置的一种线程,因为访问外部资源常常要受到外界因素的影响,为了防止让主线程受影响而长期处于阻塞状态,.NET为多个I/O操作都建立起了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每个异步方法的使用方式都非常类似,都是以BeginXXX为开始,以EndXXX结束

1.5 TheadPool定时任务

使用ThreadPool.RegisterWaitForSingleObject

最后一个false表示,循环执行,直到注销等待。

下面2个例子:
等间隔时间再执行AutoResetEvent(false)

            Console.WriteLine($"开始时间:{DateTime.Now}");
            //AutoResetEvent(true),一起执行
            //AutoResetEvent(false), 间隔时间后,再执行
            ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(false), new WaitOrTimerCallback((obj, Timeout) =>
            {
                Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId},参数obj:{obj},时间:{DateTime.Now}");
            }), "Hello", 3000, false);

            Console.Read();

立即执行AutoResetEvent(true)

            Console.WriteLine($"开始时间:{DateTime.Now}");
            //AutoResetEvent(true),一起执行
            //AutoResetEvent(false), 间隔时间后,再执行
            ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, Timeout) =>
            {
                Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId},参数obj:{obj},时间:{DateTime.Now}");
            }), "Hello", 3000, false);

            Console.Read();

发现一个问题,这里的线程ID会改变的哦!

Task

2.1.Task的开启

Task的异步

1.new Task

            var task = new Task(() =>
            {
                Console.WriteLine("工作线程");
            });
            task.Start();

2.Factory

            var task2 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("工作线程");
            });

3.Run

            var task3 = Task.Run(() =>
            {
                Console.WriteLine("工作线程");
            });

Task的同步

其他的没什么好说的,这里看看同步。

            var task = new Task(() =>
            {
                Console.WriteLine("工作线程");
            });
            task.RunSynchronously();
            Console.WriteLine("主线程");

2.2 Task等待和延续

等待

task.wait(): 等待操作,相当于thread.join()

Task.WaitAll():必须其中所有的task执行完成才算完成,相当于并且&&

Task.WaitAny():只要其中一个task执行完成就算完成,相当于或||

延续

(1)Task延续

ContinueWith方法与下面2个方法的组合,

Task.WhenAll():必须其中所有的task执行完成才算完成,相当于并且&&,再执行ContinueWith里面的方法。
Task.WhenAny():只要其中一个task执行完成就算完成,相当于或||,再执行ContinueWith里面的方法。

 Task.WhenAll(task1, task2).ContinueWith(t =>
            {
                //执行完1和2后,再执行这里的
                Console.WriteLine("我是延续线程{0}", DateTime.Now);
            });

(2)Task工厂延续,也可以采用这种方法。

ContinueWhenAll

ContinueWhenAny

Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
            {
               Console.WriteLine("我是工作线程3:{0}", DateTime.Now);
            });

2.3 枚举TaskCreationOptions的一些方法

1. AttachedToParent: 把子任务附加到父任务下

指定将任务附加到任务层次结构中的某个父级

相当于建立了父子关系。父任务想要继续执行,必须等待子任务执行完毕。
相当于是一个WaitAll的一个操作。

            Task task = new Task(() =>
            {
                Task task1 = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(2000);
                    Console.WriteLine("我是工作线程1:{0}", DateTime.Now);
                }, TaskCreationOptions.AttachedToParent);

                Task task2 = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(1000);
                   Console.WriteLine("我是工作线程2:{0}", DateTime.Now);
                }, TaskCreationOptions.AttachedToParent);
            });

            task.Start();

            task.Wait();  //task.WaitAll(task1,task2);

            Console.WriteLine("我是主线程");

            Console.ReadKey();
			

输出结果

我是工作线程2:2020/8/15 22:50 :53
我是工作线程1:2020/8/15 22:50:54
主线程


这样执行的话,结果就是

主线程
我是工作线程2:2020/8/15 22:50 :53
我是工作线程1:2020/8/15 22:50:54

2.DenyChildAttach: 不让子任务附加到父任务上去

输出结果与没有添加AttachedToParent一样

主线程
我是工作线程2:2020/8/15 22:50 :53
我是工作线程1:2020/8/15 22:50:54

3.LongRunning:长任务

指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。

相当于新开一个 Thread 任务执行,不在 ThreadPool 执行。如果长期租用不还给ThreadPool,ThreadPool会新开一些线程,这会导致ThreadPool的线程过多,销毁和调度都是一个很大的麻烦,对性能造成影响。

  • 如果运行I/O密集任务,则可以使用TaskCompletionSource异步函数,通过回调函数(延续)实现并发性,而不通过线程实现。

  • 如果是运行计算密集任务,则可以使用一个生产者/消费者队列,控制这些任务的并发数量,避免出现线程和进程阻塞的问题。

2.4 枚举TaskContinuationOptions

1.LazyCancellation 延续被取消时,先完成先前的任务再判断Source.Token的状态

使用 TaskContinuationOptions.LazyCancellation

TaskContinuationOptions.LazyCancellation 它的本质就是:
需要等待task1执行完成之后再判断source.token的状态

2.ExecuteSynchronously 强制使用同一个线程来同步

            Task task1 = new Task(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId,
                                                             DateTime.Now);
            });

            var task2 = task1.ContinueWith(t =>
            {
                Console.WriteLine("task2 tid={0}, dt={1} {2}", Thread.CurrentThread.ManagedThreadId,
                                                          DateTime.Now, task1.Status);
            }, TaskContinuationOptions.ExecuteSynchronously);


            var task3 = task2.ContinueWith(t =>
            {
                Console.WriteLine("task3 tid={0}, dt={1}  {2}", Thread.CurrentThread.ManagedThreadId,
                                                                 DateTime.Now, task2.Status);
            }, TaskContinuationOptions.ExecuteSynchronously);

            task1.Start();

            Console.ReadKey();

3.当前状态的几个判断 NotOnRanToCompletion,OnlyOnRanToCompletion

  • NotOnRanToCompletion:前面的任务非完成状态,才执行延续任务

  • OnlyOnRanToCompletion: 只有前面的任务完成状态,才执行延续任务

  • NotOnFaulted: 前面的任务非失败状态,才执行延续任务

  • OnlyOnFaulted:前面的任务失败状态,才执行延续任务

  • NotOnCanceled: 前面的任务非取消状态,才执行延续任务

  • OnlyOnCanceled: 前面的任务取消状态,才执行延续任务

2.5 取消专用 CancellationTokenSource 替代变量方式

1.共享变量方式(不推荐)

不能让多个线程操作一个共享变量,否则会在release版本中有潜在bug。

        static void Main(string[] args)
        {
            var isStop = false;

            var thread = new Thread(() =>
            {
                while (!isStop)
                {
                    Thread.Sleep(100);

                    Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
                }
            });

            thread.Start();

            Thread.Sleep(1000);

            isStop = true;

            Console.WriteLine("已经被取消了");

            Console.ReadKey();
        }

2.CancellationTokenSource方式 (推荐)

下面结果是一样的

            CancellationTokenSource source = new CancellationTokenSource();
            
            //注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。
            source.Token.Register(() =>
            {
                //如果当前的token被取消,此函数将会被执行
                Console.WriteLine("当前source已经被取消,现在可以做资源清理了。。。。");
            });

            var task = Task.Factory.StartNew(() =>
            {
                //未发送请求操作时,为false
                while (!source.IsCancellationRequested)
                {
                    Thread.Sleep(100);

                    Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
                }
            }, source.Token);

            Thread.Sleep(1000);
            source.Cancel();

            Console.ReadKey();

下面还可以针对CancellationTokenSource做功能增强。

2.1.当任务取消的时候,我希望有一个函数能够被触发

            CancellationTokenSource source = new CancellationTokenSource();

            //注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。
            source.Token.Register(() =>
            {
                //如果当前的token被取消,此函数将会被执行
                Console.WriteLine("当前source已经被取消,现在可以做资源清理了。。。。");
            });

            var task = Task.Factory.StartNew(() =>
            {
                //未发送请求操作时,为false
                while (!source.IsCancellationRequested)
                {
                    Thread.Sleep(100);

                    Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
                }
            }, source.Token);

            Thread.Sleep(1000);
            source.Cancel();

            Console.WriteLine("已经被取消了");

            Console.ReadKey();

2.2 延时取消

            CancellationTokenSource source = new CancellationTokenSource();

            //注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。
            source.Token.Register(() =>
            {
                //如果当前的token被取消,此函数将会被执行
                Console.WriteLine("当前source已经被取消,可以执行其他操作");
            });

            var task = Task.Factory.StartNew(() =>
            {
                //未发送请求操作时,为false
                while (!source.IsCancellationRequested)
                {
                    Thread.Sleep(100);

                    Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
                }
            }, source.Token);

            Thread.Sleep(1000);

            //source.Cancel();
            source.CancelAfter(1000);   //延时1秒取消

            Console.WriteLine("已经被取消了");

            Console.ReadKey();

2.3 组合取消

取消的组合 将CancellationTokenSource 组合成一个链表,其中任何一个CancellationTokenSource被取消,组合source也会被取消。

            CancellationTokenSource source1 = new CancellationTokenSource();

            //source1取消
            //source1.Cancel();

            CancellationTokenSource source2 = new CancellationTokenSource();

            //source2取消
            source2.Cancel();

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

            Console.WriteLine("source1={0}  source2={1}  combineSource={2}", source1.IsCancellationRequested,
                                                     source2.IsCancellationRequested,
                                                     combineSource.IsCancellationRequested);

            Console.Read();

输出:

source1=False
source2=True
combineSource=True

2.6 异常处理 AggregateException

AggregateException 是一个集合,因为task中可能会抛出多个异常,所以我们需要一种新的类型,把这些异常都追加到一个集合中

抛出的时间

与线程不同,Task可以随时抛出异常。
任务代码抛出一个未处理异常,那么这个异常会自动传递到调用Wait()的任务上或者访问Task<TResult>Result属性的代码上:

            Task task = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("task执行");
                Task task1 = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("task1执行");
                    throw new Exception("task1异常");
                }, TaskCreationOptions.AttachedToParent);

                Task task2 = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("task2执行");
                    throw new Exception("task2异常");
                }, TaskCreationOptions.AttachedToParent);
            });

            try
            {
                //Task.WhenAll(new Task[2] { task1, task2 });
                task.Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine($"meassage:{item.InnerException.Message},type:{item.GetType()}");
                }

            }

            Console.Read();

输出

task执行
task1执行
task2执行
meassage: task2异常,type : System.AggregateException
meassage: task1异常,type : System.AggregateException

CLR会将异常封装在AggregateException中,从而更适合并行编程场景;

使用Task的IsFaultedIsCanceled属性,就可以不重新抛出异常而检测出错的任务。
如果都返回false,则没有出错;
IsCanceled为true,任务抛出 OperationCanceledOPeration;
IsFaulted为true,则任务抛出另一种异常,而Exception属性包含该错误。

Handle方法

是处理当前的异常数组,判断上一层我当前哪些已经处理好了, 没有处理好的,还需要向上抛出

                  ex.Handle(x =>
                {
                    if (x.InnerException.Message == "我是 childTask1 异常")
                    {
                        return true;
                    }

                    return false;
                });

当前的Handle就是来遍历 异常数组,如果有一个异常信息是这样的,我认为是已经处理的。
如果你觉得异常还需要往上抛,请返回false

2.7 Task核心调度器 TaskScheduler

在Task底层有一个TaskScheduler,它决定了task该如何被调度,而在.net framework中有两种系统定义Scheduler,第一个是Task默认的ThreadPoolTaskScheduler,还是一种就是 SynchronizationContextTaskScheduler,以及这两种类型之外的如何自定义。

1.ThreadPoolTaskScheduler

这种scheduler机制是task的默认机制,而且从名字上也可以看到它是一种委托到ThreadPool的机制,刚好也从侧面说明task是基于ThreadPool基础上的封装,如果想具体查看代码逻辑,你可以通过ILSpy反编译一下代码看看:

protected internal override void QueueTask(Task task)
		{
			if ((task.Options & TaskCreationOptions.LongRunning) != TaskCreationOptions.None)
			{
				new Thread(ThreadPoolTaskScheduler.s_longRunningThreadWork)
				{
					IsBackground = true
				}.Start(task);
				return;
			}
			bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) > TaskCreationOptions.None;
			ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal);
		}

2.SynchronizationContextTaskScheduler

从这个名字中就可以看到,这是一个同步上下文的taskscheduler,原理就是把繁重的耗时工作丢给ThreadPool,然后将更新UI的操作丢给 UI线程的队列中,由UIThread来执行,具体的也可以在这种scheduler中窥得一二。

protected internal override void QueueTask(Task task)
{
		this.m_synchronizationContext.Post(SynchronizationContextTaskScheduler.s_postCallback, task);
}

然后可以从s_postCallback上看到里面有一个Invoke函数:

public virtual void Post(SendOrPostCallback d, object state)
{
      ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}

例如,在winform,或者wpf中如果给一个控件赋值,都是调用invoke方法。

(1)不要再UIThread做非常耗时的任务,否则会出问题。

        private void button1_Click(object sender, EventArgs e)
        {
            Task task = new Task(() =>
            {
                try
                {
                    label1.Text = "你好";
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            });

            task.Start(TaskScheduler.FromCurrentSynchronizationContext());
        }

(2) 耗时的操作我们要放到threadpool,更新操作放到同步上下文中。

                 var task = Task.Factory.StartNew(() =>
            {
                //默认耗时操作
                Thread.Sleep(1000 * 10);
            });

            task.ContinueWith(t =>
            {
                label1.Text = "你好";
            }, TaskScheduler.FromCurrentSynchronizationContext());

3.自定义TaskScheduler

我们知道在现有的.net framework中只有这么两种TaskScheduler,有些同学可能想问,这些Scheduler我用起来不爽,我想自定义一下,这个可以吗?当然!!!如果你想自定义,只要自定义一个类实现一下TaskScheduler就可以了,然后你可以将ThreadPoolTaskScheduler简化一下,即我要求所有的Task都需要走Thread,杜绝使用TheadPool,这样可以吗,当然了,不信你看。

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("hello world!!!");
            }, new CancellationToken(), TaskCreationOptions.None, new PerThreadTaskScheduler());

            Console.Read();
        }
    }

    /// <summary>
    /// 每个Task一个Thread
    /// </summary>
    public class PerThreadTaskScheduler : TaskScheduler
    {
        protected override IEnumerable<Task> GetScheduledTasks()
        {
            return null;
        }

        protected override void QueueTask(Task task)
        {
            var thread = new Thread(() =>
            {
                TryExecuteTask(task);
            });

            thread.Start();
        }

        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            throw new NotImplementedException();
        }
    }
}
posted @ 2020-08-10 21:32  【唐】三三  阅读(498)  评论(1编辑  收藏  举报