多线程之旅
来源:互联网 发布:java常用算法手册pdf 编辑:程序博客网 时间:2024/06/05 03:10
多线程之旅——从概念开始
对概念的理解是我们做任何事情的基础,因此我们从概念开始吧
程序执行顺序是按照串行执行的假设:
比如我们读诗词,默认从上到下
1.床 前 明 月 光,
2.疑 是 地 上 霜。
3.举 头 望 明 月,
4.低 头 思 故 乡。
而多线程以后,就很有可能变成
3.举 头 望 明 月,
1.床 前 明 月 光,
1.床 前 明 月 光,
4.低 头 思 故 乡。
一个列队中元素的数量必须小于或者等于存储元素的数组长度。如果这个列队只是在串行程序中使用,那么只要在所有共有方法的入口点和出口点保持这个不变性就足以保证程序的正确性。
一旦缺乏这种假设存在的前提条件,除非使用了某种特殊的方法,不然也许每行程序也许都要确保并发执行的时候不会出错,这将会让事情变得非常复杂。
可能一行代码被执行多次,导致你假设中的算法流程被破坏。
临界域是保证程序能串行执行的一种方式。
临界域(Critical Regiion)的概念:
信号量是规定了一定线程数可以执行的概念,这在实现对集合资源的保护时很有用。记住,信号量不属于某单一线程。
可以认为临界域是信号量的一个特例,规定了只有一个线程可以执行,也就是互斥。
信号量和临界域可以结合起来使用,以后会有例子说明。
常见的同义词诸如“加锁/释放”,“进入/退出","开始/结束"等,同义词虽多,可是表示的都是一个相同的概念。只要所有执行临界域代码的线程都按照一直的方式来访问数据,那么就可以避免发生数据间竞争的问题。
用伪代码表示就是:
EnterCriticalRegion();DoSomeThings();LeaveCriticalRegion();
有些临界域可以支持共享模式,例如read/write锁,这种方式能够使得多个线程并发的读取共享数据。
当然临界域这一概念的实现有很多种算法,我们先不讨论这个。因为操作系统对临界域这一概念有不少的既有实现。
不过相应的需求如下:
1.保证互斥性。
2.在临界域中的操作要进得来也要出得去。不能有线程因为死锁或者活锁问题导致无限期的停留在临界域中。
3.提供某种程度的公平性。
4.最好实现是低开销的。因为底层系统会频繁地使用临界域,进进出出。
可重入函数:
重入表示在一次函数执行的过程中,没有执行完时,又再次进入同一函数。冲入现象可能由同一线程造成,比如说递归;也有可能由多线程并发调用函数造成。
一个函数可重入表示这个函数的结果不会因为重入而产生变化,是稳定的。
粒度问题:
a++是用来说明多线程容易造成问题的最常用的例子。但是实现这种低级别的同步还是相对简单的,很多cpu内置提供支持。
但是更高级别的同步,比如说一个对象方法,包含很多个步骤,那么要保证其线程安全,就没那么简单。
在程序中通常包含一组子系统以及各种复合的数据结构,并且这些数据结构可能被多个线程并发访问。有两种方式来组织临界域:
粗粒度:通过只使用一个锁来保护子系统以及复合数据结构中的各个部分。优点:易于管理使用。缺点:伸缩性不强。
细粒度:对每一部分分别加锁。优点:高伸缩高并发。缺点:锁太多,难以合理划分和组织。误用时会产生很多问题。
线程:比喻为执行函数的虚拟处理器。
线程的状态:
1.假设线程没有状态的情况下,采用最简单也是最直观的“忙等待”(自旋)方式。下面这段程序中谓词(predicate ,也就是判断条件)保护了DoSomething的执行。
while(!p)DoSomething();
直到p为true,也就是获得进入临界区的资格后,DoSomething才执行。否则就一直原地打转,不停的检查P的值。
但是忙等待依然消耗CPU的运行周期,直到它的时间片用完或者系统抢占把cpu资源分配到其他线程。在自旋过程中会阻止了p为true时其他线程的运行,也就是说已经进入临界区准备执行DoSomething的线程也必须等待该自旋线程释放CPU资源。这个执行DoSomething的线程估计会郁闷,心想终于轮到我执行了,CPU运行周期却被自旋线程用来不停对P求值了。
因为把CPU周期浪费在了不停检查p这个共享内存的状态上,所以忙等待在大部分情况下都不是一种好的做法。这种大量使用CPU时钟周期和执行内存访问的操作,会导致频繁的总线通信以及能源消耗(特别是对于移动设备)
所以我们需要其他的手段来表示线程等待,最好干脆就是操作系统帮我们封装好了。这样我们只要在对线程说"hold 住",该线程就变成等待状态,不再占用CPU运行周期。
系统内置对线程状态的支持:
Windows操作系统通过各种内核对象来提供真正的等待功能。
当线程等待时,它将进入等待状态(与运行状态相对应),这将触发上下文切换操作以将这个线程立即从这个处理器上移走,并且确保Windows线程调度器不会将它作为下一个将要运行的线程。这避免了CPU计算能力以及能源的 浪费,并允许系统中其他线程的执行。假设有系统函数Wait,这个函数可以使得线程进入等待状态,上面的忙等待代码就变成
if(!p) Wati();DoSomeThing();
现在,临界域中的线程不仅要使得P变为true,而且必须要考虑其他线程也可能处于等待状态。用一个WakeUp方法唤醒等待中的一个或多个线程
p = true;WakeUP();
线程安全方面:
数据的状态
在面向对象的编程系统中,一个典型的对象由保存状态的字段和操作状态的方法组成,状态被破坏意味着会产生不可预计的后果。
1.共享状态
当状态被共享时,多个线程对状态的并发访问将会在时间上发生重叠;当这些线程在访问共享状态发生重叠时,那么彼此之间的操作将会相互干扰。
.NET框架的类型安全在一定程度上保证了私有状态,因为如果程序中能够生成一个指向进程地址空间中任意位置的指针,那么整个地址空间中的数据都是共享状态的。
共享状态具有可传递性。
new 出来的对象只有创建该对象的线程可以访问,所以是线程安全的,不过一但被共享状态所引用(如静态变量),那么就不再是线程安全的。
2.私有状态
方法栈,私有变量,参数
5.readonly一定程度上保障了数据的不可变性。虽然可以多次赋值。
多线程之旅二——线程与进程简介
进程:分配系统资源的一种数据结构。
简而言之,就像一个储物箱一样,特点是为了保证安全,一个进程中的指针被设计为不可访问其他进程的地址。就像你的钥匙正常情况下不能随便打开任何一个人的储物箱一样,你只能打开自己的储物箱。如果打开别人的储物箱,不是无意的编码疏忽,就是恶意的攻击其他程序。
多线程之旅之三——Windows内核对象同步机制
...
DWORD WINAPI WaitForSingleObject(hMutant , IFINITE);
...
mutex.WaitOne();
EventWaitHandle
AutoResetEvent
ManualResetEvent
Semaphore
Mutex
public abstract class WaitHandle : MarshalByRefObject, IDisposable { public virtual void Close(); public void Dispose(); public virtual Boolean WaitOne(); public virtual Boolean WaitOne(Int32 millisecondsTimeout); public static Int32 WaitAny(WaitHandle[] waitHandles); public static Int32 WaitAny(WaitHandle[] waitHandles, Int32 millisecondsTimeout); public static Boolean WaitAll(WaitHandle[] waitHandles); public static Boolean WaitAll(WaitHandle[] waitHandles, Int32 millisecondsTimeout); public static Boolean SignalAndWait(WaitHandle toSignal, WaitHandle toWaitOn); public static Boolean SignalAndWait(WaitHandle toSignal, WaitHandle toWaitOn, Int32 millisecondsTimeout, Boolean exitContext) public SafeWaitHandle SafeWaitHandle { get; set; } // Returned from WaitAny if a timeout occurs public const Int32 WaitTimeout = 0x102; }
public sealed class SimpleWaitLock : IDisposable { private Semaphore m_AvailableResources; public SimpleWaitLock(Int32 maximumConcurrentThreads) { m_AvailableResources = new Semaphore(maximumConcurrentThreads, maximumConcurrentThreads); } public void Enter() { // Wait efficiently in the kernel for resource access, then return m_AvailableResources.WaitOne(); } public void Leave() { // This thread doesn’t need access anymore; another thread can have it m_ AvailableResources.Release(); } public void Dispose() { m_AvailableResources.Close(); } }
public class BlockingQueueWithAutoResetEvents <T> { private Queue <T> m_queue = new Queue <T>(); private Mutex m_mutex = new Mutex (); private AutoResetEvent m_event = new AutoResetEvent (false ); public void Enqueue(T obj) { // Enter the critical region and insert into our queue. m_mutex.WaitOne(); try { m_queue.Enqueue(obj); } finally { m_mutex.ReleaseMutex(); } // Note that an item is available, possibly waking a consumer. m_event.Set(); } public T Dequeue() { // Dequeue the item from within our critical region. T value; bool taken = true ; m_mutex.WaitOne(); try { // If the queue is empty, we will need to exit the // critical region and wait for the event to be set. while (m_queue.Count == 0) { taken = false ; WaitHandle .SignalAndWait(m_mutex, m_event); m_mutex.WaitOne(); taken = true ; } value = m_queue.Dequeue(); } finally { if (taken) { m_mutex.ReleaseMutex(); } } return value; } }
public class BlockingBoundedQueue<T> { private Queue<T> m_queue = new Queue<T>(); private Mutex m_mutex = new Mutex(); private Semaphore m_producerSemaphore; private Semaphore m_consumerSemaphore; public BlockingBoundedQueue(int capacity) { m_producerSemaphore = new Semaphore(capacity, capacity); m_consumerSemaphore = new Semaphore(0, capacity); } public void Enqueue(T obj) { // Ensure the buffer hasn't become full yet. If it has, we will // be blocked until a consumer takes an item. m_producerSemaphore.WaitOne(); // Now enter the critical region and insert into our queue. m_mutex.WaitOne(); try { m_queue.Enqueue(obj); } finally { m_mutex.ReleaseMutex(); } // Note that an item is available, possibly waking a consumer. m_consumerSemaphore.Release(); } public T Dequeue() { // This call will block if the queue is empty. m_consumerSemaphore.WaitOne(); // Dequeue the item from within our critical region. T value; m_mutex.WaitOne(); try { value = m_queue.Dequeue(); } finally { m_mutex.ReleaseMutex(); } // Note that we took an item, possibly waking producers. m_producerSemaphore.Release(); return value; } }
多线程之旅之四——浅谈内存模型和用户态同步机制
Interlocked类中的每个方法都执行一次原子性的读取以及写入操作,其中publicstatic Int32 Increment(ref Int32 location)方法是最常用到的方法,后面我在自定一个混合结构锁的时候就会用到。
public static class Interlocked { // return (++location) public static Int32 Increment(ref Int32 location); // return (--location) public static Int32 Decrement(ref Int32 location); // return (location1 += value) public static Int32 Add(ref Int32 location1, Int32 value); // Int32 old = location1; location1 = value; return old; public static Int32 Exchange(ref Int32 location1, Int32 value); // Int32 old = location1; // if (location1 == comparand) location1 = value; // return old; public static Int32 CompareExchange(ref Int32 location1, Int32 value, Int32 comparand); ... }
假如有多个线程调用了Enter方法,那么只有一个线程能满足条件进入while的内部,其他线程都因为不满足条件而在不断的判断while条件。
exchange方法确保第一个调用的线程将m_ResourceInUse变为1,并且原始值为0.而其他线程将会使得m_ResourceInUse从1变为1,也就是原始值为1,不满足条件。
class SimpleSpinLock { private Int32 m_ResourceInUse; // 0=false (default), 1=true public void Enter() { // Set the resource to in-use and if this thread while (Interlocked.Exchange(ref m_ResourceInUse, 1) != 0) { } } public void Leave() { Thread.VolatileWrite(ref m_ResourceInUse, 0); } }
public sealed class SomeResource { private SimpleSpinLock m_sl = new SimpleSpinLock(); public void AccessResource() { m_sl.Enter(); // Only one thread at a time can get in here to access the resource... m_sl.Leave(); } }
exchange是原子判断true和false的一个常用办法。
int a = 0;a++;
当编译器把这行C#语句编译成汇编代码的时候,将会包含多条指令,如:
INC EAX
MOV [a], EAX
int a = 0; int tmp = a;tmp++;a = tmp;
注意,纵向是时间线,#n表示当前时候a的值。
我们的原本想法应该是这样执行:
但是由于抢占式操作系统线程的推进是不可预测的,真正执行的时候可能是这样
在上面的执行流程中,t1首先更新为1,然后t2更新为2.此时,从系统中其他线程的角度来看,似乎一切都正常。
然后,此时t3被唤醒继续执行,它将覆盖t1和t2的执行结果,重新将a的值设置为1.
这是一个典型的数据竞争问题,之所以称为“竞争”,是因为代码执行的正确性完全依赖于多个线程之间的竞争结果。每个线程都试图最先执行完代码,并且根据哪个线程最先执行完成的不同,会导致不同的结果。也就是相同的源代码,不同的执行结果。
class SimpleHybridLock : IDisposable { private Int32 m_waiters = 0; // The AutoResetEvent is the primitive kernel-mode construct private AutoResetEvent m_waiterLock = new AutoResetEvent(false); public void Enter() { if (Interlocked.Increment(ref m_waiters) == 1) //what will happen if we use m_waiters++ in this place? return; //return means we enter critical region// Another thread is waiting. There is contention, block this thread m_waiterLock.WaitOne(); // Bad performance hit here // When WaitOne returns, this thread now has the lock } public void Leave() { // This thread is releasing the lock if (Interlocked.Decrement(ref m_waiters) == 0) return; // No other threads are blocked, just return // Other threads are blocked, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } public void Dispose() { m_waiterLock.Dispose(); } }
我们用一个int私有字段来计数,确保只有一个线程调用该方法的时候不会调用到非常影响性能的内核对象。只有多个线程并发的访问这个方法的时候,才会初始化内核对象,阻塞线程。
我们可以给这个锁加入更多的功能,这时我们需要保存更多的信息,也就需要更多的字段,比如说保存哪个线程拥有这个锁,以及它拥有了多少次。在多个线程并发访问的时候,我们也可以推迟一段时间再创建内核对象,可以加入spin lock先自旋一段时间。
internal sealed class AnotherHybridLock : IDisposable { // The Int32 is used by the primitive user-mode constructs (Interlocked methods) private Int32 m_waiters = 0; // The AutoResetEvent is the primitive kernel-mode construct private AutoResetEvent m_waiterLock = new AutoResetEvent(false); // This field controls spinning in an effort to improve performance private Int32 m_spincount = 4000; // Arbitrarily chosen count // These fields indicate which thread owns the lock and how many times it owns it private Int32 m_owningThreadId = 0, m_recursion = 0; public void Enter() { // If calling thread already owns the lock, increment recursion count and return Int32 threadId = Thread.CurrentThread.ManagedThreadId; if (threadId == m_owningThreadId) { m_recursion++; return; } // The calling thread doesn't own the lock, try to get it SpinWait spinwait = new SpinWait(); for (Int32 spinCount = 0; spinCount < m_spincount; spinCount++) { // If the lock was free, this thread got it; set some state and return if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0) goto GotLock; // Black magic: give other threads a chance to run // in hopes that the lock will be released spinwait.SpinOnce(); } // Spinning is over and the lock was still not obtained, try one more time if (Interlocked.Increment(ref m_waiters) > 1) { // Other threads are blocked and this thread must block too m_waiterLock.WaitOne(); // Wait for the lock; performance hit // When this thread wakes, it owns the lock; set some state and return } GotLock: // When a thread gets the lock, we record its ID and // indicate that the thread owns the lock once m_owningThreadId = threadId; m_recursion = 1; } public void Leave() { // If the calling thread doesn't own the lock, there is a bug Int32 threadId = Thread.CurrentThread.ManagedThreadId; if (threadId != m_owningThreadId) throw new SynchronizationLockException("Lock not owned by calling thread"); // Decrement the recursion count. If this thread still owns the lock, just return if (--m_recursion > 0) return; m_owningThreadId = 0; // No thread owns the lock now // If no other threads are blocked, just return if (Interlocked.Decrement(ref m_waiters) == 0) return; // Other threads are blocked, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } public void Dispose() { m_waiterLock.Dispose(); } }
当然锁变复杂了,性能也会有相应的降低。有所得有所失去。
Sync block
堆上的每个对象都可以关联一个叫做Sync block(同步块)的数据结构。同步块包含字段,这些字段和上面我们实现的锁中的字段的作用是差不多的。具体地说,它为一个内核对象、拥有线程的ID、递归计数器、等待线程的计数提供了保存的地方。
照例配图一张,要不光看我文字描述不太容易懂:
因此同步块干啥子的?用来保存数据的呗……
当然,同步块也不是一开始就上的,上面这张图隐藏了点信息。就是其实那个指向同步块的指针有2个指针大小的内存,还保存着hashcode的值还有一些其他 东西。如果块内存不足以保存这些信息,那么才会为这个对象分配一个共享内存池中的同步块。这就是Object Header Inflation现象。
懂得相同之处了,再来理解为什么锁type类型危险的,究其原因就是type能被很多地方访问,甚至能跨appdomain,这就很有可能你莫名其妙就和另一 个appdomain中的锁用到同一个同步块了。同样情况的类型还有于AppDomain无关的反射类型,比如说啥子MemberInfo之类的。
class Program { static void Main(string[] args) { var syncTest = new SyncTest(); Thread t1 = new Thread(syncTest.LongSyncMethod); // critical region 1 t1.Start(); Thread t2 = new Thread(syncTest.NoSyncMethod); t2.Start(); Thread t3 = new Thread(syncTest.LongSyncMethod);// critical region 1 t3.Start(); Thread t4 = new Thread(syncTest.NoSyncMethod); t4.Start(); Thread t5 = new Thread(syncTest.NoSyncMethod); t5.Start(); Thread t6 = new Thread(syncTest.SyncMethodUsingPrivateObject);// critical region 2 t6.Start(); Thread t7 = new Thread(syncTest.SyncMethodUsingPrivateObject);// critical region 2 t7.Start(); } } class SyncTest { private object _lock = new object(); [MethodImplAttribute(MethodImplOptions.Synchronized)] public void LongSyncMethod() { Console.WriteLine("being asleep"); Thread.Sleep(10000); } public void NoSyncMethod() { Console.WriteLine("do sth"); } public void SyncMethodUsingPrivateObject() { lock (_lock) { Console.WriteLine("another critical section"); Thread.Sleep(5000); } } }
bool acquired = false;object tmp = listLock;try{ Monitor.Enter(tmp, ref acquired); list.Add("item");} finally { if (acquired) { Monitor.Release(tmp); } }
public class BlockingQueue<T> { private Queue<T> m_queue = new Queue<T>(); private int m_waitingConsumers = 0; public int Count { get { lock (m_queue) return m_queue.Count; } } public void Clear() { lock (m_queue) m_queue.Clear(); } public bool Contains(T item) { lock (m_queue) return m_queue.Contains(item); } public void Enqueue(T item) { lock (m_queue) { m_queue.Enqueue(item); // Wake consumers waiting for a new element. if (m_waitingConsumers > 0) Monitor.Pulse(m_queue); } } public T Dequeue() { lock (m_queue) { while (m_queue.Count == 0) { //Queue is empty, wait until en element arrives. 644 Chapter 12: Parallel Containers m_waitingConsumers++; try { Monitor.Wait(m_queue); } finally { m_waitingConsumers--; } } return m_queue.Dequeue(); } } public T Peek() { lock (m_queue) return m_queue.Peek(); } }
1.多线程之旅——从概念开始
2.多线程之旅二——线程
3.多线程之旅之三——Windows内核对象同步机制
4.多线程之旅之四——浅谈内存模型和用户态同步机制
多线程之旅
1.多线程之旅——从概念开始
2.多线程之旅二——线程
3.多线程之旅之三——Windows内核对象同步机制
4.多线程之旅之四——浅谈内存模型和用户态同步机制
5.多线程之旅之五——线程池与I/O完成端口
6.多线程之旅六——异步编程模式,自己实现IAsyncResult
7.多线程之旅七——再谈内存模型
8.多线程之旅八——多线程下的数据结构
- 多线程之旅
- [多线程之旅] 一、初识多线程
- Java之旅--多线程进阶
- 多线程之旅(1)—
- C#多线程之旅(4)
- C#多线程之旅(3)
- C#多线程之旅(1)
- Java多线程之旅(一)
- 多线程之传统多线程
- 多线程总结之旅(3):多线程的优缺点
- Android之旅--Handler与多线程
- [多线程之旅] 三、线程同步基础
- [多线程之旅]四、浅谈volatile
- 杨小麦OC之旅--多线程
- Android之旅(三):Android 多线程
- Java多线程之认识多线程
- iosGCD多线程之创建多线程
- [iphone开发多线程之]多线程之NSInvocationOperation
- 大页内存原理及使用设置
- POJ1006: 中国剩余定理的完美演绎
- android 界面设计大全
- Oracle常用Sql--初级
- Java 完美判断中文字符
- 多线程之旅
- 【C++进阶】深入理解C/C++(1)
- MFC控制输入法
- How to use tesseract-ocr
- Android Camera的HAL接口
- JS 父子页面刷新
- [ASP.NET]如何在表单提交(form,submit)后保持select的选择值
- 海量数据处理算法—Bit-Map
- 不只是技术!成为IT经理必备的十大软技能