【CSharp】线程同步

lock语句

lock语句是设置锁定解除锁定的一种简单方式。

开启20个任务,每个任务对相同的初始值为0的字段各加一50000次,理论上的结果为20*50000 = 1000000。但是,多次运行该程序几乎得不到正确的结果。

方法DoTheJob及其在IL语言中的代码如下:

public void DoTheJob()
{
    for (int i = 0; i < 50000; i++)
    {
        _sharedState.State += 1;
    }
}
.method public hidebysig instance void  DoTheJob() cil managed
{
	// 代码大小       46 (0x2e)
	
	// 该方法使用大栈的大小
	.maxstack  3
	
	// 定义局部变量
	// 定义了一个int32类型的局部变量V_0,索引为0
	// 定义了一个bool 类型的局部变量V_1,索引为1
	.locals init (int32 V_0, bool V_1)
	
	// 如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作
	IL_0000:  nop
	
	// 将整数值0作为int32推送到评估堆栈上
	// 即:i的初始值
	IL_0001:  ldc.i4.0
	
	// 从评估堆栈的顶部弹出当前值并将其存储到索引为0的句柄变量量表中
	// 即:i = 0
	IL_0002:  stloc.0
	
	// 无条件地将控制转移到目标指令(短格式)
	// 即:跳转执行 i< 50000
	IL_0003:  br.s       IL_0021
	
	// 如果修补操作码,则填充空间。
	// 尽管可能消耗处理周期,但未执行任何有意义的操作
	IL_0005:  nop
	IL_0006:  nop
	
	// 将索引为 0 的变量的值加载到评估堆栈上
	IL_0007:  ldarg.0
	
	// 查找对象中其引用当前位于评估堆栈的字段的值
	IL_0008:  ldfld      class SynchronizatonSamples.SharedState SynchronizatonSamples.Job::_sharedState
	
	// 复制评估堆栈上当前最顶端的值,然后将副本推送到评估堆栈上
	IL_000d:  dup
	
	// 对对象调用后期绑定方法,并且将返回值推送到评估堆栈上
	IL_000e:  callvirt   instance int32 SynchronizatonSamples.SharedState::get_State()
	
	// 将整数值 1 作为 int32 推送到评估堆栈上
	IL_0013:  ldc.i4.1
	
	// 将两个值相加并将结果推送到评估堆栈上
	IL_0014:  add
	
	// 对对象调用后期绑定方法,并且将返回值推送到评估堆栈上
	IL_0015:  callvirt   instance void SynchronizatonSamples.SharedState::set_State(int32)
	
	// 如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作
	IL_001a:  nop
	IL_001b:  nop
	IL_001c:  nop
	
	// 将索引 0 处的局部变量加载到评估堆栈上
	IL_001d:  ldloc.0
	
	// 将整数值 1 作为 int32 推送到评估堆栈上
	IL_001e:  ldc.i4.1
	
	// 将两个值相加并将结果推送到评估堆栈上
	IL_001f:  add
	
	从评估堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中
	IL_0020:  stloc.0
	
	// 将索引 0 处的局部变量加载到评估堆栈上
	IL_0021:  ldloc.0
	
	// 将所提供的 int32 类型的值作为 int32 推送到评估堆栈上
	IL_0022:  ldc.i4     0xc350
	
	// 比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到评估堆栈上;
	// 反之,将 0 (int32) 推送到评估堆栈上
	// 即:i < 50000
	IL_0027:  clt
	
	// 从评估堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中
	IL_0029:  stloc.1
	
	// 将索引 1 处的局部变量加载到评估堆栈上
	IL_002a:  ldloc.1
	
	// 如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)
	IL_002b:  brtrue.s   IL_0005
	
	// 从当前方法返回,并将返回值(如果存在)从调用方的评估堆栈推送到被调用方的评估堆栈上
	IL_002d:  ret
} // end of method Job::DoTheJob

程序先获取变量_sharedState.State的当前值。执行计算(+1)操作,最后将计算后的值赋值给变量_sharedState.State。

