奇技淫巧C++之方法代理
自我感觉很有趣的技术,共享^_^
http://blog.csdn.net/wingfiring/archive/2007/03/28/1543811.aspx
如果你有编写多线程程序的经历,遇到过需要共享对象的情况吗?比如,你想在两个线程中操作同一个容器,你会怎么做呢?是在每个地方都小心翼翼地加锁,还是封装一个来用?两种方法我都用过,但我比较青睐封装一个的办法,例如,封装一个带锁的队列,用于线程间的通信.
让我们先看看直接操作锁和对象的代码:
//declare:
std::Container cont;
LockType contLock;
...
//using:
contLock.lock();
cont.XXX();
contLock.unlock();
嗯,using下面的加锁和解锁的方法太老土,而且最关键的,还不安全,让我们稍加改善:
ScopedLock guard(contLock); //在ScopedLock的构造函数中lock,在析构函数中unlock
cont.XXX();
看上去稍好了一点,然而有两个小的缺点。前面提到共享对象,第一个缺点就是我们需要共享两个对象,容器和锁,这使得管理和传递共享对象都变得麻烦起来。另一个缺点是,加锁的动作需要小心谨慎,千万别忘了。可惜,即使忘了,我们也不会从编译器这里得到任何帮助。这两个不便,都会促使我们考虑是不是把对象和锁封装起来更好?很多时候,我们确实是这么做的,看一个deque的例子(省略std).
template <typename T, typename Alloc= allocator <T> >
class shared_deque{
dequeT, Alloc> m_cont;
LockType m_lock;
...
void push_back(const_reference val){
ScopedLock guard(m_lock);
m_cont.push_back(val);
}
....
};
呼,终于好了,我们现在有了一个好用的shared_queue了。只是,类似那个push_back的东西,重复了几十遍,很无聊的,还有必要对 list也来一遍吗?算了吧!万幸,没有用basic_string,那家伙可有100多个成员函数。有没有办法简化一下工作呢?重复的东西总是应该交给计算机来做是不是?还好,C++正好能帮助我们实现这一目标,这个手法在MCD中被寥寥数语带过,就是那神奇的operator-> ().
让我们回顾一下operator-> 的用法:用于对象指针,提取对象成员,函数或对象.许多smart pointer地实现都重载了这个运算符,从而可以模拟指针的语法.一般的重载形式是这样的:
cv1 T* operator-> () cv2
这里返回的是T*类型,如果返回的不是指针类型呢?C++标准对此有特别规定,会继续调用返回值的operator-> ()方法,直到最终解析出一个指针类型.假定有下面的operator-> 展开过程:
object a--> b--> T pointer
(请注意一下a,b,T对象的生命期,确定我们是安全地在使用这些对象.)a对象的operator-> 返回临时对象b,b对象的operator -> 返回最终类型T*.那么,b对象必须在其T* operator-> ()调用之前创建好,而在T::XXX()方法调用之后被销毁,因为b是临时对象.嗯,好了,有点方向了:在b的构造函数中加锁,在析构中解锁,就可以在调用T::XXX()方法时自动实现加锁可解锁了.
拓展一下思维,我们必须局限于锁和容器吗?不必.这个手法的本质效果是什么?就是在调用一个方法之前,插入一些操作,调用之后再插入一些操作(稍显遗憾的是,我们无法知道被调用的到底是什么方法).但是,这也够我们做许多事情了.实现如下:
#include
template
<
typename Pointer,
typename Monitor,
typename ScopedType = typename Monitor::scope_type
>
class call_proxy
{
public:
typedef call_proxy self_type;
typedef Pointer pointer_type;
typedef Monitor monitor_type;
typedef ScopedType scoped_type;
typedef typename boost::call_traits <pointer_type> ::param_type param_type;
typedef monitor_type& monitor_reference;
private:
struct proxy{
proxy(self_type* host) : m_host(host), m_s(host-> m_monitor){};
pointer_type operator-> (){
return m_host-> m_obj;
}
private:
self_type* m_host;
scoped_type m_s;
proxy(const proxy&);
proxy& operator=(const proxy&)
};
friend struct proxy;
public:
call_proxy(param_type p, monitor_reference m) : m_obj(p), m_monitor(m){ assert(p);}
proxy operator-> () {
return this;
}
private:
pointer_type m_obj;
boost::reference_wrapper <monitor_type> m_monitor;
};
为了可以和smart_pointer合作,call_proxy需要的第一个模版参数是被代理对象的指针类型,而不是自己产生指针类型,这就允许是一个 smart_pointer的类型.monitor类型本质上需要开始和结束两个方法,把它封装进ScopedType,依靠ScopedType的构造和析构来完成.这样做的目的是避免对限制monitor的方法名称,可以看作是traits手法的简化版本.你可以自定义合适的ScopedType类型.嵌套类proxy的构造函数的隐式转换是必要的,它可以消除额外的copy ctor的需求.
上述的实现代码已经没什么神奇之处可言了.使用方法如下:
typedef vector <int> MyVector;
MyVector cont;
LockType lock;
typedef call_proxy <MyVector*, LockType, LockType::scoped_lock> proxy_type;
proxy_type cont_proxy(&cont, lock);
至此,cont_proxy可以作为一个封装好的对象使用了.嗯,当然,cont_proxy的生命周期应该比cont来得短,这个问题就让程序员去保证吧.
剩下的问题:
call_proxy还有一些问题需要解决.我们在调用某些成员方法的时候,未必都要加锁.如果对象和锁是分离的,那么自然很容易处理.如果是手工封装,虽然工程浩大,但是也可以在适当的地方加以特别处理.特别的,对于分离的对象和锁,我们还可以使用大粒度的锁定过程,从而改善某些性能.而这里的 call_proxy则没有这种灵活性,当然手工封装的类也无此灵活性.然而,我们还是可以改善call_proxy,从而在一定程度上获得这种灵活性的好处,为call_proxy增加两个友元方法:
friend pointer_type getImp(const self_type& cp){ return cp.m_obj;}
friend monitor_reference getMonitor(const self_type& cp){ return cp.m_monitor;}
当我们需要大粒度的机制时可以这样:
{
proxy_type::scoped_type guard(getMonitor(cont_proxy));
getImp(cont_proxy)-> XXX1();
getImp(cont_proxy)-> XXX2();
...
}
对于新手,可能会奇怪getImp的用途,或者忘记调用,这不够优雅.但是这样的解决方案已经比较简单了,它简化了大部分的情况,而且留了一条优化的后路.
[解决办法]
呵呵,SF,学习下...
[解决办法]
lz,太有才了
[解决办法]
怎么让信 誉 值上升啊?LZ 105> 100
[解决办法]
偶来顶一顶。
[解决办法]
好久沒來頂了 聽説改版了才來看看
[解决办法]
mark
[解决办法]
其实就是获取对象时加锁嘛,还不如在需要的地方做个方法,明了得多
[解决办法]
学习
jf。
lz是好人。
[解决办法]
好人。接分。
[解决办法]
楼主辛苦了,顶!& 学习!
[解决办法]
看这个名字就不错,哈,mark之
[解决办法]
学习下...
[解决办法]
楼主的技巧是不错,不过,楼主是否觉得,不用线程会更好呢?
如果不使用线程,代码立即清爽无比,由加锁造成的效率损失也不复存在了。
[解决办法]
收藏了
[解决办法]
嗯,看明白了,呵呵~代码量上去了,似乎一点麻烦的
[解决办法]
看看~
[解决办法]
接分~
[解决办法]
oyd(cpp<JavaIsNotPlatform_Independent>)(MVP) ( ) 信誉:41 Blog 2007-03-30 13:57:44 得分: 0
楼主的技巧是不错,不过,楼主是否觉得,不用线程会更好呢?
如果不使用线程,代码立即清爽无比,由加锁造成的效率损失也不复存在了。
------------------------------------------------------
寒,你干脆提议windows取消对线程的支持吧,恩,让unix也取消。什么问题都解决了。
[解决办法]
不是很理解
[解决办法]
顶
[解决办法]
mark
[解决办法]
学习````
[解决办法]
mark
study
[解决办法]
Mark
[解决办法]
如果不使用线程,代码立即清爽无比,由加锁造成的效率损失也不复存在了。
------------------------------------------------------
寒,你干脆提议windows取消对线程的支持吧,恩,让unix也取消。什么问题都解决了。
====
确切的说,不用共享对象,通信用 Message Passing 方式,会简化很多。难度在于为保证效率,需要仔细的设计。
[解决办法]
m
[解决办法]
MARK
[解决办法]
先
mark一下
有空在看
[解决办法]
学习下...
[解决办法]
mark mark...谢谢lz
[解决办法]
跟在你后面顶,嘿嘿。
[解决办法]
用一变能宏会更好吧。
比如现在有个函数 fun1
再做如下#define
#define afun1(...) { \
//前面的处理
fun1(__VA_ARGS__);//执行fun1
//后面的处理
}
我觉得这样好。
[解决办法]
楼主的解决方法,无论怎么说,都与优雅两个字相差太远了。
可以说,它浪费的程序员的时间比它节约的多。
这一组模板真的不如楼上那组宏好用。
to Cybergate()
windows肯定是不能取消线程的,因为它的进程开销太大。
至于unix下面,我正儿八经的写了这么多并行处理的程序,还没看出来哪种情况是一定要用线程,或者说用线程的解决方案会更好,unix下原本就没有线程。
[解决办法]
取消线程,是个好办法~
[解决办法]
上述的实现代码已经没什么神奇之处可言了.使用方法如下:
typedef vector <int> MyVector;
MyVector cont;
LockType lock;
typedef call_proxy <MyVector*, LockType, LockType::scoped_lock> proxy_type;
proxy_type cont_proxy(&cont, lock);
========================
楼主贴一下 LockType 的代码好吗? 我想不明白LockType::scoped_lock 在构造的时候是怎么加锁的?
[解决办法]
谢谢楼主
[解决办法]
回楼主关于多线程的问题:
多线程的好处确实是实实在在的,就在前两天,我们这一个程序员好兴致勃勃地要用多线程来改善性能,这对他来说是最最方便,门槛最低的改造了。不过在随后的几天受挫和煎熬中,他还是坚决的抛开多线程,改用异步非阻塞的方式来做了。
要说多核,早在多核概念炒作之前,unix程序员一直默默地在为数个乃至数十个CPU的服务器编写并发处理程序。
为什么许多人会喜欢多线程呢?因为他或者没有别的选择(例如windows下),或者没有足够的抽象能力来进行别的选择(例如搞不懂异步是怎么回事)
[解决办法]
回:关于宏和楼主的实现谁好的问题
其实对于什么样的代码是真正简洁的,这一点要基于共同知识来判定。
例如早在STL在中国流行之前的时候,许多人即使对typedef vector <int> ::iterator ITER;这样的代码也会觉得十分费解,现在当STL成为一种通用的手法时,当时的问题也就不复存在了。
为什么许多情况下我都推荐用简明的宏来处理呢?
是因为在简单的情况下,简单的宏是人人都能方便的读懂的,复杂的宏当然不利于维护,可是事情到了需要复杂的宏时,更多的要考虑一下程序结构是不是有什么不对,而不是考虑如何用更精巧的方式来做一种貌似简洁的东西出来。
我想起曾经看过一篇关于C++中实现一个更 "好 "的min/max版本的文章,想必楼主也看过吧?
现在看来,最好的版本就是#define MAX(x,y) (x) > (y) ? (x) : (y)
而不是那些用模板、函数对象等精妙手法堆砌出来的东西。
[解决办法]
to wingfiring
关于宏
就不再讨论了,我曾经和B.S讨论过宏,他也是坚决反对用宏的。不过事实胜于雄辩,无数的代码都已表明,宏还是有其一席之地的。
关于多线程
多线程其实质不在CreateThread/beginthread,CWinThread或者其他写法。而在于多个线程共享同一全局内存,竞争和临界区的使用,极大地增加了全局复杂度。
多线程由于太容易知道彼此内部状态,而没有自动进行封装,这使它成为Bug的滋生地。而产生了些Bug,并没有为它带来性能上的显著提高,因为它虽然没有了进程上下文切换的开销,但是锁定和解锁的成本显得太高了。
线程的使用如果遵循线程本地存储,那么它更像是共享内存的使用,而众所周知的各种进程间通讯的方式,共享内存是最不得已的选择。
当然,如果你仅仅局限在windows上,线程有时候可能是不得已的选择,因为windows的进程成本太大了。不过,对于网络应用程序来说,异步IO也是一个比线程优的选择。
当然,如果既局限在windows上,又是做客户端软件,那么上面这些当我没说。毕竟少数的线程不会带来效率的明显降低,并且线程少的话,肯定比异步IO的程序要好写一些
[解决办法]
to oyd
你说的部分对
但是要想并发,除了多线程就是多进程,(异步io不是一般情况)
多进程肯定比多线程慢
因为多线程需要同步的地方,多进程也要,而且更慢,效率更低
良好的设计和构架可以降低同步的要求,但多进程可以使用的方法,多线程也可以
你说共享内存不好,是,的确不好
但是如果 多线程这个地方要等,多进程还是要等,除了要等,而且还要marshal,demarshal
所以多进程要慢
如果我没记错的话,从操作系统的演进历史来看,多线程明显是个高级东西
[解决办法]
我们用的多线程,从其本质上来说,就是对异步IO的一个代替。
你说的并行计算,如果不考虑有IO问题的话,用串行是一样的,如果你要利用多处理的优势,为什么不用多进程呢,你单个进程的计算程序将是一个专一的,易于维护和理解的程序。
多任务交互,仿真计算,其入门级设计,还是多线程比较简单,但是一个成熟的产品是不会选用多线程的,例如starcraft游戏中,每个unit的AI,那是绝不会用多线程来做的。
看起来,我们的分歧主要在:多线程和多进程的比较。
想必,你自己也知道多线程的许多缺点(不然你就不会去寻求这么多库了),但是你认为有许多解决办法可以避开这些缺点,你在解决这些问题的过程中,多线程开发的能力提高了。
可是你不曾注意到,你离你要解决的问题却远了,你不是要来用计算机解决多线程的问题的,你是用它来解决问题的。
进程其实是操作系统的线程,在操作系统的设计中,已经把线程的种种问题和应用程序相对隔离了,应用程序可以专注其应用。从这一点来说,我认为应用程序完全没有必要引入线程。
从开发上来说,
1. 如果说为了避免IO延迟,那么最直接有效的方式是采用异步IO。
2. 如果说为了更好的利用多处理器,那么最直接有效的方式是采用多进程。
3. 如果为了处理逻辑上偷懒,或者说快速原型开发,那么也许多线程在一开始更好理解,但是随后其带来的麻烦会促使开发者转用其他方案。这一点上,我宁愿直接用多进程,因为开发逻辑会更简单,并且我不需要引入多线程库,我不需要考虑我所用的其他库是否多线程安全,我在和其他人,其他模块合作时更加如鱼得水。
[解决办法]
我也引用一篇文章
为什么线程是个坏主意
http://www.softpanorama.org/People/Ousterhout/Threads/index.shtml
[解决办法]
秃子大佬的杰作,一定要好好拜读
[解决办法]
对于类似加锁解锁这种对称问题,在Effective C++中已经有很好的解决方案了,利用C++的构造函数与析构函数,就能够保证即使中间过程存在异常,也能照常释放
class LockObject
{
public:
LockObject(name)
{
...平台特定加锁方法实现...
}
~LockObject()
{
...平台特定解锁方法实现...
}
};
其实我们能根据这种方法利用到很多地方,比如内存管理,数据库连接与断开连接,文件的打开与关闭等等
[解决办法]
多线程与异步非阻塞IO的问题
Proactor这些实现模式中难道没有利用操作系统的多线程吗?
而且在很多Unix操作系统,并没有实现真的异步非阻塞IO,其较好的解决方案是:Reactor
在多核并发开发的今天,多线程大多数情况下难免,既然使用多线程,那么共享资源的同步问题也就在所难免,LZ为这方面的处理是提供了一种方案罢了。
[解决办法]
什么Reactor模式呀,看了看,笑死人了。
不就是异步IO披了个马甲吗
[解决办法]
牛淫
[解决办法]
超级顶,好帖子。我喜欢。
[解决办法]
太牛,看不懂,接点分,装装门面
[解决办法]
收藏以后看
[解决办法]
收藏 ^_^
[解决办法]
收藏
[解决办法]
up!
[解决办法]
发点微不足道的光
[解决办法]
长了点,先顶了再说吧。
[解决办法]
顶
[解决办法]
怎么收藏
[解决办法]
长了点,先顶了再说吧。
[解决办法]
很深奥的样子,太复杂了。有必要吗?
[解决办法]
没看明白
[解决办法]
学习
jf。
lz是好人
[解决办法]
谢谢 收藏
[解决办法]
赞一下楼主,学习学习。
[解决办法]
接分
------解决方案--------------------
碰到“学术交流”帖啦,顶起,呵呵~!
[解决办法]
怎么发表提问帖呢?
[解决办法]
1 除了重载operator-> (),还可以重载operator.(),可以少打1个字.
2 争论那个好比较无聊
问题的复杂性并不在技术本身,举个简单的例子,生产者/消费者问题不会因为你用多线程或者用多进程(甚至单线程)而改变它的本质,同步总是无法避免的。不要用蹩脚的方式去解决问题就好了。
我们的目标仅仅是把事情做好 - 以较小的代价。以这样的标准来衡量,你最熟悉什么技术就用什么技术,但是,不要说别人的要不得。
3 一大堆的宏我不喜欢
你要是特别青睐一大堆的宏,劝你去写C,汇编,别用C++了,这样的C++代码别叫我去做维护 —— 我情愿重构。
[解决办法]
强贴!
[解决办法]
占了座 慢慢看
[解决办法]
关于宏:
宏的确带来不少问题,但鉴于C++的编译处理方式,宏有时仍是不可替代的方式。boost::preprocessor是boost库中很基础的一个库,很多boost其它库都用到它。很难想象,没有宏,又能有谁来替代?没有宏,boost的代码都不知道要膨胀到什么地步。
但是,还是有必要提醒:宏不是来炫耀个人的技巧的。沉醉于此,难免带来他人的痛苦。
[解决办法]
关于宏(补充):
有时使用宏也是迫不得已。C++标准本身给了compiler实现者很大的实现上的自由。然而其中一些自由对于代码移植带来极大的挑战。很难想象不用宏,如何突破这个鸿沟。
[解决办法]
Up.
[解决办法]
顶一顶
[解决办法]
收藏!
[解决办法]
学习
[解决办法]
mark!
[解决办法]
借用贵宝地~
软件技术交流
qq群:4077790
[解决办法]
up
[解决办法]
最好没有锁!
加锁加锁,就是给自己戴枷锁。
[解决办法]
这个在一些并发设计的专门书籍里面独有讲解的
很成型的一些设计模式
比如可以参考 BCB中的TCriticalSection的实现
[解决办法]
好帖,偶来插句不相干的:
C++中线程同步不受编译器控制是一个大问题,所以通用同步代码有时并不是一个好主意。
[解决办法]
学习
[解决办法]
值得学习 现在还没编程经历以后常来往指导
[解决办法]
学习。。。
mark
[解决办法]
选用什么方法,与技巧、先进性等没有什么关系,有关系的仅仅是在于要做的对象是什么,符合了这一点,进程也好,线程也罢也都无所谓了。
oyd立足于实用,wingfiring立足于思考,两者都有可取之处。真正的项目开发则在于如何糅合这两点,选择一种合适的方法,并运用好它。
[解决办法]
呵呵
[解决办法]
楼上的DoItFreely(Freely) 居然能重载operator.(),太强了,我想连BS都做不到这一点。
[解决办法]
up~~~~~~~~~好贴
[解决办法]
mark,学习。。。
[解决办法]
这么多高手,mark了
[解决办法]
顶一下