การใช้รูปแบบการรวมวัตถุ C #


165

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

ในการติดตามเกี่ยวกับคำร้องขอ @Aaronaught สำหรับการชี้แจงการใช้พูลจะเป็นการร้องขอการทำโหลดบาลานซ์ให้กับบริการภายนอก การวางไว้ในสถานการณ์ที่อาจจะง่ายกว่าที่จะเข้าใจได้ทันทีเมื่อเทียบกับคำบรรยายโดยตรงของฉัน ฉันมีวัตถุเซสชันที่ทำงานคล้ายกับISessionวัตถุจาก NHibernate แต่ละเซสชั่นที่ไม่ซ้ำกันจัดการมันเชื่อมต่อกับฐานข้อมูล ขณะนี้ฉันมีวัตถุเซสชันที่ใช้งานมานาน 1 ครั้งและกำลังประสบปัญหาซึ่งผู้ให้บริการของฉันกำลัง จำกัด อัตราการใช้งานเซสชันนี้

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

หวังว่าพื้นหลังนั้นมีคุณค่า แต่จะตอบคำถามของคุณโดยตรง:

ถาม:วัตถุมีราคาแพงในการสร้างหรือไม่
ตอบ:ไม่มีวัตถุใด ๆ เป็นแหล่งรวมทรัพยากรที่ จำกัด

ถาม:พวกเขาจะได้รับ / เผยแพร่บ่อยมากหรือไม่
ตอบ:ใช่อีกครั้งที่พวกเขาสามารถนึกถึง NHibernate ISessions ที่มักจะได้รับและเผยแพร่ 1 ตลอดระยะเวลาของคำขอหน้าเดียวทุกครั้ง

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

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

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


1
คุณช่วยเล่าให้เราฟังเกี่ยวกับความต้องการของคุณได้ไหม? มีการสร้างพูลไม่เท่ากันทั้งหมด วัตถุมีราคาแพงในการสร้างหรือไม่? พวกเขาจะได้รับ / เผยแพร่บ่อยมากหรือไม่? คุณจะได้รับความพอเพียงก่อนใครหรือคุณต้องการสิ่งที่ฉลาดกว่านั่นคือการป้องกันความอดอยาก? สิ่งที่เกี่ยวกับสิ่งต่าง ๆ เช่นลำดับความสำคัญสันหลังยาวกับการโหลดที่กระตือรือร้น ฯลฯ สิ่งใดที่คุณสามารถเพิ่มจะช่วยเรา (หรืออย่างน้อยฉัน) เพื่อหาคำตอบที่ละเอียดยิ่งขึ้น
Aaronaught

Chris - เพียงแค่ดูย่อหน้าที่ 2 และ 3 ของคุณและสงสัยว่าเซสชันเหล่านี้ควรมีชีวิตอยู่อย่างไม่มีกำหนดหรือไม่? ดูเหมือนว่าเป็นสิ่งที่ผู้ให้บริการของคุณไม่ชอบ (เซสชันที่ใช้เวลานาน) ดังนั้นคุณอาจกำลังมองหาการใช้กลุ่มที่หมุนรอบเซสชันใหม่ตามความจำเป็นและจะปิดเมื่อไม่ใช้งาน (หลังจากช่วงเวลาที่กำหนด) . สามารถทำได้ แต่มีความซับซ้อนมากกว่านี้เล็กน้อยดังนั้นฉันต้องการยืนยัน
Aaronaught

ฉันไม่แน่ใจว่าฉันต้องการโซลูชันที่มีประสิทธิภาพหรือยังเนื่องจากโซลูชันของฉันเป็นเพียงข้อสมมติฐาน เป็นไปได้ว่าผู้ให้บริการของฉันกำลังโกหกฉันและบริการของพวกเขาขายเกินกว่าที่กำหนดและพบข้ออ้างเพื่อหาวิธีตำหนิผู้ใช้
Chris Marisic

1
ฉันคิดว่า TPL DataFlow BufferBlock ทำสิ่งที่คุณต้องการเป็นส่วนใหญ่
อะไรต่อมิอะไร

1
การรวมกันในสภาพแวดล้อมแบบเธรดเป็นปัญหาที่เกิดซ้ำแก้ไขโดยรูปแบบการออกแบบเช่น Resource Pool และ Resource Cache ตรวจสอบสถาปัตยกรรมซอฟต์แวร์ที่เน้นรูปแบบเล่ม 3: รูปแบบสำหรับการจัดการทรัพยากรสำหรับข้อมูลเพิ่มเติม
Fuhrmanator