如果有一个线程A执行到(+1)操作,但还未给变量赋值,线程发生了切换。假定在切换的瞬间变量_sharedState.State的值为100,+1操作后的值为101,因为发生了线程切换变量_sharedState.State的值保持100不变,+1操作的值101,进行临时存储。新的线程B判断当前变量_sharedState.State的值为100,并对其执行新的+1操作,结果变量_sharedState.State的值变为101。经过多次的线程切换,变量_sharedState.State的值增加为200。在某一时刻被暂定的线程A又重新获取时间片,并开始执行。由于之前的+1操作已经完成,仅剩下给变量赋值。继续执行会将101赋值给变量_sharedState.State。此时,变量的值由原先的200变成了101。

IL语言基本知识

https://docs.microsoft.com/zh-tw/previous-versions/dd229210(v=msdn.10)?redirectedfrom=MSDN

IL语言指令表

https://www.cnblogs.com/yinrq/p/5485630.html

namespace SynchronizatonSamples
{
    public class SharedState
    {
        public int State { get; set; }
    }

    public class Job
    {
        private SharedState _sharedState;
        public Job(SharedState sharedState) => _sharedState = sharedState;

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                _sharedState.State += 1;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            // 创建多任务共享的字段
            var state = new SharedState();
            // 定义任务的数量
            var tasks = new System.Threading.Tasks.Task[20];

            for (int i = 0; i < tasks.Length; i++)
                // 虽然使用的是不同的Job实例,但传入的是相同的字段
                tasks[i] = System.Threading.Tasks.Task.Run(() => new Job(state).DoTheJob());

            // 等待所有任务执行完成
            System.Threading.Tasks.Task.WaitAll(tasks);

            // 输出多任务的执行结果
            System.Console.WriteLine($"summarized {state.State}");
        }
    }
}

为了避免上述错误,可以通过使用lock关键字,对使用共享字段进行加锁。一个线程获取到锁之后,直到其释放锁,其他线程才能进行方位,否则只能等待。

namespace SynchronizatonSamples
{
    public class SharedState
    {
        public int State { get; set; }
    }

    public class Job
    {
        private SharedState _sharedState;
        public Job(SharedState sharedState) => _sharedState = sharedState;

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                // 多个任务使用相同的字段,可以锁定所有任务
                lock (_sharedState)
                {
                    _sharedState.State += 1;
                }
            }
        }
    }

    class Program
    {
        static void Main()
        {
            // 创建多任务共享的字段
            var state = new SharedState();
            // 定义任务的数量
            var tasks = new System.Threading.Tasks.Task[20];

            for (int i = 0; i < tasks.Length; i++)
                // 虽然使用的是不同的Job实例,但传入的是相同的字段
                tasks[i] = System.Threading.Tasks.Task.Run(() => new Job(state).DoTheJob());

            // 等待所有任务执行完成
            System.Threading.Tasks.Task.WaitAll(tasks);

            // 输出多任务的执行结果
            System.Console.WriteLine($"summarized {state.State}");
        }
    }
}

锁定静态成员

可以把锁放在object类型或静态成员:

lock (typeof(StaticClass))
{
    
}

lock(this)

一次只有一个线程能访问相同实例的方法

public class Demo
{
    // Only one thread at a time can access the DoThis and DoThat methods
 
    public void DoThis()
    {
        lock(this)
        {
        }
    }

    public void DoThat()
    {
        lock(this)
        {
        }
    }
}

Interlocked

Interlocked类用于使变量的简单语句原子化。i++不是线程安全的,它的操作包括从内存中获取一个值,给该值递增1,再将它存储回内存。这些操作都可能会被线程调度器打断。Interlocked类提供了以线程安全的方式递增、递减、交换和读取值的方法。

与其他同步技术相比,使用Interlocked类会快很多。但是,它只能用于简单的同步问题

lock(this)
{
    if(_someState == null)
    {
        _someState = newState;
    }
}

=====>>>>
Interlocked.CompareExchange(ref someState, newState, null);



public int State
{
    get
    {
        lock(this)
        {
            return ++_state;
        }
    }
}

=====>>>>
public int State
{
    get
    {
        return Interlocked.Increment(ref _state);
    }
}

Monitor

lock语句由编译器解析为使用Monitor类。

lock(obj)
{
    // ...
}

=====>>>>
Monitor.Enter(obj);
try
{
    // ...
}
finally
{
    Monitor.Exit(obj);
}

