一个STA和一个MTA线程单元最大的不同是再同一个线程单元中可以有多个线程运行并可以使用所有共享数据。如图2所示。
图 2
由于MTA线程模型支持多线程并发执行,所以处理多线程间全局数据的同步问题就变成了调用方的事情了。关于同步问题我们已经在上一章描述过。
确定线程模型
可以使用Thread 类中的ApartmentState 属性来设置一个线程的线程模型。ApartmentState 枚举定义了.NET 支持的线程模型类型。
正如我们之前学到的,你应该仅当你访问一个STA线程的COM组件时你才应该把线程标记为STA。否则,你的线程模型默认都是MTA。
设计多线程应用程序
一个多线程程序有多于两个线程,它可以通过并发实现重要性能提升,不管是否并发执行线程。线程并发执行意味着多于两个线程在同一时间执行。并发是指>=2个线程在>=2个处理器中同步执行。
在这部分,我们将探讨设计多线程应用时的考量和问题。在你开发应用程序之前,你应该问问你自己以下问题:
1. 把程序分成小块来运行在不同线程上是否可行?
2. 如果可以将一个线程分成小块,那么该如何进行拆分呢?有没有标准呢?
3. 主线程和工作线程的关系是什么?这定义了应用程序中的任务之间的关系。
你可以通过查看程序来得到第一个问题的答案。例如,你的程序需要频繁的I/O操作,比如读取一个XML文件或者查询一个数据库,又或者执行很多CPU敏感型处理任务,比如加密和解密数据以及哈希运算等等。如果是这样,这些操作可能会阻塞你程序的主线程。
如果你已经确定了你程序中的一部分可以成为独立线程的执行体,那么接下来你应该问问自己以下问题:
1. 每个确定的任务都要使用单独的全局资源吗?
例如,如果你为程序确定了两个潜在的线程且它们都要访问同一个全局资源的话,比如一个全局变量或者一个DataSet对象,然后如果两个线程都尝试在同一时间访问全局资源的话,可能会导致数据不一致或者数据崩溃,正如之前章节描述的那样。防止这类问题发生的唯一方法就是在全局资源上使用锁,这样就可以让多线程互斥访问。如果两个任务都要使用同一个全局资源,那么最好不要把它们拆分成两个任务。对于一些资源来说,你可以使用Monitor类阻止线程锁住。这也在第三章介绍过。
2. 阻塞一个线程需要多长时间?
使用独立全局资源创建应用程序并不总是可能的。例如,假设程序中有两个任务依赖同一个全局DataSet对象。如果第一个任务需要花费很长时间来填充DataSet对象,那么你应该锁住DataSet对象以防止并发问题。这里是第一个任务的伪代码表示:
1) 打开数据库连接
2) 锁住全局DataSet 对象
3) 执行查询
4) 使用从数据库查询到的50,000 行数据填充DataSet对象
5) 解锁DataSet 对象
在这种情况下,第二个任务需要在它能访问DataSet对象之前等待很长时间,仅当第一个任务执行完并释放锁后才可以访问。这是一个并发问题而且它将可能导致程序的并发机制失效。有一个更好的方式来处理这个问题:
1) 打开数据库连接
2) 执行查询
3) 使用从数据库查询到的50,000 行数据填充DataSet对象
4) 锁住全局DataSet 对象
5) 设置本地数据库为全局数据集 (DSGlobal = DSLocal)
6) 解锁DataSet 对象
在这种情况下,除非我们需要更新DataSet对象否则我们不锁住它,这样就可以减小锁在全局对象维持的时间。
3. 执行一个任务依赖其他任务吗?
例如,你定义的任务可能是查询数据库并在一个DataGrid 控件中显示数据。你可以将查询数据库作为第一个任务,将在DataGrid 中显示数据作为第二个任务。第二个任务会在第一个任务完成后执行。因此,将查询数据与显示数据拆分开来并不是一个好的选项。一个折中的方法是让第一个任务在完成时触发一个事件,当事件发生后生成一个新线程。对应的,你可以使用一个定时器来通过一个公有属性检查是否完成,并继续执行线程当它完成后。
下一篇介绍线程及线程间的关系…