ความปลอดภัยของเธรด MemoryCache จำเป็นต้องล็อคหรือไม่?


93

สำหรับผู้เริ่มต้นให้ฉันโยนมันออกไปที่นั่นฉันรู้ว่ารหัสด้านล่างไม่ปลอดภัยสำหรับเธรด (การแก้ไข: อาจจะ) สิ่งที่ฉันกำลังดิ้นรนคือการค้นหาการใช้งานที่เป็นจริงและเป็นสิ่งที่ฉันสามารถทำได้จริงภายใต้การทดสอบ ฉันกำลังปรับโครงสร้างโครงการ WCF ขนาดใหญ่ในขณะนี้ที่ต้องการข้อมูลแบบคงที่ (ส่วนใหญ่) แคชและสร้างจากฐานข้อมูล SQL จำเป็นต้องหมดอายุและ "รีเฟรช" อย่างน้อยวันละครั้งซึ่งเป็นสาเหตุที่ฉันใช้ MemoryCache

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

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

public class MemoryCacheService : IMemoryCacheService
{
    private const string PunctuationMapCacheKey = "punctuationMaps";
    private static readonly ObjectCache Cache;
    private readonly IAdoNet _adoNet;

    static MemoryCacheService()
    {
        Cache = MemoryCache.Default;
    }

    public MemoryCacheService(IAdoNet adoNet)
    {
        _adoNet = adoNet;
    }

    public void ClearPunctuationMaps()
    {
        Cache.Remove(PunctuationMapCacheKey);
    }

    public IEnumerable GetPunctuationMaps()
    {
        if (Cache.Contains(PunctuationMapCacheKey))
        {
            return (IEnumerable) Cache.Get(PunctuationMapCacheKey);
        }

        var punctuationMaps = GetPunctuationMappings();

        if (punctuationMaps == null)
        {
            throw new ApplicationException("Unable to retrieve punctuation mappings from the database.");
        }

        if (punctuationMaps.Cast<IPunctuationMapDto>().Any(p => p.UntaggedValue == null || p.TaggedValue == null))
        {
            throw new ApplicationException("Null values detected in Untagged or Tagged punctuation mappings.");
        }

        // Store data in the cache
        var cacheItemPolicy = new CacheItemPolicy
        {
            AbsoluteExpiration = DateTime.Now.AddDays(1.0)
        };

        Cache.AddOrGetExisting(PunctuationMapCacheKey, punctuationMaps, cacheItemPolicy);

        return punctuationMaps;
    }

    //Go oldschool ADO.NET to break the dependency on the entity framework and need to inject the database handler to populate cache
    private IEnumerable GetPunctuationMappings()
    {
        var table = _adoNet.ExecuteSelectCommand("SELECT [id], [TaggedValue],[UntaggedValue] FROM [dbo].[PunctuationMapper]", CommandType.Text);
        if (table != null && table.Rows.Count != 0)
        {
            return AutoMapper.Mapper.DynamicMap<IDataReader, IEnumerable<PunctuationMapDto>>(table.CreateDataReader());
        }

        return null;
    }
}

ObjectCache ปลอดภัยสำหรับเธรดฉันไม่คิดว่าคลาสของคุณจะล้มเหลว msdn.microsoft.com/en-us/library/…คุณอาจจะไปที่ฐานข้อมูลพร้อมกัน แต่จะใช้ CPU มากกว่าที่จำเป็นเท่านั้น
the_lotus

1
แม้ว่า ObjectCache จะปลอดภัยสำหรับเธรดการใช้งานอาจไม่เป็นเช่นนั้น ดังนั้นคำถาม MemoryCache
Haney

คำตอบ:


79

MS เริ่มต้นที่มีให้MemoryCacheคือเธรดที่ปลอดภัยทั้งหมด การใช้งานแบบกำหนดเองใด ๆ ที่เกิดขึ้นMemoryCacheอาจไม่ปลอดภัยต่อเธรด หากคุณใช้แบบธรรมดาMemoryCacheนอกกรอบก็ปลอดภัยต่อด้าย เรียกดูซอร์สโค้ดของโซลูชันการแคชแบบกระจายโอเพนซอร์สของฉันเพื่อดูว่าฉันใช้มันอย่างไร (MemCache.cs):

