多线程编程的原则及要点
同步机制应该遵循的规则

同步机制应该遵循的规则同步机制是指在多线程编程中,为了保证各个线程之间的有序执行和数据的一致性,需要遵循一定的规则。
本文将介绍同步机制应遵循的一些规则,包括互斥访问、临界区、互斥量、条件变量等。
一、互斥访问在多线程环境下,多个线程可能同时访问共享资源,为了避免数据竞争和不确定的结果,需要使用互斥访问机制。
互斥访问机制要求同一时间只能有一个线程访问共享资源,其他线程需要等待。
二、临界区临界区是指一段代码,同时只能有一个线程执行。
在进入临界区之前,需要先获取锁,执行完临界区代码后释放锁。
这样可以保证在临界区内的代码只能由一个线程执行,从而避免数据竞争和不一致的结果。
三、互斥量互斥量是一种同步机制,通过对共享资源加锁和解锁来实现线程的互斥访问。
在访问共享资源前,线程需要先获取互斥量的锁,如果锁被其他线程持有,则当前线程会被阻塞,直到锁被释放。
使用互斥量可以保证同一时间只有一个线程访问共享资源,从而确保数据的一致性。
四、条件变量条件变量是一种同步机制,用于线程之间的通信。
条件变量可以使线程在满足特定条件之前等待,一旦条件满足,线程将被唤醒继续执行。
条件变量通常与互斥量一起使用,以实现线程间的互斥和同步。
五、信号量信号量是一种同步机制,用于控制多个线程对共享资源的访问。
信号量有一个计数器,线程在访问资源前需要先等待信号量的计数器大于零,然后将计数器减一,表示占用一个资源。
当线程使用完资源后,需要释放信号量,使计数器加一,其他线程可以继续访问资源。
六、死锁避免在多线程编程中,死锁是一种常见的问题。
死锁指的是多个线程相互等待对方释放资源的情况,导致程序无法继续执行。
为了避免死锁,需要遵循以下原则:1.避免嵌套锁:在一个线程持有锁时,不再请求其他锁。
2.按相同的顺序获取锁:多个线程获取锁的顺序应该一致,避免出现循环等待的情况。
3.设置超时时间:在获取锁时设置超时时间,避免长时间等待导致死锁。
七、性能优化在使用同步机制时,还需要考虑性能优化的问题。
java多线程编程实验总结与体会

java多线程编程实验总结与体会[Java多线程编程实验总结与体会]本次实验锻炼了我的Java多线程编程能力,让我更深入地了解了多线程编程的实现原理和技巧,同时也让我意识到在多线程环境下需要考虑的问题和注意事项。
下面我将结合具体实验内容,分享我在实践中的体会和思考。
1. 实验环境搭建在进行本次实验之前,我首先进行了实验环境的搭建。
我选择了Java SE Development Kit 8和Eclipse作为开发工具,同时也安装了JDK8的API 文档作为参考资料。
在搭建环境的过程中,我认识到Java的生态系统非常强大,附带的工具和资源也非常充足,这为我们开发和调试带来了很大的便利。
2. 多线程原理在研究多线程编程之前,我们需要对Java语言中的线程概念有一个清晰的认识。
线程是指操作系统能够进行运算调度的最小单位,是执行线程代码的路径。
在Java中,线程是一种轻量级的进程,可以同时运行多个线程。
每个线程都有自己的堆栈和局部变量,线程之间可以共享全局变量。
Java的多线程编程是通过Thread类和Runnable接口来实现的。
在实践中,我发现多线程编程最基本的原理是线程的并发执行。
多个线程可以在同一时间内执行不同的代码,提高CPU利用率,加快程序运行速度。
但是,在多线程并发执行的过程中,我们需要注意线程之间的同步问题,避免出现数据竞争和并发安全等问题。
3. 多线程的实现在Java中,我们可以通过继承Thread类或者实现Runnable接口来创建线程。
对于简单的线程,我们可以采用继承Thread类的方式来实现。
例如,在实验一中,我们在Main线程内创建了两个子线程,分别用来执行奇数和偶数的累加操作。
我们可以分别定义两个类OddThread和EvenThread继承Thread类,分别实现run()方法,用来执行具体的奇数和偶数累加操作。
然后在Main线程内创建OddThread和EvenThread 对象,并调用start()方法来启动两个线程,并等待两个线程完成操作。
多线程注意事项范文

