MPI 中多线程的使用

本文从本人简书博客同步过来

上一篇中我们介绍了 MPI-3 中共享内存操作,下面我们将介绍 MPI 中多线程的使用,以助于我们理解 MPI-3 中引进的线程安全的 Mprobe 操作(将在下一篇中介绍)。

进程与线程

通常 MPI 中大多数操作的基本实体是进程,但是MPI 进程中可以执行多个线程。

进程是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。通常在一个进程中可以包含若干个线程,一个进程中至少有一个线程。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程粒度更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有独立的地址空间。

MPI 多线程

MPI 进程中可以执行多个线程,同一个进程的多个线程具有均等的机会参与该进程的 MPI 通信。

某些情况下在 MPI 中使用多线程能够提供很大的方便,如:

  • 多线程能够自然地实现非阻塞通信操作。比如可以创建一个单独的线程来执行阻塞接收操作,只要操作只阻塞该线程而不阻塞整个进程的执行,其效果就是一个非阻塞的接收操作。
  • 线程非常适合对称多处理机(symmetric multi-processing, SMP)共享内存编程模型。
  • 多线程能够提高某些高延时系统的性能。

在单线程的情况下,一个进程无法安全地执行到其自身的阻塞点到点通信。采用多线程,则可使同一个进程的两个线程分别执行阻塞发送和阻塞接收而不至于死锁。

MPI 要求 MPI_Init 和 MPI_Finalize 调用必须在相同线程内配对执行,执行了这两个操作的线程被称作主线程(main thread),主线程的 MPI_Finalize 调用必须在所有其它线程都执行完 MPI 相关的通信、I/O 和其它 MPI 操作之后执行。

线程安全性

线程安全性是指多个线程可以同时执行消息传递的相关调用而不会相互影响。

MPI被设计为线程安全的,但应用程序自身负责维护多线程安全,一个简单的办法是在不同线程使用不同的通信子对象,这样可实现线程间操作的互不干扰。MPI 的大多数操作都满足线程安全性的条件,但是也有例外,如在多线程中使用 MPI.Comm.Probe 或 MPI.Comm.Iprobe 来确定一个消息的来源和大小,然后接收该消息的操作就不是线程安全的,不过 MPI-3 提供了线程安全的版本 MPI.Comm.Mprobe 和 MPI.Comm.Improbe,这些将在下一篇中介绍。另外 MPI.File.Seek 也不是线程安全的,但是可以用线程安全的显式偏移文件操作函数,如 MPI.File.Read_at 等来替代其操作。

MPI 线程相关函数

MPI.Init_thread(int required=THREAD_MULTIPLE)

该函数除了实施正常由 MPI.Init 执行的初始化之外,还负责初始化 MPI 多线程执行环境。reauired 参数指出所要求的多线程支持程度,可能的取值如下:

  • MPI.THREAD_SINGLE,仅允许单线程;
  • MPI.THREAD_FUNNELED,可以多线程,单只允许主线程执行 MPI 函数;
  • MPI.THREAD_SERIALIZED,可以多线程,每个线程都可执行 MPI 函数,但某一时刻仅允许一个线程执行 MPI 函数;
  • MPI.THREAD_MULTIPLE,多线程,线程可任意执行 MPI 函数。

这些常数都是整数,并且在数值上是从小到大的。

该函数返回 MPI 环境实际支持的多线程级别。

MPI.COMM_WORLD 中不同进程可分别设置不同的线程支持级别。多线程 MPI 中调用 MPI.Init 的实际效果等价于用 MPI.THREAD_SINGLE 调用 MPI.Init_thread。

注意:使用 mpi4py 时,MPI.Init 和 MPI.Init_thread 在从 mpi4py 包中 import MPI 时会被自动调用,因此一般不用在程序中显式调用。mpi4py 中 MPI.Init_thread 默认 required 的线程级别是 MPI.THREAD_MULTIPLE,但实际得到的线程级别由 MPI 环境给出。如果想要手动控制 MPI 程序的初始化或者设置 MPI 线程级别,可以在 import MPI 之前先 import rc 模块,并设置 rc.initialize = False,然后手动初始化,或者设置 rc.thread_level 为需要的级别(”multiple”, “serialized”, “funneled” 或 “single”)。

MPI.Query_thread()

返回 MPI 环境支持的线程级别。

MPI.Is_thread_main()

判断调用该函数的线程是否为主线程,如果是,返回 True,否则返回 False。

Python threading 模块

MPI 并没有提供创建线程的函数或方法,创建线程需由其它的工具来完成,这就保证了 MPI 程序可以使用任何与 MPI 实现兼容的线程,而不局限于某一种特定的线程。比较广泛使用的线程工具有 Pthreads 库和 OpenMP 库。

在 Python 中则可以使用 thread 模块(Python 3 中被命名为 _thread),但是更常用的是建立在其之上的更易用更高级别的线程模块 threading。下面对 threading 模块作简要的介绍。

