List.Add () iş parçacığı güvenliği


91

Genel olarak bir Listenin iş parçacığı için güvenli olmadığını anlıyorum, ancak iş parçacıkları listede başka hiçbir işlem yapmıyorsa (örneğin, gezinmek gibi) bir listeye öğe eklemede yanlış bir şey var mı?

Misal:

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


Bir keresinde List <T> 'yi yalnızca paralel çalışan birden çok görevden yeni nesneler eklemek için kullandım. Bazen, çok nadiren, tüm görevler tamamlandıktan sonra liste boyunca yinelendiğinde, boş olan bir kayıtla sonuçlanırdı, bu da fazladan iş parçacığı olmasaydı, bunun olması neredeyse imkansız olurdu. Sanırım liste, elemanlarını genişletmek için dahili olarak yeniden tahsis ederken, bir şekilde başka bir iş parçacığı, başka bir nesne eklemeye çalışarak onu karıştırdı. Bu yüzden bunu yapmak iyi bir fikir değil!
osmiumbin

Tam olarak şu anda @osmiumbin'i bir nesnenin açıklanamaz bir şekilde boş olmasıyla ilgili olarak gördüğüm şey, birden fazla iş parçacığından eklerken. Onay için teşekkürler.
Blackey

Yanıtlar:


75

Tamponların yeniden tahsis edilmesi ve öğelerin kopyalanması gibi perde arkasında pek çok şey olur. Bu kod tehlikeye neden olur. Çok basit bir ifadeyle, bir listeye eklerken atomik işlem yoktur, en azından "Uzunluk" özelliğinin güncellenmesi gerekir ve öğenin doğru konuma yerleştirilmesi gerekir ve (ayrı bir değişken varsa) dizinin güncellenecek. Birden çok iş parçacığı birbirinin üzerinden geçebilir. Ve eğer bir büyüme gerekiyorsa, o zaman çok daha fazlası var. Bir listeye bir şey yazıyorsa, başka hiçbir şey onu okumamalı veya yazmamalıdır.

.NET 4.0'da eşzamanlı koleksiyonlarımız var, bunlar kullanıma hazır evreler için güvenli ve kilit gerektirmeyen.


Bu çok mantıklı, bunun için kesinlikle yeni Concurrent koleksiyonlarına bakacağım. Teşekkür ederim.
e36M3

11
Yerleşik tip olmadığını unutmayın ConcurrentList. Eşzamanlı çantalar, sözlükler, yığınlar, kuyruklar vb. Var, ancak liste yok.
LukeH

11

Şu anki yaklaşımınız iş parçacığı açısından güvenli değil - bundan tamamen kaçınmanızı öneririm - temelde bir veri dönüşümü yapıyorsunuz PLINQ daha iyi bir yaklaşım olabilir (bunun basitleştirilmiş bir örnek olduğunu biliyorum ama sonunda her işlemi başka bir "duruma yansıtıyorsunuz " nesne).

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

List.Add'ın ilgilendiğim yönünü vurgulamak için aşırı basitleştirilmiş bir örnek sundum. Benim Parallel.Foreach'im aslında iyi bir iş çıkaracak ve basit bir veri dönüşümü olmayacak. Teşekkürler.
e36M3

4
eşzamanlı koleksiyonlar gereksiz kullanılırsa paralel performansınızı sekteye uğratabilir - yapabileceğiniz başka bir şey de sabit boyutlu bir dizi kullanmak ve Parallel.Foreachindeksi alan aşırı yüklemeyi kullanmaktır - bu durumda her iş parçacığı farklı bir dizi girişini işliyor ve güvende olmalısınız.
BrokenGlass

6

Sormak mantıksız bir şey değil. Orada vardır dedikleri tek yöntem ise diğer yöntemlerle birlikte iş parçacığı emniyet sorunlarına neden olabilir yöntemleri güvenlidir davaları.

Ancak, reflektörde gösterilen kodu göz önünde bulundurduğunuzda, bu açıkça bir durum değildir:

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

EnsureCapacityKendi içinde iş parçacığı güvenli olsa bile (ve kesinlikle değildir), yukarıdaki kod, artım operatörüne eşzamanlı çağrıların yanlış yazmalara neden olma olasılığı göz önüne alındığında, açıkça iş parçacığı güvenli olmayacaktır.

Ya kilitleyin, ConcurrentList'i kullanın ya da birden fazla iş parçacığının yazdığı yer için kilitsiz bir kuyruk kullanın ve işlerini bitirdikten sonra - ya doğrudan ya da bir listeyi doldurarak - ondan okuyun (varsayıyorum ki Birden çok eşzamanlı yazma ve ardından tek iş parçacıklı okuma burada sizin kalıbınızdır, sorunuza bakılırsa, aksi takdirde Addçağrılan tek yöntemin nerede olduğu koşulun herhangi bir işe yarayabileceğini göremiyorum ).


6

Birden List.addçok iş parçacığından kullanmak istiyorsanız ve sıralamayı önemsemiyorsanız, muhtemelen bir Listzaten indeksleme yeteneğine ihtiyacınız yoktur ve bunun yerine mevcut eşzamanlı koleksiyonlardan bazılarını kullanmanız gerekir.

Bu tavsiyeyi görmezden gelirseniz ve yalnızca yaparsanız add, addipliği güvenli hale getirebilirsiniz, ancak bunun gibi öngörülemeyen bir sırayla:

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

...

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

Bu öngörülemeyen sıralamayı kabul ederseniz, daha önce de belirtildiği gibi, indeksleme yapabilen bir koleksiyona ihtiyacınız olmayabilir someList[i].


5

Liste bir dizi üzerine kurulduğundan ve iş parçacığı güvenli olmadığından, bu sorunlara neden olabilir, iş parçacığının nerede olduğuna bağlı olarak sınırların dışında bir istisna veya diğer değerleri geçersiz kılan bazı değerler elde edebilirsiniz. Temel olarak, bunu yapma.

Birden fazla potansiyel sorun var ... Sadece yapma. İş parçacığı için güvenli bir koleksiyona ihtiyacınız varsa, bir kilit veya System.Collections.Concurrent koleksiyonlarından birini kullanın.



2

İş parçacıkları listede başka hiçbir işlem yapmazsa, bir listeye öğe eklemede yanlış bir şey var mı?

Kısa cevap: evet.

Uzun cevap: aşağıdaki programı çalıştırın.

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 bunu tek çekirdekli bir makinede mi çalıştırıyorsunuz?
Bas Smit

Merhaba, 1000 sayısını bulduğunda AutoResetEvent'i ayarladığı için bu örnekte bir sorun olduğunu düşünüyorum. Çünkü bu konuları istediği zaman işleyebiliyor, örneğin 999'a ulaşmadan önce 1000'e ulaşabiliyor. AddTol yöntemine bir Console.WriteLine eklerseniz, numaralandırmanın sırayla olmadığını göreceksiniz.
Dave Walker

@dave, i == 0 olduğunda olayı ayarlıyor
Bas Smit

2

Diğerlerinin daha önce söylediği gibi, System.Collections.Concurrentad alanından eşzamanlı koleksiyonları kullanabilirsiniz . Bunlardan birini kullanabiliyorsanız bu tercih edilir.

Ama gerçekten senkronize edilmiş bir liste istiyorsanız, SynchronizedCollection<T>-Class in'e bakabilirsiniz System.Collections.Generic.

System.ServiceModel derlemesini dahil etmeniz gerektiğine dikkat edin, bu da onu çok sevmememin nedenidir. Ama bazen kullanıyorum.


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.