2012-04-19 97 views
0

我試圖實現一個基於UDP的服務器,它維護兩個插座,一個用於控制(ctrl_sock),另一個用於數據傳輸(data_sock)。事情是,ctrl_sock總是上行鏈路,data_sock是下行鏈路。也就是說,客戶端將通過ctrl_sock請求數據傳輸/停止,數據將通過data_sock發送給他們。在兩個插座之間共享信息

現在的問題是,由於模型沒有連接,服務器將不得不維護已註冊客戶信息的列表(我稱之爲peers_context),以便它可以「盲目地」向他們推送數據,直到他們詢問停止。在此盲傳期間,客戶端可能會異步通過ctrl_sock向服務器發送控制消息。除了初始請求和停止之外,這些信息也可以是例如文件部分的偏好。因此,peers_context必須異步更新。然而,通過data_sock的傳輸依賴於這個peers_context結構,因此引起了ctrl_sockdata_sock之間的同步問題。我的問題是,我能做些什麼來安全地維護這兩個襪子和peers_context結構,這樣異步更新peers_context不會造成嚴重破壞。順便說一下,peers_context的更新不會很頻繁,這就是爲什麼我需要避免請求 - 回覆模型。

我最初考慮的實現是在主線程(listener線程)中維護ctrl_sock,並且在另一個線程(工作線程)中維護通過data_sock進行的傳輸。但是,我發現在這種情況下很難同步。例如,如果我在peers_context中使用互斥鎖,每當工作線程鎖定peers_context時,由於工作線程無休止地工作,偵聽線程在它需要修改peers_context時不再有權訪問它。另一方面,如果偵聽器線程持有peers_context並寫入它,工作線程將無法讀取peers_context並終止。有人可以給我一些建議嗎?

順便說一下,這個實現是在C語言的Linux環境下完成的。只有監聽線程偶爾需要修改peers_context,工作線程只需要讀取。衷心感謝!

+0

爲什麼不使用TCP?您可以與每個客戶端建立基於流的連接,並執行必要的通信,而不用擔心同步它。比UDP更容易,更可靠。 – Raam 2012-04-19 18:06:42

+0

聽起來像你只需要稍微複雜的鎖定。顯然你不能讓工人無休止地鎖住。您需要鎖定,發送數據包,解鎖 – TJD 2012-04-19 18:10:04

+2

使用select()或poll()將這個狀態機重新編碼爲狀態機是否可能/合意?這可能比一個線程化設計更好,並且避免了所有這些刺激的線程同步問題! – 2012-04-19 18:19:53

回答

1

如果您的peers_context存在強烈爭用,那麼您需要縮短關鍵部分。你談到了使用互斥鎖。我假設你已經考慮改變爲讀者+作家鎖,並拒絕它,因爲你不想讓不斷讀者餓死作家。這個怎麼樣?

做一個非常小的結構,是一個間接引用peers_context這樣的:

struct peers_context_handle { 
    pthread_mutex_t ref_lock; 
    struct peers_context *the_actual_context; 
    pthread_mutex_t write_lock; 
}; 

數據包的發送者(讀者)和控制要求處理器(作家)總是通過這種間接訪問peers_mutex

假設:數據包發送者從不修改peers_context,也沒有釋放它。

帕克發件人簡要鎖手柄,獲得peers_context的當前版本和解鎖:

pthread_mutex_lock(&(handle->ref_lock)); 
peers_context = handle->the_actual_context; 
pthread_mutex_unlock(&(handle->ref_lock)); 

(在實踐中,你甚至可以用鎖做的路程,如果你介紹記憶障礙,因爲指針解引用是對Linux支持的所有平臺的原子,但我不會推薦它,因爲你將不得不開始鑽研記憶障礙和其他低層次的東西,也不ç也不POSIX保證它無論如何都會正常工作。)

請求處理器不更新peers_context,他們複製並共享完全取代它。這就是他們如何保持他們的關鍵部分小。他們使用write_lock序列化更新,但更新很少,所以這不是問題。

pthread_mutex_lock(&(handle->write_lock)); 

/* Short CS to get the old version */ 
pthread_mutex_lock(&(handle->ref_lock)); 
old_peers_context = handle->the_actual_context; 
pthread_mutex_unlock(&(handle->ref_lock)); 

new_peers_context = allocate_new_structure(); 
*new_peers_context = *old_peers_context; 

/* Now make the changes that are requested */ 
new_peers_context->foo = 42; 
new_peers_context->bar = 52; 

/* Short CS to replace the context */ 
pthread_mutex_lock(&(handle->ref_lock)); 
handle->the_actual_context = new_peers_context; 
pthread_mutex_unlock(&(handle->ref_lock)); 

pthread_mutex_unlock(&(handle->write_lock)); 

magic(old_peers_context); 

這有什麼用?這是最後一行代碼中的魔力。您必須釋放peers_context的舊副本以避免內存泄漏,但您無法這樣做,因爲可能有數據包發件人仍在使用該副本。

該解決方案與在Linux內核中使用的RCU類似。你必須等待所有的數據包發送者線程進入靜態狀態。我要離開這個實施作爲練習你:-)但這裏有準則:

  • magic()函數添加old_peers_context所以要被釋放隊列(這必須由一個互斥體的保護)。
  • 一個專用線程釋放這個名單在一個循環:
    1. 它鎖定到待釋放名單
    2. 它獲得的指針列表
    3. 它取代了清單,一個新的空單
    4. 它解鎖,以待釋放名單
    5. 它清除與每個工作線程
    6. 它等待所有標記再次設置
    7. 它釋放每個相關的標記在其先前獲得的待釋放列表副本中的項目
  • 同時,每個工作線程在其事件循環中的空閒點處設置其自己的標記(即,當它不忙於發送任何數據包或持有任何peer_contexts時。
+0

Hi @Celada,真誠地感謝您的回覆,這些知識相當有幫助,我有點困惑的一件事是在「魔術」部分,我怎麼知道包發送者何時進入靜態狀態並讓'free_old_peers_context'線程知道它?正如我所提到的,只要在peers_context中至少有一個對等點處於活動狀態,包發送者就會繼續發送包(通常我會遍歷對等列表一次,然後將數據包發送給每個對象,然後遍歷列表,也許我應該在這裏添加一個空閒點?)再次感謝。 – 2012-04-20 13:26:36

+0

您必須安排您的應用程序擁有一個靜止的狀態如果你使用了一個事件循環(很可能,因爲你是IO綁定的),那麼在你調用poll()之前或之後它就是正確的。 – Celada 2012-04-20 15:28:38

+0

你的意思是發送循環內部還是外部?事情是,select()或poll()將阻止發送者,這意味着,只能在監聽套接字可讀時發送一次。爲了您的信息,這裏是我的一段代碼[鏈接](http://pastebin.com/XB73mLUF)。 (我還沒有開始應用您的讀寫器自旋鎖。) – 2012-04-20 17:10:42