El hilo de la interfaz de usuario bloquea mi ventana, ¿Cómo cambiar eso?

Aquí está mi código:

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);
            }
        }

Este código funciona perfectamente cuando intento conectar varios clientes. Cuando intento mover mi formulario después de transmitir la conexión, no funciona. Sé que esto es un problema de hilos, pero ¿cómo puedo evitar este problema?

Y aquí está mi botón:

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

¿Necesito usar sentencias de bloqueo? o delegados? ¿algunas ideas? ¿entonces como?

1
Debe pensar en crear Thread dedicado solo para el propósito de comunicación de la red. Luego, si desea transferir datos desde el hilo de comunicación al hilo de la interfaz de usuario, debe usar algún contexto de sincronización.
agregado el autor m.rogalski, fuente
Necesitas poner el broadcastthecection en un hilo.
agregado el autor BugFinder, fuente
¿Así que es la tarea? Estoy bastante seguro de que debería haber marcado su pregunta como tal, pero también habrá cubierto estos conceptos con usted
agregado el autor BugFinder, fuente
Puedes usar BackgroundWorker para resolver de manera fácil
agregado el autor Mustafa Erdem Köşk, fuente
@BugFinder - si no te importa, ¿podrías darme un ejemplo ...
agregado el autor Hamun Sunga, fuente

6 Respuestas

El problema es que se está llamando a BroadcastTheConnection() desde el propio hilo de la interfaz de usuario. Dado que tiene una construcción while (ServerRunning) {} , el hilo de la interfaz de usuario girará en su código hasta que ServerRunning sea falso.

Hay varias formas de implementar la misma solución: obtener el código del servidor fuera del hilo de la IU. Cada uno de estos tiene sus ventajas y desventajas.

  • Utilice BroadcastTheConnection() como una tarea de ejecución prolongada (no se recomienda)
  • Defiende un hilo en el que BroadcastTheConnection() es el método principal.
  • Utilice llamadas de socket asíncronas. (demasiado complicado para una respuesta rápida)

Tarea de ejecución larga

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

Esto es rápido y fácil, pero no desea muchas tareas de ejecución prolongada, ya que pueden ocupar subprocesos en la agrupación de tareas durante mucho tiempo.

Hilo dedicado

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

Esto no utiliza ningún subproceso del grupo de subprocesos de tareas, le proporciona un subproceso con nombre que puede ayudar con la depuración y evita que el subproceso mantenga su aplicación en ejecución si se olvida de finalizarla.

Trabajar con la interfaz de usuario desde el código de socket

Siempre que necesite interactuar con la interfaz de usuario de alguna manera, debe volver a poner su llamada en el hilo de la interfaz de usuario. WinForms y wpf tienen formas ligeramente diferentes de hacer lo mismo.

WinForms

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

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

wpf

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

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

Tenga cuidado, demasiadas llamadas a BeginInvoke en una fila pueden sobrecargar el subproceso de la interfaz de usuario. Es mejor agruparlos por lotes que disparar muchas solicitudes seguidas.

1
agregado
El uso de Hilo en los tiempos de las abstracciones de TPL, no el uso de Async-Await para operaciones en segundo plano, pero el uso del modelo APM Begin/End Invoke se remonta a la historia. Async-Await es la solución más simple para este caso.
agregado el autor Mrinal Kamboj, fuente
Invoke bloquea hasta que el subproceso de la IU ejecute su código, por lo que es en efecto un mecanismo de bloqueo.
agregado el autor Berin Loritsch, fuente
Los métodos Invoke() y BeginInvoke() en WinForms se definen en la clase Control . Por lo tanto, cualquier ventana, botón, panel, etc. tendrá la función que le permite invocar código en el subproceso de la interfaz de usuario. Es una historia similar para el código WPF, excepto que tienen el Dispatcher que le permite hacer lo mismo.
agregado el autor Berin Loritsch, fuente
Está bien. De nada.
agregado el autor Berin Loritsch, fuente
Consulte la última sección: Debe invocar el subproceso de la interfaz de usuario del subproceso de fondo. Di ejemplos para WinForms y WPF.
agregado el autor Berin Loritsch, fuente
@HamunSunga, lo siento. Mi recomendación principal es usar un hilo dedicado al que pueda asignarle un nombre (opción 2). De esa manera, puede detectar fácilmente si inició accidentalmente más de un hilo para controlar sus conexiones si observa un comportamiento errático.
agregado el autor Berin Loritsch, fuente
@KfirGuy, tiene que ver con el código BroadcastTheConnection() que se está ejecutando durante mucho tiempo. Además, es más fácil identificar cuando suceden más de uno de estos al mismo tiempo. Es por eso que abogaría por un hilo dedicado a esto.
agregado el autor Berin Loritsch, fuente
@HamunSunga, si su lógica es lo suficientemente compleja, es posible que desee ampliar Thread y sobrecargar el método Thread.Start() . Sin embargo, puede utilizar métodos anónimos muy bien.
agregado el autor Berin Loritsch, fuente
agregado el autor Berin Loritsch, fuente
@KfirGuy, ¿cuántos puede hacer antes de quedarse sin tareas de subprocesos? ¿Y cómo puede identificar si tiene ejecuciones simultáneas del mismo código TCP ejecutándose al mismo tiempo?
agregado el autor Berin Loritsch, fuente
@BerinLoritsch estás bromeando? No debes extender clases como Thread y no puedes hacerlo porque es una clase sellada.
agregado el autor Kfir Guy, fuente
El método BroadcastConnection utiliza un bucle para administrar todas las conexiones, por lo que debe ejecutarse solo una vez.
agregado el autor Kfir Guy, fuente
async y esperar también funcionan bien con tareas de larga ejecución. No bloquea la interfaz de usuario.
agregado el autor Kfir Guy, fuente
¿Por qué no usas async y esperas? Es mucho más fácil. Mira mi respuesta
agregado el autor Kfir Guy, fuente
Lo siento, no puedo venir, no tengo suficiente reputación de 20 ... lo siento ... @ Berin Loritsch
agregado el autor Hamun Sunga, fuente
En el uso de hilos dedicados, métodos anónimos, ¿no?
agregado el autor Hamun Sunga, fuente
muchas gracias
agregado el autor Hamun Sunga, fuente
Muchas gracias ...: soy apreciado ... :)
agregado el autor Hamun Sunga, fuente