คำตอบ:


59

การรวมวัตถุใน. NET Core

หลัก dotnetมีการดำเนินงานร่วมกันของวัตถุเพิ่มไปยังห้องสมุดชั้นฐาน (BCL) คุณสามารถอ่านปัญหา GitHub เดิมที่นี่และดูรหัสสำหรับSystem.Buffers ขณะนี้ArrayPoolเป็นชนิดเดียวที่พร้อมใช้งานและใช้ในการรวมอาร์เรย์ มีโพสต์ดีบล็อกเป็นที่นี่

namespace System.Buffers
{
    public abstract class ArrayPool<T>
    {
        public static ArrayPool<T> Shared { get; internal set; }

        public static ArrayPool<T> Create(int maxBufferSize = <number>, int numberOfBuffers = <number>);

        public T[] Rent(int size);

        public T[] Enlarge(T[] buffer, int newSize, bool clearBuffer = false);

        public void Return(T[] buffer, bool clearBuffer = false);
    }
}

ตัวอย่างของการใช้งานสามารถเห็นได้ใน ASP.NET Core เพราะมันอยู่ใน dotnet core BCL ทำให้ ASP.NET Core สามารถแชร์พูลของมันกับวัตถุอื่น ๆ เช่น serializer JSON ของ Newtonsoft.Json คุณสามารถอ่านโพสต์บล็อกนี้สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานของ Newtonsoft.Json

Object Pooling ในคอมไพเลอร์ Microsoft Roslyn C #

คอมไพเลอร์ Microsoft Roslyn C # ใหม่ประกอบด้วยประเภทObjectPoolซึ่งใช้ในการรวมกลุ่มวัตถุที่ใช้บ่อยซึ่งโดยปกติแล้วจะได้รับ new'ed ขึ้นและขยะที่รวบรวมบ่อยมาก สิ่งนี้จะลดจำนวนและขนาดของการดำเนินการรวบรวมขยะที่ต้องเกิดขึ้น มีการใช้งานย่อยที่แตกต่างกันไม่กี่ทั้งหมดโดยใช้ ObjectPool (ดู: ทำไมจึงมีการใช้งานหลายอย่างของ Object Pooling ใน Roslyn? )

1 - SharedPools - เก็บพูลของวัตถุ 20 รายการหรือ 100 ถ้าใช้ BigDefault

// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
    // Do something with pooledObject.Object
}

// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);

// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][4] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
    // Do something with list
}
finally
{
    SharedPools.Default<List<Foo>>().Free(list);
}

2 - ListPoolและStringBuilderPool - ไม่แยกการใช้งานอย่างเคร่งครัด แต่ล้อมรอบการใช้งาน SharedPools ที่แสดงด้านบนโดยเฉพาะสำหรับรายการและ StringBuilder ดังนั้นสิ่งนี้จะใช้กลุ่มของวัตถุที่เก็บอยู่ใน SharedPools อีกครั้ง

// Example 1 - No using statement so you need to be sure no exceptions are thrown.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
// Do something with stringBuilder
StringBuilderPool.Free(stringBuilder);

// Example 2 - Safer version of Example 1.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
try
{
    // Do something with stringBuilder
}
finally
{
    StringBuilderPool.Free(stringBuilder);
}

3 - PooledDictionaryและPooledHashSet - เหล่านี้ใช้ ObjectPool โดยตรงและมีกลุ่มของวัตถุที่แยกจากกันโดยสิ้นเชิง เก็บพูลจำนวน 128 วัตถุ

// Example 1
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
// Do something with hashSet.
hashSet.Free();

// Example 2 - Safer version of Example 1.
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
try
{
    // Do something with hashSet.
}
finally
{
    hashSet.Free();
}

Microsoft.IO.RecyclableMemoryStream

ไลบรารีนี้จัดเตรียมการรวมกำไรสำหรับMemoryStreamวัตถุ System.IO.MemoryStreamมันเป็นแบบเลื่อนแทน มันมีความหมายเหมือนกันทุกประการ มันถูกออกแบบโดยวิศวกร Bing อ่านบล็อกโพสต์ที่นี่หรือดูรหัสบนGitHub

var sourceBuffer = new byte[]{0,1,2,3,4,5,6,7}; 
var manager = new RecyclableMemoryStreamManager(); 
using (var stream = manager.GetStream()) 
{ 
    stream.Write(sourceBuffer, 0, sourceBuffer.Length); 
}

