异步编程:线程概述及使用.doc_第1页
异步编程:线程概述及使用.doc_第2页
异步编程:线程概述及使用.doc_第3页
异步编程:线程概述及使用.doc_第4页
异步编程:线程概述及使用.doc_第5页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

异步编程:线程概述及使用 从此图中我们会发现 .NET 与C# 的每个版本发布都是有一个“主题”。即:C#1.0托管代码C#2.0泛型C#3.0LINQC#4.0动态语言C#5.0异步编程。现在我为最新版本的“异步编程”主题写系列分享,期待你的查看及点评。传送门:异步编程系列目录开始:异步编程:线程概述及使用示例:异步编程:线程概述及使用.rar 做交互式客户端应用程序,用户总希望程序能时刻响应UI操作;做高性能服务器开发,使用者总希望服务器能同时处理多个请求等等,这时我们可以使用多线程技术来保证UI线程可响应、提高服务器吞吐量、提升程序处理速度,设置任务优先级进行调度 多线程技术只是多个线程在操作系统分配的不同时间片里执行,并不是程序开12个线程12个线程都在同一个 “时间点”执行,同一“时间点”能执行多少线程由CPU决定,各个执行线程的衔接由操作系统进行调度。即,在线程数量超出用于处理它们的处理器数量的情况下,操作系统将定期为每个线程调度一个时间片来控制处理器,以此来模拟同时并发。 在认识线程前,我们需要了解下CPU,了解下进程。多核心CPU超线程CPU1. 多核心处理器(CPU)指在一块处理器(CPU)中含有多个处理单元,每一个处理单元它就相当于一个单核处理器(CPU)。因此,多核处理器的功能就相当于多台单核处理器电脑联机作战。2. 超线程处理器(CPU)指在一块CPU中,用虚拟的方法将一个物理核心模拟成多个核心(一般情况是一个单物理核心,模拟成二个核心,也即所谓的二线程。只有当线程数比物理核心数多才能叫超线程。如四核四线程并不是超线程,而四核八线程才能叫超线程)。3. 优缺点:1) 多核心是真正的物理核心,一块多核心的处理器(CPU),就相当于多块单核心的处理器(CPU)相互协作。因此,从理论上说,多核心比超线程具有更高运算能力。虽然多核心比超线程的运算速度快很多,但多核心也有一个明显的缺点,那就是多核心的使用效率比超线程处理器(CPU)低。因为,多核心在处理数据时,它们相互“合作”的并不是很完美,常常某个核心需要等待其他核心的计算数据,从而耽误时间,被迫怠工。另外,由于目前多核心都是采用共享缓存,这更使多核心的CPU运算速度减慢不少(因为:CPU读取Cache时是以行为单位读取的,如果两个硬件线程的两块不同内存位于同一Cache行里,那么当两个硬件线程同时在对各自的内存进行写操作时,将会造成两个硬件线程写同一Cache行的问题,它会引起竞争)。2) 超线程是用虚拟的方法将一个物理核心虚拟成多个核心,它能够最大限度地利用现有的核心资源,具有较高性价比。操作系统对多核处理器的支持主要体现在调度和中断上:1. 对任务的分配进行优化。使同一应用程序的任务尽量在同一个核上执行。2. 对任务的共享数据优化。由于多核处理器(Chip Multi-Processor,CMP)体系结构共享缓存(目前),可以考虑改变任务在内存中的数据分布,使任务在执行时尽量增加缓存的命中率。3. 对任务的负载均衡优化。当任务在调度时,出现了负载不均衡,考虑将较忙处理器中与其他任务最不相关的任务迁移,以达到数据的冲突最小。4. 支持抢先多任务处理的操作系统可以创建多个进程中的多个线程同时执行的效果。它通过以下方式实现这一点:在需要处理器时间的线程之间分割可用处理器时间,并轮流为每个线程分配处理器时间片。当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文。进程和线程1. 进程进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。2. 线程线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程。主执行线程终止了,进程也就随之终止。每个线程都维护异常处理程序、调度优先级和线程上下文。(线程上下文,当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文)3. 关系操作系统使用进程将它们正在执行的不同应用程序分开,.NET Framework 将操作系统进程进一步细分为System.AppDomain (应用程序域)的轻量托管子进程。线程是CPU的调度单元,是进程中的执行单位,一个进程中可以有多个线程同时执行代码。操作系统中,CPU的两种竞争策略操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。1.在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。2.所谓抢占式操作系统,就是说如果一个进程得到了CPU时间,除非它自己放弃使用CPU,否则将完全霸占CPU。因此可以看出,在抢占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出CPU。在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过CPU了),给他们算出一个总的优先级来。操作系统就会把CPU交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一次所有进程的总优先级,然后再挑一个优先级最高的把CPU控制权交给他。线程Thread类详解静态属性CurrentThread ,CurrentContext,CurrentPrincipal(负责人)静态方法AllocateDataSlot(),AllocateNamedDataSlot(),FreeNamedDataSlot(),GetNamedDataSlot(),GetData(),SetData(),BeginCriticalRegion()关键的,EndCriticalRegion(),BeginThreadAffinity(),EndThreadAffinity(), GetDomain(),GetDomainID(), ResetAbort(),Sleep(),SpinWait(),MemoryBarrier(),VolatileRead(),VolatileWrite(),Yield()实例属性Priority,ThreadState ,IsAlive,IsBackground,IsThreadPoolThread,ManagedThreadId,ApartmentState,CurrentCulture,CurrentUICulture,ExecutionContext,Name实例方法GetHashCode(),Start(),Abort(), Resume(),Suspend(),Join(),Interrupt(),GetApartmentState(),SetApartmentState(),TrySetApartmentState(),GetCompressedStack(),SetCompressedStack(),DisableComObjectEagerCleanup()1.常用属性1) CurrentContext 获取线程正在其中执行的当前上下文。主要用于线程内部存储数据。2) ExecutionContext 获取一个System.Threading.ExecutionContext对象,该对象包含有关当前线程的各种上下文的信息。主要用于线程间数据共享。3) IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池。4) ManagedThreadId 获取一个整数,表示此托管线程的唯一标识符。5) IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程。前台线程和后台线程并不等同于主线程和工作线程,如果所有的前台线程终止,那所有的后台线程也会被自动终止。应用程序必须运行完所有的前台线程才可以退出,所以,要特别注意前台线程的使用,会造成应用程序终止不了。默认情况下:通过Thread.Start()方法开启的线程都默认为前台线程。可以设置IsBackground属性将线程配置为后台线程。属于托管线程池的线程(即其 IsThreadPoolThread 属性为 true 的线程)是后台线程。从非托管代码进入托管执行环境的所有线程都被标记为后台线程。6) IsAlive 判断此线程是否还存活。经测试只有 Unstarted、Stopped 返回false;其他线程状态都返回true。2. 创建线程1234publicThread(ParameterizedThreadStart start);publicThread(ThreadStart start);publicThread(ParameterizedThreadStart start, intmaxStackSize);publicThread(ThreadStart start, intmaxStackSize);Thread包含使用ThreadStart或ParameterizedThreadStart委托做参数的构造函数,这些委托包装调用Start()时由新线程执行的方法。线程一旦启动,就不必保留对Thread对象的引用。线程会继续执行直到线程所调用委托执行完毕。1) 向线程传递数据(见示例)我们可以直接使用接收ParameterizedThreadStart参数Thread构造函数创建新线程,再通过Start(object parameter)传入参数并启动线程。由于Start方法接收任何对象,所以这并不是一种类型安全的实现。所以我们可以使用一种替代方案:将线程执行的方法和待传递数据封装在帮助器类中,使用无参的Start()启动线程。必要的时候需在帮助器类中使用同步基元对象避免线程共享数据的死锁和资源争用。2)使用回调方法检索数据(见示例)Thread构造函数接收的ThreadStart或ParameterizedThreadStart委托参数,这两个委托的声明都是返回void,即线程执行完后不会有数据返回(实际上主线程也不会等待Thread创建的新线程返回,否则创建新线程就无意义了)。那么如何在异步执行完时做出响应呢?使用回调方法。示例-关键代码(详见Simple4CallBackWithParam()):12345678910111213141516171819202122232425262728293031323334353637/ 包装异步方法的委托publicdelegatevoidExampleCallback(intlineCount);/ 帮助器类publicclassThreadWithStateprivatestringboilerplate;privateintvalue;privateExampleCallback callback;publicThreadWithState(stringtext, intnumber,ExampleCallback callbackDelegate)boilerplate = text;value = number;callback = callbackDelegate;publicvoidThreadProc()Console.WriteLine(boilerplate, value);/ 异步执行完时调用回调if(callback != null)callback(1);/ 异步调用/ 将需传递给异步执行方法数据及委托传递给帮助器类ThreadWithState tws = newThreadWithState(This report displays the number 0.,42,newExampleCallback(ResultCallback);Thread t = newThread(newThreadStart(tws.ThreadProc);t.Start();3. 调度线程 使用Thread.Priority属性获取或设置任何线程的优先级。优先级:Lowest BelowNormal Normal AboveNormaltryConsole.WriteLine(try内部,调用Abort前。);/ 等待其他线程调用该线程的Abort()Console.WriteLine(try内部,调用Abort后。);catch(ThreadAbortException abortEx)Console.WriteLine(catch:+ abortEx.GetType();Thread.ResetAbort();Console.WriteLine(catch:调用ResetAbort()。);catch(Exception ex)Console.WriteLine(catch:+ ex.GetType();finallyConsole.WriteLine(finally);/ 在finally中调用Thread.ResetAbort()不能取消线程的销毁/Thread.ResetAbort();/Console.WriteLine(调用ResetAbort()。);Console.WriteLine(try外面,调用Abort后(若再catch中调用了ResetAbort,则try块外面的代码依旧执行,即:线程没有终止)。);/ 其他线程调用该线程的Abort()t.Abort(); Console.WriteLine(主线程,调用Abort。);输出: 若在catch中没有调用Thread.ResetAbort(),哪么try块外面的代码就不会输出(详见输出截图的两处红线)。3) 阻塞线程调用Sleep()方法使当前线程放弃剩余时间片,立即挂起(阻塞)并且在指定时间内不被调度。Sleep(timeout),会有条件地将调用线程从当前处理器上移除,并且有可能将它从线程调度器的可运行队列中移除。这个条件取决于调用 Sleep 时timeout 参数。a) 当 timeout = 0, 即 Sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。一个时间片结束时,如果Windows决定再次调度同一个线程(而不是切换到另一个线程),那么Windows不会执行上下文切换。b) 当 timeout 0 时,如:Sleep(1),可能会引发线程上下文切换(如果发生线程切换):调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。调用Thread.Sleep(Timeout.Infinite)将使线程休眠,直到其他运行线程调用 Interrupt ()中断处于WaitSleepJoin线程状态的线程,或调用Abort()中止线程。应用实例:轮询休眠while (!proceed) Thread.Sleep (x); / 轮询休眠!4) 线程的挂起和唤醒可结合Suspend()与Resume()来挂起和唤醒线程,这两方法已过时。当对某线程调用Suspend()时,系统会让该线程执行到一个安全点,然后才实际挂起该线程(与Thread.Sleep()不同, Suspend()不会导致线程立即停止执行)。无论调用了多少次 Suspend(),调用Resume()均会使另一个线程脱离挂起状态,并导致该线程继续执行。注意:由于Suspend()和Resume()不依赖于受控制线程的协作,因此,它们极具侵犯性并且会导致严重的应用程序问题,如死锁(例如,如果您在安全权限评估期间挂起持有锁的线程,则AppDomain中的其他线程可能被阻止。如果您在线程正在执行类构造函数时挂起它,则AppDomain中试图使用该类的其他线程将被阻止。很容易发生死锁)。线程的安全点:是线程执行过程中可执行垃圾回收的一个点。垃圾回收器在执行垃圾回收时,运行库必须挂起除正在执行回收的线程以外的所有线程。每个线程在可以挂起之前都必须置于安全点。5) Join()在线程A中调用线程B的Join()实例方法。在继续执行标准的 COM 和 SendMessage 消息泵处理期间,线程A将被阻塞,直到线程B终止为止。6) Interrupt()中断处于WaitSleepJoin线程状态的线程。如果此线程当前未阻塞在等待、休眠或联接状态中,则下次开始阻塞时它将被中断并引发ThreadInterruptedException异常。线程应该捕获ThreadInterruptedException并执行任何适当的操作以继续运行。如果线程忽略该异常,则运行库将捕获该异常并停止该线程。如果调用线程的 Interrupt()方法时线程正在执行非托管代码,则运行库将其标记为ThreadState.SuspendRequested。待线程返回到托管代码时引发ThreadInterruptedException异常。6. SpinWait(int iterations)SpinWait实质上会将处理器置于十分紧密的自旋转中,当前线程一直占用CPU,其循环计数由 iterations 参数指定。SpinWait并不是一个阻止的方法:一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,并且也不会被其它的线程过早的中断(Interrupt)。SpinWait的作用是等待一个在极短时间(可能小于一微秒)内可准备好的可预期的资源,而避免调用Sleep()方法阻止线程而浪费CPU时间(上下文切换)。优点:避免线程上下文切换的耗时操作。缺点:CPU不能很好的调度CPU利用率。这种技术的优势只能在多处理器计算机上体现,对单一处理器的电脑,直到轮询的线程结束了它的时间片之前,别的资源无法获得cpu调度执行。7. 设置和获取线程的单元状态12345678910/ System.Threading.Thread 的单元状态。publicenumApartmentState/ System.Threading.Thread 将创建并进入一个单线程单元。STA = 0,/ System.Threading.Thread 将创建并进入一个多线程单元。MTA = 1,/ 尚未设置 System.Threading.Thread.ApartmentState 属性。Unknown = 2,1) 可使用ApartmentState获取和设置线程的单元状态,次属性已经过时2) SetApartmentState()+TrySetApartmentState()+GetApartentState()可以标记一个托管线程以指示它将承载一个单线程或多线程单元。如果未设置该状态,则GetApartmentState返回ApartmentState.Unknown。只有当线程处于ThreadState.Unstarted状态时(即线程还未调用Start()时)才可以设置该属性;一个线程只能设置一次。如果在启动线程之前未设置单元状态,则该线程被初始化为默认多线程单元 (MTA)。(终结器线程和由ThreadPool控制的所有线程都是 MTA)要将主应用程序线程的单元状态设置为ApartmentState.STA的唯一方法是将STAThreadAttribute属性应用到入口点方法。(eg:Main()方法)8.设置和检索线程数据(数据槽)线程使用托管线程本地存储区 (TLS,Thread-Local Storage)来存储线程特定的数据,托管 TLS 中的数据都是线程和应用程序域组合所独有的,其他任何线程(即使是子线程)都无法获取这些数据。公共语言运行库在创建每个进程时给它分配一个多槽数据存储区数组,数据槽包括两种类型:命名槽和未命名槽。1) 若要创建命名数据槽,使用 Thread.AllocateNamedDataSlot() 或 Thread.GetNamedDataSlot() 方法。命名数据槽数据必须使用Thread.FreeNamedDataSlot()来释放。在任何线程调用Thread.FreeNamedDataSlot()之后,后面任何线程使用相同名称调用Thread.GetNamedDataSlot()都将返回新槽。但是,任何仍具有以前通过调用Thread.GetNamedDataSlot()返回的System.LocalDataStoreSlot引用的线程可以继续使用旧槽。只有当调用Thread.FreeNamedDataSlot()之前获取的所有LocalDataStoreSlot已被释放并进行垃圾回收之后,与名称关联的槽才会被释放。2) 若要获取对某个现有命名槽的引用,将其名称传递给 Thread.GetNamedDataSlot() 方法。3) 若要创建未命名数据槽,使用 Thread.AllocateDataSlot() 方法。未命名数据槽数据在线程终止后释放。4) 对于命名槽和未命名槽,使用 Thread.SetData() 和 Thread.GetData() 方法设置和检索槽中的信息。命名槽可能很方便,因为您可以在需要它时通过将其名称传递给 GetNamedDataSlot 方法来检索该槽,而不是维护对未命名槽的引用。但是,如果另一个组件使用相同的名称来命名其线程相关的存储区,并且有一个线程同时执行来自您的组件和该组件的代码,则这两个组件可能会破坏彼此的数据。(本方案假定这两个组件在同一应用程序域内运行,并且它们并不用于共享相同数据。)为了获得更好的性能,请改用以 System.ThreadStaticAttribute特性标记的线程相关的静态字段。9.原子操作由于编译器,或者CPU的优化,可能导致程序执行的时候并不是真正的按照代码顺序执行。在多线程开发的时候可能会引起错误。在debug模式下,编译器不会做任何优化,而当Release后,编译器做了优化,此时就会出现问题。1) Thread.MemoryBarrier()按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 Thread.MemoryBarrier()调用之后的内存存取,再执行 Thread.MemoryBarrier() 调用之前的内存存取的方式。2) Thread.VolatileRead()+Thread.VolatileWrite()(内部使用MemoryBarrier()内存屏障)a) VolatileRead() 读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。b) VolatileWrite () 立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。3) 关键字Volatile:为了简化编程,C#编译器提供了volatile关键字。确保JIT编译器对易失字段都以易失读取或者易失写入的方法执行,不用显示调用Thread的VolatileRead()和VolatileWrite()。10. BeginCriticalRegion()+EndCriticalRegion() (Critical:关键性的)若要通知宿主代码进入关键区域,调用BeginCriticalRegion。当执行返回到非关键代码区域时,调用EndCriticalRegion。公共语言运行库 (CLR) 的宿主可在关键代码区域和非关键代码区域建立不同的失败策略。关键区域是指线程中止或未处理异常的影响可能不限于当前任务的区域。相反,非关键代码区域中的中止或失败只对出现错误的任务有影响。当关键区域中出现失败时,宿主可能决定卸载整个AppDomain,而不是冒险在可能不稳定的状态下继续执行。例如,假设有一个尝试在占有锁时分配内存的任务。如果内存分配失败,则中止当前任务并不足以确保AppDomain的稳定性,原因是域中可能存在其他等待同一个锁的任务。如果终止当前任务,则可能导致其他任务死锁。11. BeginThreadAffinity()+EndThreadAffinity() (Affinity:喜爱,密切关系)使用BeginThreadAffinity和EndThreadAffinity方法通知宿主代码块依赖于物理操作系统线程的标识。公共语言运行库的某些宿主提供其自己的线程管理。提供其自己的线程管理的宿主可以在任何时候将正在执行的任务从一个物理操作系统线程移至另一个物理操作系统线程。大多数任务不会受此切换影响。但是,某些任务具有【线程关联】 - 即它们依赖于物理操作系统线程的标识。这些任务在其执行“不应被切换的代码”时必须通知宿主。例如,如果应用程序调用系统 API 以获取具有【线程关联】的操作系统锁(如 Win32 CRITICAL_SECTION),则必须在获取该锁之前调用BeginThreadAffinity,并在释放该锁之后调用EndThreadAffinity。还必须在从WaitHandle继承的任何 .NET Framework 类型上发生阻止之前调用BeginThreadAffinity,因为这些类型依赖于操作系统对象。线程本地存储区和线程相关的静态字段可以使用托管线程本地存储区 (TLS,Thread-Local Storage) 和线程相关的静态字段来存储某一线程和应用程序域所独有的数据。a) 如果可以在编译时预料到确切需要,请使用线程相关的静态字段。b) 如果只能在运行时发现实际需要,请使用数据槽。为了获得更好的性能,请尽量改用以 System.ThreadStaticAttribute特性标记的线程相关的静态字段。无论是使用线程相关的静态字段还是使用数据槽,托管 TLS 中的数据都是线程和应用程序域组合所独有的。a) 在应用程序域内部,一个线程不能修改另一个线程中的数据,即使这两个线程使用同一个字段或槽时也不能。b) 当线程从多个应用程序域中访问同一个字段或槽时,会在每个应用程序域中维护一个单独的值。1) 线程相关的静态字段(编译时)如果您知道某类型的字段【总是某个线程和应用程序域组合】所独有的(即不是共享的),则使用ThreadStaticAttribute修饰静态字段(static)。需要注意的是,任何类构造函数代码都将在访问该字段的第一个上下文中的第一个线程上运行。在所有其他线程或上下文中,如果这些字段是引用类型,将被初始化为 null;如果这些字段是值类型,将被初始化为它们的默认值。因此,不要依赖于类构造函数来初始化线程相关的静态字段ThreadStatic。相反,应总是假定与线程相关的静态字段被初始化为 null 或它们的默认值。2) 数据槽(运行时)见上一小节(线程Thread类详解)第8点分析 示例:托管TSL中数据的唯一性(数据槽|线程相关静态字段)1234567891011121314151617181920212223242526272829303132/ / 数据槽 的使用示例/ privatestaticvoidTLS4DataSlot()LocalDataStoreSlot slot = Thread.AllocateNamedDataSlot(Name);Console.WriteLine(String.Format(ID为0的线程,命名为Name的数据槽,开始设置数据。, Thread.CurrentThread.ManagedThreadId);Thread.SetData(slot, 小丽);Console.WriteLine(String.Format(ID为0的线程,命名为Name的数据槽,数据是1。, Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot);Thread newThread = newThread() =LocalDataStoreSlot storeSlot = Thread.GetNamedDataSlot(Name);Console.WriteLine(String.Format(ID为0的线程,命名为Name的数据槽,在新线程为其设置数据 前 为1。, Thread.CurrentThread.ManagedThreadId, Thread.GetData(storeSlot);Console.WriteLine(String.Format(ID为0的线程,命名为Name的数据槽,开始设置数据。, Thread.CurrentThread.ManagedThreadId);Thread.SetData(storeSlot, 小红);Console.WriteLine(String.Format(ID为0的线程,命名为Name的数据槽,在新线程为其设置数据 后 为1。, Thread.CurrentThread.ManagedThreadId, Thread.GetData(storeSlot);/ 命名数据槽中分配的数据必须用

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论