質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.34%
.NET Core

.NET Coreは、マネージソフトウェアフレームワークでオープンソースで実装されています。クロスプラットフォームを前提に考えられており、Windows/Mac/Linuxで動くアプリケーションを作成することが可能です。

SignalR

SignalRは、マイクロソフト社のASP.NETを基盤とした技術の一つ。ASP.NETアプリにリアルタイム性を持たせることができるライブラリです。サーバサイドとクライアントサイド双方でのリアルタイム通信アプリの開発を容易にします。

Q&A

解決済

1回答

203閲覧

.NET Core SignalR で別スレッドから Progress 表示したい

menshan

総合スコア55

.NET Core

.NET Coreは、マネージソフトウェアフレームワークでオープンソースで実装されています。クロスプラットフォームを前提に考えられており、Windows/Mac/Linuxで動くアプリケーションを作成することが可能です。

SignalR

SignalRは、マイクロソフト社のASP.NETを基盤とした技術の一つ。ASP.NETアプリにリアルタイム性を持たせることができるライブラリです。サーバサイドとクライアントサイド双方でのリアルタイム通信アプリの開発を容易にします。

0グッド

0クリップ

投稿2024/12/22 00:00

実現したいこと

VS2022 .Net Core 8.0 ASP.NET Core Webアプリ(Razor Pages)で
公式チュートリアルのチャットを少し改造してサーバタスクの進捗表示を行おうとしています。
サーバ側処理を別スレッドとして行おうとすると Hub の Clients.Client 参照で例外が発生してしまいます。
参照:
https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio

発生している問題・分からないこと

ボタン押下1回目の処理はOKですが2回目で以下 Clients が Disposed されているというエラーになってしまします。
何か解決策お分かりの方いらっしゃいますでしょうか?

エラーメッセージ

error

1Clients = 'Clients' は型 'System.ObjectDisposedException' の例外をスローしました 2例外がスローされました: 'System.ObjectDisposedException' (System.Private.CoreLib.dll の中) 3型 'System.ObjectDisposedException' の例外が System.Private.CoreLib.dll で発生しましたが、ユーザー コード内ではハンドルされませんでした 4Cannot access a disposed object.

該当のソースコード

ProgressHub.cs