โปรดทราบว่าRecyclableMemoryStreamManagerควรได้รับการประกาศเพียงครั้งเดียวและจะมีผลตลอดกระบวนการ - นี่คือพูล เป็นการดีที่จะใช้หลายพูลถ้าคุณต้องการ


2
นี่คือคำตอบที่ดี หลังจาก C # 6 & VS2015 เป็น RTM ฉันน่าจะทำให้คำตอบนี้เป็นที่ยอมรับเพราะชัดเจนที่สุดถ้าปรับตามที่ Rosyln ใช้
Chris Marisic

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

1
ดูเหมือนว่าแต่ละคนมีวัตถุประสงค์ที่กำหนดไว้อย่างชัดเจนดีกว่าเพียงทางเลือกของการเปิดสิ้นสุดวันที่หนึ่งขนาดเหมาะกับรองเท้าทั้งหมด
Chris Marisic

1
@ MuhammadRehanSaeed นอกจากนี้ยอดเยี่ยมกับ ArrayPool
Chris Marisic

1
การเห็นRecyclableMemoryStreamว่าเป็นส่วนเสริมที่ยอดเยี่ยมสำหรับการปรับแต่งประสิทธิภาพสูงสุดพิเศษ
Chris Marisic

315

คำถามนี้มีเล่ห์เหลี่ยมกว่าที่คาดไว้เล็กน้อยเนื่องจากมีหลายสิ่งที่ไม่ทราบ: พฤติกรรมของทรัพยากรที่ถูกพูลอายุการใช้งานที่คาดหวัง / ที่ต้องการของวัตถุเหตุผลที่แท้จริงที่ต้องใช้พูล ฯลฯ โดยทั่วไปพูลมีวัตถุประสงค์พิเศษ - เธรด พูล, พูลการเชื่อมต่อ, ฯลฯ - เพราะมันง่ายกว่าในการออปติไมซ์เมื่อคุณรู้ว่าทรัพยากรทำอะไรและที่สำคัญกว่านั้นมีการควบคุมการใช้ทรัพยากร

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

กลุ่มวัตถุประสงค์ทั่วไปจะต้องมี "การตั้งค่า" หลักบางประการซึ่งรวมถึง:

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

สำหรับกลไกการโหลดทรัพยากร. NET ได้มอบสิ่งที่เป็นนามธรรมให้กับเราอย่างสมบูรณ์

private Func<Pool<T>, T> factory;

ผ่านสิ่งนี้ผ่าน Constructor ของ Pool และเรากำลังทำสิ่งนั้น การใช้ประเภทสามัญที่มีnew()ข้อ จำกัด ก็ใช้ได้เช่นกัน แต่มันก็มีความยืดหยุ่นมากกว่า


ในอีกสองพารามิเตอร์กลยุทธ์การเข้าถึงเป็นสัตว์ที่มีความซับซ้อนมากขึ้นดังนั้นวิธีการของฉันคือการใช้วิธีการสืบทอด (อินเตอร์เฟส):

public class Pool<T> : IDisposable
{
    // Other code - we'll come back to this

    interface IItemStore
    {
        T Fetch();
        void Store(T item);
        int Count { get; }
    }
}

แนวคิดที่นี่ง่ายมากเราจะให้Poolชั้นเรียนสาธารณะจัดการกับปัญหาทั่วไปเช่นความปลอดภัยของเธรด แต่ใช้ "ที่เก็บรายการ" ที่แตกต่างกันสำหรับแต่ละรูปแบบการเข้าถึง LIFO นั้นแทนด้วยสแต็กได้อย่างง่ายดาย FIFO เป็นคิวและฉันได้ใช้การปรับใช้บัฟเฟอร์แบบวงกลมที่ไม่ได้เพิ่มประสิทธิภาพ แต่อาจพอเพียงโดยใช้List<T>ตัวชี้และดัชนีเพื่อประมาณรูปแบบการเข้าถึงแบบกลม

คลาสทั้งหมดด้านล่างเป็นคลาสภายในของPool<T>- นี่คือตัวเลือกรูปแบบ แต่เนื่องจากสิ่งเหล่านี้ไม่ได้หมายถึงการใช้ภายนอกPoolดังนั้นจึงเหมาะสมที่สุด

    class QueueStore : Queue<T>, IItemStore
    {
        public QueueStore(int capacity) : base(capacity)
        {
        }

        public T Fetch()
        {
            return Dequeue();
        }

        public void Store(T item)
        {
            Enqueue(item);
        }
    }

    class StackStore : Stack<T>, IItemStore
    {
        public StackStore(int capacity) : base(capacity)
        {
        }

        public T Fetch()
        {
            return Pop();
        }

        public void Store(T item)
        {
            Push(item);
        }
    }