多线程注意事项范文多线程是指在一个程序中同时运行多个线程,每个线程独立执行不同的任务。
相比单线程,多线程可以提高程序的执行效率和资源利用率。
然而,多线程编程也存在一些注意事项,下面将详细介绍:1.线程安全问题:多个线程同时访问共享的数据,可能引发竞态条件或死锁等问题。
为避免这些问题,可以采用锁、信号量、互斥量等机制来保护共享数据的访问。
2.同步问题:当多个线程并发执行时,可能会出现对共享资源的不同步访问。
为解决这个问题,可以使用线程同步机制,如条件变量、读写锁等,来保证多个线程按照特定的顺序访问共享资源。
3.上下文切换开销:切换线程间的上下文需要保存和恢复线程的状态信息,这会带来一定的开销。
因此,在多线程编程时,应避免频繁的线程切换,合理调度线程的执行顺序,以降低上下文切换的开销。
4.线程间通信问题:多个线程之间可能需要进行通信,传递数据或控制信息。
为确保线程间的正确通信,可以使用消息队列、管道、共享内存等机制来实现线程间的数据交换。
5.线程优先级问题:多线程环境中,线程的调度是由操作系统决定的,因此无法确定线程的执行顺序。
这就导致线程的执行结果可能与预期不符。
为避免这个问题,可以设置线程的优先级,提高重要线程的执行优先级。
6.死锁问题:多个线程之间的循环等待资源的释放,导致所有线程都无法继续执行,称为死锁。
为避免死锁问题,应避免循环等待的发生,可以按照特定的顺序申请和释放资源。
7.线程创建和销毁开销:创建和销毁线程需要消耗系统资源,因此应合理控制线程的数量,避免频繁的线程创建和销毁操作。
8.线程安全方法和非线程安全方法:在多线程环境中,一些方法可能是线程安全的,即多个线程同时调用不会引发竞态条件等问题。
而一些方法可能是非线程安全的,多个线程同时调用可能导致不确定的结果。
在多线程编程时,应注意选择线程安全的方法。
9.CPU资源的合理利用:多线程程序可能会占用过多的CPU资源,导致其他程序无法正常工作。
多线程处理:提升程序并发和响应能力的技巧

多线程处理:提升程序并发和响应能力的技巧多线程处理是一种提升程序并发和响应能力的重要技巧。
随着计算机的发展和处理器的不断升级,多核处理器成为主流,计算机拥有更多的处理单元,但是单个线程只能在一个处理单元上执行。
为了充分利用计算机资源,我们需要使用多线程技术。
多线程处理指的是在一个程序中同时运行多个线程,每个线程独立执行自己的任务。
通过多线程处理,可以实现同时处理多个任务,提升程序的并发能力和响应能力。
下面我将介绍一些多线程处理的技巧,以帮助提升程序的并发和响应能力。
1.合理划分任务:在设计多线程程序时,首先需要合理划分任务。
将一个大任务划分成多个小任务,并将这些小任务分配给不同的线程。
这样可以充分利用多核处理器的计算能力,并提高程序的并发能力。
2.线程池:线程池是一种管理和复用线程的机制。
通过线程池可以避免频繁地创建和销毁线程,提高线程的利用率。
线程池可以预先创建一定数量的线程,并将任务分配给空闲的线程来处理,当任务完成后,线程可以继续处理其他任务,而不需要销毁重新创建。
3.并发容器:并发容器是一种在多线程环境下安全访问的数据结构。
Java中提供了多种并发容器,如ConcurrentHashMap、ConcurrentLinkedQueue 等,可以在多线程环境下高效地操作数据。
使用并发容器可以避免多线程竞争导致的数据不一致和线程安全问题。
4.锁和同步机制:多线程是在共享的资源上进行操作,因此需要考虑线程安全问题。
在多线程程序中,使用锁和同步机制可以保证多线程之间的顺序和互斥。
Java中提供了synchronized关键字和Lock接口,可以实现线程的同步与互斥。
5.避免死锁:死锁是多线程编程中常见的问题,指的是多个线程因互相等待对方释放资源而陷入无限等待的状态。
为了避免死锁,需要合理设计线程之间的依赖关系和资源的请求顺序。
另外,还可以使用线程池和资源分配策略来减少死锁的发生。
6.异步编程:异步编程是一种非阻塞的编程方式,可以提高程序的响应能力。
VB_.NET多线程编程的详细说明(完整版)

