持久性內存編程——事務

在之前一直用的持久化內存,現在感覺有一種說不出的怪怪的感覺,之後都改爲持久性內存。

前面介紹了訪問持久性內存的方式,其中拋出了一些在持久性內存上編程的要點,接下來就翻譯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中列出的一系列階段,其過程如下圖所示:

lifecycle

可以使用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_ONCOMMITTX_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;
}

示例

here

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章