https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs


2
เดวิดเพียงเพื่อยืนยันในคลาสตัวอย่างที่ง่ายมากที่ฉันมีข้างต้นการเรียกร้องให้ลบ () เป็นเธรดที่ปลอดภัยหากเธรดอื่นอยู่ในกระบวนการเรียก Get ()? ฉันคิดว่าฉันควรใช้รีเฟลกเตอร์และเจาะลึกลงไป แต่มีข้อมูลที่ขัดแย้งกันอยู่มากมาย
James Legan

12
เป็นเธรดที่ปลอดภัย แต่มีแนวโน้มที่จะเกิดสภาวะการแข่งขัน ... หากการรับของคุณเกิดขึ้นก่อนที่จะลบข้อมูลจะถูกส่งกลับใน Get หากการลบเกิดขึ้นก่อนจะไม่เกิดขึ้น สิ่งนี้เหมือนกับการอ่านสกปรกบนฐานข้อมูล
Haney

10
มูลค่าที่จะกล่าวถึง (ตามที่ฉันแสดงความคิดเห็นในคำตอบอื่นด้านล่าง) การใช้งานdotnet coreในขณะนี้ไม่ปลอดภัยกับเธรดทั้งหมด โดยเฉพาะGetOrCreateวิธีการ มีปัญหาเกี่ยวกับเรื่องนั้นบน github
AmitE

แคชหน่วยความจำมีข้อยกเว้น "หน่วยความจำไม่เพียงพอ" ขณะรันเธรดโดยใช้คอนโซลหรือไม่
Faseeh Haris

50

ในขณะที่ MemoryCache เป็นเธรดที่ปลอดภัยตามที่คำตอบอื่น ๆ ระบุไว้ แต่ก็มีปัญหาแบบมัลติเธรดทั่วไป - หาก 2 เธรดพยายามGetจาก (หรือตรวจสอบContains) แคชในเวลาเดียวกันทั้งสองจะพลาดแคชและทั้งสองจะสิ้นสุดการสร้าง ผลลัพธ์และทั้งสองจะเพิ่มผลลัพธ์ลงในแคช

บ่อยครั้งสิ่งนี้ไม่เป็นที่ต้องการ - เธรดที่สองควรรอให้เธรดแรกเสร็จสมบูรณ์และใช้ผลลัพธ์แทนที่จะสร้างผลลัพธ์สองครั้ง

นี่เป็นหนึ่งในเหตุผลที่ฉันเขียน LazyCacheซึ่งเป็นกระดาษห่อหุ้มที่เป็นมิตรบน MemoryCache ซึ่งช่วยแก้ปัญหาต่างๆเหล่านี้ได้ มันก็ยังมีอยู่Nuget


"มันจะมีปัญหาที่พบบ่อยเกลียวหลาย" นั่นคือเหตุผลที่คุณควรใช้วิธีการเช่นอะตอมแทนของตรรกะที่กำหนดเองการดำเนินการรอบAddOrGetExisting วิธีการ MemoryCache เป็นอะตอมและด้ายreferencesource.microsoft.com/System.Runtime.Caching/R/...ContainsAddOrGetExisting
อเล็กซ์

1
ใช่ AddOrGetExisting เป็นเธรดที่ปลอดภัย แต่ถือว่าคุณมีการอ้างอิงถึงวัตถุที่จะเพิ่มลงในแคชแล้ว โดยปกติคุณไม่ต้องการ AddOrGetExisting คุณต้องการ "GetExistingOrGenerateThisAndCacheIt" ซึ่งเป็นสิ่งที่ LazyCache ให้คุณ
alastairtree

ใช่ตกลงในประเด็น "ถ้าคุณยังไม่มีเชื่อฟัง"
อเล็กซ์

AddOrGetExisting ไม่ใช่ปรมาณูในความเป็นจริง โปรดดูgithub.com/dotnet/runtime/issues/36499
Andrew

18

ตามที่คนอื่น ๆ ระบุไว้ MemoryCache นั้นปลอดภัยแน่นอน อย่างไรก็ตามความปลอดภัยของเธรดของข้อมูลที่จัดเก็บอยู่ภายในนั้นขึ้นอยู่กับการใช้งานของคุณ

