关于多线程
为什么使用多线程
- 使用多线程的优点:
● 提高性能,改善界面响应和体验的效果
● 充分利用机器的资源
● 支持并发控制 - 使用多线程的缺点:
● 增加了代码的复杂度
● 需考虑数据的一致性
线程、进程的概念
进程
进程是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。线程
线程(Thread)是进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。区别:
通俗的比喻:如你去一个食堂吃饭,里面有A,B,C,D等一些窗口可以打饭菜或者米粉麻辣烫什么的,但是每一个窗口又有打这些菜的师傅,那么这些窗口就是进程,那个那些窗口里面打菜的师傅就是线程,这个食堂就是系统了,系统去分配这些进程。
参考链接:
- https://www.zhihu.com/question/25532384 (知乎上关于进程和线程的讨论)
- https://www.cnblogs.com/lmule/archive/2010/08/18/1802774.html (进程、线程的区别,这个我觉得讲的挺好)
STA和MTA的概念
STA:单线程套间,只允许存在一个线程。
MTA:多线程套间,允许存在多个线程。
==注:AO对象采用的是STA #191a19==
使用多线程
使用多线程需考虑的方面
- 使用多线程需考虑线程的安全性和可扩展性。
- 需了解AO中的COM对象模型和.NET中的多线程
- 多线程会增加程序的复杂度,并不总是会是你的程序效率提高,在很多情况下多线程会增加额外的系统开销和程序的复杂度。
使用多线程的条件
一条判断是否使用多线程的规则:如果你的任务可以被拆解为多个不同的独立的任务,那么这个任务就适合用多线程处理。
使用多线程需掌握的基础知识
AO对象线程模型
- 所有的AO对象都被标记为STA。STA限制只允许一个线程执行,但是COM不限制每个进程中STA的数量。在STA中一个对象一次只能接受和处理一个方法的调用,,并且它所接收到的每个方法调用都将在同一线程中到达。
- AO组件对于线程是安全的,在AO中使用多线程需要考虑线程的独立性,应避免跨线程之间的交流,一个线程中的所有AO对象应用只能和同一个线程的对象通信。
- AO中的单实例只是线程中的单实例,并不是进程中的单实例。(如果不了解单实例,请百度单实例模式),如果在一个线程中创建单例对象,然后从其他线程访问该线程, 在进程中托管多个单例的资源开销超过了停止跨线程通信的性能收益。
AO中使用多线程的场景
在后台进程处理长时间的操作
可以使用后来线程来处理相对较长时间的操作,避免前台UI没有响应,提升用户体检。使用后台线程处理长时间操作的时候,需考虑一下几点:
- AO组件不支持在线程间共享,单实例对象只是线程中单实例而不是进程中单实例。(注:AO中工作空间工厂类就是单实例对象)
- 线程中所有信息的传递都必须是窗体支持的简单的系统类型。
- 在一些情况下你可能必须从主线程把AO对象传递到其他工作的线程,那么请先将其序列化成String对象传到目标线程,然后再把string对象反序列化成AO对象。(这块使用的AO类有XmlSerializerClass,具体请参考AE的帮助文档)
- 当使用后台线程的时候,你可以将当前执行任务的进度反馈给UI,这块你可以参考AE中的帮助(Updating the UI from a background )
// Generate the thread that populates the locations table.
Thread t = new Thread(new ThreadStart(PopulateLocationsTableProc));
// Mark the thread as a single threaded apartment (STA) to efficiently run ArcObjects.
t.SetApartmentState(ApartmentState.STA);
// Start the thread.
t.Start();
/// <summary>
/// Load the information from the MajorCities feature class to the locations table.
/// </summary>
private void PopulateLocationsTableProc()
{
//Get the ArcGIS path from the registry.
RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcGIS");
string path = Convert.ToString(key.GetValue("InstallDir"));
//Open the feature class. The workspace factory must be instantiated
//since it is a singleton per thread.
IWorkspaceFactory wf = new ShapefileWorkspaceFactoryClass()as IWorkspaceFactory;
IWorkspace ws = wf.OpenFromFile(System.IO.Path.Combine(path, @
"DeveloperKit10.0\Samples\Data\USZipCodeData"), 0);
IFeatureWorkspace fw = ws as IFeatureWorkspace;
IFeatureClass featureClass = fw.OpenFeatureClass(m_sShapefileName);
//Map the name and ZIP Code fields.
int zipIndex = featureClass.FindField("ZIP");
int nameIndex = featureClass.FindField("NAME");
string cityName;
long zip;
try
{
//Iterate through the features and add the information to the table.
IFeatureCursor fCursor = null;
fCursor = featureClass.Search(null, true);
IFeature feature = fCursor.NextFeature();
int index = 0;
while (null != feature)
{
object obj = feature.get_Value(nameIndex);
if (obj == null)
continue;
cityName = Convert.ToString(obj);
obj = feature.get_Value(zipIndex);
if (obj == null)
continue;
zip = long.Parse(Convert.ToString(obj));
if (zip <= 0)
continue;
//Add the current location to the locations table.
//m_locations is a DataTable that contains the cities and ZIP Codes.
//It is defined in the full code before this excerpt starts.
DataRow r = m_locations.Rows.Find(zip);
if (null == r)
{
r = m_locations.NewRow();
r[1] = zip;
r[2] = cityName;
lock(m_locations)
{
m_locations.Rows.Add(r);
}
}
feature = fCursor.NextFeature();
index++;
}
//Release the feature cursor.
Marshal.ReleaseComObject(fCursor);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
}
实现独立AO应用程序
如果引用程序的主线程没有设置是STA还是MTA,默认一MTA初始化。通过设置线程属性代替设置Threading.ApartmentState ,你可以不再设置线程的属性来标记主程序的线程为STA.
namespace ConsoleApplication1
{
class Program
{
[STAThread] static void Main(string[] args)
{
// ...
}
}
}
如果您使用项目向导创建一个Windows应用程序,它会自动将STAThread放在主函数上。
使用可管理的线程池(ThreadPool)和后台工作线程
线程池线程是后台线程。通过向应用程序提供由系统管理的工作线程池,线程池使您能够更有效地使用线程。使用线程池为每个任务创建新线程的优点是,线程创建和销毁所花费的开销可以被忽略,这将导致更好的性能和更好的系统稳定性。
==然而因为所有的ThreadPool线程都位于多线程的公寓(MTA)中,因此不应该用于运行ArcObjects,即STA。 #0f0e0e==
为了解决这个问题,你有一些选择:
- 一种是实现一个专用的ArcObjects线程,它被标记为STAThread,并将来自MTA线程的每一个调用委托给这个专用的ArcObjects线程。
- 另一种解决方案是使用定制的STA线程池的实现,比如一个被标记为STAThread的线程数组来运行ArcObjects。
/// <summary>
/// Class used to pass the task information to the working thread.
/// </summary>
public class TaskInfo
{
... public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
m_bandID = BandID;
m_doneEvent = doneEvent;
}
...
}
... public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
...
// Run the subset thread that will spin off separate subset tasks.
//By default, this thread will run as MTA.
// This is needed to use WaitHandle.WaitAll(). The call must be made
// from MTA.
Thread t = new Thread(new ThreadStart(SubsetProc));
t.Start();
}
/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
...
//Create a dedicated thread for each band of the input raster.
//Create the subset threads.
Thread[] threadTask = new Thread[m_intBandCount];
//Each thread will subset a different raster band.
//All information required for subsetting the raster band will be
//passed to the task by the user-defined TaskInfo class.
for (int i = 0; i < m_intBandCount; i++)
{
TaskInfo ti = new TaskInfo(i, doneEvents[i]);
...
// Assign the subsetting thread for the rasterband.
threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
// Note the STA that is required to run ArcObjects.
threadTask[i].SetApartmentState(ApartmentState.STA);
threadTask[i].Name = "Subset_" + (i + 1).ToString();
// Start the task and pass the task information.
threadTask[i].Start((object)ti);
}
...
// Wait for all threads to complete their task…
WaitHandle.WaitAll(doneEvents);
...
}
/// <summary>
/// Subset task method.
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
// The state object must be cast to the correct type, because the
// signature of the WaitForTimerCallback delegate specifies type
// Object.
TaskInfo ti = (TaskInfo)state;
//Deserialize the workspace connection properties.
IXMLSerializer xmlSerializer = new XMLSerializerClass();
object obj = xmlSerializer.LoadFromString(ti.InputRasterWSConnectionProps, null,
null);
IPropertySet workspaceProperties = (IPropertySet)obj;
...
}
同步运行线程的同步执行
在许多情况下,您必须同步执行并发运行的线程。通常情况下,您必须等待一个或多个线程来完成它们的任务,在满足某个条件时发出一个信号让等待的线程继续它的任务,可以通过更改一个线程优先级或者给出一些其他的指示,用来测试一个给定的线程是否还在线并运行,
在.NET中有几种方法来管理运行线程的执行,用于帮助线程管理的主要类如下:
- System.Threading.Thread—用于创建和控制线程、更改优先级和获得状态。
- System.Threading.WaitHandle—定义一个信号机制来指示对共享资源的独占访问,允许您限制对一个代码块的访问。
- 调用WaitHandle.WaitAll()方法必须从一个MTA线程完成。要运行多个同步任务,首先必须运行一个工作线程,而这个线程又将运行多个线程。
-System.Threading.Monitor—类似于System.Threading,提供一种机制来同步对对象的访问。
-System.Threading.AutoResetEvent and System.Threading.ManualResetEvent—用于通知正在发生事件的等待线程,允许线程通过发送信号进行通信。
/// <summary>
/// Class used to pass the task information to the working thread.
/// </summary>
public class TaskInfo
{
//Signal the main thread that the thread has finished its task.
private ManualResetEvent m_doneEvent;
... public TaskInfo(int BandID, ManualResetEvent doneEvent)
{
m_bandID = BandID;
m_doneEvent = doneEvent;
}
... public ManualResetEvent DoneEvent
{
get
{
return m_doneEvent;
}
set
{
m_doneEvent = value;
}
}
}
//Block access to the shared resource (raster dataset).
private static AutoResetEvent m_autoEvent = new AutoResetEvent(false);
... public override void OnMouseDown(int Button, int Shift, int X, int Y)
{
...
// Run the subset thread that will spin off separate subset tasks.
// By default, this thread will run as MTA.
// This is needed because to use WaitHandle.WaitAll(), the call must be made
// from MTA.
}
/// <summary>
/// Main subset method.
/// </summary>
private void SubsetProc()
{
...
//Create ManualResetEvent to notify the main threads that
//all ThreadPool threads are done with their tasks.
ManualResetEvent[] doneEvents = new ManualResetEvent[m_intBandCount];
//Create a ThreadPool task for each band of the input raster.
//Each task will subset a different raster band.
//All information required for subsetting the raster band will be
//passed to the ThreadPool task by the user-defined TaskInfo class.
for (int i = 0; i < m_intBandCount; i++)
{
//Create the ManualResetEvent field for the task to
// signal the waiting thread that the task had been completed.
doneEvents[i] = new ManualResetEvent(false);
TaskInfo ti = new TaskInfo(i, doneEvents[i]);
...
// Assign the subsetting thread for the rasterband.
threadTask[i] = new Thread(new ParameterizedThreadStart(SubsetRasterBand));
// Note the STA, which is required to run ArcObjects.
threadTask[i].SetApartmentState(ApartmentState.STA);
threadTask[i].Name = "Subset_" + (i + 1).ToString();
// Start the task and pass the task information.
threadTask[i].Start((object)ti);
}
//Set the state of the event to signaled to allow one or more of the
//waiting threads to proceed.
m_autoEvent.Set();
// Wait for all threads to complete their task…
WaitHandle.WaitAll(doneEvents);
...
}
/// <summary>
/// Subset task method.
/// </summary>
/// <param name="state"></param>
private void SubsetRasterBand(object state)
{
// The state object must be cast to the correct type, because the signature
// of the WaitOrTimerCallback delegate specifies type Object.
TaskInfo ti = (TaskInfo)state;
...
//Lock all other threads to get exclusive access.
m_autoEvent.WaitOne();
//Insert code containing your threading logic here.
//Signal the next available thread to get write access.
m_autoEvent.Set();
//Signal the main thread that the thread has finished its task.
ti.DoneEvent.Set();
}
在多线程中共享一个管理的类型
很多时候一个对象对于多线程读取是安全的,但对写入是不安全的。当你的创建的对象被多个线程访问的时候,你应该使用一个独占锁来确保线程安全。你可以再MSDN中查找你使用的.NET对象的线程安全信息。
private DataTable m_locations = null;
...
DataRow rec = m_locations.NewRow();
rec["ZIPCODE"] = zipCode; //ZIP Code.
rec["CITYNAME"] = cityName; //City name.
//Lock the table and add the new record.
lock(m_locations)
{
m_locations.Rows.Add(rec);
}
用后台线程更新UI
当你使用后台线程来处理较长时间的操作时,你想向用户告知进度、状态、错误等其他线程运行的信息,你可以通过更新控件的UI来实现。在Windows中,窗体控件被绑定到一个特定的线程(通常是主线程),而主线程不是线程安全的。
因此,您必须将对UI控制的任何调用委托给控制所属的线程。委派是通过调用控件的Invoke方法,它在拥有控件底层窗口句柄的线程上执行委托。要验证调用方是否必须调用invoke方法,请使用控件的InvokeRequired属性。您必须确保控件的句柄是在试图调用控件的Invoke方法或Control.InvokeRequired属性之前创建的。
In the user form, declare a delegate through which you will pass the information to the control:
[C#]
public class WeatherItemSelectionDlg: System.Windows.Forms.Form
{
private delegate void AddListItmCallback(string item);
...
In the user form, set the method to update the UI control. Notice the call to Invoke. The method must have the same signature as the previously declared delegate:
[C#]
//Make thread-safe calls to Windows Forms Controls.
private void AddListItemString(string item)
{
// InvokeRequired compares the thread ID of the
//calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.lstWeatherItemNames.InvokeRequired)
{
//Call itself on the main thread.
AddListItmCallback d = new AddListItmCallback(AddListItemString);
this.Invoke(d, new object[]
{
item
}
);
}
else
{
//Guaranteed to run on the main UI thread.
this.lstWeatherItemNames.Items.Add(item);
}
}
On the background thread, implement the method that will use the delegate and pass over the message to be displayed on the user form:
[C#]
private void PopulateSubListProc()
{
//Insert code containing your threading logic here.
//Add the item to the list box.
frm.AddListItemString(data needed to update the UI control, string in this case )
;
}
Write the call that starts the background thread itself, passing in the method written in step 3:
[C#]
//Generate a thread to populate the list with items that match the selection criteria.
Thread t = new Thread(new ThreadStart(PopulateSubListProc));
t.Start();
用主线程以外的线程调用AO对象
在许多多线程应用程序中,您需要对来自不同运行线程的ArcObjects进行调用。例如,您可能有一个后台线程用来获取Web服务的响应,相应地用来添加新的地图到地图显示中,改变地图的显示范围或者运行一个GP工具来执行一些分析操作。
一个非常常见的情况是,从一个计时器事件处理程序方法调用ArcObjects。在ThreadPool任务(不是主线程的线程)上增加一个计时器的事件。然而,它需要使用ArcObjects,这似乎需要交叉公寓的调用。但是,可以通过将ArcObjects组件看作是一个UI控件,并使用Invoke将调用委托给主线程,在那里创建了ArcObjects组件,从而可以避免这种情况。因此,没有交叉公寓的调用发生。
ISynchronizeInvoke接口包括Invoke、BeginInvoke和EndInvoke方法。实现这些方法可能是一项艰巨的任务。相反,让您的类直接继承system.windows.forms。控制或拥有一个可以继承控制的助手类。两种选择都提供了一种简单而有效的方法来调用方法。
下面的代码示例使用一个用户定义的InvokeHelper类来调用一个定时器的经过事件处理程序,以重新映射映射的可见边界,并设置映射的旋转。
[C#]
/// <summary>
/// A helper method used to delegate calls to the main thread.
/// </summary>
public sealed class InvokeHelper: Control
{
//Delegate used to pass the invoked method to the main thread.
public delegate void MessageHandler(NavigationData navigationData);
//Class members.
private IActiveView m_activeView;
private IPoint m_point = null;
/// <summary>
/// Class constructor.
/// </summary>
/// <param name="activeView"></param>
public InvokeHelper(IActiveView activeView)
{
//Make sure that the control was created and that it has a valid handle.
this.CreateHandle();
this.CreateControl();
//Get the active view.
m_activeView = activeView;
}
/// <summary>
/// Delegate the required method onto the main thread.
/// </summary>
/// <param name="navigationData"></param>
public void InvokeMethod(NavigationData navigationData)
{
// Invoke HandleMessage through its delegate.
if (!this.IsDisposed && this.IsHandleCreated)
Invoke(new MessageHandler(CenterMap), new object[]
{
navigationData
}
);
}
/// <summary>
/// The method that gets executed by the delegate.
/// </summary>
/// <param name="navigationData"></param>
public void CenterMap(NavigationData navigationData)
{
//Get the current map visible extent.
IEnvelope envelope =
m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds;
if (null == m_point)
{
m_point = new PointClass();
}
//Set the new map center coordinate.
m_point.PutCoords(navigationData.X, navigationData.Y);
//Center the map around the new coordinate.
envelope.CenterAt(m_point);
m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds = envelope;
//Rotate the map to the new rotation angle.
m_activeView.ScreenDisplay.DisplayTransformation.Rotation =
navigationData.Azimuth;
}
/// <summary>
/// Control initialization.
/// </summary>
private void InitializeComponent(){}
/// <summary>
/// A user-defined data structure used to pass information to the Invoke method.
/// </summary>
public struct NavigationData
{
public double X;
public double Y;
public double Azimuth;
/// <summary>
/// Struct constructor.
/// </summary>
/// <param name="x">map x coordinate</param>
/// <param name="y">map x coordinate</param>
/// <param name="azimuth">the new map azimuth</param>
public NavigationData(double x, double y, double azimuth)
{
X = x;
Y = y;
Azimuth = azimuth;
}
/// <summary>
/// This command triggers the tracking functionality.
/// </summary>
public sealed class TrackObject: BaseCommand
{
//Class members.
private IHookHelper m_hookHelper = null;
... private InvokeHelper m_invokeHelper = null;
private System.Timers.Timer m_timer = null;
...
/// <summary>
/// Occurs when this command is created.
/// </summary>
/// <param name="hook">Instance of the application</param>
public override void OnCreate(object hook)
{
...
//Instantiate the timer.
m_timer = new System.Timers.Timer(60);
m_timer.Enabled = false;
//Set the timer's elapsed event handler.
m_timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
}
/// <summary>
/// Occurs when this command is clicked.
/// </summary>
public override void OnClick()
{
//Create the InvokeHelper class.
if (null == m_invokeHelper)
m_invokeHelper = new InvokeHelper(m_hookHelper.ActiveView);
...
//Start the timer.
if (!m_bIsRunning)
m_timer.Enabled = true;
else
m_timer.Enabled = false;
...
}
/// <summary>
/// Timer elapsed event handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
...
//Create the navigation data structure.
NavigationData navigationData = new NavigationData(currentPoint.X,
currentPoint.Y, azimuth);
//Update the map extent and rotation.
m_invokeHelper.InvokeMethod(navigationData);
...
}
多线程与GP操作
在异步或多线程应用程序中使用地理处理操作,使用以下选项之一:
在ArcGIS Server9.2及以上的版本,使用地理处理服务。该操作使得桌面端可以以异步模式执行GP操作,并且可以异步执行多线程工具。
在ArcGIS10及以上版本,可以使用Geoprocessor.ExecuteAsync方法。你可以异步执行工具。这意味着当一个程序在后台执行时,ArcGIS桌面程序或控件(MapControl、PageLayoutControl、GlobeControl、SceneControl)依旧可以保持响应。换言之,当执行导入数据集的时候 数据可以查看和查询。更多描述请查看帮助:Running a geoprocessing tool using background geoprocessing.