下面是我的代码:
SELECT DAY
FROM RESERVE R
WHERE NOT EXISTS (
SELECT SID
FROM SAILOR S
EXCEPT
SELECT S.SID
FROM SAILOR S, RESERVE R
WHERE S.SID = R.SID)
GROUP BY DAY;
输出为
1
// 3秒后
3
完成工作!
所以您可以看到主线程(id为1)更改为工作线程(id为3),那么主线程怎么会消失?
答案 0 :(得分:2)
异步入口点只是一个编译器技巧。在后台,编译器会生成以下实际入口点:
private static void <Main>(string[] args)
{
_Main(args).GetAwaiter().GetResult();
}
如果将代码更改为这样:
class Program
{
private static void Main(string[] args)
{
MainAsync(args).GetAwaiter().GetResult();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
static async Task MainAsync(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
string message = await DoWorkAsync();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
}
static async Task<string> DoWorkAsync()
{
await Task.Delay(3_000);
return "Done with work!";
}
}
您会得到的:
1
4
Done with work!
1
按预期,主线程正在等待工作完成。
答案 1 :(得分:1)
在您的代码中,一旦在此处调用await
,主线程便结束:
string message = await DoWorkAsync();
自DoWorkAsync()
创建任务以来执行会有所不同,并且此调用之后的所有代码都将在新创建的任务中执行,因此调用await DoWorkAsync();
后主线程无事可做将完成。
答案 2 :(得分:1)
这是您选择的应用程序类型的结果。控制台应用程序和GUI应用程序在SynchronizationContext
方面的行为有所不同。使用await
时,当前的SynchronizationContext
被捕获并传递给后台线程。
这个想法不是通过等待后台线程完成来阻塞主线程。其余代码入队,当前上下文存储在背景线程将捕获的SynchronizationContext
中。后台线程完成后,它返回捕获的SynchronizationContext
,以便排队的剩余代码可以恢复执行。您可以通过访问SynchronizationContext.Current
属性来获取当前上下文。等待await
完成的代码(await
之后的其余代码)将作为继续,并在捕获的SynchronizationContext
上执行。
SynchronizationContext.Current
的默认值是用于GUI应用程序的UI线程,例如WPF或用于控制台应用程序的NULL。控制台应用程序没有SynchronizationContext
,因此为了能够使用async
,框架使用了ThreadPool
SynchronizationContext
。 SynchronizationContext
行为的规则是
SynchronizationContext.Current
返回NULL,则
延续线程将默认为线程池线程SynchronizationContext.Current
不为NULL,则继续
将在捕获的上下文中执行。await
用于后台线程(因此,
从后台线程启动后台线程),然后
SynchronizationContext
将始终是线程池线程。 场景1,控制台应用程序:
规则1)适用:线程1 调用await
,它将尝试捕获当前上下文。 await
将使用ThreadPool
中的后台线程线程3 执行异步委托。
委托完成后,调用线程的其余代码将在捕获的上下文中执行。由于此上下文在控制台应用程序中为NULL,因此默认的SynchronizationContext
将生效(第一条规则)。因此,调度程序决定继续在ThreadPool
线程线程3 上执行(出于效率考虑。上下文切换非常昂贵)。
方案2,GUI应用程序:
规则2)适用:线程1 调用await
,它将尝试捕获当前上下文(UI SynchronizationContext
)。 await
将使用ThreadPool
中的后台线程线程3 执行异步委托。
委托完成后,调用线程的其余代码将在捕获的上下文UI SynchronizationContext
线程1 上执行。
场景3,一个GUI应用程序和Task.ContinueWith
:
规则2)和规则3)适用:线程1 调用await
,它将尝试捕获当前上下文(UI SynchronizationContext
)。 await
将使用ThreadPool
中的后台线程线程3 来执行异步委托。委托完成后,继续TaskContinueWith
。由于我们仍在后台线程中,因此将新的TreadPool
线程线程4 与捕获的线程3 的SynchronizationContext
一起使用。一旦继续操作完成,上下文将返回到线程3 ,它将在捕获的SynchronizationContext
(即UI线程线程1 )上执行调用者的其余代码。 / p>
方案4,一个GUI应用程序和Task.ConfigureAwait(false)
(await DoWorkAsync().ConfigureAwait(false);
):
规则1)适用:线程1 调用await
并在ThreadPool
后台线程线程3 上执行异步委托。但是由于该任务是使用Task.ConfigureAwait(false)
线程3 配置的,因此无法捕获调用方(UI SynchronizationContext
)的SynchronizationContext
。因此,线程3的SynchronizationContext.Current
属性将为NULL,并且将应用默认的SynchronizationContext
:上下文将是ThreadPool
线程。由于性能优化(上下文切换非常昂贵),因此上下文将成为线程3 的当前SynchronizationContext
。这意味着一旦线程3 完成,则将在默认的SynchronizationContext
线程3 上执行调用程序的其余代码。默认的Task.ConfigureAwait
值为true
,可以捕获呼叫者SynchronizationContext
。
方案5,GUI应用程序和Task.Wait
,Task.Result
或Task.GetAwaiter.GetResult
:
规则2适用,但应用程序将死锁。捕获了线程1 的当前SynchronizationContext
。但是由于异步委托是同步执行的(Task.Wait
,Task.Result
或Task.GetAwaiter.GetResult
会将异步操作转换为委托的同步执行),因此 thread 1 将阻止,直到现在的同步委托完成。
由于该代码是同步执行的,因此 thread 1 的其余代码不会作为 thread 3 的延续而入队,因此,一旦 thread 1 执行,委托完成。既然线程3上的委托完成了,它就不能将线程1的SynchronizationContext
返回到线程1了,因为线程1 仍处于阻塞状态(因此锁定了SynchronizationContext
)。 线程3 将无限期等待线程1 释放对SynchronizationContext
的锁定,从而使线程1 无限期等待< em>线程3 返回->死锁。
场景6,控制台应用程序和Task.Wait
,Task.Result
或Task.GetAwaiter.GetResult
:
规则1适用。捕获了线程1 的当前SynchronizationContext
。但是,由于这是一个控制台应用程序,因此上下文为NULL,并且适用默认值SynchronizationContext
。异步委托是在Task.Wait
后台线程线程3 上同步执行的(Task.Result
,Task.GetAwaiter.GetResult
或ThreadPool
会将异步操作转换为同步操作)。 em>和线程1 将阻塞,直到线程3 上的委托完成。由于代码是同步执行的,因此剩余的代码不会作为 thread 3 的延续入队,因此一旦委托完成,它将在 thread 1 上执行。在控制台应用程序的情况下,不会出现死锁情况,因为线程1 的SynchronizationContext
为NULL,并且线程3 必须使用默认上下文。
您的示例代码与方案1匹配。这是因为您正在运行控制台应用程序,而默认设置是SynchronizationContext
,因为控制台应用程序的SynchronizationContext
始终为NULL。当捕获的SynchronizationContext
为NULL时,Task
使用默认上下文,它是ThreadPool
的线程。由于异步委托已经在ThreadPool
线程上执行,因此TaskScheduler
决定停留在该线程上,并因此在<中执行调用者线程 thread 1 的已排队剩余代码。 em>线程3 。
在GUI应用程序中,最佳实践是始终在所有地方都使用Task.ConfigureAwait(false)
,除非您明确想要捕获调用方的SynchronizationContext
。这样可以防止应用程序意外死锁。