CLR via C#:混合线程同步构造

基础知识:如下所示:
1.混合线程同步构造合并了用户模式构造和内核模式构造。
2.同步块中包含内核对象,拥有线程标识,递归计数以及等待线程计数。
3.CLR初始化时在堆中分配一个可以动态扩容的同步块数组。
4.堆对象的同步块索引初始值为-1。
5.在选择使用哪种线程同步方案时,优先选择用户模式构造(Volatile, Interlock),其次选择混合模式构造(ManualResetEventSlim, SemaphoreSlim, 禁用Monitor, ReaderWriterLockSlim, CountdownEvent, Barrier),最后选择内核模式构造(AutoResetEvent, ManualResetEvent, Semaphore, 禁用Mutex)。
6.在CLR中,对任何锁函数的调用都构成一个完整的内存栅栏。在栅栏之前写入的任何变量都必须在栅栏之前完成;在栅栏之后的任何变量读取都必须在栅栏之后开始。

ManualResetEventSlim / SemaphoreSlim类:如果线程没有竞争资源,就使用用户模式构造。如果线程有竞争资源,当线程自旋期间获取到资源就使用用户模式构造;否则就使用内核模式构造。

Monitor类:具有以下特性:
1.Enter函数:当对象的同步块索引值为-1时,CLR就设置对象的同步块索引指向同步块数组中的一个空白同步块,然后调用该函数的线程就可以拥有锁。当对象同步块索引值不为-1时,调用该函数的线程就阻塞。
2.Exit函数:调用该函数的线程会释放锁。当没有线程等待对象引用的同步块时,对象的同步块索引就被设回成-1,同步块也被设回成同步块数组中的空白同步块。当有线程等待对象引用的同步块时,该等待的线程就可以拥有锁。
3.Monitor类应该被设计成可以实例化并在上面调用实例函数。
4.调用Monitor类的函数时,传递对代理对象(父类为MarshalByRefObject的对象)的引用,锁定的是代理对象,而不是代理引用的实际对象。
5.永远不要向Monitor类的函数传递类型对象引用。因为当该类型对象是通过AppDomain中立方式加载时,线程就会跨越进程中所有AppDomain在那个类型上获取锁,这就破坏了AppDomain本应提供的隔离能力。
6.永远不要向Monitor类的函数传递String对象引用。因为不同的AppDomain中含有该操作的线程都会以同步的方式执行,这就破坏了AppDomain本应提供的隔离能力。
7.永远不要向Monitor类的函数传递值类型变量。因为该值类型变量会被装箱成Object类型对象,从而造成每次锁定的Object对象都不相同,进而无法实现线程同步。
8.向函数应用[MethodImpl(MethodImplOptions.Synchoronized)]定制特性,会造成JIT编译器使用Monitor.Enter函数和Monitor.Exit函数来包围函数实现体。
9.永远不要使用C#的lock语句。因为它不仅会让代码的速度变慢,还会造成线程访问损坏的状态。等价代码如下所示:

Boolean lockTaken = false;
try
{
	Monitor.Enter(this, ref lockTaken);
	// 这里的代码拥有对数据的独立访问权......
}
finally
{
	if (lockTaken)
	{
		Monitor.Exit(this);
	}
}

10.Wait函数:调用该函数的线程处于阻塞状态并释放锁。
11.Pulse函数:调用该函数的线程释放锁后唤醒一个阻塞时间最长的线程。
12.PulseAll函数:调用该函数的线程释放锁后唤醒所有阻塞的线程。

ReaderWriterLockSlim类:具有以下特性:
1.一个线程向数据写入时,请求访问的其他所有线程都被阻塞。
2.向数据写入的线程结束后,要么解除一个写入线程的阻塞,使它能向数据写入;要么解除所有读取线程的阻塞,使它们能并发读取数据。如果没有线程被阻塞,锁就进入可以自由使用状态,可供下一个读或写线程使用。
3.一个线程从数据读取时,请求读取的其他线程允许继续执行,但请求写入的所有线程仍被阻塞。
4.从数据读取的所有线程结束后,一个写入线程被解除阻塞,使它能向数据写入。如果没有线程被阻塞,锁就进入可以自由使用状态,可供下一个读或写线程使用。
5.当使用LockRecursionPolicy.SupportRecursion参数来构造ReaderWriterLockSlim对象时,该对象内部会使用一个互斥的自旋锁来支持线程所有权和递归计数行为。但是这些行为对锁的性能有负面影响,所以建议使用LockRecursionPolicy.NoRecursion参数来构造ReaderWriterLockSlim对象。
6.ReaderWriterLockSlim对象还支持线程在读和写之间相互切换。但是这样对锁的性能有负面影响,所以建议最好不要使用。
7.禁用ReaderWriterLock类。因为它存在以下问题:
1>.即使不存在线程竞争,它的速度也非常慢。
2>.线程所有权和递归计数行为被强加到ReaderWriterLock对象上,不能被取消,这就让锁变的更慢。
3>.读取线程被优先服务,写入线程排很长的队都未必能获取到服务,从而造成拒绝服务(Dos)问题。

CountdownEvent类:具有以下特性:
1.CountdownEvent对象内部使用了一个ManualResetEventSlim对象。
2.CountdownEvent对象的CurrentCount字段值不为0时,调用线程就会在该对象上阻塞;否则在该对象上阻塞的线程就会被唤醒,但是该对象就不能被再次更改了,此时调用Add函数会抛出InvalidOperationException,调用TryAdd函数会返回false。

