C++多线程入门

一.概念


在讲解多线程之前,有些概念还是有必要了解清楚的。

①程序,进程,线程的区别


程序:程序是由一系列的指令和逻辑组成的一个静态文件(如cpp文件),无论能不能运行,它都客观的存在于储存器中。


进程:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位--来源于百度百科。如果你是初学者,可能你并不能真正地理解这句话。通俗地说,系统为特定的静态程序分配好运行时需要的各种资源,这个时候系统会连带地生成一个PCB(进程控制块,一种数据结构)用来记录程序运行时(这里的运行并不是指进程的运行态)的各种信息(如进程当前的状态等),这个时候你的程序就可以运行了,只需要等待CPU对其的调用,我们用进程来称呼其为程序的一次运行。


线程:在以前,进程是系统独立调度和分派的基本单位,后面由于多道处理的出现,产生了并发的概念(不明白并发的话可以先看第②点),它加大了系统的容量与对硬件的利用率。我们知道,对于单处理器的机器来说,实现并发典型的方法便是使用分时,即CPU将时间片按特定算法分发给各个进程,虽然总的计算次数可能并没有发生什么变化,但是由于CPU的计算速度越来越快,从宏观上来看,几个进程就像是在同一时间段内运行的。于是,当进程A的时间用完了之后就要切换到另一个进程B,此时计算机需要为进程A保存下结束时的状态以便下一次从上一次结束处继续执行,还需要为进程B的运行做各种准备工作,由于进程相对而言比较大,反复切换会浪费很多的资源,所以人们想能不能将系统独立调度和分派的基本单位做得更小,以减少进程切换所浪费的资源--于是线程出现了。现在,大部分的OS都是以线程为系统独立调度和分派的基本单位。另外,进程是由一个或者多个线程组成,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。


给大家举一个例子:

假如你是一个很牛逼的科学家,至少是爱因斯坦那种,呵呵,某一天你突然产生了一个念头--你要做一个机器人女朋友,她无所不能,大家可以尽情想象!一天之内你就将整个生产流程以及各种制造细节都记录在了纸上程序)。尽管你很牛逼,但是你却很孤傲,从不发表自己的发明,结果肯定就是你比较穷,于是你缺少各种原料来实现(资源)。上帝总是很眷顾你!于是第二天你就莫名其妙有了各种原料,开始着手于组建你的无所不能的机器人女友(进程)。当然制作的过程肯定是分得很细的,如制造头,制造手等等(线程),你每花1分钟的时间来制造头就会花1分钟的时间来制造手(并发)。最后,你的女朋友做好了,尽情地享受吧!


②并发,并行的区别


并发:同一时间段内交替运行多个进程(线程)
并行:同一时刻运行多个进程(线程)。很明显,只有多处理器才能支持。

并发就像我们的大脑思考一样,同一个时刻只能想一件事,但是在很短的一个时间段内我们可以三心二意。当然如果你长了几个脑袋,那你就可以并行思考了呵呵。


③同步与异步,阻塞与非阻塞方式


让我们再来了解一下同步异步阻塞非阻塞这个概念,加深对多线程编程的理解。

有了之前的概念,我们可以想象,当几个线程或者进程在并发执行时,如果我们不加任何干预措施,那么他们的执行顺序是由系统当时的环境来决定的,所以不同时间段不同环境下运行的顺序都会不尽相同,这便是异步(有差异的步骤)。当然,同步肯定就是通过一定的措施,使得几个线程或者进程总是按照一定顺序来执行(总是按照相同的步骤)

当一个进程或者线程请求某一个资源而不得时,如I/O,便会进入阻塞状态,一直等待。scanf()便是一个很好的例子,当程序运行到scanf()时,如果输入缓存区为空,那么程序便会进入阻塞状态等待我们从键盘输入,这便是以阻塞的方式调用scanf()。
通过一定方法,我们可以将scanf()变成非阻塞的方式来执行。如给scanf()设置一个超时时间,如果时间到了还是没有输入那么便跳过scanf(),这个时候我们就称为用非阻塞的方式来调用scanf()。

对比可以发现,同步即阻塞。想要按照某特定顺序来执行一系列过程,在上一个过程完成之前下一个过程必须等待,这就是阻塞在了这个地方。当同步运行的时候,会等待同步操作完成才会返回,否则会一直阻塞在同步操作处。

相反的,异步即非阻塞,当异步调用某个函数时,函数会立刻返回,而不会阻塞在那。
怎么判断异步操作是否已经完成?通常有3种方式:
1.状态:异步操作完成时会将某个全局变量置为特定值,可以通过轮询判断变量的值以确定是否操作完成。
2.通知:异步操作完成会给调用者发送特定信号。
3.回调:异步操作完成时会调用回调函数。

所以同步即阻塞,异步即非阻塞。

二.thread类简单剖析


①Thread类接口的定义

一个线程类无论具体执行什么任务,其基本的共性无非就是 
创建并启动线程 
停止线程 
另外还有就是能睡,能等,能分离执行(有点拗口,后面再解释)
还有其他的可以继续加…
于是我们可以把线程抽象为:
class thread
{
    public:
        Thread();
        virtual ~Thread();
        int start (void * = NULL);
        void stop();
        void sleep (int);
        void detach();
        void * join();
        bool joinable();
    protected:
	virtual void * run(void *) = 0;
    private:
	...   //这里只介绍主要的方法,对于不同的库方法会有差异,不过都是大同小异。
}; 


