none
同步调用async的方法,这种方式是否可行?

    问题

  • 刚开始学习await/async的使用,看了很多资料,都说同步调用异步的方法的话可能会引起死锁,也看到了解决的方式(task.Wait()改为task.ConfigureAwait( false )的方式)。在实践的时候我发现一种用法,看起来似乎也没有问题,不知道会不会引起死锁?代码如下(WinForm环境):

    		private void btnAsync_Click( object sender, EventArgs e )
    		{
    			textBox1.Text = "点了异步按钮。";
    			//var dt = await ExecuteTableAsync( "select * from Users" );
    			//textBox1.Text += "读取完毕,行数:" + dt.Result.Rows.Count.ToString();
    			GetDataAsync( rc => textBox1.Text += "读取完毕,行数:" + rc.ToString() );
    		}
    
    		public async Task<DataTable> ExecuteTableAsync(string sql)
    		{
    			DataTable result = null;
    
    			using( SqlConnection conn = new SqlConnection() )
    			{
    				conn.ConnectionString = "...";
    				await conn.OpenAsync();
    				using( SqlCommand cmd = new SqlCommand( sql ) )
    				{
    					cmd.Connection = conn;
    					using( var dr = await cmd.ExecuteReaderAsync() )
    					{
    						result = new DataTable();
    						await Task.Run( () =>
    						{
    							result.Load( dr );
    						} );
    					}
    				}
    			}
    
    			return result;
    		}
    
    		private async Task GetDataAsync(Action<int> callback)
    		{
    			var dt = await ExecuteTableAsync( "select * from Users" );//一个会阻塞大约10秒的查询
    
    			if( callback != null )
    			{
    				callback( dt.Rows.Count );
    			}
    		}

    希望对这两个关键字及异步编程方式理解得更深的各位前辈帮忙解惑,先谢过。

    2016年6月16日 6:58

答案

