c#使用异步
- 异步方法用async关键字修饰的方法
- 异步方法的返回值一般是Task<T>,T是真是的返回值类型,如Task<int>
- 自定义的异步方法命名时一般用Async结尾,便于后续维护时一眼就可以看出来。一些系统自带的异步函数也是一Async结尾的。如httpclient.GetStringAsync
- 异步方法具有传染性,即一个方法内使用了await,则这个方法也必须用async修饰,即若这个方法是有返回值的则加async Task<T>修饰,没有返回值则写async Task修饰
- 下面是方法体内写了await,函数没加async修饰的,所以会报错
- 正确写法:
static async Task DownLoadHtmlAsync(string url,string filename)
{
using (HttpClient httpclient = new HttpClient())
{
Task<string> t = httpclient.GetStringAsync(url);
string html = await t;
}
}
- 下面由于Main中调用了await且无返回值,所以也需要加async Task修饰
static async Task Main(string[] args)
{
int leng= await DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
Console.WriteLine(leng);
Console.ReadKey();
}
static async Task<int> DownLoadHtmlAsync(string url,string filename)
{
using (HttpClient httpclient = new HttpClient())
{
Task<string> t = httpclient.GetStringAsync(url);
string html = await t;
File.WriteAllText(filename, html);
return html.Length;
}
}
- 加了Await后,程序会等待线程结束,但是界面不会卡主
- 假如一些函数中它调用了await,但是它不支持async修饰,或者不想要async修饰,可以修改成一下(尽量不要这样写,有死锁风险,加了这个,程序运行到.Result时会阻塞线程,一直等待到结果):
- 有返回值
static void Main(string[] args)//不加async修饰
{
Task<int> t=DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
int leng = t.Result;
Console.WriteLine(leng);
Console.ReadKey();
}
static async Task<int> DownLoadHtmlAsync(string url,string filename)
{
using (HttpClient httpclient = new HttpClient())
{
Task<string> t = httpclient.GetStringAsync(url);
string html = await t;
File.WriteAllText(filename, html);
return html.Length;
}
}
- 没返回值
static void Main(string[] args)//不加async修饰
{
Task t=DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
t.Wait();
Console.ReadKey();
}
static async Task DownLoadHtmlAsync(string url,string filename)
{
using (HttpClient httpclient = new HttpClient())
{
Task<string> t = httpclient.GetStringAsync(url);
string html = await t;
File.WriteAllText(filename, html);
}
}
- 在winform程序中也可以调用,不过button1_Click下不需要把void改成Task,因为要响应事件
private async void button1_Click(object sender, EventArgs e)
{
int leng = await DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
}
- 带返回值委托调用异步:
Func<int, Task<string>> DD2 = async delegate (int g)
{
Console.WriteLine("hh" + g);
await DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
return "hh" + g;
};//指向
Task<string> t = DD2(4);
string ret = t.Result;
- 不带返回值委托调用异步:
Action<int> DD2 = async delegate (int g)
{
await DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
};//指向
DD2(4);//调用
二、原理
用ILspy反编译可以看到(要用低版本的.NET),可以反编译后,其实系统帮我们把这个await里的代码分成了若干块,通过switch(num)来执行每块内容,全部执行完后返回
三、自定义异步函数
上述的GetStringAsync都是系统自带的异步函数,实际应用中可以使用Task.Run来自定义异步函数
- 无传入参数,无传出参数
private async void button1_Click(object sender, EventArgs e)
{
await WriteAllText( ) ;
}
public async Task WriteAllText()
{
string S = "";
await Task.Run(() =>
{
for (int i = 0; i < 10000; i++)
{
S = S + "iiiiiiiiiiii";
}
File.WriteAllText(S, "1.txt");
});
}
- 有传入参数,有传出参数
private async void button1_Click(object sender, EventArgs e)
{
this.Text=await WriteAllText(900000) ;
}
public async Task<string> WriteAllText(int Count)
{
Task<string> t =Task.Run(( ) => mWriteText(Count));
string html = await t;
return html;
}
public string mWriteText(int mCount)
{
string S = "";
for (int i = 0; i < mCount; i++)
{
S = S + "iiiiiiiiiiii";
}
File.WriteAllText( "1.txt",S);
return "333";
}
注上述的也可以这样写,并且建议这样写。可以去掉async变得更加简洁
public Task<string> WriteAllText(int Count)
{
return Task.Run(() => mWriteText(Count));
//Task<string> t =Task.Run(( ) => mWriteText(Count));
//string html = await t;
//return html;
}
四、Task的回调函数
回调函数即等待线程完成后,再执行的函数。其实Task.Run中不想线程Thread函数专门有个回调函数,可以使用ContinueWith方法来实现
private void button8_Click(object sender, EventArgs e)
{
Console.WriteLine($"hh1--{Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
Console.WriteLine($"hh2--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
Console.WriteLine($"hh3--{Thread.CurrentThread.ManagedThreadId}");
});
task.ContinueWith((t) =>
{
Console.WriteLine($"hh4--{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"hh5--{Thread.CurrentThread.ManagedThreadId}");
}
结果及其解析:
hh1--1 //程序刚从按钮进入,按钮下线程是1
hh2—3 //已经启动新线程,线程一进去就执行hh2,新线程是3
hh5—1 //由于是Task.Run启用的线程,所以不等待直接执行hh5,hh5是在按钮下不在线程内,所以线程还是1
hh3—3 //线程等待了2秒,在新线程里执行hh3, 新线程还是3
hh4—4 //线程完成后执行"回调"hh4,hh4有可能是新线程,也可能是按钮下线程。随机性。
上面代码与下面是等价的:
private void button8_Click(object sender, EventArgs e)
{
Console.WriteLine($"hh1--{Thread.CurrentThread.ManagedThreadId}");
ReturnTask(); //这里会警告,因为调用async函数没写await。不过不影响执行
Console.WriteLine($"hh6--{Thread.CurrentThread.ManagedThreadId}");
}
public async Task ReturnTask()
{
Console.WriteLine($"hh2--{Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
Console.WriteLine($"hh3--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
Console.WriteLine($"hh4--{Thread.CurrentThread.ManagedThreadId}");
});
await task;
Console.WriteLine($"hh5--{Thread.CurrentThread.ManagedThreadId}");
}
结果及其解析:
hh1--1 //程序刚从按钮进入,按钮下线程是1
hh2--1 //进入函数执行hh2,线程仍然是1
hh3--3 //已经启动新线程,线程一进去就执行hh3,新线程是3
hh6--1 //遇到await,程序会把await后面的代码包装成一个回调委托,待线程执行完再执行,然后直接返回函数,执行hh6,线程是按钮下线程是1
hh4--3 //线程等待了2秒,在新线程里执行hh4, 新线程还是3
hh5--1 //线程完成后执行await后面的代码类似"回调"hh5,hh4有可能是新线程,也可能是按钮下线程。随机性。
扩展例子
private void button8_Click(object sender, EventArgs e)
{
Console.WriteLine($"hh1--{Thread.CurrentThread.ManagedThreadId}");
ReturnTask();
Console.WriteLine($"hh6--{Thread.CurrentThread.ManagedThreadId}");
}
public async Task ReturnTask()
{
Console.WriteLine($"hh2--{Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
Console.WriteLine($"hh3--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
Console.WriteLine($"hh4--{Thread.CurrentThread.ManagedThreadId}");
});
await task; //遇到await,返回主函数,并把下面所有代码封装成task的回调
Console.WriteLine($"hh5--{Thread.CurrentThread.ManagedThreadId}");
Task task2 = Task.Run(() =>
{
Console.WriteLine($"hh7--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
Console.WriteLine($"hh8--{Thread.CurrentThread.ManagedThreadId}");
});
await task2; //遇到await把下面所有代码封装成task2的回调
Console.WriteLine($"hh9--{Thread.CurrentThread.ManagedThreadId}");
}
结果:
hh8--3
hh9--1
hh1--1
hh2--1
hh3--3
hh6--1
hh4--3
hh5--1
hh7--4
hh8--4
hh9—1
规律:在一个函数中有多个await时,它是从上往下执行。即用同步的方法写异步
四、任务超时取消任务
CancellationTokenSource:假如执行时间过长,可以设置取消任务
例子:在任务循环中通过代码判断:
private async void button1_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
this.Text=await WriteAllText(40000, cts.Token);
}
public Task<string> WriteAllText(int Count,CancellationToken mCancellationToken)
{
return Task.Run(() => mWriteText(Count, mCancellationToken) );
//Task<string> t =Task.Run(( ) => mWriteText(Count));
//string html = await t;
//return html;
}
public string mWriteText(int mCount, CancellationToken mCancellationToken)
{
string S = "";
for (int i = 0; i < mCount; i++)
{
S = S + "iiiiiiiiiiii";
if (mCancellationToken.IsCancellationRequested)
{
return "任务超时,已被取消" ;
}
}
File.WriteAllText("1.txt",S);
return DateTime.Now.ToString("HH:mm:ss");
}
也可以手动取消任务
CancellationTokenSource cts = new CancellationTokenSource();
private void button3_Click(object sender, EventArgs e)
{
cts.Cancel();
}
private async void button1_Click(object sender, EventArgs e)
{
cts.CancelAfter(5000);
this.Text = await WriteAllText(40000, cts.Token);
}
五、多任务等待
1、WhenAny:集合中任何一个任务完成Task就完成
2、WhenAll:所有任务完成Tsak才完成,用于等待多任务都执行结束,不在乎它们的执行顺序
例:
private async void button1_Click(object sender, EventArgs e)
{
cts.CancelAfter(50000);
Task<string> t1 = WriteAllText(10000, cts.Token);
Task<string> t2 = WriteAllText(20000, cts.Token);
Task<string> t3 = WriteAllText(30000, cts.Token);
string[] rets = await Task.WhenAll(t1, t2, t3);
string rets1 = rets[0];
string rets2 = rets[1];
string rets3 = rets[2];
this.Text = rets1 + "," + rets2 + "," + rets3;
}
public Task<string> WriteAllText(int Count, CancellationToken mCancellationToken)
{
return Task.Run(() => mWriteText(Count, mCancellationToken));
//Task<string> t =Task.Run(( ) => mWriteText(Count));
//string html = await t;
//return html;
}
public string mWriteText(int mCount, CancellationToken mCancellationToken)
{
string S = "";
for (int i = 0; i < mCount; i++)
{
S = S + "iiiiiiiiiiii";
if (mCancellationToken.IsCancellationRequested)
{
return "任务超时,已被取消";
}
}
File.WriteAllText("1.txt", S);
return DateTime.Now.ToString("HH:mm:ss");
}
注:WhenAll也可以接受一个IEnumerable数组形式的Task<T>等多种函数。可能会用到yield关键字来构建IEnumerable数组。即以下两种方式实现效果是一样的
方式一:
static IEnumerable<string> Test1()
{
List<string> list = new List<string>();
list.Add("hh1");
list.Add("hh2");
list.Add("hh3");
return list;
}
方式二:
static IEnumerable<string> Test2()
{
yield return "hh1";
yield return "hh2";
yield return "hh3";
}
六、多线程安全
线程安全问题是,一段代码,单线程顺序执行的时候是完全没问题的,但是一放到多线程里就会得到意象不到的结果。一般在多线程同时修改一个对象时
测试例子1
1、多线程假如不加Await,它是不会等待的,如下,输出i结果所有都是5,因为i变量只有一个,从而系统值开辟了
一个内存空间给i,程序是先把for循环执行完,才执行线程内的代码。所以i一直是5
private void button4_Click(object sender, EventArgs e)
{
for (int i = 0; i < 5; i++)
{
Task.Run(() =>
{
Console.WriteLine($"{i}Star");
Thread.Sleep(2000);
Console.WriteLine($"{i}End");
});
}
}
- 假如要修改上面输出i结果,使得每次输出跟着循环变量变化而变化,可以这样改进,让系统每次循环都开辟一个独自的内存空间:
private void button4_Click(object sender, EventArgs e)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
Console.WriteLine($"{k}Star");
Thread.Sleep(2000);
Console.WriteLine($"{k}End");
});
}
}
测试例子2
如下代码,在线程中对数组进行添加10000次,待全部完成后,数组的个数会少于10000个。这是由于数组是一个连续的内存,多线程在并发时,可能对同一内存被不同的线程写了两次。
private void button5_Click(object sender, EventArgs e)
{
List<int> intList = new List<int>();
for(int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
intList.Add(i);
});
}
}
- 解决办法:Lock单线程化,每次线程进来都对Lock块作用域内进行加锁,其他线程在外面等待。待执行完作用域块完后,系统会自动释放说,其他线程才能进来。
//保证方法块只有一个线程运行,其他线程在外面等待
private static readonly object hhLock = new object();
private void button5_Click(object sender, EventArgs e)
{
List<int> intList = new List<int>();
for(int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (hhLock)
{
intList.Add(i);
}
});
}
}
- 共用Lock,如下两个For循环共用一个Lock对象,这样程序会先执行完第一个For的所有线程,再执行第二个for的所有线程
private void button6_Click(object sender, EventArgs e)
{
for (int i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock)
{
Console.WriteLine($"{k}Star1");
Thread.Sleep(2000);
Console.WriteLine($"{k}End1");
}
});
}
for (int i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock)
{
Console.WriteLine($"{k}Star2");
Thread.Sleep(2000);
Console.WriteLine($"{k}End2");
}
});
}
}
结果是:
0Star1
0End1
1Star1
1End1
0Star2
0End2
1Star2
1End2
- 假如想第一个For与第二个For并发的话,可以使用不同的Lock对象
private static readonly object hhLock2 = new object();
private static readonly object hhLock = new object();
private void button6_Click(object sender, EventArgs e)
{
for (int i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock)
{
Console.WriteLine($"{k}Star1");
Thread.Sleep(2000);
Console.WriteLine($"{k}End1");
}
});
}
for (int i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock2)
{
Console.WriteLine($"{k}Star2");
Thread.Sleep(2000);
Console.WriteLine($"{k}End2");
}
});
}
}
结果是:
1Star1
0Star2
1End1
0Star1
0End2
1Star2
0End1
1End2
- 泛型类时,如下,Show(1)与Show(2)时不可以并发的,即要么Show(1)进入锁区块内要么Show(2)进入。不可以同时进入。因为使用泛型同类型<int>时,其内部静态变量是共用的。
private void button7_Click(object sender, EventArgs e)
{
huanghai<int>.Show(1);
huanghai<int>.Show(2);
huanghai<int>.Show(2);
}
public class huanghai<T>
{
private static readonly object hhLock2 = new object();
public static void Show(int index)
{
for (int i = 0; i< 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock2) {
Console.WriteLine($"{k}Star{index}");
Thread.Sleep(2000);
Console.WriteLine($"{k}End{index}");
}
});
}
}
}
结果:
2Star1
2End1
0Star1
0End1
1Star2
1End2
4Star2
4End2
1Star1
1End1
3Star1
3End1
4Star1
4End1
0Star2
0End2
2Star2
2End2
3Star2
3End2
- 假如上述这样调用,两个就可以并发:
huanghai<int>.Show(1);
huanghai<string>.Show(2);
- 是否能并发,就看lock ()内的变量是否是用同一个。