C++多线程学习篇(2)之概念

此博文作为以前学习的总结,写作过程中参考过其他博客

1. 程序、进程、线程的区别

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

进程:进程是指程序在计算机中一次运行的活动,是系统进行资源分配和调度的基本单位(参考百科,而现在都是以线程为基本单位)。系统为特定的静态程序分配好运行时需要的各种资源,这个时候系统会连带地生成一个PCB(进程控制块,一种数据结构)用来记录程序运行时(这里的运行并不是指进程的运行态)的各种信息(如进程当前的状态等),这个时候你的程序就可以运行了,只需要等待CPU对其的调用,我们用进程来称呼其为程序的一次运行。在以前,进程是系统独立调度和分派的基本单位,后面由于多道处理的出现,产生了并发的概念,它加大了系统的容量与对硬件的利用率。

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

2. 并行、并发

并发:同一时间段内交替运行多个进程(线程)

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

3. 同步与异步、阻塞与非阻塞

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

当一个进程或者线程请求某一个资源而不得时,如I/O,便会进入阻塞状态,一直等待。scanf()便是一个很好的例子,当程序运行到scanf()时,如果输入缓存区为空,那么程序便会进入阻塞状态等待我们从键盘输入,这便是以阻塞的方式调用scanf()。

通过一定方法,我们可以将scanf()变成非阻塞的方式来执行。如给scanf()设置一个超时时间,如果时间到了还是没有输入那么便跳过scanf(),这个时候我们就称为用非阻塞的方式来调用scanf()。

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

 

相反的,异步即非阻塞,当异步调用某个函数时,函数会立刻返回,而不会阻塞在那。

怎么判断异步操作是否已经完成?通常有3种方式:

(1).状态:异步操作完成时会将某个全局变量置为特定值,可以通过轮询判断变量的值以确定是否操作完成。

(2).通知:异步操作完成会给调用者发送特定信号。

(3).回调:异步操作完成时会调用回调函数。

4. C++中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行,这样就可以用来同步子线程和主线程了。(有必要在这里专门再写一篇线程同步文章)一句话就是,join()的作用是阻塞线程知道线程运行结束。

5. 多线程实际应用举例(来源于网络资源)

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

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()函数,所以子线程终止后不能被销毁,系统资源无法得到回收。下面在这个程序的基础上结合join()与detach()函数进行改进。假设初步改成下面这种代码:

...
int main(int argc, char *argv[])
{
    ...
    string sendBuf;
    thread trdRecv(getMsg);        //创建子线程以完成getMsg
    trdRecv.join();		   /*新加,这样对吗?*/
    while(1)
    {                  
	getline(cin,sendBuf);           		
	sendMsg(sendBuf);				
    }
    ...
    return 0;
}

其实多线程问题往往可以利用异步来完成。(后面会专门写一篇关于线程异步同步文章)。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值