UIスレッドがウィンドウをブロックしている、どう変更するか

これが私のコードです:

public void BroadcastTheConnection()
      {
        try
        {
            //............avoid unnecessary codes 
            while (ServerRunning)
            {
                s = myList.AcceptSocket();// blocking the content until client request accomplished 



                displayText.AppendText("\nConnected");
                Thread tcpHandlerThread = new Thread(tcpHandler);
                tcpHandlerThread.Name = "tcpHandler";
                tcpHandlerThread.Start();

             }

         }catch (Exception ex)
            {
                displayText.AppendText("Error----" + Environment.NewLine + ex.StackTrace);
            }
        }

複数のクライアントを接続しようとすると、このコードは完璧に機能します。接続をブロードキャストした後にフォームを移動しようとすると、うまくいきません。私はこれがスレッドの問題であることを知っていますが、どうやってこの問題を回避できますか

そしてここに私のボタンがあります:

private void bBroadcast_Click(object sender, EventArgs e)
        {
            BroadcastTheConnection();          
        }

lockステートメントを使う必要がありますか?または代議員?何か案は?それではどうですか?

1
あなたはネットワーク通信のためだけに専用の Thread を作ることを考えるべきです。その後、通信スレッドからUIスレッドにデータを転送したい場合は、同期コンテキストを使用する必要があります。
追加された 著者 m.rogalski,
broadcasttheconnectionをスレッドに入れる必要があります
追加された 著者 BugFinder,
だからその宿題?あなたがあなたの質問にそのようなものとして印を付けたべきであることを確信しているだけでなく、彼はあなたと共にこれらの概念をカバーしているでしょう
追加された 著者 BugFinder,
あなたは簡単な方法で解決するためにBackgroundWorkerを使うことができます
追加された 著者 Mustafa Erdem Köşk,
@BugFinder - 気にしないのであれば、例を教えてください。
追加された 著者 Hamun Sunga,

6 答え

問題は、 BroadcastTheConnection()がUIスレッド自体から呼び出されていることです。 while(ServerRunning){} 構造を持っているので、 ServerRunning がfalseになるまで、UIスレッドはコード上で回転します。