เพื่ออ้างถึงReed Copseyจากโพสต์ที่ยอดเยี่ยมของเขาเกี่ยวกับการทำงานพร้อมกันและConcurrentDictionary<TKey, TValue>ประเภท ซึ่งใช้ได้แน่นอนที่นี่

หากสองเธรดเรียกสิ่งนี้ว่า [GetOrAdd] พร้อมกันจะสามารถสร้าง TValue สองอินสแตนซ์ได้อย่างง่ายดาย

คุณสามารถจินตนาการได้ว่าสิ่งนี้จะแย่มากถ้า TValueมีราคาแพงในการสร้าง

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

GetOrAdd()( GetOrCreate()ในกรณีของMemoryCache) จะส่งคืนค่าเดียวกันเป็นเอกพจน์Lazy<T>สำหรับทุกเธรดอินสแตนซ์ "พิเศษ" ของLazy<T>จะถูกโยนทิ้งไป

เนื่องจากLazy<T>ไม่ได้ทำอะไรเลยจนกว่า.Valueจะถูกเรียกจึงมีการสร้างวัตถุเพียงอินสแตนซ์เดียว

ตอนนี้สำหรับบางรหัส! ด้านล่างนี้เป็นวิธีการขยายIMemoryCacheที่ใช้ข้างต้น เป็นการตั้งค่าโดยพลการSlidingExpirationตามไฟล์int secondsพารามิเตอร์วิธีการ แต่สามารถปรับแต่งได้ทั้งหมดตามความต้องการของคุณ

โปรดทราบว่านี่เป็นสิ่งเฉพาะสำหรับแอป. netcore2.0

public static T GetOrAdd<T>(this IMemoryCache cache, string key, int seconds, Func<T> factory)
{
    return cache.GetOrCreate<T>(key, entry => new Lazy<T>(() =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return factory.Invoke();
    }).Value);
}

โทร:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

ในการดำเนินการทั้งหมดแบบอะซิงโครนัสฉันขอแนะนำให้ใช้การใช้งานที่ยอดเยี่ยมของ Stephen Toubที่AsyncLazy<T>พบในบทความของเขาเกี่ยวกับ MSDN ซึ่งรวมการเริ่มต้นขี้เกียจในตัวLazy<T>กับคำสัญญาTask<T>:

public class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<T> valueFactory) :
        base(() => Task.Factory.StartNew(valueFactory))
    { }
    public AsyncLazy(Func<Task<T>> taskFactory) :
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
    { }
}   

ตอนนี้เป็นเวอร์ชัน async ของGetOrAdd():

public static Task<T> GetOrAddAsync<T>(this IMemoryCache cache, string key, int seconds, Func<Task<T>> taskFactory)
{
    return cache.GetOrCreateAsync<T>(key, async entry => await new AsyncLazy<T>(async () =>
    { 
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return await taskFactory.Invoke();
    }).Value);
}

และสุดท้ายโทร:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());

2
ฉันลองแล้วดูเหมือนจะไม่ได้ผล (dot net core 2.0) GetOrCreate แต่ละรายการจะสร้างอินสแตนซ์ Lazy ใหม่และแคชได้รับการอัปเดตด้วย Lazy ใหม่ดังนั้นค่าจะได้รับการประเมิน \ สร้างขึ้นหลายครั้ง (ในเธรดหลายเธรด)
AmitE

2
จากรูปลักษณ์ของมัน. netcore 2.0 MemoryCache.GetOrCreateไม่ปลอดภัยต่อเธรดในลักษณะเดียวกันConcurrentDictionaryคือ
AmitE

อาจเป็นคำถามโง่ ๆ แต่คุณแน่ใจหรือไม่ว่าเป็นค่าที่สร้างขึ้นหลายครั้งไม่ใช่Lazyหรือ? ถ้าเป็นเช่นนั้นคุณตรวจสอบสิ่งนี้อย่างไร
PIM

