ฉันควรวางใบขับขี่ไว้ในที่ใด


27

ฉันเป็นผู้พัฒนารุ่นน้องที่กำลังทำงานเกี่ยวกับการเขียนอัปเดตสำหรับซอฟต์แวร์ที่รับข้อมูลจากโซลูชันของบุคคลที่สามจัดเก็บไว้ในฐานข้อมูลจากนั้นกำหนดเงื่อนไขข้อมูลสำหรับการใช้งานโดยโซลูชันของ บริษัท อื่น ซอฟต์แวร์ของเราทำงานเป็นบริการ Windows

ดูรหัสจากรุ่นก่อนหน้าฉันเห็นสิ่งนี้:

        static Object _workerLocker = new object();
        static int _runningWorkers = 0;
        int MaxSimultaneousThreads = 5;

        foreach(int SomeObject in ListOfObjects)
        {
            lock (_workerLocker)
            {
                while (_runningWorkers >= MaxSimultaneousThreads)
                {
                    Monitor.Wait(_workerLocker);
                }
            }

            // check to see if the service has been stopped. If yes, then exit
            if (this.IsRunning() == false)
            {
                break;
            }

            lock (_workerLocker)
            {
                _runningWorkers++;
            }

            ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);

        }

ตรรกะดูเหมือนชัดเจน: รอห้องในพูลเธรดตรวจสอบให้แน่ใจว่าบริการไม่ได้ถูกหยุดทำงานจากนั้นเพิ่มเธรดตัวนับและเพิ่มคิวงาน _runningWorkersเป็น decremented ภายในSomeMethod()ภายในคำว่าสายแล้วlockMonitor.Pulse(_workerLocker)

คำถามของฉันคือ: มีประโยชน์ในการจัดกลุ่มรหัสทั้งหมดภายในหนึ่งเดียวlockเช่นนี้

        static Object _workerLocker = new object();
        static int _runningWorkers = 0;
        int MaxSimultaneousThreads = 5;

        foreach (int SomeObject in ListOfObjects)
        {
            // Is doing all the work inside a single lock better?
            lock (_workerLocker)
            {
                // wait for room in ThreadPool
                while (_runningWorkers >= MaxSimultaneousThreads) 
                {
                    Monitor.Wait(_workerLocker);
                }
                // check to see if the service has been stopped.
                if (this.IsRunning())
                {
                    ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
                    _runningWorkers++;                  
                }
                else
                {
                    break;
                }
            }
        }

ดูเหมือนว่าอาจทำให้เธรดอื่นรออีกเล็กน้อย แต่จากนั้นดูเหมือนว่าการล็อคซ้ำ ๆ ในบล็อกโลจิคัลบล็อกเดียวอาจใช้เวลาค่อนข้างนาน อย่างไรก็ตามฉันใหม่สำหรับมัลติเธรดดังนั้นฉันสมมติว่ามีข้อกังวลอื่น ๆ ที่นี่ที่ฉันไม่รู้

สถานที่อื่นเท่านั้นที่_workerLockerได้รับการล็อคอยู่SomeMethod()และเพื่อจุดประสงค์ในการลดค่า_runningWorkersและนอกสถานที่foreachเพื่อรอให้จำนวน_runningWorkersถึงศูนย์ก่อนที่จะเข้าสู่ระบบและกลับมา

ขอบคุณสำหรับความช่วยเหลือใด ๆ

แก้ไข 4/8/15

ขอบคุณ @delnan สำหรับคำแนะนำในการใช้สัญญาณ รหัสกลายเป็น:

        static int MaxSimultaneousThreads = 5;
        static Semaphore WorkerSem = new Semaphore(MaxSimultaneousThreads, MaxSimultaneousThreads);

        foreach (int SomeObject in ListOfObjects)
        {
            // wait for an available thread
            WorkerSem.WaitOne();

            // check if the service has stopped
            if (this.IsRunning())
            {
                ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
            }
            else
            {
                break;
            }
        }