เหล่านี้คือสิ่งที่ชัดเจน - สแต็กและคิว ฉันไม่คิดว่าพวกเขารับประกันคำอธิบายมาก บัฟเฟอร์วงกลมนั้นซับซ้อนกว่านี้เล็กน้อย:

    class CircularStore : IItemStore
    {
        private List<Slot> slots;
        private int freeSlotCount;
        private int position = -1;

        public CircularStore(int capacity)
        {
            slots = new List<Slot>(capacity);
        }

        public T Fetch()
        {
            if (Count == 0)
                throw new InvalidOperationException("The buffer is empty.");

            int startPosition = position;
            do
            {
                Advance();
                Slot slot = slots[position];
                if (!slot.IsInUse)
                {
                    slot.IsInUse = true;
                    --freeSlotCount;
                    return slot.Item;
                }
            } while (startPosition != position);
            throw new InvalidOperationException("No free slots.");
        }

        public void Store(T item)
        {
            Slot slot = slots.Find(s => object.Equals(s.Item, item));
            if (slot == null)
            {
                slot = new Slot(item);
                slots.Add(slot);
            }
            slot.IsInUse = false;
            ++freeSlotCount;
        }

        public int Count
        {
            get { return freeSlotCount; }
        }

        private void Advance()
        {
            position = (position + 1) % slots.Count;
        }

        class Slot
        {
            public Slot(T item)
            {
                this.Item = item;
            }

            public T Item { get; private set; }
            public bool IsInUse { get; set; }
        }
    }

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

โปรดจำไว้ว่าคลาสเหล่านี้เป็นคลาสส่วนตัวภายใน - นั่นคือเหตุผลที่พวกเขาไม่ต้องการการตรวจสอบข้อผิดพลาดจำนวนมากทั้งสระว่ายน้ำเอง จำกัด การเข้าถึงพวกเขา

แสดงวิธีการแจงนับและวิธีทำจากโรงงานเราทำส่วนนี้เสร็จแล้ว:

// Outside the pool
public enum AccessMode { FIFO, LIFO, Circular };

    private IItemStore itemStore;

    // Inside the Pool
    private IItemStore CreateItemStore(AccessMode mode, int capacity)
    {
        switch (mode)
        {
            case AccessMode.FIFO:
                return new QueueStore(capacity);
            case AccessMode.LIFO:
                return new StackStore(capacity);
            default:
                Debug.Assert(mode == AccessMode.Circular,
                    "Invalid AccessMode in CreateItemStore");
                return new CircularStore(capacity);
        }
    }

ปัญหาต่อไปที่จะแก้ไขคือกลยุทธ์การโหลด ฉันได้กำหนดสามประเภท:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

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

วิธีการโหลดนั้นไม่ซับซ้อนเกินไปตอนนี้เรามีรายการร้านค้านามธรรม:

    private int size;
    private int count;

    private T AcquireEager()
    {
        lock (itemStore)
        {
            return itemStore.Fetch();
        }
    }

    private T AcquireLazy()
    {
        lock (itemStore)
        {
            if (itemStore.Count > 0)
            {
                return itemStore.Fetch();
            }
        }
        Interlocked.Increment(ref count);
        return factory(this);
    }

    private T AcquireLazyExpanding()
    {
        bool shouldExpand = false;
        if (count < size)
        {
            int newCount = Interlocked.Increment(ref count);
            if (newCount <= size)
            {
                shouldExpand = true;
            }
            else
            {
                // Another thread took the last spot - use the store instead
                Interlocked.Decrement(ref count);
            }
        }
        if (shouldExpand)
        {
            return factory(this);
        }
        else
        {
            lock (itemStore)
            {
                return itemStore.Fetch();
            }
        }
    }

    private void PreloadItems()
    {
        for (int i = 0; i < size; i++)
        {
            T item = factory(this);
            itemStore.Store(item);
        }
        count = size;
    }

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

AcquireLazyตรวจสอบเพื่อดูว่ามีรายการฟรีในพูลหรือไม่และจะสร้างขึ้นใหม่หรือไม่ AcquireLazyExpandingจะสร้างทรัพยากรใหม่ตราบใดที่พูลยังไม่ถึงขนาดเป้าหมาย ฉันพยายามปรับให้เหมาะสมเพื่อลดการล็อกและฉันหวังว่าฉันจะไม่ทำผิดพลาด (ฉันได้ทดสอบสิ่งนี้ภายใต้เงื่อนไขแบบมัลติเธรด แต่เห็นได้ชัดว่าไม่ละเอียดถี่ถ้วน)

