admin管理员组

文章数量:1530517

#线程详解

1. Thread基础之从 WinDbg 角度理解你必须知道的时间和空间上的开销

一:空间上的开销
1、thread本身来说就是操作系统的概念。。。
  • <1> thread的内核数据结构,其中有osid,context => CPU寄存器的里面的一些变量。 30 ms

  • <2>. thread 环境块 :
    tls【thread本地存储】, execptionList 的信息。。。。
    WinDbg 来给大家演示。。。 32,64 =可以达到clr的层面给大家展示底层知识
    .loadby sos clr

  • <3> 用户模式堆栈 内存溢出的一个异常 【堆栈溢出】

    ​ 一个线程 分配 1M的堆栈空间,,【参数,局部变量】

  • <4> 内核模式堆栈

    • 在CLR的线程操作,包括线程同步,大多都是调用底层的win32 函数 ,用户模式的参数需要传递到内核模式。。。
二、时间的开销
  • <1> 我们进程启动的时候,会加载很多的dll [托管和非托管的], exe,资源,元数据。。。。

  • ​ 进程启动的时候,我怎么没有看到应用程序域。。。

  • ​ 进程启动的时候,默认会有三个应用程序域。system domain, shared domain[int,long…] ,domain1.

  • ​ 开启一个thread,销毁一个thread 都会通知进程中的dll,attach,detach 标志位。。。

  • ​ 通知dll的目的就是 给thread做准备工作,比如销毁,让这些dll做资源清理。。。。

  • ​ <2> 时间片切换

  • ​ 8个逻辑处理器,可供8个thread并行执行。。。。

  • ​ 比如说9个thread并发执行。 必然会有一个thread休眠30 ms。。。。

  • ​ for => palleral for


2. Thread生命周期管理的Start,Suspend,Resume,Join,Interrupt,Abort五大方法介绍

一、在clr中Thread这个名字来表示线程这个概念
  • ​ 了解Thread的实例方法。。。 【id,ThreadState】

  • ​ 管理Thread生命周期 Start, Suspend, Resume, Intterupt,Abort。。。 Join 在使用Thread的时候是用的非 常多的。。。

1、Start演示
thread = new Thread(new ThreadStart(() =>
       {
   
           while (true)
           {
   
               try
               {
   
                 Thread.Sleep(1000);
          		 textBox1.Invoke(new Action(() =>{
   
                      textBox1.AppendText(string.Format("{0},", index++));
                      }));
               }
               catch (Exception ex)
               {
   
                   MessageBox.Show(string.Format("{0}, {1}", ex.Message, index));
              }
   			}
   }));
   thread.Start();
2、suspend演示
  • 通过winddebug看一下thread 的状态。。。。
 0:017> !ThreadState ab024
User Suspend Pending
Legal to Join
CLR Owns
CoInitialized
In Multi Threaded Apartment
Fully initialized
Sync Suspended
3、Resume演示
  • ​ 用来恢复suspend的暂停功能。。。
4、Interrupt 演示
  • ​ 用来中断处于WaitSleepJoin状态的线程。。。。

  • ​ while(true){ continue… 效果}

  • ​ 当你调用interrupt的时候,会抛出一个interrupt的异常。。。。

5、Abort演示
  • ​ 通过抛出异常的方式销毁一个线程。。。。
    • while(true) { break… 效果 }
  • Interrupt 和 Abort做对比。。。
6、Join演示
  • ​ task.wait();
0:007> !ThreadState 202b020
Legal to Join
CLR Owns
CoInitialized
In Multi Threaded Apartment
Fully initialized
Interruptible

3、Thread静态方法之三大TLS操作 ThreadStatic、AllocateDataSlot、ThreadLocal

[线程本地存储]

  • Thread中的一些静态方法 AllocateDataSlot、AllocateNamedDataSlot、GetNamedDataSlot、FreeNamedDataSlot,给所有线程分配一个数据槽。 存放数据。
    • SetData
    • GetData
