Don't panic.
Follow these rules/guidelines:
1) Stick to one thread for the GUI system, handling messages and calling events. Ths is enforced my most GUI frameworks anyway.
2) Never, ever wait in a GUI event-handler, in any way, for any thread signal or state. Use your framework messaging system, (signals/slots, Windows messages, whatever), to communicate data to/from other threads.
3) Do not continually create/terminate/destroy/join threads. Never do this. If your 'Intro to multithreading' site says that you must always use use join() to wait for thread termination, remove all references to the site from your browser and its history. Use app-lifetime threads that are pooled, or dedidated to one task, and loop around some blocking input queue mechanism and process input messages until the app is terminated. Don't try to termiante them yourself unless you have absolutely no choice or you enjoy pain.
4) 'i decided to open a thread for each new client. the received data will be displayed on the GUI'. Fine, sounds like a plan. For each widget group that represents a single client, start a thread to handle the comms and pass, as part of the thread creation, the instance of the widget group to which is is bound so that it can direct messages back to the correct widget, and some kind of queue event/whatever that the thread can wait on for queued input request struct.
5) Do not write from the GUI thread to any data/fields/whatever of the client threads directly. If you want to communicate something from the GUI thread, use your framework and/or OS API to queue a request struct to the thread.
6) Do not write from the client threads to any data/fields/whatever of the GUI thread. If you want to communicate something to the GUI thread, use your framework and/or OS API to queue a request struct message to the thread, so firing an event handler with the message to perform the GUI actions/s.
7) Take some care with message lifetimes - if you intend to queue up a message struct pointer to another thread, it's useless allocating the struct with auto storage - it will likely be gone by the time the receiving thread gets it. One method: malloc it, queue it to the other thread, receive it, free it in the other thread. Other, ore controlled inter-thread, inter-GUI comms mechanisms are possible, eg. using message pools, but I suspect that is beyond you ATM.
7) If at all possible, design to avoid the 'clean and graceful termination' of app -lifetime and pooled threads. User code cannot reliably stop threads running on other cores, (without messy and wasteful polling). The OS can easily stop all threads at process termination - let it do its job.
8) Forget many of those 'must do' rules from those who have only ever written single-threaded command-line apps. For instance, it's grossly unrealistic for your code to always release all memory that was allocated during your program run at termination time. In a complex app, with multiple cores reading/writing messages etc, any attempt to write user code to close it down is hugely likely to create far more problems that it is worth in terms of testing/debugging. If you allocate some thing once and you need it for the whole run, do just that - don't try to free it, there's no point at best. If multiple threads are reading/writing/calling through it, there is a massive quiz-show-full of points in absolutely not freeing it with user code, (the OS can easily free it on termination - it stops all threads first before dealocating process memory).
If you only queue up requests/messages, don't wait in event handlers and don't make any suicide-calls like join(), you should get no deadlocks:)