Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923

Синхронизация асинхронного заполения списка и CancellationToken

06.10.2021, 14:07. Показов 8822. Ответов 95
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Открываю GUI страницу со списком картинок.
Проблема: из-за того, что картинки весят много -- какое-то время, допустим 2 секунды, страница просто пустая, а после резко заполняется всем списком.

Желаемый результат: хотелось бы, чтобы при открывании страницы элементы постепенно подгружались.

Решение 1: решил сделать само заполнение асинхронным (просьба сначала досмотреть все 3 решения)
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
        protected async Task UpdateAll()
        {
            var devices = await devicesRepository.GetDevices(currentDevice == null ? Root : currentDevice.Id);
 
            await Task.Run(() =>
            {
                if (devices != null)
                {
                    var filterDevices = devices.FindAll(d => !d.IsArchived);
 
                    //remove old items
                    foreach (var oldItem in Devices)
                    {
                        if (filterDevices.Find(x => x.Id == oldItem.Id) == null)
                            Devices.Remove(oldItem);
                    }
 
                    //add or change new items
                    foreach (var newItem in filterDevices)
                    {
                        var elm = Devices.FirstOrDefault(x => x.Id == newItem.Id);
                        if (elm != null)
                        {
                            if (!newItem.Equals(elm))
                                elm = newItem;
                        }
                        else
                        {
                            Devices.Add(newItem);
                        }
                    }
                }
                else
                {
                    Devices.Clear();
                }
            });
 
            await UpdateContextMenu();
        }
Это решило мою проблему, но так я оставляю код потоконебезопасным.
Решение 2: добавил локировку
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
        protected async Task UpdateAll()
        {
            var devices = await devicesRepository.GetDevices(currentDevice == null ? Root : currentDevice.Id);
 
            await Task.Run(() =>
            {
                lock (((ICollection)Devices).SyncRoot)
                {
                    if (devices != null)
                    {
                        var filterDevices = devices.FindAll(d => !d.IsArchived);
 
                        //remove old items
                        foreach (var oldItem in Devices)
                        {
                                if (filterDevices.Find(x => x.Id == oldItem.Id) == null)
                                Devices.Remove(oldItem);
                        }
 
                        //add or change new items
                        foreach (var newItem in filterDevices)
                        {
                            var elm = Devices.FirstOrDefault(x => x.Id == newItem.Id);
                            if (elm != null)
                            {
                                if (!newItem.Equals(elm))
                                    elm = newItem;
                            }
                            else
                            {
                                Devices.Add(newItem);
                            }
                        }
                    }
                    else
                    {
                        Devices.Clear();
                    }
                }
            });
 
            await UpdateContextMenu();
        }
Сильно сомневаюсь что такой подход можно назвать "потокобезопасным"
Далее я подумал, что если я выйду из страницы и резко зайду обратно -- у меня перепутаются все элементы и, весьма вероятно, продублируются или ещё чего похуже.
Решение 3: я подумал, что во время закрытия страницы будет уместным сделать cancelTokenSource.Cancel();
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
        private static CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
        private CancellationToken token = cancelTokenSource.Token;
        protected async Task UpdateAll()
        {
            await UpdateAllTest();
        }
 
 
        protected async Task UpdateAllTest()
        {
            var devices = await devicesRepository.GetDevices(currentDevice == null ? Root : currentDevice.Id);
 
            await Task.Run(() =>
            {
                lock (((ICollection)Devices).SyncRoot)
                {
                    if (devices != null)
                    {
                        var filterDevices = devices.FindAll(d => !d.IsArchived);
 
                        //remove old items
                        foreach (var oldItem in Devices)
                        {
                            if (token.IsCancellationRequested)
                                return;
 
                                if (filterDevices.Find(x => x.Id == oldItem.Id) == null)
                                Devices.Remove(oldItem);
                        }
 
                        //add or change new items
                        foreach (var newItem in filterDevices)
                        {
                            if (token.IsCancellationRequested)
                                return;
 
                            var elm = Devices.FirstOrDefault(x => x.Id == newItem.Id);
                            if (elm != null)
                            {
                                if (!newItem.Equals(elm))
                                    elm = newItem;
                            }
                            else
                            {
                                Devices.Add(newItem);
                            }
                        }
                    }
                    else
                    {
                        Devices.Clear();
                    }
                }
                cancelTokenSource.Cancel();
            });
 
            await UpdateContextMenu();
        }
