置顶随笔
#
2012年5月10日
#
原文来自:http://chenlq.net/vc11-bit-sweet-condition-variable-condition_variable-header-files.html
天啊,cppblog的编辑器能不能再烂一点?
有兴趣的同学,去看原文吧,无语了 :(
条件变量,是C++11中为了简化线程之间访问某个共享资源而提出的。在这个应用场景中,这个共享资源往往表现为某种条件。例如在生产者-消费者模式中,我们往往需要判断用于存放产品的容器是在什么状态(条件)下。如果容器是空的,生产者线程可以继续,而消费者线程则需要暂停,而如果容器已经满了,生产者线程需要暂停,而消费者线程可以继续。而在这个场景中,我们就可以用条件变量来协调两个线程之间的动作。关于条件变量在生产者-消费者模式下的应用,我给出了一个例子。
条件变量在协调两个线程之间的协作的时候非常有用,我们这里再补充一个例子。假设我们需要为政府设计一个程序来管理路灯。在这里,我们用一个线程来检查是否到了观灯的时间,而另外一个线程则负责在条件满足后关闭路灯。
#include
#include // 时间工具
#include // 线程
#include // 条件变量
#include // 互斥
using namespace std;
using namespace std::chrono;
// 条件变量
condition_variable cond;
// 互斥
mutex m;
// 表示条件的共享资源
bool morning = false;
// 检查是否到了关灯的时间
void check()
{
// 记录开始时间
auto start = system_clock::now();
do
{
// 当前线程休眠1000毫秒
this_thread::sleep_for(milliseconds(1000));
cout<<"it is still night."<<endl;
} // 检查是否已经到了关灯的时刻
// 这里用seconds(4)表示路灯持续4秒
while ( system_clock::now() < start + seconds(4));
// 到达关灯时间,锁定互斥对象,
// 修改表示条件的共享数据morning
lock_guard lk(m);
cout<<"it is morning."<<endl;
morning = true;
// 用notify_one()通知另外的线程,条件已经发送变化
cond.notify_one();
}
/// 关灯线程
void turnoff()
{
// 锁定互斥对象,访问表示条件的共享资源morning
unique_lock lk(m);
// 构造一个循环,只要条件没有满足
// 就一直执行条件变量的wait()方法,让当前线程等待
while(!morning)
{
cond.wait(lk);
}
// 条件满足。执行关灯动作
cout<<"turn off the light."<<endl;
}
int main(int argc, char* argv[])
{
// 创建两个线程,分别执行检查和关灯的动作
thread c(check);
thread t(turnoff);
c.join();
t.join();
return 0;
}
从这个例子中,我们可以得到这样一些使用条件变量的要点:
条件变量总是需要与一个表示条件的共享资源以及对这个共享资源进行访问控制的互斥对象。这就是我们在程序的开始部分定义的morning,m和cond。
// 条件变量
condition_variable cond;
// 互斥
mutex m;
// 表示条件的共享资源
bool morning = false;
这三者几乎总是相伴同时出现。
在一个线程中,我们需要在条件满足的时候修改表示条件的共享资源的值,然后用条件变量的notify_one()或者notify_all()通知正在等待的线程。这就是
// 到达关灯时间,锁定互斥对象,
// 修改表示条件的共享数据morning
lock_guard lk(m);
cout<<"it is morning."<<endl;
morning = true;
// 用notify_one()通知另外的线程,条件已经发送变化
cond.notify_one();
而在另外一个线程中,我们需要构造一个以共享资源为条件的无限循环,当条件无法满足时,就用条件变量的wait()或者wait_until()等函数进行等待,直到条件得到满足,循环结束。
// 锁定互斥对象,访问表示条件的共享资源morning
unique_lock lk(m);
// 构造一个循环,只要条件没有满足
// 就一直执行条件变量的wait()方法,让当前线程等待
while(!morning)
{
cond.wait(lk);
}
总结起来,条件变量简化了对表示条件的共享资源的访问,也省去了对共享资源的频繁的锁操作,进一步提高了效率。
2012年5月1日
#
学习C++11正当时,C++11 FAQ中文版本帮你快速了解和学习C++11,从而快人一步,登上C++11这趟快速列车 >> http://chenlq.net/cpp11-faq-chs对此你有什么意见和建议呢?
C++11标准已经在2011年正式通过。而各大编译器也即将实现对C++11的完全支持。C++11可以说是C++历史上最大的一次变革,增加了大量的语法特性,标准库也得到了极大的增强。C++11 FAQ中文版是我们学习和掌握这一新标准的快捷通道。
http://chenlq.net/cpp11-faq-chs
2011年11月12日
#
源文来自:
http://imcc.blogbus.com/logs/172675220.html
在新颁布的C++新标准C++11中,最令人激动人心的,我想不是auto关键字,也不是Lambda表达式,而是其中的对并行计算的支持——新的线程库(thread)的加入。
多核心CPU的普及应用,C++的主要应用领域,服务器程序,高性能计算等等,都对并行计算提出了新的要求,而这次C++中全新添加的线程库,就是
对这一趋势的应对。现在,C++程序员可以轻松地编写多线程的程序,而无需借助系统API或者是第三方程序库的支持。线程库的加入给C++带来的变化,无
异于 194,翻身的程序员们把歌唱。
C++11中的线程库,很大程度上直接来自boost这块C++的试验田,其基本架构和组件都完全相同,如果你是一个boost线程库的使用者,那
么在C++11中,你会感觉到是回到了老家一样,到处都是熟人。而如果你是一个完全的新手,也不要紧,C++11中的线程库非常简单,任何人都可以轻松上
手,我就是这样,但是要深究,还得好好学习。
下面是一个简单的例子,用到了线程库中的线程(thread),互斥(mutex),条件变量(condition),来模拟一个演唱会的入场检票的场景,另外,为了模拟观众,用到了C++11中的新的随机数的产生,模拟一个正态分布的访客人群。不说了,还是看代码:
#include <iostream>
#include <queue>
#include <vector>
// 随机数
#include <random>
// 这里,我使用了boost实现的线程库,如果你的编译器已经支持C++11,则使用<thread>是一样的
#include <boost\thread.hpp>
#include <boost\thread\locks.hpp>
#include <boost\thread\condition.hpp>
using namespace std;
using namespace boost;
// 共享资源和互斥对象
mutex mtx;
bool finish = false; // 表示观众到来是否结束
// 观众,主要是为了表示检票过程中的检票耗费时间
class viewer
{
public:
void check()
{
// 线程等待
posix_time::milliseconds worktime(400);
this_thread::sleep(worktime);
}
void arrival(int t)
{
posix_time::seconds arrtime(t);
this_thread::sleep(arrtime);
}
};
// 检票口
// 它有一个队列,用于保存到来的观众,并且用一个线程来处理队列中的观众
class gate
{
typedef boost::mutex::scoped_lock scoped_lock;
public:
gate():count(0),no(0){};
// 启动线程
void start(int n)
{
no = n;
t = thread(&gate::check,this);
}
// 检票
void check()
{
// 无限循环,知道观众数为0且不会有新的观众到来
while(true)
{
viewer v;
{
// 锁定互斥对象,开始访问对列
scoped_lock lock(m);
if(0==vque.size()) // 如果队列为空
{
{
// 判断是否还会有新的观众到来,也即是表示到达的线程是否结束
scoped_lock finlk(mtx);
if(finish)
return; // 如果已经结束,检票也同样结束
}
// 如果观众数为0,则等待新的观众的到来
while(0 == vque.size())
{
// 这里的wait()是条件变量的关键,它会先是否lock所锁定的互斥对象m一定时间,
// 然后再次锁定,接着进行(0==vque.size())的判断。如此往复,知道size不等于0,
// 循环条件无法满足而结束循环,这里表达的条件就是,只有size!=0,也就是队列中有
// 观众才继续向下。
cond.wait(lock);
}
}
// 从对列中获得观众,对其进行检票
v = vque.front();
vque.pop();
cond.notify_one(); // 这里是通知添加观众的进程,表示队列已经有空位置了,可以添加新的观众
}
v.check();
++count;
}
}
// 将观众添加到队列
void add(viewer v)
{
// 同样运用条件变量,判断队列是否已经满了
// 只有在队列尚未满的情况下才向下继续
scoped_lock lock(m);
while(vque.size() >= 15 )
{
cond.wait(lock);
}
vque.push(v); // 将观众添加到队列
cond.notify_one(); // 通知检票进程,新的观众进入队列,这样在size=0时等待的条件可以更新
}
int getcount()
{
return count;
}
int getno()
{
return no;
}
// 等待线程执行完毕返回
void join()
{
t.join();
}
private:
thread t;
mutex m;
condition cond;
queue<viewer> vque;
int count;
int no;
};
// 一共有10个检票口
vector<gate> vgates(10);
// 用随机数模拟观众到达
void arrival()
{
default_random_engine re{}; // 产生一个均值为31的正态分布的随机数
normal_distribution<double> nd(31,8);
// 将随机数引擎和分布绑定一个函数对象
auto norm = std::bind(nd, re);
// 保存随机数的容器
vector<int> mn(64);
// 产生随机数
for(int i = 0;i<700;++i)
++mn[round(norm())];
int secs = 100;
// 产生0到9的随机数,表示观众随机地到达某一个检票口
uniform_int_distribution<int> index{0,9};
// 进入检票口队列
for(auto i:mn)
{
cout<<i<<endl;
for(auto vi = 1; vi <= i; ++vi)
{
// 将观众添加到某个gate的队列中
(vgates[index(re)]).add(viewer());
// 等待一段时间
int t = round(secs/(float)(i+1));
this_thread::sleep(
posix_time::milliseconds(t));
}
}
// 观众已经全部到达,进入队列
cout<<"finish"<<endl;
mtx.lock();
finish = true;
mtx.unlock();
//cout<<"unlock"<<endl;
}
int main()
{
int i = 1;
// 启动检票线程
for(gate& g:vgates)
{
g.start(i);
++i;
}
// 启动到达线程,看看,在C++11中新线程的创建就这么简单
thread arr = thread(arrival);
// 等待线程结束
arr.join();
int total = 0;
// 等待检票线程结束,并输出处理的人数
for(gate& g:vgates)
{
g.join();
total += g.getcount();
cout<<"gate "<<g.getno()
<<" processed "<<g.getcount()<<" viewers."<<endl;
}
cout<<"there are "<<total<<"viewers in total."<<endl;
return 0;
}
这就是一个线程库的简单应用,模拟了非常复杂的场景。
因为自己对多线程开发还不太熟悉,这个程序在某些特定条件下会产生了死锁,还有待进一步完善
2011年9月20日
#
原文来自:
http://imcc.blogbus.com/logs/162618478.html
在C++11中,我们可以使用shared_ptr管理某个对象的所有权,负责对象的析构。然而在某些情况下,我们只是希望安全的访问某个对象,而不想拥有这个对象的所有权,对这个的析构负责(有点像电视剧中的那些不负责任的男人哦,只是玩玩而已,不会负责)。在这种情况下,我们可以使用表示弱引用的weak_ptr。
weak_ptr可以由一个shared_ptr构建,表示这个weak_ptr拥有这个shared_ptr所指向的对象的访问权,注意,这里仅仅是访问权,它不会改变智能指针的引用计数,自然也就不会去析构这个对象。利用weak_ptr,我们就可以安全地访问那些不具备所有权的对象。
一个现实中的例子就是学校的传达室,传达室拥有一本学生的名单,如果某个电话来了找某个学生,传达室会根据花名册去尝试访问这个学生,如果这个学生还在学校,就直接呼叫这个学生,如果已经离开了,这给这个学生留一个消息。在这里,花名册上的学生可能还在学校(对象还存在),也可能已经离开学校(对象已经析构),我们都需要对其进行访问,而weak_ptr就是用来访问这种不确定是否存在的对象的。
2011年9月10日
#
原文来自
http://imcc.blogbus.com/
C++小品:吃火锅与shared_ptr,指针,拷贝构造函数和delete
读者Terry问到一个关于拷贝构造函数的问题,大家可以参考答Terry:拷贝构造函数,其中论述了拷贝构造函数的必要性,然而,任何事物都是具有两面性的,有时候我们需要自己定义类的拷贝构造函数来完成类的拷贝,然后,有的时候,这种以拷贝一个对象来完成另外一个对象的创建是不合理的(也就是在现实世界中,这种对象没有可复制性,例如,人民币),是应该被禁止的。我们来举一个吃火锅的例子:
// 火锅,可以从中取出其中烫的东西
class hotpot
{
public:
hotpot(string f) :
food(f)
{
}
string fetch()
{
return
food;
}
private:
string food;
};
// 吃火锅用的碗,当然是每个人专用的
class bowl
{
public:
bowl(string o) :
owner(o)
{
}
void put(string food)
{
cout<<"put
"< }
private:
string owner;
};
// 吃火锅的人
class
human
{
public:
// 名子和吃的火锅
human(string n,shared_ptr ppot) :
name(n),pot(ppot)
{
pbowl = new bowl(name);
};
//
OK了,从火锅中取出来放到自己的碗里
void fetch()
{
string food =
pot->fetch();
// 放到自己的碗里
coutput(food);
}
private:
string name;
shared_ptr pot;
bowl* pbowl;
};
int
main()
{
// 服务员端上来牛肉火锅
shared_ptr fpot(new hotpot("beaf"));
//
terry入席
human terry("terry",fpot);
//
又来了一个姓陈的,这里用的是默认的拷贝构造函数来创建terry的副本
human chen = terry;
//
terry夹了一块肉
terry.fetch();
// 陈先生也夹了一块肉
chen.fetch();
return 0;
}
到这里,似乎看起来一切OK,然而从程序输出中我们却发现了问题:
terry put beaf into terry's bowl.
terry put beaf into terry's bowl.
O my god!明明是两个人(terry和chen),但是好像却只有一个人做了两次,陈先生也把肉加到了terry的碗里。
这就是当类中有指针类型的数据成员时,使用默认的拷贝构造函数所带来的问题,导致其中的某些指针成员没有被合理地初始化,这别是当这些指针指向的是与这个对象(human)有所属关系的资源(bowl),在这种时候,我们必须自己定义类的拷贝构造函数,完成指针成员的合理初始化。在human中添加一个拷贝构造函数
human(const human& h)
{
// 两个人显然不能同名,所以只好给个无名氏了
name =
"unknown";
// 使用不同的碗
// bowl和human有所属关系,所以这里必须创建新的对象
pbowl = new
bowl(name);
// 不过可以吃同一个火锅
// pot和human并没有所属关系,所以可以共享一个对象
pot =
h.pot;
};
添加拷贝构造构造函数之后,两个人不会将东西放到同一个碗中了,自己取得东西不会放到别人的碗里:
terry put beaf into terry's bowl.
unknown put beaf into unknown's
bowl.
这样修改好多了,至少两个人不会用同一个碗了。然而,这样还是有问题,我们无法给第二个人命名,他成了无名氏了,这就是类当中的那些没有可复制性的数据成员(一个人的名字自然不可以复制给另外一个人,如果human中有个wife,那肯定要上演世界大战了),拷贝构造函数就会产生这样的问题。
实际上,对于这类不具备可复制性的对象,为了不引起混乱,其拷贝构造操作是应当被禁止的,新标准C++11就注意到了这个问题,提供了一个delete关键字来禁用某些可能存在的(即使你规定human不可复制,也无法阻止程序员在使用human时写出human
chen =
terry这样的不合理的代码)默认的(类的拷贝构造函数是默认提供的,对于那些不具备可复制性的类来说,这简直是画蛇添足,好心办了坏事情)不合理的操作,这样,我们就不能使用拷贝
构造函数了:
// 禁用human的拷贝构造函数
human(const human& h) = delete;
经过这样的定义,当我们在代码中尝试将一个对象复制给另外一个对象(会调用拷贝构造函数)时,编译器就会出错误提示,提醒程序员:hi,这样可不行,我是独一无二的,不能够被复制
human chen = terry;
编译器给这样的提示:
Noname1.cpp:41:2: error: deleted function 'human::human(const
human&)'
Noname1.cpp:59:15: error: used here
所以,总结起来,在使用拷贝构造函数时,有两个需要注意的地方:
- 如果类当中有指向具有所属关系的对象的指针时(human中的pbowl指向的是属于human的bowl对象,每个human对象应该有专属的bowl对象),我们必须自定义拷贝构造函数,为这个指针创建属于自己的专属对象。
- 如果这个类当中,有不具备可复制性的成员(例如name,rmb,wife等),为了防止对象被错误的复制(即使我们没有定义拷贝构造函数,编译器也会默认提供,真是多此一举),我们必须用delete禁用拷贝构造函数,这样才能保证对象不会被错误地复制。关于human的克隆技术,应当是被明令禁止(delete)的。
2011年7月23日
#
更多来自:http://imcc.blogbus.com
5.1.2 函数调用机制
在学习编写函数之前,我们首先要了解函数的调用机制,学会如何调用一个已经存在的函数。世界上已经有很多函数,我们可以直接调用这些函数来完成日常任务。世界上已经有很多轮子,我们没有必要再去发明更多同样的轮子,只需要用好它们就可以了。在实际的开发中,可供调用的现有函数主要有编译器提供的库函数、Windows API及第三方提供的函数库等。通过调用他人的函数,可以复用他人的开发成果,在其开发成果的基础上,实现快速开发,如图5-3所示。
有了别人提供的函数,就可以调用这些函数来完成自己的功能。两个函数之间的关系是调用与被调用的关系,我们把调用其他函数的函数称为主调函数,被其他函数调用的函数称为被调函数。一个函数是主调函数还是被调函数并不是绝对的,要根据其所处的相对位置而定:如果一个函数内部有函数,则相对其内部的函数它就是主调函数;如果它的外部有函数,则相对其外部函数它就是被调函数。
图5-3 天上掉下个函数库
2011年7月22日
#
更多来自:http://imcc.blogbus.com
5.1.1 将程序装到箱子中:函数的声明和定义
提问:把大象装到冰箱中需要几步?
回答:需要三步。第一,打开冰箱;第二,把大象放进冰箱;第三,关上冰箱。
提问:那么,把一个程序放进箱子需要几步?
回答:需要两步。第一,声明一个函数;第二,定义这个函数。
没错,把一个函数放进箱子比把大象放进冰箱还要简单。当分析一段长的程序代码时,往往会发现一些代码所实现的功能相对比较独立。我们将程序中这些相对比较独立的功能代码组织到一起,用函数对其进行封装,也就是将一个较长的程序分放到各个函数箱子中。
要装东西,先得准备好箱子。为了找到具体功能实现代码的箱子,需要给箱子贴上标签,这个标签就是函数的声明,如图5-2所示。
图5-2 声明一个函数,为箱子贴上
2011年7月21日
#
更多来自:http://imcc.blogbus.com
Ÿ 把程序装进箱子:用函数封装程序功能
在完成豪华的工资统计程序之后,我们信心倍增,开始向C++世界的更深远处探索。
现在,可以使用各种数据类型和程序流程控制结构来编写完整的程序了。但是,随着要处理的问题越来越复杂,程序的代码也越来越复杂,主函数也越来越长了。这就像我们将所有东西都堆放到一个仓库中,随着东西越来越多,仓库慢慢就被各种东西堆满了,显得杂乱无章,管理起来非常困难。面对一个杂乱无章的仓库,聪明的仓库管理员提供了一个很好的管理办法:将东西分门别类地装进箱子,然后有序地堆放各个箱子。
这个好方法也可以用到程序设计中,把程序装进箱子,让整个程序结构清晰。
5.1 函数就是一个大箱子
当要处理的问题越来越复杂,程序越来越庞大的时候,如果把这些程序代码都放到主函数中,将使得整个主函数异常臃肿,这样会给程序的维护带来麻烦。同时,要让一个主函数来完成所有的事情,几乎是一个不可能完成的任务。在这种情况下,可以根据“分而治之”的原则,按照功能的不同将大的程序进行模块划分,具有相同功能的划分到同一个模块中,然后分别处理各个模块。函数,则成为模块划分的基本单位,是对一个小型问题处理过程的一种抽象。这就像管理一个仓库,总是将同类的东西放到同一个箱子中,然后通过管理这些箱子来管理整个仓库。在具体的开发实践中,我们先将相对独立的、经常使用的功能抽象为函数,然后通过这些函数的组合来完成一个比较大的功能。举一个简单的例子:看书看得肚子饿了,我们要泡方便面吃。这其实是一个很复杂的过程,因为这一过程中我们先要洗锅,然后烧水,水烧开后再泡面,吃完面后还要洗碗。如果把整个过程描述在主函数中,那么主函数会非常复杂,结构混乱。这时就可以使用函数来封装整个过程中的一些小步骤,让整个主函数简化为对这些函数的调用,如图5-1所示。
图5-1 将程序封装到箱子,分而治之
2011年7月20日
#
更多来自:
http://imcc.blogbus.com4.3.4 对循环进行控制:break与continue
// 大款的收支统计程序
int nTotal = 0;
int nInput = 0;
do
{
cout<< "请输入你的收入或支出:";
cin>>nInput;
if( 1000< nInput ) // 毛毛雨啊,就不用统计了
continue;
nTotal += nInput;
}while( 0 != nInput );
在这个大款的收支统计程序中,nInput接收用户输入后判断其值是否小于1 000,如果小于1 000,则执行continue关键字,跳过后面的加和语句“nTotal += nInput;”,而直接跳转到对条件表达式“0 != nInput”的计算,判断是否可以开始下一次循环。值得注意的是,在for循环中,执行continue后,控制条件变化的更改语句并没有被跳过,仍然将被执行,然后再计算条件表达式,尝试下一次循环。
虽然break和continue都是在某种条件下跳出循环,但是两者有本质的差别:break是跳出整个循环,立刻结束循环语句的执行;而continue只跳出本次循环,继续执行下一次循环。图4-6展示了break和continue之间的区别。
图4-6 break和continue之间的区别
2011年7月19日
#
The world is built on C++.
——
Herb Sutter
看得有趣、学得轻松
看图也能学C++?!
没错,看图也能学C++!
这本迷你书是《我的第一本C++书》的迷你版,它抽取了《我的第一本C++书》中的全部的精美插图,并配上相应的解释说明。它以图文并茂的生动形式,向你讲解那些所谓的高深的C++知识,让你对那些抽象的C++知识有一个更加形象的理解,向你展示这个美丽而神秘的C++世界,让你在有趣的看图过程中,轻松地学到了C++知识。
看得有趣、学得轻松
免费下载