1public class ProgressHub : Hub 2{ 3 static void ThreadMethod(IClientProxy cl, int delay) // ★これは追加 4 { 5 for (int i = 0; i < 5; i++) 6 { 7 Thread.Sleep(delay); 8 cl.SendAsync("ReceiveMessage", "notify", $"msg-{i}"); 9 } 10 } 11 public async Task SendMessage(string user, string message) 12 { 13 // ★ここを変更 14 //await Clients.Client(user).SendAsync("ReceiveMessage", user, message); 15 Thread thread1 = new Thread(() => ThreadMethod(Clients.Client(user), 500)); //ここで例外 16 thread1.Start(); 17 // これは何回やっても問題なし 18 // ThreadMethod(Clients.Client(user), 500); 19 20 await Task.Delay(0); 21 } 22} 23

Program.cs

1//.NET8.0 では Setup.cs が無くなってますが Program.cs に同じ内容を書けば良い様です。 2// Add services to the container. 3builder.Services.AddRazorPages(); 4builder.Services.AddSignalR(); // ★ここを変更 5 ・・・ 6app.MapRazorPages(); 7app.MapHub<ProgressHub>("/progressHub"); // ★ここを変更 8 ・・・ 9

index.cshtml

1document.getElementById("sendButton").addEventListener("click", function (event) { 2 //var user = document.getElementById("userInput").value; 3 var user = connection.connection.connectionId; // ★ここを変更 4 var message = document.getElementById("messageInput").value; 5 connection.invoke("SendMessage", user, message).catch(function (err) { 6 return console.error(err.toString()); 7 }); 8 event.preventDefault(); 9}); 10

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

別スレッドにしなければOKなのですが、リクエストが戻ってくるまでクライアントから別のリクエストをすることができないので困っています。

補足

特になし

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

原因はHubインスタンス破棄後に、そのメンバーを別スレッドで参照している、ただ破棄タイミングによっては、時折成功するのではと思います。

対策案として、ThreadMethod()にuser名(文字列)を渡して、普通にSignalRとして送信するメソッドを作ります。また現代のC#では new Thread()はしないのでそれも修正します。

1.ThreadMethod()をクラス に切り出し

別のクラス HeavyWork を作り重たい処理を移動。SignalR送信用にIHubContextを注入。

cs:HeavyWork

1public class HeavyWork(IHubContext<ProgressHub> hub) 2{ 3 public async Task ThreadMethod(string user, int delay) 4 { 5 //ProgressHubからクライアントを取得 6 var client = hub.Clients.Client(user); 7 for (int i = 0; i < 5; i++) 8 { 9 await Task.Delay(delay); 10 await client.SendAsync("ReceiveMessage", "notify", $"msg-{i}"); 11 } 12 } 13}
2.HeavyWork クラスをサービス登録

作ったクラスをサービス登録。Program.cs の var app = builder.Build();より前に1行追記

cs:Program.cs

1builder.Services.AddSingleton<HeavyWork>();
3.ProgressHub クラス修正

ThreadPoolを使って実行。

cs:ProgressHub.cs

1public class ProgressHub(HeavyWork heavy) : Hub 2{ 3 public void SendMessage(string user, string message) 4 { 5 //別スレッドで実行 6 _ = Task.Run(() => heavy.ThreadMethod(user, 500)); 7 } 8}

重たい処理の内容が分からないのでそのままThreadPoolで実行する例にしましたが、
処理内容によっては重たい処理はBackgourndServiceとして登録、待機し、それをSignalR契機で起動させるような形のほうが適しているかもしれません。

追記

別案も書いておきます。 こちらのほうがProgressHub.csの変更のみで、別スレッド内でどうすべきかも理解しやすいかも。

cs:ProgressHub.cs

1public class ProgressHub(IServiceScopeFactory factory) : Hub 2{ 3 public void SendMessage(string user, string message) { 4 _ = Task.Run(() => { 5 using var scoped = factory.CreateScope(); 6 var hub = scoped.ServiceProvider.GetRequiredService<IHubContext<ProgressHub>>(); 7 var client = hub.Clients.Client(user); 8 ThreadMethod(client, 500); 9 }); 10 } 11 static void ThreadMethod(IClientProxy cl, int delay) { 12 //変更なし 13 }

投稿2024/12/22 08:33

編集2024/12/23 11:41
hqf00342

総合スコア374

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

menshan

2024/12/22 12:17

回答ありがとうございます。 素晴らしいです。 何回ボタンを押してもエラーが出なくなりました。 サービス登録という事をしないといけないのですね。 もっとC#勉強しなくては! 重たい処理はBackgourndServiceにするほどでもないので提示頂いたThreadPoolにしたいと思います。 ベストアンサーに選ばせていただきました。
hqf00342

2024/12/23 04:39

動いたようで良かったです。 少し気になったのでサービス登録部分を修正しました。 説明をだいぶ端折ってしまいましたが、元コードのスレッドはスレッド内からスレッドの外のClientsを利用しようとした=スレッドセーフではない、ということです。 別のクラスに切り出してもスレッドの外のオブジェクトたちにアクセスすると同じような事になる可能性があるので、スレッドセーフになるよう気をつけましょう。
menshan

2024/12/23 23:51

別案の提示ありがとうございます。 こちらの方が Program.cs も修正不要でいいですね。 重たい処理の方は別のクラスに切り出してClientにはアクセスさせない方がいいかなと思いましたので以下の様にしました。 class HeavyWork { public delegate void CallBackNotify(string msg); public void SomeJob(int delay, CallBackNotify callBackNotify) { for (int i = 0; i < 5; i++) { Thread.Sleep(delay); callBackNotify($"msg-{i}"); } } } static void ThreadMethod(IClientProxy cl, int delay) // ★これは追加 { HeavyWork hw = new HeavyWork(); hw.SomeJob(delay,(msg)=> { cl.SendAsync("ReceiveMessage", "notify", msg); }); }
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.34%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問