- 异步方法用async关键字修饰的方法
- 异步方法的返回值一般是Task<T>,T是真是的返回值类型,如Task<int>
- 自定义的异步方法命名时一般用Async结尾,便于后续维护时一眼就可以看出来。一些系统自带的异步函数也是一Async结尾的。如httpclient.GetStringAsync
- 异步方法具有传染性,即一个方法内使用了await,则这个方法也必须用async修饰,即若这个方法是有返回值的则加async Task<T>修饰,没有返回值则写async Task修饰
- 下面是方法体内写了await,函数没加async修饰的,所以会报错
- 正确写法:
staticasyncstringstring filename)
{
usingnew HttpClient())
{
string> t = httpclient.GetStringAsync(url);
stringawait t;
}
}
- 下面由于Main中调用了await且无返回值,所以也需要加async Task修饰
1. staticasyncstring[] args)
{
intawait"https://www.baidu.com/", "1.txt");
Console.WriteLine(leng);
Console.ReadKey();
}
staticasyncint> DownLoadHtmlAsync(stringstring filename)
{
usingnew HttpClient())
{
string> t = httpclient.GetStringAsync(url);
stringawait t;
File.WriteAllText(filename, html);
return html.Length;
}
}- 加了Await后,程序会等待线程结束,但是界面不会卡主
- 假如一些函数中它调用了await,但是它不支持async修饰,或者不想要async修饰,可以修改成一下(尽量不要这样写,有死锁风险,加了这个,程序运行到.Result时会阻塞线程,一直等待到结果):
- 有返回值
1. staticvoidstring[] args)//不加async修饰
{
int> t=DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");
int leng = t.Result;
Console.WriteLine(leng);
Console.ReadKey();
}
staticasyncint> DownLoadHtmlAsync(stringstring filename)
{
usingnew HttpClient())
{
string> t = httpclient.GetStringAsync(url);
stringawait t;
File.WriteAllText(filename, html);
return html.Length;
}
}- 没返回值
staticvoidstring[] args)//不加async修饰
{
"https://www.baidu.com/", "1.txt");
t.Wait();
Console.ReadKey();
}
staticasyncstringstring filename)
{
usingnew HttpClient())
{
string> t = httpclient.GetStringAsync(url);
stringawait t;
File.WriteAllText(filename, html);
}
}- 在winform程序中也可以调用,不过button1_Click下不需要把void改成Task,因为要响应事件
privateasyncvoidobject sender, EventArgs e)
{
intawait"https://www.baidu.com/", "1.txt");
}- 带返回值委托调用异步:
1. Func<int, Task<string>> DD2 = asyncdelegateint g)
{
"hh" + g);
await"https://www.baidu.com/", "1.txt");
return"hh" + g;
};//指向
Task<string> t = DD2(4);
string ret = t.Result;- 不带返回值委托调用异步:
Action<int> DD2 = asyncdelegateint g)
{
await"https://www.baidu.com/", "1.txt");
};//指向
DD2(4);//调用二、原理
用ILspy反编译可以看到(要用低版本的.NET),可以反编译后,其实系统帮我们把这个await里的代码分成了若干块,通过switch(num)来执行每块内容,全部执行完后返回

