在之前一直用的持久化內存,現在感覺有一種說不出的怪怪的感覺,之後都改爲持久性內存。
前面介紹了訪問持久性內存的方式,其中拋出了一些在持久性內存上編程的要點,接下來就翻譯pmem.io上的第二個編程指導——事務。
原文來自:http://pmem.io/2015/06/15/transactions.html
目錄
通過前一部分的介紹(https://blog.csdn.net/SweeNeil/article/details/90296581),現在應該對持久性內存編程的基礎知識非常熟悉了。
了確保應用程序的一致性,通過之前學習的知識,我們還必須依賴自己的解決方案和技巧 - 例如前一個示例中的緩衝區長度。
本文將介紹pmemobj庫爲這類問題提供的通用解決方案 - 事務。
目前我們僅關注沒有加鎖的單線程應用程序。
生命週期
在pmemobj庫中,通過使用pmemobj_tx_ *系列函數來管理事務。
單個事務將經歷enum pobj_tx_stage中列出的一系列階段,其過程如下圖所示:
可以使用pmemobj_tx_process函數代替其他函數來移動事務 - 如果不知道當前處於哪個階段,可以調用它。
爲了避免對整個過程進行微觀管理,pmemobj庫提供了一組構建在這些函數之上的宏,這些宏極大地簡化了事務的使用,本教程將專門使用它們。
下面就是整個事務塊的樣子:
/* TX_STAGE_NONE */
TX_BEGIN(pop) {
/* TX_STAGE_WORK */
} TX_ONCOMMIT {
/* TX_STAGE_ONCOMMIT */
} TX_ONABORT {
/* TX_STAGE_ONABORT */
} TX_FINALLY {
/* TX_STAGE_FINALLY */
} TX_END
/* TX_STAGE_NONE */
從上面我們可以看出,這與生命週期圖非常密切相關。
除TX_BEGIN和TX_END之外的所有代碼塊都是可選的。
可以沒有任何限制地嵌套事務,遞歸事務在技術上也是可以的。
如果嵌套事務中止,則整個事務將中止。
開發者可能想知道爲什麼存在TX_FINALLY階段,爲什麼不在事務塊之後執行該代碼 - 好吧,由於事務工的作方式(在開始時setjmp和所有中止的longjmp),不能保證代碼是在嵌套事務中的TX_END之後直接執行。
例如如下代碼:
void do_work() {
struct my_task *task = malloc(sizeof *task);
if (task == NULL) return;
TX_BEGIN(pop) {
/* important work */
pmemobj_tx_abort(-1);
} TX_END
free(task);
}
...
TX_BEGIN(pop)
do_work();
TX_END
此代碼段有內存泄漏,它永遠不會調用free,因爲do_work中的TX_END最終會使longjmp返回到外部事務 實現do_work的正確方法是使用TX_FINALLY:
void do_work() {
volatile struct my_task *task = NULL;
TX_BEGIN(pop) {
task = malloc(sizeof *task);
if (task == NULL) pmemobj_tx_abort(ENOMEM);
/* important work */
pmemobj_tx_abort(-1);
} TX_FINALLY {
free(task);
} TX_END
}
上述代碼保證了finally塊總是會被執行。
另請注意TX_FINALLY塊中volatile限定變量的用法。 這是因爲本地非易失性限定對象如果它們的值在setjmp之後已更改,在執行longjmp後具有未定義的值。
因此,對於libpmemobj事務塊,在TX_STAGE_WORK中修改並在TX_STAGE_ONABORT / TX_STAGE_FINALLY中使用的每個局部變量都需要進行volatile限定 - 否則可能會遇到未定義的行爲。
有關更多信息,可以參考libpmemobj manpage 中的CAVEATS部分。
事務操作
pmemobj庫區分了3種不同的事務操作:分配(allocation),釋放(free)和設置(set)。
在本文中只介紹最後一個,顧名思義,它用於安全地將內存塊設置爲某個值。 這是通過2個API函數實現的:
- pmemobj_tx_add_range
- pmemobj_tx_add_range_direct
引用文檔:
獲取內存塊的“快照”...並將其保存在撤消日誌中,應用程序可以直接修改該內存範圍內的對象,如果發生故障或中止,此範圍內的所有更改都將自動回滾。
這意味着當調用上述兩個函數中的任何一個時,將分配一個新對象並將內存範圍的現有內容複製到其中。
除非庫在事務回滾中需要舊內存,否則該對象將被丟棄。另請注意,該庫假定當添加要寫入的內存範圍時,並且在提交事務時內存會自動保留 - 因此不必自己調用pmemobj_persist。
那麼如何使用這些功能呢? pmemobj_tx_add_range採用原始持久性內存指針(PMEMoid),它與它的偏移量及其大小。 所以可以在這個結構中設置一些值:
struct vector {
int x;
int y;
int z;
}
PMEMoid root = pmemobj_root(pop, sizeof (struct vector));
簡單的使用方式如下:
struct vector *vectorp = pmemobj_direct(root);
TX_BEGIN(pop) {
pmemobj_tx_add_range(root, offsetof(struct vector, x), sizeof(int));
vectorp->x = 5;
pmemobj_tx_add_range(root, offsetof(struct vector, y), sizeof(int));
vectorp->y = 10;
pmemobj_tx_add_range(root, offsetof(struct vector, z), sizeof(int));
vectorp->z = 15;
} TX_END
但這不是最優的 - 使用該方法在撤消日誌中添加三個對象。
其實單個撤消日誌條目的大小至少等於128字節就足夠了,最好一次添加整個對象:
struct vector *vectorp = pmemobj_direct(root);
TX_BEGIN(pop) {
pmemobj_tx_add_range(root, 0, sizeof (struct vector));
vectorp->x = 5;
vectorp->y = 10;
vectorp->z = 15;
} TX_END
這樣就不會爲元數據浪費不必要的內存,效果將完全相同。
pmemobj_tx_add_range_direct執行相同的操作,但是以更方便的方式用於某些用途,它直接引用字段及其大小,例如:
struct vector *vectorp = pmemobj_direct(root);
int *to_modify = &vectorp->x;
TX_BEGIN(pop) {
pmemobj_tx_add_range_direct(to_modify, sizeof (int));
*to_modify = 5;
} TX_END
當沒有簡單的方法來訪問此內存塊所屬的PMEMoid時,這非常有用。
條件事務塊
It might seem that and explanation isn’t really required, one is called when the transaction commits and the other one when it aborts - simple as that. As long as there are no inner transactions, that is true. But once we start nesting, things get a little bit more complicated. Consider the following example:
TX_ONCOMMIT
和TX_ONABORT,
一個在事務提交時調用,另一個在事務被中止時調用。只要沒有內部事務,這就很簡單,但一旦我們開始事務層疊,事情就變得有點複雜了。考
慮以下代碼:
#define MAX_HASHMAP 1000
TOID(struct hash_entry) hashmap[MAX_HASHMAP]; /* volatile hashmap */
void hash_set(int key, int value) {
TOID(struct hash_entry) nentry;
TX_BEGIN(pop) {
nentry = TX_NEW(struct hash_entry);
D_RW(nentry)->key = key;
D_RW(nentry)->value = value;
} TX_ONCOMMIT {
size_t hash = hash_func(key);
if (TOID_IS_NULL(hashmap[hash]))
hashmap[hash] = nentry;
else
/* ... */
} TX_END
}
TX_BEGIN(pop) {
hash_set(5, 10);
pmemobj_tx_abort(-1);
} TX_END
上述代碼中,一個哈希映射,其中包含持久內存中的條目,但包含它們的易失性hash表。
此代碼正確嗎?
散列值自己設置是完全正常的-但不在另一個事務中。當調用pmemobj_tx_abort函數時,將還原tx_begin塊中的所有內容,但嵌套事務的tx_oncommit已執行(並且不會在該函數中調用tx_onabort),最終結果是volatile表中的無效持久指針。
這通常很難解決,需要每個問題的解決方案-我建議設計應用程序以主動避免它。對於這個特定的用例,您可以有一個額外的hash-revert-previous函數,該函數是從最外層事務的tx-onabort塊調用的。
tx-oncommit和tx-onabort的預期用途是打印日誌信息,並使用嵌套事務設置函數的返回變量,如下所示:
int do_work() {
int ret;
TX_BEGIN(pop) {
} TX_ONABORT {
LOG_ERR("work transaction failed");
ret = 1;
} TX_ONCOMMIT {
LOG("work transaction successful");
ret = 0;
} TX_END
return ret;
}
示例