调用Enter()方法,该方法会一直等待,直到线程锁定对象为止。一次只有一个线程能锁定对象。只要解除了锁定,线程就可以进入同步阶段。Monitor类的Exit方法解除了锁定。

与lock语句相比,Monitor类的主要优点是:可以添加一个等待被锁定的超时值。这样就不会无限期地等待被锁定,而可以像下面的例子那样使用TryEnter()方法,其中给它传递一个超时值,指定等待被锁定的最长时间。如果obj被锁定,TryEmter()方法就把布尔类型的引用参数设置未true。如果另一个线程锁定obj的事件超过了500毫秒,TryEnter()方法就把变量lockTaken设置为false,线程不在等待,而是用于其他操作。也许在以后,该线程会尝试再次获得锁定。

bool _lockTaken = false;
Monitor.TryEnter(_obj, 500, ref _lockTaken);
if(_lockTaken)
{
    try
    {
        // ...
    }
    finally
    {
        Monitor.Exit(obj);
    }
}
else
{
    // didn't get the lock, do something else
}

SpinLock

如果基于对象的锁定(Monitor)的系统开销由于垃圾回收而过高,可以使用SpinLock结构。如果有大量的锁定,且锁定的时间总是非常短,SpinLock结构就很有用。应避免使用多个SpinLock结构,也不要调用任何可能阻塞的内容。

除了体系结构上的区别之外,SpinLock结构的用法非常类似与Monitor类。使用Enter或TryEnter方法获得锁定,使用Eixt方法释放锁定。SpinLock结构还提供了属性IsHeld和IsHeldByCurrentThread,指定它当前是否是锁定的。

WaitHandle

WaitHandle用于等待一个信号。可以等待不同的信号,因为WaitHandle是一个基类。

namespace AsyncDelegate
{
    public delegate int TakesAWhileDelegate(int x, int ms);
    class Program
    {
        static void Main()
        {
            try
            {
                TakesAWhileDelegate d1 = TakesAWhile;
                System.IAsyncResult ar = d1.BeginInvoke(1, 3000, null, null);
                while (true)
                {
                    System.Console.Write(".");
                    // 等待50ms,如果未等到信号,则停止等待
                    if (ar.AsyncWaitHandle.WaitOne(50))
                    {
                        System.Console.WriteLine("Can get the result now");
                        break;
                    }
                }
                int result = d1.EndInvoke(ar);
                System.Console.WriteLine($"result: {result}");
            }
            catch (System.PlatformNotSupportedException)
            {
                System.Console.WriteLine("PlatformNotSupported exception - with async delegates please use the full .NET Framework");
            }
        }

        public static int TakesAWhile(int x, int ms)
        {
            System.Threading.Tasks.Task.Delay(ms).Wait();
            return 42;
        }
    }
}

可以利用BeginInvoke方法的返回值的AsyncWaitHandle属性访问WaitHandle基类。在调用WaitOne方法或者超时发生时,线程会等待接收一个与等待句柄相关的信号。调用EndInvoke方法,线程最终会阻塞,直到得到结果为止。

  • WaitOne - 等待一个信号、
  • WaitAll   - 等待必须获得所有信号
  • WaitAny - 等待多个对象中的任意一个

WaitAll与WaitAny时WaitHandle类的静态方法,接收一个WaitHandle数组。

Mutex

Mutex是跨进程同步访问的一个类。它非常类似与Monitor类,因为它们都只有一个线程能拥有锁定。

在构造函数中,可以指定互斥是否最初应由主线程拥有,定义互斥的名称,获得互斥是否已存在的信息。在下面的示例代码中,第3个参数定义输入参数 ,接收一个表示互斥是否为新建的布尔值。如果返回值为false,就表示互斥已经定义。互斥可以在另一个进程中定义,因为操作系统能够识别由名称的互斥,它由不同的进程共享。如果没有给互斥指定名称,互斥就是未命名的,不在不同进程之间共享

var mutex = new Mutex(false, "MutexName", out var createNew);

要打开已有的互斥,还可以使用Mutex.OpenExisting方法。由于Mutex类派生自WaitHandle,因此可以利用WaitOne方法获得互斥锁定,在该过程中称为该互斥的拥有者。通过调用ReleaseMutex方法释放互斥。

if(mutex.WaitOne())
{
    try
    {
        // ...
    }
    finally
    {
        mutex.ReleaseMutex();
    }
}
else
{
    // ...
}