一、变量 => Thread 的关系 t1, t2
  • 《1》 t1 ,t2 共享 变量 public注意有“锁”的概念

  • 《2》 t1 , t2 各自有一个 变量 internel 没有锁争用的概念

static void Main(string[] args)
{
   
    var slot = Thread.AllocateNamedDataSlot("username");
     //主线程 上 设置槽位,, 也就是hello world 只能被主线程读取,其他线程无法读取
    Thread.SetData(slot, "hello world!!!");
    var t = new Thread(() =>
    {
   
        var obj = Thread.GetData(slot);
        Console.WriteLine("当前工作线程:{0}", obj);
    });
    t.Start();
    var obj2 = Thread.GetData(slot);
    Console.WriteLine("主线程:{0}", obj2);
    Console.Read();
}
二、性能提升版: 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: 也是用来做 线程可见性
static void Main(string[] args)
{
   
    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);
    Console.Read();
}
  • 这些数据都是存放在线程环境块中。。【是线程的空间开销】 !teb来查看。。TLS: thread local storage。。。

4、Thread静态方法之经典 Release Bug 认识MemoryBarrier、VolatileRead、Write

​ 【如何禁止编译器优化和Cache读取】

一、thread中一些静态方法 【内存栅栏】
  • ​ MemoryBarrier、VolatileRead/Write 这些方法到底有什么用处。。。。

  • 在实际项目中,我们都喜欢用Release版本,而不是Debug。。。。

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

二、release和debug到底性能差异有多大。。。
  • ​ 冒泡排序 O(N)2 1w * 1w = 1亿

  • 从结果中可以看到,大概有5倍的差距。。。

  • 在任何时候,不见得release都是好的。。有可能会给你引入一些bug。。。

class Program
{
   
        static void Main(string[] args)
        {
   
            var path = Environment.CurrentDirectory + "//1.txt";
            var list = System.IO.File.ReadAllLines(path).Select(i => 		 Convert.ToInt32(i)).ToList();
            for (int i = 0; i < 5; i++)
                {
   
                    var watch = Stopwatch.StartNew();
                    var mylist = BubbleSort(list);
                    watch.Stop();
                    Console.WriteLine(watch.Elapsed);
                }
                Console.Read();
            }
            //冒泡排序算法
            static List<int> BubbleSort(List<int> list)
            {
   
                int temp;
                //第一层循环: 表明要比较的次数,比如list.count个数,肯定要比较count-1次
                for (int i = 0; i < list.Count - 1; i++)
                {
   
                    //list.count-1:取数据最后一个数下标,
                    //j>i: 从后往前的的下标一定大于从前往后的下标,否则就超越了。
                    for (int j = list.Count - 1; j > i; j--)
                    {
   
                        //如果前面一个数大于后面一个数则交换
                        if (list[j - 1] > list[j])
                        {
   
                            temp = list[j - 1];
                            list[j - 1] = list[j];
                            list[j] = temp;
                        }
                    }
                }
                return list;
            }
      }        
}
static void Main(string[] args)
{
   
    var isStop = false;
    var t = new Thread(() =>
    {
   
        var isSuccess = false;
        while (!isStop)
        {
   
            isSuccess = !isSuccess;
        }
    });
    t.Start();
    Thread.Sleep(1000);
    isStop = true;
    t.Join();
    Console.WriteLine("主线程执行结束!");
    Console.ReadLine();
}
  • ​ 上面这段代码在release环境下出现问题了。。。主线程不能执行结束。。。。

  • ​ 从代码中可以发现,有两个线程在共同一个isStop变量。。。

  • ​ 就是t这个线程会将isStop加载到Cpu Cache中。。。 【release大胆的优化】

  • ​ 两种方法解决:

    • 1、不要让多个线程去操作 一个共享变量,否则容易出问题。
    • 2、如果一定要这么做,那就需要使用节所涉及到的内容。
    • MemoryBarrier VolatileRead/Write
    • 不要进行缓存,每次读取数据都是从memrory中读取数据。。。
    • MemoryBarrier => 在此方法之前的内存写入都要及时从cpu cache中更新到 memory。。。
    • 在此方法之后的内存读取都要从memory中读取,而不是cpu cache。。。
    static void Main(string[] args)
    {
         
        var isStop = 0;
        var t = new Thread(() =>
        {
         
            var isSuccess = false;
            while (isStop == 0)
            {
         
                Thread.VolatileRead(ref isStop);
                isSuccess = !isSuccess;
            }
        });
        t.Start();
        Thread.Sleep(1000);
        isStop = 1;
        t.Join();
        Console.WriteLine("主线程执行结束!");
        Console.ReadLine();
    }
    

