سؤال

في المقتطفين التاليين، هل الأول آمن أم يجب عليك القيام بالثاني؟

أعني بالأمان هل كل مؤشر ترابط مضمون لاستدعاء الطريقة على Foo من نفس تكرار الحلقة التي تم إنشاء مؤشر الترابط فيها؟

أم يجب عليك نسخ المرجع إلى متغير جديد "محلي" لكل تكرار للحلقة؟

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
{      
    Thread thread = new Thread(() => f.DoSomething());
    threads.Add(thread);
    thread.Start();
}

-

var threads = new List<Thread>();
foreach (Foo f in ListOfFoo)
{      
    Foo f2 = f;
    Thread thread = new Thread(() => f2.DoSomething());
    threads.Add(thread);
    thread.Start();
}

تحديث: كما هو مذكور في إجابة جون سكيت، هذا ليس له أي علاقة بالخيوط على وجه التحديد.

هل كانت مفيدة؟

المحلول

يحرر:كل هذا يتغير في C# 5، مع تغيير مكان تعريف المتغير (في نظر المترجم).من C # 5 وما بعده، هم نفس الشيء.


قبل ج#5

والثاني آمن؛الأول ليس كذلك.

مع foreach, ، تم الإعلان عن المتغير الخارج الحلقة - أي

Foo f;
while(iterator.MoveNext())
{
     f = iterator.Current;
    // do something with f
}

وهذا يعني أنه لا يوجد سوى 1 f من حيث نطاق الإغلاق، ومن المحتمل جدًا أن يتم الخلط بين الخيوط - استدعاء الطريقة عدة مرات في بعض الحالات وليس على الإطلاق في حالات أخرى.يمكنك إصلاح هذا بإعلان المتغير الثاني داخل الحلقة:

foreach(Foo f in ...) {
    Foo tmp = f;
    // do something with tmp
}

هذا ثم لديه منفصلة tmp في كل نطاق إغلاق، لذلك لا يوجد خطر من هذه المشكلة.

وهذا دليل بسيط على المشكلة:

    static void Main()
    {
        int[] data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        foreach (int i in data)
        {
            new Thread(() => Console.WriteLine(i)).Start();
        }
        Console.ReadLine();
    }

المخرجات (عشوائيا):

1
3
4
4
5
7
7
8
9
9

أضف متغيرًا مؤقتًا ويعمل:

        foreach (int i in data)
        {
            int j = i;
            new Thread(() => Console.WriteLine(j)).Start();
        }

(كل رقم مرة واحدة، ولكن بالطبع الطلب غير مضمون)

نصائح أخرى

إجابات Pop Catalin وMarc Gravell صحيحة.كل ما أريد إضافته هو رابط ل مقالتي عن الإغلاق (الذي يتحدث عن كل من Java وC#).اعتقدت فقط أنه قد يضيف القليل من القيمة.

يحرر:أعتقد أنه من المفيد إعطاء مثال لا يحتوي على عدم القدرة على التنبؤ بالخيوط.فيما يلي برنامج قصير ولكن كامل يوضح كلا النهجين.طباعة قائمة "الإجراء السيئ" 10 عشر مرات؛قائمة "الإجراء الجيد" تعد من 0 إلى 9.

using System;
using System.Collections.Generic;

class Test
{
    static void Main() 
    {
        List<Action> badActions = new List<Action>();
        List<Action> goodActions = new List<Action>();
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            badActions.Add(() => Console.WriteLine(i));
            goodActions.Add(() => Console.WriteLine(copy));
        }
        Console.WriteLine("Bad actions:");
        foreach (Action action in badActions)
        {
            action();
        }
        Console.WriteLine("Good actions:");
        foreach (Action action in goodActions)
        {
            action();
        }
    }
}

حاجتك لاستخدام الخيار 2، فإن إنشاء إغلاق حول متغير متغير سيستخدم قيمة المتغير عند استخدام المتغير وليس في وقت إنشاء الإغلاق.

تنفيذ الأساليب المجهولة في لغة C# وعواقبها (الجزء الأول)

تنفيذ الأساليب المجهولة في لغة C# وعواقبها (الجزء الثاني)

تنفيذ الأساليب المجهولة في لغة C# وعواقبها (الجزء 3)

يحرر:لتوضيح الأمر، في C# عمليات الإغلاق هي "الإغلاقات المعجمية"مما يعني أنهم لا يلتقطون قيمة المتغير ولكن المتغير نفسه.وهذا يعني أنه عند إنشاء إغلاق لمتغير متغير، فإن الإغلاق هو في الواقع إشارة إلى المتغير وليس نسخة من قيمته.

تحرير 2:تمت إضافة روابط لجميع منشورات المدونة إذا كان أي شخص مهتمًا بالقراءة عن الأجزاء الداخلية للمترجم.

هذا سؤال مثير للاهتمام ويبدو أننا رأينا الناس يجيبون عليه بكل الطرق المختلفة.كان لدي انطباع بأن الطريقة الثانية ستكون الطريقة الآمنة الوحيدة.لقد قمت بجلد دليل سريع حقيقي:

class Foo
{
    private int _id;
    public Foo(int id)
    {
        _id = id;
    }
    public void DoSomething()
    {
        Console.WriteLine(string.Format("Thread: {0} Id: {1}", Thread.CurrentThread.ManagedThreadId, this._id));
    }
}
class Program
{
    static void Main(string[] args)
    {
        var ListOfFoo = new List<Foo>();
        ListOfFoo.Add(new Foo(1));
        ListOfFoo.Add(new Foo(2));
        ListOfFoo.Add(new Foo(3));
        ListOfFoo.Add(new Foo(4));


        var threads = new List<Thread>();
        foreach (Foo f in ListOfFoo)
        {
            Thread thread = new Thread(() => f.DoSomething());
            threads.Add(thread);
            thread.Start();
        }
    }
}

إذا قمت بتشغيل هذا سترى أن الخيار 1 ليس آمنًا بالتأكيد.

في حالتك، يمكنك تجنب المشكلة دون استخدام خدعة النسخ عن طريق تعيين ملف ListOfFoo إلى سلسلة من المواضيع:

var threads = ListOfFoo.Select(foo => new Thread(() => foo.DoSomething()));
foreach (var t in threads)
{
    t.Start();
}

كلاهما آمن اعتبارًا من الإصدار 5 من C# (.NET Framework 4.5).انظر هذا السؤال للحصول على التفاصيل: هل تم تغيير استخدام foreach للمتغيرات في C# 5؟

Foo f2 = f;

يشير إلى نفس المرجع كما

f 

لذلك لم يخسر شيئا ولم يكسب شيئا..

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top