Hay una variante asíncrona de AcceptSocket llamada BeginAcceptSocket , que espera una conexión de forma asíncrona e inicia un nuevo hilo para un nuevo socket conectado. Todavía tiene que esperar a que se complete la operación, porque se encuentra en un while , pero puede usar este tiempo para una llamada a Application.DoEvents , que permitir que la interfaz de usuario para actualizar.

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;
        }
    }
}

La solución hace que su IU responda con relativamente cambios en la estructura de su código. Sin embargo, recomendaría repensar su estrategia completa de usar el TcpListener . Por lo general, no es una buena idea escuchar las conexiones TCP en el subproceso de la interfaz de usuario. Cree una clase dedicada que escuche por usted en un hilo separado y acceda a ella desde su código de UI.

También debe tener en cuenta que en el código sobre su bloque catch no manejará nada dentro del delegado anónimo utilizado por BeginAcceptSocket . También he agregado código para dejar de escuchar cuando el servidor ya no se está ejecutando. Probablemente esto no sea necesario, ya que en este caso BeginAcceptSocket lanzará una excepción. Sin embargo, sirve como una salvaguardia adicional.

1
agregado

El ejemplo más simple es usar Thread para escuchar y comunicarse con el extremo remoto:

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) { /*...*/ }
    }
}

Este es el ejemplo más simple posible, por lo que debe modificarlo para sus necesidades.
Para usar esto con su ejemplo, simplemente inicie ListenerThread :

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

Lo último es que si desea realizar llamadas a la interfaz de usuario, sugeriría usar SynchronizationContext . Para hacerlo más claro en el constructor ListenerThread , llame a esto:

m_Sync = SynchronizationContext.Current;

Y hacer otro campo:

SynchronizationContext m_Sync;

Luego simplemente pase este contexto al constructor ClientConnection como new ClientConnection (m_Sync, cliente); .

Ahora puede utilizar el método SynchronizationContext.Post , por ejemplo. :

m_Sync.Post( state => { someUITextElement.AppendText((string)state); }, "hello world");
1
agregado

Realice los cambios a continuación utilizando Async y 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
agregado
Así que la asignación es incorrecta. En C#, la forma correcta de realizar la programación asíncrona es async y esperar. Comprueba mi respuesta para ver cuánto es más fácil que usar subprocesos o tareas.
agregado el autor Kfir Guy, fuente
¿Por qué manejar las tareas directamente? Solo usa async y espera.
agregado el autor Kfir Guy, fuente

Puede usar async y esperar para lograr una red asíncrona en C #.

Intenta esto (también he refaccionado tu código):

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);
    }
}

Y su gestor de eventos clic:

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
agregado
Async-Await es la mejor y la opción sugerida, aunque hay un problema, ya que el evento es un tipo de retorno nulo, el método Async también debe ser void return no Task. También iniciar otro subproceso, actualizar el control de Ui dentro del método Async no son buenas operaciones
agregado el autor Mrinal Kamboj, fuente
Gracias, intentaré esto también ... :)
agregado el autor Hamun Sunga, fuente

Puedes usar el trabajador de fondo también. A continuación se muestra un pequeño ejemplo de lo mismo.

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
agregado