5、从Windbg角度理解ThreadPool、Thread的差异、工作线程和IO线程的工作差异分析

一、thread 它是clr表示一个线程的数据结构
二、ThreadPool 线程池
  • thread 我如果想做一个异步任务,就需要开启一个Thread。 具有专有性。。。

  • ThreadPool =》 如果想做异步任务 只需要向租车公司借用 =》 使用完了就要归还

三、ThreadPool的使用方式
static void Main(string[] args)
{
   
        ThreadPool.QueueUserWorkItem((obj) =>
        {
   
            var func = obj as Func<string>;   
     Console.WriteLine("我是工作线程:{0}, content={1}", Thread.CurrentThread.ManagedThreadId,func());
    }, new Func<string>(() => "hello world"));
    Console.WriteLine("主线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
    Console.Read();
}
四、Thread 和 ThreadPool 到底多少区别。。。
  • 现在有10个任务,如果用Thread来做,需要开启10个Thread

  • 如果用ThreadPool来做,只需要将10个任务丢给线程池

  • windbg的角度来看一下两者的区别。。。。

  • 1、区别: DeadThread: 10

    • 虽然都挂掉了,但是没有被GC回收。。。。
    • Thread( ) { this.InternalFinalize () ; }
    • 从析构函数中可以看到 this.InternalFinalize(); 就是说销毁之后,先进入终结器。。。
    • 《1》 或许能够被复活。。。 《2》 下次被GC回收。。。。
    • 虽然thread已经死掉了,但是该占的资源还是要占。。。。
static void Main(string[] args)
        {
   
            for (int i = 0; i < 10; i++)
            {
   
                Thread thread = new Thread(() =>
                {
   
                    for (int j = 0; j < 10; j++)
                    {
   
                        Console.WriteLine("work:{0},tid={1}", i, Thread.CurrentThread.ManagedThreadId);
                    }
                }); 
     thread.Name = "main" + i;

        thread.Start();
    }

    Console.Read();
}
  • ​ 2.threadPool解决同样的问题。。。

  • 从windbg中可以看到,当前没有死线程,而是都是默认初始化的。。。

DeadThread:       0
0:014> !threadpool
CPU utilization: 4%
Worker Thread: Total: 8 Running: 0 Idle: 8 MaxLimit: 2047 MinLimit: 8
Work Request in Queue: 0
Number of Timers: 0
Completion Port Thread:Total: 0 Free: 0 MaxFree: 16 CurrentLimit: 0 MaxLimit: 1000 MinLimit: 8
  • ​ 看到了当前的threadpool,

  • ​ 其中有“工作线程” 和 “IO线程”

  • 工作线程: 给一般的异步任务执行的。。其中不涉及到 网络,文件 这些IO操作。。。 【开发者调用】

  • IO线程: 一般用在文件,网络IO上。。。 【CLR调用】

  • 8的又来就是因为我有 8个逻辑处理器,也就是说可以8个Thread 并行处理。。。。

总结:
  • ​ 1.threadPool 可以用8个线程 解决 thread 10个线程干的事情,

  • 节省了空间和时间:

  • ​ 时间: 通过各个托管和非托管的dll。。。

  • ​ 空间:teb,osthread结构, 堆栈。


6、定时任务之RegisterWaitForSingleObject、Timer以及专业的Quarz.Net的简介

一、定时器 Timer
  • ​ ThreadPool 也有定时器的功能。。。。
  • 定时器的功能肯定需要 工作线程来处理。。。

​ 1、ThreadPool 定时器功能

static void Main(string[] args)
{
   
    ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, b) =>
    {
   
        //做逻辑判断,判断是否在否以时刻执行。。。
        Console.WriteLine("obj={0},tid={1}, datetime={2}", obj, Thread.CurrentThread.ManagedThreadId,
                                                                 DateTime.Now);
    }), "hello world", 1000, false);
	Console.Read();
}
  •  一般在使用Timer的时候,有一个延期执行的功能。
    
  • ​ windbg 来看一下底层线程是什么样的。。。。

