web-dev-qa-db-ja.com

Unity / C#では、.Netのasync / awaitは文字通り、別のスレッドで始まりますか?

重要 Unityでこの難しいトピックを研究している人のために

関連する主要な問題を提起した、私が尋ねた別の質問を必ず確認してください。

nityでは具体的に、「どこで」待機は文字通り戻りますか?


C#の専門家にとって、Unityはシングルスレッドです1

別のスレッドで計算などを行うのが一般的です。

別のスレッドで何かをするとき、ええと、すべての優れたC#プログラマーがそれを行う簡単な方法だと言っているので、しばしば非同期/待機を使用します!

void TankExplodes() {

    ShowExplosion(); .. ordinary Unity thread
    SoundEffects(); .. ordinary Unity thread
    SendExplosionInfo(); .. it goes to another thread. let's use 'async/wait'
}

using System.Net.WebSockets;
async void SendExplosionInfo() {

    cws = new ClientWebSocket();
    try {
        await cws.ConnectAsync(u, CancellationToken.None);
        ...
        Scene.NewsFromServer("done!"); // class function to go back to main tread
    }
    catch (Exception e) { ... }
}

OK、これを行うと、Unity/C#でより通常の方法でスレッドを起動するのときに、「通常どおりに」すべてを実行します(したがって、スレッドまたはその他のものを使用するか、ネイティブプラグインを許可します)それをするか、OSまたはケースが何であれ)。

すべてがうまくいきます。

一日の終わりにたどり着くのに十分なC#しか知らない、不器用なUnityプログラマーとして、私は常に想定上記の非同期/待機パターン文字通り別のスレッドを起動するを使用しています。

実際、上記のコードは文字通り別のスレッドを起動しますか、またはc#/。Netはnatty async/waitパターンを使用するときにタスクを達成するために他のアプローチを使用していますか?

多分それは「一般的にC#を使用すること」とは異なるまたは具体的にUnityエンジンで動作しますか? (IDK?)

Unityでは、スレッドであるかどうかにかかわらず、次のステップの処理方法に大きく影響します。したがって質問です。


Issue:「スレッドを待っている」については多くの議論があることに気づきましたが、(1)これについて議論/回答したのを見たことがないnity設定(そうする何か違いはありますか?IDK?)(2)明確な答えを見たことがありません!


1 いくつかの補助的な計算(物理など)は他のスレッドで行われますが、実際の「フレームベースのゲームエンジン」は1つの純粋なスレッドです。 (メインエンジンフレームスレッドに「アクセス」することは決して不可能です。たとえば、ネイティブプラグインや別のスレッドでの計算をプログラミングするときは、エンジンフレームスレッドのコンポーネントのマーカーと値をそのままにして、各フレームを実行するときに使用します。)

14
Fattie

自分の質問に答えるのは好きではありませんが、結局のところ、ここでの答えはまったく正しいものではありません。 (ただし、ここでの答えの多く/すべては、さまざまな意味で非常に役立ちます)。

実際、実際の答えは一言で言えます:

待機がSynchronizationContext.Currentによって制御された後、どのスレッドで実行が再開されます。

それでおしまい。

したがって、Unityの特定のバージョンでは(そして、2019年を書いている時点で、それらは劇的にUnityを変更していることに注意してください- https:// unity .com/dots )-または実際にC#/。Net環境-このページの質問には適切に回答できます。

このフォローアップQAですべての情報が明らかになりました。

https://stackoverflow.com/a/55614146/294884

2
Fattie

この読み取り: タスクは(まだ)スレッドではなく、非同期は並列ではありません は、内部で何が行われているのかを理解するのに役立ちます。つまり、タスクを別のスレッドで実行するには、呼び出す必要があります

_Task.Run(()=>{// the work to be done on a separate thread. }); 
_

その後、必要な場所でそのタスクを待つことができます。

あなたの質問に答えるために

「実際、上記のコードは文字通り別のスレッドを起動しますか、それともc#/。Netはnatty async/waitパターンを使用するときにタスクを達成するために他のアプローチを使用しますか?」

いいえ-ありません

あなたがしたなら

_await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));
_

次に、cws.ConnectAsync(u, CancellationToken.None)は別のスレッドで実行されます。

ここでのコメントへの回答として、より多くの説明で変更されたコードがあります:

_    async void SendExplosionInfo() {

        cws = new ClientWebSocket();
        try {
            var myConnectTask = Task.Run(()=>cws.ConnectAsync(u, CancellationToken.None));

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // class function to go back to main tread
        }
        catch (Exception e) { ... }
    }
_

別のスレッドでは必要ないかもしれませんが、これは、実行している非同期作業がCPUにバインドされていないためです(そう思われます)。したがって、あなたは大丈夫でなければなりません

_ try {
            var myConnectTask =cws.ConnectAsync(u, CancellationToken.None);

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // continue from here
        }
        catch (Exception e) { ... }
    }
