1、前言
這個項目是一次課程作業,老師要求寫一個並行計算框架,本人本身對openmp比較熟,加上又是scala
的愛好者,所以想了許久,終於想到了用scala來實現一個類似openmp的一個簡單的並行計算框架。
項目github地址:ScalaMp
2、框架簡介
該並行計算框架是受openmp啓發,以scala語言實現的一個模仿openmp基本功能的簡單並行計算框架,
該框架的設計目標是,讓用戶可以只需關心並行的操作的實現而無需考慮線程的創建和管理。本框架實現了最
基本的並行代碼塊和並行循環兩個功能。
接下來會介紹框架的接口設計和具體的技術實現細節。然後會以3個具體的例子來演示框架的
使用方法,和驗證框架的正確性,更多的例子詳見github上的example.Main.scala文件。
3個具體的並行計算問題包括:
1、梯形積分法
2、計算pi值
3、多線程分段下載文件(圖片、mp3)
3、框架接口設計與技術實現
3.1、接口設計
該框架主要是模仿了openmp的“omp parallel”和“omp parallel for”兩條並行命令,
以scala語言實現了自己的版本。
在介紹接口設計之前首先我們可以分析一下以上五個問題的做一下抽象,把相同的可並行的
部分抽象出來。並行這五個問題,抽象出來可以看成是給定一個任務(有固定長度)和線程數,
每個線程負責這個任務某一段的計算。比如:
1、梯形積分法
給了定積分區間和梯形個數,每個線程就負責某一段區間的梯形面積的計算。
2、計算pi值
公式:然後給定精度k,每個線程就計算某段的和。
3、多線程分段下載文件(圖片、mp3)
當知道了需要下載的文件的長度,每個線程就也是負責某段區間的數據下載。
所以根據以上並行問題的抽象和對openmp的理解再結合Scala語言,該框架設計兩個接口:
第一個是並行for 循環的接口:
range指的是循環的範圍,比如for循環是從0到99則range等於0 to 99,對應於for循環的結束條件,
然後下一個參數是設置schedule,目前實現了static和dynamic,如果不想自己設置,可以用提供的
默認參數:“Default_Schedule_Static”和“Default_Schedule_Dynamic”。
然後withThread代表需要開啓的線程數目,each函數接受一個lamda表達式作爲參數,表示一個線程
執行的操作,具體實現由用戶定義,my_rank參數代表線程的標號,threadNum代表線程的總數目,
range參數表示該線程分到的某段長度範圍,然後線程根據這段範圍來做自己的事情。
Critical代表臨界區,需要同步的代碼就放到critical函數裏面。
第二個是並行代碼塊的接口:
對應參數和parallel_for一樣,只是代碼塊的並行接口比for版本簡單,因爲就是對代碼塊的並行。
3.2技術實現細節
實現上主要是藉助了Scala 和 Akka。
Scala(Scalable Langeaue) 是一種多範式的編程語言,設計初衷是要集成面向對象編程和函數式編程
的各種特性。Akka 是一個用 Scala 編寫的庫,用於簡化編寫容錯的、高可伸縮性的 Java 和 Scala 的
Actor 模型應用。
實現上主要是利用akka框架來實現後臺的actor(輕量級的線程)的創建和管理。爲了使得接口的調用
更接近於openmp,利用了scala語言的特性。
首先ScalaMp是一個單例對象,而且後面的parallel_for, parallel, withThread, op, each等都是
ScalaMp對象的成員函數,由於scala語言的特性,符合某些條件的成員函數的調用可以省略“.”號,
並且加上函數的鏈式調用就形成了接口的表現形式。
ScalaMp粗略代碼,詳細代碼見github:
schedule的定義:
當ScalaMp對象被創建的時候,會在內部創建一個ActorSystem,可以看成是一個線程環境,然後在
環境中創建一個管理者actor,然後該actor會創建100個工人actor,並對它們進行管理,可以看成是線程池。
然後每次用戶進行並行操作的時候,就從線程池中分配製定的工人actor個數來執行操作。ScalaMp對象只會
在第一次被訪問的時候創建,然後在整個程序週期結束前都會存在。
當用戶調用接口時,管理者會將用戶定義的線程函數發送給每個actor,然後每個actor執行用戶定義
的函數。
臨界區的實現時藉助了actor模型的郵箱來實現的,因爲actor之間的通信是通過發送郵件的方式通信,
而郵箱會對消息做同步,使得actor能夠處理完一條消息再處理下一條消息。所以臨界區內的代碼其實是
被封裝成了一個函數,然後由每個工人actor發送給管理者,管理者一條一條的處理來自工人actor的
臨界區函數,也就是相當於同步執行了臨界區的代碼,也就是說其實臨界區的代碼並不由每個
工人actor執行,而是由工人actor發送給管理者,然後由管理者執行,並且藉助郵箱的同步特點,
使得能夠實現線程同步的操作。
4、框架演示
我們還是從經典的“hello world”例子開始
4.1、hello World
代碼:
運行結果:
4.2、梯形積分法
代碼:
運行結果:
4.3、計算pi值
代碼:
運行結果:
4.4、多線程下載文件
下載的文件時古巨基的“情歌王”:
代碼:
//多線程下載文件 multi-thread download file
println("multi-thread download file: ")
println("parallel version: ")
import java.net._
import java.io._
import java.io.RandomAccessFile;
val url = new URL("http://yinyueshiting.baidu.com/data2/music/5140129/20572571421424061128.mp3?xcode=974dbf2923e1208ffe561ce0b05f51646b547126504ae00a")
val connection = url.openConnection.asInstanceOf[HttpURLConnection];
connection setRequestMethod "GET"
connection setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:17.0) Gecko/20100101 Firefox/17.0")
connection setAllowUserInteraction true
val length = connection.getContentLength
println(s"content-length: $length")
val file = new File("F:\\情歌王_P.mp3")
CreateFile.createFile(file, length.toLong)
var start = System.currentTimeMillis
ScalaMp parallel_for(0 until length, Default_Schecule_Static) withThread(10) each{ (my_rank, threadNum, range) =>
val fos = new RandomAccessFile(file, "rw");
val BUFFER_SIZE = 256
val buf = new Array[Byte](BUFFER_SIZE);
var startPos = range(0)
var endPos = startPos + range.length - 1
var curPos = startPos
val connection2 = url.openConnection.asInstanceOf[HttpURLConnection];
connection2 setRequestMethod "GET"
connection2 setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)")
connection2 setAllowUserInteraction true
connection2.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);//設置獲取資源數據的範圍,從startPos到endPos
fos.seek(startPos);
val bis = new BufferedInputStream(connection2.getInputStream());
while (curPos < endPos) {
val len = bis.read(buf, 0, BUFFER_SIZE);
fos.write(buf, 0, len);
curPos = curPos + len;
}
}
println(s"parallel Time: ${System.currentTimeMillis - start} ms\n")
println("serial version: ")
val file2 = "F:\\情歌王_S.mp3"
if(connection.getResponseCode == 200) {
val out = new java.io.FileWriter(file2)
val in = connection.getInputStream
// 1K的數據緩衝
val bs = new Array[Byte](1024)
// 讀取到的數據長度
var len = 0
val sf = new File(file2)
val os = new FileOutputStream(sf)
// 開始讀取
len = in.read(bs)
while(len != -1){
os.write(bs, 0, len)
len = in.read(bs)
}
// 完畢,關閉所有鏈接
os.close
in.close
}
println(s"serial Time: ${System.currentTimeMillis - start} ms\n")
運行結果:
5、總結
目前該框架只是實現了簡單的線程管理,還有代碼還存在許多bug,比如最大線程數不能超過100,
還有程序不會終止等,而且schedule策略只實現了static和dynamic策略,dynamic的策略實現的可能不太對。
最後希望感興趣的朋友可以和我一起改進這個小框架,雖然在實際問題中測試的不夠多,但是我也嘗試過
在實際中的應用,並行還是顯著效果的,比如某個問題是我現在有4000個400維的特徵,每個特徵要尋找
在另外3999個特徵中距離的top20個,使用了ScalaMp的並行版本比原串行快了6,7倍左右。