三、自定义异步函数
上述的GetStringAsync都是系统自带的异步函数,实际应用中可以使用Task.Run来自定义异步函数
- 无传入参数,无传出参数
1. privateasyncvoidobject sender, EventArgs e)
{
await WriteAllText( ) ;
}
publicasync Task WriteAllText()
{
string"";
await Task.Run(() =>
{
forint i = 0; i < 10000; i++)
{
"iiiiiiiiiiii";
}
"1.txt");
});
}- 有传入参数,有传出参数
1. privateasyncvoidobject sender, EventArgs e)
{
this.Text=await WriteAllText(900000) ;
}
publicasyncstring> WriteAllText(int Count)
{
string> t =Task.Run(( ) => mWriteText(Count));
stringawait t;
return html;
}
publicstringint mCount)
{
string"";
forint i = 0; i < mCount; i++)
{
"iiiiiiiiiiii";
}
"1.txt",S);
return"333";
}注上述的也可以这样写,并且建议这样写。可以去掉async变得更加简洁
publicstring> 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方法来实现
privatevoidobject sender, EventArgs e)
{
$"hh1--{Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
$"hh2--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
$"hh3--{Thread.CurrentThread.ManagedThreadId}");
});
task.ContinueWith((t) =>
{
$"hh4--{Thread.CurrentThread.ManagedThreadId}");
});
$"hh5--{Thread.CurrentThread.ManagedThreadId}");
}结果及其解析:
hh1--1
hh2—3 //已经启动新线程,线程一进去就执行hh2,新线程是3
hh5—1 //由于是Task.Run启用的线程,所以不等待直接执行hh5,hh5是在按钮下不在线程内,所以线程还是1
hh3—3 //线程等待了2秒,在新线程里执行hh3, 新线程还是3
hh4—4 //线程完成后执行"回调"hh4,hh4有可能是新线程,也可能是按钮下线程。随机性。
上面代码与下面是等价的:
privatevoidobject sender, EventArgs e)
{
$"hh1--{Thread.CurrentThread.ManagedThreadId}");
//这里会警告,因为调用async函数没写await。不过不影响执行
$"hh6--{Thread.CurrentThread.ManagedThreadId}");
}
publicasync Task ReturnTask()
{
$"hh2--{Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
$"hh3--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
$"hh4--{Thread.CurrentThread.ManagedThreadId}");
});
await task;
$"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有可能是新线程,也可能是按钮下线程。随机性。
扩展例子
privatevoidobject sender, EventArgs e)
{
$"hh1--{Thread.CurrentThread.ManagedThreadId}");
ReturnTask();
$"hh6--{Thread.CurrentThread.ManagedThreadId}");
}
publicasync Task ReturnTask()
{
$"hh2--{Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
$"hh3--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
$"hh4--{Thread.CurrentThread.ManagedThreadId}");
});
await
$"hh5--{Thread.CurrentThread.ManagedThreadId}");
Task task2 = Task.Run(() =>
{
$"hh7--{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
$"hh8--{Thread.CurrentThread.ManagedThreadId}");
});
await
$"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:假如执行时间过长,可以设置取消任务
例子:在任务循环中通过代码判断:
privateasyncvoidobject sender, EventArgs e)
{
new CancellationTokenSource();
cts.CancelAfter(5000);
this.Text=await WriteAllText(40000, cts.Token);
}
publicstring> WriteAllText(int Count,CancellationToken mCancellationToken)
{
return Task.Run(() => mWriteText(Count, mCancellationToken) );
//Task<string> t =Task.Run(( ) => mWriteText(Count));
//string html = await t;
//return html;
}
publicstringint mCount, CancellationToken mCancellationToken)
{
string"";
forint i = 0; i < mCount; i++)
{
"iiiiiiiiiiii";
if (mCancellationToken.IsCancellationRequested)
{
return"任务超时,已被取消" ;
}
}
"1.txt",S);
return"HH:mm:ss");
}也可以手动取消任务
CancellationTokenSource cts = new CancellationTokenSource();
privatevoidobject sender, EventArgs e)
{
cts.Cancel();
}
privateasyncvoidobject sender, EventArgs e)
{
cts.CancelAfter(5000);
this.Text = await WriteAllText(40000, cts.Token);
}五、多任务等待
1、WhenAny:集合中任何一个任务完成Task就完成
2、WhenAll:所有任务完成Tsak才完成,用于等待多任务都执行结束,不在乎它们的执行顺序
例:
privateasyncvoidobject sender, EventArgs e)
{
cts.CancelAfter(50000);
string> t1 = WriteAllText(10000, cts.Token);
string> t2 = WriteAllText(20000, cts.Token);
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 + ",""," + rets3;
}
publicstring> WriteAllText(int Count, CancellationToken mCancellationToken)
{
return Task.Run(() => mWriteText(Count, mCancellationToken));
//Task<string> t =Task.Run(( ) => mWriteText(Count));
//string html = await t;
//return html;
}
publicstringint mCount, CancellationToken mCancellationToken)
{
string"";
forint i = 0; i < mCount; i++)
{
"iiiiiiiiiiii";
if (mCancellationToken.IsCancellationRequested)
{
return"任务超时,已被取消";
}
}
"1.txt", S);
return"HH:mm:ss");
}注:WhenAll也可以接受一个IEnumerable数组形式的Task<T>等多种函数。可能会用到yield关键字来构建IEnumerable数组。即以下两种方式实现效果是一样的
方式一:
staticstring> Test1()
{
string> list = newstring>();
"hh1");
"hh2");
"hh3");
return list;
}方式二:
staticstring> Test2()
{
yieldreturn"hh1";
yieldreturn"hh2";
yieldreturn"hh3";
}六、多线程安全
线程安全问题是,一段代码,单线程顺序执行的时候是完全没问题的,但是一放到多线程里就会得到意象不到的结果。一般在多线程同时修改一个对象时
测试例子1
1、多线程假如不加Await,它是不会等待的,如下,输出i结果所有都是5,因为i变量只有一个,从而系统值开辟了
一个内存空间给i,程序是先把for循环执行完,才执行线程内的代码。所以i一直是5
privatevoidobject sender, EventArgs e)
{
forint i = 0; i < 5; i++)
{
Task.Run(() =>
{
$"{i}Star");
Thread.Sleep(2000);
$"{i}End");
});
}
}- 假如要修改上面输出i结果,使得每次输出跟着循环变量变化而变化,可以这样改进,让系统每次循环都开辟一个独自的内存空间:
1. privatevoidobject sender, EventArgs e)
{
forint i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
$"{k}Star");
Thread.Sleep(2000);
$"{k}End");
});
}
}测试例子2
如下代码,在线程中对数组进行添加10000次,待全部完成后,数组的个数会少于10000个。这是由于数组是一个连续的内存,多线程在并发时,可能对同一内存被不同的线程写了两次。
voidobject sender, EventArgs e)
{
int> intList = newint>();
for(int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
intList.Add(i);
});
}
}- 解决办法:Lock单线程化,每次线程进来都对Lock块作用域内进行加锁,其他线程在外面等待。待执行完作用域块完后,系统会自动释放说,其他线程才能进来。
1. //保证方法块只有一个线程运行,其他线程在外面等待
privatestaticreadonlyobjectnewobject();
privatevoidobject sender, EventArgs e)
{
int> intList = newint>();
for(int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (hhLock)
{
intList.Add(i);
}
});
}
}- 共用Lock,如下两个For循环共用一个Lock对象,这样程序会先执行完第一个For的所有线程,再执行第二个for的所有线程
privatevoidobject sender, EventArgs e)
{
forint i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock)
{
$"{k}Star1");
Thread.Sleep(2000);
$"{k}End1");
}
});
}
forint i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock)
{
$"{k}Star2");
Thread.Sleep(2000);
$"{k}End2");
}
});
}
}- 结果是:
0Star1
0End1
1Star1
1End1
0Star2
0End2
1Star2
1End2- 假如想第一个For与第二个For并发的话,可以使用不同的Lock对象
privatestaticreadonlyobjectnewobject();
privatestaticreadonlyobjectnewobject();
privatevoidobject sender, EventArgs e)
{
forint i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock)
{
$"{k}Star1");
Thread.Sleep(2000);
$"{k}End1");
}
});
}
forint i = 0; i < 2; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock2)
{
$"{k}Star2");
Thread.Sleep(2000);
$"{k}End2");
}
});
}
}- 结果是:
1.
1Star1
0Star2
1End1
0Star1
0End2
1Star2
0End1
1End2- 泛型类时,如下,Show(1)与Show(2)时不可以并发的,即要么Show(1)进入锁区块内要么Show(2)进入。不可以同时进入。因为使用泛型同类型<int>时,其内部静态变量是共用的。
privatevoidobject sender, EventArgs e)
{
int>.Show(1);
int>.Show(2);
int>.Show(2);
}
publicclasshuanghai<T>
{
privatestaticreadonlyobjectnewobject();
publicstaticvoidint index)
{
forint i = 0; i< 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (hhLock2) {
$"{k}Star{index}");
Thread.Sleep(2000);
$"{k}End{index}");
}
});
}
}
}结果:
2Star1
2End1
0Star1
0End1
1Star2
1End2
4Star2
4End2
1Star1
1End1
3Star1
3End1
4Star1
4End1
0Star2
0End2
2Star2
2End2
3Star2
3End2- 假如上述这样调用,两个就可以并发:
int>.Show(1);
string>.Show(2);- 是否能并发,就看lock ()内的变量是否是用同一个。
