ID OSID ThreadOBJ   State GC Mode   GC Alloc Context  Domain   Count Apt Exception
0  1 3f54 01157bc8  2a020 Preemptive  02E8A3E4:00000000 01152258 1   MTA 
5  2 2594 011678f8  2b220 Preemptive  00000000:00000000 01152258 0   MTA (Finalizer) 
6  3 3c28 01189990  1020220 Preemptive  00000000:00000000 01152258 0     Ukn (Threadpool Worker) 
7  4 121c 0118a2c0   8029220 Preemptive  02E8D8A4:00000000 01152258 0     MTA (Threadpool Completion Port) 
8  5 28f4 0118bd70   8029220 Preemptive  00000000:00000000 01152258 0     MTA (Threadpool Completion Port) 
0:009> !threadpool
CPU utilization: 9%
Worker Thread: Total: 0 Running: 0 Idle: 0 MaxLimit: 2047 MinLimit: 8
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 2 Free: 2 MaxFree: 16 CurrentLimit: 2 MaxLimit: 1000 MinLimit: 8
二、Timer
  • ​ System.threading 下面有timer

  • ​ System.Timer 下面Timer。。。

  • ​ System.Windows.Form 下面Timer。。。

  • ​ System.Web.UI 下面Timer。。。

0:009> !threads
ThreadCount:      4
UnstartedThread:  0
BackgroundThread: 3
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
Lock  
ID OSID ThreadOBJ   State GC Mode    GC Alloc Context  Domain   Count Apt Exception
0   1 2d74 00f785c8     2a020 Preemptive  02E360F0:00000000 00f72030 1     MTA 
5   2 3784 00f87ea0     2b220 Preemptive  00000000:00000000 00f72030 0     MTA (Finalizer) 
6   3 2dc4 00faae18   102a220 Preemptive  00000000:00000000 00f72030 0     MTA (Threadpool Worker) 
7   4 3e34 00fab748   1029220 Preemptive  02E3D4D0:00000000 00f72030 0     MTA (Threadpool Worker)
  • 底层有一个队列 TimerQueue instance2 = TimerQueue.Instance; internal class TimerQueue

  • Timer 首先是用 ThreadPool.UnsafeQueueUserWorkItem(waitCallback, timer); 来完成定时功能。。

三、实战开发中,基本上不会用Timer来处理问题。。。。
  • ​ 因为处理的功能太少:

  • ​ 例:1、我希望早上8点执行。。。

  • ​ 2、我希望明天8点执行。。。

  • ​ 3、我希望每天8点执行。。。

  • ​ 4、我希望每个月的8号执行。。。

  • ​ 5、我希望下个月8号执行,排除双休日。。。

  • ​ 6、半个小时执行一次。。。

  • ​ 所以用第四种方法执行这些任务

​ ↓

四、Quarz

​ Quartz.dll(详情请查文档)


7、Task和Task[T]之启动任务的三大方式Run,RunSynchronously,Start,StartNew以及内部代码异同点解析

一、Task 4.0
  • 为什么要有Task。
  • Task => Thread + ThreadPool + 优化和功能扩展
  • Thread:容易造成时间 + 空间开销,而且使用不当,容易造成线程过多,导致时间片切换。。。
  • ThreadPool:控制能力比较弱。 做thread的延续,阻塞,取消,超时等等功能。。。。
  • 控制权在CLR,而不是在我们这里。。。
  • Task 看起来像是一个Thread。。。
  • Task 是在ThreadPool的基础上进行的封装。。。。
  • 4.0之后,微软是极力的推荐 Task。。。来作为异步计算。。。