Когда закрываю страницу делаю:
C#
1
2
3
4
5
        public override void Deactivate()
        {
            base.Deactivate();
            cancelTokenSource.Cancel();
        }
Ужасный код, я понимаю. Ищу помощи тут.

Добавлено через 9 минут
Также хотел упомянуть следущий момент:
C#
1
2
3
4
await Task.Run(() =>
{
    lock (((ICollection)Devices).SyncRoot)
    //..
тут мне кажется я вообще полную чушь написал.
0
Лучшие ответы (1)
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
06.10.2021, 14:07
Ответы с готовыми решениями:

BlockingCollection<T>, Add(T item, CancellationToken cancellationToken);
Возник вопрос в механизме метода Add() BlockingCollection. В реализации BlockingCollection есть возможность добавить как элемент...

Зачем нужен CancellationToken?
у CancellationTokenSource есть тот-же свойство IsCancellationRequested, и метод Cancel вызывается у CancellationTokenSource. Зачем они...

Не происходит отмена задачи (CancellationToken)
wpf создаю private CancellationTokenSource cts = new CancellationTokenSource(); на команде кнопки вперед реализую: private...

95
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
10.10.2021, 19:51
Студворк — интернет-сервис помощи студентам
Цитата Сообщение от limeniye Посмотреть сообщение
там правильно я написал "если возвращаю отображение картинки".
DeviceViewModel, в принципе, не может возвращать ОТОБРАЖЕНИЕ картинки.
Это не UI элемент.
Каким типом в DeviceViewModel отражена картинка?
GUID, ImegeSource, поток, другое?


И уже потом EntityImage строит по полученному Entity по данным из DeviceViewModel отображение картинки.
Вот надо понять в чём тормоза.
Просто сам вывод картинок в Xamarin это долго.
Или конкретно в этом случае получение данных для картинки по DeviceViewModel это долгий процесс.
0
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
10.10.2021, 19:52  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Никто этот Stream не использует.
ну тут нет, это уже излишки моего урезания кода.
Сейчас отправлю подробнее:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
        private async Task GetDocumentImageAsync(Guid documentId, Guid entityId)
        {
            var document = await GetDocument(documentId);
            if (document != null)
            {
                try
                {
                    GetEntityIcon(document, entityId);
                }
                catch (Exception ex)
                {
                    Logger.Error($"[EntityImage][GetDocumentImageAsync] {ex.Message}");
                }
            }
        }
 
 
        private async Task<Stream> GetDocument(Guid documentId)
        {
            try
            {
                var photoDocument = await Task.Run(() => _simpleClient.Documents.GetDocument(documentId));
                if (photoDocument != null)
                {
                    return photoDocument;
                }
            }
            catch (Exception ex)
            {
                Logger.Error($"[EntityImage][GetPersonImage] {ex.Message}");
            }
 
            return null;
        }
C#
1
2
3
4
5
6
7
8
9
        public void GetPersonImage(Entity entity)
        {
            // тут тоже какой-то код и вот:
 
            if (documentId != Guid.Empty)
            {
                Task.Run(() => GetDocumentImageAsync(documentId, entity.Id));
            }
        }
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
10.10.2021, 19:53
Цитата Сообщение от limeniye Посмотреть сообщение
а потом элементы списка прогружаются 1 за одним в течении 7 секунд
Это сё равно очень долго.
В этом же случае, это просто Frame?
За секунду должны успевать создаваться тысячи Frame.
0
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
10.10.2021, 19:58  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
DeviceViewModel, в принципе, не может возвращать ОТОБРАЖЕНИЕ картинки.
если возвращаю отображение картинки в xaml-код*, не знаю уже как тут правильнее объяснить, что имел ввиду

Добавлено через 3 минуты
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Каким типом в DeviceViewModel отражена картинка?
GUID, ImegeSource, поток, другое?
там ещё зависит от того, где эта картинка получается: локально, или через сервер.

Например локально будет так:
C#
1
deviceIcon = BitmapExtensions.LoadBitmapResource(typeof(EntityImage),$"{PathToImages}.Devices.dev_Device.png");
Но, там не везде png, может быть и картинка в формате .xml (конвертируется из svg). Тогда формат будет другой.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
10.10.2021, 20:02
Цитата Сообщение от limeniye Посмотреть сообщение
если возвращаю отображение картинки в xaml-код
Теперь понятно.

То есть основные тормоза создаёт controls:EntityImage ?
Надо тогда разбираться в его логике.

И мне всё равно кажется, что даже без картинок вывод 7 секунд - это очень много.
Хотя может для Xamarin это нормально.

Добавлено через 2 минуты
Цитата Сообщение от limeniye Посмотреть сообщение
Например локально будет так:
Я не знаю что это за метод BitmapExtensions.LoadBitmapResource?
В каком потоке выполняется?
Можно ли его dspsdfnm на потоке пула?
0
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
10.10.2021, 22:12  [ТС]
Одну из проблем я нашёл:

вот так не лагает
C#
1
2
3
4
5
6
7
  private void UpdateDevicesWithCancellationTokenChecker(CancellationToken token)
        {
            
            Task.Run(()=>
            {
                var id = currentDevice == null ? Root : currentDevice.Id;
                var devices = Task.Run(async () => await devicesRepository.GetDevices(id)).Result;
А вот так лагает:
C#
1
2
3
4
5
6
  private void UpdateDevicesWithCancellationTokenChecker(CancellationToken token)
        {
            var id = currentDevice == null ? Root : currentDevice.Id;
            var devices = Task.Run(async () => await devicesRepository.GetDevices(id)).Result;
            Task.Run(()=>
            {
Также, AsyncObservableCollection работает очень медленно. Обычный ObservableCollection сразу вываливает список, а AsyncObservableCollection отображает элементы около 7 секунд, как-будто есть какая-то задержка между элементами.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
10.10.2021, 22:40
Цитата Сообщение от limeniye Посмотреть сообщение
AsyncObservableCollection отображает элементы около 7 секунд, как-будто есть какая-то задержка между элементами
Так и есть.
У него же марщалинг в UI поток каждого действия.

Я поэтому и отказался от его использования.
Но вот я не пойму как вы используете простой ObservableCollection без маршалинга и синхронизации привязок.
На WPF простой ObservableCollection при изменении из других потоков выкидывает исключения.

Наверное, в Xamarin это реализовано по другому.
Это надо тестить, разбираться.
Документации толковой очень мало.
А на все эти тесты нет времени.

Цитата Сообщение от limeniye Посмотреть сообщение
Одну из проблем я нашёл:
Вы уверенны?
Вы проверяли, что у вас содержит var devices?
Там точно не нулл?
У вас же нет ожидания результата.

И сама по себе строчка Task.Run(async () => await devicesRepository.GetDevices(id)).Result, вроде, эквивалентна devicesRepository.GetDevices(id).Result.

Добавлено через 2 минуты
limeniye, и вы отказались от асинхронного метода?
Сделали UpdateDevicesWithCancellationTokenChecke r синхронным?
0
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
10.10.2021, 22:52  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Но вот я не пойму как вы используете простой ObservableCollection без маршалинга и синхронизации привязок.
На WPF простой ObservableCollection при изменении из других потоков выкидывает исключения.
да, это не только в Xamarin, даже в том же WPF мне удавалось сделать то же самое. Не помню точно, по-моему если Вы попробуете сделать это на том же .Net Standart 2.0, или на .Net 5 -- Вам тоже не выдаст никаких ошибок.

Добавлено через 53 секунды
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Там точно не нулл?
я уже переделал, как Вы и сказали.

Добавлено через 2 минуты
Цитата Сообщение от Элд Хасп Посмотреть сообщение
И сама по себе строчка Task.Run(async () => await devicesRepository.GetDevices(id)).Result , вроде, эквивалентна devicesRepository.GetDevices(id).Result.
GetDevices -- это асинхронный метод, поэтому я его синхронизировал.
Но, как я понимаю, если пишу Result -- то будет автоматическое ожидание, когда из devices понадобится взять значение, если оно ещё не пришло туда. Верно?

Добавлено через 1 минуту
Цитата Сообщение от Элд Хасп Посмотреть сообщение
limeniye, и вы отказались от асинхронного метода?
я всё сделал синхронно
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
11.10.2021, 00:12
Цитата Сообщение от limeniye Посмотреть сообщение
Но, как я понимаю, если пишу Result -- то будет автоматическое ожидание, когда из devices понадобится взять значение, если оно ещё не пришло туда. Верно?
Нет.
Я думаю, там у вас возвращается null.
Проверьте.

Добавлено через 22 секунды
Цитата Сообщение от limeniye Посмотреть сообщение
да, это не только в Xamarin, даже в том же WPF мне удавалось сделать то же самое. Не помню точно, по-моему если Вы попробуете сделать это на том же .Net Standart 2.0, или на .Net 5 -- Вам тоже не выдаст никаких ошибок.
Проверю.
1
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
11.10.2021, 00:57  [ТС]
Элд Хасп, тот метод, который возвращал null я переделал уже. Там будет пустой список.
0
 Аватар для Midian
608 / 390 / 198
Регистрация: 31.03.2013
Сообщений: 2,700
11.10.2021, 12:10
Цитата Сообщение от limeniye Посмотреть сообщение
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        private async Task<Stream> GetDocument(Guid documentId)
        {
            try
            {
                var photoDocument = await Task.Run(() => _simpleClient.Documents.GetDocument(documentId));
                if (photoDocument != null)
                {
                    return photoDocument;
                }
            }
            catch (Exception ex)
            {
                Logger.Error($"[EntityImage][GetPersonImage] {ex.Message}");
            }
            return null;
        }
А зачем вы тут проверяете на null, если потом все ровно возвращаете его?
не проще возвращать сразу photoDocument?
2
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
11.10.2021, 13:05  [ТС]
Цитата Сообщение от Midian Посмотреть сообщение
А зачем вы тут проверяете на null, если потом все ровно возвращаете его?
это легаси код

Действительно, я думаю там можно упростить.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
11.10.2021, 13:09
Цитата Сообщение от limeniye Посмотреть сообщение
Действительно, я думаю там можно упростить.
Не только упростить но и улучшить.
Или дополнить другим методом.
Для WPF нужен ImageSource.
Поэтому в задаче надо не поток возвращать, а создавать экземпляр ImageSource из этого потока, замораживать и возвращать его.
1
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
11.10.2021, 20:52  [ТС]
Сделав кучу тестов на новом, чистом проекте, выяснил, что проблема точно где-то в коде, или подходе его реализации.

Список, с 500 картинками, в тестовом проекте, прогружается за доли секунды без какой-либо виртуализации или способов оптимизации.

План исправления следующий:

- реализовать новый способ асинхронного отображения картинки в тестовом проекте;
- выпилить старую реализацию из проекта, выкинуть её в мусор и заменить новой.

Всем огромное спасибо за участие в теме. Много узнал нового.
Тема закрыта.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16165 / 11285 / 2891
Регистрация: 21.04.2018
Сообщений: 33,174
Записей в блоге: 2
11.10.2021, 21:20
Цитата Сообщение от limeniye Посмотреть сообщение
- реализовать новый способ асинхронного отображения картинки в тестовом проекте;
- выпилить старую реализацию из проекта, выкинуть её в мусор и заменить новой.
1
 Аватар для limeniye
1182 / 624 / 160
Регистрация: 19.04.2018
Сообщений: 2,923
15.10.2021, 15:21  [ТС]
Элд Хасп, сделал всё с нуля в другом, тестовом приложении.

Также хочу подметить, что если не использовать передачу в Dispatcher -- ошибок может не быть, но приложение, порой, работает криво. Поэтому теперь использую всегда.
Пример:
C#
1
Device.BeginInvokeOnMainThread(() => Items.Clear());
Создал Model, создал ViewModel -- всё как положено. Далее создал ImageAsync.

Из модели в ViewModel приходит событие добавления в список нового элемента.
Во ViewModel'е добавляю так, без await'a:
C#
1
2
3
4
5
6
7
            switch (e.Action)
            {
                case NotifyDictionaryChangedAction.Added:
                    Device.BeginInvokeOnMainThread(() => Items.Add(e.NewValue.ConverterToDeviceViewModel()));
                    break;
                 ...
            }
C#
1
2
3
4
5
6
7
8
9
10
11
12
        public ImageSource Image
        {
            get
            {
                if (ImageLoadState == ImageLoadStateEnum.Loaded)
                    return UploadedImage;
 
                if (ImageInfo != DownloadImage)
                    ImageLoadAsync();
                return ImageDefault;
            }
        }
Метод асинхронной загрузки изображения.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        private async void ImageLoadAsync()
        {
            if (ImageInfo == default)
                return;
            IEntityImageInfoViewModel ImageItemLoad = DownloadImage = ImageInfo;
 
            await Device.InvokeOnMainThreadAsync(async() =>
            {
                ImageSource image = await Task.Factory.StartNew(ImageLoad, ImageItemLoad);
 
                if (ImageItemLoad.Equals(ImageInfo))
                {
                    UploadedImage = image;
                    ImageLoadState = ImageLoadStateEnum.Loaded;
                }
            });
        }

image.Freeze(); нет в Xamarin и в UWP, по-моему, тоже. Поэтому делаю через await Device.InvokeOnMainThreadAsync
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
        protected ImageSource ImageLoad(object item)
        {
            if (item is IEntityImageInfoViewModel)
            {
                ImageSource image = ImageLoad((IEntityImageInfoViewModel)item);
                //image.Freeze();
                return image;
            }
            else
            {
                return null;
            }   
        }
Результат: сначала запрос, потом отображается весь список элементов, потом приложение подтормаживает и ожидает, пока прогрузятся картинки. Я подгружаю локальные картинки. При этом скролить список нельзя, пока картинки не прогрузятся.
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
15.10.2021, 15:21

Данные об отделах ( процент заполения )
Добрый вечер, помогите сделать запрос *Данные об отделах, у которых процент заполнения штата меньше 80* Файл снизу. Заранее спасибо :)

DataGridView1 не соблюдается условие заполения
Это окно редактирования документа в новой форме. Когда нажимаешь на button1, должна заполнится dataGridView1 согласно тому номеру который...

Почему CancellationToken не сбрасывает Token
Пытаюсь сбросить операцию ConnectAsync токеном. Но при выставлении значения 1 сек операцию не сбрасывается. В результате выполнения...

Проверка на правильность заполения полей
Как проверить форму на правильность заполнения полей...Например мне нужно проверить правильно ли у меня заполнено поле Емайл...Как это...

Отправка формы после проверки заполения reCAPTCHA
Можно ли сделать отправку формы после того, как reCAPTCHA примет статус &quot;Галочка&quot;?


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
96
Ответ Создать тему
Опции темы

Новые блоги и статьи
Когда логика программы не спасает от человеческих ошибок
Maks 18.08.2026
В последнее время всё чаще и чаще сталкиваюсь с таким явлением, как абсолютная невнимательность (или глупость) пользователей. Проявляется это чаще всего на работе в коллективе. Допустим, человек с. . .
Лето уходит
kumehtar 17.08.2026
Мысли в слух
kumehtar 17.08.2026
Забавно, насколько сейчас стала доступна информация. Например о магии, духовном развитии, медитациях, и других подобных направлениях, ранее зачастую тайных, передаваемых от учителя к ученику. Хотя. . .
Перемещение строк из ТЧ в другой документ с учетом текущего пробега
Maks 17.08.2026
Реализация из решения ниже выполнена на примере нетипового документа "Автозапчасти", с ТЧ "Шины". За основу взят алгоритм отсюда: https:/ / www. cyberforum. ru/ blogs/ 359708/ 10838. html Задача: . . .
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru