基於MPI的文件分類並行程式設計(C)

RKTeddy發表於2017-10-08

國慶時間有點多,但是被父母四天的探望擠掉了一半,算了算自己身上還有三個專案,兩個比賽進行中…幸好沒有偷懶跑回家度假…

原本計組老師讓我國慶期間粗略看完《MPI與OpenMP並行程式設計:C語言版》這本年代略微有點久遠的書,浪到七號的我想想覺得有點困難…

畢竟像我這種沒有自制力的人,國慶剛放假就被師兄強行安利了一波PyTorch,怎麼甘心不看嘛!而且自從看了師兄推薦的資源,一天比一天仙~(這裡mark一下https://morvanzhou.github.io/

這算是我第一次寫技術部落格,以後會漸漸把以前寫過的東西放上來,不管有沒有人看就當作對自己的回顧。


這次的任務是設計一個文件分類程式:

    “讀入一個關鍵詞字典,找到一批待處理的文字文件,讀取它們的內容,為每個文件產生一個向量,並最終將這些向量儲存起來”

計組老師所帶專案的核心技術,但是感覺並沒有很難。

首先這個程式採用的是稱為“管理者/工人模式”的方法,即:

    管理者:負責為工人分配任務、接收任務結果並將所有結果整合儲存起來

    工人:讀取字典、為分配到的文件構造特徵向量,返回任務結果。

涉及到平行計算的優化重點在於:

    1.將工人程式分為兩部分,一部分只包含一個工人程式(工人0),負責開啟字典檔案,讀取內容並廣播給其他工人,將字典大小返回給管理程式;另一部分包含其他所有工人程式,負責執行任務。(這個方法只有在平行計算機內部的廣播頻寬比平行計算機與伺服器之間的頻寬要大時效果較好,若廣播頻寬較小,則不需要分兩部分,由所有工人程式直接讀取詞典)

    2.工人0:將讀取詞典與通知管理者同時進行。

    3.管理者:將搜尋目錄結構與從工人0接收一個訊息同時進行,涉及到非阻塞通訊:點到點的訊息傳遞屬於阻塞操作,在訊息到達目標緩衝區之前,函式不返回。阻塞同信將通訊分為兩部分操作,分別涉及到MPI_Isend(MPI_Irecv)和 MPI_Wait函式。MPI_Isend(MPI_Irecv)僅僅初始化一部分通訊操作,因此可以同時進行一些不同的計算或輸入輸出操作,最後再使用MPI_Wait接受訊息,完成通訊。

    工人0在使用非阻塞操作傳送詞典大小時,管理者可以呼叫接收操作直接接收工人0的訊息(若使用阻塞通訊,工人在傳送訊息時將寫入一個臨時的系統緩衝區,直到管理者從系統緩衝區讀取訊息),這樣做一是可以減少一次複製操作從而節省時間和記憶體(訊息長度越長,非阻塞接收的好處就越大),二是允許專門的通訊協處理器來完成通訊相關操作,而將處理器專門用來處理設計本地資料的計算操作。

順便解釋一下幾個需要用到的MPI函式:


附完整程式(Visual Studio 2017下,不得已用了幾個安全函式 ( 帶_s的那種 ),不能執行的自行修改咯):

#include "mpi.h"
#include "string.h"
#include "sys/stat.h"
#include "memory.h"
#include "io.h"
#include "stdio.h"
#include "stdlib.h"

#define uchar unsigned char
#define uint unsigned int

//訊息型別標籤
#define DICT_SIZE_MSG 0    // 字典大小標籤
#define FILE_NAME_MSG 1    // 文件名字標籤
#define VECTOR_MSG 2    // 特徵向量標籤
#define EMPTY_MSG 3    // 空訊息標籤

//引數型別標籤
#define DIR_ARG 1    // 路徑引數
#define DICT_ARG 2    // 字典引數
#define RES_ARG 3    // 結果引數

#define HASH_SIZE 100

// 鏈地址法解決雜湊碼衝突

int matrix[5][5] = { 0 };

typedef struct _node
{
    char *data;    // 雜湊節點所存字串
    int num;    // 表示在字典中的序號
    struct _node *next;    // 連結串列的下個節點
}node;

typedef struct _hash_table
{
    node* value;    // 多個雜湊節點構成雜湊表
}hash_table;

// 雜湊函式
int hash_func(char *string)
{
    int len = strlen(string);
    int sum = 0;
    int h = 0;
    int a = 0;
    char *ptr = string;
    while (ptr - string < len)
    {
        a = *(ptr++)*(ptr - string);
        sum += sum^a;
        h += a;
    }
    return (((sum) << 16) | h) % HASH_SIZE;
}

// 插入一個字串到雜湊表中
void hash_insert(char *string, hash_table *hash_tbl, int num)
{
    int key_value = hash_func(string);
    node *nde = &(hash_tbl->value[key_value]);

    // 搜尋連結串列直到節點為空
    while (nde->data != NULL)
    {
        nde = nde->next;
        printf("Position was used, turn to next.\n");
    }

    // 插入字串
    nde->data = (char*)malloc(strlen(string) * sizeof(char));
    strcpy_s(nde->data, strlen(string) + 1, string);
    nde->num = num;
    nde->next = (node*)malloc(sizeof(node));

    // 置下一個節點為空
    nde->next->data = NULL;
    nde->next->next = NULL;
    printf("\"%s\" Insert success! Key value: %d\n", string, key_value);
}

// 根據緩衝區資料建立雜湊表
void build_hash_table(char *buffer, hash_table *hash_tbl, int *words_cnt)
{
    char *string = (char*)malloc(50 * sizeof(char));
    char *ptr;
    int i = 0;
    *words_cnt = 0;
    ptr = buffer;

    // 以空格為分界 以換行符為結尾讀取字串並插入雜湊表中
    while (*ptr != '\0')
    {
        i = 0;

        string = (char*)malloc(50 * sizeof(char));
        memset(string, 0, 50);
        while (*ptr != ' ')
        {
            string[i] = *ptr;
            i++;
            ptr++;
        }
        *words_cnt += 1;
        hash_insert(string, hash_tbl, *words_cnt);
        ptr++;
        /*if (string != NULL)
        {
            free(string);
            string = NULL;
        }*/
    }

    // 返回單詞總數
    printf("End building hash table.\n");
}

// 生成並初始化雜湊表 返回指向雜湊表的指標
hash_table *init_hash_table()
{
    int i;
    hash_table *hash_tbl = (hash_table*)malloc(sizeof(hash_table*));
    hash_tbl->value = (node*)malloc(HASH_SIZE * sizeof(node));

    for (i = 0; i < HASH_SIZE; i++)
    {
        hash_tbl->value[i].data = NULL;
        hash_tbl->value[i].next = NULL;
    }

    return hash_tbl;
}

// 查詢某個字串是否存在雜湊表中
char hash_exist(hash_table hash_tbl, char *string)
{
    int key_value = hash_func(string);
    node *nde = &(hash_tbl.value[key_value]);

    // 如果關鍵字處的連結串列第一個節點便為空 則不存在
    if (nde->data == NULL)
        return 0;

    // 與連結串列中的一個個節點進行比較
    else while (nde->next != NULL)
    {
        if (strcmp(nde->data, string) == 0)
            return nde->num;
        nde = nde->next;
    }

    // 找不到返回0
    return 0;
}

// 得到某路徑下的所有txt檔名
void get_names(char *path, char ***file_name, int *cnt)
{
    int i;
    int count = 0;
    char **name = *file_name;
    long file;
    struct _finddata_t find;

    _chdir(path);
    file = _findfirst("*.txt", &find);    // 得到第一個txt檔案控制程式碼
    if ( file == -1)    // 不存在txt檔案報錯
    {
        printf("Nothing in this directory!\n");
        exit(0);
    }
    else    // 讀取第一個txt檔案的名字並寫入file_name
    {
        name[count] = (char*)malloc(strlen(find.name) + strlen(path) + 1);
        snprintf(name[count], (strlen(find.name) + strlen(path) + 1), "%s%s", path, find.name);
        count++;
    }
    while (_findnext(file, &find) == 0)    // 繼續搜尋
    {
        name[count] = (char*)malloc(strlen(find.name) + strlen(path) + 1);
        snprintf(name[count], (strlen(find.name) + strlen(path)) + 1, "%s%s", path, find.name);
        count++;
    }

    _findclose(file);

    *cnt =  count;
}

// 讀取字典txt中的所有單詞存在content中並返回字典總大小
void read_dictionary(char *path, char **content, long *len)
{
    char chars;
    int i = 0;
    *len = 0;
    FILE *f;

    int err = fopen_s(&f, path, "r");
    if (err != 0)
    {
        printf("No such file in this directory!\n");
        return;
    }

    // 先搜尋一遍得到字典總大小
    printf("Scanning dictionary...\n");
    while ((chars = fgetc(f)) != EOF)
        *len += 1;
    *content = (char*)malloc(*len*sizeof(char));
    fclose(f);

    // 將單詞全部存在content中
    err = fopen_s(&f, path, "r");
    printf("Reading dictionary...\n");
    fgets(*content, *len, f);
    printf("Complete!\n");
    fclose(f);
}

// 將文件中的單詞與字典單詞進行比較得到特徵向量
void make_profile(char *path, hash_table hash_tbl, uchar **profile)
{
    char string[50];
    memset(string, 0, 50);
    char chars;
    int num;
    int i = 0;
    FILE *f;
    i = 0;

    int err = fopen_s(&f, path, "r");
    if (err != 0)
    {
        printf("No such file in this directory!\n");
        return;
    }

    while ((chars = fgetc(f)) != EOF)
    {
        if (chars == ',' || chars == '.' || chars == '!' || chars == '\?' || chars == ':' || chars == '\"' || chars == '\'')    // 如果為標點符號則忽略
            continue;
        if (chars == ' ')    //以空格為分界
        {
            printf("%s: %d\n", string, hash_exist(hash_tbl, string));

            if (num = hash_exist(hash_tbl, string))    //如果單詞存在於雜湊表中則寫入特徵向量
                (*profile)[num - 1] += 1;

            i = 0;
            memset(string, 0, 50);
        }
        else    // 遇到空格前的字元存在字串緩衝區裡
        {
            string[i] = chars;
            i++;
        }
    }

    fclose(f);
}

// 建立空矩陣並存在vector中
void build_2d_array(int file_cnt, int words_cnt, uchar ***vector)
{
    int i, j;
    *vector = (uchar**)malloc(file_cnt * sizeof(uchar *));

    for (i = 0; i < file_cnt; i++)
    {
        (*vector)[i] = (uchar*)malloc(words_cnt * sizeof(uchar));
        for (j = 0; j < words_cnt; j++)
            (*vector)[i][j] = 0;
    }
}

// 將結果寫入檔案
void write_profiles(char *path, int file_cnt, int words_cnt, char** file_name, uchar **vector)
{   
    int i, j;
    _chdir(path);
    FILE *f;
    int err = fopen_s(&f, "result.txt", "w");    // 建立並開啟結果檔案

    if (err != 0)    // 無法開啟路徑
    {
        printf("No such file in this directory!\n");
        return;
    }

    for (i = 0; i < file_cnt; i++)    // 寫入矩陣
    {
        fprintf(f, "Document%d :", i);
        for (j = 0; j < words_cnt; j++)
        {
            fprintf(f, "%d ", vector[i][j]);
        }
        fprintf(f, "\n");
    }

    fclose(f);
}

void manager(int argc, char* argv[], int p)
{
    int i, j;
    int words_cnt;
    int assign_cnt;    // 已分配的文件
    int *assigned;    // 分配的文件指標
    uchar *buffer;    // 特徵向量
    int dict_size;    // 字典大小
    int file_cnt;    // 文件總數
    char **file_name = (char**)malloc(sizeof(char**));    // 文件名
    MPI_Request pending;    // MPI請求的控制程式碼
    int src;    // 訊息的來源
    MPI_Status status;    // 訊息狀態
    int tag;    // 訊息標籤
    int terminated;    // 已經完成的工人程式
    uchar **vector;    // 存放特徵向量的文件

    printf("\n\nManager:\n");

    MPI_Irecv(&dict_size, 1, MPI_INT, MPI_ANY_SOURCE, DICT_SIZE_MSG, MPI_COMM_WORLD, &pending);    // 即將接收字典大小
    MPI_Irecv(&words_cnt, 1, MPI_INT, MPI_ANY_SOURCE, 4, MPI_COMM_WORLD, &pending);    // 即將接收字典個數
    get_names(argv[DIR_ARG], &file_name, &file_cnt);    // 搜尋目錄, 並得到所有文件名
    printf("File_cnt: %d\n", file_cnt);
    for (i = 0; i < file_cnt; i++)
        printf("File_name: %s\n", file_name[i]);
    MPI_Wait(&pending, &status);    // 接收訊息
    printf("Dict_size: %d\n", dict_size);
    printf("Words_cnt: %d\n", words_cnt);
    buffer = (uchar*)malloc(words_cnt * sizeof(MPI_UNSIGNED_CHAR));    //申請記憶體空間準備接收特徵向量
    build_2d_array(file_cnt, words_cnt, &vector);    // 建立存放特徵向量的二維陣列
    terminated = 0;
    assign_cnt = 0;
    assigned = (int*)malloc(p * sizeof(int));
    while (terminated < (p - 1))
    {
        MPI_Recv(buffer, dict_size, MPI_UNSIGNED_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);    // 從工人程式接收訊息
        src = status.MPI_SOURCE;
        tag = status.MPI_TAG;
        if (tag == VECTOR_MSG)    // 如果是包含特徵向量的訊息
        {
            for (i = 0; i < words_cnt; i++)
            {
                vector[assigned[src]][i] = buffer[i];    // 寫入特徵向量 
            }
            printf("\n");
        }

        if (assign_cnt < file_cnt)    // 如果已分配的文件小於文件總數
        {
            MPI_Send(file_name[assign_cnt], strlen(file_name[assign_cnt]) + 1, MPI_CHAR, src, FILE_NAME_MSG, MPI_COMM_WORLD);    // 傳送下一個文件名
            printf("File%d: %s\n", assign_cnt, file_name[assign_cnt]);
            assigned[src] = assign_cnt;    // 標記文件
            assign_cnt++;
        }
        else    //如果文件已分配完
        {
            MPI_Send(NULL, 0, MPI_CHAR, src, FILE_NAME_MSG, MPI_COMM_WORLD);    // 傳送結束訊息
            terminated++;
        }
    }
    write_profiles(argv[RES_ARG], file_cnt, words_cnt, file_name, vector);    // 將特徵向量寫入檔案
    for (i = 0; i < file_cnt; i++)
        free(file_name[i]);
    free(file_name);
    free(buffer);
    free(assigned);
}

void worker(int argc, char *argv[], MPI_Comm worker_comm)
{
    int i;
    int words_cnt;
    char *buffer = NULL;     // 字典緩衝區
    hash_table hash_tbl = *init_hash_table();   // 單詞的雜湊表
    long dict_size;    // 字典大小
    char *name;    // 文件內容
    int name_len;    // 文件大小
    MPI_Request pending;    // 傳送的控制程式碼
    uchar *profile;   // 文件的特徵向量
    MPI_Status status;    // 通訊狀態
    int worker_id;    // 程式號

    MPI_Comm_rank(worker_comm, &worker_id);    // 獲取當前程式在通訊域中的編號
    printf("\n\nWorker %d:\n", worker_id);

    if (!worker_id)
    {
        read_dictionary(argv[DICT_ARG], &buffer, &dict_size);    // 如果為工人0則讀取字典
        MPI_Isend(&dict_size, 1, MPI_INT, 0, DICT_SIZE_MSG, MPI_COMM_WORLD, &pending);    // 傳送字典大小
        MPI_Wait(&pending, &status);
    }
    MPI_Bcast(&dict_size, 1, MPI_LONG, 0, worker_comm);    // 廣播字典大小
    if (worker_id)
        buffer = (char*)malloc(dict_size);    // 如果非工人0則為緩衝區申請記憶體空間
    MPI_Bcast(buffer, dict_size, MPI_CHAR, 0, worker_comm);    // 得到字典字串存入緩衝區
    printf("Buffer: %s\n", buffer);
    build_hash_table(buffer, &hash_tbl, &words_cnt);    // 建立雜湊表
    if (!worker_id)
    {
        MPI_Isend(&words_cnt, 1, MPI_INT, 0, 4, MPI_COMM_WORLD, &pending);    // 傳送字典大小
        MPI_Wait(&pending, &status);
    }
    profile = (uchar*)malloc(words_cnt * sizeof(uchar));
    for (i = 0; i < words_cnt; i++)
        profile[i] = 0;
    MPI_Send(NULL, 0, MPI_UNSIGNED_CHAR, 0, EMPTY_MSG, MPI_COMM_WORLD);    // 傳送空訊息表示準備就緒

    for (;;)
    {
        MPI_Probe(0, FILE_NAME_MSG, MPI_COMM_WORLD, &status);    // 得到接收的文件訊息狀態
        MPI_Get_count(&status, MPI_CHAR, &name_len);    // 則從訊息狀態中獲取文件內容長度
        if (!name_len) break;    // 如果沒有更多工作則退出迴圈

        name = (char*)malloc(name_len);    // 為文件內容申請記憶體
        MPI_Recv(name, name_len, MPI_CHAR, 0, FILE_NAME_MSG, MPI_COMM_WORLD, &status);
        make_profile(name, hash_tbl, &profile);    //得到特徵向量
        free(name);    //釋放文件記憶體
        MPI_Send(profile, dict_size, MPI_UNSIGNED_CHAR, 0, VECTOR_MSG, MPI_COMM_WORLD);
        printf("Profile: ");
        for (i = 0; i < words_cnt; i++)
            printf("%d ", profile[i]);
        printf("\n");
    }
    free(buffer);
    free(profile);
}

int main(int argc, char *argv[])
{
    int id;
    int p; 
    MPI_Comm worker_comm = MPI_COMM_WORLD;

    MPI_Comm manager_comm = MPI_COMM_WORLD;

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &id);    //返回當前程式在通訊域中的程式號
    MPI_Comm_size(MPI_COMM_WORLD, &p);    //返回通訊域的程式數

    if (argc != 4)    //傳入引數錯誤處理
    {
        if (!id)
        {
            printf("Program needs three arguments:\n");
            printf("%s <dir> <dict> <results> \n", argv[0]);
        }
    }
    else if (p < 2)    //少於兩個程式錯誤處理
    {
        printf("Program needs at least two process\n");
    }
    else    //分離通訊域
    {
        if (!id)
        {
            MPI_Comm_split(MPI_COMM_WORLD, MPI_UNDEFINED, id, &manager_comm);
            manager(argc, argv, p);
        }
        else
        {
            MPI_Comm_split(MPI_COMM_WORLD, 0, id, &worker_comm);
            worker(argc, argv, worker_comm);
        }
    }

    MPI_Finalize();
    return 0;
}

都是一些很簡單的MPI函式操作,理解了流程就容易很多。

滾去彈琴了。

相關文章