多线程基础
转自:http://www.cnblogs.com/huangxincheng/category/362940.html
第一天 尝试Thread 一:Thread的使用
我们知道这个类代表处理器线程,在Thread中有几个比较常用和重要的方法。
<1> sleep: 这个算是最简单的了。
<2> join: 这个可以让并发行处理变成串行化,什么意思呢?上代码说话最清楚。 <3> Interrupt和Abort:这两个关键字都是用来强制终止线程,不过两者还是有区别的。
① Interrupt: 抛出的是 ThreadInterruptedException 异常。
Abort: 抛出的是 ThreadAbortException 异常。
② Interrupt:如果终止工作线程,只能管到一次,工作线程的下一次sleep就管不到了,相当于一个
contine操作。
Abort:这个就是相当于一个break操作,工作线程彻底死掉。 二:线程使用场景
可能线程的使用有点类似wcf,做一些耗时但不很及时的需求,比如可以开线程下图片,连接数据库等等,当然线程可以用来做负载
三:对线程的一些思考
我们知道线程的优点还是比较多的,每个线程都需要默认的堆栈空间,所以说线程数受到内存空间大小的限制,如果线程数开的太多
反而适得其反,进程被分配的时间片会被线程分的更细,也就导致了处理器需要更频繁的在线程之间来回切换。
~~~~~~~~~~~~~~~~~~~~~~~~
第二天 锁机制
当多个线程在并发的时候,难免会碰到相互冲突的事情,比如最经典的ATM机的问题,并发不可怕,可怕的是我们没有能力控制线程以我的理解可以分为三种
① 锁。
② 互斥。
③ 信号。
好,这一篇主要整理“锁”,C#提供了2种手工控制的锁
一: Monitor类
这个算是实现锁机制的纯正类,在锁定的临界区中只允许让一个线程访问,其他线程排队等待。主要整理为2组方法。 1:Monitor.Enter和Monitor.Exit
微软很照护我们,给了我们语法糖Lock,对的,语言糖确实减少了我们不必要的劳动并且让代码更可观,但是如果我们要精细的
控制,则必须使用原生类,这里要注意一个问题就是“锁住什么”的问题,一般情况下我们锁住的都是静态对象,我们知道静态对象
属于类级别,当有很多线程共同访问的时候,那个静态对象对多个线程来说是一个,不像实例字段会被认为是多个 //进入临界区 Monitor.Enter(obj);
Console.WriteLine("当前数字:{0}", ++count);
//退出临界区 Monitor.Exit(obj);
2:Monitor.Wait和Monitor.Pulse
首先这两个方法是成对出现,通常使用在Enter,Exit之间。
Wait: 暂时的释放资源锁,然后该线程进入”等待队列“中,那么自然别的线程就能获取到资源锁。
Pulse: 唤醒“等待队列”中的线程,那么当时被Wait的线程就重新获取到了锁。
这里我们是否注意到了两点:
① 可能A线程进入到临界区后,需要B线程做一些初始化操作,然后A线程继续干剩下的事情。
② 用上面的两个方法,我们可以实现线程间的彼此通信。
二:ReaderWriterLock类
先前也知道,Monitor实现的是在读写两种情况的临界区中只可以让一个线程访问,那么如果业务中存在”读取密集型“操作,就
好比数据库一样,读取的操作永远比写入的操作多。针对这种情况,我们使用Monitor的话很吃亏,不过没关系,ReadWriterLock
就很牛X,因为实现了”写入串行“,”读取并行“。
ReaderWriteLock中主要用3组方法:
<1> AcquireWriterLock: 获取写入锁。
ReleaseWriterLock:释放写入锁。
<2> AcquireReaderLock: 获取读锁。
ReleaseReaderLock:释放读锁。
<3> UpgradeToWriterLock:将读锁转为写锁。
DowngradeFromWriterLock:将写锁还原为读锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
第三天 互斥体
一:Mutex
出现了一个亮点,可用于“进程间同步“,既然进程间都可以同步,那线程同步对它来说不是小菜一碟吗?好的,还是看下Mutex在
线程中发挥的神奇功效。
1: 线程间同步
Metux中提供了WatiOne和ReleaseMutex来确保只有一个线程来访问共享资源,是不是跟Monitor很类似,下面我还是举个简单的例子,
注意我并没有给Metux取名字。
2:进程间同步
这次我给Mutex取个名字叫cnblogs,把Console程序copy一份,然后看看真的能够实现进程同步吗?
3: 小结
①: 当给Mutex取名的时候能够实现进程同步,不取名实现线程同步,详细细节参考MSDN:
②: Mutex封装了win32的同步机制,而Monitor是由framework封装,所以在线程同步角度来说,Monitor更加短小精悍,优于Mutex,要是实现进程
同步,Monitor也干不了,所以Mutex是首选
二:Interlocked
同样先向MSDN讨个说法,看看如何解释。 ”原子操作”是个亮点,我们知道“原子”是不可再分的,深一点的意思就是说站在程序员的角度来看是不需要手工干预的,也就是所谓的“无锁编程”。
实际应用中有时候我们可能只是对共享变量进行一些简单的操作,比如说“自增,自减,求和,赋值,比较"。
1:Increment
看看是不是达到了不可再分的自增效果,蛮有意思。 2:Decrement
这个就不用举例子了。
3:Add
发现MSDN解释的还是蛮详细的。
复制代码 1 static void Main(string[] args) 2 { 3 int i = 10; 4 5 Interlocked.Add(ref i, 20); 6 7 Console.WriteLine(i); //i=30 8 }
复制代码
4:Exchange
这个就是所谓的原子性赋值操作
5:CompareExchange
所谓的比较操作,还是看看经典的MSDN的说法
如果相等,返回第二个参数值: int i = 10; Interlocked.CompareExchange(ref i, 30, 10); Console.WriteLine(i); //i=30
如果不相等,则返回原始值: int i = 10; Interlocked.CompareExchange(ref i, 30, 100); Console.WriteLine(i); //i=10
~~~~~~~~~~~~~~~~~~
第四天 信号量
锁,互斥,信号量都可以实现线程同步,在framework里面主要有三种。
<1>:ManualResetEvent
<2>:AutoResetEvent
<3>: Semaphore
好,下面就具体看看这些玩意的使用。
一:ManualResetEvent
该对象有两种信号量状态True和False,好奇的我们肯定想知道True和False有什么区别,稍后的例子见分晓,有三个方法值得学习一下。
1:WaitOne
该方法用于阻塞线程,默认是无限期的阻塞,有时我们并不想这样,而是采取超时阻塞的方法,如果超时就放弃阻塞,这样也就避免了无限期
等待的尴尬。
2:Set
手动修改信号量为True,也就是恢复线程执行。
3:ReSet
手动修改信号量为False,暂停线程执行。
好了,下面举个例子说明一下。 <1> 信号量初始为False,WaitOne采用无限期阻塞,可以发现线程间可以进行交互。 <2> 信号量初始为True,WaitOne采用无限期阻塞,实验发现WaitOne其实并没有被阻塞。 <3>信号量初始为False,WaitOne采用超时2s,虽然主线程要等5s才能进行Set操作,但是WaitOne已经等不及提前执行了。
二:AutoResetEvent
在VS对象浏览器中,我们发现AutoResetEvent和ManualResetEvent都是继承于EventWaitHandle,所以基本功能是一样的,不过值得注意
的一个区别是WaitOne会改变信号量的值,比如说初始信号量为True,如果WaitOne超时信号量将自动变为False,而ManualResetEvent则不会。
三:Semaphore
这玩意是.net 4.0新增的,用于控制线程的访问数量,默认的构造函数为initialCount和maximumCount,表示默认设置的信号量个数和
最大信号量个数,其实说到底,里面是采用计数器来来分配信号量,当你WaitOne的时候,信号量自减,当Release的时候,信号量自增,然而
当信号量为0的时候,后续的线程就不能拿到WaitOne了,所以必须等待先前的线程通过Release来释放。
<1> initialCount=1,maximunCount=10,WaitOne采用无限期等待。 悲剧的发现t2线程不能执行,我们知道WaitOne相当于自减信号量,然而默认的信号量个数为1,所以t2想执行必须等待t1通过Release来释放。 我不是设置了maximunCount=10吗?为什么没有起到作用?是的,默认情况下是没有起到作用,必须要我们手动干预一下,
我们知道调用Release方法相当于自增一个信号量,然而Release有一个重载,可以指定自增到maximunCount个信号量,这里我就在主线程上
Release(10),看看效果。
<2> Semaphore命名,升级进程交互。
在VS对象浏览器中发现Semaphore是继承字WaitHandle,而WaitHandle封装了win32的一些同步机制,所以当我们给Semaphore命名的时候
就会在系统中可见,下面举个例子,把下面的代码copy一份,运行两个程序。 是的,我设置了信号量是3个,所以只能有三个线程持有WaitOne,后续的线程只能苦苦的等待。
~~~~~~~~~~~~~~~~~~~~~~~
第五天 线程池
如果你很懒,如果你的执行任务比较短,如果你不想对线程做更精细的控制,那么把这些繁琐的东西丢给线程池吧。
一:ThreadPool
好了,下面看看TheadPool下有哪些常用的方法。
1:GetMaxThreads,GetMinThreads
首先我们肯定好奇线程池到底给我们如何控制线程数,下面就具体的看一看。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int workerThreads; 6 7 int completePortsThreads; 8 9 ThreadPool.GetMaxThreads(out workerThreads, out completePortsThreads); 10 11 Console.WriteLine("线程池中最大的线程数{0},线程池中异步IO线程的最大数目{1}", workerThreads, completePortsThreads);//1000,1000 12 13 ThreadPool.GetMinThreads(out workerThreads, out completePortsThreads); 14 15 Console.WriteLine("线程池中最小的线程数{0},线程池中异步IO线程的最小数目{1}", workerThreads, completePortsThreads);//4,4 16 } 17 }
有的同学可能就要问,我可以将1023设置为10230吗?那么就会有10230个线程帮我做事多好啊,其实不然。
①:我先前的文章也说过,线程很多的话,线程调度就越频繁,可能就会出现某个任务执行的时间比线程调度花费的时间短很多的尴尬局面。
②:我们要知道一个线程默认占用1M的堆栈空间,如果10230个线程将会占用差不多10G的内存空间,我想普通的电脑立马罢工。
2:SetMaxTheads,SetMinThreads
当然,默认的线程设置只是一个参考,如果我们处于性能和实际情况确实需要修改也没关系,framework也给我们提供了现成的方法。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int workerThreads; 6 7 int completePortsThreads; 8 9 ThreadPool.SetMaxThreads(100, 50); 10 11 ThreadPool.SetMinThreads(20, 10); 12 13 ThreadPool.GetMaxThreads(out workerThreads, out completePortsThreads); 14 15 Console.WriteLine("线程池中最大的线程数{0},线程池中异步IO线程的最大数目{1}\n", workerThreads, completePortsThreads); 16 17 ThreadPool.GetMinThreads(out workerThreads, out completePortsThreads); 18 19 Console.WriteLine("线程池中最小的线程数{0},线程池中异步IO线程的最小数目{1}\n", workerThreads, completePortsThreads); 20 } 21 }
3: QueueUserWorkItem
需要容纳任务并执行的方法来了,该方法有一个WaitCallBack的委托,我们只需要把将要执行的任务丢给委托,CLR将会在线程池中调派空闲的
线程执行。
namespace ConsoleApplication3 { class Program { static void Main(string[] args) { ThreadPool.QueueUserWorkItem(Run1); Console.Read(); } static void Run1(object obj) { Console.WriteLine("我是线程{0},我是线程池中的线程吗? \n回答:{1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); } } }
可能我们也需要像普通的Thread一样带一些参数到工作线程中,QueueUserWorkItem的第二个重载版本解决了我们的问题。
ThreadPool.QueueUserWorkItem(Run1, "我是主线程");
4:RegisterWaitForSingleObject
我们知道,如果我们把要执行的任务丢给线程池后,相当于把自己的命运寄托在别人的手上。
①:我们再也不能控制线程的优先级了。
②:丢给线程池后,我们再也不能将要执行的任务取消了。
是的,给别人就要遵守别人的游戏规则,不过RegisterWaitForSingleObject提供了一些简单的线程间交互,因为该方法的第一个参数是
WaitHandle,在VS对象浏览器中,我们发现EventWaitHandle继承了WaitHandle,而ManualResetEvent和AutoResetEvent都继承于
EventWaitHandle,也就是说我们可以在RegisterWaitForSingleObject溶于信号量的概念。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 AutoResetEvent ar = new AutoResetEvent(false); 6 7 ThreadPool.RegisterWaitForSingleObject(ar, Run1, null, Timeout.Infinite, false); 8 9 Console.WriteLine("时间:{0} 工作线程请注意,您需要等待5s才能执行。\n", DateTime.Now); 10 11 //5s 12 Thread.Sleep(5000); 13 14 ar.Set(); 15 16 Console.WriteLine("时间:{0} 工作线程已执行。\n", DateTime.Now); 17 18 Console.Read(); 19 } 20 21 static void Run1(object obj, bool sign) 22 { 23 Console.WriteLine("当前时间:{0} 我是线程{1}\n", DateTime.Now, Thread.CurrentThread.ManagedThreadId); 24 } 25 }
我们知道在Threading下面有一个Timer计时器,当定期触发任务的时候都是由线程池提供并给予执行,那么这里我们溶于信号量的概念以后同样
可以实现计时器的功能
1 using System; 2 using System.Threading; 3 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 AutoResetEvent ar = new AutoResetEvent(false); 9 10 //参数2000:其实就是WaitOne(2000),采取超时机制 11 ThreadPool.RegisterWaitForSingleObject(ar, Run1, null, 2000, false); 12 13 Console.Read(); 14 } 15 16 static void Run1(object obj, bool sign) 17 { 18 Console.WriteLine("当前时间:{0} 我是线程{1}\n", DateTime.Now, Thread.CurrentThread.ManagedThreadId); 19 } 20 }
有时候,跑着跑着我们需要在某个时刻停止它,没关系,RegisterWaitForSingleObject返回一个RegisteredWaitHandle类,那么我们就通过
RegisteredWaitHandle来动态的控制,比如说停止计数器的运行。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 RegisteredWaitHandle handle = null; 6 7 AutoResetEvent ar = new AutoResetEvent(false); 8 9 handle = ThreadPool.RegisterWaitForSingleObject(ar, Run1, null, 2000, false); 10 11 //10s后停止 12 Thread.Sleep(10000); 13 14 handle.Unregister(ar); 15 16 Console.WriteLine("小子,主线程要干掉你了。"); 17 18 Console.Read(); 19 } 20 21 static void Run1(object obj, bool sign) 22 { 23 Console.WriteLine("当前时间:{0} 我是线程{1}", DateTime.Now, Thread.CurrentThread.ManagedThreadId); 24 } 25 }
结语:我只想把我所知道的,尽量简洁清楚地表达出来。