Barrier类:具有以下特性:
1.创建Barrier对象时,可以指定参与阶段工作的线程个数以及所有参与线程都完成阶段工作时调用的回调函数。
2.可以调用AddParticipant函数和RemoveParticipant函数来动态添加和删除参与阶段工作的线程。
3.每一个参与线程完成阶段工作后,就会调用SignalAndWait函数。当调用该函数的参与线程是最后一个时,就会调用创建Barrier对象时传递的回到函数,然后解除所有阻塞线程,使它们开始下一阶段工作。

双检索技术:具有以下特性:
1.开发人员用它将单实例对象的构造推迟到应用程序首次请求该对象时进行。
2.当多个线程同时请求单实例对象时,就必须使用线程同步机制保证单实例对象只被构造一次。
3.多线程环境下,以阻塞方式只创建一个单实例对象的参考代码如下所示:

public sealed class Singleton
{
	// 线程安全所需要的操作对象
	private static Object s_lock = new Object();
	// 该字段引用一个单实例对象
	private static Singleton s_value = null;

	// 私有构造函数阻止任何外部代码创建实例
	private Singleton()
	{
		// 把初始化单实例对象的代码放在这里......
	}

	// 以下公共静态函数返回单实例对象(如有必要就创建它)
	public static Singleton GetSingleton()
	{
		// 如果单实例对象已经创建,直接返回它(这样速度很快)
		if (s_value != null)
		{
			return s_value;
		}

		// 还没有创建,让一个线程创建它
		Monitor.Enter(s_lock);
		if (s_value == null)
		{
			// 仍未创建,创建它
			Singleton temp = new Singleton();
			// 使用用户模式构造Volatile,保证temp必须分配内存以及调用了构造函数后,才写入到
			// 单实例字段s_value中
			Volatile.Write(ref s_value, temp);
		}
		Monitor.Exit(s_lock);

		// 返回对单实例对象的引用
		return s_value;
	}
}

4.多线程环境下,以非阻塞方式创建多个单实例对象,但是最终只保留一个单实例对象的参考代码如下所示:

public sealed class Singleton
{
	// 该字段引用一个单实例对象
	private static Singleton s_value = null;

	// 私有构造函数阻止任何外部代码创建实例
	private Singleton()
	{
		// 把初始化单实例对象的代码放在这里......
	}

	// 以下公共静态函数返回单实例对象(如有必要就创建它)
	public static Singleton GetSingleton()
	{
		// 如果单实例对象已经创建,直接返回它(这样速度很快)
		if (s_value != null)
		{
			return s_value;
		}

		// 存在多线程都构造单实例对象
		Singleton temp = new Singleton();
		// 使用用户模式构造Interlock,保证s_value没有引用单实例对象且
		// temp必须分配内存以及调用了构造函数后才写入到单实例字段s_value中
		Interlock.CompareExchange(ref s_value, temp, null);

		// 返回对单实例对象的引用
		return s_value;
	}
}

5.FCL的Lazy类具有以下特性:
1>.构造Lazy实例时,需要指定委托(Func)以及线程安全模式(LazyThreadSafetyMode)。其中线程安全模式的定义如下表说是:

变量名 说明
None 完全没有线程安全支持(适合GUI应用程序)
ExecutionAndPublication 使用双检索技术,参考第3点实现方式。
PublicationOnly 使用Interlock.CompareExchange技术,参考第4点实现方式。

2.Value函数:没有调用委托时就调用委托并返回委托值;否则就返回之前委托值。
3.IsValueCreated函数:没有调用Value函数时就返回false;否则就返回true。
4.LazyInitializer.EnsureInitialized静态函数可以实现在不额外创建Lazy实例的情况下,延时构造指定类型的实例。

条件变量模式:线程根据复合条件来决定它的同步操作。以线程安全的队列为例,参考代码如下所示:

internal sealed class SynchronizedQueue<T>
{
	private readonly Object m_lock = new Object();
	private readonly Queue<T> m_queue = new Queue<T>();

	public void Enqueue(T item)
	{
		Monitor.Enter(m_lock);

		// 一个数据项入队列后,就唤醒所有正在阻塞的线程
		m_queue.Enqueue(item);
		Monitor.PulseAll(m_lock);
		Monitor.Exit(m_lock);
	}

	public T Dequeue()
	{
		Monitor.Enter(m_lock);

		// 队列为空(这是条件)就一直循环
		while (m_queue.Count == 0)
			Monitor.Wait(m_lock);

		// 使一个数据项出队列,返回它供处理
		T item = m_queue.Dequeue();
		Monitor.Exit(m_lock);
		return item;
	}
}

异步的同步构造:具有以下特性:
1.SemaphoreSlim.WaitAsync函数:如果线程得不到锁,可以直接返回并执行其他工作,而不必在那里傻傻的阻塞。以后当锁可用时,代码可恢复执行并访问锁所保护的资源。
2.ConcurrentExclusiveSchedulerPair类当中的ExclusiveScheduler属性调度的任何Task将独占式的运行。
3.ConcurrentExclusiveSchedulerPair类当中的ConcurrentScheduler属性调度的Task可以同时运行。

并发集合类:具有以下特性:
1.FCL自带四个线程安全且非阻塞的并发集合类。依次为:并发队列(ConcurrentQueue),并发栈(ConcurrentStack),并发背包(ConcurrentBag),并发字典(ConcurrentDictionary)。
2.实现了IProducerConsumerCollection接口的非阻塞并发集合对象都可以通过BlockingCollection类的辅助函数来转变成一个阻塞的并发集合对象。如果集合已满,那么负责生产(添加)数据项的线程会阻塞;如果集合已空,那么负责消费(移除)数据项的线程会阻塞。

你可能感兴趣的:(.NET)