同じ修正を実装するには、いくつかの方法があります。サーバースレッドをUIスレッドから取得する。それぞれにトレードオフがあります。

  • 長期的なタスクとして BroadcastTheConnection()を使用する(お勧めしません)
  • BroadcastTheConnection()がメインメソッドであるスレッドを立ち上げます。
  • 非同期ソケット呼び出しを使用します。 (回答が複雑すぎるので

長期実行タスク

Task.Factory.StartNew(BroadcastTheConnection,
                      TaskCreationOptions.LongRunning);

これは迅速で簡単ですが、タスクスレッドプール内のスレッドを長時間占有する可能性があるため、あまりにも多くの長時間実行されるタスクは望ましくありません。

専用スレッド

Thread connectionThread = new Thread(BroadcastTheConnection)
{
    Name = "BroadcaseTheConnection Thread",
    IsBackground = true
};
connectionThread.Start();

これはタスクthreadpoolからのスレッドを一切使用せず、デバッグに役立つ名前の付いたスレッドを提供し、あなたがそれを終了するのを忘れた場合スレッドがあなたのアプリケーションを実行し続けることを妨げます。

ソケットコードからUIを操作する

何らかの方法でUIと対話する必要があるときはいつでも、呼び出しを再びUIスレッドに入れる必要があります。 WinFormsとWPFでは、同じことをやる方法が少し異なります。

WinForms

myControl.BeginInvoke(myControl.Method);//nonblocking

myControl.Invoke(myControl.Method);//blocking

wpf

myControl.Dispatcher.BeginInvoke(myControl.Method);//nonblocking

myControl.Dispatcher.Invoke(myControl.Method);//blocking

注意してください、 BeginInvoke を連続して呼び出す回数が多すぎると、UIスレッドが過負荷になる可能性があります。大量のリクエストを連続して実行するよりも、それらをまとめたほうが良いでしょう。

1
追加された
バックグラウンド操作にAsync-Awaitを使用するのではなく、TPL抽象化の時代にThreadを使用するのではなく、APMモデルのBegin/End Invokeを使用することは、歴史に遡ります。この場合は、Async-Awaitが最も簡単な解決策です。
追加された 著者 Mrinal Kamboj,
Invoke はUIスレッドがコードを実行するまでブロックするので、事実上ロックメカニズムです。
追加された 著者 Berin Loritsch,
WinFormsの Invoke()および BeginInvoke()メソッドは Control クラスで定義されています。したがって、どのウィンドウ、ボタン、パネルなどにも、UIスレッドでコードを呼び出すことができる関数があります。 WPFコードについても同様の話ですが、 Dispatcher を使用することで、同じことができます。
追加された 著者 Berin Loritsch,
そのとおり。どういたしまして。
追加された 著者 Berin Loritsch,
最後のセクションを見てください:バックグラウンドスレッドからUIスレッドを起動する必要があります。私はWinFormとWPFの両方の例を挙げました。
追加された 著者 Berin Loritsch,
@ HamunSunga、ごめんなさい。私の主な推奨はあなたが名前を付けることができる専用スレッドを使うことです(オプション2)。このようにして、あなたが誤った振る舞いをした場合、あなたが誤ってあなたの接続を制御するために誤って複数のスレッドを始めたかどうかを簡単に検出することができます。
追加された 著者 Berin Loritsch,
@KfirGuy、 BroadcastTheConnection()コードが長期実行されていることと関係があります。また、同時にこれらの現象が複数発生している場合は、それを特定する方が簡単です。だからこそ、私はこれに専用のスレッドを提唱します。
追加された 著者 Berin Loritsch,
@HamunSunga、ロジックが十分に複雑な場合は、 Thread を拡張して Thread.Start()メソッドをオーバーロードすることをお勧めします。ただし、匿名メソッドを使用することもできます。
追加された 著者 Berin Loritsch,
追加された 著者 Berin Loritsch,
@ KfirGuy、スレッドタスクがなくなるまでに何回できますか。また、同じTCPコードを同時に実行しているかどうかを確認する方法はありますか。
追加された 著者 Berin Loritsch,
@BerinLoritschあなたは冗談ですか? Threadのようなクラスを拡張してはいけませんし、それはシールされたクラスなのでできません。
追加された 著者 Kfir Guy,
BroadcastConnectionメソッドはループを使用してすべての接続を管理するため、1回だけ実行する必要があります。
追加された 著者 Kfir Guy,
asyncとawaitは、長時間実行されているタスクでもうまく機能します。 UIはブロックされません。
追加された 著者 Kfir Guy,
非同期を使用して待機しないのはなぜですか。それはずっと簡単です。私の答えを確認してください。
追加された 著者 Kfir Guy,
申し訳ありませんが、私は来ることができません、私は十分な20の評判を持っていません...それについてすみません…
追加された 著者 Hamun Sunga,
専用スレッド使用では、匿名メソッドは正しいですか?
追加された 著者 Hamun Sunga,
どうもありがとうございました
追加された 著者 Hamun Sunga,
どうもありがとうございます。
追加された 著者 Hamun Sunga,

BeginAcceptSocket と呼ばれる AcceptSocket の非同期バリアントがあります。これは非同期で接続を待ち、接続された新しいソケットに対して新しいスレッドを開始します。 while ループに入っているので、操作が完了するのを待つ必要がありますが、この時間を Application.DoEvents の呼び出しに使用できます。 UIの更新を許可します。

while (ServerRunning)
{
    AsyncHandler handler = delegate(asyncResult)
    {
        //Get the new socket
        Socket socket = myList.EndAcceptSocket(asyncResult);

        //Marshal UI specific code back to the UI thread
        MethodInvoker invoker = delegate()
        {
            listOFClientsSocks.Add(socket);
            listBox1.DataSource = listOFClientsSocks;
            displayText.AppendText("\nConnected");
        };
        listBox1.Invoke(invoker);

        //Call the handler
        tcpHandler();
    }
    IAsyncResult waitResult = myList.BeginAcceptSocket(handler, null);

    //Wait until the async result's wait handle receives a signal
    //Use a timeout to referesh the application every 100 milliseconds
    while (!waitResult.AsyncWaitHandle.WaitOne(100))
    {
        Application.DoEvents();
        if (!ServerRunning)
        {
            break;
        }
    }
}

このソリューションは、コード構造に比較的変更を加えることでUIをレスポンシブにします。ただし、 TcpListener を使用するという全体的な戦略を再考することをお勧めします。あなたのUIスレッドでTCP接続を聴くのは、一般的には良い考えではありません。リスニングを行う専用のクラスを別のスレッドで作成し、それをUIコードからアクセスします。

catch ブロックの上のコードでは、 BeginAcceptSocket によって使用される匿名デリゲート内のものは何も処理されないことにも注意してください。私はまた、サーバーがもう稼働していないときにlistenを停止するためのコードを追加しました。この場合、 BeginAcceptSocket は例外をスローするため、これはおそらく必要ありません。それは追加の保護手段としても役立ちます。

1
追加された

Thread を使用してリモートエンドと通信して通信する最も簡単な例:

public class ListenerThread
{
   //clients list/queue
    Queue m_Clients;
   //thread used to listen for new connections
    Thread m_Thread;
    Socket m_Socket;
    IPEndPoint m_LocalEndPoint;

    volatile bool m_IsListening;

    public ListenerThread(int port)
    {
       //get this machine hostname
        IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());  
       //resolve ip address from hostname
        IPAddress ipAddress = ipHostInfo.AddressList[0];  
       //create local end point object 
        m_LocalEndPoint = new IPEndPoint(ipAddress, port);  
    }

    void Listen()
    {
       //reset clients list
        m_Clients = new Queue();
       //initialize socket
        m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); 
       //bind this socket to listen for incomming connections to specified end point
        m_Socekt.Bind(localEndPoint);
       //start listening with backlog of 1337 connections
        m_Socket.Listen(1337);  
       //dont forget to dispose after socket was used to "unbind" it
        using ( m_Socket )
        {
            while ( m_IsListening )
            {
               //while listening just accept connections and start them at another thread
                Socket client = m_Socket.Accept();
                if ( client != null )
                {
                    m_Clients.Enqueue(new ClientConnection(client));
                }
            }
        }
    }

   //method used to start the listening server
    public void Start()
    {
        if ( m_Thread == null )
        {
            m_Thread = new Thread(Listen);
        }

        m_IsListening = true;
        m_Thread.Start();
    }

   //method used to stop listening server
    public void Stop()
    {
        m_Listening = false;
        m_Thread.Join();
        while ( m_Clients.Count != 0 )
        {
            m_Clients.Dequeue().Kill();
        }
    }
}

