Go:使用稀疏数组的线程安全并发问题Read&写

时间:2015-04-03 08:08:32

标签: multithreading go

我在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切片中。如果此nil StemID当前为StemID_to_Index,则结果不会被缓存并需要加载,否则它已经加载(缓存),因此可以直接使用。每次加载新结果时,都会检查内存使用情况,如果它超过阈值,则会丢弃2/3的加载结果(nil设置为nil这些StemID并强制进行垃圾收集。)

我的问题是并发性。什么是最快和最有效的方法,我可以同时搜索多个线程而不会遇到同时尝试读取和写入同一位置的不同线程的问题?我试图避免在所有内容上使用互斥锁,因为这会减慢每次访问尝试的速度。

你认为我会放弃在工作线程中从磁盘加载结果,然后将指向这个结构的指针发送到&#34;更新程序&#34;线程使用通道,然后将StemID_to_Index切片中的StemID_to_Index值更新为加载结果的指针?这意味着两个线程永远不会尝试同时写入,但是如果另一个线程试图从nil的精确索引读取而另一个线程会在&#34;更新程序&#34;线程正在更新指针?如果线程被赋予了当前正在加载的结果的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() } 指针并不重要,因为它只会被加载两次,而这会浪费资源,它仍会提供相同的结果因为这不太可能经常发生,所以可以原谅。

此外,如何将发送指针的工作线程更新到&#34;更新程序&#34;线程知道&#34;更新程序&#34;线程已完成更新切片中的指针?它应该只是睡眠并继续检查,还是有一种简单的方法让更新程序将消息发送回推送到频道的特定线程?

更新

我做了一个小测试脚本,看看如果在修改它的同时尝试访问指针会发生什么......似乎总是没问题。没有错误。我错过了什么吗?

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++
    }
}

更新2

进一步说,即使我同时从多个线程读取和写入同一个变量......它没有任何区别,仍然没有错误:

从上面:

{{1}}

这意味着我根本不必担心并发......再次:我在这里遗漏了什么?

2 个答案:

答案 0 :(得分: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不会同时读取/写入同一段内存。

答案 1 :(得分:0)

我的最佳答案是将弹性搜索与elastigo等客户端一起使用。

如果这不是一个选项,那么了解你对种族行为的关注真的很有帮助。如果你不在乎,写入可能会在读取完成后立即发生,完成读取的用户将获得过时的数据。您可以拥有一个写入和读取操作的队列,并将多个线程提供给该队列,并且一个调度程序在它们到来时一次一个地向该映射发出操作。在所有其他场景中,如果有多个读者和编写者,则需要一个互斥锁。地图不是thread safe in go

老实说,我现在只需添加一个互斥锁即可使事情变得简单,并通过分析瓶颈实际存在的位置进行优化。看起来你正在检查一个阈值,然后清除2/3的缓存有点武断,如果你通过做那样的事情来扼杀性能,我也不会感到惊讶。这是关于会崩溃的情况:

请求者1​​,2,3和4经常访问文件A和A上的许多相同的单词。 B. 请求者5,6,7和8经常访问存储在文件C&amp; C上的许多相同的单词。 d。

现在,当这些请求者和文件之间的请求快速连续发生时,您可能会一次又一次地清除2/3的缓存,这些结果可能会在不久之后被请求。还有其他几种方法:

  1. 缓存经常在同一个盒子上同时访问并具有多个缓存框的单词。
  2. 以每个单词为基础进行缓存,并对该单词的受欢迎程度进行某种排序。如果在缓存已满时从文件中访问新单词,请查看该文件中是否存在其他更常用的单词,并清除缓存中不太受欢迎的条目,希望这些单词的命中率更高。
  3. 两种方法都是1&amp; 2。