由于系统能识别有名称的互斥 ,因此可以使用它禁止程序启动两次。在下面的WPF应用程序中,调用Mutex对象的构造函数。接着验证指定名称的互斥是否存在。如果存在,退出应用。

public partial class App : Application
{
    protected override void OnStartup(StartupEvenArgs e)
    {
        var mutex = new Mutex(false, "MutexName", out var mutexCreated);
        if(!mutexCreate)
        {
            MessageBox.Show("You can onlu start one instance of the application");
            Application.Current.Shutdown();
        }
        base.OnStartup(e);
    }
}

Semaphore

信号量非常类似与互斥,其区别是,信号量可以同时由多个线程使用。信号量是一种计数的互斥锁定。使用信号量,可以定义允许同时访问受旗语锁定保护的资源的线程个数。如果需要限制可以访问可使用资源的线程数,信号量就很有用。例如,如果系统有3个物理端口可用,就允许3个线程同时访问I/O端口,但第4个线程需要等待前3个线程中的一个释放资源。

Semaphore类可以命名,使用系统范围内的资源,允许在不同进程之间同步。

SemaphoreSlim类是简化版本,只能在一个进程内的不同线程之间同步。

public SemaphoreSlim(int initialCount, int maxCount);

// initialCount - 在程序初始状态时,释放出了多少个可以直接被线程使用的资源
// maxCount     - Semaphore所包含的所有资源的数量. 包括当前可用的资源,和当前尚未释放的资源
using System.Threading;
using System.Threading.Tasks;
using static System.Console;

namespace SemaphoreSample
{
    class Program
    {
        static void Main(string[] args)
        {
            int taskCount = 6;
            int semaphoreCount = 3;
            var semaphore = new SemaphoreSlim(semaphoreCount, semaphoreCount);
            var tasks = new Task[taskCount];

            for (int i = 0; i < taskCount; i++)
            {
                tasks[i] = Task.Run(() => TaskMain(semaphore));
            }

            Task.WaitAll(tasks);

            WriteLine("All tasks finished");
        }

        private static void TaskMain(SemaphoreSlim semaphore)
        {
            bool isCompleted = false;
            while (!isCompleted)
            {
                if (semaphore.Wait(600))
                {
                    try
                    {
                        WriteLine($"Task {Task.CurrentId} locks the semaphore");
                        Task.Delay(2000).Wait();
                    }
                    finally
                    {
                        WriteLine($"Task {Task.CurrentId} releases the semaphore");
                        semaphore.Release();
                        isCompleted = true;
                    }
                }
                else
                {
                    WriteLine($"Timeout for task {Task.CurrentId}; wait again");
                }
            }
        }
    }
}

Event

事件也是一个系统范围内的资源同步方法。

  • Wait()   - 等待信号,当前线程处于阻塞状态
  • Set()     - 发送信号,解除阻塞线程的状态
  • Reset() - 重置信号,线程再次调用Wait方法,会处于阻塞状态
  • ManualResetEvent          - 接收到信号后,解除Wait方法的阻塞状态。直到调用Reset方法,不论执行多少次Wait方法,都不阻塞
  • AutoResetEvent              - 接收到信号后,解除Wait方法的阻塞状态。在下一次调用Wait方法时,重新处于阻塞状态 
  • ManualResetEventSlim  - ManualResetEvent 的简化版本
  • CountdownEvent            - 接收到指定次数,才解除阻塞

对于AutoResetEvent,如果同时有多个线程处于等待状态。当接收到Set信号时,只有一个线程结束其等待状态,它不是等待时间最长的线程,而是优先级最高的线程。

namespace EventSample
{
    class Program
    {
        static void Main()
        {
            const int taskCount = 4;

            var cEvent = new System.Threading.CountdownEvent(taskCount);
            var calcs = new Calculator[taskCount];

            for (int i = 0; i < taskCount; i++)
            {
                calcs[i] = new Calculator(cEvent);
                int i1 = i;
                System.Threading.Tasks.Task.Run(() => calcs[i1].Calculation(i1 + 1, i1 + 3));
            }

            cEvent.Wait();
            System.Console.WriteLine("all finished");

            for (int i = 0; i < taskCount; i++)
            {
                System.Console.WriteLine($"task for {i}, result: {calcs[i].Result}");
            }
        }
    }

