c#使用异步

  1. 异步方法用async关键字修饰的方法
  2. 异步方法的返回值一般是Task<T>,T是真是的返回值类型,如Task<int>
  3. 自定义的异步方法命名时一般用Async结尾,便于后续维护时一眼就可以看出来。一些系统自带的异步函数也是一Async结尾的。如httpclient.GetStringAsync
  4. 异步方法具有传染性,即一个方法内使用了await,则这个方法也必须用async修饰,即若这个方法是有返回值的则加async Task<T>修饰,没有返回值则写async Task修饰
    1. 下面是方法体内写了await,函数没加async修饰的,所以会报错

    2. 正确写法:

    static async Task DownLoadHtmlAsync(string url,string filename)

    {

    using (HttpClient httpclient = new HttpClient())

    {

    Task<string> t = httpclient.GetStringAsync(url);

    string html = await t;

    }

    }

  5. 下面由于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;

    }

    }

  6. 加了Await后,程序会等待线程结束,但是界面不会卡主
  7. 假如一些函数中它调用了await,但是它不支持async修饰,或者不想要async修饰,可以修改成一下(尽量不要这样写,有死锁风险,加了这个,程序运行到.Result时会阻塞线程,一直等待到结果):
    1. 有返回值

      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;

      }

    }

    1. 没返回值

      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);

      }

      }

  8. 在winform程序中也可以调用,不过button1_Click下不需要把void改成Task,因为要响应事件

private async void button1_Click(object sender, EventArgs e)

{

int leng = await DownLoadHtmlAsync("https://www.baidu.com/", "1.txt");

}

  1. 带返回值委托调用异步:

    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;

  2. 不带返回值委托调用异步:

    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来自定义异步函数

  1. 无传入参数,无传出参数

    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");

    });

}

  1. 有传入参数,有传出参数

    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

hh23 //已经启动新线程,线程一进去就执行hh2,新线程是3

hh51 //由于是Task.Run启用的线程,所以不等待直接执行hh5,hh5是在按钮下不在线程内,所以线程还是1

hh33 //线程等待了2秒,在新线程里执行hh3, 新线程还是3

hh44 //线程完成后执行"回调"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

hh91

 

规律:在一个函数中有多个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");

});

}

}

  1. 假如要修改上面输出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);

});

}

}

  1. 解决办法: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);

    }

    });

    }

    }

  2. 共用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

  3. 假如想第一个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

  4. 泛型类时,如下,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

  1. 假如上述这样调用,两个就可以并发:

huanghai<int>.Show(1);

huanghai<string>.Show(2);

  1. 是否能并发,就看lock ()内的变量是否是用同一个。
  2.  
posted @ 2022-09-13 17:47  ihh2021  阅读(175)  评论(0编辑  收藏  举报