В коллекцию иногда попадает null, хотя не должен

Сегодня пол-дня дебажил код, но не могу найти ошибку :man_shrugging:

        public static TwitchVod ParseVodInfo(JObject json)
        {
            try
            {
                ulong vodId = json.Value<ulong>("id");
                JToken jt = json.Value<JToken>("stream_id");
                ulong streamId = jt != null && jt.Type != JTokenType.Null ? jt.Value<ulong>() : 0U;
                string userLogin = json.Value<string>("user_login");
                string title = json.Value<string>("title");
                string description = json.Value<string>("description");
                string dateCreated = json.Value<string>("created_at");
                if (!DateTime.TryParseExact(dateCreated, "MM/dd/yyyy HH:mm:ss",
                    null, System.Globalization.DateTimeStyles.None, out DateTime creationDate))
                {
                    creationDate = DateTime.MaxValue;
                }
                string datePublished = json.Value<string>("published_at");
                if (!DateTime.TryParseExact(datePublished, "MM/dd/yyyy HH:mm:ss",
                    null, System.Globalization.DateTimeStyles.None, out DateTime publishedDate))
                {
                    publishedDate = DateTime.MaxValue;
                }
                string url = json.Value<string>("url");
                string thumbnailTemplateUrl = json.Value<string>("thumbnail_url");
                string viewable = json.Value<string>("viewable");
                ulong viewCount = json.Value<ulong>("view_count");
                string language = json.Value<string>("language");
                string vodTypeString = json.Value<string>("type");

                TwitchVodType vodType = GetVodType(vodTypeString);

                TimeSpan duration = TimeSpan.Zero;

                TwitchUserResult twitchUserResult = TwitchUser.Get(userLogin);
                DateTime deletionDeletion = DateTime.MaxValue;
                if (vodType == TwitchVodType.Archive && twitchUserResult.ErrorCode == 200 && twitchUserResult.User != null)
                {
                    bool isPartner = twitchUserResult.User.BroadcasterType == TwitchBroadcasterType.Partner;
                    deletionDeletion = creationDate.AddDays(isPartner ? 60d : 14d);
                }

                TwitchGame game;
                VideoMetadataResult videoMetadataResult = TwitchApiGql.GetVodMetadata(vodId.ToString(), userLogin);
                if (videoMetadataResult.ErrorCode == 200)
                {
                    int seconds = videoMetadataResult.Metadata.GetVideoLengthSeconds();
                    if (seconds > 0)
                    {
                        duration = TimeSpan.FromSeconds(seconds);
                    }

                    game = videoMetadataResult.Metadata.GetGameInfo();
                }
                else
                {
                    game = TwitchGame.CreateUnknownGame();
                }

                return new TwitchVod(vodId, title, description, duration, game, creationDate,
                    publishedDate, deletionDeletion, url, thumbnailTemplateUrl, viewable, viewCount,
                    language, vodType, streamId, twitchUserResult.User,
                    json.ToString(), videoMetadataResult.Metadata);
            }
            catch (Exception ex)
            {
			    //сюда не заходит
                System.Diagnostics.Debug.WriteLine(ex.Message);
                return null;
            }
        }
        public List<TwitchVod> GetVideosMultiThreaded(uint maxVideos,
            int millisecondsTimeout, CancellationToken cancellationToken = default)
        {
            List<TwitchVod> resultList = new List<TwitchVod>();
            if (maxVideos == 0U) { return resultList; }

            JArray jaRawVods = GetVideosRaw(maxVideos);
            System.Diagnostics.Debug.WriteLine(jaRawVods.ToString()); //здесь все элементы не-null

            if (jaRawVods.Count > 0)
            {
                var tasks = jaRawVods.Select((j) => Task.Run(() =>
                {
                    TwitchVod vod = Utils.ParseVodInfo(j as JObject);
                    if (vod == null)
                    {
						//сюда тоже не заходит
                        System.Diagnostics.Debug.WriteLine(j);
                    }
                    resultList.Add(vod);
                }));

                if (millisecondsTimeout > 0)
                {
                    Task.WhenAll(tasks).Wait(millisecondsTimeout, cancellationToken);
                }
                else
                {
                    Task.WhenAll(tasks).Wait(cancellationToken);
                }
            }

            return resultList;
        }

Вызывается так:

   List<TwitchVod> vods = twitchUserResult.User.GetVideosMultiThreaded(10U, 0);

Иногда в коллекцию попадает null. Не могу понять, каким образом :thinking:


Возьми какую-нибудь коллекцию с синхронизацией.
«In C#, a thread-safe list is called a ConcurrentBag<T>»

В чём принципиальная разница? И там и там ведь ожидается завершение всех тасок.
И почему в конце написано, что бэг должен быть пуст? :thinking:
Снимок экрана 2023-10-06 134617
Туда ведь только что 500 интегеров добавили.

Кажется, нашёл

                var tasks = jaRawVods.Select((j) => Task.Run(() =>
                {
                    TwitchVod vod = Utils.ParseVodInfo(j as JObject);
                    if (vod == null)
                    {
                        System.Diagnostics.Debug.WriteLine(j);
                    }

                    resultList.Add(vod); //<<< вот тут нужна синхронизация
                }));

:thinking: Оно вообще работать не должно. Должно крашиться. Но, почему-то, нет :man_shrugging:

Оно вообще работать не должно.

Многопоточное увеличение размера коллекции без синхронизации доступа к этому целочисленному полю.

Должно крашиться.

Почему должно крешиться? Просто может добавить размер два раза, а потом в топовый элемент записать пару значений на одно место.

Аллокация новой памяти под массив редко происходит (потому что это делается сразу с запасом). И на многопоточный конфликт для этого сценария тяжело попасть, то есть будет ли оно крашится (крайне редко) - непонятно, надо смотреть, как память выделяется/удаляется в реализации. Так как удаляется не синхронно, а сборщиком мусора, это ещё снижает шанс на креш.

почему в конце написано, что бэг должен быть пуст?

Потому что метод TryTake удаляет один элемент из коллекции. Так как происходит ожидание завершения всех потоков, то они вместе удалят все элементы. Это и проверяется в проверке.

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

Видимо, так и происходит :man_shrugging:

Да-да, я знаю.

Ок, понял.

Нет. Просто неясно, каким станет значение переменной (какой поток будет последним). А вот то, что значение элемента массива остаётся из-за гонок пустым - вот это потом приводит (может приводить) к крешу, потому что обращение по нулевому указателю.

А где мне лайк? Я тут зря стараюсь?

Играть ради циферок - пустая затея. В любой момент сервер могут отключить и циферки обнулятся.
Вот если бы ты прямым текстом написал, что у меня все потоки одновременно пишут в коллекцию - тогда да. Можно и лайк просить. Лайк это как донат - субстанция сугубо добровольная.

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

А ещё написал бы небольшой учебник про разработку при многопоточности… Понимаю.

у тебя снижается показатель уровня бесполезности

Пиши документацию, постановку задачи в виде текста на русском языке, что есть, что надо сделать, почему Progress много экземпляров (а не один для синхронизации с потоком UI), откуда возникают те или иные программные навороты в твоём коде, и какими такими необходимостями они вызваны, почему без них нельзя обойтись.