// class used to communicate with the client
public class ClientConnection
{
    Socket m_Socket;//client socket
    Thread m_Thread;//communication thread

    volatile bool m_IsCommunicating;

   //this should start immediately because of the incomming connection priority
    internal ClientConnection(Socket socket)
    {
        m_Socket = socket;
        m_Thread = new Thread(Communicate);
        m_Thread.Start();
    }

   //loop in which you should send/receive data
    void Communicate()
    {
        while ( m_IsCommunicating )
        {
           //.. do your communication stuff
        }
    }

   //should be only used by ListenerThread to end communication.
    internal void Kill()
    {
        m_IsCommunicating = false;
        try
        {
            m_Thread.Join(5 * 1000);
            m_Thread.Abort();
        }
        catch(Exception ex) { /*...*/ }
    }
}

これは最も簡単な例ですので、必要に応じて変更してください。
あなたの例でこれを使うためには、単に ListenerThread を始めてください:

ListenerThread listener = new ListenerThread(8001);
listener.Start();
displayText.AppendText("The server is running at port 8001...\n");

最後に、UIを呼び出す場合は、 SynchronizationContext を使用することをお勧めします。これを ListenerThread コンストラクタでより明確にするためには、これを呼び出します。

m_Sync = SynchronizationContext.Current;

そして別の分野を作る:

SynchronizationContext m_Sync;

次に、このコンテキストを new ClientConnection(m_Sync、client); として ClientConnection コンストラクタに渡します。

これで SynchronizationContext.Post メソッドを使うことができます。 :

m_Sync.Post( state => { someUITextElement.AppendText((string)state); }, "hello world");
1
追加された

AsyncとAwaitを使用して以下の変更を行います。

private async void bBroadcast_Click(object sender, EventArgs e) //-- Async
{
    ipAdrsNew = ipBox.Text;
    portNo = Convert.ToInt32(portBox.Text);
    await BroadcastTheConnection();           //-- await
}

public Task BroadcastTheConnection()
{
   return Task.Run(() =>
   {
       //---- code for BroadcastTheConnection 
   });
}
1
追加された
だから割り当ては間違っています。 C#では、非同期プログラミングを行う正しい方法は、asyncとawaitを使用することです。私の答えをチェックして、スレッドやタスクを使うよりもどれほど簡単かを確認してください。
追加された 著者 Kfir Guy,
なぜあなたはタスクを直接管理するのですか? asyncを使って待つだけです。
追加された 著者 Kfir Guy,

C#で非同期ネットワークを実現するには、非同期と待機を使用できます。

これを試してください(私はあなたのコードもリファクタリングしました):

public async Task BroadcastConnectionAsync(IPAddress address, int port)
{
    try
    {
        var listener = new TcpListener(address, port);

        ServerRunning = true;
       //Start Listeneting at the specified port

        listener.Start();
        displayText.AppendText("The server is running at port 8001...\n");

        while (ServerRunning)
        {
            using (var socket = await listener.AcceptSocketAsync())
            {
                listOFClientsSocks.Add(socket);
                listBox1.DataSource = listOFClientsSocks;

                displayText.AppendText("\nConnected");
                new Thread(tcpHandler)
                {
                    Name = "tcpHandler"
                }.Start();
            }
        }
    }
    catch (Exception ex)
    {
        displayText.AppendText("Error----" + Environment.NewLine + ex.StackTrace);
    }
}

そしてあなたのクリックイベントハンドラ:

private async void bBroadcast_Click(object sender, EventArgs e)
{
    var address = IPAddress.Parse(ipBox.Text);
    int port = Convert.ToInt32(portBox.Text);
    await BroadcastConnectionAsync(address, port);
}
1
追加された
eventはvoidの戻り値の型なので、Async-Awaitが最も推奨される選択肢ですが、問題はありますが、AsyncメソッドもTaskではなくvoidの戻り値になります。また、Asyncメソッド内でUiコントロールを更新して別のスレッドを起動しても、うまく動作しません。
追加された 著者 Mrinal Kamboj,
ありがとう、私もこれを試してみます... :)
追加された 著者 Hamun Sunga,

バックグラウンドワーカーも使用できます。以下は同じの小さな例です。

private void MainMethod()
        {
            BackgroundWorker bg = new BackgroundWorker();
            bg.DoWork += Bg_DoWork;
            bg.RunWorkerCompleted += Bg_RunWorkerCompleted;

        }

        private void Bg_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            //here do your UI related work like changing the color of label or filling up data in grid etc
        }

        private void Bg_DoWork(object sender, DoWorkEventArgs e)
        {
            //Here do your time consuming work without blocking ui thread
        }
0
追加された