以下是学习笔记:
回顾:
Thread线程和ThreadPool线程池
Thread:我们可以开启一个线程。但是请大家记住:线程开启会在空间和时间上有不小的开销。所以,不能随便开。
ThreadPool:会根据你的CPU的核心数开启一个最合适的线程数量。如果你操作中,非常耗时,就不要用线程池,如果耗时十几分钟,那就不合适线程池了。
Task=>Thread + ThreadPool结合 ,使用多线程,尽量使用Task
1,Task和各种任务阻塞、延续及其线程锁Lock
#region Task使用【1】多线程任务的开启3种方式
//【1】通过new的方式创建一个Task对象,并启动
static void Method1_1()
{
Task task1 = new Task(() =>
{
//在这个地方编写我们需要的逻辑...
Console.WriteLine($"new一个新的Task启动的子线程Id={Thread.CurrentThread.ManagedThreadId}");
});
task1.Start();
}
//【2】使用Task的Run()方法
static void Method1_2()
{
Task task2 = Task.Run(() =>
{
//在这个地方编写我们需要的逻辑...
Console.WriteLine($"使用Task的Run()方法开启的子线程Id={Thread.CurrentThread.ManagedThreadId}");
});
}
//1和2对比
//1,灵活开启线程,想什么时候开启就什么时候开启
//2, 马上开启线程
//【3】使用TaskFactory启动(类似于ThreadPool)
static void Method1_3()
{
Task task3 = Task.Factory.StartNew(() =>
{
//在这个地方编写我们需要的逻辑...
Console.WriteLine($"使用TaskFactory开启的子线程Id={Thread.CurrentThread.ManagedThreadId}");
});
}
#endregion
#region Task使用【2】Task的阻塞方式和任务延续
//【1】回顾之前使用Thread多个子线程执行时阻塞的方法
static void Method2()
{
Thread thread1 = new Thread(() =>
{
Thread.Sleep(2000);
Console.WriteLine("Child Thread (1)......");
});
Thread thread2 = new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Child Thread (2)......");
});
thread1.Start();
thread2.Start();
//...
thread1.Join();//让调用线程阻塞
thread2.Join();
//如果有很多的thread,是不是也得有很多的Join?还有,我们只希望其中一个执行完以后,后面的其他线程就能执行,这个也做不了!
Console.WriteLine("This is Main Thread!");
}
//【2】Task各种【阻塞】方式(3个)
static void Method3()
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task1.Start();
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task2.Start();
////第1种方式:挨个等待和前面一样
//task1.Wait();
//task2.Wait();
////第2种方式:等待所有的任务完成 【推荐】
Task.WaitAll(task1, task2);
//第3种方式:等待任何一个完成即可 【推荐】
//Task.WaitAny(task1, task2);
Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString());
/*
第2中方式结果:
Task1子线程Id=4 21:46:58
Task2子线程Id=3 21:46:59
主线程开始运行!Time=21:46:59
第3种方式结果
Task1子线程Id = 3 21:41:34
主线程开始运行!Time = 21:41:34
Task2子线程Id = 4 21:41:35
*/
}
//Task任务的延续:WhenAll 希望前面所有任务执行完毕后,再继续执行后面的线程,和前面相比,既有阻塞,又有延续。
static void Method4()
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task1.Start();
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task2.Start();
//线程的延续(主线程不等待,子线程依次执行,如果你需要主线程也按照子线程的顺序来,请你自己把主线程的任务放到延续任务中就可以)
//线运行主线程,然后task1和task2都执行完,再执行task3
Task.WhenAll(task1, task2).ContinueWith(task3 =>
{
//在这里可以编写你需要的业务...
Console.WriteLine($"Task3子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString());
/*
主线程开始运行!Time = 21:44:46
Task1子线程Id = 3 21:44:47
Task2子线程Id = 4 21:44:48
Task3子线程Id = 3 21:44:48
*/
}
//Task的延续:WhenAny
static void Method5()
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task1.Start();
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task2.Start();
//线程的延续(主线程不等待,子线程任何一个执行完毕,就会执行后面的线程)
Task.WhenAny(task1, task2).ContinueWith(task3 =>
{
//在这里可以编写你需要的业务...
Console.WriteLine($"Task3子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString());
/*
主线程开始运行!Time=21:48:51
Task1子线程Id=3 21:48:52
Task3子线程Id=6 21:48:52
Task2子线程Id=4 21:48:53
*/
}
#endregion
#region Task使用【3】Task常见枚举 TaskCreationOptions(父子任务运行、长时间运行的任务处理)
//请大家通过Task的构造方法,观察TaskCreationOptions这个枚举的类型,自己通过F12查看
static void Method6()
{
Task parentTask = new Task(() =>
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}, TaskCreationOptions.AttachedToParent);
Task task2 = new Task(() =>
{
Thread.Sleep(3000);
Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}, TaskCreationOptions.AttachedToParent);
task1.Start();
task2.Start();
});
parentTask.Start();
parentTask.Wait();//等待附加的子任务全部完成。相当于Task.WaitAll(taks1,task2);
//TaskCreationOptions.AttachedToParent如果这个枚举参数不添加,主线程会直接运行,不等待
Console.WriteLine("主线程开始执行!Time= " + DateTime.Now.ToLongTimeString());
/*
Task1子线程Id=4 21:52:17
Task2子线程Id=5 21:52:19
主线程开始执行!Time= 21:52:19
*/
}
//长时间的任务运行,需要采取的方法
static void Method7()
{
Task task1 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}, TaskCreationOptions.LongRunning);
//LongRunning:如果你明确知道这个任务是长时间运行的,建议你加上。
//当然你使用Thread也是可以的。但是不要使用ThreadPool,因为长时间占用不归还线程,系统会强制开启新的线程,会一定程度影响性能
task1.Start();
task1.Wait();
Console.WriteLine("主线程开始执行!Time= " + DateTime.Now.ToLongTimeString());
/*
Task1子线程Id=3 21:57:42
主线程开始执行!Time= 21:57:42
*/
}
#endregion
#region Task使用【4】Task中的取消功能:使用的是CacellationTokenSoure解决多任务中协作取消和超时取消方法
//【1】Task任务的取消和判断
static void Method8()
{
//创建取消信号源对象
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Factory.StartNew(() =>
{
int i = 0;
while (!cts.IsCancellationRequested) //判断任务是否被取消
{
Thread.Sleep(200);
i++;
Console.WriteLine(
$"执行次数:{i},子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}
}, cts.Token);
//我们在这个地方模拟一个事件产生,如果发生某个错误,就取消线程
Thread.Sleep(2000);
cts.Cancel(); //取消任务,只要传递这样一个信号就可以
/*
执行次数:1,子线程Id=3 22:06:18
执行次数:2,子线程Id=3 22:06:18
执行次数:3,子线程Id=3 22:06:18
执行次数:4,子线程Id=3 22:06:18
执行次数:5,子线程Id=3 22:06:19
执行次数:6,子线程Id=3 22:06:19
执行次数:7,子线程Id=3 22:06:19
执行次数:8,子线程Id=3 22:06:19
执行次数:9,子线程Id=3 22:06:19
执行次数:10,子线程Id=3 22:06:20
*/
}
//【2】Task任务取消:同时我们也希望做一些清理的工作,也就是取消这个动作会触发一个任务。
static void Method9()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Factory.StartNew(() =>
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(500);
Console.WriteLine($"子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}
}, cts.Token);
//注册一个委托:这个委托将在任务取消的时候调用
cts.Token.Register(() =>
{
//在这个地方可以编写自己要处理的逻辑...
Console.WriteLine($"任务取消,开始清理工作......{DateTime.Now.ToLongTimeString()}");
Thread.Sleep(2000);
Console.WriteLine($"任务取消,清理工作结束......{DateTime.Now.ToLongTimeString()}");
});
//这个地方肯定是有其他的逻辑来控制取消
Thread.Sleep(3000);//模拟其他的耗时工作
cts.Cancel();//取消任务
/*
子线程Id=3 22:12:52
子线程Id=3 22:12:53
子线程Id=3 22:12:53
子线程Id=3 22:12:54
子线程Id=3 22:12:54
任务取消,开始清理工作......22:12:55
子线程Id=3 22:12:55
任务取消,清理工作结束......22:12:57
*/
}
//【3】Task任务延时自动取消:比如我们请求一个远程接口,如果长时间没有返回数据,我们可以做一个时间限制,超时可以取消任务(比如微信红包退回)
static void Method10()
{
CancellationTokenSource cts = new CancellationTokenSource();
// CancellationTokenSource cts = new CancellationTokenSource(3000);
Task task = Task.Factory.StartNew(() =>
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(300);
Console.WriteLine($"子线程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}
}, cts.Token);
//注册一个委托:这个委托将在任务取消的时候调用
cts.Token.Register(() =>
{
//在这个地方可以编写自己要处理的逻辑...
Console.WriteLine($"任务取消,开始清理工作......{DateTime.Now.ToLongTimeString()}");
Thread.Sleep(2000);
Console.WriteLine($"任务取消,清理工作结束......{DateTime.Now.ToLongTimeString()}");
});
cts.CancelAfter(3000); //3秒后自动取消
/*
子线程Id=3 22:16:49
子线程Id=3 22:16:50
子线程Id=3 22:16:50
子线程Id=3 22:16:50
子线程Id=3 22:16:50
子线程Id=3 22:16:51
子线程Id=3 22:16:51
子线程Id=3 22:16:51
子线程Id=3 22:16:52
任务取消,开始清理工作......22:16:52
子线程Id=3 22:16:52
任务取消,清理工作结束......22:16:54
*/
}
#endregion
#region Task使用【5】Task中专门的异常处理:AggregateException
//AggregateException:是一个异常集合,因为Task中可能抛出异常,所以我们需要新的类型来收集异常对象
static void Method11()
{
var task = Task.Factory.StartNew(() =>
{
var childTask1 = Task.Factory.StartNew(() =>
{
//实际开发中这个地方写你处理的业务,可能会发生异常....
//自己模拟一个异常
throw new Exception("my god!Exception from childTask1 happend!");
}, TaskCreationOptions.AttachedToParent);
var childTask2 = Task.Factory.StartNew(() =>
{
throw new Exception("my god!Exception from childTask2 happend!");
}, TaskCreationOptions.AttachedToParent);
});
try
{
try
{
task.Wait(); //1.异常抛出的时机(等待task执行完毕,这里是等到异常抛出)
}
catch (AggregateException ex) //2.异常所在位置
{
foreach (var item in ex.InnerExceptions)
{
Console.WriteLine(item.InnerException.Message + " " + item.GetType().Name);
}
//3.异常集合,如果你想往上抛,需要使用Handle方法处理一下
ex.Handle(p =>
{
if (p.InnerException.Message == "my god!Exception from childTask1 happend!")
return true;//就结束了,不往上抛了
else
return false; //返回false表示往上继续抛出异常
});
}
}
catch (Exception ex)
{
Console.WriteLine("-----------------------------------------------------");
Console.WriteLine(ex.InnerException.InnerException.Message);
}
/*
my god!Exception from childTask2 happend! AggregateException
my god!Exception from childTask1 happend! AggregateException
-----------------------------------------------------
my god!Exception from childTask2 happend!
*/
}
#endregion
#region 监视锁:Lock 限制线程个数的一把锁
//为什么要用锁?在多线程中,尤其是静态资源的访问,必然会有竞争
private static int nums = 0;
private static object myLock = new object();
static void Method12()
{
for (int i = 0; i < 5; i++)
{
//开启5线程调用一个nums
Task.Factory.StartNew(() =>
{
//TestMethod1();//不加锁的结果顺序是乱的,1,3,2,4,6,9,,,,500
TestMethod2();//加锁的结果顺序是对的,因为把资源给锁住了,1,2,3,4,5,6,,,,500
});
}
}
static void TestMethod1()
{
for (int i = 0; i < 100; i++)
{
nums++;
Console.WriteLine(nums);
}
}
static void TestMethod2()
{
for (int i = 0; i < 100; i++)
{
lock (myLock)
{
nums++;
Console.WriteLine(nums);
}
}
}
//Lock是Monitor语法糖,本质是解决资源的锁定问题
//我们锁住的资源一定是让线程可访问到的,所以不能是局部变量。
//锁住的资源千万不要是值类型。
//lock也不能锁住string类型。
}
#endregion
2,Task中的跨线程访问控件和UI耗时任务卡顿的解决方法