_

順次、上記のコードとまったく同じことを行いますが、同じスレッドで実行されます。 「ConnectAsync」の後のコードの実行を許可し、「ConnectAsyncの完了を待つために停止するだけです」 "はawaitとあり、それ以降は(ConnectAsync "はCPUにバインドされていません(他の場所(つまり、ネットワーキング)で行われる作業の意味である程度並列にする)は、コードが "...."もCPUにバインドされた多くの作業が必要であり、並行して実行する必要があります。

また、async voidを使用しないようにすることもできます。これは、トップレベルの関数にのみ存在するためです。メソッドシグネチャでasync Taskを使用してみてください。詳しくはこちら こちら

17
agfc

いいえ、非同期/待機は意味しません-別のスレッド。別のスレッドを開始できますが、そうする必要はありません。

ここであなたはそれについて非常に興味深い投稿を見つけることができます: https://blogs.msdn.Microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-非平行/

5
Adam Jachocki

重要なお知らせ

まず、質問の最初のステートメントに問題があります。

Unityはシングルスレッドです

Unityはではありませんシングルスレッドです。実際、Unityはマルチスレッド環境です。どうして? nityの公式Webページ にアクセスして、そこを読んでください。

高性能マルチスレッドシステム:重いプログラミングをせずに、現在(および明日)利用可能なマルチコアプロセッサを最大限に活用します。高性能を実現するための新しい基盤は、3つのサブシステムで構成されています。C#ジョブシステムは、並列コードを作成するための安全で簡単なサンドボックスを提供します。 Entity Component System(ECS)、デフォルトで高性能コードを記述するためのモデル、およびBurst Compilerは高度に最適化されたネイティブコードを生成します。

Unity 3Dエンジンは、「Mono」と呼ばれる.NETランタイムを使用します。これは、その性質上、isマルチスレッドです。一部のプラットフォームでは、マネージコードがネイティブコードに変換されるため、.NETランタイムはありません。しかし、コード自体はとにかくマルチスレッドになります。

ですので、誤解を招くような、技術的に正しくない事実を述べないでください。

あなたが主張しているのは、Unityのメインスレッドがフレームベースの方法でコアワークロードを処理するというメインステートメントがある(---)という単純なステートメントです。これは本当です。しかし、それは新しくてユニークなものではありません!例えば。 .NET Framework(または3.0以降の.NET Core)で実行されているWPFアプリケーションにもメインスレッドがあり(しばしばUIスレッドと呼ばれます)、ワークロードはそのスレッドで処理されますWPF Dispatcher(ディスパッチャーキュー、操作、フレームなど)を使用してフレームベースの方法で。ただし、これらすべてでは環境がシングルスレッド化されません。これは、アプリケーションのロジックを処理する方法にすぎません。


あなたの質問への答え

注意:私の回答は、.NETランタイム環境(Mono)を実行するUnityインスタンスにのみ適用されます。マネージC#コードをネイティブC++コードに変換し、ネイティブバイナリをビルド/実行するインスタンスの場合、私の答えはおそらく少なくとも不正確です。

あなたが書く:

別のスレッドで何かをするとき、ええと、すべての優れたC#プログラマーがそれを行う簡単な方法だと言っているので、しばしば非同期/待機を使用します!

C#のasyncおよびawaitキーワードは、TAP( Task-Asynchronous Pattern )を使用するための単なる方法です。

TAPは任意非同期操作に使用されます。一般的に言えば、スレッドはありませんこのStephen Clearyの記事「スレッドはありません」 を読むことを強くお勧めします。 (あなたが知らなければ、Stephen Clearyは有名な非同期プログラミングの第一人者です。)

_async/await_機能を使用する主な原因は、非同期操作です。 _async/await_を使用するのは、「別のスレッドで何かを行う」ためではなく、待機する必要がある非同期操作があるためです。この操作が実行されるバックグラウンドスレッドがあるかどうか-これは問題ではありません(まあ、ほとんど、以下を参照してください)。 TAPは、これらの詳細を隠す抽象化レベルです。

実際、上記のコードを文字通り別のスレッドで起動します、またはc#/。Netを実行しますnatty async /待機パターン?