    public class Calculator
    {
        private System.Threading.CountdownEvent _cEvent;

        public int Result { get; private set; }

        public Calculator(System.Threading.CountdownEvent ev)
        {
            _cEvent = ev;
        }

        public void Calculation(int x, int y)
        {
            System.Console.WriteLine($"Task {System.Threading.Tasks.Task.CurrentId} starts calculation");
            System.Threading.Tasks.Task.Delay(new System.Random().Next(3000)).Wait();
            Result = x + y;

            // signal the event-completed!
            System.Console.WriteLine($"Task {System.Threading.Tasks.Task.CurrentId} is ready");
            _cEvent.Signal();
        }
    }
}

Barrier

参考

https://blog.csdn.net/wangzhiyu1980/article/details/45688075

Barrier 是 .Net 提供的一直并发的机制,它允许多个任务同步不同阶段的并发工作,多任务按阶段进行同步。

这里的关键点是【多个任务】和【不同阶段】。

假设有4个相同的任务(Task),每个任务都有4个阶段(Phase),当他们并发工作时,只有当所有任务的相同步骤都完成时,所有任务才可以开始下一个步骤。

【CSharp】线程同步_第1张图片

namespace MyBarrier
{
    class Program
    {
        private static void Phase0Doing(int TaskID) => System.Console.WriteLine($"Task : #{TaskID}   =====  Phase 0");
        private static void Phase1Doing(int TaskID) => System.Console.WriteLine($"Task : #{TaskID}   *****  Phase 1");
        private static void Phase2Doing(int TaskID) => System.Console.WriteLine($"Task : #{TaskID}   ^^^^^  Phase 2");
        private static void Phase3Doing(int TaskID) => System.Console.WriteLine($"Task : #{TaskID}   $$$$$  Phase 3");

        private const int TaskNum = 4;
        private static readonly System.Threading.Tasks.Task[] _Tasks = new System.Threading.Tasks.Task[TaskNum];
        private static System.Threading.Barrier _barrier = new System.Threading.Barrier(TaskNum, (barrier) => System.Console.WriteLine($"-- Current Phase:{_barrier.CurrentPhaseNumber} --"));

        static void Main(string[] args)
        {
            for (int i = 0; i < TaskNum; i++)
            {
                // 创建并启动四个任务,每个任务各执行四个阶段的任务
                _Tasks[i] = System.Threading.Tasks.Task.Factory.StartNew((num) => {
                    var taskid = (int)num;
                    Phase0Doing(taskid); _barrier.SignalAndWait();
                    Phase1Doing(taskid); _barrier.SignalAndWait();
                    Phase2Doing(taskid); _barrier.SignalAndWait();
                    Phase3Doing(taskid); _barrier.SignalAndWait();
                }, i);
            }

            var finalTask = System.Threading.Tasks.Task.Factory.ContinueWhenAll(_Tasks, (tasks) =>
            {
                System.Threading.Tasks.Task.WaitAll(_Tasks);
                System.Console.WriteLine("========================================");
                System.Console.WriteLine("All Phase is completed");

                _barrier.Dispose();
            });

            finalTask.Wait();

            System.Console.ReadLine();
        }
    }
}

运行结果:

Task : #1   =====  Phase 0
Task : #0   =====  Phase 0
Task : #3   =====  Phase 0
Task : #2   =====  Phase 0
-- Current Phase:0 --
Task : #0   *****  Phase 1
Task : #3   *****  Phase 1
Task : #2   *****  Phase 1
Task : #1   *****  Phase 1
-- Current Phase:1 --
Task : #3   ^^^^^  Phase 2
Task : #1   ^^^^^  Phase 2
Task : #0   ^^^^^  Phase 2
Task : #2   ^^^^^  Phase 2
-- Current Phase:2 --
Task : #1   $$$$$  Phase 3
Task : #3   $$$$$  Phase 3
Task : #2   $$$$$  Phase 3
Task : #0   $$$$$  Phase 3
-- Current Phase:3 --
========================================
All Phase is completed

根据运行结果可知,在相同的阶段内运行的顺序不尽相同,但是在每个阶段都等到所有任务完成之后才开始下一个阶段的任务。

ReaderWriterLockSlim

