List.Add () 스레드 안전성


87

일반적으로 목록이 스레드로부터 안전하지 않다는 것을 이해합니다. 그러나 스레드가 목록에서 다른 작업 (예 : 순회)을 수행하지 않는 경우 목록에 항목을 추가하는 데 문제가 있습니까?

예:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});


한 번은 List <T>를 사용하여 병렬로 실행되는 여러 작업의 새 개체를 추가했습니다. 때로는 매우 드물지만 모든 작업이 완료된 후 목록을 반복 할 때 null 인 레코드가 생성되어 추가 스레드가 포함되지 않았다면 실제로 불가능했을 것입니다. 목록이 확장하기 위해 내부적으로 요소를 다시 할당 할 때 다른 스레드가 다른 개체를 추가하려고 시도하여 어떻게 든 엉망으로 만든 것 같습니다. 그래서 그렇게하는 것은 좋은 생각이 아닙니다!
osmiumbin

단순히 여러 스레드에서 추가 할 때 설명 할 수없는 null 인 개체와 관련하여 현재 @osmiumbin을보고 있습니다. 확인 감사합니다.
Blackey

답변:


75

배후에서는 버퍼 재 할당 및 요소 복사를 포함하여 많은 일이 발생합니다. 이 코드는 위험을 초래합니다. 아주 간단히 말해서 목록에 추가 할 때 원자 적 연산이 없으며 최소한 "Length"속성이 업데이트되어야하고 항목이 올바른 위치에 있어야하며 (별도의 변수가있는 경우) 인덱스가 필요합니다. 업데이트 할. 여러 스레드가 서로를 짓밟을 수 있습니다. 그리고 성장이 필요하다면 더 많은 일이 진행됩니다. 목록에 무언가를 쓰고 있다면 다른 어떤 것도 그것을 읽거나 쓰지 않아야합니다.

.NET 4.0에는 쉽게 스레드로부터 안전하고 잠금이 필요하지 않은 동시 컬렉션이 있습니다.


그것은 완벽하게 말이되며, 나는 이것을 위해 새로운 Concurrent 컬렉션을 확실히 볼 것입니다. 감사합니다.
e36M3 2011

11
기본 제공 ConcurrentList유형이 없습니다 . 동시 백, 사전, 스택, 대기열 등이 있지만 목록은 없습니다.
LukeH 2011

11

현재 접근 방식은 스레드로부터 안전하지 않습니다. 기본적으로 데이터 변환을 수행하기 때문에 PLINQ가 더 나은 접근 방식이 될 수 있기 때문에이 모든 것을 피하는 것이 좋습니다 (이는 간단한 예이지만 결국에는 각 트랜잭션을 다른 "상태로 프로젝션합니다." "개체).

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();

내가 관심이있는 List.Add의 측면을 강조하기 위해 지나치게 단순화 된 예제를 제시했습니다. My Parallel.Foreach는 실제로 많은 양의 작업을 수행하고 단순한 데이터 변환이 아닙니다. 감사.
e36M3 2011

4
동시 수집은 필요하지 않은 경우 병렬 성능을 손상시킬 수 있습니다. 또 다른 방법은 고정 크기 배열을 사용하고 Parallel.Foreach인덱스를 받는 오버로드를 사용하는 것입니다.이 경우 각 스레드가 다른 배열 항목을 조작하므로 안전해야합니다.
BrokenGlass 2011

6

물어 보는 것은 불합리한 일이 아닙니다. 이 있습니다 그들이라는 유일한 방법이 있다면 다른 방법과 결합하여 스레드 안전 문제를 일으킬 수있는 방법이 안전 경우가.

그러나 리플렉터에 표시된 코드를 고려할 때 이것은 분명히 해당되지 않습니다.

public void Add(T item)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    this._items[this._size++] = item;
    this._version++;
}

비록 EnsureCapacity그 자체가 스레드 세이프 (그리고 가장 확실하지 않음)이더라도, 위의 코드는 증분 연산자에 대한 동시 호출이 잘못된 쓰기를 유발할 가능성을 고려할 때 분명히 스레드 세이프가 아닐 것입니다.