正解は次のとおりです:それは依存します

  • _ClientWebSocket.ConnectAsync_が引数検証例外をすぐにスローした場合(たとえば、ArgumentNullExceptionがnullの場合はuri)、新しいスレッドなしが開始されます
  • そのメソッドのコードが非常に速く完了すると、メソッドの結果は同期して利用可能になります。新しいスレッドは開始されません
  • _ClientWebSocket.ConnectAsync_メソッドの実装がスレッドを含まない純粋な非同期操作である場合、呼び出しメソッドは「一時停止」されます(awaitにより)-したがって、新しいスレッドは開始されません
  • メソッドの実装にスレッドが含まれ、現在のTaskSchedulerが実行中のスレッドプールスレッドでこの作業項目をスケジュールできる場合、新しいスレッドはありません開始されます。代わりに、作業項目はすでに実行中のスレッドプールスレッドのキューに入れられます
  • すべてのスレッドプールスレッドがすでにビジーの場合、ランタイムはその構成と現在のシステム状態に応じて新しいスレッドを生成する可能性があるため、はい-新しいスレッドが開始される可能性があります、作業項目はその新しいスレッドでキューに入れられます

ほら、これはかなり複雑です。しかし、それがまさにTAPパターンと_async/await_キーワードのペアがC#に導入された理由です。これらは通常、開発者が気になりたくないものなので、ランタイム/フレームワークでこれを非表示にしましょう。

@agfcはまったく正しくないことを述べています:

「これはバックグラウンドスレッドでメソッドを実行しません」

_await cws.ConnectAsync(u, CancellationToken.None);
_

「しかし、これは」

_await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));
_

ConnectAsyncの同期部分の実装が小さい場合、タスクスケジューラmightはどちらの場合もその部分を同期的に実行します。したがって、これらの両方のスニペットは、呼び出されたメソッドの実装によってはまったく同じになる場合があります。

ConnectAsyncにはAsyncサフィックスがあり、Taskを返すことに注意してください。これは、メソッドが完全に非同期であるという慣例に基づく情報です。そのような場合、常にawait MethodAsync()よりawait Task.Run(() => MethodAsync())を優先する必要があります。

さらに興味深い読書:

4
dymanoid

待機後のコードは、別のスレッドプールスレッドで続行されます。これは、Unity、EFのDbContext、および独自のカスタムコードを含む他の多くのクラスなどのメソッドでスレッドセーフでない参照を処理するときに影響を与える可能性があります。

次の例を見てください。

    [Test]
    public async Task TestAsync()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread Before Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = await names;
            Console.WriteLine("Thread After Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

出力:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 29
Thread Before Await: 29
Thread After Await: 12

1 passed, 0 failed, 0 skipped, took 3.45 seconds (NUnit 3.10.1).

ToListAsyncの前後のコードが同じスレッドで実行されていることに注意してください。そのため、結果のいずれかを待つ前に、処理を続行できますが、非同期操作の結果は利用できなくなり、作成されたTaskだけが利用可能になります。 (中止、待機など)awaitを入れると、続くコードは継続として効果的に分割され、別のスレッドに戻ることができます。

これは、インラインで非同期操作を待つときに適用されます。

    [Test]
    public async Task TestAsync2()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = await context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

出力:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async/Await: 6
Thread After Async/Await: 33

1 passed, 0 failed, 0 skipped, took 4.38 seconds (NUnit 3.10.1).

この場合も、待機後のコードは、元のスレッドとは別のスレッドで実行されます。

非同期コードを呼び出すコードが同じスレッドに残るようにしたい場合は、ResultTaskを使用して、非同期タスクが完了するまでスレッドをブロックする必要があります。

    [Test]
    public void TestAsync3()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = names.Result;
            Console.WriteLine("Thread After Result: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

出力:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 20
Thread After Async: 20
Thread After Result: 20

1 passed, 0 failed, 0 skipped, took 4.16 seconds (NUnit 3.10.1).

したがって、UnityやEFなどに関しては、これらのクラスがスレッドセーフではない場所で非同期を自由に使用することに注意する必要があります。たとえば、次のコードは予期しない動作を引き起こす可能性があります。

        using (var context = new TestDbContext())
        {
            var ids = await context.Customers.Select(x => x.CustomerId).ToListAsync();
            foreach (var id in ids)
            {
                var orders = await context.Orders.Where(x => x.CustomerId == id).ToListAsync();
                // do stuff with orders.
            }
        }

コードに関してはこれで問題ないように見えますが、DbContextはnotスレッドセーフであり、最初のCustomerでの待機に基づいてOrdersを照会すると、単一のDbContext参照が別のスレッドで実行されます。負荷。

同期呼び出しに比べて大きなメリットがあり、継続がスレッドセーフコードのみにアクセスする場合は、非同期を使用します。

1
Steve Py