3
ฉันใช้ฟังก์ชั่นโรงงานที่พิมพ์ไปยังหน้าจอเมื่อใช้ + สร้างตัวเลขสุ่มและเริ่ม 10 เธรดทั้งหมดโดยพยายามGetOrCreateใช้คีย์เดียวกันและจากโรงงานนั้น ผลลัพธ์โรงงานถูกใช้ 10 ครั้งเมื่อใช้กับแคชหน่วยความจำ (ดูภาพพิมพ์) + ทุกครั้งที่GetOrCreateส่งคืนค่าที่แตกต่างกัน! ฉันทำการทดสอบแบบเดียวกันโดยใช้ConcurrentDicionaryและเห็นว่าโรงงานถูกใช้เพียงครั้งเดียวและได้รับค่าเดียวกันเสมอ ฉันพบปัญหาที่ปิดอยู่ใน github ฉันเพิ่งเขียนความคิดเห็นที่นั่นว่าควรเปิดใหม่
AmitE

คำเตือน: คำตอบนี้ใช้ไม่ได้: Lazy <T> ไม่ได้ป้องกันการเรียกใช้ฟังก์ชันแคชหลายครั้ง ดูคำตอบของ @snicker [net core 2.0]
Fernando Silva

11

ตรวจสอบลิงค์นี้: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

ไปที่ด้านล่างสุดของหน้า (หรือค้นหาข้อความ "Thread Safety")

แล้วคุณจะได้เห็น:

^ ความปลอดภัยของเธรด

ประเภทนี้ปลอดภัยต่อด้าย


7
ฉันเลิกเชื่อถือคำจำกัดความของ "Thread Safe" ของ MSDN ไปสักพักแล้วโดยอาศัยประสบการณ์ส่วนตัว นี่คือสิ่งที่น่าอ่าน: ลิงค์
James Legan

3
โพสต์นั้นแตกต่างจากลิงค์ที่ฉันให้ไว้ด้านบนเล็กน้อย ความแตกต่างมีความสำคัญมากเนื่องจากลิงก์ที่ฉันให้ไว้ไม่ได้ให้ข้อแม้ใด ๆ ในการประกาศความปลอดภัยของเธรด ฉันยังมีประสบการณ์ส่วนตัวMemoryCache.Defaultในการใช้งานในปริมาณที่สูงมาก (จำนวนแคชนับล้านครั้งต่อนาที) โดยยังไม่มีปัญหาเรื่องเธรด
EkoostikMartin

ฉันคิดว่าสิ่งที่พวกเขาหมายถึงคือการดำเนินการอ่านและเขียนเกิดขึ้นในอะตอมสั้น ๆ ในขณะที่เธรด A พยายามอ่านค่าปัจจุบันมันจะอ่านการดำเนินการเขียนที่เสร็จสมบูรณ์เสมอไม่ใช่ระหว่างการเขียนข้อมูลไปยังหน่วยความจำโดยเธรดใด ๆ เมื่อเธรด A พยายาม การเขียนลงในหน่วยความจำจะไม่มีการแทรกแซงจากเธรดใด ๆ นั่นคือความเข้าใจของฉัน แต่มีคำถาม / บทความมากมายเกี่ยวกับสิ่งที่ไม่ได้นำไปสู่ข้อสรุปที่สมบูรณ์ดังต่อไปนี้ stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

เพิ่งอัปโหลดไลบรารีตัวอย่างเพื่อแก้ไขปัญหาสำหรับ. Net 2.0

ดูที่ repo นี้:

RedisLazyCache

ฉันใช้แคช Redis แต่มันก็เฟลโอเวอร์หรือแค่ Memorycache หาก Connectionstring หายไป

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


กรุณาแบ่งปันคำตอบเท่านั้นข้อมูลอื่น ๆ สามารถแบ่งปันเป็นความคิดเห็น
ElasticCode

1
@WaelAbbas. ฉันลองแล้ว แต่ดูเหมือนว่าฉันต้องการ 50 ชื่อเสียงก่อน : ง. แม้ว่าจะไม่ใช่คำตอบโดยตรงสำหรับคำถาม OP (สามารถตอบได้โดยใช่ / ไม่ใช่พร้อมคำอธิบายว่าทำไม) คำตอบของฉันคือคำตอบที่เป็นไปได้สำหรับคำตอบที่ไม่
Francis Marasigan

2

