2015-04-03 51 views
-1

我在Go中編寫了一個搜索引擎,在該搜索引擎中,每個單詞對應的結果都有一個單詞倒排索引。有一組單詞字典,因此這些單詞已經轉換爲StemID,這是一個從0開始的整數。這使我可以使用一個指針片段(即一個sparse array)將每個StemID映射到包含該查詢的結果。例如。 var StemID_to_Index []*resultStruct。如果aardvark0那麼指向aardvark的resultStruct的指針位於StemID_to_Index[0],如果此字的結果當前未加載,則該指針將爲nil轉到:使用稀疏陣列讀寫的線程安全併發問題

服務器上沒有足夠的內存將所有內容存儲在內存中,因此每個StemID的結構都將保存爲單獨的文件,這些文件可以加載到StemID_to_Index切片中。如果StemID_to_Index當前爲nilStemID然後結果不緩存,需要加載,否則它已經加載(緩存),因此可以直接使用。每次加載新結果時都會檢查內存使用情況,如果超出閾值,則會丟棄2/3的加載結果(對於這些StemID,StemID_to_Index設置爲nil,並強制執行垃圾回收)。

我的問題是併發性。什麼是最快和最有效的方式,我可以同時搜索多個線程,而不會遇到不同線程同時嘗試讀取和寫入同一個地方的問題?我試圖避免在一切中使用互斥鎖,因爲這會減慢每一次訪問嘗試的速度。

您是否認爲我會在工作線程中加載磁盤的結果,然後使用通道將指向此結構的指針傳遞到「更新程序」線程,然後將StemID_to_Index切片中的nil值更新爲加載結果的指針?這意味着兩個線程永遠不會同時嘗試寫入,但是如果另一個線程嘗試從StemID_to_Index的確切索引中讀取,而「updater」線程正在更新指針時會發生什麼情況?如果一個線程被賦予了一個nil指針,這對於當前正在加載的結果是沒有關係的,因爲它只會被加載兩次,雖然這是浪費資源,但仍然會提供相同的結果,並且因爲這不太可能經常發生,這是可以原諒的。

此外,發送指針的工作線程如何更新到「更新器」線程,知道「更新器」線程何時完成更新片中的指針?它應該只是睡覺並繼續檢查,還是有一個簡單的方法讓更新者發送消息回到推送到通道的特定線程?

UPDATE

我做了一個小測試腳本,看看是否試圖在同一時間訪問一個指針修改它......它似乎永遠是確定會發生什麼。沒有錯誤。我錯過了什麼嗎?

package main 

import (
    "fmt" 
    "sync" 
) 

type tester struct { 
a uint 
} 

var things *tester 

func updater() { 
    var a uint 
    for { 
     what := new(tester) 
     what.a = a 
     things = what 
     a++ 
    } 
} 

func test() { 
    var t *tester 
    for { 
     t = things 
     if t != nil { 
      if t.a < 0 { 
       fmt.Println(`Error1`) 
      } 
     } else { 
      fmt.Println(`Error2`) 
     } 
    } 
} 

func main() { 
    var wg sync.WaitGroup 
    things = new(tester) 
    go test() 
    go test() 
    go test() 
    go test() 
    go test() 
    go test() 
    go updater() 
    go test() 
    go test() 
    go test() 
    go test() 
    go test() 
    wg.Add(1) 
    wg.Wait() 
} 

更新2

進一步考慮這一點,即使我讀,並在同一時間從多個線程寫入相同的變量...它沒有什麼區別,依然沒有任何錯誤:

從上面:

func test() { 
    var a uint 
    var t *tester 
    for { 
     t = things 
     if t != nil { 
      if t.a < 0 { 
       fmt.Println(`Error1`) 
      } 
     } else { 
      fmt.Println(`Error2`) 
     } 
     what := new(tester) 
     what.a = a 
     things = what 
     a++ 
    } 
} 

這意味着我不必擔心併發在所有...再次:我失去了一些東西在這裏?

+1

