Не блокируется поток в клиент-серверном приложении

программирование C# .net

Всем доброго дня!

На лабораторной (да я студент) задали реализовать чат. В ходе реализации решил использовать асинхроный метод приема данных от клиента, и путём вызова событий обрабатыват их (например при приходе текстового сообщения от клиента вызывается событие getTextMs). Также на серверной части хранится список всех подключёных пользователей (для рассылки полученых сообщений).

При любом обращении к списку пользователей я использую lock чтобы не вылететь из перечисления. Например когда пользователь выходит из сети, то вызывается функция которая удаляет его из списка, и если другой поток в это время рассылал данный - выходит не очень красиво.

Собственно проблема - указаный элемент в lock не блокируется. И когда я запускаю к примеру сразу десять окон и одновременно их отключаю - сервер вылетает с InvalidOperationException Collection was modified; enumeration operation may not execute. (то есть у меня во время перечисления this.clientList изменился).

Код C#
private readonly Object locker = new Object();
...
private void client_getExit(object sender, ExitEventArgs e)
{
ServerClient client = sender as ServerClient;
lock (this.locker)
{
this.clientList.Remove(client);
if (client.IsAuthorization)
{
//рассылка всем пользователям, что клиент вышел из сети.
OutServerMessage serverMessage = new OutServerMessage(client.Id);
foreach (ServerClient serverClient in this.clientList)
{
serverClient.SendMessage(serverMessage);
}
}
}
}

this.clientList - список всех подключенных пользователей.
serverMessage - сообщение, которое отправляется на клиент.
serverClient.SendMessage - отправление текущего сообщения пользователю.

Я сначала думал, что всё это каким-то чудом висит в одном потоке, но проверка по System.Threading.Thread.CurrentThread.ManagedThreadId показала разные числа (надеюсь то смотрел).

также решил ради "шутки" добавить ещё одну переменную и посмотреть в дэбаге, при ошибке

Код C#
private readonly Object locker = new Object();
int kso = 0;
...
private void client_getExit(object sender, ExitEventArgs e)
{
ServerClient client = sender as ServerClient;
lock (this.locker)
{
this.kso++;

this.clientList.Remove(client);
if (client.IsAuthorization)
{
//рассылка всем пользователям, что клиент вышел из сети.
OutServerMessage serverMessage = new OutServerMessage(client.Id);
foreach (ServerClient serverClient in this.clientList)
{
serverClient.SendMessage(serverMessage);
}
}

this.kso--;
}
}

так вот... переменная this.kso при вылавливании ошибки показывала от 2 до 5.

P.S. Пожалуйста, отзовитесь люди! Я уже второй день с этой ошибкой воюю... ='(

Примечание:
-- как сервер обрабатывает запросы клиента?

Имеется отдельный класс DataChanel отвечающий за прием сообщения, в нашем случаем получение объекта класса Message, в котором описаны нужные нам поля, (скажем текст сообщения и от кого оно пришло). В этом классе находится сокет, который асинхронно считывает входящие данный, и когда сообщение полностью получено, генерируется событие getMessage, одним из параметров которого - наше полученное сообщение.

-- сколько потоков на сервере занимаются обработкой пула запросов?

Как таковые, потоки не выделяют. Сервер просто обрабатывает событие getMessage и рассылает всем/указаным пользователям сообщение. То есть подписчик работает с общим ресурсом "список пользователей, что онлайн"

-- как эти потоки взаимодействуют друг с другом?

Никак. Главное чтобы пока у одного подписчика занят список, второй его не трогал и ждал.

-- какое отношение имеет лок потока ThreadId=1 к локу потока ThreadId=2?

Хоть убейте, не пойму вопроса.

-- если к задаче подойти тупо в лоб, то лок должен быть статичным синглтоном, что из кода не очевидно, либо совсем не наблюдается.

Сервер по сути является классом. То есть в приложении может быть два сервера, каждый из которых имеет свой независимый список, который нужно блокировать внутри. Хотя вписать static private readonly Object locker = new Object(); пробовал - та же картина, что и без него.

Примечание:
-- опять же не видно внутренней реализации, может там зацикливание идет, типа клиент проставил лок и отправил себе сообщение

По сути ошибка была в этом, сяпки.
Ответы:
> указаный элемент в lock не блокируется. И когда я запускаю к примеру сразу десять окон
встречные вопросы:
как сервер обрабатывает запросы клиента?
сколько потоков на сервере занимаются обработкой пула запросов?
как эти потоки взаимодействуют друг с другом?
какое отношение имеет лок потока ThreadId=1 к локу потока ThreadId=2?
про потоки написал, потомучто неочевидна реализация DataChanel. какой-то странный получается сервер, что обрабатывает клиентов в один поток, пусть даже асинхронно. возможно, к одному объекту получают доступ разные потоки-обработчики разных запросов. про  this.kso от 2 до 5 прочитал невнимательно, да и описание лока статичным с тем же результатом говорит скорее о том, что доступ имеет один и тот же объект. Кстати, фраза "Главное чтобы пока у одного подписчика занят список, второй его не трогал и ждал." как раз про описанную блокировку. А что произойдет в случае "пока у одного подписчика занят список, этот же подписчик его не трогал и ждал."?
Конкретно вот эти строки смущают
lock (this.locker)
serverClient.SendMessage(serverMessage);//как реализована отправка сообщений?
опять же не видно внутренней реализации, может там зацикливание идет, типа клиент проставил лок и отправил себе сообщение (это не обязательно OutServerMessage), сам же его получил (вспоминаем, что блокировка работает для разных потоков) и что-то стал делать. На дебагу надо поставить тот случай, когда ксо становится 2 (какое сообщение, какой поток, кто и кому отправляет кто сендер, а кто обрабатывает сообщение, проверить IsThreadPoolThread потока, в котором все это крутится и пр.)
используйте для списка клиентов потокобезопасные коллекции
например ConcurrentDictionary<T>


12 лет назад

RPI.su - самая большая русскоязычная база вопросов и ответов. Наш проект был реализован как продолжение популярного сервиса otvety.google.ru, который был закрыт и удален 30 апреля 2015 года. Мы решили воскресить полезный сервис Ответы Гугл, чтобы любой человек смог публично узнать ответ на свой вопрос у интернет сообщества.

Все вопросы, добавленные на сайт ответов Google, мы скопировали и сохранили здесь. Имена старых пользователей также отображены в том виде, в котором они существовали ранее. Только нужно заново пройти регистрацию, чтобы иметь возможность задавать вопросы, или отвечать другим.

Чтобы связаться с нами по любому вопросу О САЙТЕ (реклама, сотрудничество, отзыв о сервисе), пишите на почту [email protected]. Только все общие вопросы размещайте на сайте, на них ответ по почте не предоставляется.