为了使锁定机制允许锁定多个读取器(而不是一个写入器)访问某个资源,可以使用ReaderWriterLockSlim类。这个类提供了一个锁定功能,如果没有写入器锁定资源,就允许多个读取器访问资源,但只能有一个写入器锁定该资源。

ReaderWriterLockSlim类有阻塞或不阻塞的方法来获取读取锁,如阻塞的EnterReadLock和不阻塞的TryEnterReadLock方法,还可以使用阻塞的EnterWriterLock和不阻塞的TryEnterWriteLock方法获得 写入锁定。如果任务先读取资源,之后写如资源,它就可以使用EnterUpgradableReadLock或TryEnterUpgradableReadLock方法获得可升级的读取锁定。有了这个锁定,就可以获得写入锁定给,而无需释放读取锁定。

下面的示例程序创建了一个包含6项的集合和一个ReaderWriteLockSlim对象。ReaderMethod方法获得一个读取锁定,读取列表中的所有项,并把它们写到控制台。WriteMethod方法试图获得一个写入锁定,已 改变集合的所有值。在Main方法中,启动6个任务,已调用ReaderMethod或WriteMethod方法 。

namespace ReaderWriterLockSample
{
    class Program
    {
        private static System.Collections.Generic.List _items = new System.Collections.Generic.List() { 0, 1, 2, 3, 4, 5 };
        private static System.Threading.ReaderWriterLockSlim _rwl = new System.Threading.ReaderWriterLockSlim(System.Threading.LockRecursionPolicy.SupportsRecursion);

        public static void ReaderMethod(object reader)
        {
            try
            {
                _rwl.EnterReadLock();

                for (int i = 0; i < _items.Count; i++)
                {
                    System.Console.WriteLine($"读取器({reader})获得锁定,输出集合值。 当前索引:{i}, 当前值:{_items[i]}");
                    System.Threading.Tasks.Task.Delay(40).Wait();
                }
            }
            finally
            {
                _rwl.ExitReadLock();
            }
        }

        public static void WriterMethod(object writer)
        {
            try
            {
                while (!_rwl.TryEnterWriteLock(50))
                {
                    System.Console.WriteLine($"写入器({writer}):等待获取写入锁定");
                    System.Console.WriteLine($"当前读取锁定的数量: {_rwl.CurrentReadCount}");
                }

                System.Console.WriteLine($"写入器({writer}):获取到写入锁定");
                for (int i = 0; i < _items.Count; i++)
                {
                    _items[i]++;
                    System.Threading.Tasks.Task.Delay(50).Wait();
                }
                System.Console.WriteLine($"写入器({writer}):写入完成,即将释放写入锁定");
            }
            finally
            {
                _rwl.ExitWriteLock();
            }
        }

        static void Main()
        {
            System.Console.WriteLine($"程序启动 ...");
            var taskFactory = new System.Threading.Tasks.TaskFactory(System.Threading.Tasks.TaskCreationOptions.LongRunning, System.Threading.Tasks.TaskContinuationOptions.None);
            var tasks = new System.Threading.Tasks.Task[5];
            tasks[0] = taskFactory.StartNew(WriterMethod, 1); // 写1
            tasks[1] = taskFactory.StartNew(ReaderMethod, 1); // 读1
            tasks[2] = taskFactory.StartNew(ReaderMethod, 2); // 读2
            tasks[3] = taskFactory.StartNew(WriterMethod, 2); // 写2
            tasks[4] = taskFactory.StartNew(ReaderMethod, 3); // 读3

            System.Threading.Tasks.Task.WaitAll(tasks);
        }
    }

}

程序运行结果如下:

程序启动后,三个读取锁先后获得锁定,开始遍历集合并输出。在三个读取器执行的过程中,写入锁定处于等待状态。当三个读取操作完成之后,写入才获取到锁定。此时对集合集合的元素+1。同时,可以观察到写入1获取到锁定之后,写入2依然处于等待状态。当写入1完成之后,写入2才能获取到锁定。