[沒有良性數據競賽](https://software.intel.com/zh-cn/blogs/2013/01/06/benign-data-races-what-c​​ould-possibly-go-wrong) !用賽跑探測器運行你的最後一個例子。只是因爲你無法引發錯誤或觀察到未定義的行爲,並不意味着它不會發生。你要麼有比賽,要麼沒有。 – JimB 2015-04-03 15:15:17

+0

感謝您的鏈接,這很有趣!但事情是,只能有兩個結果,一個指向結構的指針,它總是包含相同的數據......或者無。在我的情況下,它是什麼都沒有區別。因此,只要指針過時,只有在發生實際的運行時錯誤時,根本不重要。 「nil指針解引用」,只有當指針實際上損壞時纔會發生。只要它始終是一個有效的指針(或零),那麼即使它已過期,我也沒有問題。 – Alasdair 2015-04-04 04:09:08

回答

0

我的最佳答案是使用elasticsearch與elastigo這樣的客戶端。

如果這不是一個選項,它將有助於知道你有多少關心種族的行爲。如果你不在意,讀取完成後可能發生寫入,用戶完成讀取將會得到過時的數據。您可以擁有一個寫入和讀取操作的隊列,並將多個線程饋送到該隊列中,並且一個調度程序在它們出現時一次性向該映射發佈操作。在所有其他情況下,如果有多個讀者和作者,您將需要互斥體。地圖不是thread safe in go

雖然說實話,我現在只需添加一個互斥對象,讓事情變得簡單,並通過分析瓶頸實際所在的位置來進行優化。看起來你正在檢查一個閾值,然後清除2/3的緩存有點武斷,如果你通過這樣做來殺死性能,我不會感到驚訝。以下是可能發生故障的情況:

請求者1,2,3和4經常訪問文件A & B上的許多相同字詞。 請求者5,6,7和8經常訪問許多存儲在文件上的相同字C & D.

現在,當這些請求者和文件之間的請求交替發生時,您可能會一次又一次地清除2/3緩存中可能需要的結果不久之後。還有其他一些方法:

  1. 緩存在同一個盒子上同時經常訪問的單詞並且有多個緩存框。
  2. 根據每個單詞的基礎上的某種排序來緩存每個單詞。如果在緩存已滿時從文件訪問新單詞,請查看該文件中是否存在其他更流行的單詞,並清除緩存中不太流行的條目,以期這些單詞具有更高的命中率。
  3. 兩個接近1 & 2.
+0

對於清除,我打算按最後訪問進行排序,然後清除最長訪問時間最長的2/3。很可能這些都是晦澀難懂的術語,很少被搜索到 - 在這種情況下,只要人們不斷搜索它們,常用術語將永遠不會被清除。 – Alasdair 2015-04-03 08:39:24

+0

關於更新...它始終是相同的數據,因爲索引在開始時只創建一次。所以每個結果要麼在那裏,要麼是零,然後加載,這從用戶的角度來看並不重要。唯一的問題是由於在半寫入的寫入上讀取一個零指針解引用,但似乎沒有發生,是否有可能? – Alasdair 2015-04-03 08:42:08

+0

我已經更新了我的併發性答案。你是對的,讓讀寫器讀/寫相同的數據是不安全的。您必須使用互斥鎖。 http://stackoverflow.com/questions/11063473/map-with-concurrent-access – Vinay 2015-04-03 09:05:08

1

這聽起來像一個完美的使用情況爲memory mapped file

package main 

import (
    "log" 
    "os" 
    "unsafe" 

    "github.com/edsrzf/mmap-go" 
) 

func main() { 
    // Open the backing file 
    f, err := os.OpenFile("example.txt", os.O_RDWR|os.O_CREATE, 0644) 
    if err != nil { 
     log.Fatalln(err) 
    } 
    defer f.Close() 

    // Set it's size 
    f.Truncate(1024) 

    // Memory map it 
    m, err := mmap.Map(f, mmap.RDWR, 0) 
    if err != nil { 
     log.Fatalln(err) 
    } 
    defer m.Unmap() 

    // m is a byte slice 
    copy(m, "Hello World") 
    m.Flush() 

    // here's how to use it with a pointer 
    type Coordinate struct{ X, Y int } 
    // first get the memory address as a *byte pointer and convert it to an unsafe 
    // pointer 
    ptr := unsafe.Pointer(&m[20]) 
    // next convert it into a different pointer type 
    coord := (*Coordinate)(ptr) 
    // now you can use it directly 
    *coord = Coordinate{1, 2} 
    m.Flush() 
    // and vice-versa 
    log.Println(*(*Coordinate)(unsafe.Pointer(&m[20]))) 
} 

存儲器映射可以比實際內存和操作系統會處理所有的大凌亂的細節給你。

您仍然需要確保單獨的goroutine不會同時讀取/寫入同一段內存。