//普通方法
private void btnUpdate_Click(object sender, EventArgs e)
{
Task task = new Task(() =>
{
this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!";
});
//task.Start(); //这样使用会报错
//使用下面的方式解决报错的问题
task.Start(TaskScheduler.FromCurrentSynchronizationContext());//使用任务调度器
}
//针对UI耗时的情况,单独重载其实并不是很好
private void btnUpdate_Click1(object sender, EventArgs e)
{
Task task = new Task(() =>
{
//模拟耗时(这个地方会卡主)
Thread.Sleep(5000);//界面会卡5秒钟,多线程不是万能,多线程并不是解决卡界面的。
this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!";
});
//task.Start(); //这样使用会报错
//使用下面的方式解决报错的问题
task.Start(TaskScheduler.FromCurrentSynchronizationContext());
}
//以后耗时任务都可以用这个方法
//针对耗时任务,我们可以使用新的方法
private void btnUpdate_Click2(object sender, EventArgs e)
{
this.btnUpdate.Enabled = false;
this.lblInfo.Text = "数据更新中,请等待......";
Task task =Task.Factory.StartNew(() =>
{
Thread.Sleep(5000); //有耗时的任务,我们可以放到ThreadPool中
});
//在ContinueWith中更新我们的数据
task.ContinueWith(t =>
{
this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!";
this.btnUpdate.Enabled = true;
},TaskScheduler.FromCurrentSynchronizationContext()); //更新操作到同步的上下文中
}
