一、文件處理
作為IO處理的一種重要場景,文件處理是幾乎所有編程都無法繞過的一個情況。稍微復雜的一些的程序都可能需要文件處理,不管這種文件處理對開發者來說是顯式的還是隱式的。相對于其它語言,C++并未提供多么好的文件處理API接口,即使發展到現在,C++新標準的文件處理,相比與C#等語言處理起文件的方式仍然要落后不少。
文件處理相對來說的復雜再加上C++中線程管理的復雜,二者結合到一起,就會產生各種大大小小的問題。
二、多線程與資源控制
其實對開發者來說,不管是文件管理還是其它資源管理,最主要的就是在多線程的切換中,保持安全性(不能崩潰)、數據的準確性(不能寫得不對)。在這些基礎上,如何提高資源處理的速度并按照開發者既定的意愿去完成相關的資源控制,這才是難點所在。
特別在一些具體的場景中,如大文件(海量日志文件)、數據庫操作以及圖像視頻等的處理,這都需要多線程參與下的高效率的處理。在前面學習多線程時知道對資源的控制一般有幾種處理方法:
1、使用鎖
這里的鎖,包括各種的互斥體和信號量等
2、使用原子變量
其實,使用原子變量的目的也類似于鎖
3、使用無鎖編程
這個就相對復雜很多,而且適用場景也受限
三、多線程條件下的文件處理方法
上面的這些方法對于所有的資源控制都是行之有效的,但針對文件處理,可能一些更具體的方法。對于文件來說,需要處理兩種情況即:
1、文件的寫
文件的寫是一種常見的保存數據的方法,數據庫的寫其實也一種文件的寫,只不過,上面又抽象了一層數據庫的相關操作。對于寫文件來說,最基礎的是寫入的完整性、一致性,最重要的是寫入的速度。
多線程的寫入,往往因為同步的問題,引起以下的情況:
a)由于無法同步導致的問題
包括數據寫入順序不一致引起的數據覆蓋以及數據順序的不正確,導致數據的完整性的缺損,從而最終導致文件可能無法打開
b)因為同步導致寫入效率的問題
多線程的情況下,不適當的同步,或者說即使是適當的同步,也會大幅的降低寫文件的速度
c)引入異步IO導致的編程復雜性
異步IO的操作本身就是一個難點,這對于很多開發者說,掌握的都不是很到位
d)是否使用寫緩沖
其實在很多情況,特別是在多線程的情況下,往往會把并發寫轉成串行寫,數據量的增加往往要求引入緩沖區
總之,寫文件,是多線程操作中相對復雜和困難的情況。
寫文件有幾種情況比較特殊,一種是寫入大量的小文件,這種情況在互聯網中特別常見,比如大量的商品的縮略圖;另外一種是寫一個非常大的文件,如日志;另外還有大家常見的如BT等下載軟件,多線程分段下載然后最終再組成一個大文件(分塊處理)。
2、文件的讀
文件的讀相對寫來說應用場景更豐富,在互聯網中針對文件讀還專門有各種的優化方法。比如各種緩沖、臨時文件等等。
多線程情況下的文件讀其實有很多種方法,來適應不同的場景:
a)使用緩沖
這種緩沖既包括硬件本身的緩沖也包括軟件層次的緩沖,甚至是框架之間的緩沖,如使用內存型數據庫(如Redis)+傳統的數據庫(如MySql),前者就可以作為后者的緩沖。而在C++編程中也提供了iosteam的緩沖的控制。其它的一些系統API和庫的API也多少都提供了類似的功能。
b)使用異步IO
異步IO的問題主要就在于異步編程的復雜度,這里不再贅述
c)多線程讀取的時效問題
也就是常見的讀寫同時在進行時,如何保證讀的時效性(即盡可能減少臟讀),特別是在分布式、多線程的情況下。
四、典型的文件處理方式
內存緩沖區 使用新的異步框架 或新技術
典型的文件處理方式一般有以下幾種處理方法:
1、使用內存映射
這個在操作一些大的日志文件時,經常使用這種內存映射,從而快速讀寫日志。
2、使用緩沖
包括前面提到的軟件層次的緩沖、硬件緩沖及內核緩沖等。
3、使用最新的框架或技術
比如前文“Linux新的IO模型io_uring”提到的io_uring以及其它的新技術、新框架甚至是新思想,來解決某些對文件操作極度嚴苛的場景。
4、使用合理的策略
這個就比較靈活了,比如上面提到的大文件的多線程下載,就可以使用合理的策略來進行分塊然后再進行傳輸、驗證、組合等等。另外在數據庫讀寫中,面對大量的寫可以使用批處理,而使用緩沖時,可以在大多數場景下指定臨界值再進行真正的寫入等等。
5、多線程的合理控制
無論何種情況,只要發生在了多線程并發或并行的場景下,合理調試線程和分配任務就是一種高優先級的考慮方向了。也可以這樣認為,面對復雜的文件處理,不是某一層可以解決的,它是從上到下,從里到外,一個綜合應用的場景。
五、實際的例子
下面級出一個大日志文件使用內存映射操作的例程:
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <system_error>class MemMappedFile {
public:explicit MemMappedFile(const std::string& path, bool enable = false) {// 打開文件int flags = enable ? O_RDWR : O_RDONLY;m_fd = open(path.c_str(), flags);if (m_fd == -1) {return;}// 獲取映射文件大小struct stat sb;if (fstat(m_fd, &sb) == -1) {close(m_fd);return ;}m_size = sb.st_size;// 創建內存映射int prot = PROT_READ | (enable ? PROT_WRITE : 0);m_data = mmap(nullptr, m_size, prot, MAP_SHARED, m_fd, 0);if (m_data == MAP_FAILED) {close(m_fd);return;}}~MemMappedFile() {if (m_data != nullptr) {munmap(m_data, m_size);}if (m_fd != -1) {close(m_fd);}}MemMappedFile(const MemMappedFile&) = delete;MemMappedFile& operator=(const MemMappedFile&) = delete;
public:char* data() const { return static_cast<char*>(m_data); }size_t size() const { return m_size; }// 同步修改到磁盤void syncToDisk() {if (msync(m_data, m_size, MS_SYNC) == -1) {return;}}private:int m_fd = -1;void* m_data = nullptr;size_t m_size = 0;
};
// 處理數據
void processData(const char* chunk, size_t size) {// 日志處理,略過
}
int main(int argc, char* argv[]) {if (argc < 2) {std::cerr << "cur use: " << argv[0] << std::endl;return 1;}try {//創建映射bool enable = (argc >= 3);MemMappedFile mmapFile(argv[1], enable);const char* data = mmapFile.data();size_t size = mmapFile.size();size_t lineCount = 0;size_t errNum = 0;const char* keyword = "ERR";for (size_t i = 0; i < size; ++i) {if (data[i] == '\n') {lineCount++;}if (strncmp(&data[i], keyword, strlen(keyword)) == 0) {errNum++;i += strlen(keyword) - 1;}}std::cout << "lines is: " << lineCount<< "errors is: " << errNum << std::endl;if (enable) {const char* newHeader = "start modify log...\n";size_t headerLen = strlen(newHeader);if (size >= headerLen) {memcpy(mmapFile.data(), newHeader, headerLen);mmapFile.syncToDisk();}}//分塊處理const size_t chunkSize = 1024 * 1024;for (size_t offset = 0; offset < size; offset += chunkSize) {size_t chunkLen = std::min(chunkSize, size - offset);processData(data + offset, chunkLen);}} catch (const std::exception& e) {std::cerr << "Error: " << e.what() << std::endl;return 1;}return 0;
}
注釋已經很清晰,大家可以參考一下。
六、總結
多線程下的文件處理,需要整合前面學習的很多知識點。大家不用把它想象的多么難,重點在于分析實際的應用場景,找出一個合適的解決方案就可以了。不是每個開發者都會遇到海量的數據讀寫。但掌握一些海量數據下的文件處理的經驗卻是非常必要的。