mdb.Dequemdb.Listmdb.BlobMapДа, можно бесконечно спорить о ненужности для тех, кто все читал в оригинале... Но ведь любой нормальный перевод мгновенно расширяет Аудиторию! И как глоток свежего воздуха, он способен спасти потерявших Надежду... (смахнул слезу пробелом)
С уважением, Сергей Деревяго.
Ну а сейчас есть шанс и насладиться переводом. Внезапно, переводом с C++ на Go!
И что тут эдакого?
Ну, как сказать... Оригинальная хеш-таблица с транзакциями, внутри процесса, без плясок с бубнами да с ускорением в десятки раз... Это было совсем непросто! Можно даже сказать, Удивительно.
Суть в том, что удивительное НЕВОЗМОЖНО! При переходе с C++ на Go:
- George Orwell
unsafe и go:linkname? А в новоязе это мыслепреступление!
mdb.MemDb и с чем его едят?
Ну, в первом приближении, все выглядит примерно так:
| lib\mdb\memdb.go |
|---|
func NewMemDb() MemDb { /* ... */ }
type MemDb interface {
Close() error
StartTrn() Transaction
}
type Transaction interface {
Close() error
Get(key Ptrsz) (Ptrsz, bool)
All(getKeys bool, getVals bool) (keys []Ptrsz, vals []Ptrsz)
Set(key Ptrsz, val Ptrsz)
Del(key Ptrsz)
DependVal(key Ptrsz, val Ptrsz)
DependDel(key Ptrsz)
Commit() error
Rollback() error
}
|
А именно:
MemDb создается с помощью функции NewMemDb().
MemDb есть функция Close() -- мы ОБЯЗАНЫ ее вызвать!!!
Transaction создается с помощью функции StartTrn().
Transaction тоже есть функция Close(). Да, мы ОБЯЗАНЫ!
Transaction работает с данными через lib.Ptrsz. Точно также, как и mdb.BlobMap.
Get() и All(). Возвращаемые ими Ptrsz указывают на внутренние структуры MemDb. Они остаются валидными пока не завершена транзакция и не было вызовов Set() и Del(), инвалидирующих указатели.
Set() и Del(). MemDb копирует себе байты, на которые указывают key и val.
DependVal() и DependDel() устанавливают зависимости. Их проверяет Commit().
Commit() и Rollback() завершают транзакцию. Завершают, но не закрывают! Мы ОБЯЗАНЫ вызвать Close()!!
Close() означает Rollback().
| lib\mdb\memdb.go |
|---|
type MemDb interface {
// ...
StartRdr() Reader
}
type Reader interface {
Close() error
Get(key Ptrsz) (Ptrsz, bool)
All(getKeys bool, getVals bool) (keys []Ptrsz, vals []Ptrsz)
}
|
Так точно:
StartRdr(), создающая объект Reader.
Reader -- это read-only транзакция. Завершать ее нечем, но закрыть мы обязаны!
Как всегда, речь пойдет о деньгах: совершим пару-тройку транзакций. А для этого создадим минимальный Account с функциями сериализации/десериализации:
| examples\memdb\account.go |
|---|
type Account struct {
FirstName string
LastName string
BirthDate time.Time
Amt int
}
func (my *Account) Write(to *lib.MemOutBuf, overwrite bool) lib.Ptrsz {
if overwrite {
to.Clear()
}
to.WriteString(my.FirstName)
to.WriteString(my.LastName)
{
var un int64 = my.BirthDate.UnixNano()
lib.WriteBytes(to, &un)
}
lib.WriteBytes(to, &my.Amt)
return to.Get()
}
func (my *Account) Read(from lib.Ptrsz) {
buf := lib.MemInBuf(from)
my.FirstName = buf.ReadString()
my.LastName = buf.ReadString()
{
var un int64
lib.ReadBytes(&buf, &un)
my.BirthDate = time.Unix(0, un).UTC()
}
lib.ReadBytes(&buf, &my.Amt)
}
|
Все очевидно. Шагом марш!
| examples\memdb\main.go |
|---|
func main() {
db := mdb.NewMemDb()
defer func() { lib.AssertNil(db.Close()) }()
Setup(db)
Print(db)
// ...
}
func Print(db mdb.MemDb) {
rd := db.StartRdr()
defer func() { lib.AssertNil(rd.Close()) }()
keys, vals := rd.All(true, true)
for i := 0; i < len(keys); i++ {
var key int
lib.PtrszAssign(&key, keys[i])
var acc Account
acc.Read(vals[i])
fmt.Printf("%d\t%s\t%s\t%s\t%d\n",
key, acc.FirstName, acc.LastName, acc.BirthDate.Format("2006-1-2"), acc.Amt)
}
fmt.Println()
}
|
Мы видим в деле MemDb и Reader: функция main() создает и закрывает MemDb, а Print() работает через Reader. И для старта имеем:
1 First1 Last1 2000-1-21 100 2 First2 Last2 2000-2-22 200 3 First3 Last3 2000-3-23 300 |
А сейчас переведем 10 тугриков с первого на второй счет посредством Transfer1(db, 1, 2, 10):
| examples\memdb\main.go |
|---|
func main() {
// ...
lib.AssertNil(Transfer1(db, 1, 2, 10))
Print(db)
}
func Transfer1(db mdb.MemDb, keyFrom int, keyTo int, amt int) error {
buf := lib.NewMemOutBuf(100)
tr := db.StartTrn()
defer func() { lib.AssertNil(tr.Close()) }()
pk1 := lib.PtrToPtrsz(&keyFrom)
val1, ok := tr.Get(pk1)
if !ok {
return fmt.Errorf("not found: %d", keyFrom)
}
var acc1 Account
acc1.Read(val1)
acc1.Amt -= amt
tr.DependVal(pk1, val1)
tr.Set(pk1, acc1.Write(&buf, true))
pk2 := lib.PtrToPtrsz(&keyTo)
val2, ok := tr.Get(pk2)
if !ok {
return fmt.Errorf("not found: %d", keyTo)
}
var acc2 Account
acc2.Read(val2)
acc2.Amt += amt
tr.DependVal(pk2, val2)
tr.Set(pk2, acc2.Write(&buf, true))
{
// concurrent transaction
// lib.AssertNil(UpdateName(&buf, db, keyFrom, "From", "Here"))
// lib.AssertNil(UpdateName(&buf, db, keyTo, "To", "There"))
// Print(db)
}
VeryLongOperation()
return tr.Commit(nil, nil)
}
|
Здесь очень много интересного:
pk1 создается с помощью вызова функции lib.PtrToPtrsz(): она превращает *int в Ptrsz.
int, т.е. 4 или 8 байт. Как повезет.
tr.Get(pk1) возвращает нам val1, указывающий на байты сериализованного счета (если нашелся).
tr.DependVal(pk1, val1) сообщает транзакции, что у нас есть зависимость. А именно: в момент коммита в базе должен существовать ключ pk1 со значением val1!
tr.Set(pk1, acc1.Write(&buf, true)) сериализует обновленный acc1 и записывает в базу его байты.
tr.Set() потенциально ИНВАЛИДИРУЕТ все ранее полученные в этой транзакции указатели! Так что, после изменений, val1 может указывать неизвестно куда!!
1 First1 Last1 2000-1-21 90 2 First2 Last2 2000-2-22 210 3 First3 Last3 2000-3-23 300 |
От кого-то кому-то ушло 10 тугриков. Что, в общем, не удивительно.
Но мы же хотим удивиться!
Хорошо. Все сейчас удивятся от действия DependVal(). А для этого раскомментируем параллельную транзакцию:
| examples\memdb\main.go |
|---|
func Transfer1(db mdb.MemDb, keyFrom int, keyTo int, amt int) error {
// ...
{
// concurrent transaction
lib.AssertNil(UpdateName(&buf, db, keyFrom, "From", "Here"))
lib.AssertNil(UpdateName(&buf, db, keyTo, "To", "There"))
Print(db)
}
VeryLongOperation()
return tr.Commit(nil, nil)
}
|
Тем самым, мы имитируем одновременное изменение счета кем-то еще, между нашими вызовами StartTrn() и Commit(). Причем, изменяется только имя! Что, по сути, на сумму влиять не должно, но:
1 First1 Last1 2000-1-21 100 2 First2 Last2 2000-2-22 200 3 First3 Last3 2000-3-23 300 1 From Here 2000-1-21 100 2 To There 2000-2-22 200 3 First3 Last3 2000-3-23 300 panic: record updated goroutine 1 [running]: ders/lib.AssertNil(...) lib/assert.go:19 main.main() examples/memdb/main.go:26 +0xf3 exit status 2 |
Да, вы правильно поняли: апдейты сработали, но Commit() завершился ошибкой! Т.к. зависимость учитывает каждый байт.
Хорошо, а что будет, если мы дополнительно уберем DependVal()? Ну, давайте попробуем:
1 First1 Last1 2000-1-21 100 2 First2 Last2 2000-2-22 200 3 First3 Last3 2000-3-23 300 1 From Here 2000-1-21 100 2 To There 2000-2-22 200 3 First3 Last3 2000-3-23 300 1 First1 Last1 2000-1-21 90 2 First2 Last2 2000-2-22 210 3 First3 Last3 2000-3-23 300 |
Несомненно, Commit() отработал. Но мы затерли имена!
В том и есть смысл зависимостей: проверяем нет ли параллельных изменений. А коль не проверить, то мы их отбросим!
Да, это мощно... А как выглядит UpdateName()?
Не вопрос! Я сейчас кое-что покажу:
| lib\mdb\memdb.go |
|---|
type MemDb interface {
// ...
Write(writeCb func(Writer))
}
type Writer interface {
Reader
Set(key Ptrsz, val Ptrsz)
Del(key Ptrsz)
}
|
Это функция Write(), предоставляющая прямой доступ к базе: вы передаете свой writeCb, и она его вызывает с аргументом Writer.
Тут очень важно понимать, что на время работы Write() база блокируется, и никто не может начинать и завершать транзакции. Вы фактически получаете прямой эксклюзивный доступ, чтобы быстро что-то поправить:
| examples\memdb\main.go |
|---|
func UpdateName(buf *lib.MemOutBuf, db mdb.MemDb, key int, first string, last string) error {
var err error
db.Write(func(wr mdb.Writer) {
var acc Account
if !GetAccount(wr, key, &acc) {
err = fmt.Errorf("not found: %d", key)
return
}
acc.FirstName = first
acc.LastName = last
SetAccount(buf, wr, key, &acc)
})
return err
}
func GetAccount(rd mdb.Reader, key int, acc *Account) bool {
val, ok := rd.Get(lib.PtrToPtrsz(&key))
if !ok {
return false
}
acc.Read(val)
return true
}
func SetAccount(buf *lib.MemOutBuf, wr mdb.Writer, key int, acc *Account) {
wr.Set(lib.PtrToPtrsz(&key), acc.Write(buf, true))
}
|
Удобно? Шикарно!
Только снова замечу, что внутри вызова Write() не должно быть и близко VeryLongOperation(), т.к. вы вынуждаете всех ожидать!
Хорошо, но а как бы нам тут изменить только деньги? Не мешая всем прочим играться с другими полями.
Можно? Можно!
Но для этого нам понадобятся дополнительные возможности:
| lib\mdb\memdb.go |
|---|
type Transaction interface {
Writer
DependVal(key Ptrsz, val Ptrsz)
DependDel(key Ptrsz)
Commit(writeCb func(Writer), checkCb func(Reader) bool) CommitError
Rollback() error
}
|
Как и Write(), наш Commit() принимает колбэки:
checkCb будет вызван в самом начале. Он дает нам возможность отменить транзакцию до внесения изменений.
writeCb будет вызван в конце. Он дает нам возможность что-либо поправить после всех изменений транзакции.
Ну, мы можем:
VeryLongOperation() -- мы никому не мешаем!
Commit() свой writeCb, который проверит суммы и произведет перевод.
| examples\memdb\main.go |
|---|
func main() {
db := mdb.NewMemDb()
defer func() { lib.AssertNil(db.Close()) }()
Setup(db)
Print(db)
lib.AssertNil(Transfer1(db, 1, 2, 10))
Print(db)
lib.AssertNil(Transfer2(db, 3, 2, 20))
Print(db)
}
func Transfer2(db mdb.MemDb, keyFrom int, keyTo int, amt int) error {
buf := lib.NewMemOutBuf(100)
tr := db.StartTrn()
defer func() { lib.AssertNil(tr.Close()) }()
var acc1 Account
if !GetAccount(tr, keyFrom, &acc1) {
return fmt.Errorf("not found: %d", keyFrom)
}
oldAmt1 := acc1.Amt
var acc2 Account
if !GetAccount(tr, keyTo, &acc2) {
return fmt.Errorf("not found: %d", keyTo)
}
oldAmt2 := acc2.Amt
{
// concurrent transaction
lib.AssertNil(UpdateName(&buf, db, keyFrom, "From", "Here"))
lib.AssertNil(UpdateName(&buf, db, keyTo, "To", "There"))
Print(db)
}
var err error
writeCb := func(wr mdb.Writer) {
var acc1, acc2 Account
if !(GetAccount(wr, keyFrom, &acc1) && acc1.Amt == oldAmt1 &&
GetAccount(wr, keyTo, &acc2) && acc2.Amt == oldAmt2) {
err = errors.New("concurrent modification")
return
}
acc1.Amt -= amt
SetAccount(&buf, wr, keyFrom, &acc1)
acc2.Amt += amt
SetAccount(&buf, wr, keyTo, &acc2)
}
VeryLongOperation()
tr.Commit(writeCb, nil)
return err
}
|
А вот вам и полный выхлоп:
1 First1 Last1 2000-1-21 100 2 First2 Last2 2000-2-22 200 3 First3 Last3 2000-3-23 300 1 First1 Last1 2000-1-21 90 2 First2 Last2 2000-2-22 210 3 First3 Last3 2000-3-23 300 1 First1 Last1 2000-1-21 90 2 To There 2000-2-22 210 3 From Here 2000-3-23 300 1 First1 Last1 2000-1-21 90 2 To There 2000-2-22 230 3 From Here 2000-3-23 280 |
Transfer2(db, 3, 2, 20) успешно перевел все 20 тугриков, не потеряв при этом имена!
Ну, раз вы видите такой раздел, то можно смело быть уверенным: фсё чотка!
Мы будем сравнивать с оригиналом, и вот что стало с Записью Клиента:
| perf\perf1\client.go |
|---|
type ClientRec struct {
NumCred int
NumDeb int
Amt int
}
type ClientKey struct {
buf [2]int64
}
func (my *ClientKey) Get(n int) lib.Ptrsz {
my.buf[0] = int64(n)
my.buf[1] = int64(-n)
return lib.PtrToPtrsz(my)
}
|
Что-что? Нет, все не сложно: ClientKey -- это просто "какой-то ключ". Примитивный и быстрый. Можно вставить другой, если надо.
| perf\perf1\main.go |
|---|
var (
goMtx sync.RWMutex
goDB map[string]string
)
func goRun(l1, l2 lib.Latch) {
var ck1, ck2 ClientKey
var cr1, cr2 ClientRec
l1.ArriveAndWait()
for i := 1; i <= M; i++ {
for {
n1, n2 := randMnMx(1, N), randMnMx(1, N)
key1, key2 := ck1.Get(n1), ck2.Get(n2)
if float64(n1) <= N*float64(WRITE_PROB) {
ok := func() bool {
goMtx.Lock()
defer goMtx.Unlock()
val1, ok1 := goDB[lib.PtrszToString(key1)]
lib.Assert(ok1)
lib.PtrszAssign(&cr1, *lib.StringAsPtrsz(&val1))
val2, ok2 := goDB[lib.PtrszToString(key2)]
lib.Assert(ok2)
lib.PtrszAssign(&cr2, *lib.StringAsPtrsz(&val2))
// ...
cr1.Amt -= amt
cr1.NumDeb++
cr2.Amt += amt
cr2.NumCred++
randSleep(sleep_in)
goDB[lib.PtrszToString(key1)] = lib.PtrszToString(lib.PtrToPtrsz(&cr1))
goDB[lib.PtrszToString(key2)] = lib.PtrszToString(lib.PtrToPtrsz(&cr2))
return true
}()
if !ok {
continue
}
} else {
// ...
}
break
}
randSleep(sleep_out)
}
l2.ArriveAndWait()
}
|
Да, это функция goRun(), переводящая деньги с помощью синхронизированной хеш-таблицы. Полный аналог unordered_map<string, string>.
А вот как выглядит myRun1(), использующая myDB.Write():
| perf\perf1\main.go |
|---|
var (
myDB mdb.MemDb
)
func myRun1(l1, l2 lib.Latch) {
var ck1, ck2 ClientKey
var cr1, cr2 ClientRec
l1.ArriveAndWait()
for i := 1; i <= M; i++ {
if float64(randMnMx(1, N)) <= N*float64(WRITE_PROB) {
myDB.Write(func(wr mdb.Writer) {
for {
n1, n2 := randMnMx(1, N), randMnMx(1, N)
key1, key2 := ck1.Get(n1), ck2.Get(n2)
val1, ok := wr.Get(key1)
lib.Assert(ok)
lib.PtrszAssign(&cr1, val1)
val2, ok := wr.Get(key2)
lib.Assert(ok)
lib.PtrszAssign(&cr2, val2)
// ...
cr1.Amt -= amt
cr1.NumDeb++
cr2.Amt += amt
cr2.NumCred++
wr.Set(key1, lib.PtrToPtrsz(&cr1))
wr.Set(key2, lib.PtrToPtrsz(&cr2))
randSleep(sleep_in)
break
}
})
} else {
// ...
}
randSleep(sleep_out)
}
l2.ArriveAndWait()
}
|
Как и синхронизированная хеш-таблица, она просто блокирует доступ. И, тем самым, позволяет нам оценить относительную эффективность реализации.
Ну и главное блюдо до кучи:
| perf\perf1\main.go |
|---|
func myRun2(l1, l2 lib.Latch) {
var ck1, ck2 ClientKey
var cr1, cr2 ClientRec
l1.ArriveAndWait()
for i := 1; i <= M; i++ {
for {
n1, n2 := randMnMx(1, N), randMnMx(1, N)
key1, key2 := ck1.Get(n1), ck2.Get(n2)
if float64(n1) <= N*float64(WRITE_PROB) {
ok := func() bool {
tr := myDB.StartTrn()
defer func() { lib.AssertNil(tr.Close()) }()
val1, ok := tr.Get(key1)
lib.Assert(ok)
lib.PtrszAssign(&cr1, val1)
val2, ok := tr.Get(key2)
lib.Assert(ok)
lib.PtrszAssign(&cr2, val2)
// ...
cr1.Amt -= amt
cr1.NumDeb++
cr2.Amt += amt
cr2.NumCred++
tr.DependVal(key1, val1)
tr.Set(key1, lib.PtrToPtrsz(&cr1))
tr.DependVal(key2, val2)
tr.Set(key2, lib.PtrToPtrsz(&cr2))
randSleep(sleep_in)
if err := tr.Commit(nil, nil); err != nil {
return false
}
return true
}()
if !ok {
continue
}
} else {
// ...
}
break
}
randSleep(sleep_out)
}
l2.ArriveAndWait()
}
|
Это функция myRun2(), использующая транзакции.
И как я уже не раз упоминал, наш выигрыш в том, что блокирующими операциями здесь будут лишь начало и конец транзакции: myDB.StartTrn() и tr.Commit(). А все что между ними -- абсолютно параллельно!
Тем самым, все потоки одновременно треплют свои данные в транзакциях и отдыхают в randSleep(sleep_in). Красота? Красота!
А сейчас мы сравним то, что стало:
| 1ms 1ms | Windows 10, go1.24.3 | ||||||||
| num_threads | 1 | 2 | 3 | 4 | 8 | 16 | 32 | 64 | 128 |
| go/my1 | 1.0 | 1.1 | 1.1 | 1.2 | 1.6 | 2.2 | 2.2 | 2.2 | 2.3 |
| go/my2 | 1.0 | 1.2 | 1.3 | 1.5 | 2.6 | 5.1 | 10.0 | 19.5 | 28.4 |
и было:
| 1ms 1ms | Ubuntu 16, g++ 5.4.0 | ||||||||
| num_threads | 1 | 2 | 3 | 4 | 8 | 16 | 32 | 64 | 128 |
| std/ders1 | 1.0 | 1.1 | 1.1 | 1.2 | 1.6 | 2.0 | 2.1 | 2.1 | 2.0 |
| std/ders2 | 1.0 | 1.2 | 1.3 | 1.5 | 2.6 | 4.9 | 9.7 | 17.9 | 33.7 |
Ну как, ВПЕЧАТЛЯЕТ?!
Да, фактически то же самое. Поначалу, один в один!
Можно ли было мечтать о ТАКОМ результате на чистом Go?! Нет, практически невозможно.
Я не мечтал, а сел и сделал. Пользуйтесь на здоровье!
Да, речь пойдет о структурах данных, реализованных внутри других структур данных. Да, теперь и на Go!
Скажите, полезно ли это?
Все гораздо полезней, чем кажется! А покуда коня за рога...
mdb.Deque
| lib\mdb\deque.go |
|---|
type DequeReader interface {
Close() error
IsEmpty() bool
Beg() int32
End() int32
Get(pos int32) (Ptrsz, bool)
Key(pos int32) Ptrsz
GetFront() (Ptrsz, bool)
GetBack() (Ptrsz, bool)
}
type Deque interface {
DequeReader
Resize(newBeg int32, newEnd int32)
Clear()
Set(pos int32, val Ptrsz)
Del(pos int32)
AddFront(val Ptrsz) int32
AddBack(val Ptrsz) int32
DelFront() bool
DelBack() bool
TrimFront()
TrimBack()
}
func DequeExists(id Ptrsz, rd Reader) bool { /* ... */ }
func DequeRemove(id Ptrsz, wr Writer) bool { /* ... */ }
func DequeOpenRd(id Ptrsz, rd Reader) DequeReader { /* ... */ }
func DequeOpen(create bool, id Ptrsz, wr Writer) Deque { /* ... */ }
|
Пройдемся по функциям:
DequeExists(id Ptrsz, rd Reader) bool | существует ли Дек id в rd |
DequeRemove(id Ptrsz, wr Writer) bool | удаляет Дек id из wr |
DequeOpenRd(id Ptrsz, rd Reader) DequeReader | открывает Дек id для чтения, если он существует |
DequeOpen(create bool, id lib.Ptrsz, wr Writer) Deque | создает/открывает Дек id для чтения/записи. будет создан, если create==true |
И интерфейсам:
Интерфейс DequeReader |
|
|---|---|
Close() error | деструктор |
IsEmpty() bool | пустой ли |
Beg() int32 | индекс первого элемента |
End() int32 | индекс запоследнего элемента |
Get(pos int32) (Ptrsz, bool) | элемент pos, если он существует |
Key(pos int32) Ptrsz | ключ элемента pos |
GetFront() (Ptrsz, bool) | первый элемент, если он существует |
GetBack() (Ptrsz, bool) | последний элемент, если он существует |
Интерфейс Deque |
|
|---|---|
Resize(newBeg int32, newEnd int32) | изменяет границы Дека, удаляя элементы за их пределами |
Clear() | удаляет все элементы и устанавливает нулевые границы |
Set(pos int32, val Ptrsz) | присваивает val элементу pos |
Del(pos int32) | удаляет элемент pos |
AddFront(val Ptrsz) int32 | добавляет val в начало Дека и возвращает индекс. изменяет границу |
AddBack(val Ptrsz) int32 | добавляет val в конец Дека и возвращает индекс. изменяет границу |
DelFront() bool | удаляет первый элемент, если он существует. изменяет границу |
DelBack() bool | удаляет последний элемент, если он существует. изменяет границу |
TrimFront() | изменяет начальную границу, отбрасывая несуществующие элементы |
TrimBack() | изменяет конечную границу, отбрасывая несуществующие элементы |
Повторяться не буду, смотрите оригинал. Тема очень глубокая!
Но реальный пример приведу:
| examples\deque\main.go |
|---|
func main() {
db := mdb.NewMemDb()
defer func() { lib.AssertNil(db.Close()) }()
bk := AddCustomer(db, S2P("Brian Kernighan"))
dr := AddCustomer(db, S2P("Dennis Ritchie"))
bs := AddCustomer(db, S2P("Bjarne Stroustrup"))
PrnDeques(db)
PrnDb(db)
AddOrder(db, bk, S2P("Somewhere Far Beyond"))
AddOrder(db, bk, S2P("Imaginations from the Other Side"))
AddOrder(db, dr, S2P("Seventh Son of a Seventh Son"))
AddOrder(db, dr, S2P("Brave New World"))
AddOrder(db, bs, S2P("Walls of Jericho"))
AddOrder(db, bs, S2P("Keeper of the Seven Keys"))
PrnDeques(db)
PrnDb(db)
SetCustomer(db, bk, S2P("updated customer"))
SetOrder(db, bk, 1, S2P("updated order"))
DelOrder(db, dr, 0)
DelCustomer(db, bs)
PrnDeques(db)
PrnDb(db)
}
func SetOrder(db mdb.MemDb, custN int32, ordN int32, val lib.Ptrsz) {
db.Write(func(wr mdb.Writer) {
dqc := mdb.DequeOpenRd(DqName, wr)
if dqc != nil {
defer func() { lib.AssertNil(dqc.Close()) }()
}
CheckPos(dqc, custN)
dqo := mdb.DequeOpen(false, dqc.Key(custN), wr)
if dqo != nil {
defer func() { lib.AssertNil(dqo.Close()) }()
}
CheckPos(dqo, ordN)
dqo.Set(ordN, val)
})
}
|
Все та же музыка, все те же лица:
beg()=0 end()=3 0 Brian Kernighan 1 Dennis Ritchie 2 Bjarne Stroustrup beg()=0 end()=3 0 Brian Kernighan 0 beg()=0 end()=2 0 0 Somewhere Far Beyond 0 1 Imaginations from the Other Side 1 Dennis Ritchie 1 beg()=0 end()=2 1 0 Seventh Son of a Seventh Son 1 1 Brave New World 2 Bjarne Stroustrup 2 beg()=0 end()=2 2 0 Walls of Jericho 2 1 Keeper of the Seven Keys beg()=0 end()=3 0 updated customer 0 beg()=0 end()=2 0 0 Somewhere Far Beyond 0 1 updated order 1 Dennis Ritchie 1 beg()=1 end()=2 1 1 Brave New World 2 |
mdb.List
| lib\mdb\list.go |
|---|
type ListReader interface {
Close() error
IsEmpty() bool
Belongs(key Ptrsz) bool
KeyFront() (Ptrsz, bool)
KeyBack() (Ptrsz, bool)
KeyPrev(pos Ptrsz) (Ptrsz, bool)
KeyNext(pos Ptrsz) (Ptrsz, bool)
}
type List interface {
ListReader
Clear() int
AddFront(key Ptrsz)
AddBack(key Ptrsz)
AddBefore(pos Ptrsz, key Ptrsz)
AddAfter(pos Ptrsz, key Ptrsz)
DelFront() bool
DelBack() bool
Del(key Ptrsz)
}
func ListExists(id Ptrsz, rd Reader) bool { /* ... */ }
func ListRemove(id Ptrsz, wr Writer) bool { /* ... */ }
func ListOpenRd(id Ptrsz, rd Reader) ListReader { /* ... */ }
func ListOpen(create bool, id Ptrsz, wr Writer) List { /* ... */ }
|
И мы снова пройдемся по функциям:
ListExists(id Ptrsz, rd Reader) bool | существует ли Список id в rd |
ListRemove(id Ptrsz, wr Writer) bool | удаляет Список id из wr |
ListOpenRd(id Ptrsz, rd Reader) ListReader | открывает Список id для чтения, если он существует |
ListOpen(create bool, id lib.Ptrsz, wr Writer) List | создает/открывает Список id для чтения/записи. будет создан, если create==true |
И интерфейсам:
Интерфейс ListReader |
|
|---|---|
Close() error | деструктор |
IsEmpty() bool | пустой ли |
Belongs(key Ptrsz) bool | принадлежит ли key |
KeyFront() (Ptrsz, bool) | первый ключ, если он существует |
KeyBack() (Ptrsz, bool) | последний ключ, если он существует |
KeyPrev(pos Ptrsz) (Ptrsz, bool) | предшествующий pos ключ, если он существует |
KeyNext(pos Ptrsz) (Ptrsz, bool) | следующий за pos ключ, если он существует |
Интерфейс List |
|
|---|---|
Clear() int | удаляет все элементы и возвращает количество |
AddFront(key Ptrsz) | добавляет key в начало |
AddBack(key Ptrsz) | добавляет key в конец |
AddBefore(pos Ptrsz, key Ptrsz) | добавляет key перед pos |
AddAfter(pos Ptrsz, key Ptrsz) | добавляет key после pos |
DelFront() bool | удаляет первый ключ, если он существует |
DelBack() bool | удаляет последний ключ, если он существует |
Del(key Ptrsz) | удаляет key |
И, конечно же, будет тот самый пример:
| examples\list\main.go |
|---|
func main() {
db := mdb.NewMemDb()
defer func() { lib.AssertNil(db.Close()) }()
db.Write(func(wr mdb.Writer) {
lst1 := mdb.ListOpen(true, LstId1, wr)
defer func() { lib.AssertNil(lst1.Close()) }()
lst1.AddBack(S2P("key1"))
lst1.AddBack(S2P("key2"))
lst2 := mdb.ListOpen(true, LstId2, wr)
defer func() { lib.AssertNil(lst2.Close()) }()
lst2.AddBack(S2P("key2"))
lst2.AddBack(S2P("key3"))
})
PrnLst(db, LstId1)
PrnLst(db, LstId2)
PrnDb(db)
db.Write(func(wr mdb.Writer) {
wr.Set(S2P("key1"), S2P("val1"))
wr.Set(S2P("key2"), S2P("val2"))
wr.Set(S2P("key3"), S2P("val3"))
})
PrnLst(db, LstId1)
PrnLst(db, LstId2)
PrnDb(db)
db.Write(func(wr mdb.Writer) {
wr.Set(S2P("key2"), S2P("updated"))
lst1 := mdb.ListOpen(true, LstId1, wr)
defer func() { lib.AssertNil(lst1.Close()) }()
lst1.AddBefore(S2P("key2"), S2P("key1.5"))
lst2 := mdb.ListOpen(true, LstId2, wr)
defer func() { lib.AssertNil(lst2.Close()) }()
lst2.Del(S2P("key3"))
})
PrnLst(db, LstId1)
PrnLst(db, LstId2)
PrnDb(db)
}
|
И получится все то же самое:
PrnLst(list1) key1 key2 PrnLst(list2) key2 key3 PrnLst(list1) key1 val1 key2 val2 PrnLst(list2) key2 val2 key3 val3 PrnLst(list1) key1 val1 key1.5 key2 updated PrnLst(list2) key2 updated |
mdb.BlobMap
| lib\mdb\blobmaprw.go |
|---|
func NewBlobMapReader(bm *BlobMap) Reader {
return &blobMapRW{bm, nil}
}
func NewBlobMapWriter(bm *BlobMap) Writer {
return &blobMapRW{bm, bm}
}
type blobMapRW struct {
rd *BlobMap
wr *BlobMap
}
|
Т.е. пара удобных функций, превращающих отдельно стоящий BlobMap в жутко полезные Reader и Writer.
Вау, офигеть! А зачем?
Ну, затем, что и Список и Дек теперь могут юзать наш BlobMap напрямую! И на Go это ОЧЕНЬ И ОЧЕНЬ полезно!!
А суть в том, что BlobMap не создает проблемы мусорщику: вы можете держать хоть сотни миллионов элементов.
Ох... И наверное что-то еще?
Несомненно! Ведь MemDb реализован тем же самым способом и может без Проблемы содержать такие же объемы данных... То есть ЛЮБЫЕ!!
Ведь Удивительная Магия mdb.OffPool зовет использовать Структуры в любом серьезном приложении.
Но для начала следует вкурить оригинал:
Там сразу осознаете, что под крышечкой и как (не) надо действовать! Хотя можно и просто трясти.Главное, вы увидели, что невозможное возможно! Don't let your thoughts be narrowed. Period.
Никакая часть данного материала не может быть использована в коммерческих целях без письменного разрешения автора.