# Concurrent and Rapid Dictionary contributed by <`f74034067`>, <`catpig1630`>, <`rex662624`>, <`e94046165`> ## 預期目標 * 基於 [prefix search](https://hackmd.io/giJnGUwMQGqS9bNcLcF1Lw),導入 Bloom Filter ,實作快速字串搜尋機制。如果字串根本不存在於資料庫中,我們根本不需走訪 ternary search tree; * 需要支援多個 thread ,讓多個使用者能同時查字典; * 注意處理某個使用者新增 word 的同時其他使用者也要可以同時搜尋; ## 何謂 Bloom Filter 當我們要在大量的資料中搜尋一個字串時,最簡單的方式就是每一個走過一次,這樣時間複雜度會是 O(n) 。 而 Bloom Filter 就是利用一個 n bits 的 table ,不走訪資料結構就能透過這個 table 預測字串是不是在這個資料結構內。而這樣時間複雜度就變為了 O(1)。 這樣的應用其實很常見。例如在社群網站 facebook ,在搜尋欄鍵入名字時,能夠在20億註冊用戶中很快的找到結果,甚至是根據與使用者的關聯度排序。 ### 實作方式 首先,建立一個 n bits 的 table,並將每個 bit 初始化為 0。 我們所有的字串 set 表示為 S = {x~1~ ,x~2~ ,x~3~...,x~n~ }, Bloom Filter 會使用 k 個不同的 hash function,每個 hash function 轉換後的範圍都是 0 ~ n-1 (為了能夠對應上面建立的 n bits table)。而對每個 S 內的 element x~i~,都需要經過 k 個 hash function ,一一轉換成 k 個 index。轉換過後,table 上的這 k 個 index 的值就必須設定為 1。(注意到所以這裡可能會有同一個 index 被多次設定為 1 的狀況) Bloom Filter 這樣的機制,是會有錯誤機率的。若今天想要找一個字串 x 在不在 S 中,這樣的資料結構只能保證某個 x~1~ 一定不在 S中,但沒辦法 100% 確定某個 x~2~一定在 S 中。因為會有誤判( false positive )的可能。 此外,資料只能夠新增,而不能夠刪除,試想今天有兩個 string x~1~ , x~2~經過某個 hash function h~i~ 轉換後的結果 h~i~(x~1~)=h~i~(x~2~) ,若今天要刪除x~1~ 而把 table 中 set 的 1 改為 0,豈不是連 x~2~ 都受到影響了 ? ### 錯誤率計算 首先假設我們的所有字串 set S 裡面有 n 個字串, hash 總共有 k 個,Bloom Filter 的 table 總共 m bits。我們會判斷一個字串存在於 S 內,是看經過轉換後的每個 bits 都被 set 了,我們就會說可能這個字串在 S 內。但試想若是其實這個字串不在 S 內,但是其他的 a b c 等等字串經過轉換後的 index ,剛好把我們的目標字串轉換後的 index 包含了,就造成了誤判這個字串在 S 內的情況。 如剛剛所述,Bloom Filter 有錯誤機率,而我們開發程式時可能必須把錯誤機率回報給使用者,因此以下將證明錯誤率公式。 當我們把 S 內的所有字串,每一個由 k 個 hash 轉換成 index 並把 table[index] 設為 1,而全部轉完後, table 中某個 bits 仍然是 0 的機率是 $(1-\dfrac{1}{m})^{kn}$ 。 其中$(1-\dfrac{1}{m})$是每次 hash 轉換後,table 的某個 bits 仍然是 0 的機率 。因為我們把 hash 轉換後到每個 index (而這個 index 會被 set )的機率視為相等(每人$\dfrac{1}{m}$),所以用 1 減掉即為不會被 set 的機率。我們總共需要做 kn 次 hash,所以就得到答案$(1-\dfrac{1}{m})^{kn}$ 。 由$(1-\dfrac{1}{m})^{m}≈e^{-1}$特性,$(1-\dfrac{1}{m})^{kn}=((1-\dfrac{1}{m})^{m})^{\frac{kn}{m}}≈(e^{-1})^{\frac{kn}{m}}≈e^{-\frac{kn}{m}}$ 以上為全部的字串轉完後某個 bit 還沒有被 set 的機率。 因此誤判的機率=全部經由 k 個 hash 轉完後的 k bit 已經被其他人 set 的機率: 轉完後某個 bit 被 set機率 : $1-e^{-\frac{kn}{m}}$,因此某 k bit 被 set機率 : ==$(1-e^{-\frac{kn}{m}})^k$== ### 如何選參數值( k (多少個不同 hash )、 m ( table size 的大小)) **如何選 k 值(要多少個不同hash):** 我們必須讓錯誤率是最小值,因此必須讓$(1-e^{-\frac{kn}{m}})^k$的值是最小。先把原式改寫成$(e^{k\ ln(1-e^{-\frac{kn}{m}})})$,我們只要使${k\ ln(1-e^{-\frac{kn}{m}})}$最小,原式就是最小值。可以看出當 $e^{-\frac{kn}{m}}=\dfrac{1}{2}$ 時,會達到最小值。因此 ==$k=ln2\dfrac{m}{n}$== 即能達到最小值。 [Bloom Filter calculator](https://hur.st/bloomfilter/?n=5000&p=3&m=&k=45)、[Bloom Filter calculator 2](https://krisives.github.io/bloom-calculator/) 這兩個網站可以透過設定自己要的錯誤率與資料的多寡,得到 k 和 m 值。 ### 在 prefix search 導入 bloom filter 機制 [這是暫時找到的 dictionary file](https://stackoverflow.com/questions/4456446/dictionary-text-file),裡面總共有36922個詞彙 **(太小,後面必須找更大的詞彙集,否則推測難以體現效能)**。透過上面的網址知道當我們容許5%錯誤(0.05)時,table size=230217,k=4。 看懂[ bloom filter c code ](http://sircmpwn.github.io/2016/04/12/How-to-write-a-better-bloom-filter-in-C.html),並試著導入原來程式。並先用原來的 cities.txt 檔案做測試以評估正確性。 首先,這裡選用了兩種 hash ,`jenkins` 和`djb2`,另外 murmur3 hash 也是一個不錯的選擇,這裡主要是選用夠 uniform 的即可。 ```clike= unsigned int djb2(const void *_str) { const char *str = _str; unsigned int hash = 5381; char c; while ((c = *str++)) { hash = ((hash << 5) + hash) + c; } return hash; } unsigned int jenkins(const void *_str) { const char *key = _str; unsigned int hash=0; while (*key) { hash += *key; hash += (hash << 10); hash ^= (hash >> 6); key++; } hash += (hash << 3); hash ^= (hash >> 11); hash += (hash << 15); return hash; } ``` 再來宣告我們的 Bloom filter 主體,這裡是用一個 structure 包住,裡面共有三個元素: `func` 是用來把我們的 hash function 做成一個 list 串起來,使用這個方法的用意是方便擴充。若我們今天要加進第三個 hash function,只要串進 list ,後面在實做時就會走訪 list 內的所有 function。 `bits` 是我們的 filter 主體,也就是 table 。 size 則是 table 的大小。 ```clike= typedef unsigned int (*hash_function)(const void *data);//宣告成function pointer形式 typedef struct bloom_filter * bloom_t; struct bloom_hash { hash_function func; struct bloom_hash *next; }; struct bloom_filter { struct bloom_hash *func; void *bits; size_t size; }; ``` 以下是 bloom filter 實做主體,以下是主要的程式碼: 首先在 `bloom_create` 的部份,可以看到我們把兩個 hash function 透過`bloom_add_hash` 加入了結構中。 再來 `bloom_add` 裡面是透過對目標字串 `item` 套用 hash ,並把 hash 產生的結果 module size (第20行),這樣才會符合 table size 。最後第21行把產生的 bit 設定為 1 。 最後`bloom_test`就是去檢驗使用者輸入的 `item` 是不是在結構當中。與 add大致上都相等,只是把 bit 設定為1的部份改為檢驗是不是 1,若是任一個 bit 不是 1 ,我們就可以說這個結構裡面沒有這個字串。 這樣的實做,讓我們可以在 bloom_create 初始化完,接下來要加入字串就呼叫 bloom_add ,要檢驗字串就呼叫 bloom_test,不需要其他的操作。 ```clike= bloom_t bloom_create(size_t size) { bloom_t res = calloc(1, sizeof(struct bloom_filter)); res->size = size; res->bits = malloc(size); //加入hash function bloom_add_hash(res, djb2); bloom_add_hash(res, jenkins); return res; } void bloom_add(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; bits[hash] = 1 ; h = h->next; } } bool bloom_test(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size ; if (!(bits[hash])) { return false; } h = h->next; } return true; } ``` 再來是 main 改的部份(這裡是去修改 test_ref.c檔案): 這裡需要改的地方是 add 和 find 功能 (雖然 delete 其實會失去效用,如同文內`何謂 Bloom Filter`-`實做方式`所說,但程式碼並未刪除。) 首先 find 功能修改: ```clike= ... t1 = tvgetf(); //用bloom filter去判斷是否在 tree 內 if (bloom_test(bloom,word)==1) { //version1:bloom filter偵測到在裡面就不走下去了 t2 = tvgetf(); printf(" Bloomfilter found %s in %.6f sec.\n",word, t2 - t1); printf(" Probability of false positives:%lf\n",pow(1 - exp(-(double)HashNumber /(double) ((double)TableSize /(double) idx)), HashNumber)); // version2:bloom filter偵測到在裡面,就去走tree(防止偵測錯誤) t1 = tvgetf(); res = tst_search(root, word); t2 = tvgetf(); if(res) printf(" ----------\n Tree found %s in %.6f sec.\n", (char *) res, t2- t1); else printf(" ----------\n %s not found by tree.\n", word); } else printf(" %s not found by bloom filter.\n", word); break; ``` 由以上程式碼可以看到,第11行會先用 bloom filter 去判斷是否在結構內,如果不在的話就不用走訪,直接進入 else 部份。如果在的話,我先印出 bloom filter 找到的時間,還有錯誤機率,然後真的去走訪看是不是在 tree 裡面。 而根據我的參數是 TableSize 5000000(五百萬),hashNumber=2(總共兩個hash),及用原來的 cities 數量為 259112 ,算出來(理論上)錯誤率約為 0.009693 ,也就是不到 1%。 以下是 add 的部份: ```clike= ... /* FIXME: insert reference to each string */ if(bloom_test(bloom,Top)==1)//已經被filter偵測存在,不要走tree res=NULL; else//否則就去走訪tree加入,並加入bloom filter { bloom_add(bloom,Top); res = tst_ins_del(&root, &p, INS, REF); } t2 = tvgetf(); if (res) { idx++; Top += (strlen(Top) + 1); printf(" %s - inserted in %.10f sec. (%d words in tree)\n",(char *) res, t2 - t1, idx); } else printf(" %s - already exists in list.\n", (char *) res); if(argc>1 && strcmp(argv[1],"--bench")==0)//a for auto goto quit; break; ``` 以上程式碼可以看到,第3行的部份會先偵測要加入的字串是否在結構中,若是回傳 1 表示 bloom filter 偵測在結構中,已經重複了。因此會把 res 設為 null,這樣下方第12行的判斷自然就不會成立。如果偵測字串不在結構中,會跑進第5行的 else 加入結構還有 bloom filter 當中。 至此,基本 bloom filter 已經導入完畢。 ## 導入 multi-threading 因為我們要支援多個使用者可以執行不同功能,所以我想必須要把程式分成 Server 端和 Client 端,一次開一個 Server ,就可以支援不同的 Client 連上。 首先主要的程式運作模式是,先開啟一個 server 程式,然後 server會透過 socket 去監聽有沒有 client 要來連,若是連上了,server 就用 thread 去服務這個 client ,然後 main thread 繼續監聽有沒有 client 。 因為所有 thread ,共享程式的 heap 部份,所以要查詢的資料存在 global variable ,讓所有 thread 共同存取。細部的功能部份,是透過 send 和 recv 一問一答,例如 server 去問 client 要什麼加入什麼字串,client 就回答。 以下將逐步講解程式碼: ### 用 Socket 讓 Server 能夠服務多個 Client 首先,我們讓 Server 和每個 Client ,透過 Socket 相連通訊。 #### Server 端 首先以下程式碼是在 Server 中,設定好 Socket 的 IP 、Port 等初始化動作。 ```clike= sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { printf("Fail to create a socket."); } //socket的連線 //提供socket訊息 struct sockaddr_in serverInfo,clientInfo; socklen_t addrlen = sizeof(clientInfo); bzero(&serverInfo,sizeof(serverInfo));//初始化 serverInfo.sin_family = PF_INET;//sockaddr_in為Ipv4結構 serverInfo.sin_addr.s_addr = inet_addr("127.0.0.1");//IP serverInfo.sin_port = htons(59487);//port //綁定server在socket上 bind(sockfd,(struct sockaddr *)&serverInfo,sizeof(serverInfo)); ``` 再來,放一個`while(1)`表示 main thread 不斷監聽有沒有 Client 要連上,第3行的 `listen()` 就是去查看有沒有 Client要連結上。 在第20行的部分用`accept()`,就會去真的接受這個 Client ,這裡把回傳的值儲存於`forClientSockfd`中,因為每個 Client 都有自己的通話管線,這裡就好像是管線的編號一樣,想傳訊息給這個 Client 就要透過這個 Socket 。 最後連上這個 Client 之後,就建立一個新 thread ,進去`thread_function()`中做各自的操作。並在第30行用 `thread_join` 表示等所有人做完再結束。(但是這裡其實是執行不到,因為 main thread 會不斷在 while 中監聽有沒有 client 要連,直到使用者按 ctrl+c )。 ```clike= while(1) { //監聽有沒有client來 listen(sockfd,10000); //增加新的client,要把舊的data移到新allocate的地方 int temp[num]; pthread_t ptemp[num]; int i; for(i=0; i<num; i++) { temp[i]=forClientSockfd[i];//先用temp存放剛剛的 ptemp[i]=thread[i]; } //重新allocate array(記得把num+1) forClientSockfd = malloc(sizeof(int)*(num+1)); thread = malloc(sizeof(pthread_t)*(num+1)); for(i=0; i<num; i++) { forClientSockfd[i]=temp[i];//還原剛剛備份的data thread[i]=ptemp[i]; } forClientSockfd[num] = accept(sockfd,(struct sockaddr*)&clientInfo,&addrlen); int ptr = num; if(pthread_create( &(thread[num]), NULL, thread_function,(void*)&ptr)!=0) { printf("thread create failed"); } threadid=thread[num]; num++; } pthread_join(threadid,NULL); ``` #### Client 端 Client 的部分就比較簡單,只要設定好之後,用 `connect()` 連上 Server即可。 ```clike= sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { printf("Fail to create a socket."); } //連線socket struct sockaddr_in info; bzero(&info,sizeof(info)); info.sin_family = PF_INET; info.sin_addr.s_addr = inet_addr("127.0.0.1"); info.sin_port = htons(59487); int err = connect(sockfd,(struct sockaddr *)&info,sizeof(info)); if(err==-1) { printf("Connection error"); } ``` ### 2.Multi-thread 上面 Client 連上 Server 之後,Server 會建立新的 Thread 去服務這個 Client,然後 Main thread 會繼續在 while 迴圈內看有沒有 Client 要連上。 以下演示 find 功能的運作,其他功能的設計都大同小異。 #### Server 端 以下是於 thread function 內的部分程式碼,也就是每個服務 Client 的新 thread 各自都要做以下的事,與 Client 做對答。 首先,將所有動作包在 while 裡面,每一輪 while 代表著一次完整的動作。第6行會先把上面2~5行要問 client 的問題傳送給他。然後第7行接收 client 要求作的選項。 例如 client 要做 f 選項,就會進到第11行。然後第14行 Server 再傳問題問 Client 要 find 什麼字串並在下一行接收。之後下面的動作就是做 Bloom filter 版本的 find ,並把結果回傳給Client。 ```clike= while(1) { sprintf(message,"%s","\nCommands:\na add word to the \ tree\nf find word in tree\ns search words matching \ prefix\nd delete word from the tree\nq quit, \ freeing all data\n\nchoice: "); send(forClientSockfd[localindex],message,sizeof(message),0);//把上面的message傳給client recv(forClientSockfd[localindex],inputBuffer,sizeof(inputBuffer),0);//receive client 要執行什麼選項 ...省略 else if(strcmp(inputBuffer,"f")==0) { char word[WRDMAX] = ""; sprintf(message,"%s","find word in tree:"); send(forClientSockfd[localindex],message,sizeof(message),0); recv(forClientSockfd[localindex],inputBuffer,sizeof(inputBuffer),0);//從client拿到要找的word printf("Get from thread %d :%s\n",localindex,inputBuffer); sprintf(word,"%s",inputBuffer); rmcrlf(word); t1 = tvgetf(); //用bloom filter去判斷是否在 tree 內 if (bloom_test(bloom,word)==1) {//如果bloom filter有找到 //version1:bloom filter偵測到在裡面就不走下去了 t2 = tvgetf(); sprintf(message," Bloomfilter found %s in %.6f sec.\n Probability of false positives:%lf\n" \ ,word, t2 - t1,pow(1 - exp(-(double)HashNumber /(double) ((double)TableSize /(double) idx)), HashNumber)); // version2:bloom filter偵測到在裡面,就去走tree(防止偵測錯誤) t1 = tvgetf(); res = tst_search(root, word); t2 = tvgetf(); if(res) { //如果bloom filter有找到且tree也有找到 //這裡因為要把字串收集在char * message裡面,所以用strcat串起來 char * temp =malloc(128); sprintf(temp," ----------\n Tree found %s in %.6f sec.\n", (char *) res, t2- t1); strcat(message,temp); free(temp); } else { //如果bloom filter有找到但是tree沒有找到 char * temp =malloc(128); sprintf(temp," ----------\n %s not found by tree.\n", word); strcat(message,temp); free(temp); } //把字串收集好後一次傳送,以免大量傳送造成的i/o負擔 send(forClientSockfd[localindex],message,sizeof(message),0); } else { //bloom filter沒找到 sprintf(message," %s not found by bloom filter.\n", word); send(forClientSockfd[localindex],message,sizeof(message),0); } } ``` #### Client 端 Client 端的部分,也是在 while 迴圈裏面繞,每一輪 while 就代表一次完整的對答,也就表示了做完一次完整的工作。 例如首先第2行會先接到 Server 詢問要做什麼指令,若這次我要做 find 指令,就會在第6行回答 server,server 這時就會回要 find 什麼字串(第16行接收 server 問題,並在第17行印出)。第19行就會回答 server 要加入的字串, server 做完一連串 find 動作後,Client 就會接收結果並印出來(第20 21行)。 ```clike= while(1) { recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//先接到要求的訊息 printf("%s",receiveMessage); scanf(" %s",message); send(sockfd,message,sizeof(message),0);//送出request if(strcmp(message,"a")==0) {//如果自己送出的request==a recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//接收add what word? printf("%s\n",receiveMessage); scanf(" %s",message); send(sockfd,message,sizeof(message),0);//送出要add什麼字 recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//接收是否add成功 printf("%s\n",receiveMessage); } else if (strcmp(message,"f")==0) { recv(sockfd,receiveMessage,sizeof(receiveMessage),0); printf("%s\n",receiveMessage); scanf(" %s",message); send(sockfd,message,sizeof(message),0);//送出要find什麼字 recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//接收find結果 printf("%s\n",receiveMessage); } ``` ## 將本來的城市檔案改成字典檔 ### 程式開發 這個部份需要做的改動是,在 client 用 find option,也就是找特定的字時,輸出有沒有找到這個字,如果有找到要連解釋一起輸出。另外也要改 add option ,改成加入字串的同時,也要鍵入這個字串的解釋。其他部份就不用改變。 讀檔時,把單字和解釋分開存,單字用 ternary tree 存,解釋用 array 儲存,而 ternary tree 的 leaf 會儲存這個單字解釋的index ```clike= while (fgets(line,1024,fp) != NULL) { if(line[0] == '\n') continue; //單字存入Top,解釋存入array sscanf(line, "%s %[^\t\n]", Top, explain[count]); char *p = Top; /* insert reference to each string */ //把array的index傳入做ternary tree的function裡 if (!tst_ins_del(&root, &p, INS, REF,count)) {//沒有insert成功 fprintf(stderr, "error: memory exhausted, tst_insert.\n"); fclose(fp); return 1; } else { //有insert 進資料結構,因此也要加入bloom filter bloom_add(bloom,Top); } //完成一個單字index+1 count ++; idx++; Top += (strlen(Top) + 1); } ``` find 單字時用一個全域變數儲存 find 的單字解釋的 index ,然後再一起把單字和解釋 send 給 client 端 ```clike= void *tst_search(const tst_node *p, const char *s) { const tst_node *curr = p; while (curr) { /* loop over each char in 's' */ int diff = *s - curr->key; /* calculate the difference */ if (diff == 0) { /* handle the equal case */ if (*s == 0){ /* if *s = curr->key = nul-char, 's' found */ explain_index = curr->index; /*儲存單字解釋的index return (void *) curr->eqkid; /* return pointer to 's' */ } s++; curr = curr->eqkid; } else if (diff < 0) /* handle the less than case */ curr = curr->lokid; else curr = curr->hikid; /* handle the greater than case */ } return NULL; } ``` ``` clike= sprintf(temp," ----------\n Tree found %s in %.6f sec.\n%s\n%s\n", (char *) res, t2- t1,(char *) res,explain[explain_index]); strcat(message,temp); ``` 至於 add option 是 client 端先傳送單字到 server 端,接下來等 user 打完解釋後把解釋送到 server 端 ``` clike= recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//接收add what word? printf("%s\n",receiveMessage); scanf(" %s",message); send(sockfd,message,sizeof(message),0);//送出要add什麼字 recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//接收what is it's explanation? printf("%s\n",receiveMessage); scanf(" %[^\t\n]",message); send(sockfd,message,sizeof(message),0);//送出字的解釋 recv(sockfd,receiveMessage,sizeof(receiveMessage),0);//接收是否add成功 printf("%s\n",receiveMessage); ``` 至於 server 就是把接收到的單字存到 ternary tree 中,而解釋存到 array 中 ### sizeof(pointer) v.s. sizeof(array) 在這裡我們忽略掉了一個基本的概念: 我們在傳送訊息時,是用 `send(forClientSockfd[localindex],message,sizeof(message),0);`,這裡的第二個和第三個 argument 分別是儲存訊息的字串,後面接的是要傳送出去的 byte 大小。 但我們發現最多只能傳出 8 byte 的訊息,是因為我們用 `char* message = malloc(1024);`來宣告,而 sizeof(message) ,就會是這個指標所佔的記憶體大小,也就是 8 byte。解決方式就是把宣告方式變成 `char message[1024]`。 這裡設計實驗釐清一下概念,考慮以下程式碼: ```clike= #include <stdio.h> #include <stdlib.h> struct str { int a; int b; int c; int d; int e; }; int main(void) { int * x = malloc(sizeof(int)*2); int y[2]; printf("sizeof(x): %d\n", sizeof(x)); printf("sizeof(y): %d\n", sizeof(y)); printf("sizeof(*x): %d\n", sizeof(*x)); printf("sizeof(*y): %d\n", sizeof(*y)); printf("---------------\n"); struct str *a = malloc(sizeof(struct str)*2); struct str b[2]; printf("sizeof(a): %d\n", sizeof(a)); printf("sizeof(b): %d\n", sizeof(b)); printf("sizeof(*a): %d\n", sizeof(*a)); printf("sizeof(*b): %d\n", sizeof(*b)); return 0; } ``` output: ``` sizeof(x): 8 sizeof(y): 8 sizeof(*x): 4 sizeof(*y): 4 --------------- sizeof(a): 8 sizeof(b): 40 sizeof(*a): 20 sizeof(*b): 20 ``` 首先 `sizeof(x)` 和 `sizeof(a)` 不難理解,是 pointer 的大小,也就是一個address,8byte。`sizeof(y)` 和 `sizeof(b)` ,因為他們是 array,所以是整個array 的記憶體大小。 接下來 `sizeof(*a)` 和 `sizeof(*x)`,會是指標指向的記憶體區塊之第一個 item 之型態大小。前者的第一個元素是一個 20 byte 的 structure ,而後者是一個 int(4Byte)。 最後 `sizeof(*b)` 和 `sizeof(*y)` ,也會是第一個元素大小,分別是 20 和 4 byte 。 ## thread safe ### 程式開發 因為有些 thread 會寫資訊進結構,有些 thread 會從結構中讀資料出來,要達成 thread safe 就要對資料做一些保護,我們用到的是 read write lock。pthread 本身就已經有 read write lock的功能,以下將介紹呼叫方式與行為。 首先,在 pthread 用 read write lock 的方式是: ```clike= pthread_rwlock_t tree_lock; int main(int argc, char **argv) { pthread_rwlock_init(&tree_lock, NULL); ...... pthread_rwlock_wrlock(&tree_lock); //這一行放要保護的資料結構 pthread_rwlock_unlock(&tree_lock); ...... pthread_rwlock_rdlock(&bloom_lock); //這一行放要保護的資料結構 pthread_rwlock_unlock(&bloom_lock); ``` 首先第一行是宣告 lock ,在第五行做初始化,然後第7行如果要用 write lock ,就要呼叫`pthread_rwlock_wrlock()`。若是要用 read lock ,就要呼叫`pthread_rwlock_rdlock()`。 而 read write lock 的行為是: 如果有 writer 要寫入資料到資料結構,就要用 write lock 鎖住,這時候任何其他 thread 都不能再進來使用這個資料結構,因為若是其他 thread 再 access ,就可能有錯誤。而若是 reader 要寫入資料結構,所有的 writer 都不能進來使用,但是其他 reader 還是可以使用資料結構,因為他們只有讀值,對資料結構沒有寫入。 :::warning 參考 [Concurrent Hash Tables](https://parsa.epfl.ch/courses/cs206/slides/L09-Hashs.pdf),提出正確的 read/write lock 機制 :notes: jserv ::: ### 不同的 lock 機制 #### Coarse-Grained Locking ![](https://i.imgur.com/1SmTRW0.png =500x) 以上是示意圖,Coarse-Grained(粗質)的意思是,整個資料結構共享一個 lock,例如圖中綠色的 CPU(或是thread)要修改其中的某個node,就要把整個資料結構都 lock 住,但這會導致平行化毫無意義,因為其他的 thread 要等這個 thread 所有操作完成才能再操作。 #### Fine-grained Locking ![](https://i.imgur.com/PUD9Kce.png =600x) Fine-grained 就可以解決上面的問題,因為一個元素自己本身就用一個 lock(實作方法是把本來的資料結構多一個 lock 元素,每個 node 自己有專屬的lock)。如上圖,如果紅色的 thread 要用 node a 和 head 的話,就獲取他們兩個的lock就好,因此這樣藍色的 thread 如果還要對node c、node d 做操作,就不需要等紅色 thread 操作完。 #### Lock-free Lists 這種避免資料錯誤的方法,沒有用到 lock ,所有的 thread 還是能同時存取所有資料,但還是能保證資料正確。 一個典型的方法是: [CAS](https://en.wikipedia.org/wiki/Compare-and-swap)(compare-and-swap) 實作程式碼如下: ```clike= bool compare_and_swap(int *accum, int *dest, int newval) { if (*accum == *dest) { *dest = newval; return true; } else { *accum = *dest; return false; } } ...... //使用: int accum =目前的 data//先備份一次,後來的備份都會在第7行的部分 while(!compare_and_swap(暫存的變數 address,要修改的 data address,修改後的值)); ``` compare_and_swap() 的 parameter,accum 是備份的舊數據,dest 是要修改的數據位置,newval 是新值。 主要的概念為,當備份的舊 data,與要修改的 data 進行比較時,如果相等(進入第4行),則證明共享數據沒有被修改,替換成新值,然後繼續往下運行。如果不相等(第7行),說明共享數據已經被修改,放棄已經所做的操作,然後重新執行剛才的操作,這裡注意到第7行要重新備份舊值,下次再執行這個 function 才會是正確的。 這裡注意到 function 裡面都要是 atomic 做完,因為試想一個 thread1 通過 if,另一個 thread 也在這時通過 if(因為這時 thread1 還沒修改值)並把值修改了,然後 thread1 這時才修改值。就造成了 thread1 先進到 if, thread2 後來才進入,但是最後修改的卻是 thread1 。 ## 目前程式已知問題: 1. **情境:** 如果 server 開啟後,有多個 client 連上的情況下,若是某一個 client 沒有正常用 q 離開而是直接用`ctrl+c`離開,則 server 也會直接關掉。就算再次打開 server 與 client ,client 都會 connect error。這個情況就算用 make clean 再重新 make 也無法解決。但是去加上一些無關緊要的 code ,例如在某行多加上一個分號,然後重新 make ,就可以恢復運作。(或是隔一段時間再開 server ?) **解決方法:** 需要加上 signal handler 去改變 client 的 ctrl+C 行為,讓它可以先關掉 socket 再正常離開。 2. **情境:** 連續 send ,例如 server send 一次,沒有 receive動作就直接在 send 一次 ,會導致傳出去的字串有問題。 **解決方法:(已解決)** 在連續 send 中間加入一個 dummy 的 receive ,但應該要去找有什麼可以讓 socket buffer flush 的方式。 3. **情境:** prefix search 因為傳送的資料量可能會非常大(例如我們想找以 R 為開頭的所有字),這樣要傳給 client 的 buffer size只設 1024 可能會不夠,但是宣告太大,例如測試過宣告 5120 的陣列,在 free 的時候會造成越界導致 core dumped 。不 free 又將會造成 memory leak 。 **解決方法:(已解決)**:分段傳送,buffer快滿時就先傳出去 1.server 端:在尋找字串的同時,在迴圈內同時加上`if(strlen(message)>999)`就去 send ,也就是快要超過 1024 時就先把目前累積的資料的傳出去,並把陣列清空重新累積,然後還要加上如果是找到最後一輪的話,就算沒有快超過 buffer 也要把資料傳出去。也就是變成`if(strlen(message)>999||i==sidx-1)` 2.client 端:用一個while迴圈一直接收訊息,如果不是接收到`"Server send over"`就繼續接收。所以相對的剛才的 server 要在迴圈跑完,找到所有的字串後送出`"Server send over"`。 4. **情境:** 發現本來 prefix search 程式就有的問題,如果要尋找以"A"為prefix的字串會 core dumped,這應該是因為有一個變數LMAX設成 1024 導致最多只能傳 suggest[0]~suggest[1023] 。 ## 改進方向 * 比照 [linux-list](https://github.com/sysprog21/linux-list),允許透過 `make check` 執行內建的 unit test 或 test suite * 引入 stress test,檢驗各種邊界狀況 * 透過 [mutrace](https://github.com/leventov/mutrace) 一類的工具追蹤 lock contention: http://0pointer.de/blog/projects/mutrace.html * client-server 程式需要檢查連線狀況並給予清楚的訊息 (如使用 `perror`),當首度連線失敗時,client 可嘗試用 fork + execve 建立 server process 以重新連線 ## reference * [StarDict](http://ossacc.moe.edu.tw/uploads/datafile/ezgo7_win/ezgo_win/_stardict.html) * [Bloom Filter 影片解說](https://www.youtube.com/watch?v=bEmBh1HtYrw) * [Bloom Filter](https://blog.csdn.net/jiaomeng/article/details/1495500) * [dictionary file 下載處](https://stackoverflow.com/questions/4456446/dictionary-text-file) * [murmur3 hash](https://github.com/PeterScott/murmur3) * [Jenkins' One-at-a-Time hash](https://stackoverflow.com/questions/114085/fast-string-hashing-algorithm-with-low-collision-rates-with-32-bit-integer) * [sizeof](http://applezu.netdpi.net/2014/02/blog-post_22.html) * [不同 read write lock 的圖片](http://fileadmin.cs.lth.se/cs/education/eda015f/2013/herlihy4-5-presentation.pdf)