คุณอาจสงสัยว่าทำไมไม่มีวิธีการเหล่านี้รบกวนการตรวจสอบเพื่อดูว่าร้านค้ามีขนาดสูงสุดหรือไม่ ฉันจะไปที่นั้นสักครู่


ตอนนี้สำหรับสระว่ายน้ำของตัวเอง นี่คือชุดข้อมูลส่วนตัวแบบเต็มซึ่งบางส่วนได้ถูกแสดงแล้ว:

    private bool isDisposed;
    private Func<Pool<T>, T> factory;
    private LoadingMode loadingMode;
    private IItemStore itemStore;
    private int size;
    private int count;
    private Semaphore sync;

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

ตัวสร้างมีลักษณะดังนี้:

    public Pool(int size, Func<Pool<T>, T> factory,
        LoadingMode loadingMode, AccessMode accessMode)
    {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", size,
                "Argument 'size' must be greater than zero.");
        if (factory == null)
            throw new ArgumentNullException("factory");

        this.size = size;
        this.factory = factory;
        sync = new Semaphore(size, size);
        this.loadingMode = loadingMode;
        this.itemStore = CreateItemStore(accessMode, size);
        if (loadingMode == LoadingMode.Eager)
        {
            PreloadItems();
        }
    }

ไม่น่าประหลาดใจที่นี่ สิ่งเดียวที่ควรทราบคือเคสพิเศษสำหรับการโหลดแบบกระตือรือร้นโดยใช้PreloadItemsวิธีที่แสดงไว้ก่อนหน้านี้แล้ว

เนื่องจากเกือบทุกอย่างถูกแยกออกไปอย่างหมดจดในตอนนี้ความจริงAcquireและReleaseวิธีการต่าง ๆ นั้นตรงไปตรงมามาก:

    public T Acquire()
    {
        sync.WaitOne();
        switch (loadingMode)
        {
            case LoadingMode.Eager:
                return AcquireEager();
            case LoadingMode.Lazy:
                return AcquireLazy();
            default:
                Debug.Assert(loadingMode == LoadingMode.LazyExpanding,
                    "Unknown LoadingMode encountered in Acquire method.");
                return AcquireLazyExpanding();
        }
    }

    public void Release(T item)
    {
        lock (itemStore)
        {
            itemStore.Store(item);
        }
        sync.Release();
    }

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

สุดท้าย แต่ไม่ท้ายสุดมีการล้างข้อมูล:

    public void Dispose()
    {
        if (isDisposed)
        {
            return;
        }
        isDisposed = true;
        if (typeof(IDisposable).IsAssignableFrom(typeof(T)))
        {
            lock (itemStore)
            {
                while (itemStore.Count > 0)
                {
                    IDisposable disposable = (IDisposable)itemStore.Fetch();
                    disposable.Dispose();
                }
            }
        }
        sync.Close();
    }

    public bool IsDisposed
    {
        get { return isDisposed; }
    }

วัตถุประสงค์ของIsDisposedคุณสมบัตินั้นจะชัดเจนในชั่วขณะ ทั้งหมดหลักDisposeวิธีจริงๆไม่เป็นทิ้งรายการ pooled IDisposableจริงถ้าพวกเขาใช้


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

สมมติว่าเราเริ่มต้นด้วยอินเตอร์เฟส / คลาสอย่างง่ายต่อไปนี้:

public interface IFoo : IDisposable
{
    void Test();
}

public class Foo : IFoo
{
    private static int count = 0;

    private int num;

    public Foo()
    {
        num = Interlocked.Increment(ref count);
    }

    public void Dispose()
    {
        Console.WriteLine("Goodbye from Foo #{0}", num);
    }

    public void Test()
    {
        Console.WriteLine("Hello from Foo #{0}", num);
    }
}

นี่คือFooทรัพยากรที่ใช้แล้วทิ้งของเราซึ่งใช้IFooและมีรหัสสำเร็จรูปสำหรับสร้างรหัสประจำตัว สิ่งที่เราทำคือการสร้างวัตถุพิเศษที่รวมกันอีก:

public class PooledFoo : IFoo
{
    private Foo internalFoo;
    private Pool<IFoo> pool;

    public PooledFoo(Pool<IFoo> pool)
    {
        if (pool == null)
            throw new ArgumentNullException("pool");

        this.pool = pool;
        this.internalFoo = new Foo();
    }

