contributed by <f74034067
>, <catpig1630
>, <rex662624
>, <e94046165
>
當我們要在大量的資料中搜尋一個字串時,最簡單的方式就是每一個走過一次,這樣時間複雜度會是 O(n) 。
而 Bloom Filter 就是利用一個 n bits 的 table ,不走訪資料結構就能透過這個 table 預測字串是不是在這個資料結構內。而這樣時間複雜度就變為了 O(1)。
這樣的應用其實很常見。例如在社群網站 facebook ,在搜尋欄鍵入名字時,能夠在20億註冊用戶中很快的找到結果,甚至是根據與使用者的關聯度排序。
首先,建立一個 n bits 的 table,並將每個 bit 初始化為 0。
我們所有的字串 set 表示為 S = {x1 ,x2 ,x3…,xn }, Bloom Filter 會使用 k 個不同的 hash function,每個 hash function 轉換後的範圍都是 0 ~ n-1 (為了能夠對應上面建立的 n bits table)。而對每個 S 內的 element xi,都需要經過 k 個 hash function ,一一轉換成 k 個 index。轉換過後,table 上的這 k 個 index 的值就必須設定為 1。(注意到所以這裡可能會有同一個 index 被多次設定為 1 的狀況)
Bloom Filter 這樣的機制,是會有錯誤機率的。若今天想要找一個字串 x 在不在 S 中,這樣的資料結構只能保證某個 x1 一定不在 S中,但沒辦法 100% 確定某個 x2一定在 S 中。因為會有誤判( false positive )的可能。
此外,資料只能夠新增,而不能夠刪除,試想今天有兩個 string x1 , x2經過某個 hash function hi 轉換後的結果 hi(x1)=hi(x2) ,若今天要刪除x1 而把 table 中 set 的 1 改為 0,豈不是連 x2 都受到影響了 ?
首先假設我們的所有字串 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 的機率是
其中
由
以上為全部的字串轉完後某個 bit 還沒有被 set 的機率。
因此誤判的機率=全部經由 k 個 hash 轉完後的 k bit 已經被其他人 set 的機率:
轉完後某個 bit 被 set機率 :
如何選 k 值(要多少個不同hash):
我們必須讓錯誤率是最小值,因此必須讓
Bloom Filter calculator、Bloom Filter calculator 2 這兩個網站可以透過設定自己要的錯誤率與資料的多寡,得到 k 和 m 值。
這是暫時找到的 dictionary file,裡面總共有36922個詞彙 (太小,後面必須找更大的詞彙集,否則推測難以體現效能)。透過上面的網址知道當我們容許5%錯誤(0.05)時,table size=230217,k=4。
看懂 bloom filter c code ,並試著導入原來程式。並先用原來的 cities.txt 檔案做測試以評估正確性。
首先,這裡選用了兩種 hash ,jenkins
和djb2
,另外 murmur3 hash 也是一個不錯的選擇,這裡主要是選用夠 uniform 的即可。
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 的大小。
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,不需要其他的操作。
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 功能修改:
...
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 的部份:
...
/* 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 已經導入完畢。
因為我們要支援多個使用者可以執行不同功能,所以我想必須要把程式分成 Server 端和 Client 端,一次開一個 Server ,就可以支援不同的 Client 連上。
首先主要的程式運作模式是,先開啟一個 server 程式,然後 server會透過 socket 去監聽有沒有 client 要來連,若是連上了,server 就用 thread 去服務這個 client ,然後 main thread 繼續監聽有沒有 client 。
因為所有 thread ,共享程式的 heap 部份,所以要查詢的資料存在 global variable ,讓所有 thread 共同存取。細部的功能部份,是透過 send 和 recv 一問一答,例如 server 去問 client 要什麼加入什麼字串,client 就回答。
以下將逐步講解程式碼:
首先,我們讓 Server 和每個 Client ,透過 Socket 相連通訊。
首先以下程式碼是在 Server 中,設定好 Socket 的 IP 、Port 等初始化動作。
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 )。
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 的部分就比較簡單,只要設定好之後,用 connect()
連上 Server即可。
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");
}
上面 Client 連上 Server 之後,Server 會建立新的 Thread 去服務這個 Client,然後 Main thread 會繼續在 while 迴圈內看有沒有 Client 要連上。
以下演示 find 功能的運作,其他功能的設計都大同小異。
以下是於 thread function 內的部分程式碼,也就是每個服務 Client 的新 thread 各自都要做以下的事,與 Client 做對答。
首先,將所有動作包在 while 裡面,每一輪 while 代表著一次完整的動作。第6行會先把上面2~5行要問 client 的問題傳送給他。然後第7行接收 client 要求作的選項。
例如 client 要做 f 選項,就會進到第11行。然後第14行 Server 再傳問題問 Client 要 find 什麼字串並在下一行接收。之後下面的動作就是做 Bloom filter 版本的 find ,並把結果回傳給Client。
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 端的部分,也是在 while 迴圈裏面繞,每一輪 while 就代表一次完整的對答,也就表示了做完一次完整的工作。
例如首先第2行會先接到 Server 詢問要做什麼指令,若這次我要做 find 指令,就會在第6行回答 server,server 這時就會回要 find 什麼字串(第16行接收 server 問題,並在第17行印出)。第19行就會回答 server 要加入的字串, server 做完一連串 find 動作後,Client 就會接收結果並印出來(第20 21行)。
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
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 端
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;
}
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 端
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 中
在這裡我們忽略掉了一個基本的概念:
我們在傳送訊息時,是用 send(forClientSockfd[localindex],message,sizeof(message),0);
,這裡的第二個和第三個 argument 分別是儲存訊息的字串,後面接的是要傳送出去的 byte 大小。
但我們發現最多只能傳出 8 byte 的訊息,是因為我們用 char* message = malloc(1024);
來宣告,而 sizeof(message) ,就會是這個指標所佔的記憶體大小,也就是 8 byte。解決方式就是把宣告方式變成 char message[1024]
。
這裡設計實驗釐清一下概念,考慮以下程式碼:
#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 會寫資訊進結構,有些 thread 會從結構中讀資料出來,要達成 thread safe 就要對資料做一些保護,我們用到的是 read write lock。pthread 本身就已經有 read write lock的功能,以下將介紹呼叫方式與行為。
首先,在 pthread 用 read write lock 的方式是:
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 還是可以使用資料結構,因為他們只有讀值,對資料結構沒有寫入。
參考 Concurrent Hash Tables,提出正確的 read/write lock 機制
以上是示意圖,Coarse-Grained(粗質)的意思是,整個資料結構共享一個 lock,例如圖中綠色的 CPU(或是thread)要修改其中的某個node,就要把整個資料結構都 lock 住,但這會導致平行化毫無意義,因為其他的 thread 要等這個 thread 所有操作完成才能再操作。
Fine-grained 就可以解決上面的問題,因為一個元素自己本身就用一個 lock(實作方法是把本來的資料結構多一個 lock 元素,每個 node 自己有專屬的lock)。如上圖,如果紅色的 thread 要用 node a 和 head 的話,就獲取他們兩個的lock就好,因此這樣藍色的 thread 如果還要對node c、node d 做操作,就不需要等紅色 thread 操作完。
這種避免資料錯誤的方法,沒有用到 lock ,所有的 thread 還是能同時存取所有資料,但還是能保證資料正確。
一個典型的方法是: CAS(compare-and-swap)
實作程式碼如下:
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 。
情境: 如果 server 開啟後,有多個 client 連上的情況下,若是某一個 client 沒有正常用 q 離開而是直接用ctrl+c
離開,則 server 也會直接關掉。就算再次打開 server 與 client ,client 都會 connect error。這個情況就算用 make clean 再重新 make 也無法解決。但是去加上一些無關緊要的 code ,例如在某行多加上一個分號,然後重新 make ,就可以恢復運作。(或是隔一段時間再開 server ?)
解決方法: 需要加上 signal handler 去改變 client 的 ctrl+C 行為,讓它可以先關掉 socket 再正常離開。
情境: 連續 send ,例如 server send 一次,沒有 receive動作就直接在 send 一次 ,會導致傳出去的字串有問題。
解決方法:(已解決) 在連續 send 中間加入一個 dummy 的 receive ,但應該要去找有什麼可以讓 socket buffer flush 的方式。
情境: 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"
。
情境: 發現本來 prefix search 程式就有的問題,如果要尋找以"A"為prefix的字串會 core dumped,這應該是因為有一個變數LMAX設成 1024 導致最多只能傳 suggest[0]~suggest[1023] 。
make check
執行內建的 unit test 或 test suiteperror
),當首度連線失敗時,client 可嘗試用 fork + execve 建立 server process 以重新連線