แคชนั้นปลอดภัยสำหรับเธรด แต่ก็เหมือนกับที่คนอื่น ๆ ระบุไว้อาจเป็นไปได้ว่า GetOrAdd จะเรียก func หลายประเภทหากเรียกจากหลายประเภท

นี่คือการแก้ไขขั้นต่ำของฉันเกี่ยวกับเรื่องนั้น

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

และ

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();

ฉันคิดว่ามันเป็นทางออกที่ดี แต่ถ้าฉันมีหลายวิธีในการเปลี่ยนแคชที่แตกต่างกันถ้าฉันใช้การล็อคพวกเขาจะถูกล็อคโดยไม่จำเป็น! ในกรณีนี้เราควรมี _cacheLocks หลายตัวฉันคิดว่ามันจะดีกว่าถ้า cachelock มี Key ด้วย!
Daniel

มีหลายวิธีในการแก้ปัญหาหนึ่งคือ generics semaphore จะไม่ซ้ำกันต่ออินสแตนซ์ของ MyCache <T> จากนั้นคุณสามารถลงทะเบียน AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Anders

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

ปัญหาของเวอร์ชันนี้คือหากคุณมี 2 สิ่งที่แตกต่างกันในการแคชในเวลาเดียวกันคุณต้องรอให้อันแรกสร้างเสร็จก่อนจึงจะสามารถตรวจสอบแคชสำหรับรายการที่สองได้ จะมีประสิทธิภาพมากกว่าสำหรับพวกเขาที่จะสามารถตรวจสอบแคช (และสร้าง) ได้พร้อมกันหากคีย์ต่างกัน LazyCache ใช้การรวมกันของ Lazy <T> และการล็อกแบบลายเพื่อให้แน่ใจว่ารายการของคุณจะแคชเร็วที่สุดและสร้างเพียงครั้งเดียวต่อหนึ่งคีย์ ดูgithub.com/alastairtree/lazycache
alastairtree

1

ดังที่ @AmitE กล่าวไว้ที่คำตอบของ @pimbrouwers ตัวอย่างของเขาไม่ทำงานตามที่แสดงไว้ที่นี่:

class Program
{
    static async Task Main(string[] args)
    {
        var cache = new MemoryCache(new MemoryCacheOptions());

        var tasks = new List<Task>();
        var counter = 0;

        for (int i = 0; i < 10; i++)
        {
            var loc = i;
            tasks.Add(Task.Run(() =>
            {
                var x = GetOrAdd(cache, "test", TimeSpan.FromMinutes(1), () => Interlocked.Increment(ref counter));
                Console.WriteLine($"Interation {loc} got {x}");
            }));
        }

        await Task.WhenAll(tasks);
        Console.WriteLine("Total value creations: " + counter);
        Console.ReadKey();
    }

    public static T GetOrAdd<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory)
    {
        return cache.GetOrCreate(key, entry =>
        {
            entry.SetSlidingExpiration(expiration);
            return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
        }).Value;
    }
}

เอาท์พุต:

Interation 6 got 8
Interation 7 got 6
Interation 2 got 3
Interation 3 got 2
Interation 4 got 10
Interation 8 got 9
Interation 5 got 4
Interation 9 got 1
Interation 1 got 5
Interation 0 got 7
Total value creations: 10

ดูเหมือนว่าGetOrCreateจะส่งคืนรายการที่สร้างขึ้นเสมอ โชคดีที่แก้ไขได้ง่ายมาก:

public static T GetOrSetValueSafe<T>(IMemoryCache cache, string key, TimeSpan expiration,
    Func<T> valueFactory)
{
    if (cache.TryGetValue(key, out Lazy<T> cachedValue))
        return cachedValue.Value;

    cache.GetOrCreate(key, entry =>
    {
        entry.SetSlidingExpiration(expiration);
        return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
    });

    return cache.Get<Lazy<T>>(key).Value;
}

ทำงานได้ตามที่คาดไว้:

Interation 4 got 1
Interation 9 got 1
Interation 1 got 1
Interation 8 got 1
Interation 0 got 1
Interation 6 got 1
Interation 7 got 1
Interation 2 got 1
Interation 5 got 1
Interation 3 got 1
Total value creations: 1

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