您的位置:首页 > 运维架构 > Linux

异常安全使用场景以及设计,(基于linux c和c++设计以posix的mutex互斥锁为例)

2020-01-14 20:04 471 查看

异常安全设计艺术以及使用场景

异常是什么?什么是异常安全?关于这个我建议看effictive c++的第29个条款还有c++的RAII设计理论。
所谓的异常并不是所谓的简单的try catch,异常安全这个东西在Linux c的API设计很常见(当然严重错误肯定还是弹Segmentation fault),我们假设这样一个场景。

pthread_mutex_t mutex;
//假设在主函数初始化了。并且假设这个锁已经初始化好,并且是default初始类型
//保证这个锁本身不存在问题。
void *function(void *value)//假设这个线程会处理一个临界资源
{
pthread_mutex_lock(&mutex);
//...临界业务
pthread_mutex_unlock(&mutex);
}

这看起来很好的处理了临界资源是不是?然而如果在中间加入

pthread_mutex_lock(&mutex);
//...临界业务,类似进程共用一个套接口。
fd=open(...);
pthread_mutex_unlock(&mutex);

假设这个系统操作返回-1,怎么办?比如打开一个不存在的文件或者权限问题导致了这个意外错误。我们当然不能再继续操作了。否则这个线程太不安全了,可能有人说了直接perror然后退出线程。

pthread_mutex_lock(&mutex);
//...临界业务,类似进程共用一个套接口。
fd=open(...);
if(fd==-1)
{perror("open error\n");return;}
pthread_mutex_unlock(&mutex);

那么现在问题来了,另外一个线程也在操作,获取到的这个锁会发生什么?死锁!因为这边的锁资源没释放。当然细心的人可能就会发现,我在检测到业务会出错的时候直接解锁啊!所以就变成这样了。

pthread_mutex_lock(&mutex);
//...临界业务,类似进程共用一个套接口。
fd=open(...);
if(fd==-1)
{perror("open error\n");pthread_mutex_unlock(&mutex);return;}
pthread_mutex_unlock(&mutex);

这样是对了,但是考虑你如果操作多个系统api呢?比如我申请内存失败了(平时不会出现,但是服务器可能会出现这个情况)socket连接异常断开了,设备被断开了。天知道呢,你要给每个函数都进行unlock吗?当然有人可能会使用goto语句直接跑到最后(可以百度下为什么很多语言不要goto语句)当然再聪明点的人会建立一个日志,然后记录原因,然后结束线程。这比吞掉错误好。
让我们再进一步,一个好的程序不仅能处理错误,还能继续运行(这里不是在忽视错误的前提,而是进行尝试修复的前提,假设你的文档正在使用,你突然重命名,你的windows会崩掉吗?显然不会。)在不遇到无法尝试的错误(比如U盘被拔了或者东西被删了,这个时候你就不要试了可以退出了。)我们应该继续给用户服务,而不是草率结束。那也就是说我们需要一直关注锁的问题,其实可以用别的方法解决。(本文最后会给出c++的例子)
我们定义一个资源管理的function跟struct

struct{
int lockstatus;
pthread_mutex_t *mutex;
}MutexManager;
void lock(struct  MutexManager* pmm)
{
pmm->lockstatus=1;
pthread_mutex_lock(pmm->mutex);
}
void unlock(struct  MutexManager* pmm)
{
if(pmm->lockstatus==1)
pthread_mutex_unlock(pmm->mutex);
pmm->lockstatus=0;
}

放到我们的业务中

struct MutexManger mm;//确保这个变量只在一个线程内,因为这本身就是线程不安全的
mm.lockstatus=0;
mm.mutex=&mutex;//这里有兴趣可以写成一个初始化函数
lock(&mm);
//...临界业务,类似进程共用一个套接口。
fd=open(...);
if(fd==-1)
{perror("open error\n");unlock(&mm);return;}
unlock(&mm);

这样每次调用错误返回前调用一次unlock是不是会好多了?毕竟你可能不记得哪里不是临时资源,万一锁是处于解锁状态的,这种不明意义的调用可能会产生一些问题(比如其他线程在等待这个互斥锁资源,结果给你的错误调用导致异常获取到了),而封装好了保证会让出临界资源。当然如果你在线程里使用了系统资源要记得归还,你可以用一个结构体记录使用到的资源以及各自的描述,添加到管理器中(比如文件描述符,内存指针指向),特别是非线程空间的内容,这部分内容是不会归还的,像内存申请,有时候会发生大问题。所以请记得一定要封装好释放资源的方法,就像让出临界资源一样。
到这里c的异常处理应该差不多了,这里只是一个简单的互斥锁资源例子。毕竟调试的时候大家都是使用,简单明了

#define ERR_EXIT(m)\
do{\
perror(m);\
exit(1);\
}while(0);

但是组装的时候千万不能这么写,因为debug是确定正确逻辑可行,往上就是添加错误处理了,每个API都不能保证自己不出错,所以确认方法本身是正确了就要处理API的异常问题了。
接下来就是RAII的C++处理方法了,所谓RAII就是资源获取就是初始化

class Mutex//effective c++ 资源管理使用类=>资源管理类不要使用指针,因为会导致不确定什么时候释放。
{
private:
pthread_mutex_t *mutex;
/*
这里不能使用智能指针,原因是mutex大多时候是异步共享的,智能指针异步处理会比较难处理特别是取址内容
(有些内容没必要动态申请),因为智能指针的作用域范围限制导致了其更适合在同步情况下(只能指针用了RAII思维),
如果这里使用了智能指针,弹个错误给你也不稀奇(非heap的内容你想delete?),但是这里可以用引用(个人奉劝指针引用不要混用)。
*/
public:
Mutex(pthread_mutex_t *mutex);
~Mutex();
};
Mutex::Mutex(pthread_mutex_t *mutex)//RAII获取资源就是初始化=>初始化就是获取资源=>构造就是获取资源
{
this->mutex = mutex;
pthread_mutex_lock(this->mutex);//上锁
}

Mutex::~Mutex()//析构即是释放资源
{
pthread_mutex_unlock(mutex);//解锁
}

使用的话

void function(void *mutex) {
pthread_mutex_t *pm = (pthread_mutex_t *)mutex;
while (1)//测试析构结果
{
std::cout << "pthread_id:" << pthread_self() << std::endl;
{//内域,出域即释放资源。
Mutex raiimutex(pm);
}
/*谨记,不要使用指针对象+引用对象组合*/
sleep(5);
}
}

这时候可以开两个线程进行观察了,可以观察到锁资源释放是合理的,由于设置解锁后睡眠5秒,所以每5秒会有2个输出

利用构造跟解析的优势达成自管理,c++的好处很明显,也就是说你不用像c那样每次都要自己释放了。
但是c的API混编的时候请务必小心,资源的申请以及使用并不是单纯的new delete问题。特别是文件描述符,
如果你没有归还,那这个资源会被你占用。构造跟析构一定要了解每个资源的归还申请方法。这是RAII的重点。
第一次写长博客,写的烂或者有错也很正常,请大家见谅!

  • 点赞
  • 收藏
  • 分享
  • 文章举报
没有技术的菜逼 发布了4 篇原创文章 · 获赞 0 · 访问量 117 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