TryTake крадет элемент, который был добавлен совсем недавно в другом потоке

Объясняя ConcurrentBag<T>, онлайн-ссылка говорит, что:

...Итак, если быть точным, вызов Take дает вам элемент, добавленный больше всего. недавно в этой теме; если в этом потоке нет элементов, это дает вам элемент, добавленный последним в другом потоке, выбранном в случайный.

Фактически, в VS, если вы в глубине души, вы увидите этот комментарий к методу TrySteal (TryTake внутренне вызывает этот метод):

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

Теперь позвольте мне показать вам следующую программу:

using static System.Console;
using System.Collections.Concurrent;

IProducerConsumerCollection<Car> cars = new ConcurrentBag<Car>();

var addBlackCars = Task.Run(ProcessBlackCarModels);
var addNonBlackCars = Task.Run(ProcessNonBlackCarModels);
Task.WaitAll(addBlackCars, addNonBlackCars);
WriteLine($"At present, the repository contains {cars.Count} car(s).");
void ProcessNonBlackCarModels()
{
    Car car;
    car = new("Hyundai Creta", "Pearl");
    WriteLine($"Adding: {car} using task-{Task.CurrentId}");
    cars.TryAdd(car);
    Thread.Sleep(1000);

    car = new("Maruti Suzuki Alto 800", "Red");
    WriteLine($"Adding: {car} using task-{Task.CurrentId}");
    cars.TryAdd(car);
    Thread.Sleep(1000);

    car = new("Toyota Fortuner Avant", "Bronze");
    WriteLine($"Adding: {car} using task-{Task.CurrentId}");
    cars.TryAdd(car);
    Thread.Sleep(1000);

    WriteLine($"Task-{Task.CurrentId} will try removing one item now.");
    if (cars.Count > 0)
    {
        cars.TryTake(out Car removeCar);
        WriteLine($"Tried removing: {removeCar} using task-{Task.CurrentId}");
    }
}

void ProcessBlackCarModels()
{
    Car car;
    car = new("Toyota Fortuner Attitude", "Black");
    WriteLine($"Adding: {car} using task-{Task.CurrentId}");
    cars.TryAdd(car);
    Thread.Sleep(1000);

    car = new("Hyundai Creta Abyss", "Black");
    WriteLine($"Adding: {car} using task-{Task.CurrentId}");
    cars.TryAdd(car);


    // Putting a relatively long sleep so that the other task can finish in between.
    Thread.Sleep(5000);

    WriteLine($"Task-{Task.CurrentId} will try removing three items now.");

    for (int i = 0; i < 3; i++)
    {
        if (cars.Count > 0)
         {
            cars.TryTake(out Car removeCar);
            WriteLine($"Tried removing: {removeCar} using task-{Task.CurrentId}");
        }
    } 

}

// Using primary constructor
class Car(string model, string color)
{
    private string _model = model;
    private string _color = color;

    public override string ToString()
    {
        return $"[{_model}, {_color}]";
    }
}

Вот пример вывода:

Adding: [Toyota Fortuner Attitude, Black] using task-1
Adding: [Hyundai Creta, Pearl] using task-2
Adding: [Maruti Suzuki Alto 800, Red] using task-2
Adding: [Hyundai Creta Abyss, Black] using task-1
Adding: [Toyota Fortuner Avant, Bronze] using task-2
Task-2 will try removing one item now.
Tried removing: [Toyota Fortuner Avant, Bronze] using task-2
Task-1 will try removing three items now.
Tried removing: [Hyundai Creta Abyss, Black] using task-1
Tried removing: [Toyota Fortuner Attitude, Black] using task-1
Tried removing: [Hyundai Creta, Pearl] using task-1
At present, the repository contains 1 car(s).

Обратите внимание, что задаче 1 необходимо украсть третий элемент в этих выходных данных, и она делает это путем удаления элемента, который был добавлен задачей 2 в начале, т. е. он был добавлен последним.

Насколько я понимаю, задача-1 должна была удалить элемент задачей-2, которая была добавлена ​​последней, но не последней. Пожалуйста, помогите мне исправить это понимание.

Из любопытства, вы имеете в виду конкретное использование коллекции ConcurrentBag<T> или вам просто интересно узнать об этой коллекции в целом? Я спрашиваю, потому что лично я никогда не пользовался этой коллекцией и, наверное, никогда не буду.

Theodor Zoulias 27.04.2024 22:02
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
60
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я просмотрел репозиторий GitHub для Concurrentbag::ThreadLocalListcode и, глядя на приведенный ниже код кражи, он соответствует вашему выводу.

В контексте класса ConcurrentBag в .NET элементы добавляются и извлекаются из заголовка локального списка каждого потока. Это означает, что локальный список каждого потока ведет себя как стек (последний вход — первый выход — LIFO). Последний добавленный элемент («голова» списка) будет первым, который будет использован тем же потоком.

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

        /// <summary>
        /// Steal an item from the tail of the list
        /// </summary>
        /// <param name = "result">the removed item</param>
        /// <param name = "remove">remove or peek flag</param>
        internal void Steal(out T result, bool remove)
        {
            Node tail = m_tail;
            Debug.Assert(tail != null);
            if (remove) // Take operation
            {
                m_tail = m_tail.m_prev;
                if (m_tail != null)
                {
                    m_tail.m_next = null;
                }
                else
                {
                    m_head = null;
                }
                // Increment the steal count
                m_stealCount++;
            }
            result = tail.m_value;
        }

Спасибо за ответ. Это также развеивает сомнения.

Vaskaran Sarcar 05.05.2024 07:19
Ответ принят как подходящий

Хотя книга Джозефа Альбахари «Потоки в C#» — отличное чтение, но следует отметить, что 1) порядок не фиксирован в документации и является деталью реализации 2) исходная статья была написана довольно давно, во времена .NET Framework.

Судя по исходному коду реализации .NET Frameworks, он мог быть изменен с момента написания книги (или, возможно, автор ошибся). Если я правильно понимаю - элементы добавляются в заголовок связанного списка (т.е. новый элемент становится новым заголовком, название немного сбивает с толку) - источник и удаляется из хвоста при краже ( источник) :

/// <summary>
/// Steal an item from the tail of the list
/// </summary>
/// <param name = "result">the removed item</param>
/// <param name = "remove">remove or peek flag</param>
internal void Steal(out T result, bool remove)  

А репродукция 4.7.2 ведет себя по методу LIFO за кражу - демо @dotnetfiddle.

А текущая реализация добавляет в хвост очереди и крадет из головы:

/// <summary>Steal an item from the head of the queue.</summary>
/// <param name = "result">the removed item</param>
/// <param name = "take">true to take the item; false to simply peek at it</param>
internal bool TrySteal([MaybeNullWhen(false)] out T result, bool take)

В результате происходит то же поведение, что вы наблюдали (демо @dotnetfiddle)

Еще один пример того, почему людям не следует полагаться на детали реализации.

Etienne de Martel 27.04.2024 23:14

Другие вопросы по теме