List.Add () benang pengaman

91

Saya memahami bahwa secara umum List bukanlah thread safe, namun apakah ada yang salah dengan menambahkan item ke dalam daftar jika thread tidak pernah melakukan operasi lain di daftar (seperti melewatinya)?

Contoh:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});
e36M3
sumber
3
Duplikat persis dari keamanan benang Daftar <T>
JK.
Saya pernah menggunakan Daftar <T> hanya untuk menambahkan objek baru dari beberapa tugas yang dijalankan secara paralel. Kadang-kadang, sangat jarang, ketika mengulang melalui daftar setelah semua tugas selesai, itu berakhir dengan catatan yang nol, yang, jika tidak ada utas tambahan yang terlibat, secara praktis tidak mungkin hal ini terjadi. Saya rasa, ketika daftar secara internal mengalokasikan ulang elemennya untuk diperluas, entah bagaimana utas lain mengacaukannya dengan mencoba menambahkan objek lain. Jadi bukan ide yang bagus untuk melakukannya!
osmiumbin
Persis apa yang saat ini saya lihat @osmiumbin sehubungan dengan objek yang entah kenapa menjadi null ketika hanya menambahkan dari beberapa utas. Terima kasih atas konfirmasinya.
Blackey

Jawaban:

75

Di balik layar banyak hal terjadi, termasuk mengalokasikan ulang buffer dan menyalin elemen. Kode itu akan menyebabkan bahaya. Sederhananya, tidak ada operasi atomik saat menambahkan ke daftar, setidaknya properti "Panjang" perlu diperbarui, dan item perlu diletakkan di lokasi yang tepat, dan (jika ada variabel terpisah) yang dibutuhkan indeks untuk diperbarui. Beberapa utas dapat saling menginjak-injak. Dan jika pertumbuhan diperlukan maka masih banyak lagi yang terjadi. Jika ada sesuatu yang menulis ke daftar, tidak ada hal lain yang harus dibaca atau ditulis di sana.

Di .NET 4.0 kami memiliki koleksi serentak, yang mudah digunakan untuk threadsafe dan tidak memerlukan kunci.

Talljoe
sumber
Itu sangat masuk akal, saya pasti akan melihat koleksi Concurrent baru untuk ini. Terima kasih.
e36M3
11
Perhatikan bahwa tidak ada ConcurrentListtipe bawaan. Ada tas, kamus, tumpukan, antrian, dll secara bersamaan, tetapi tidak ada daftar.
LukeH
11

Pendekatan Anda saat ini tidak thread-safe - Saya sarankan untuk menghindari ini sama sekali - karena pada dasarnya Anda melakukan transformasi data PLINQ mungkin merupakan pendekatan yang lebih baik (saya tahu ini adalah contoh yang disederhanakan tetapi pada akhirnya Anda memproyeksikan setiap transaksi ke "status" lain "objek).

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();
Gelas pecah
sumber
Saya memberikan contoh yang terlalu disederhanakan untuk menekankan aspek List.Add yang saya minati. My Parallel.Foreach sebenarnya akan melakukan banyak pekerjaan dan tidak akan menjadi transformasi data yang sederhana. Terima kasih.
e36M3
4
koleksi bersamaan dapat melumpuhkan kinerja paralel Anda jika digunakan tidak diperlukan - hal lain yang dapat Anda lakukan adalah menggunakan larik ukuran tetap dan menggunakan Parallel.Foreachbeban berlebih yang mengambil indeks - dalam hal ini setiap utas memanipulasi entri larik yang berbeda dan Anda harus aman.
BrokenGlass
6

Bukan hal yang tidak masuk akal untuk bertanya. Ada yang kasus di mana metode yang dapat menyebabkan masalah benang-keselamatan dalam kombinasi dengan metode lain yang aman jika mereka adalah satu-satunya metode yang disebut.

Namun, ini jelas bukan masalahnya, ketika Anda mempertimbangkan kode yang ditampilkan di reflektor:

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

Bahkan jika EnsureCapacityitu sendiri threadsafe (dan tentu saja tidak), kode di atas jelas tidak akan menjadi threadsafe, mengingat kemungkinan panggilan simultan ke operator tambahan menyebabkan kesalahan penulisan.

Kunci, gunakan ConcurrentList, atau mungkin gunakan antrean bebas kunci sebagai tempat menulis beberapa utas, dan baca darinya - baik secara langsung atau dengan mengisi daftar dengannya - setelah mereka menyelesaikan pekerjaan mereka (saya berasumsi bahwa beberapa penulisan simultan diikuti dengan pembacaan utas tunggal adalah pola Anda di sini, dilihat dari pertanyaan Anda, karena jika tidak, saya tidak dapat melihat bagaimana kondisi di mana Addsatu-satunya metode yang dipanggil dapat digunakan).

Jon Hanna
sumber
6

Jika Anda ingin menggunakan List.adddari beberapa utas dan tidak peduli tentang pengurutan, maka Anda mungkin tidak memerlukan kemampuan pengindeksan a List, dan harus menggunakan beberapa koleksi bersamaan yang tersedia.

Jika Anda mengabaikan saran ini dan hanya melakukannya add, Anda dapat membuat addutas aman tetapi dalam urutan yang tidak dapat diprediksi seperti ini:

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

...

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

Jika Anda menerima pemesanan yang tidak dapat diprediksi ini, kemungkinan besar Anda seperti yang disebutkan sebelumnya tidak memerlukan koleksi yang dapat melakukan indexing seperti pada someList[i].

tomsv
sumber
5

Ini akan menyebabkan masalah, karena List dibuat di atas larik dan tidak aman untuk thread, Anda mungkin mendapatkan pengecualian indeks di luar batas atau beberapa nilai menimpa nilai lain, tergantung di mana utasnya. Pada dasarnya, jangan lakukan itu.

Ada beberapa potensi masalah ... Jangan. Jika Anda memerlukan koleksi thread yang aman, gunakan kunci atau salah satu dari koleksi System.Collections.Concurrent.

Linkgoron
sumber
5

Saya memecahkan masalah saya menggunakan ConcurrentBag<T>alih-alih List<T>seperti ini:

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

Apakah ada yang salah dengan menambahkan item ke dalam daftar jika utas tidak pernah melakukan operasi lain di daftar?

Jawaban singkatnya: ya.

Jawaban panjang: jalankan program di bawah ini.

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();
    }
}
Bas Smit
sumber
@royi apakah Anda menjalankan ini pada mesin inti tunggal?
Bas Smit
Hai, Saya rasa ada masalah dengan contoh ini karena menyetel AutoResetEvent setiap kali menemukan nomor 1000. Karena dapat memproses utas ini agak kapan pun ia mau dapat mencapai 1000 sebelum mencapai 999 misalnya. Jika Anda menambahkan Console.WriteLine dalam metode AddTol maka Anda akan melihat bahwa penomoran tidak berurutan.
Dave Walker
@ Dave, pengaturan acara ketika i == 0
Bas Smit
2

Seperti yang telah dikatakan orang lain, Anda dapat menggunakan koleksi bersamaan dari System.Collections.Concurrentnamespace. Jika Anda dapat menggunakan salah satunya, ini lebih disukai.

Tetapi jika Anda benar-benar menginginkan daftar yang baru saja disinkronkan, Anda dapat melihat SynchronizedCollection<T>-Class in System.Collections.Generic.

Perhatikan bahwa Anda harus menyertakan rakitan System.ServiceModel, yang juga merupakan alasan mengapa saya tidak begitu menyukainya. Tapi terkadang saya menggunakannya.

lembut maf
sumber