全部回复

  • Hi,

    你不加await关键字调用的话,编译器就把这个方法作为同步方法调用了,你的代码我测试了下死锁倒是不会。

    Regards,

    Moonlight


    We are trying to better understand customer views on social support experience, so your participation in this interview project would be greatly appreciated if you have time. Thanks for helping make community forums a great place.
    Click HERE to participate the survey.


    2016年6月17日 6:15
  • Hi,

    你不加await关键字调用的话,编译器就把这个方法作为同步方法调用了,你的代码我测试了下死锁倒是不会。

    Regards,

    Moonlight


    多谢答疑,不加await会变成同步调用这个我是知道,VS给了提示,但同时也很疑惑,为什么变成同步调用了以后,我这段代码也不会阻塞UI呢?希望您能继续讲解一下,非常感谢。
    2016年6月17日 8:04
  • ?????

    • 已标记为答案 aben008 2016年6月21日 3:38
    • 已编辑 [-] 2018年1月11日 12:52
    2016年6月17日 11:08
  • 不加await叫做并行异步,加了await叫做线程同步。await是个语法糖,不是真正的feature,最终还是任务链。你这里是并行异步然后回调,这种操作是毫无意义的,增加了很多不必要的构造,性能和内存占用都受损失,直接用await就好了。对于一个异步方法除了使用Task中Wait开头的方法外是没有其它方式来实现同步调用的,因为返回值是Task或Task<T>,而不是void,所以根本不存在不加await时为同步调用一说。await线程同步不是同步调用,而是在异步完成之后回调到原线程上,实际上你手写的那段GetDataAsync正式await在编译时自动生成的东西。所以说你实际上做了无用功,await完成的就是这项工作。当然你也可以手动使用任务链形式:

    Task.Factory.StartNew(...).ContinueWith(...);

    https://msdn.microsoft.com/zh-cn/library/dd321405(v=vs.110).aspx

    补充:因为不太清除你对“同步调用”一词的定义,两种可能都给你列出来。

    1. 如果你所说的同步调用是指在当前线程上运行或阻塞当前线程。

    只有Task中的Wait开头的方法可以实现。如果异步尚未开始则尝试在当前线程上内联,否则就阻塞。

    2. 如果你所说的同步调用是指运行完异步方法后返回当前线程。

    那么加上await是“同步调用”,不加则是异步。一般这种情况不能被称之为同步调用,所以我打了引号。具体原理的请参看任务链方面的知识。






    多谢您的解答。

    我所说的同步调用意思是阻塞了当前的线程,原帖里的测试代码里面有注释掉的两句,是最初为了测试异步效果的,就是看看会不会在查询时阻塞当前线程,造成UI无响应之类的(原先标记按钮事件的async去掉了)。

    根据您的说明,我测试了一下 Task.Factory.StartNew(...).ContinueWith(...); 的方式,发现这样是会阻塞UI的。如果说await会在编译时生成类似GetDataAsync的实现方式,那么我应如何在没有async的方法内实现查询时不阻塞UI呢?能不能像调用GetDataAsync一样显式的定义一个回调函数呢?

    2016年6月20日 2:09
  • 如果一个方法(假设为DoSomething)没有async修饰符,那它就没办法使用await关键词,这里可能有两种情况。

    1. 如果这个方法内部不涉及UI,你可以直接使用同步方式。然后在调用该方法时使用Task.Run包裹,并对该Task进行await。例如:await Task.Run(() => { DoSomething(); });

    2. 如果这个方法内部涉及到了UI,那就不得不给它添加async修饰符。如我前面所说async/await这个语法糖实际上就是自动完成你所需的回调,而且还自动回到之前的线程上。所以如果你有权为一个方法添加回调函数作为参数,那你肯定也有权为其添加async修饰符,尽管二者均可行,但肯定是直接添加async比较省事,否则在回调中还要自己手动切换到UI线程。

    噢!通过对您回复的理解和实践,现在总算搞懂了前面说的“异步方法被同步调用”是什么意思了。我期望的是这样,比如下面的代码:

    static void Main( string[] args )
    {
    	Program p = new Program();
    	p.Do1();
    	Console.WriteLine( "main end" );
    	Console.ReadKey();
    }
    
    public void Do1()
    {
    	Console.WriteLine( "begin" );
    	var x = Do2();
    	Console.WriteLine( "end" );
    	Console.WriteLine( x.Result );
    }
    
    public async Task<string> Do2()
    {
    	await Task.Delay( 5000 );
    	Console.WriteLine( "do2 delay" );
    	return "do2 end";
    }

    我期望的输出是:

    begin

    end

    main end

    (延时5秒)

    do2 delay

    do2 end

    实际的输出当然是:

    begin

    end

    (延时5秒)

    do2 delay

    do2 end

    main end

    也就是说,由于没有await Do2,所以Do2方法是同步执行的,延时了5秒。想要实现我期望的输出,就得为Do1方法添加async关键字。

    但在winform程序里面,调用了btn_click方法的是事件代理,我用ILSpy查看System.Windows.Forms.Button的源码,跟踪到System.Windows.Forms.Control,发现事件处理的代码也并没有await/async关键字对:

    // System.Windows.Forms.Control
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    protected virtual void OnClick(EventArgs e)
    {
    	EventHandler eventHandler = (EventHandler)base.Events[Control.EventClick];
    	if (eventHandler != null)
    	{
    		eventHandler(this, e);
    	}
    }

    为什么不会同步调用btn_click方法呢(我以为会是await eventHandler(this,e);的方式进行调用)?如果是自定义的事件,是否也能同样实现所谓的“非async方法调用async方法”的效果呢?


    • 已编辑 aben008 2016年6月20日 10:54
    2016年6月20日 10:49
  • Do2并不是同步执行的,而是并行异步。如果一个函数里面调用了其它异步函数,比如Do2中的Task.Delay,这时就会另行开启一个线程,如果有await则当这个异步函数执行完之后继续执行后面的语句,如果没有await则后面的语句立即继续执行,与Task中的那些一起并行执行。eventHandler后面已经没有其它语句了,因此加不加await是一样的。await方便使用,但并不方便理解,如果想理解它的工作原理必须去理解“任务链”。await最终实际上都会被编译器转换成任务链,任务链才是真正的逻辑过程。

    比如:

    public async Task<string> Do2()
    {
    	await Task.Delay( 5000 );
    	Console.WriteLine( "do2 delay" );
    	return "do2 end";
    }

    相当于

    public Task<string> Do2()
    {
        return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
        }).ContinueWith(antecendent =>
        {
            Console.WriteLine("do2 delay");
            return "do2 end";
        });
    }


    如果不加await

    public string Do2()
    {
    	Task.Delay( 5000 );
    	Console.WriteLine( "do2 delay" );
    	return "do2 end";
    }

    则相当于

    public string Do2()
    {
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
        });
        Console.WriteLine("do2 delay");
        return "do2 end";
    }




    太感谢了,这两个例子很典型,一下子就明白了“并行异步”的表现效果。任务链我也试了试,貌似与await都一样是在线程里执行的?搜了一下没搜到什么资料,我再看看吧。

    再次致谢!

    2016年6月21日 3:36