threading 模块提供了若干函数和对象,这里主要介绍 threading.Thread 对象。该对象表示一个单独运行的线程活动,可以通过两种方式来创建和运行一个单独的线程:为其传递一个可以被调用的对象;或者继承该类并重载 run() 方法。一般在子类中只能重载 __init__() 和 run() 方法,其它方法都不应该被重载。

当一个线程被创建后,必须使用 start() 方法来启动该线程,该方法会在内部调用 run() 方法。

一个线程被启动后,它的状态会是 “alive”,直到该线程的 run() 方法运行停止(不管是正常结束还是异常中止)。可以用 is_alive() 方法来测试该线程是否 “alive”。

其它线程可以调用一个线程的 join() 方法,此操作会阻塞该调用线程直到被调用线程停止。

每个线程都有一个名称,如果没有设置,系统会为其分配一个默认的名称。可以在构造一个线程时为其传递一个指定的名称,或者在运行过程中通过 name 属性动态改变。

一个线程可以被标记为守护线程(daemon),守护线程可以一直运行而不阻塞主程序退出。如果一个服务无法用一种容易的方法来中断线程,或者希望线程工作到一半时中止而不会损失或破坏数据,对此服务,使用守护线程就很有用。可以通过设置线程的 daemon 属性来标记一个线程为守护线程。默认情况下线程不作为守护线程。

程序的初始线程为主线程,主线程不是守护线程。

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={})

初始化一个线程目标。group 初始应该设置为 None,目前没有作用,为今后的扩展所保留,target 为一个可被调用的对象,name 如果非 None,设置线程的名称,默认名称为 “Thread-N”,N 为一个数字,argskwargs 为其它参数。

如果子类要重载此构造方法,必须首先调用基类的构造方法(Thread.__init__()),然后再做其它事情。

start()

启动该线程。一个线程只能至多调用一次该方法,否则会抛出 RuntimeError 异常。

run()

线程的活动或工作。可以在子类中重载该方法以完成需要的工作。

join([timeout])

等待线程停止。默认情况下调用该方法的线程会无限阻塞直到被调用线程停止,但是如果 timeout 参数设置为一个浮点数,则只会等待 timeout 秒就返回,无论被调用线程是否中止。该方法总是返回 None,因此必须调用 is_alive() 方法来确定该调用的返回是由于被调用线程停止还是由于等待了 timeout 时间。

可以调用一个线程的 join() 方法多次。

is_alive()

返回该线程是否 “alive”。

name

线程的名称,为一个字符串。

ident

线程的标识符,为一个非 0 整数。

daemon

一半布尔值表示线程是否为守护线程。

例程

下面给出使用例程。

# thread.py

"""
Demonstrates the usage of threads with MPI.

Run this with 2 processes like:
$ mpiexec -n 2 python thread.py
"""


import sys
import numpy as np
import threading
from mpi4py import MPI


if MPI.Query_thread() < MPI.THREAD_MULTIPLE:
    sys.stderr.write("MPI does not provide enough thread support\n")
    sys.exit(0)

comm = MPI.COMM_WORLD
rank = comm.rank

if rank == 0:
    other = 1
elif rank == 1:
    other = 0
else:
    sys.exit(0)

# initialize recv_buf to -1
recv_buf = np.array(-1)


def send():
    current_thread = threading.currentThread()
    print '%s is main thread: %s' % (current_thread.name, MPI.Is_thread_main())
    print '%s sends %d to rank %d...' % (current_thread.name, rank, other)
    comm.Send(np.array(rank), dest=other, tag=11)

def recv():
    current_thread = threading.currentThread()
    print '%s is main thread: %s' % (current_thread.name, MPI.Is_thread_main())
    comm.Recv(recv_buf, source=other, tag=11)
    print '%s receives %d from rank %d' % (current_thread.name, recv_buf, other)

# create thread by using a function
send_thread = threading.Thread(target=send, name='[rank-%d send_thread]' % rank)
recv_thread = threading.Thread(target=recv, name='[rank-%d recv_thread]' % rank)

current_thread = threading.currentThread()
print '%s is main thread: %s' % (current_thread.name, MPI.Is_thread_main())
print 'before: rank %d has %d' % (rank, recv_buf)
# start the threads
send_thread.start()
recv_thread.start()

# wait for terminate
send_thread.join()
recv_thread.join()
print 'after: rank %d has %d' % (rank, recv_buf)

运行结果如下:

$ mpiexec -n 2 python  thread.py
MainThread is main thread: True
before: rank 0 has -1
[rank-0 send_thread] is main thread: False
[rank-0 send_thread] sends 0 to rank 1...
MainThread is main thread: True
before: rank 1 has -1
[rank-1 send_thread] is main thread: False
[rank-1 send_thread] sends 1 to rank 0...
[rank-1 recv_thread] is main thread: False
[rank-1 recv_thread] receives 0 from rank 0
after: rank 1 has 0
[rank-0 recv_thread] is main thread: False
[rank-0 recv_thread] receives 1 from rank 1
after: rank 0 has 1

以上介绍了 MPI 中多线程的使用,在下一篇中我们将介绍 MPI-3 中线程安全的 Mprobe。

猜你喜欢

转载自blog.csdn.net/zuoshifan/article/details/81006507
mpi
今日推荐