②方法解析

stop()sleep()这2个看名字就知道,一个停止,一个休眠等待。


start()run() 这2个是由区别的,执行 start() 会告诉系统创建一个新线程并就绪,但是并不一定马上运行,而是让系统选择一个合适的时间来调用 run() 来运行,这样就有了异步的可能。而 run() 的话就是将CPU腾出来立刻运行此线程,run() 可以用来确保线程的首次运行。


join()detach() 这2个方法就显得更加难以理解了,且听我慢慢道来。

一个线程,总是会有下面2种状态之间的一种:

joinable:可会合的

detachable:分离的(不可会合的)

一个子线程被创建之后默认为 joinable ,在子线程终止之前,我们需要调用 join() 函数来将其与父线程会合,只有这样在子线程终止之后才能被摧毁,其所占有的资源(内存,端口等)才会被释放,否则会导致内存泄露。当然我们可以用 detach() 方法来将其设置为分离的,一个线程被分离之后将不再受我们的控制,可以想象成托管给了系统,当其被终止的时候会被马上摧毁。joinable() 方法可以用来检验当前是否为 joinable。

当然 join() 还被设置用来实现一个强大的功能--同步

如果我们在主线程XX行调用了 join() ,那么在子线程终止之前,主线程会一直阻塞在XX行,这样就可以用来同步子线程和主线程了。


三.实战


呵呵,啰里啰嗦讲了这么多。
那么多线程到底有什么用呢?下面让我们进入实战环节,用实例来加深对多线程的理解。

想象一个场景,你想做一个聊天的功能,你能够不断的给其他人发送信息,也可以实时收到他们发来的信息,你可能会写出下面的代码(省略一部分):

...
int main(int argc, char *argv[])
{
    ...
    string recvBuf,sendBuf;
    while(1)
    {
	recvBuf = recvMsg();            		//由于涉及到网络编程,所以这里仅用recvMsg()来表示获取别人发来的消息,如果没有消息则阻塞,之后我会写一些网络编程的文章
	cout << "对方向你发送:" << revcBuf << endl;    //输出对方发来的信息
	getline(cin,sendBuf);           		//输入要发送的信息至sendBuf中,如果没有输入则阻塞
	sendMsg(sendBuf);				//发送自己输入的信息
    }
    ...
    return 0;
}



由于recvMsg()和getline()都是以阻塞方式运行的,所以只能收一条信息发一条消息这样轮回,而不能想什么时候发就什么时候发。
当程序运行到recvMsg()的时候,如果此时对方并没有发来消息,那么主线程就会处于阻塞状态,导致下面的sendMsg()无法运行,而此时你想要给对方发送消息该怎么办呢?
如果能够让recvMsg()和getline()并发运行就好了,这样的话你收你的信息,我发我的信息,你不能阻塞我,我也不能阻塞你。多线程能帮我们很顺利地实现这一功能,于是你将代码改成了如下所示(以C++11标准库thread为例):
#include<thread>
...
void getMsg()
{
    ...
    while(1)
    {
	recvBuf = recvMsg();
	cout << "对方向你发送:" << revcBuf << endl;
    }
    ...
    return;
}
int main(int argc, char *argv[])
{
    ...
    string sendBuf;
    thread trdRecv(getMsg);        //创建子线程以完成getMsg
    while(1)
    {                  
	getline(cin,sendBuf);           		
	sendMsg(sendBuf);				
    }
    ...
    return 0;
}










此时,你的进程由2个线程组成,一个主线程(main),一个子线程(trdRecv),主线程用来发送消息,子线程用来接收消息,2者并发执行,共用CPU,所以实现了我们想要的功能。然而,大功告成了吗?还记得上面提过的join()detach() 吗,我们并没有使用,所以结果必然是导致子线程终止之后不被回收,造成内存泄露,资源不被释放。那好,我们加上一个 join() 试试?
...
int main(int argc, char *argv[])
{
    ...
    string sendBuf;
    thread trdRecv(getMsg);        //创建子线程以完成getMsg
    trdRecv.join();		   /*新加,这样对吗?*/
    while(1)
    {                  
	getline(cin,sendBuf);           		
	sendMsg(sendBuf);				
    }
    ...
    return 0;
}


仔细想一下,这样行吗?前面已经说过了,join() 可以用来实现同步,所以在trdRecv终止之前,主进程会一直阻塞在 join() 处,就不能再发送消息了,只有等待子线程终止了才能发送消息,所以我们把 join() 放在return 0;之前,这样的话就OK了。当然我们还可以把 join() 改成 detach() ,这样的话放哪里都是可以的,代码略。

其实多线程问题往往可以利用异步来完成,这里只是给大家拓展一下:
我们只需要异步执行recvMsg和sendMsg就可以达到相同的效果了。为什么?若recvMsg是异步执行的,那么它会立马返回,不会导致sendMsg一直等待,我们只需要给recvMsg传递一个回调函数用于打印消息就好了,只要recvMsg完成就会打印收到的消息。同样的,sendMsg也不会导致recvMsg一直等待。具体操作我就不献丑了,因为我自己也还没试验过呢,只是给大家分享下我的想法。

谢谢观赏,呵呵!

  • 10
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值