二:Task启动的几种方式
1、实例化的方式启动Task
Task task = new Task(() =>
{
   
    Console.WriteLine("我是工作线程: tid={0}", Thread.CurrentThread.ManagedThreadId);
});
task.Start();
Console.Read();
2、TaskFactory的方式启动Task
//使用TaskFactory启动
         var task = Task.Factory.StartNew(() =>
         {
   
             Console.WriteLine("我是工作线程: tid={0}", Thread.CurrentThread.ManagedThreadId);
         });
3、Task.Run 方法
//使用Task的Run方法
        var task = Task.Run(() =>
        {
   
            Console.WriteLine("我是工作线程: tid={0}", Thread.CurrentThread.ManagedThreadId);
        });
4、Task的同步方法
//这个是同步执行。。。。也就是阻塞执行。。。
var task = new Task(() =>
{
   
    Console.WriteLine("我是工作线程: tid={0}", Thread.CurrentThread.ManagedThreadId);
});

task.RunSynchronously();
三:Task是建立在ThreadPool上面吗???
  • ​ 我们的Task底层都是由不同的TaskScheduler支撑的。。。

  • ​ TaskScheduler 相当于Task的CPU处理器。。。

  • ​ 默认的TaskScheduler是ThreadPoolTaskScheduler。。。

  • ​ wpf中的TaskScheduler是 SynchronizationContextTaskScheduler

  • ​ ThreadPoolTaskScheduler

  • ​ this.m_taskScheduler.InternalQueueTask(this);

  • ​ 大家也可以自定义一些TaskScheduler。。。。

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);
}
四、Task
  • 让Task具有返回值。。。 它的父类其实就是Task。。

  • 具体的启动方式和Task是一样的。。。


8、Task详解之7种阻塞方式Wait,WaitAll,WaitAny,WhenAll,WhenAny,ContinueWith等实现任务的延续和阻塞代码解析

【这些都是task的核心

一:task的阻塞和延续操作
1、阻塞 thread => Join方法 【让调用线程阻塞】
   Thread t = new Thread(() =>
    {
   
        System.Threading.Thread.Sleep(100);
        Console.WriteLine("我是工作线程1");
    });
    Thread t2 = new Thread(() =>
    {
   
        System.Threading.Thread.Sleep(100);
        Console.WriteLine("我是工作线程2");
    });
    t.Start();
    t2.Start();
    t.Join();   // t1 && t2 都完成了 WaitAll操作。。。  WaitAny  t1 ||  t2 
    t2.Join();
    Console.WriteLine("我是主线程");
    Console.Read();
2、延续

Task:

  • WaitAll方法 必须其中所有的task执行完成才算完成
  • WaitAny方法,只要其中一个task执行完成就算完成
  • task.wait方法: 等待操作
  • 上面这些等待操作,返回值都是void。。。。
  • 现在有一个想法就是,我不想阻塞主线程实现一个waitall的操作。。。。
  • t1 执行完成了执行 t2 ,这就是延续的概念。。。。
  • 延续 = 它的基础就是wait。。。
