Синхронизация коллекций и прерывание задач
Я пишу небольшой многопоточный сетевой сервер. Все классические вещи: он прослушивает входящие соединения, принимает их и затем обслуживает их в разных потоках. Кроме того, этот сервер иногда должен будет перезапускаться, и для этого он должен: a) прекратить прослушивание, b) удалить все подключенные клиенты, c) настроить некоторые параметры / подождать, d) возобновить прослушивание.
Ну, я почти ничего не знаю о разработке многопоточных программ, поэтому я ищу помощи. Вот к чему я пришел (только основной материал):
class Server
{
class MyClient
{
Server server;
TcpClient client;
bool hasToFinish = false;
public MyClient(Server server, TcpClient client)
{
this.server = server;
this.client = client;
}
public void Go()
{
while (!hasToFinish)
{
// do all cool stuff
}
CleanUp();
}
private void CleanUp()
{
// finish all stuff
client.Close();
server.myClients.Remove(this);
}
public void Finish()
{
hasToFinish = true;
}
}
bool running = false;
TcpListener listener;
HashSet<MyClient> myClients = new HashSet<MyClient>();
public void Start()
{
if (running)
return;
myClients.Clear();
listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 1234);
listener.Start();
listener.BeginAcceptTcpClient(AcceptClient, this);
running = true;
}
public void Stop()
{
if (!running)
return;
listener.Stop();
foreach (MyClient client in myClients)
{
client.Finish();
}
myClients.Clear();
running = false;
}
public void AcceptClient(IAsyncResult ar)
{
MyClient client = new MyClient(this, ((TcpListener)ar.AsyncState).EndAcceptTcpClient(ar));
myClients.Add(client);
client.Go();
}
}
Это абсолютно неудовлетворительно. Синхронизации нет (я просто не знаю, где ее разместить!), И вызов Server.Stop () не приводит к немедленной остановке MyClient-s. Как я могу исправить эти проблемы?
1 ответ
Код выглядит достаточно чистым, мы можем сделать его поточно-ориентированным с помощью простых модификаций.
Проблема состоит из трех частей: "клиент", "сервер" и взаимодействие клиент-сервер.
Сначала клиент, метод Go() вызывается одним потоком (назовем его A), а метод Finish() вызывается другим потоком (B). Когда поток B изменяет поле hasToFinish, поток A может не сразу увидеть изменение, потому что переменная может быть кэширована в кэше ЦП. Мы можем исправить это, сделав поле hasToFinish "volatile", что заставит поток B публиковать изменение переменной в потоке A при обновлении.
Теперь класс сервера. Я рекомендую вам синхронизировать три метода на экземпляре "Server", как в примере ниже. Это гарантирует, что Start и Stop вызываются последовательно и переменные, которые они изменяют, публикуются в потоках.
Взаимодействие клиент-сервер также необходимо учитывать. В вашем коде Клиент удаляет свою ссылку с Сервера, но сервер удаляет все ссылки клиентов, когда Finish() любым способом. Это выглядит избыточным для меня. Если мы сможем удалить часть кода в клиенте, нам не о чем беспокоиться. Если по какой-либо причине вы решите сохранить логику в клиенте, а не на сервере, создайте открытый метод RemoveClient(клиент-клиент) в классе Server и синхронизируйте его с экземпляром Server. Затем позвольте клиенту вызывать этот метод вместо непосредственного манипулирования HashSet.
Я надеюсь, что это решит вашу проблему.
public void Start()
{
lock(this)
{
if (running)
return;
myClients.Clear();
listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 1234);
listener.Start();
listener.BeginAcceptTcpClient(AcceptClient, this);
running = true;
}
}
public void Stop()
{
lock(this)
{
if (!running)
return;
listener.Stop();
foreach (MyClient client in myClients)
{
client.Finish();
}
myClients.Clear();
running = false;
}
}
public void AcceptClient(IAsyncResult ar)
{
lock(this)
{
MyClient client = new MyClient(this, ((TcpListener)ar.AsyncState).EndAcceptTcpClient(ar));
myClients.Add(client);
client.Go();
}
}