接上一篇:https://blog.csdn.net/lin20044140410/article/details/104450727
在mmkv的使用中,肯定是有多線程,多進程的同步問題,有同步問題就肯定會用到鎖,所以先從mmkv中鎖的使用說起,mmkv處理線程的同步使用了mutex互斥鎖, 比如在從集合中獲取mmkv的c++層的對象時,就加了鎖,因爲可能會有多個線程同時操作的情況;
處理進程間的同步時使用了flock文件鎖,比如在處理寫指針的同步,內存重整時.
以下鎖的使用都是在native層.
1,先看下互斥鎖是怎麼用的,這個demo是展示線程鎖的使用,進一步引出mmkv對鎖的使用時怎麼封裝的。
queue<int> m_queue;
void *dequeue(void *args) {
if (!m_queue.empty()) {
printf("dequeue data :%d \n", m_queue.front());
__android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,dequeue data :%d \n", m_queue.front());
m_queue.pop();
} else {
__android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,no data");
}
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_test_mmkvdemo_MainActivity_nativeThreadTest(JNIEnv *env, jobject instance) {
for (int i = 0; i < 5; ++i) {
m_queue.push(i);
}
pthread_t tid[10];
for (int i = 0; i < 10; ++i) {
pthread_create(&tid[i], 0, dequeue, &m_queue);
}
__android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,m_queue," );
}
在這個demo中,先往一個隊列放了5個元素,然後起了10個線程來取隊列中的元素,
2020-02-24 21:49:29.213 20930-20949/? D/mmap: nativeThreadTest,dequeue data :0 ,tid= 3529775472
2020-02-24 21:49:29.214 20930-20948/? D/mmap: nativeThreadTest,dequeue data :1 ,tid= 3530815856
2020-02-24 21:49:29.214 20930-20950/? D/mmap: nativeThreadTest,dequeue data :1 ,tid= 3528735088
2020-02-24 21:49:29.214 20930-20951/? D/mmap: nativeThreadTest,dequeue data :3 ,tid= 3527694704
2020-02-24 21:49:29.215 20930-20953/? D/mmap: nativeThreadTest,dequeue data :4 ,tid= 3525613936
2020-02-24 21:49:29.215 20930-20954/? D/mmap: nativeThreadTest,dequeue data :4 ,tid= 3524573552
2020-02-24 21:49:29.215 20930-20952/? D/mmap: nativeThreadTest,dequeue data :-374526740 ,tid= 3526654320
2020-02-24 21:49:29.216 20930-20956/? D/mmap: nativeThreadTest,dequeue data :-374526724 ,tid= 3522492784
2020-02-24 21:49:29.216 20930-20930/? D/mmap: nativeThreadTest,m_queue,
2020-02-24 21:49:29.216 20930-20955/? D/mmap: nativeThreadTest,dequeue data :-374526708 ,tid= 3523533168
2020-02-24 21:49:29.216 20930-20957/? D/mmap: nativeThreadTest,dequeue data :-374526692 ,tid= 3521452400
從輸出看,線程取出的數據出現了重複,並且還可能取出未知的數據,這是因爲多線程去操作共享數據沒有加鎖,導致的髒讀。
給操作pop的方法加上互斥鎖,可以解決髒讀的問題,這裏用的是pthead_mutex_t互斥量,這個鎖默認是非遞歸鎖,也就是不可重入,對於不可重入鎖,如果一個線程內多次獲取同一個這樣的鎖,就會產生死鎖,所以在pthread_mutex_t鎖初始化時,需要設置屬性pthread_mutexattr_t,把它設置爲遞歸鎖。 pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE);
queue<int> m_queue;
pthread_mutex_t m_mutex;
pthread_mutexattr_t m_attr;
void *dequeue(void *args) {
pthread_mutex_lock(&m_mutex);
pthread_t tid = pthread_self();
if (!m_queue.empty()) {
printf("dequeue data :%d \n", m_queue.front());
__android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,dequeue data :%d ,tid= %u\n",
m_queue.front(), (unsigned int)tid);
m_queue.pop();
} else {
__android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,no data,tid= %u\n",(unsigned int)tid);
}
pthread_mutex_unlock(&m_mutex);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_test_mmkvdemo_MainActivity_nativeThreadTest(JNIEnv *env, jobject instance) {
pthread_mutexattr_init(&m_attr);
pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&m_mutex, &m_attr);
pthread_mutexattr_destroy(&m_attr);
for (int i = 0; i < 5; ++i) {
m_queue.push(i);
}
pthread_t tid[10];
for (int i = 0; i < 10; ++i) {
pthread_create(&tid[i], 0, dequeue, &m_queue);
}
pthread_mutex_destroy(&m_mutex);
__android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,m_queue," );
}
鎖使用過程中,調用pthread_mutex_lock(&m_mutex); 上鎖,一定要記得調用pthread_mutex_unlock(&m_mutex);解鎖,如果不小心忘記了解鎖,那肯定就悲劇了,而且如果要加個鎖,需要寫那麼多代碼,肯定也會不爽,所以就有了對鎖的封裝,實現鎖的自動管理。
先用c++類的構造方法和析構方法進行封裝,在構造函數中進行鎖及屬性的初始化,在析構函數中銷燬鎖對象。
class ThreadLock {
private:
pthread_mutex_t m_lock;
public:
ThreadLock() {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&m_lock, &attr);
pthread_mutexattr_destroy(&attr);
}
~ThreadLock() {
pthread_mutex_destroy(&m_lock);
}
void lock() {
auto result = pthread_mutex_lock(&m_lock);
if (result != 0) {
//failed.
}
}
bool try_lock() {
auto result = pthread_mutex_lock(&m_lock);
return (result == 0);
}
void unlock() {
auto result = pthread_mutex_unlock(&m_lock);
if (0 != result){
//failed.
}
}
};
這樣封裝了以後,儘管簡單了很多,但是依然可能會因爲忘記unlock,導致悲劇,爲了確保使用過程中不至於lock之後,忘記unlock,就需要更進一步的封裝,就是在創建一個class,讓他持有一個ThreadLock的對象,在這個類的構造方法中上鎖,在其析構方法中解鎖.
template <typename T>
class ScopedLock {
T *m_lock;
// just forbid it for possibly misuse
ScopedLock(const ScopedLock<T> &other) = delete;
ScopedLock &operator=(const ScopedLock<T> &other) = delete;
public:
ScopedLock(T *oLock) : m_lock(oLock) {
assert(m_lock);
lock();
}
~ScopedLock() {
unlock();
m_lock = nullptr;
}
void lock() {
if (m_lock) {
m_lock->lock();
}
}
bool try_lock() {
if (m_lock) {
return m_lock->try_lock();
}
return false;
}
void unlock() {
if (m_lock) {
m_lock->unlock();
}
}
};
如此封裝後,使用方法就是在代碼塊中僅需要創建鎖對象即可實現上鎖,在代碼塊結束,會隨着鎖對象生命週期結束,調用其析構函數釋放鎖.
{
ThreadLocak * thread_lock = new ThreadLock();
ScopedLock scoped_lock(thread_lock);
}
ScopedLock類是一個模板類,實際在mmkv中,文件鎖中讀鎖和寫鎖,及線程鎖都是通過這個模板類使用的.
上面的封裝對鎖的使用已經非常方便了,然後mmkv又讓鎖的使用更加的便捷,那就是又定義了宏函數簡化了鎖的使用.
#define SCOPEDLOCK(lock) _SCOPEDLOCK(lock, __COUNTER__)
#define _SCOPEDLOCK(lock, counter) __SCOPEDLOCK(lock, counter)
#define __SCOPEDLOCK(lock, counter) ScopedLock<decltype(lock)> __scopedLock##counter(&lock)
第一個宏函數,把ThreadLock對象傳進去,會調用第二個宏函數,
第二個宏函數,除了傳入的ThreadLock參數外,還加了一個編譯器中宏定義,__COUNTER__在編譯時會被替換成具體的值0,1,2,...
它就相當於一個計數器,記錄了這個宏函數在一個編譯單元中被調用了多少次.
第三個宏函數,就是創建了一個具體的鎖對象,
ScopedLock<decltype(lock)> __scopedLock##counter(&lock)
decltype(lock)獲取lock的具體類型,加上計數值,就得到了一個ScopedLock類型的鎖對象,
第一次調用 __scopedLock0,
第二次調用 __scopedLock1,
第三次調用 __scopedLock2 ,....
2,接着看下文件鎖.
儘管pthread_mutex可以用作進程鎖,但是因爲andorid版本的pthread_mutex不夠健壯,如果加了pthread_mutex鎖的進程被kill了,系統不會進行清理工作,這個鎖會一直存在下去,其他等待鎖的進程會被餓死.
https://github.com/Tencent/MMKV/wiki/android_ipc
多進程的實現中用到了文件鎖,多進程同時操作一個文件時,可能存在A進程去寫這個文件,而B進程去讀這個文件,就會導致B進程讀取到的是髒數據.爲了保證B進程可以讀取到最新的數據,並且保證數據的完整性,使用文件鎖來完成多進程操作文件的同步.
文件鎖的使用:
#include <sys/file.h>
//通過open方法打開一個文件
string m_path;
int m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
//通過文件句柄對文件上鎖
int flock(m_fd, operation);
其中的參數operation是上鎖的類型,flock支持兩中鎖類型LOCK_SH,LOCK_EX
LOCK_SH, 共享鎖,多個進程可以同時使用,可以作爲讀鎖.
LOCK_EX,排他鎖,同時只允許一個進程使用,可以作爲寫鎖.
LOCK_UN, 解鎖
LOCK_BN, 非阻塞請求, 與讀寫鎖配合使用.
使用flock對一個文件上讀鎖,或者寫鎖,都是會阻塞的,比如A進程對持有一個文件的寫鎖,B進程想要對這個文件上寫鎖,就會阻塞住,如果不想被阻塞,可以配合LOCK_BN屬性使用,即LOCK_BN | LOCK_EX.
flock有幾個特點:
1)flock支持對一個文件多次上鎖,並且因爲是狀態鎖,沒有計數器,不管加了多少次鎖,都只需要解鎖一次.所以,mmkv中對flock封裝時,加了計數器,就是保證上了幾次鎖,就要執行幾次解鎖.
2)鎖升級,降級,當一個進程對一個文件加了讀鎖後,如果再次執行flock操作,傳入的operation是LOCK_EX,那麼這個進程對文件的讀鎖就升級爲了寫鎖,這就是鎖升級,反之,就是鎖降級,但是文件鎖降級是無法進行的,因爲他不支持遞歸,導致一降級就沒鎖了.
爲了解決上面的問題,mmkv對文件鎖進行了封裝,增加了讀寫鎖計數器,支持遞歸.
結合源碼看mmkv是怎麼多的.
InterProcessLock.h
class FileLock {
int m_fd;
size_t m_sharedLockCount;
size_t m_exclusiveLockCount;
bool doLock(LockType lockType, bool wait);
}
增加了讀寫鎖計數器,通過調用doLock對文件上鎖.locktype決定了上鎖的類型,第二個參數是否阻塞.所以重點就是doLock是怎麼實現的.
上鎖的處理邏輯:
1),//先判斷上的是什麼鎖
//上讀鎖,計數器先加1,如果加1後,讀鎖計數器大於1了,說明已經上過讀鎖了,爲了實現遞歸,這種情況就不在執行上鎖,只是增加了計數器,如果這個文件的寫鎖計數器大於0,說明已經上過寫鎖了,當前還在用寫鎖,所以不
//執行上讀鎖的操作,會導致鎖降級,所以直接返回
//如果是第一次上讀鎖,後面會執行加鎖操作
2),/上寫鎖,計數器先加1,判斷如果已經上過寫鎖,直接return.
//如果讀鎖計數器大於0,這時會先嚐試上寫鎖,如果失敗了,說明有別的進程在使用這個文件的讀鎖,
//這種情況就要先把自己的讀鎖釋放掉,再去以阻塞的形式上寫鎖,這麼做的原因是爲了避免死鎖.
//假設,A進程沒有解自己的讀鎖,直接上寫鎖在這裏阻塞着,如果別的進程B進程剛好釋放了讀鎖,那麼當前進程//A進程上寫鎖
//就成功了,但是,如果別的進程B集成也要上寫鎖,因爲A進程的讀鎖沒有釋放,會導致B進程上寫鎖的操作一直
//阻塞着,這就是A,B進程都在等對方的讀鎖釋放,而導致死鎖.
bool FileLock::doLock(LockType lockType, bool wait) {
if (!isFileLockValid()) {
return false;
}
bool unLockFirstIfNeeded = false;
//先判斷上的是什麼鎖
//上讀鎖,計數器先加1,如果加1後,讀鎖計數器大於1了,說明已經上過讀鎖了,爲
了實現遞歸,這種情況就不在執行上鎖,只是增加了計數器,如果這個文件的寫鎖計數器大於0,
說明已經上過寫鎖了,當前還在用寫鎖,所以不
//執行上讀鎖的操作,會導致鎖降級,所以直接返回
//如果是第一次上讀鎖,後面會執行加鎖操作
if (lockType == SharedLockType) {
m_sharedLockCount++;
// don't want shared-lock to break any existing locks
if (m_sharedLockCount > 1 || m_exclusiveLockCount > 0) {
return true;
}
} else {
//上寫鎖,計數器先加1,判斷如果已經上過寫鎖,直接return.
//如果讀鎖計數器大於0,這時會先嚐試上寫鎖,如果失敗了,說明有別的進程在使用這個文件的讀鎖,
//這種情況就要先把自己的讀鎖釋放掉,再去以阻塞的形式上寫鎖,這麼做的原因是爲了避免死鎖.
//假設,A進程沒有解自己的讀鎖,直接上寫鎖在這裏阻塞着,如果別的進程B進程剛好釋放了讀鎖,那麼當前進程//A進程上寫鎖
//就成功了,但是,如果別的進程B集成也要上寫鎖,因爲A進程的讀鎖沒有釋放,會導致B進程上寫鎖的操作一直
//阻塞着,這就是A,B進程都在等對方的讀鎖釋放,二導致死鎖.
m_exclusiveLockCount++;
// don't want exclusive-lock to break existing exclusive-locks
if (m_exclusiveLockCount > 1) {
return true;
}
// prevent deadlock
if (m_sharedLockCount > 0) {
unLockFirstIfNeeded = true;
}
}
return platformLock(lockType, wait, unLockFirstIfNeeded);
}
再看解鎖的處理邏輯:
1),//如果是解讀鎖,讀鎖計數器爲0,直接返回,
//如果讀鎖計數器 減1後,還大於了,爲了實現遞歸鎖,那麼上幾次讀鎖,就要解幾次讀鎖,所以直接
//返回,
//如果這裏有寫鎖,也直接返回,因爲如果執行LOCK_UN,就把寫鎖也解了.
2),//如果解寫鎖,計數器爲0,直接返回,
//如果計數器減1後大於0,直接返回
//否則,如果寫鎖計數器爲0了,但是有讀鎖,也不能直接LOCK_UN,而是要做鎖降級,
bool FileLock::unlock(LockType lockType) {
if (!isFileLockValid()) {
return false;
}
bool unlockToSharedLock = false;
//如果是解讀鎖,讀鎖計數器爲0,直接返回,
//如果讀鎖計數器 減1後,還大於了,爲了實現遞歸鎖,那麼上幾次讀鎖,就要解幾次讀鎖,所以直接
//返回,
//如果這裏有寫鎖,也直接返回,因爲如果執行LOCK_UN,就把寫鎖也解了.
if (lockType == SharedLockType) {
if (m_sharedLockCount == 0) {
return false;
}
m_sharedLockCount--;
// don't want shared-lock to break any existing locks
if (m_sharedLockCount > 0 || m_exclusiveLockCount > 0) {
return true;
}
} else {
//如果解寫鎖,計數器爲0,直接返回,
//如果計數器減1後大於0,直接返回
//否則,如果寫鎖計數器爲0了,但是有讀鎖,也不能直接LOCK_UN,而是要做鎖降級,
if (m_exclusiveLockCount == 0) {
return false;
}
m_exclusiveLockCount--;
if (m_exclusiveLockCount > 0) {
return true;
}
// restore shared-lock when all exclusive-locks are done
if (m_sharedLockCount > 0) {
unlockToSharedLock = true;
}
}
return platformUnLock(unlockToSharedLock);
}
3,這樣文件鎖就封裝好了,利用這個文件鎖就可以實現同一時間只有一個進程對file進行操作了,但是A進程修改了文件後,B進程怎麼知道這個修改呢?
針對上面的問題,mmkv是做的呢? 實際跟我們想象的並不一樣,mmkv並沒有去對保存key-value數據的那個default文件枷鎖,而是鎖了個.crc校驗文件.這個校驗文件就是來解決上面的問題的.
struct MMKVMetaInfo {
uint32_t m_crcDigest = 0;
uint32_t m_version = 1;
uint32_t m_sequence = 0; // full write-back count
unsigned char m_vector[AES_KEY_LEN] = {0};
}
這個mmkv.default.crc文件中有一個校驗碼,就是mmkv.default文件的MD5值,通過他可以判斷文件是否合法.
還有一個序列號,當序列化文件進行了去重,擴容操作,這個序列號就會增加,
具體看源碼是怎麼做的?
1)當mmkv初始化時,會讀取crc文件,記錄其中的校驗碼,序列號,,
2)當要讀取數據時,會通過checkLoadData來做校驗.
void MMKV::checkLoadData() {
//讀取校驗文件,對比序列號,如果序列號不一致,說明發生了內存重整,
//這時,重新讀取整個文件,
MMKVMetaInfo metaInfo;
metaInfo.read(m_metaFile.getMemory());
if (m_metaInfo.m_sequence != metaInfo.m_sequence) {
SCOPEDLOCK(m_sharedProcessLock);
clearMemoryState();
loadFromFile();
notifyContentChanged();
}else if (m_metaInfo.m_crcDigest != metaInfo.m_crcDigest) {
//校驗碼不匹配,說明數據文件被修改了,可能時數據增加,也可能是去重操作,還可能時文件擴容了
//如果文件大小不一致了,可能發生了擴容,這時重新加載整個文件,
//文件大小一致,說明只是新增了k-v,也即是增量更新,這時只要加載新增加的數據.
size_t fileSize = 0;
if (m_isAshmem) {
fileSize = m_size;
} else {
struct stat st = {0};
if (fstat(m_fd, &st) != -1) {
fileSize = (size_t) st.st_size;
}
}
if (m_size != fileSize) {
MMKVInfo("file size has changed [%s] from %zu to %zu",
m_mmapID.c_str(), m_size,
fileSize);
clearMemoryState();
loadFromFile();
} else {
partialLoadFromFile();
}
}
}
所以,通過校驗文件,在讀取數據時,來做校驗,就實現了多個進程的數據同步.