    public void Dispose()
    {
        if (pool.IsDisposed)
        {
            internalFoo.Dispose();
        }
        else
        {
            pool.Release(this);
        }
    }

    public void Test()
    {
        internalFoo.Test();
    }
}

นี่เป็นเพียงวิธีพร็อกซีของ "ของจริง" ทั้งหมดไปด้านในIFoo(เราสามารถทำได้ด้วย Dynamic Proxy library เช่น Castle แต่ฉันจะไม่เข้าไปในนั้น) นอกจากนี้ยังรักษาการอ้างอิงถึงสิ่งPoolที่สร้างขึ้นดังนั้นเมื่อเราDisposeวัตถุนี้มันจะปล่อยตัวเองกลับไปที่กลุ่มโดยอัตโนมัติ ยกเว้นเมื่อสระได้ถูกกำจัดแล้ว - หมายความว่าเราอยู่ในโหมด "ล้าง" และในกรณีนี้มันล้างทรัพยากรภายในแทน


เมื่อใช้วิธีการด้านบนเราจะได้เขียนโค้ดดังนี้:

// Create the pool early
Pool<IFoo> pool = new Pool<IFoo>(PoolSize, p => new PooledFoo(p),
    LoadingMode.Lazy, AccessMode.Circular);

// Sometime later on...
using (IFoo foo = pool.Acquire())
{
    foo.Test();
}

นี่เป็นสิ่งที่ดีมากที่สามารถทำได้ มันหมายความว่ารหัสที่ใช้IFoo (เมื่อเทียบกับรหัสที่สร้างมัน) ไม่ต้องการจริงที่จะตระหนักถึงสระว่ายน้ำ คุณยังสามารถฉีด IFooวัตถุใช้ห้องสมุด DI ที่คุณชื่นชอบและPool<T>เป็นผู้ให้บริการ / โรงงาน


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

แจ้งให้เราทราบหากคุณมีคำถามหรือข้อสงสัยเกี่ยวกับเรื่องนี้


62
หนึ่งในคำตอบที่สมบูรณ์มีประโยชน์และน่าสนใจที่สุดที่ฉันได้อ่านดังนั้น
Josh Smeaton

ฉันไม่เห็นด้วยเพิ่มเติมกับ @Josh เกี่ยวกับการตอบสนองนี้โดยเฉพาะอย่างยิ่งในส่วนของ PooledFoo เนื่องจากการปล่อยวัตถุดูเหมือนว่าจะถูกจัดการในลักษณะที่รั่วไหลมากและฉันคิดว่ามันจะสมเหตุสมผลที่จะใช้การใช้ สร้างเหมือนที่คุณแสดงฉันไม่ได้นั่งลงและพยายามสร้างสิ่งที่คำตอบของคุณให้ข้อมูลทั้งหมดที่ฉันต้องการเพื่อแก้ปัญหาของฉัน ฉันคิดว่าสำหรับสถานการณ์เฉพาะของฉันฉันจะสามารถลดความซับซ้อนนี้ได้ค่อนข้างน้อยเนื่องจากฉันสามารถแบ่งปันอินสแตนซ์ในกลุ่มกระทู้และไม่จำเป็นต้องปล่อยพวกเขากลับไปที่กลุ่ม
Chris Marisic

อย่างไรก็ตามหากวิธีการง่าย ๆ ใช้ไม่ได้ผลฉันมีความคิดเล็ก ๆ น้อย ๆ ในหัวของฉันว่าฉันสามารถจัดการกับกรณีของฉันได้อย่างชาญฉลาด ฉันคิดว่าโดยเฉพาะอย่างยิ่งฉันจะสร้างการเปิดตัวเพื่อให้สามารถตรวจสอบว่าเซสชั่นตัวเองผิดพลาดและกำจัดมันและแทนที่ใหม่ในสระว่ายน้ำ ไม่ว่าบทความนี้ ณ จุดนี้จะเป็นแนวทางที่ชัดเจนมากนักเกี่ยวกับการรวมวัตถุใน C # 3.0 ฉันรอคอยที่จะดูว่าใครมีความคิดเห็นเพิ่มเติมเกี่ยวกับเรื่องนี้
Chris Marisic