VB .NET多线程编程的详细说明介绍传统的Visual Basic开发人员已经建立了同步应用程序,在这些程序中事务按顺序执行。
尽管由于多个事务多多少少地同时运行使多线程应用程序效率更高,但是使用先前版本的Visual Basic很难建立这类程序。
多线程程序是可行的,因为操作系统是多任务的,它有模拟同一时刻运行多个应用程序的能力。
尽管多数个人计算机只有一个处理器,但是现在的操作系统还是通过在多个执行代码片断之间划分处理器时间提供了多任务。
线程可能是整个应用程序,但通常是应用程序可以单独运行的一个部分。
操作系统根据线程的优先级和离最近运行的时间长短给每一个线程分配处理时间。
多线程对于时间密集型事务(例如文件输入输出)应用程序的性能有很大的提高。
但是也有必须细心的地方。
尽管多线程能提高性能,但是每个线程还是需要用附加的内存来建立和处理器时间来运行,建立太多的线程可能降低应用程序的性能。
当设计多线程应用程序时,应该比较性能与开销。
多任务成为操作系统的一部分已经很久了。
但是直到最近Visual Basic程序员才能使用无文档记录特性(undocumented)或者间接使用COM组件或者操作系统的异步部分执行多线程事务。
.NET框架组件为开发多线程应用程序,在System.Threading名字空间中提供了全面的支持。
本文讨论多线程的好处以及怎样使用Visual Basic .NET开发多线程应用程序。
尽管Visual Basic .NET和.NET框架组件使开发多线程应用程序更容易,但是本文作了调整使其适合高级读者和希望从早期Visual Basic转移到Visual Basic .NET的开发人员。
多线程处理的优点尽管同步应用程序易于开发,但是它们的性能通常比多线程应用程序低,因为一个新的事务必须等待前面的事务完成后才能开始。
如果完成某个同步事务的时间比预想的要长,应用程序可能没有响应。
多线程处理可以同时运行多个过程。
JAVA多线程的使用场景与注意事项总结