WorkerSem.Release()SomeMethod()เรียกว่าภายใน


1
หากบล็อกทั้งหมดถูกล็อควิธีการจะได้รับวิธีการบางอย่างเพื่อลดการ _runningWorkers?
รัสเซลที่ ISC

@RussellatISC: ThreadPool.QueueUserWorkItem เรียกใช้SomeMethodแบบอะซิงโครนัสส่วน "ล็อค" ด้านบนจะถูกทิ้งไว้ก่อนหรืออย่างน้อยหลังจากเธรดใหม่ที่SomeMethodเริ่มทำงาน
Doc Brown

จุดดี. ฉันเข้าใจว่าจุดประสงค์ของการใช้Monitor.Wait()คือการปลดล็อคและรับล็อคใหม่เพื่อให้ทรัพยากรอื่น ( SomeMethodในกรณีนี้) สามารถใช้งานได้ ในอีกด้านหนึ่งSomeMethodได้รับการล็อคลดเคาน์เตอร์และจากนั้นโทรMonitor.Pulse()ที่ส่งกลับล็อคไปยังวิธีการที่เป็นปัญหา นี่คือความเข้าใจของฉันเองอีกครั้ง
โจเซฟ

@Doc พลาดไป แต่ยัง ... ดูเหมือนว่า SomeMethod จะต้องเริ่มต้นก่อน foreach ล็อคในการทำซ้ำครั้งถัดไปหรือจะยังคงถูกแขวนอยู่กับล็อคที่ถือ "ในขณะที่ (_runningWorkers> = MaxSim พร้อมกันเธรด)"
รัสเซลที่ ISC

@RussellatISC: ตามที่โจเซฟระบุไว้แล้ว: Monitor.Waitปลดล็อค ฉันแนะนำให้ดูเอกสาร
Doc Brown

คำตอบ:


33

นี่ไม่ใช่คำถามของประสิทธิภาพ มันเป็นคำถามแรกเกี่ยวกับความถูกต้องและสำคัญที่สุด หากคุณมีคำสั่งล็อกสองคำสั่งคุณไม่สามารถรับประกันอะตอมมิกซิตี้สำหรับการดำเนินการที่แพร่กระจายระหว่างคำสั่งนั้นหรือบางส่วนอยู่นอกคำสั่งล็อก เหมาะสำหรับรหัสรุ่นเก่าของคุณซึ่งหมายความว่า:

ระหว่างปลายของwhile (_runningWorkers >= MaxSimultaneousThreads)และ_runningWorkers++, สิ่งที่ทุกอาจจะเกิดขึ้นเพราะยอมจำนนรหัสและอีกครั้งได้มาล็อคในระหว่าง ยกตัวอย่างเช่นด้ายอาจได้รับล็อคเป็นครั้งแรกรอจนกว่าจะมีบางส่วนออกจากหัวข้ออื่น ๆ lockและจากนั้นแยกออกจากวงและ มันจะถูกจองไว้แล้วและเธรด B จะเข้าสู่รูปภาพรวมทั้งรอห้องในพูลเธรด เพราะกล่าวออกจากหัวข้ออื่น ๆ มีเป็นห้องพักจึงไม่รอนานมากที่ทั้งหมด ทั้งเธรด A และเธรด B จะดำเนินการตามลำดับเพิ่มขึ้น_runningWorkersและเริ่มทำงาน

ตอนนี้ไม่มีการแข่งขันของข้อมูลเท่าที่ฉันเห็น แต่มีเหตุผลมันผิดเพราะตอนนี้มีมากกว่าMaxSimultaneousThreadsคนทำงาน การตรวจสอบ (บางครั้ง) ไม่ได้ผลเพราะงานการสล็อตในพูลเธรดไม่ได้เป็นแบบอะตอมมิก สิ่งนี้ควรเกี่ยวข้องกับคุณมากกว่าการปรับให้เหมาะสมเล็ก ๆ น้อย ๆ รอบ ๆ เม็ดเล็ก ๆ ของการล็อก! (โปรดทราบว่าในทางกลับกันการล็อคเร็วเกินไปหรือนานเกินไปอาจทำให้เกิดการหยุดชะงักได้อย่างง่ายดาย)