@Chris: หากคุณกำลังพูดถึงผู้รับมอบฉันทะลูกค้า WCF แล้วฉันมีรูปแบบที่ดีแม้ว่าคุณจะต้องใช้หัวฉีดพึ่งพาหรือวิธีการดักเพื่อใช้อย่างมีประสิทธิภาพ รุ่น DI ใช้เคอร์เนลกับผู้ให้บริการที่กำหนดเองเพื่อรับเวอร์ชันใหม่ถ้าผิดพลาด, รุ่นการสกัดกั้นวิธี (การตั้งค่าของฉัน) เพียงแค่ห่อพร็อกซี่ที่มีอยู่และแทรกการตรวจสอบความผิดก่อนแต่ละรายการ ฉันไม่แน่ใจว่ามันง่ายเพียงใดที่จะรวมเข้ากับสระว่ายน้ำแบบนี้ (ยังไม่ได้ลองจริง ๆ เพราะฉันเพิ่งเขียนสิ่งนี้!) แต่มันจะเป็นไปได้แน่นอน
Aaronaught

5
น่าประทับใจมากแม้ว่าจะมีการปรับเปลี่ยนโครงสร้างเล็กน้อยสำหรับสถานการณ์ส่วนใหญ่ ฉันคาดหวังว่าสิ่งนี้จะเป็นส่วนหนึ่งของกรอบงาน
ChaosPandion

7

บางสิ่งเช่นนี้อาจเหมาะกับความต้องการของคุณ

/// <summary>
/// Represents a pool of objects with a size limit.
/// </summary>
/// <typeparam name="T">The type of object in the pool.</typeparam>
public sealed class ObjectPool<T> : IDisposable
    where T : new()
{
    private readonly int size;
    private readonly object locker;
    private readonly Queue<T> queue;
    private int count;


    /// <summary>
    /// Initializes a new instance of the ObjectPool class.
    /// </summary>
    /// <param name="size">The size of the object pool.</param>
    public ObjectPool(int size)
    {
        if (size <= 0)
        {
            const string message = "The size of the pool must be greater than zero.";
            throw new ArgumentOutOfRangeException("size", size, message);
        }

        this.size = size;
        locker = new object();
        queue = new Queue<T>();
    }


    /// <summary>
    /// Retrieves an item from the pool. 
    /// </summary>
    /// <returns>The item retrieved from the pool.</returns>
    public T Get()
    {
        lock (locker)
        {
            if (queue.Count > 0)
            {
                return queue.Dequeue();
            }

            count++;
            return new T();
        }
    }

    /// <summary>
    /// Places an item in the pool.
    /// </summary>
    /// <param name="item">The item to place to the pool.</param>
    public void Put(T item)
    {
        lock (locker)
        {
            if (count < size)
            {
                queue.Enqueue(item);
            }
            else
            {
                using (item as IDisposable)
                {
                    count--;
                }
            }
        }
    }

    /// <summary>
    /// Disposes of items in the pool that implement IDisposable.
    /// </summary>
    public void Dispose()
    {
        lock (locker)
        {
            count = 0;
            while (queue.Count > 0)
            {
                using (queue.Dequeue() as IDisposable)
                {

                }
            }
        }
    }
}

ตัวอย่างการใช้งาน

public class ThisObject
{
    private readonly ObjectPool<That> pool = new ObjectPool<That>(100);

    public void ThisMethod()
    {
        var that = pool.Get();

        try
        { 
            // Use that ....
        }
        finally
        {
            pool.Put(that);
        }
    }
}

1
เกาที่ความคิดเห็นก่อนหน้านี้ ฉันคิดว่าฉันเพิ่งพบว่ามันแปลกเพราะสระนี้ดูเหมือนจะไม่ได้มีเกณฑ์ใด ๆ และบางทีมันอาจไม่จำเป็นต้องขึ้นอยู่กับข้อกำหนด
Aaronaught

1
@Aaraught - มันแปลกจริงๆเหรอ? ฉันต้องการสร้างสระว่ายน้ำน้ำหนักเบาซึ่งมีฟังก์ชั่นที่จำเป็น มันขึ้นอยู่กับลูกค้าที่จะใช้คลาสได้อย่างถูกต้อง
ChaosPandion

1
+1 สำหรับโซลูชันที่ง่ายมากที่สามารถปรับให้เข้ากับวัตถุประสงค์ของฉันได้โดยเพียงแค่เปลี่ยนประเภทสำรองเป็นรายการ / HashTable ฯลฯ และเปลี่ยนตัวนับเพื่อพลิกกลับ คำถามแบบสุ่มคุณจะจัดการกับการจัดการวัตถุพูลได้อย่างไร? คุณเพิ่งใส่มันลงในคอนเทนเนอร์ IOC ที่กำหนดให้เป็นแบบซิงเกิลที่นั่นหรือไม่?
Chris Marisic

