一、前言
最近,在項目中涉及到多線程訪問臨界資源的問題。為了保護臨界資源,可以是使用互斥量或者是使用臨界區。由於,我不需要在多進程中同步,又為了效率的考量,所以選擇了使用臨界區的方式。但是,在使用臨界區的時候,發現了一個類是雞生蛋蛋生雞的問題。現將問題和自己的解決方法記錄如下,如有不對之處,還請指教。
二、出現的問題
在項目的開發過程中,需要把視屏流輸出成磁盤的文件,有時候可能有多路視頻流同時需要輸出到各自的文件中去。對於不同路的視頻需要進行分類,不同路的視頻存儲在不同的目錄下。於是在初始化輸出文件的時候,需要設置當前目錄。有多路視頻的時候,每一個都需要在初始化的時候設置當前目錄,於是設置的當前目錄就是臨界資源了。
下面,是我的輸出視頻流文件的類:
1 class CAVIFile 2 { 3 public: 4 CAVIFile(); 5 6 HRESULT OpenFile(const string& strFileName, const string& strDirectoryPath) 7 { 8 // .... 9 SetCurrentDirectory(strDirectoryPath.c_str()); 10 // ... 11 } 12 13 HRESULT WriteVideo(LPVOID lpBuffer, LONG cbBuffer); // 寫入視頻數據 14 HRESULT WriteAudio(LPVOID lpBuffer, LONG cbBuffer); // 寫入獲取到的音頻數據 15 void CloseFile(); 16 17 private: 18 // 一些與本內容無關的成員變量 19 // ... 20 };
可以看出,如果多個線程同時,進行多路視頻的輸出時,都調用OpenFile()初始化輸出文件,當第一個線程設置當前目錄,還沒有初始化完成的時候,第二個線程也調用了OpenFile()設置了不同的當前目錄,那么第一個線程的初始化的當前目錄就會被覆蓋。此時就需要加鎖,這里選擇了臨界區。由於不同CAVIFile對象由不同的線程執行,而不同的CAVIFile對象設置當前目錄,都會相互影響,所以不同的CAVIFile對象需要公用一個臨界區。所以聲明臨界區為靜態成員。
下面是加上臨界區之后的代碼:
1 class CAVIFile 2 { 3 public: 4 CAVIFile() 5 { 6 InitializeCriticalSection(&m_criticalSection); // 初始化臨界區 7 } 8 HRESULT OpenFile(const string& strFileName, const string& strDirectoryPath) 9 { 10 // .... 11 EnterCriticalSection(&m_criticalSection); 12 SetCurrentDirectory(strDirectoryPath.c_str()); 13 // ... 14 LeaveCriticalSection(&m_criticalSection); 15 //... 16 } 17 // ... 18 private: 19 // ... 20 // 整個類設置當前目錄的互斥鎖 21 static CRITICAL_SECTION m_criticalSection; 22 }; 23 CRITICAL_SECTION CAVIFile::m_criticalSection;
但是上面的代碼還是有問題,那么就是每生成一個CAVIFile對象,就會InitializeCriticalSection(&m_criticalSection);一次,這是有問題的,m_criticalSection是靜態成員變量,這個臨界區是CAVIFile類對象共享的,每個m_criticalSection應該只需要InitializeCriticalSection(&m_criticalSection)一次,而這里會InitializeCriticalSection()多次,這樣可能導致資源的泄漏(對一個CRITICAL_SECTION對象InitializeCriticalSection()多次,會出現什么樣的問題,我自己沒有深入研究過,這里只是猜測!!但是CRITICAL_SECTION對象正常只需要InitializeCriticalSection()操作一次)。
1.加靜態變量標識是否需要InitializeCriticalSection()
對此這個InitializeCriticalSection(&m_criticalSection);多次的問題,我看到的一種解決方法是,加一個靜態變量去標識m_criticalSection所代表的臨界區是否已經InitializeCriticalSection(),如果已經InitializeCriticalSection()就不再InitializeCriticalSection()。
代碼如下:
1 class CAVIFile 2 { 3 public: 4 CAVIFile() 5 { 6 if (m_shouldInit) 7 { 8 InitializeCriticalSection(&m_criticalSection); // 初始化臨界區 9 m_shouldInit = false; 10 } 11 } 12 HRESULT OpenFile(const string& strFileName, const string& strDirectoryPath) 13 { 14 // .... 15 EnterCriticalSection(&m_criticalSection); 16 SetCurrentDirectory(strDirectoryPath.c_str()); 17 // ... 18 LeaveCriticalSection(&m_criticalSection); 19 //... 20 } 21 // ... 22 private: 23 // ... 24 // 整個類設置當前目錄的互斥鎖 25 static CRITICAL_SECTION m_criticalSection; 26 static bool m_shouldInit; 27 }; 28 CRITICAL_SECTION CAVIFile::m_criticalSection; 29 bool CAVIFile::m_shouldInit = true;
這其實還是有問題的。在上面的代碼中多線程可能同時訪問m_shouldInit,判斷是否需要InitializeCriticalSection()臨界區,如果已經InitializeCriticalSection(),就不再InitializeCriticalSection()初始化臨界區,這看起來是正確一樣。但是僅僅是看起來。此時,當多線程訪問m_shouldInit的時候,m_shouldInit也變成了臨界資源了。為了保護m_shouldInit,難道我們再定義一個臨界區?那么我們再次定義的臨界區的只能InitializeCriticalSection(&m_criticalSection)一次的問題又出現了,我們再定義另一個靜態變成去標識我們剛剛為了保護m_shouldInit定義的臨界區的只一次InitializeCriticalSection()問題?此時,我們已經落入到蛋生雞,雞生蛋的邏輯中了。
明顯這種解決方法是不正確的!
2、使用單例模式
在單例模式中,一個使用單例模式的類,只能創建一個類的對象。我突然想起,這個類只能創建一個對象,那么也就是只會調用一次構造函數。那么,我可以利用單例模式進行InitializeCriticalSection()這個臨界區。
實現代碼如下:
1 class CAVIFile 2 { 3 public: 4 // ... 5 HRESULT OpenFile(const string& strFileName, const string& strDirectoryPath) 6 { 7 // .... 8 EnterCriticalSection(&m_criticalSection); 9 SetCurrentDirectory(strDirectoryPath.c_str()); 10 // ... 11 LeaveCriticalSection(&m_criticalSection); 12 //... 13 } 14 // ... 15 private: 16 //... 17 // 整個類設置當前目錄的互斥鎖 18 static CRITICAL_SECTION m_criticalSection; 19 private: 20 // Singleton,這個類使用單例模式,為了只初始化一次m_criticalSection 21 // 餓漢式 22 class Singleton 23 { 24 private: 25 Singleton() { InitializeCriticalSection(&m_criticalSection); } 26 Singleton(const Singleton& other); 27 Singleton& operator=(const Singleton& other); 28 29 static Singleton m_Singleton; 30 }; 31 friend class Singleton; 32 }; 33 CRITICAL_SECTION CAVIFile::m_criticalSection; 34 CAVIFile::Singleton CAVIFile::Singleton::m_Singleton;
這里,在類內定義了一個嵌套類,而嵌套類使用了餓漢式的單例模式,自動只會構造一次,那么就只會對m_criticalSection ,InitializeCriticalSection()一次。這樣就完美的解決了問題。