C#マルチスレッド(10):読み書きロック

10921 ワード

目次
  • ReaderWriterLockSlim
  • ReaderWriterLockSlim
  • 一般的な方法
  • 受注システム例
  • 同時辞書作成例
  • ReaderWriterLock


  • 本編では主にReader WriterLockSlimクラスを紹介し,マルチスレッドでの読み書き分離を実現する.

    ReaderWriterLockSlim


    Reader WriterLockクラス:単一の書き込みスレッドと複数の読み取りスレッドをサポートするロックを定義します.
    ReaderWriterLockSlimクラス:リソースアクセスを管理するためのロック状態を示し、マルチスレッド読み取りまたは排他的書き込みアクセスを実現します.
    両方のAPIは非常に近く、Reader WriterLockSlimはReader WriterLockよりも安全です.そこで本稿では主にReader WriterLockSlimについて説明する.
    どちらも、複数のスレッドを同時に読み取ることができ、1つのスレッドのみを書き込むことができるクラスです.

    ReaderWriterLockSlim


    古いルールでは、まずReader WriterLockSlimがよく使う方法を理解しておきます.

    一般的な方法


    方法
    説明
    EnterReadLock()
    読み込みモードロック状態にしてみます.
    EnterUpgradeableReadLock()
    アップグレード可能モードのロック状態に入ってみます.
    EnterWriteLock()
    書き込みモードのロック状態にしてみます.
    ExitReadLock()
    読み出しモードの再帰カウントを減らし、生成したカウントが0(ゼロ)のときに読み出しモードを終了します.
    ExitUpgradeableReadLock()
    アップグレード可能モードの再帰カウントを減らし、生成されたカウントが0(ゼロ)のときにアップグレード可能モードを終了します.
    ExitWriteLock()
    書き込みモードの再帰カウントを減らし、生成したカウントが0(ゼロ)のときに書き込みモードを終了します.
    TryEnterReadLock(Int32)
    読み込みモードのロック状態にしようとします.整数タイムアウト時間を選択できます.
    TryEnterReadLock(TimeSpan)
    読み込みモードのロック状態にしようとします.タイムアウト時間を選択できます.
    TryEnterUpgradeableReadLock(Int32)
    アップグレード可能モードのロック状態にしようとします.タイムアウト時間を選択できます.
    TryEnterUpgradeableReadLock(TimeSpan)
    アップグレード可能モードのロック状態にしようとします.タイムアウト時間を選択できます.
    TryEnterWriteLock(Int32)
    書き込みモードのロック状態にしようとします.タイムアウト時間を選択できます.
    TryEnterWriteLock(TimeSpan)
    書き込みモードのロック状態にしようとします.タイムアウト時間を選択できます.
    Reader WriterLockSlimの読み込み、書き込みロックテンプレートは以下の通りです.
            private static ReaderWriterLockSlim toolLock = new ReaderWriterLockSlim();
    
    		//  
            private T Read()
            {
    
                try
                {
                    toolLock.EnterReadLock();           //  
                    return obj;
                }
                catch { }
                finally
                {
                    toolLock.ExitReadLock();            //  
                }
                return default;
            }
    
            //  
            public void Write(int key, int value)
            {
                try
                {
                    toolLock.EnterUpgradeableReadLock();
    
                    try
                    {
                        toolLock.EnterWriteLock();
                        /*
                         * 
                        */
                    }
                    catch
                    {
    
                    }
                    finally
                    {
                        toolLock.ExitWriteLock();
                    }
                }
                catch { }
                finally
                {
                    toolLock.ExitUpgradeableReadLock();
                }
            }
    

    受注システムの例


    ここでは、単純で粗い受注システムをシミュレートします.
    コードの作成を開始する前に、いくつかの方法の具体的な使用を理解します.EnterReadLock()/TryEnterReadLockおよびExitReadLock()のペアが現れる.EnterWriteLock()/TryEnterWriteLock()およびExitWriteLock()のペアが現れる.EnterUpgradeableReadLock()は、アップグレード可能なリードモードロック状態に入る.EnterReadLock()EnterUpgradeableReadLock()を使用してアップグレード状態に入り、適切な時点でEnterWriteLock()を介して書き込みモードに入る.(逆さまにすることもできます)
    3つの変数を定義します.
    Reader WriterLockSlimマルチスレッド読み書きロック;
    MaxId現在のオーダーIdの最大値.
    orders受注表;
            private static ReaderWriterLockSlim tool = new ReaderWriterLockSlim();   //  
    
            private static int MaxId = 1;
            public static List orders = new List();       //  
    
            //  
            public class DoWorkModel
            {
                public int Id { get; set; }     //  
                public string UserName { get; set; }    //  
                public DateTime DateTime { get; set; }  //  
            }
    

    次に、クエリーと受注の作成の2つの方法を実装します.
    オーダーのページング:
    読み込み前にEnterReadLock()を使用してロックを取得する.
    読み込みが完了したら、ExitReadLock()を使用してロックを解除します.
    これにより、マルチスレッド環境では、読み出すたびに最新の値であることを保証することができる.
            //  
            private static DoWorkModel[] DoSelect(int pageNo, int pageSize)
            {
    
                try
                {
                    DoWorkModel[] doWorks;
                    tool.EnterReadLock();           //  
                    doWorks = orders.Skip((pageNo - 1) * pageSize).Take(pageSize).ToArray();
                    return doWorks;
                }
                catch { }
                finally
                {
                    tool.ExitReadLock();            //  
                }
                return default;
            }
    

    オーダーの作成:
    注文の作成情報は簡単で、ユーザー名と作成時間を知っていればいいです.
    注文システムが保証する場合、各Idは一意(実際にはGuidを使用する必要があります)であり、ここでは読み書きロックを実証するために数値に設定されています.
    マルチスレッド環境では,Interlocked.Increment()ではなく+= 1を直接使用し,読み書きロックが存在するため,操作も原則的である.
            //  
            private static DoWorkModel DoCreate(string userName, DateTime time)
            {
                try
                {
                    tool.EnterUpgradeableReadLock();        //  
                    try
                    {
                        tool.EnterWriteLock();              //  
    
                        //  
                        MaxId += 1;                         // Interlocked.Increment(ref MaxId);
    
                        DoWorkModel model = new DoWorkModel
                        {
                            Id = MaxId,
                            UserName = userName,
                            DateTime = time
                        };
                        orders.Add(model);
                        return model;
                    }
                    catch { }
                    finally
                    {
                        tool.ExitWriteLock();               //  
                    }
                }
                catch { }
                finally
                {
                    tool.ExitUpgradeableReadLock();         //  
                }
                return default;
            }
    

    Mainメソッド:
    5つのスレッドを開き、読み続け、2つのスレッドを開いて注文を作成し続けます.スレッドは、注文の作成時にThread.Sleep()が設定されていないため、実行速度が非常に速い.
    Mainメソッドのコードには意味がありません.
            static void Main(string[] args)
            {
                // 5 
                for (int i = 0; i < 5; i++)
                {
                    new Thread(() =>
                    {
                        while (true)
                        {
                            var result = DoSelect(1, MaxId);
                            if (result is null)
                            {
                                Console.WriteLine(" ");
                                continue;
                            }
                            foreach (var item in result)
                            {
                                Console.Write($"{item.Id}|");
                            }
                            Console.WriteLine("
    "); Thread.Sleep(1000); } }).Start(); } for (int i = 0; i < 2; i++) { new Thread(() => { while(true) { var result = DoCreate((new Random().Next(0, 100)).ToString(), DateTime.Now); // if (result is null) Console.WriteLine(" "); else Console.WriteLine(" "); } }).Start(); } }

    ASP.NET Coreでは,読み書きロックを利用して,HTTPリクエストの同時送信によるデータベース読み書き問題を解決することができる.
    ここでは例を挙げません.
    別のスレッドで問題が発生し、書き込みロックがなかなかコミットされない場合は、他のスレッドが無限に待機する可能性があります.
    そうすれば、TryEnterWriteLock()を使用して待ち時間を設定し、ブロック時間が長すぎることを回避することができる.
    bool isGet = tool.TryEnterWriteLock(500);
    

    同時辞書の書き込み例


    理論的なものなので、筆者はここではあまり言わないが、主にいくつかのAPI(方法、属性)の使用を把握し、簡単に例を書き、後で徐々に底層原理を深く理解することだ.
    ここでは、マルチスレッド共有使用辞書(Dictionary)の使用例を書きます.
    2つの静的変数を追加します.
            private static ReaderWriterLockSlim toolLock = new ReaderWriterLockSlim();
            private static Dictionary dict = new Dictionary();
    

    書き込みを実行するには、次の手順に従います.
            public static void Write(int key, int value)
            {
                try
                {
                    //  
                    toolLock.EnterUpgradeableReadLock();
                    //  , 
                    if (dict.ContainsKey(key))
                        return;
    
                    try
                    {
                        //  
                        toolLock.EnterWriteLock();
                        dict.Add(key,value);
                    }
                    finally
                    {
                        toolLock.ExitWriteLock();
                    }
                }
                finally
                {
                    toolLock.ExitUpgradeableReadLock();
                }
            }
    

    上にcatch { }がないのは、コードをよりよく観察するためであり、読み書きロックが使用されているため、理論的に問題が発生するべきではない.
    5つのスレッドをシミュレートして辞書に同時に書き込むと,原子操作ではないためsumの値が重複する場合がある.
    原子操作については、以下を参照してください.https://www.cnblogs.com/whuanle/p/12724371.html#1、問題が発生
            private static int sum = 0;
            public static void AddOne()
            {
                for (int i = 0; i < 100_0000; i++)
                {
                    sum += 1;
                    Write(sum,sum);
                }
            }
            static void Main(string[] args)
            {
                for (int i = 0; i < 5; i++)
                    new Thread(() => { AddOne(); }).Start();
                Console.ReadKey();
            }
    

    ReaderWriterLock


    ほとんどの場合、Reader WriterLockSlimが推奨されており、両者の使い方は非常に近い.
    例えばAcquireReader Lockはリードロックを取得し、AcquireWriterLockはライトロックを取得する.Reader WriterLockSlimの例を、対応する方法で置き換えることができます.
    ここではReader WriterLockについては後述しない.
    Reader WriterLockの一般的な方法は次のとおりです.
    方法
    説明
    AcquireReaderLock(Int32)
    1つのIntel 32スーパータイム値を使用してリードスレッドロックを取得します.
    AcquireReaderLock(TimeSpan)
    TimeSpanスーパータイム値を使用して、リードスレッドロックを取得します.
    AcquireWriterLock(Int32)
    1つのIntel 32スーパータイム値を使用して書き込みスレッドロックを取得します.
    AcquireWriterLock(TimeSpan)
    TimeSpanスーパータイム値を使用して書き込みスレッドロックを取得します.
    AnyWritersSince(Int32)
    シリアル番号を取得した後、書き込みスレッドロックをスレッドに付与したかどうかを示します.
    DowngradeFromWriterLock(LockCookie)
    スレッドのロック状態を、UpgradeToWriterLock(Int 32)を呼び出す前の状態に戻します.
    ReleaseLock()
    スレッドがロックを取得した回数にかかわらず、ロックを解除します.
    ReleaseReaderLock()
    ロック数を減らす.
    ReleaseWriterLock()
    書き込みスレッドロックのロック数を減らします.
    RestoreLock(LockCookie)
    スレッドのロック状態を、ReleaseLock()を呼び出す前の状態に戻します.
    UpgradeToWriterLock(Int32)
    1つのIntel 32タイムアウト値を使用して、リードスレッドロックをライトスレッドロックにアップグレードします.
    UpgradeToWriterLock(TimeSpan) TimeSpanのタイムアウト値を使用して、リードスレッドロックをライトスレッドロックにアップグレードします.
    公式の例は次のとおりです.
    https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlock?view=netcore-3.1#examples