static void Main(string[] args)
  {
   
      Task task1 = new Task(() =>{
   
                System.Threading.Thread.Sleep(1000);
    		  Console.WriteLine("我是工作线程1:{0}", DateTime.Now);
    });
    task1.Start();
    Task task2 = new Task(() =>
    {
   
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("我是工作线程2:{0}", DateTime.Now);
    });
    task2.Start();
    Task.WhenAll(task1, task2).ContinueWith(t =>
    {
   
        //执行“工作线程3”的内容
        Console.WriteLine("我是工作线程 {0}", DateTime.Now);
    });
    Console.Read();
}
  • WhenAll

  • WhenAny

  • Task工厂中的一些延续操作。。。

  • ContinueWhenAll


 Task.Factory.ContinueWhenAll(new Task[2] {
    task1, task2 }, (t) =>
            {
   
                //执行“工作线程3”的内容
                Console.WriteLine("我是主线程 {0}", DateTime.Now);
            });
  • ContinueWhenAny

  • 介绍Task的7种阻塞方式 + 延续

  • 如果会打组合拳,task异步任务还是写的非常漂亮。。。。

    Program代码:例:
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication2
    {
         
        class Program
        {
         
                static void Main(string[] args)
                {
         
                    Task task1 = new Task(() =>
                    {
         
                        System.Threading.Thread.Sleep(1000);  
                          Console.WriteLine("我是工作线程1:{0}", DateTime.Now);
                });
                task1.Start();
                Task task2 = new Task(() =>
                {
         
                    System.Threading.Thread.Sleep(2000);
                    Console.WriteLine("我是工作线程2:{0}", DateTime.Now);
                });
                task2.Start();
                //Task.WhenAny(task1, task2).ContinueWith(t =>
                //{
         
                //    //执行“工作线程3”的内容
                //    Console.WriteLine("我是主线程 {0}", DateTime.Now);
                //});
    
                //Task.WhenAll(task1, task2).ContinueWith(t =>
                //{
         
                //    //执行“工作线程3”的内容
                //    Console.WriteLine("我是工作线程 {0}", DateTime.Now);
                //});
                Console.Read();
            }
    	}
    }
    

9.Task实用枚举之TaskCreationOptions在【父子任务允许和拒绝,长任务,公平处理】分析

一、Task中的常见两种枚举
1、Task构造函数中使用的。。。
public Task(Action action, TaskCreationOptions creationOptions);
//
// 摘要:
//     指定可控制任务的创建和执行的可选行为的标志。
[Flags]
public enum TaskCreationOptions
{
   
    //
    // 摘要:
    //     指定应使用默认行为。
    None = 0,
    //
    // 摘要:
    //     提示 System.Threading.Tasks.TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
    PreferFairness = 1,
    //
    // 摘要:
    //     指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。它会向 System.Threading.Tasks.TaskScheduler
    //     提示,过度订阅可能是合理的。您可以通过过度订阅创建比可用硬件线程数更多的线程。
    LongRunning = 2,
    //
    // 摘要:
    //     指定将任务附加到任务层次结构中的某个父级。有关详细信息,请参阅 已附加和已分离的子任务。
    AttachedToParent = 4,
    //
    // 摘要:
    //     如果尝试附有子任务到创建的任务,指定 System.InvalidOperationException 将被引发。
    DenyChildAttach = 8,
    //
    // 摘要:
    //     防止环境计划程序被视为已创建任务的当前计划程序。这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.Tasks.TaskScheduler.Default
    //     当前计划程序。
    HideScheduler = 16
}


2、任务延续中的枚举

​ public Task ContinueWith(Action continuationAction, TaskContinuationOptions continuationOptions);

二、演示

TaskCreationOptions :

  • AttachedToParent :指定将任务附加到任务层次结构中的某个父级
  • 建立了父子关系。。。 父任务想要继续执行,必须等待子任务执行完毕。。。。
  • 看例子可以看到,其中是一个WaitAll的一个操作。。。
Task task = new Task(() =>
    {
   
        Task task1 = new Task(() =>
        {
   
            Thread.Sleep(100);
            Console.WriteLine("task1");
        }, TaskCreationOptions.AttachedToParent);
        Task task2 = new Task(() =>
        {
   
            Thread.Sleep(10);
            Console.WriteLine("task2");
        }, TaskCreationOptions.AttachedToParent);
        task1.Start();
        task2.Start();
    });

    task.Start();
    task.Wait();  //task.WaitAll(task1,task2);
    Console.WriteLine("我是主线程!!!!");
    Console.Read();

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

   static void Main(string[] args)
        {
   
            Task task = new Task(() =>
            {
   
                Task task1 = new Task(() =>
                {
   
                    Thread.Sleep(100);
                    Console.WriteLine(

本文标签: 线程详解