ตัวอย่างที่สองแก้ไขปัญหานี้เท่าที่ฉันเห็น การเปลี่ยนแปลงที่มีการบุกรุกน้อยกว่าในการแก้ไขปัญหาอาจทำให้++_runningWorkersถูกต้องหลังจากการwhileค้นหาภายในข้อความสั่งการล็อกแรก

ตอนนี้ถูกต้องกันแล้วสิ่งที่เกี่ยวกับประสิทธิภาพ? เป็นการยากที่จะบอก โดยทั่วไปแล้วการล็อคเป็นเวลานาน ("หยาบ") จะยับยั้งการเกิดพร้อมกัน แต่ในขณะที่คุณพูดสิ่งนี้จะต้องมีความสมดุลกับค่าใช้จ่ายจากการซิงโครไนซ์เพิ่มเติมของการล็อคแบบละเอียด โดยทั่วไปทางออกเดียวคือการเปรียบเทียบและตระหนักว่ามีตัวเลือกมากกว่า "ล็อคทุกอย่างทุกที่" และ "ล็อคขั้นต่ำเปลือยเท่านั้น" มีความหลากหลายของรูปแบบและการทำงานพร้อมกันแบบดั้งเดิมและโครงสร้างข้อมูลแบบ thread-safe ตัวอย่างเช่นนี่ดูเหมือนว่ามีการใช้เซมาฟอร์แอปพลิเคชันมากดังนั้นลองใช้หนึ่งในนั้นแทนการนับด้วยมือที่ล็อคด้วยมือนี้


11

IMHO คุณกำลังถามคำถามที่ผิด - คุณไม่ควรสนใจเรื่องการเสียประสิทธิภาพอย่างมาก

ตัวแปรแรกทำให้แน่ใจว่า_runningWorkersเข้าถึงได้เฉพาะในระหว่างการล็อก แต่มันหายไปในกรณีที่_runningWorkersอาจมีการเปลี่ยนแปลงโดยเธรดอื่นในช่องว่างระหว่างล็อคแรกและที่สอง สุจริตรหัสมองฉันถ้าใครบางคนใส่สุ่มสี่สุ่มห้ารอบจุดเชื่อมต่อทั้งหมด_runningWorkersโดยไม่ต้องคำนึงถึงผลกระทบและข้อผิดพลาดที่อาจเกิดขึ้น บางทีผู้เขียนอาจมีความกลัวไสยศาสตร์เกี่ยวกับการดำเนินการbreakคำสั่งภายในlockบล็อก แต่ใครจะรู้?

ดังนั้นคุณควรใช้ตัวแปรที่สองไม่ใช่เพราะมันมีประสิทธิภาพมากกว่าหรือน้อยกว่า แต่เพราะ (หวังว่า) มันจะถูกต้องมากกว่าตัวแปรแรก


ในอีกด้านหนึ่งการถือกุญแจในขณะที่ทำงานที่อาจต้องได้รับการล็อคอื่นอาจทำให้เกิดการหยุดชะงักที่แทบจะเรียกว่าพฤติกรรม "ถูกต้อง" ไม่ได้ หนึ่งควรตรวจสอบให้แน่ใจว่ารหัสทั้งหมดที่ต้องทำในฐานะหน่วยถูกล้อมรอบด้วยล็อคทั่วไป แต่ควรย้ายออกไปข้างนอกสิ่งล็อคที่ไม่จำเป็นต้องเป็นส่วนหนึ่งของหน่วยโดยเฉพาะสิ่งที่อาจต้องได้รับล็อคอื่น ๆ .
supercat

@supercat: นี่ไม่ใช่กรณีที่นี่โปรดอ่านความคิดเห็นด้านล่างคำถามเดิม
Doc Brown

9