JAVA多线程的使用场景与注意事项总结Java多线程是指在一个程序中同时运行多个线程,每个线程都有自己的执行代码,但是又共享同一片内存空间和其他系统资源。
多线程的使用场景和注意事项是我们在开发中需要关注的重点,下面将详细进行总结。
一、Java多线程的使用场景:1.提高程序的执行效率:多线程可以充分利用系统资源,将一些耗时的操作放到一个线程中执行,避免阻塞主线程,提高程序的执行效率。
2.实现并行计算:多线程可以将任务拆分成多个子任务,每个子任务分配给一个线程来执行,从而实现并行计算,提高计算速度。
3.响应性能提升:多线程可以提高程序的响应性能,比如在用户界面的开发中,可以使用多线程来处理用户的输入和操作,保证界面的流畅性和及时响应。
4.实时性要求高:多线程可以实现实时性要求高的任务,比如监控系统、实时数据处理等。
5.任务调度与资源管理:多线程可以实现任务的调度和资源的管理,通过线程池可以更好地掌控任务的执行情况和使用系统资源。
二、Java多线程的注意事项:1.线程安全性:多线程操作共享资源时,要注意线程安全问题。
可以通过使用锁、同步方法、同步块等方式来解决线程安全问题。
2.死锁:多线程中存在死锁问题,即多个线程相互等待对方释放资源,导致程序无法继续执行。
要避免死锁问题,应尽量减少同步块的嵌套和锁的使用。
3.内存泄漏:多线程中存在内存泄漏问题,即线程结束后,线程的资源没有得到释放,导致内存占用过高。
要避免内存泄漏问题,应及时释放线程资源。
4.上下文切换:多线程的切换会带来上下文切换的开销,影响程序的执行效率。
要注意合理分配线程的数量,避免过多线程的切换。
5. 线程同步与通信:多线程之间需要进行同步和通信,以保证线程之间的正确协调和数据的一致性。
可以使用synchronized关键字、wait(和notify(方法等方式进行线程同步和通信。
6.线程池的使用:在多线程编程中,可以使用线程池来管理线程的创建和销毁,可以减少线程的创建和销毁的开销,提高程序的性能。
JAVA开发中的多线程编程技术
JAVA开发中的多线程编程技术Java作为一种广泛应用于企业级应用以及各种工业自动化系统的编程语言,其对于处理多线程并发的问题起到了巨大的作用。
在Java开发过程中,我们经常会遇到需要多线程并发处理的情况,比如高并发的Web服务、大数据处理、图像处理等等。
如何正确合理的使用Java多线程技术是一个非常重要的问题。
本文将详细讲解Java开发中的多线程编程技术。
1.了解Java线程模型Java语言具有完善的线程模型,并提供了Thread类以及Runnable接口,方便程序员进行多线程编程。
在进行Java多线程编程的过程中,必须先理解Java的线程模型,包括线程的创建、使用、同步、互斥、线程间通信等。
同时,也要掌握Java虚拟机的内存结构以及线程调度器的工作原理,这些对多线程编程至关重要。
2.使用synchronized实现线程同步在多线程编程中,需要涉及到许多复杂的操作,如多个线程同时对同一共享数据进行读写操作会造成数据不一致等问题。
这时需要使用synchronized关键字来进行同步。
通过对象锁的机制,保证每个时间段只有一个线程能够访问同一个对象的同步代码块。
当线程进入一个对象的同步块时,将获得该对象的锁,只有等线程退出同步块或发生异常时才会释放锁,其他线程才能进入同步块。
通过synchronized关键字的同步机制能控制线程的读写顺序,使多个线程协同工作,防止数据不一致的问题。
3.使用volatile变量实现线程间通信在多线程编程中,需要进行线程间的通信。
在Java语言中,volatile变量可以用来实现线程间的通信。
当一个变量被声明为volatile变量后,所有线程对这个变量的读写操作都会直接在内存中进行,而不会使用线程的缓存中间值。
这样可以避免数据缓存的不一致,并保证在不同线程中读写的顺序是一致的,从而实现了线程之间的通信。
4.掌握并发包中的工具类Java并发包提供了许多实用的工具类,方便程序员在多线程编程中使用。
多线程 注意事项
多线程注意事项多线程是指在一个程序中同时运行多个线程,每个线程独立执行不同的任务。
多线程的使用可以提高程序的性能和响应速度,但同时也需要注意一些问题和注意事项。
1. 线程安全性:在多线程编程中,线程与线程之间共享同一块内存空间,因此需要关注线程安全性。
如果多个线程同时访问和修改同一份数据,可能会导致数据不一致或出现竞态条件。
为了确保线程安全,可以使用同步机制,如互斥锁(mutex)、条件变量、信号量等来控制对共享数据的访问。
2. 线程同步:线程同步是保证多个线程按照一定的顺序协同工作的一种机制。
例如,如果一个线程需要依赖另一个线程的结果,则需要使用同步机制来等待另一个线程完成任务并获取结果。
常见的线程同步机制包括互斥锁、条件变量、信号量等。
3. 死锁:当多个线程相互等待对方释放资源时,可能会导致死锁。
死锁是指所有的线程都无法继续执行,程序陷入僵局。
为了避免死锁,需要合理设计线程间资源的请求和释放顺序,避免循环等待。
4. 线程优先级:线程在操作系统中会分配一个优先级,优先级高的线程会获得更多的系统资源。
但在实际开发中,不建议过分依赖线程优先级来控制线程的执行顺序,因为不同操作系统和硬件平台对线程优先级的实现方式不同。
5. 线程创建和销毁的开销:创建线程和销毁线程都需要一定的系统资源。
频繁创建和销毁线程会带来开销,所以需要根据实际需求和系统资源的限制,合理选择线程的创建和销毁时机。
6. 上下文切换开销:当一个处理器从一个线程切换到另一个线程时,需要保存当前线程的上下文状态以及加载新线程的上下文状态,这个过程称为上下文切换。
上下文切换会带来一定的开销,特别是当线程数量较多时。
因此,合理控制线程数量,避免不必要的线程切换,可以提高程序的性能。
7. 资源管理:多线程需要共享系统资源,如内存、文件、网络连接等。
因此,需要合理地管理和分配这些资源,避免出现资源争用的情况。
特别是当多个线程同时访问和修改同一份数据时,需要确保对资源的访问和修改都是线程安全的。
使用Java编写线程安全的代码的准则
使用Java编写线程安全的代码的准则Java语言提供了多种机制来实现线程安全的代码。
下面是一些编写线程安全代码的准则:1.使用不可变对象:不可变对象是指一旦创建就不能修改其状态的对象。
由于不可变对象是无法修改的,所以它们可以在多个线程之间共享而不需要进行额外的同步操作。
因此,使用不可变对象来编写线程安全的代码是一个很好的选择。
2.同步访问共享资源:如果有多个线程需要访问共享的可变资源,必须使用同步机制来保证同时只有一个线程访问该资源。
Java提供了synchronized关键字和锁机制来实现同步访问。
a.使用synchronized关键字:可以使用synchronized关键字来修饰方法或代码块,从而保证同一时间只有一个线程能够执行被修饰的方法或代码块。
b.使用锁机制:Java提供了内置的锁机制,可以使用synchronized关键字或ReentrantLock类来创建锁对象,并使用lock()和unlock()方法来加锁和释放锁。
3.使用volatile关键字保证可见性:如果一个变量被多个线程访问并且其中至少有一个线程修改了它的值,那么必须使用volatile关键字来确保所有线程都能看到最新的值。
使用volatile关键字修饰的变量将会在每次访问时从主内存中读取其最新的值。
4.避免共享数据的修改:尽量避免多个线程修改同一个共享的可变数据,因为同时对同一数据进行修改可能导致不一致或者竞态条件。
如果需要对共享数据进行修改,应该将其封装在一个对象中,并使用锁机制来确保同一时间只有一个线程修改该数据。
5.使用ThreadLocal类:ThreadLocal类可以用来创建线程本地变量,每个线程都会有自己的变量副本,从而避免了线程之间的竞争和同步。
ThreadLocal类在为每个线程创建独立的变量副本时使用了Map结构,每个线程都有自己的Map实例。
6.使用并发集合类:Java提供了许多线程安全的并发集合类(如ConcurrentHashMap、ConcurrentLinkedQueue),可以用来替代传统的非线程安全的集合类。
Python中的多线程和多进程编程技术
Python中的多线程和多进程编程技术随着计算机系统硬件性能的提高,多核心处理器的出现和并行计算能力的加强,多线程和多进程编程技术越来越受到了关注。
在Python编程中,使用多线程和多进程技术可以有效地提高程序的运行效率和性能。
本文将介绍Python中的多线程和多进程编程技术,以及它们的优缺点和适用条件。
一、多线程编程技术在计算机系统中,线程是指进程中的一个单独的执行路径,可以共享进程的资源和数据,每个线程独立地执行任务。
在Python 中,可以使用threading模块来实现多线程编程。
下面是一个基本的多线程示例:```pythonimport threadingdef say_hello(name):print("Hello, %s!" %name)if __name__ == '__main__':t1 = threading.Thread(target=say_hello, args=('Alice',))t2 = threading.Thread(target=say_hello, args=('Bob',))t1.start()t2.start()t1.join()t2.join()```在上面的示例中,我们定义了一个名为say_hello的函数,该函数接收一个参数name,并在控制台打印出“Hello, name!”。
然后,我们使用threading.Thread类创建两个线程t1和t2,将say_hello 函数作为线程的目标函数,并将参数传递给args参数。
然后,我们通过调用t1和t2的start()方法启动这两个线程,并使用join()方法等待它们完成。
多线程编程技术的优点在于可以充分利用多核心处理器的并行计算能力,提高程序的运行效率和性能。
另外,多线程编程适用于一些CPU密集型的任务,例如图像处理、密码破解等。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
2.4多线程编程的原则及要点:随着多核CPU的出世,多核编程方面的问题将摆上了程序员的日程,有许多老的程序员以为早就有多CPU的机器,业界在多CPU机器上的编程已经积累了很多经验,多核CPU上的编程应该差不多,只要借鉴以前的多任务编程、并行编程和并行算法方面的经验就足够了。
但是,多核机器和以前的多CPU机器有很大的不同,以前的多CPU机器都是用在特定领域,比如服务器,或者一些可以进行大型并行计算的领域,这些领域很容易发挥出多CPU的优势,而现在多核机器则是应用到普通用户的各个层面,特别是客户端机器要使用多核CPU,而很多客户端软件要想发挥出多核的并行优势恐怕没有服务器和可以进行大型并行计算的特定领域简单。
多核CPU中,要很好地发挥出多个CPU的性能的话,必须保证分配到各个CPU上的任务有一个很好的负载平衡。
否则一些CPU在运行,另外一些CPU处于空闲,无法发挥出多核CPU 的优势来。
要实现一个好的负载平衡通常有两种方案,一种是静态负载平衡,另外一种是动态负载平衡。
1、静态负载平衡静态负载平衡中,需要人工将程序分割成多个可并行执行的部分,并且要保证分割成的各个部分能够均衡地分布到各个CPU上运行,也就是说工作量要在多个任务间进行均匀的分配,使得达到高的加速系数。
2、动态负载平衡动态负载平衡是在程序的运行过程中来进行任务的分配达到负载平衡的目的。
实际情况中存在许多不能由静态负载平衡解决的问题,比如一个大的循环中,循环的次数是由外部输入的,事先并不知道循环的次数,此时采用静态负载平衡划分策略就很难实现负载平衡。
动态负载平衡中对任务的调度一般是由系统来实现的,程序员通常只能选择动态平衡的调度策略,不能修改调度策略,由于实际任务中存在很多的不确定因素,调度算法无法做得很优,因此动态负载平衡有时可能达不到既定的负载平衡要求。
3、负载平衡的难题在那里?负载平衡的难题并不在于负载平衡的程度要达到多少,因为即使在各个CPU上分配的任务执行时间存在一些差距,但是随着CPU核数的增多总能让总的执行时间下降,从而使加速系数随CPU核数的增加而增加。
负载平衡的困难之处在于程序中的可并行执行块很多要靠程序员来划分,当然CPU核数较少时,比如双核或4核,这种划分并不是很困难。
但随着核数的增加,划分的粒度将变得越来越细,到了16核以上时,估计程序员要为如何划分任务而抓狂。
比如一段顺序执行的代码,放到128核的CPU上运行,要手工划分成128 个任务,其划分的难度可想而知。
负载划分的误差会随着CPU核数的增加而放大,比如一个需要16个时间单位的程序分到4个任务上执行,平均每个任务上的负载执行时间为4个时间单位,划分误差为1个时间单位的话,那么加速系数变成 16/(4+1)=3.2,是理想情况下加速系数 4的80%。
但是如果放到一个16核CPU上运行的话,如果某个任务的划分误差如果为0.5个时间单位的话,那么加速系数变成16/(1+0.5) = 10.67,只有理想的加速系数16的66.7%,如果核数再增加的话,由于误差的放大,加速系数相比于理想加速系数的比例还会下降。
负载划分的难题还体现在CPU和软件的升级上,比如在4核CPU上的负载划分是均衡的,但到了8核、16核上,负载也许又变得不均衡了。
软件升级也一样,当软件增加功能后,负载平衡又会遭到破坏,又需要重新划分负载使其达到平衡,这样一来软件设计的难度和麻烦大大增加了。
难题一:串行化方面的难题1)加速系数衡量多处理器系统的性能时,通常要用到的一个指标叫做加速系数,定义如下:S(p) = 使用单处理器执行时间(最好的顺序算法)/ 使用具有p个处理器所需执行时间2)Amdahl定律并行处理时有一个Amdahl定律,用方程式表示如下:S(p) = p / (1 + (p-1)*f)其中 S(p)表示加速系数p表示处理器的个数f表示串行部分所占整个程序执行时间的比例当f = 5%, p = 20时, S(p) = 10.256左右当f = 5%, p = 100时, S(p) = 16.8左右也就是说只要有5%的串行部分,当处理器个数从20个增加到100个时,加速系数只能从10.256增加到16.8左右,处理器个数增加了5倍,速度只增加了60%多一点。
即使处理器个数增加到无穷多个,加速系数的极限值也只有20。
如果按照Amdahl定律的话,可以说多核方面几乎没有任何发展前景,即使软件中只有1%的不可并行化部分,那么最大加速系统也只能到达100,再多的CPU也无法提升速度性能。
按照这个定律,可以说多核CPU的发展让摩尔定律延续不了多少年就会到达极限。
3)Gustafson定律Gustafson 提出了和Amdahl定律不同的假设来证明加速系数是可以超越Amdahl定律的限制的,Gustafson认为软件中的串行部分是固定的,不会随规模的增大而增大,并假设并行处理部分的执行时间是固定的(服务器软件可能就是这样)。
Gustafson定律用公式描述如下:S(p) = p + (1-p)*fts其中fts表示串行执行所占的比例如果串行比例为5%,处理器个数为20个,那么加速系数为20+(1-20)*5%=19.05如果串行比例为5%,处理器个数为100个,那么加速系数为100+(1-100)*5%=95.05Gustafson定律中的加速系数几乎跟处理器个数成正比,如果现实情况符合Gustafson定律的假设前提的话,那么软件的性能将可以随着处理个数的增加而增加。
4)实际情况中的并行化分析Amdahl定律和Gustafson定律的计算结果差距如此之大,那么现实情况到底是符合那一个定律呢?我个人认为现实情况中既不会象Amdahl定律那么悲观,但也不会象Gustafson定律那么乐观。
为什么这样说呢?还是进行一下简单的分析吧。
首先需要确定软件中到底有那么内容不能并行化,才能估计出串行部分所占的比例,20世纪60年代时,Bernstein就给出了不能进行并行计算的三个条件:条件1:C1写某一存储单元后,C2读该单元的数据。
称为“写后读”竞争条件2:C1读某一存储单元数据后,C2写该单元。
称为“读后写”竞争条件3:C1写某一存储单元后,C2写该单元。
称为“写后写”竞争满足以上三个条件中的任何一个都不能进行并行执行。
不幸的是在实际的软件中大量存在满足上述情况的现象,也就是我们常说的共享数据要加锁保护的问题。
加锁保护导致的串行化问题如果在任务数量固定的前提下,串行化所占的比例是随软件规模的增大而减小的,但不幸的是它会随任务数量的增加而增加,也就是说处理器个数越多,锁竞争导致的串行化将越严重,从而使得串行化所占的比例随处理器个数的增加而急剧增加。
所以串行化问题是多核编程面临的一大难题。
5)可能的解决措施对于串行化方面的难题,首先想到的解决措施就是少用锁,甚至采用无锁编程,不过这对普通程序员来说几乎是难以完成的工作,因为无锁编程方面的算法太过于复杂,而且使用不当很容易出错,许多已经发表到专业期刊上的无锁算法后来又被证明是错的,可以想象得到这里面的难度有多大。
第二个解决方案就是使用原子操作来替代锁,使用原子操作本质上并没有解决串行化问题,只不过是让串行化的速度大大提升,从而使得串行化所占执行时间比例大大下降。
不过目前芯片厂商提供的原子操作很有限,只能在少数地方起作用,芯片厂商在这方面可能还需要继续努力,提供更多功能稍微强大一些的原子操作来避免更多的地方的锁的使用。
第三个解决方案是从设计和算法层面来缩小串行化所占的比例。
也许需要发现实用的并行方面的设计模式来缩减锁的使用,目前业界在这方面已经积累了一定的经验,如任务分解模式,数据分解模式,数据共享模式,相信随着多核CPU的大规模使用将来会有更多的新的有效的并行设计模式和算法冒出来。
第四个解决方案是从芯片设计方面来考虑的。
主要的想法是在芯片层面设计一些新的指令,这些指令不象以前单核CPU指令那样是由单个CPU完成的,而是由多个CPU进行并行处理完成的一些并行指令,这样程序员调用这些并行处理指令编程就象编写串行化程序一样,但又充分利用上了多核的优势。
前面我们提到了锁竞争会让程序中的串行化比例随使用的CPU的核数增多而加剧的现象,现在我们就来对多核编程中的锁竞争进行深入的分析。
为了简化起见,我们先看一个简单的情况,假设有4个对等的任务同时启动运行,假设每个任务刚开始时有一个需要锁保护的操作,耗时为1,每个任务其他部分的耗时为25。
这几个任务启动运行后的运行情况如下图所示:图2.9:对等任务的锁竞争示意图在上图中,可以看出第1个任务直接执行到结束,中间没有等待,第2个任务等待了1个时间单位,第3个任务等待了2个时间单位,第3个任务等待了3个时间单位。
这样整个任务总计等待了6个时间单位,如果这几个任务是采用OpenMP里的所有任务都在同一点上进行等待到全部任务执行完再向下执行时,那么总的运行时间将和第四个任务一样为29个时间单位,加速系数为:(1+4×25)/ 29 = 3.48即使以4个任务的平均时间27.5来进行计算,加速系数=101/27.5 = 3.67按照Amdahl定律来计算加速系数的话,上述应用中,串行时间为1,并行处理的总时间转化为串行后为100个时间单位,如果放在4核CPU上运行的话,加速系数=p / (1 + (p-1)*f) = 4/(1+(4-1)*1/101) = 404/104 = 3.88这就产生了一个奇怪的问题,使用了锁之后,加速系数连阿姆尔达定律计算出来的加速系数都不如,更别说用Gustafson定律计算的加速系数了。
其实可以将上面4个任务的锁竞争情况推广到更一般的情况,假设有锁保护的串行化时间为1,可并行化部分在单核CPU上的运行时间为t,CPU核数为p,那么在p个对等任务同时运行情况下,锁竞争导致的总等待时间为:1+2+…+p = p*(p-1)/2耗时最多的一个任务所用时间为: (p-1) + t/p使用耗时最多的一个任务所用时间来当作并行运行时间的话,加速系数如下S(p) = t / (p-1 + t/p) = p*t / (p*(p-1)+t) (锁竞争下的加速系数公式)这个公式表明在有锁竞争情况下,如果核数固定情况下,可并行化部分越大,那么加速系数将越大。
在并行化时间固定的情况下,如果CPU核数越多,那么加速系数将越小。
还是计算几个实际的例子来说明上面公式的效果:令t=100, p=4, 加速系数=4×100 / (4*(4-1)+100) = 3.57令t=100, p=16, 加速系数=16×100 / (16*(16-1)+100) = 4.7令t=100, p=64, 加速系数=64×100 / (64*(64-1)+100) = 1.54令t=100, p=128, 加速系数=128×100 / (128*(128-1)+100) = 0.78从以上计算可以看出,当核数多到一定的时候,加速系数不仅不增加反而下降,核数增加到128时,加速系数只有0.78,还不如在单核CPU上运行的速度。