程序启动 ...
读取器(3)获得锁定,输出集合值。 当前索引:0, 当前值:0
读取器(1)获得锁定,输出集合值。 当前索引:0, 当前值:0
读取器(2)获得锁定,输出集合值。 当前索引:0, 当前值:0
读取器(2)获得锁定,输出集合值。 当前索引:1, 当前值:1
读取器(1)获得锁定,输出集合值。 当前索引:1, 当前值:1
读取器(3)获得锁定,输出集合值。 当前索引:1, 当前值:1
写入器(1):等待获取写入锁定
当前读取锁定的数量: 3
写入器(2):等待获取写入锁定
当前读取锁定的数量: 3
读取器(2)获得锁定,输出集合值。 当前索引:2, 当前值:2
读取器(3)获得锁定,输出集合值。 当前索引:2, 当前值:2
读取器(1)获得锁定,输出集合值。 当前索引:2, 当前值:2
写入器(1):等待获取写入锁定
当前读取锁定的数量: 3
写入器(2):等待获取写入锁定
当前读取锁定的数量: 3
读取器(1)获得锁定,输出集合值。 当前索引:3, 当前值:3
读取器(2)获得锁定,输出集合值。 当前索引:3, 当前值:3
读取器(3)获得锁定,输出集合值。 当前索引:3, 当前值:3
写入器(1):等待获取写入锁定
当前读取锁定的数量: 3
写入器(2):等待获取写入锁定
当前读取锁定的数量: 3
读取器(1)获得锁定,输出集合值。 当前索引:4, 当前值:4
读取器(2)获得锁定,输出集合值。 当前索引:4, 当前值:4
读取器(3)获得锁定,输出集合值。 当前索引:4, 当前值:4
写入器(1):等待获取写入锁定
当前读取锁定的数量: 3
写入器(2):等待获取写入锁定
当前读取锁定的数量: 3
读取器(3)获得锁定,输出集合值。 当前索引:5, 当前值:5
读取器(1)获得锁定,输出集合值。 当前索引:5, 当前值:5
读取器(2)获得锁定,输出集合值。 当前索引:5, 当前值:5
写入器(1):等待获取写入锁定
当前读取锁定的数量: 3
写入器(2):等待获取写入锁定
当前读取锁定的数量: 3
写入器(1):获取到写入锁定
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(1):写入完成,即将释放写入锁定
写入器(2):获取到写入锁定
写入器(2):写入完成,即将释放写入锁定

再次运行程序:

此时,程序开始运行后,写入1获得到锁定。写入2与读取操作均处于等待状态。写入1完成操作,释放锁定后,写入2获得锁定,读取操作继续处于等待状态。直到写入2完成,并释放锁定之后,读取操作才获取到锁定。

程序启动 ...
写入器(1):获取到写入锁定
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(2):等待获取写入锁定
当前读取锁定的数量: 0
写入器(1):写入完成,即将释放写入锁定
写入器(2):获取到写入锁定
写入器(2):写入完成,即将释放写入锁定
读取器(2)获得锁定,输出集合值。 当前索引:0, 当前值:2
读取器(3)获得锁定,输出集合值。 当前索引:0, 当前值:2
读取器(1)获得锁定,输出集合值。 当前索引:0, 当前值:2
读取器(1)获得锁定,输出集合值。 当前索引:1, 当前值:3
读取器(3)获得锁定,输出集合值。 当前索引:1, 当前值:3
读取器(2)获得锁定,输出集合值。 当前索引:1, 当前值:3
读取器(2)获得锁定,输出集合值。 当前索引:2, 当前值:4
读取器(3)获得锁定,输出集合值。 当前索引:2, 当前值:4
读取器(1)获得锁定,输出集合值。 当前索引:2, 当前值:4
读取器(1)获得锁定,输出集合值。 当前索引:3, 当前值:5
读取器(3)获得锁定,输出集合值。 当前索引:3, 当前值:5
读取器(2)获得锁定,输出集合值。 当前索引:3, 当前值:5
读取器(2)获得锁定,输出集合值。 当前索引:4, 当前值:6
读取器(3)获得锁定,输出集合值。 当前索引:4, 当前值:6
读取器(1)获得锁定,输出集合值。 当前索引:4, 当前值:6
读取器(2)获得锁定,输出集合值。 当前索引:5, 当前值:7
读取器(3)获得锁定,输出集合值。 当前索引:5, 当前值:7
读取器(1)获得锁定,输出集合值。 当前索引:5, 当前值:7

 

你可能感兴趣的:(CSharp)