一、锁
在学习多线程、多进程的开发过程中,为了保证它们之间的通信安全,往往会使用锁来进行保护。锁很容易理解,一般的锁都是a线程使用时锁住,其它线程就无法使用了。
当然,为了适应更多的场景,可能会出现各种不同的锁,比如递归锁、自旋锁、只读锁等等。但锁的根本目的,本质是没有区别的,都是为了资源的安全。
一般来说,锁可以应用到各种需要对资源进行保护的场景下。比如线程通信中的资源、异步处理中的资源等等。特别是在各种的io操作中,也经常会遇到使用锁的情况。
二、文件锁
在常见的io操作中,对文件的操作是一种非常普遍的行为。不管是在大数据的读写还是在文件的传输下载中,都经常会使用到锁。所以说如果能够对文件进行直接的锁操作,那么对开发者来说,就简单了很多。
它就像使用raii封装了锁一样,让文件和锁结合在一起,安全而又高效。
那么在c/c++的开发中,有没有相关的文件锁的api呢?下面将对其展开进行分析和说明。
三、c/c++中的文件锁
c和c++,看着长得挺像,但其实内部区别很大。虽然c++兼容了c,但其实在很多的细节上还是有着显著的不同。
正如前面所讲,只要不在一个领域中,每个领域都会有自己的私货夹带,这个非常正常。
比如gnu c中扩展了c标准的不少技术,更不要提两种语言这么大的差别了。
一句话来说,c标准中支持了文件锁而c++没有显式的文件锁的支持。也就是说,在c++中想达到文件锁的目的,除了使用变通的方式外,就需要显式的使用锁机制自行进行文件操作的控制了。
1.c语言中的文件锁
c语言标准中提供了三个api来进行文件锁的处理。
void flockfile(file *filehandle); //阻塞锁定文件,确保线程访问互斥,增加锁定计数 int ftrylockfile(file *filehandle);//非阻塞锁定文件,尝试锁定,不成功不作任何操作 void funlockfile(file *filehandle);//释放文件锁定并减少计数
其实看到这几个接口,大家很容易的想到锁的应用,基本上类似。锁的本质就是资源的安全管理,flockfile接口的底层实现中,提供了一个引用计数器和一个互斥锁。
在不同的具体实现中,对锁的使用也有所不同。glibc和android bionic中基本使用的是pthread_mutex_lock,而有些库则使用了自行开发的文件锁。明白即可,不需要深究。
另外一个需要说明的是,flockfile接口的锁机制是在用户态,它是一个可重入锁,只能对进程内的线程起作用。而不要简单的把其与flock和lockf这种系统级的文件锁联系在一起。
当然,在实际的应用中,可能会有各种情况,不能简单的使用文件锁就一切应付过去。还是要针对具体的场景来使用,如果需要不断的反复的操作某个或某些文件,或者说需要动态的调整访问的效率等等,这可能都需要自行手动的进行使用各种机制,甚至是不使用这种带锁的文件操作。
2.c++语言中的文件锁
刚刚也说过,在c++中并未提供显式的类似c语言中的这种文件锁。但一些三方库或其它变通的方法可以实现类似的效果。第一种机制是使用c++17的std::filesystem来变通的实现锁。
它一般用于轮询场景下的短时间持有锁的情况。它主要利用了std::filesystem::create_directory的原子性操作。
缺点也比较明显,无法适用高频或长时间持有锁的场景。
namespace fs = std::filesystem;
const fs::path ldir = "test.lock";
// 尝试获取锁
if (fs::create_directory(ldir)) {
// 获取锁
// 具体操作
// 释放锁
fs::remove(ldir);
} else {
// 异常情况
}
第二种可以使用boost中的文件锁——boost::interprocess::file_lock。它可以跨平台,甚至可以跨进程使用。支持独占或共享锁。不好之处就在于为了一个锁引入一个库有点成本过高。如果已经使用了boost库,就不必再乎这种成本了。
第三种就是自行封装各种文件锁,如flock(linux)或lockfileex(windows)以及其它一些锁,实现文件操作的锁处理。但灵活就意味着需要自行处理一些具体的细节
很多的技术点的实现,往往从互相借鉴开始。可能会互相嫌弃,但过一段时间又发现彼此很香,可能会在后期不断的引入对方的经验,这本来就是各种语言在发展过程中的一个特点。看看后面c++标准中会不会也引入类似的功能吧。
四、例程
下面看一个例程:
#define _posix_c_source 200809l
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <system_error>
#include <thread>
#include <vector>
void writeunlocked(file* stream, const std::string& text)
{
for (unsigned char ch : text) {
if (::putc_unlocked(ch, stream) == eof) {
throw std::runtime_error("putc_unlocked failed");
}
}
}
void blockingstdiolockexample(file* stream)
{
::flockfile(stream);
try {
writeunlocked(
stream,
"call flockfile-funlockfile\n"
);
writeunlocked(
stream,
"operate the same lock\n"
);
} catch (...) {
::funlockfile(stream);
throw;
}
::funlockfile(stream);
}
void trystdiolockexample(file* stream)
{
std::atomic<bool> workerhaslock{false};
std::thread worker([&] {
::flockfile(stream);
workerhaslock.store(true, std::memory_order_release);
writeunlocked(
stream,
"thread get lock,keep 500ms\n"
);
std::this_thread::sleep_for(
std::chrono::milliseconds(500)
);
writeunlocked(
stream,
"thread release lock\n"
);
::funlockfile(stream);
});
while (!workerhaslock.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
if (::ftrylockfile(stream) == 0) {
writeunlocked(
stream,
" ftrylockfile() success\n"
);
::funlockfile(stream);
} else {
std::fprintf(
stderr,
"ftrylockfile() err:"
"other thread keep lock\n"
);
}
worker.join();
if (::ftrylockfile(stream) == 0) {
writeunlocked(
stream,
"other thead exit,ftrylockfile() ok\n"
);
::funlockfile(stream);
}
}
void stdiowriter(file* stream, int threadid)
{
for (int index = 0; index < 5; ++index) {
const std::string line =
"thread=" + std::to_string(threadid) +
", index=" + std::to_string(index) + "\n";
::flockfile(stream);
writeunlocked(stream, line);
::funlockfile(stream);
}
}
void runstdiolockdemo()
{
const char* path = "stdio_lock_output.txt";
file* stream = std::fopen(path, "w");
if (stream == nullptr) {
throw std::system_error(
errno,
std::generic_category(),
"fopen"
);
}
try {
blockingstdiolockexample(stream);
trystdiolockexample(stream);
::flockfile(stream);
writeunlocked(
stream,
"flockfile demo\n"
);
::funlockfile(stream);
std::vector<std::thread> threads;
for (int id = 0; id < 4; ++id) {
threads.emplace_back(stdiowriter, stream, id);
}
for (std::thread& thread : threads) {
thread.join();
}
if (std::fflush(stream) != 0) {
throw std::system_error(
errno,
std::generic_category(),
"fflush"
);
}
} catch (...) {
std::fclose(stream);
throw;
}
if (std::fclose(stream) != 0) {
throw std::system_error(
errno,
std::generic_category(),
"fclose"
);
}
std::printf("file* streamlock output:%s\n", path);
}
int main()
{
runstdiolockdemo();
return 0;
}
五、总结
很多开发者,特别是c++的开发者,天然的认为进入到c开发中很是平滑。这种想当然有一定的道理,但在实际的开发中,很可能会遇到各种钉子。
要想真正的实现c和c++的无缝畅通,还是需要不断的在开发中打磨技术并且持续跟进二者的技术更新脚步。求同存异,异曲同工,所谓“君子和而不同”。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论