多线程开发(1)
多线程开发
异步:Async与Await简单用法
简单写法:
- 创建方法使用Async进行修饰,
- 在方法内部耗时操作前面使用Await修饰。
代码如下:
public Form2()
{
InitializeComponent();
}
private async void TaskTestFun()
{
var task = await Task.Run(Fun).ConfigureAwait(true);
Console.WriteLine(task.ToString());
var d = 1;
return;
}
public string Fun()
{
Thread.Sleep(5000);
return "Sleep 5000 has Finished";
}
private void button1_Click(object sender, EventArgs e)
{
TaskTestFun();
}
运行现象描述:
当主进程进入TaskTestFun方法后,执行到创建Task方法后,就空闲了(表现为可以被拖动),这个时候,程序的运行指针还在var task那行代码上。
Task方法执行的时候,自己创建了一个工作子线程,用来执行动作,当动作执行完了以后,通知主进程获取var task的结果,让主线程继续执行下去。
图解如下:
- 当前主线程进入TaskTestFun()
- 主线程执行Task.Run方法(创建一个新的工作线程)
-
当前主界面可以拖动()说明主线程空闲,没有被阻塞=非阻塞
-
当Task任务结束即将以后
-
执行task完Return以后,工作子线程被销毁
-
最后输出子线程返回的信息
多线程并发(所有线程全结束了,才能继续往下运行)
要点:
- 创建List
集合 - 使用WhenAll方法,等待所有的线程完成。
特点:这里跑的快的线程,先结束,然后要等待跑得慢的线程。大家一起结束才能继续往下面进行
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
InitializeList();
}
private void InitializeList()
{
for (int i = 0; i < 9; i++)
{
Targetlist.Add($" 任务{i} ");
}
}
private List<string> Targetlist = new List<string>();
List<Task<string>> Tasks = new List<Task<string>>();
private async void TaskTestFun()
{
Targetlist.ForEach(ItemActivation => Tasks.Add(Task.Run(Fun))) ;
var task = await Task.WhenAll(Tasks).ConfigureAwait(false);
foreach (var item in task)
{
Console.WriteLine(task.ToString());
}
return;
}
public string Fun()
{
Thread.Sleep(1000);
return "This Task has Finished";
}
private void button1_Click(object sender, EventArgs e)
{
TaskTestFun();
}
}
异步回调:多线程多线程并发(快的线程,先结束,先往下跑)(主线程不等待所有线程都结束)
- 要点:
不再使用async 和await关键字
使用ContinueWith关键字
-
出现的问题:
UI线程不再等待工作主线程任务是否结束,自己私下跑了
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
InitializeList();
}
private void InitializeList()
{
for (int i = 0; i < 9; i++)
{
Targetlist.Add($" 任务{i} ");
}
}
private List<string> Targetlist = new List<string>();
List<Task<string>> Tasks = new List<Task<string>>();
private void TaskTestFun()
{
int idx = 0;
foreach (string item in Targetlist)
{
Task.Run(Fun).ContinueWith(t => {Console.WriteLine($"{idx++} 当前任务结束"); });
}
Console.WriteLine("所有任务已经结束");
return;
}
public string Fun()
{
Thread.Sleep(1000);
return "This Task has Finished";
}
private void button1_Click(object sender, EventArgs e)
{
TaskTestFun();
}
}
运行结果:
异步回调:多线程多线程并发(快的线程,先结束,先往下跑)(主线程等待所有线程都结束)
要点:
- 使用 Async 和Await 关键字标记UI主线程等待执行
- 任务方法的内部,编写事件。在外层注册事件和绑定回调的方法
情景介绍:幼儿园,有很多孩子在吃饭,吃完饭后自己要洗碗。当最后一个孩子把碗洗碗以后。老师会安排孩子们午休
输出结果:
代码逻辑:
编写自定义事件EatEvent,然后让Eat完成以后调用EatEvent注册的WashDishes方法。
主线程收到await Task.WhenAll(Tasks)影响,所以暂时不会继续往下走。
本质:任务动作方法,有编写触发的事件
public partial class Form2 : Form
{
/// <summary>
/// 创建孩子集合
/// </summary>
List<Child> children = new List<Child>();
public Form2()
{
InitializeComponent();
InitializeList();
}
private void InitializeList()
{
/// 初始化6个孩子
for (int i = 0; i < 6; i++)
{
Child child = new Child();
///注册洗碗事件
child.EatFinish_Event += WashDishes;
children.Add(new Child());
}
}
/// <summary>
/// 执行洗碗动作
/// </summary>
private void WashDishes(object sender, EatEvent e)
{
Console.WriteLine("洗碗完成了");
}
private void button1_Click(object sender, EventArgs e)
{
TaskTestFun();
}
private async void TaskTestFun()
{
List<Task> Tasks = new List<Task>();
foreach (var child in children)
{
Tasks.Add(Task.Run(child.Eat));
}
await Task.WhenAll(Tasks);
Console.WriteLine("最后一个孩子吃完饭并且完成洗碗了");
Console.WriteLine("老师让孩子去午休了");
return;
}
}
/// <summary>
/// 自定义事件类
/// </summary>
public class EatEvent : EventArgs
{
}
/// <summary>
/// 孩子
/// </summary>
public class Child
{
/// <summary>
/// 吃饭用时
/// </summary>
public int Duration;
public void Eat() {
Thread.Sleep(Duration);
Console.WriteLine("完成吃饭动作,将要洗碗");
EatFinish_Event?.Invoke(this,new EatEvent());
}
public Child() {
Random rd = new Random();
int n = rd.Next(5,10);
Duration = n * 1000;
}
public Action<object, EatEvent> EatFinish_Event;
}
ConfigureAwait用法
使用场合:含有UI的程序。(不含有UI界面的程序是没有影响的)
代码:ConfigureAwait(false)
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
Stopwatch stopwatch = new Stopwatch();
private void button1_Click(object sender, EventArgs e)
{
stopwatch.Start();
Console.WriteLine("main1 thread:" + Thread.CurrentThread.ManagedThreadId);
TestConfigureAwait();
Console.WriteLine("main2 thread:" + Thread.CurrentThread.ManagedThreadId);
}
public async Task TestConfigureAwait()
{
await Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Task thread :" + Thread.CurrentThread.ManagedThreadId);
}).ConfigureAwait(false);
Thread.Sleep(3000);
Console.WriteLine("TestConfigureAwait thread : " + Thread.CurrentThread.ManagedThreadId);
stopwatch.Stop();
Console.WriteLine($"消耗时间{stopwatch.Elapsed.TotalSeconds.ToString()}");
}
}
输出结果:
代码:ConfigureAwait(true)
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
Stopwatch stopwatch = new Stopwatch();
private void button1_Click(object sender, EventArgs e)
{
stopwatch.Start();
Console.WriteLine("main1 thread:" + Thread.CurrentThread.ManagedThreadId);
TestConfigureAwait();
Console.WriteLine("main2 thread:" + Thread.CurrentThread.ManagedThreadId);
}
public async Task TestConfigureAwait()
{
await Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Task thread :" + Thread.CurrentThread.ManagedThreadId);
}).ConfigureAwait(true);
Thread.Sleep(3000);
Console.WriteLine("TestConfigureAwait thread : " + Thread.CurrentThread.ManagedThreadId);
stopwatch.Stop();
Console.WriteLine($"消耗时间{stopwatch.Elapsed.TotalSeconds.ToString()}");
}
}
简单说明一下:
使用ConfigureAwait(false)时,下一句会从线程池里面取出一个线程执行下面的语句。ConfigureAwait(true)时,下面的代码会等到主线程,然后由主线程继续运行下去。
总结:对于带有UI的程序有效,为了减少时间损耗,建议尽量使用ConfigureAwait(false)。
Task Wait/WaitAny/WaitAll
Wait 阻塞
让调用的线程阻塞在Wait处。
只有三种方法可以让调用线程结束等待:
- 线程执行完毕
- 线程被取消
- 线程当前引发异常
代码案例:
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
InitializeList();
}
List<People> peoples = new List<People>();
int count = 10;
private void InitializeList()
{
for (int i = 0; i < count; i++)
{
peoples.Add(new People());
}
}
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < count; i++)
{
Task task = Task.Run(() =>
{
peoples[i].Sleep();
});
task.Wait();
Console.WriteLine("完成当前线程任务");
}
Console.WriteLine("已经完成所有任务");
}
}
public class People
{
public People() { }
public void Sleep()
{
Thread.Sleep(5000);
}
}
当前效果:
主要线程一直等待(阻塞),直到任务线程完成动作以后才能执行下面的语句。
WaitAny 阻塞部分线程
调用线程也会在线程阻塞。但是当有一个线程完成任务以后,调用线程就能被释放,继续下面的语句
代码:
public partial class Form2 : Form
{
Stopwatch Stopwatch = new Stopwatch();
public Form2()
{
InitializeComponent();
InitializeList();
}
List<People> peoples = new List<People>();
int count = 10;
private void InitializeList()
{
for (int i = 0; i < count; i++)
{
peoples.Add(new People());
}
}
private void button1_Click(object sender, EventArgs e)
{
Stopwatch.Start();
List<Task> Tasks = new List<Task>();
for (int i = 0; i < count-1; i++)
{
Tasks.Add(
Task.Run(() =>
{
peoples[i].Sleep();
}
));
}
Task.WaitAny(Tasks.ToArray());
Console.WriteLine("当前已经有一个任务已经完成");
}
public class People
{
public People() { }
public void Sleep()
{
Console.WriteLine("执行睡觉任务");
Thread.Sleep(3000);
}
}
}
当创建完所有的任务以后
此时先执行到Task.waitAny
主线程阻塞到Task.waitAny上
当有一个任务线程执行完成以后:主线程继续往下面执行,其他线程释放
WaitAll 阻塞全部的线程
当所有的工作线程都执行完成以后,主线程才能从阻塞的状态继续往下执行
task Result()
这个现在没学明白,后面再写
死锁
借用某个案例中的代码。
有可能的触发函数:ContinueWith(true) 切换主线程执行
原理:当主线程遇到task.result这类方法被阻塞线程时,子线程执行完成触发ContinueWith,调度主线程回来,但是主线程已经被阻塞。所以触发死锁
关键代码如下:
主线程函数:
private void BtnDeadlock_Click(object sender, EventArgs e)
{
#region 死锁演示
Stopwatch sw = Stopwatch.StartNew(); //计时器
resultTextBox.Clear();
AppendLine("Wait开始......");
for (int i = 0; i < Data.Books.Count; i++)
{
var book = Data.Books[i];
var idx = i + 1;
var task = book.SearchAsync();
AppendLine($"{task.Result} {task.Id}.{task.Status}");
}
sw.Stop();
AppendLine($"Wait完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
#endregion
}
任务线程的执行函数:
public async Task<string> SearchAsync()
{
Stopwatch sw = Stopwatch.StartNew();
await Task.Delay(Duration * 1000).ConfigureAwait(true);
sw.Stop();
return Result(sw.ElapsedMilliseconds);
}
当主线程到达SearchAsync方法后,遇到await关键字,知道当前方法为异步方法。所以继续往下执行,然后遇到task.Result需要获取线程返回结果,就被阻塞住了。与此同时,任务线程完成Delay任务,通过ConfigureAwait(true)方法来让主线程切换回来,切换失败。所以导致了死锁。
如果修改ConfigureAwait(false),那么导致的结果将是主线程一直阻塞,表现为界面卡住。
分析如下:ConfigureAwait(false),任务完成以后,当前系统从线程池里面抽取一个子线程来完成后续任务。但是当主线程只是在task.Result上等到需要的返回值以后,才能继续往下执行。所以表现为主线程能够往下执行,流程正确,但是主线程阻塞,界面卡住。
解决方法:
第一步:ConfigureAwait(false)
第二部:因为主线程需要接收到子线程的返回值来执行方法。
那么把子线程的执行方法封装为异步回调ContinueWith(),让主线程不要去接触这些内容,就能让主线程避免死锁
private void BtnDeadlock_Click(object sender, EventArgs e)
{
#region 死锁演示
Stopwatch sw = Stopwatch.StartNew(); //计时器
resultTextBox.Clear();
AppendLine("Wait开始......");
for (int i = 0; i < Data.Books.Count; i++)
{
var book = Data.Books[i];
var idx = i + 1;
var task = book.SearchAsync().ContinueWith(t => InvworkAppendLine($"{idx}.{t.Result}"));
}
sw.Stop();
AppendLine($"Wait完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
#endregion
}
线程的暂停,继续和取消
ManualResetEvent
ManualResetEvent是一个通过信号机制,实现线程间状态同步的类。常用的方法有以下三个:
WaitOne:阻止当前线程,直到收到信号
Reset:将事件状态设置为非终止状态,导致线程阻止
Set:将事件状态设置为终止状态,从而允许继续执行一个或多个等待线程
CancellationTokenSource
CTS是用来发出取消标记,线程和任务根据CTS发出的信号,在自行决定在合适的位置取消线程或任务。
CTS是发出取消标记方式有2种 自动和手动:
1、手动通过调用Cancled()发出取消标记。
2、自动方式通过定时器、指定时间后取消信号。
public partial class FormComm : Form
{
private CancellationTokenSource? TokenSource;
private ManualResetEvent? ManualReset;
public FormComm()
{
InitializeComponent();
}
private void Print(string text)
{
BeginInvoke(() =>
{
richTextBox1.AppendText(text);
richTextBox1.ScrollToCaret();
richTextBox1.Refresh();
});
}
private void BtnStart_Click(object sender, EventArgs e)
{
TokenSource = new();
ManualReset = new(true);
int i = 0;
Task.Run(() =>
{
while (!TokenSource.Token.IsCancellationRequested)
{
ManualReset.WaitOne(); //根据是否收到信号判断是否阻塞当前线程
Thread.Sleep(200);
Print($"线程【{Environment.CurrentManagedThreadId}】正在运行第{++i}次{Environment.NewLine}");
}
}, TokenSource.Token);
}
private void BtnPause_Click(object sender, EventArgs e)
{
ManualReset?.Reset();
}
private void BtnContinue_Click(object sender, EventArgs e)
{
ManualReset?.Set();
}
private void BtnStop_Click(object sender, EventArgs e)
{
TokenSource?.Cancel();
}
}
扩展:取消令牌CancellationTokenSource的用法
第一种用法:注册事件
当执行取消动作以后,将会执行注册的事件
private void button1_Click(object sender, EventArgs e)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
CancellationToken cancellationToken = cancellationTokenSource.Token;
cancellationToken.Register(() => System.Console.WriteLine("我被取消了."));
System.Console.WriteLine("先等五秒钟.");
System.Console.WriteLine("手动取消.");
cancellationTokenSource.Cancel();
}
第二种用法:定时取消
通过构造函数,传入时间值
//设置3000毫秒(即3秒)后取消
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
CancellationToken cancellationToken = cancellationTokenSource.Token;
cancellationToken.Register(() => System.Console.WriteLine("我被取消了."));
System.Console.WriteLine("先等五秒钟.");
System.Console.WriteLine("手动取消.")
cancellationTokenSource.Cancel();
CancelAfter方法,传入时间值
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Token.Register(() => System.Console.WriteLine("我被取消了."));
//五秒之后取消
cancellationTokenSource.CancelAfter(5000);
System.Console.WriteLine("不会阻塞,我会执行.");
第三种用法:关联取消
设置一组关联的CancellationTokenSource,当其中的一个被取消了,那么其他的都被取消
private void button1_Click(object sender, EventArgs e)
{
//声明几个CancellationTokenSource
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationTokenSource tokenSource2 = new CancellationTokenSource();
CancellationTokenSource tokenSource3 = new CancellationTokenSource();
tokenSource2.Token.Register(() => System.Console.WriteLine("tokenSource2被取消了"));
//创建一个关联的CancellationTokenSource
CancellationTokenSource tokenSourceNew = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token, tokenSource2.Token, tokenSource3.Token);
tokenSourceNew.Token.Register(() => System.Console.WriteLine("tokenSourceNew被取消了"));
//取消tokenSource2
tokenSource2.Cancel();
}
第四种用法 判断取消(一般作为多线程令牌)
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = tokenSource.Token;
//打印被取消
cancellationToken.Register(() => System.Console.WriteLine("被取消了."));
//模拟传递的场景
Task.Run(async ()=> {
while (!cancellationToken.IsCancellationRequested)
{
System.Console.WriteLine("一直在执行...");
await Task.Delay(1000);
}
});
//5s之后取消
tokenSource.CancelAfter(5000);