คำตอบอื่น ๆ ค่อนข้างดีและชัดเจนเกี่ยวกับความถูกต้องกังวล ให้ฉันตอบคำถามทั่วไปของคุณเพิ่มเติม:

ฉันควรวางใบขับขี่ไว้ในที่ใด

เริ่มจากคำแนะนำมาตรฐานที่คุณอ้างอิงและ delnan alludes ในย่อหน้าสุดท้ายของคำตอบที่ยอมรับ:

  • ทำงานให้น้อยที่สุดเท่าที่จะทำได้ขณะล็อกวัตถุเฉพาะ ล็อคที่จัดขึ้นเป็นเวลานานอาจมีการช่วงชิงและการต่อสู้ช้า โปรดทราบว่านี่หมายถึงว่าจำนวนรวมของรหัสในการล็อคโดยเฉพาะและจำนวนรวมของรหัสในคำสั่งการล็อคทั้งหมดที่ล็อคในวัตถุเดียวกันมีความเกี่ยวข้อง

  • มีล็อคน้อยที่สุดเท่าที่จะทำได้เพื่อลดโอกาสของการหยุดชะงัก (หรือมีชีวิตชีวา) ต่ำลง

ผู้อ่านที่ฉลาดจะทราบว่าสิ่งเหล่านี้ตรงกันข้าม จุดแรกแนะนำให้แบ่งการล็อคขนาดใหญ่เป็นล็อคที่เล็กกว่าและละเอียดกว่าเพื่อหลีกเลี่ยงการทะเลาะวิวาท ที่สองแนะนำการรวมการล็อคที่แตกต่างกันในวัตถุล็อคเดียวกันเพื่อหลีกเลี่ยงการหยุดชะงัก

เราสามารถสรุปอะไรจากข้อเท็จจริงที่ว่าคำแนะนำมาตรฐานที่ดีที่สุดนั้นขัดแย้งอย่างละเอียด? เราได้รับคำแนะนำที่ดีจริง ๆ :

  • อย่าไปที่นั่นตั้งแต่แรก หากคุณกำลังแชร์ความทรงจำระหว่างหัวข้อต่างๆคุณกำลังเปิดใจรับโลกแห่งความเจ็บปวด

คำแนะนำของฉันคือถ้าคุณต้องการให้เกิดพร้อมกันให้ใช้กระบวนการเป็นหน่วยของการเกิดพร้อมกัน หากคุณไม่สามารถใช้กระบวนการได้ให้ใช้โดเมนแอปพลิเคชัน หากคุณไม่สามารถใช้โดเมนแอปพลิเคชันได้ให้จัดการเธรดของคุณโดย Task Parallel Library และเขียนโค้ดของคุณในรูปของงานระดับสูง (งาน) แทนที่จะเป็นเธรดระดับต่ำ (คนงาน)

หากคุณต้องใช้ primitive concurrency เบื้องต้นในระดับต่ำอย่างเช่นเธรดหรือเซมาฟอร์สให้ใช้เพื่อสร้างนามธรรมในระดับที่สูงขึ้นซึ่งจับสิ่งที่คุณต้องการจริงๆ คุณอาจจะพบว่าสิ่งที่เป็นนามธรรมในระดับที่สูงกว่านั้นคือ "ดำเนินงานแบบอะซิงโครนัสที่ผู้ใช้สามารถยกเลิกได้" และ TPL ก็รองรับแล้วดังนั้นคุณจึงไม่จำเป็นต้องหมุนตัวเอง คุณอาจจะพบว่าคุณต้องการบางอย่างเช่นการเริ่มต้นที่ปลอดภัยของเธรดขี้เกียจ; อย่าม้วนตัวเองใช้งานLazy<T>ซึ่งเขียนโดยผู้เชี่ยวชาญ ใช้คอลเลกชัน threadsafe (ไม่เปลี่ยนรูปหรืออย่างอื่น) เขียนโดยผู้เชี่ยวชาญ เลื่อนระดับนามธรรมขึ้นไปให้สูงที่สุดเท่าที่จะทำได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.