잠금, ConcurrentList 사용 또는 잠금없는 큐를 여러 스레드가 쓰는 장소로 사용하고, 작업을 완료 한 후 직접 또는 목록을 채워서 읽습니다 (나는 여러 동시 쓰기와 단일 스레드 읽기가 여기에서 귀하의 패턴입니다. 그렇지 않으면 Add호출 된 유일한 방법이 어디 에서 어떤 용도로 사용될 수 있는지 알 수 없습니다 .)


6

List.add여러 스레드에서 사용 하고 순서에 신경 쓰지 않는 경우 List어쨌든 인덱싱 기능이 필요하지 않으며 대신 사용 가능한 동시 컬렉션 중 일부를 사용해야합니다.

이 충고를 무시하고 만하면 스레드를 안전하게 add만들 수 add있지만 다음과 같이 예측할 수없는 순서로 만들 수 있습니다 .

private Object someListLock = new Object(); // only once

...

lock (someListLock)
{
    someList.Add(item);
}

이 예측할 수없는 순서를 수락하면 앞서 언급 한대로에서와 같이 인덱싱을 수행 할 수있는 컬렉션이 필요하지 않을 가능성이 있습니다 someList[i].


5

List가 배열 위에 만들어지고 스레드로부터 안전하지 않기 때문에 문제가 발생할 수 있습니다. 스레드의 위치에 따라 범위를 벗어난 예외 또는 일부 값이 다른 값을 재정의 할 수 있습니다. 기본적으로하지 마십시오.

잠재적 인 문제가 여러 개 있습니다 ... 그냥하지 마세요. 스레드로부터 안전한 컬렉션이 필요한 경우 잠금 또는 System.Collections.Concurrent 컬렉션 중 하나를 사용합니다.


5

ConcurrentBag<T>대신 사용하여 내 문제를 해결 List<T>했습니다.

ConcurrentBag<object> list = new ConcurrentBag<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

2

스레드가 목록에서 다른 작업을 수행하지 않는 경우 단순히 항목을 목록에 추가하는 데 문제가 있습니까?

짧은 대답 : 예.

긴 대답 : 아래 프로그램을 실행하십시오.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

class Program
{
    readonly List<int> l = new List<int>();
    const int amount = 1000;
    int toFinish = amount;
    readonly AutoResetEvent are = new AutoResetEvent(false);

    static void Main()
    {
        new Program().Run();
    }

    void Run()
    {
        for (int i = 0; i < amount; i++)
            new Thread(AddTol).Start(i);

        are.WaitOne();

        if (l.Count != amount ||
            l.Distinct().Count() != amount ||
            l.Min() < 0 ||
            l.Max() >= amount)
            throw new Exception("omg corrupted data");

        Console.WriteLine("All good");
        Console.ReadKey();
    }

    void AddTol(object o)
    {
        // uncomment to fix
        // lock (l) 
        l.Add((int)o);

        int i = Interlocked.Decrement(ref toFinish);

        if (i == 0)
            are.Set();
    }
}

@royi 단일 코어 머신에서 실행 중입니까?
Bas Smit

안녕, 나는 그것이 숫자 1000을 찾을 때마다 AutoResetEvent를 설정하기 때문에이 예제에 문제가 있다고 생각합니다. 원할 때마다 이러한 스레드를 처리 할 수 ​​있기 때문에 예를 들어 999에 도달하기 전에 1000에 도달 할 수 있습니다. AddTol 메서드에 Console.WriteLine을 추가하면 번호 매기기 순서가 잘못되었음을 알 수 있습니다.
Dave Walker

@dave, i == 0 일 때 이벤트 설정
Bas Smit

2

다른 사람들이 이미 말했듯이 System.Collections.Concurrent네임 스페이스 에서 동시 컬렉션을 사용할 수 있습니다 . 그중 하나를 사용할 수 있다면 이것이 선호됩니다.

당신은 정말 그냥 동기화 목록을 원한다면, 당신은 볼 수 있었다 SynchronizedCollection<T>에서 -Class System.Collections.Generic.

System.ServiceModel 어셈블리를 포함해야했는데, 이것이 제가 그다지 좋아하지 않는 이유이기도합니다. 하지만 때때로 나는 그것을 사용합니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.