1
ควรจะคงที่อ่านได้อย่างเดียว? แต่ฉันคิดว่ามันแปลกที่คุณจะใส่คำแถลงในที่สุดหากมีข้อยกเว้นมันจะไม่เป็นไปได้หรือไม่ที่วัตถุที่ตนเองนั้นผิดพลาด คุณจะจัดการกับสิ่งนั้นภายในPutวิธีการและปล่อยมันออกมาเพื่อความเรียบง่ายในการตรวจสอบว่าวัตถุนั้นมีความผิดปกติหรือไม่และเพื่อสร้างอินสแตนซ์ใหม่ที่จะเพิ่มลงในพูลแทนที่จะใส่ก่อนหน้า
Chris Marisic

1
@Chris - ฉันแค่เสนอเครื่องมือง่ายๆที่ฉันพบว่ามีประโยชน์ในอดีต ส่วนที่เหลือขึ้นอยู่กับคุณ ปรับเปลี่ยนและใช้รหัสตามที่เห็นสมควร
ChaosPandion

6

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

ดีและเรียบง่าย
Daniel de Zwaan

4

ย้อนกลับไปในวันที่ Microsoft จัดทำเฟรมเวิร์กผ่าน Microsoft Transaction Server (MTS) และหลังจากนั้น COM + เพื่อทำการรวมวัตถุสำหรับวัตถุ COM ฟังก์ชันดังกล่าวถูกส่งต่อไปยัง System.EnterpriseServices ใน. NET Framework และขณะนี้อยู่ใน Windows Communication Foundation

การรวมวัตถุใน WCF

บทความนี้มาจาก. NET 1.1 แต่ยังควรใช้ใน Framework ปัจจุบันรุ่น (แม้ว่า WCF เป็นวิธีที่ต้องการ)

การรวมวัตถุ. NET


+1 สำหรับแสดงให้ฉันเห็นว่ามีIInstanceProviderอินเทอร์เฟซที่ฉันจะใช้สำหรับการแก้ปัญหาของฉัน ฉันมักจะเป็นแฟนตัวยงของรหัสของฉันอยู่ด้านหลัง Microsoft ให้อินเทอร์เฟซเมื่อพวกเขาให้คำจำกัดความที่เหมาะสม
Chris Marisic

4

ฉันชอบการนำของ Aronaught ไปใช้โดยเฉพาะอย่างยิ่งเมื่อเขาจัดการกับการรอทรัพยากรเพื่อให้สามารถใช้งานได้ผ่านการใช้สัญญาณ มีหลายภาพที่ฉันอยากจะทำ:

  1. เปลี่ยนsync.WaitOne()เป็นsync.WaitOne(timeout)และแสดงการหมดเวลาเป็นพารามิเตอร์ในAcquire(int timeout)เมธอด สิ่งนี้จะทำให้การจัดการเงื่อนไขเมื่อเธรดหมดเวลาที่รอให้วัตถุพร้อมใช้งาน
  2. เพิ่มRecycle(T item)วิธีการจัดการสถานการณ์เมื่อวัตถุต้องถูกนำกลับมาใช้ใหม่เมื่อเกิดความล้มเหลวตัวอย่างเช่น

3

นี่คือการใช้งานอื่นโดยมีจำนวนวัตถุในพูล จำกัด

public class ObjectPool<T>
    where T : class
{
    private readonly int maxSize;
    private Func<T> constructor;
    private int currentSize;
    private Queue<T> pool;
    private AutoResetEvent poolReleasedEvent;

    public ObjectPool(int maxSize, Func<T> constructor)
    {
        this.maxSize = maxSize;
        this.constructor = constructor;
        this.currentSize = 0;
        this.pool = new Queue<T>();
        this.poolReleasedEvent = new AutoResetEvent(false);
    }

    public T GetFromPool()
    {
        T item = null;
        do
        {
            lock (this)
            {
                if (this.pool.Count == 0)
                {
                    if (this.currentSize < this.maxSize)
                    {
                        item = this.constructor();
                        this.currentSize++;
                    }
                }
                else
                {
                    item = this.pool.Dequeue();
                }
            }

            if (null == item)
            {
                this.poolReleasedEvent.WaitOne();
            }
        }
        while (null == item);
        return item;
    }

    public void ReturnToPool(T item)
    {
        lock (this)
        {
            this.pool.Enqueue(item);
            this.poolReleasedEvent.Set();
        }
    }
}



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