《C++》基础入门_22——多线程:一对一聊天实例

文章目录

  • 一、并行和并发
    • 1.1 并行
    • 1.2 并发
    • 1.3 为啥要并发和并行
    • 1.4 并发编程的方法
      • 1.4.1 多进程并发
      • 1.4.2 多线程并发
  • 二、程序、进程、线程
    • 2.1 程序
    • 2.2 进程
    • 2.3 线程
  • 三、同步和异步
    • 3.1 同步
    • 3.2 异步
  • 四、阻塞和非阻塞
    • 4.1 阻塞
    • 4.2 非阻塞
  • 五、Winsock中实现异步的方法
  • 六、Thread类
  • 七、简单多线程实例
    • 7.1 detch()
    • 7.2 join()
    • 7.3 子线程函数带有参数的多线程
  • 八、实战篇:一对一聊天

一、并行和并发

1.1 并行

同一时间段内交替运行多个进程(线程)
在操作系统中是指,一组程序按独立异步的速度执行,不等于时间上的重叠(同一个时刻发生)。

并行也指8位数据同时通过并行线进行传送,这样数据传送速度大大提高,但并行传送的线路长度受到限制,因为长度增加,干扰就会增加,数据也就容易出错。

对于单核计算机操作系统中的并行,指的是同时存在于内存中的多道作业都处于运行状态。实际上都是宏观上并行,微观上串行,因为这些作业都是开始各自的运行,但都没运行完毕,只是交替地使用cpu。

1.2 并发

同一时刻运行多个进程(线程)。很明显,只有多处理器才能支持。
在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。

并发是两个任务可以在重叠的时间段内启动,运行和完成。并行是任务在同一时间运行,例如,在多核处理器上。
并发是独立执行过程的组合,而并行是同时执行(可能相关的)计算。
并发是一次处理很多事情,并行是同时做很多事情。
应用程序可以是并发的,但不是并行的,这意味着它可以同时处理多个任务,但是没有两个任务在同一时刻执行。
应用程序可以是并行的,但不是并发的,这意味着它同时处理多核CPU中的任务的多个子任务。
一个应用程序可以即不是并行的,也不是并发的,这意味着它一次一个地处理所有任务。
应用程序可以即是并行的也是并发的,这意味着它同时在多核CPU中同时处理多个任务。

并行才是我们通常认为的那个同时做多件事情,而并发则是在线程这个模型下产生的概念。
并发表示同时发生了多件事情,通过时间片切换,哪怕只有单一的核心,也可以实现“同时做多件事情”这个效果。
根据底层是否有多处理器,并发与并行是可以等效的,这并不是两个互斥的概念。
举个我们开发中会遇到的例子,我们说资源请求并发数达到了1万。
这里的意思是有1万个请求同时过来了。但是这里很明显不可能真正的同时去处理这1万个请求的吧!
如果这台机器的处理器有4个核心,不考虑超线程,那么我们认为同时会有4个线程在跑。
也就是说,并发访问数是1万,而底层真实的并行处理的请求数是4。
如果并发数小一些只有4的话,又或者你的机器牛逼有1万个核心,那并发在这里和并行一个效果。
也就是说,并发可以是虚拟的同时执行,也可以是真的同时执行。而并行的意思是真的同时执行。
结论是:并行是我们物理时空观下的同时执行,而并发则是操作系统用线程这个模型抽象之后站在线程的视角上看到的“同时”执行。

来源:https://www.bughui.com/2017/08/23/difference-between-concurrency-and-parallelism/

1.3 为啥要并发和并行

通过并发和并行能够使得应用程序可以充分利用多核以及GPU的计算能力,从而提高应用程序的性能,比如在以下几个方面中:

  • 使用异步I/O操作可以提高应用程序的响应性。大多数的GUI应用程序都是用单个线程来控制所有UI界面的更新。UI线程不应该被占用过长时间,不然UI界面就会失去对用户的响应。
  • 跨多线程的并行工作可以更好的利用系统的资源。具有多CPU和GPU的现代计算机,通过并行可以指数级的提高CPU计算受限的应用程序的性能。
  • 同时执行多个I/O操作(如同时从多个网站上获取信息)可以提高总体的吞吐量(throughput),等待I/O相应的操作可以用来发起新的操作,或者是处理操作返回的结果。

1.4 并发编程的方法

1.4.1 多进程并发

将一个应用程序划分为多个独立的进程,每个进程只有一个独立的线程,这些独立的进程之间可以相互通信,共同完成任务。

由于操作系统对进程提供了大量的保护机制,以避免一个进程修改另一个进程的数据,使用多进程更容易写出安全的代码。
多进程并发的两个缺点:

  1. 在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
  2. 运行多个进程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。

由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。

1.4.2 多线程并发

线程并发指的是在同一个进程中执行多个线程。线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程间的多个线程共享同一地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递

同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。

由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。

二、程序、进程、线程

2.1 程序

由一系列的指令和逻辑组成的一个静态文件,无论能不能运行,他都客观的存在于存储器中。

2.2 进程

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位

通俗地说,系统为特定的静态程序分配好运行时需要的各种资源,这个时候系统会连带地生成一个PCB(进程控制块,一种数据结构)用来记录程序运行时(这里的运行并不是指进程的运行态)的各种信息(如进程当前的状态等),这个时候你的程序就可以运行了,只需要等待CPU对其的调用,我们用进程来称呼其为程序的一次运行。

2.3 线程

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

三、同步和异步

3.1 同步

发出一个功能调用时,在没得到结果前,该调用永不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。就想起床要先刷牙、后吃饭,不能同时做。

同步:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式

按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 sendmessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的lresult值返回给调用者。

3.2 异步

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

异步:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。

当一个客户端通过调用connect函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。

这里提到执行部件和调用者通过三种途径返回结果:状态、通知、回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。

  1. 如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。
  2. 如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。
  3. 至于回调函数,其实和通知没太多区别。

四、阻塞和非阻塞

4.1 阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。

有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同 步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数 据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操 作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。

4.2 非阻塞

指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。

以例子说明理解:
  假设我是老板,你是员工,我手头上有件事,做完这件事我要回家
同步: 我吩咐你去做事情,同时我在一旁等待结果,直到你做完我才回家.
阻塞: 与此同时,在等待的这段时间内,我只能静静的等着(线程被挂起),什么事也不能做,即为阻塞
非阻塞: 相反,如果我继续做别的事,则为非阻塞
异步: 我吩咐你去做事情,我直接回家,你做完后在通知我

五、Winsock中实现异步的方法

在Winsock中实现异步的方法有很多,Winsock的IO模型有下面六种:
一:select模型
二:WSAAsyncSelect模型
三:WSAEventSelect模型
四:Overlapped I/O 事件通知模型
五:Overlapped I/O 完成例程模型
六:IOCP模型

从一到六越来越高级,越来越高效,实现越来越复杂。
曾在网上看到一些比喻用来很好的说明这些模型,在这里引用一下。


老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。

一:select模型
老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信~~~~~
在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。

二:WSAAsyncSelect模型
后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天…不是,微软~~~~~~~~

三:WSAEventSelect模型
后来,微软的信箱非常畅销,购买微软信箱的人以百万计数…以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使~~~~~~

微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以睡觉了。

四:Overlapped I/O 事件通知模型
后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!

五:Overlapped I/O 完成例程模型
老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件…为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读/回复了!老陈终于过上了小资生活!

六:IOCP模型
微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏…

微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!

原文: https://blog.csdn.net/pizi0475/article/details/6243083

其实,上面每种模型都有优点,要根据程序需求而适当选择合适的模型,前面三种模型效率已经比较高,实现起来难度也不大,很多一般的网络程序都采用前三种模型,只有对网络要求特别高的一些服务器才会考虑用后面的那些模型

MFC中的CAsyncSocket类就是用的WSAAsyncSelect模型。

六、Thread类

一个线程类无论具体执行什么任务,其基本的共性无非就是创建并启动线程、停止线程、另外还有就是能睡,能等,能分离执行(有点拗口,后面再解释)

Class Thread{	//不可拷贝
	Public:
	    Thread();
	    Explicit thread(F f);	//传递可调用对象
	    Thread(F f, A1 a1, A2 a2, ...);  	//传递可调用对象及参数 
	    Thread(thread&&) noexcept;	//转移构造函数(C++11)
	    Thread& operator = (thread&&)noexcept;	//转移赋值函数(C++11)
	    Bool joinable() const;	//是否可join
	    Void join();	//等待线程
	    Void detach();	//分离线程
	
	    Bool try_join_for(const duration& rel_time); //超时等待(非C++11)
	    Bool try_join_until(const time_point& t);	//超时等待(非C++11)
	    Void interrupt();	//中断线程(非C++11)
	    Bool interruption_requested() const;	//判断是否被中断(非C++11)
	    
		Class id;	//内部类线程ID
	    Id get_id() const;	//获得线程id对象
	    Native_handle_type native_handle();	//获得系统操作相关的handle
	    Static unsigned hardware_concurrency();	//获得可并发核心数
	    Static unsigned physical_concurrency();	//获得真实CPU核心数
};
namespace this_thread{
	Thread::id ger_id();	//获得线程ID对象
	Void yield();	//允许重复调度线程
	Void sleep_nutil(const time_point&t);	//睡眠等待
    Void sleep_for(const duration& d);	//睡眠等待
}

方法解析:

  • 成员变量:
    1. class id
    2. typedef void *native_handle_type;
  • 成员函数:
    1. swap()
      函数原型:void swap(thread& _Other) noexcept
      功能: 线程交换
    2. joinable()
      函数原型:_NODISCARD bool joinable() const noexcept
      功能:判断线程是否可以加入等待
    3. stop()
    4. sleep()
    5. start()和 run()
      start() 和run() 这2个是由区别的,执行 start() 会告诉系统创建一个新线程并就绪,但是并不一定马上运行,而是让系统选择一个合适的时间来调用 run() 来运行,这样就有了异步的可能。而
      run() 的话就是将CPU腾出来立刻运行此线程,run() 可以用来确保线程的首次运行。
    6. join()和 detach()
      1. 函数原型:void join();
        功能:加入等待

      2. 函数原型:void detach()
        功能: 分离线程

      3. 一个线程,总是会有下面2种状态之间的一种:
        joinable:可会合的
        detachable:分离的(不可会合的)

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

    7. get_id()
      函数原型_NODISCARD id get_id() const noexcept;
      功能: 获取线程id

七、简单多线程实例

7.1 detch()

#include 
#include 
using namespace std;

void TestThread1();
void TestThread2();

int main(){

    thread t1(TestThread1);
    t1.detach();
    thread t2(TestThread2);
    t2.detach();
    printf("主线程:你好帅!!!!\n");
    system("pause");

}
void TestThread1(){
    for (int i = 0; i < 10; i++){
        printf("TestThread1:%d\n", i);
        Sleep(100);
    }
}
void TestThread2(){
    for (int i = 100; i < 110; i++){
        printf("TestThread2:%d\n", i);
        Sleep(100);
    }
}

《C++》基础入门_22——多线程:一对一聊天实例_第1张图片

7.2 join()

#include 
#include 
using namespace std;
void TestThread1();
void TestThread2();

int main(){

    thread t1(TestThread1);
    t1.join();
    thread t2(TestThread2);
    t2.join();
    printf("主线程:你好帅!!!!\n");
    system("pause");

}

void TestThread1(){
    for (int i = 0; i < 10; i++){

        printf("TestThread1:%d\n", i);
        Sleep(100);
    }
}
void TestThread2(){
    for (int i = 100; i < 110; i++){

        printf("TestThread2:%d\n", i);
        Sleep(100);
    }
}

《C++》基础入门_22——多线程:一对一聊天实例_第2张图片

7.3 子线程函数带有参数的多线程

#include 
#include 

using namespace std;

void TestThread1(int count);
void TestThread2(int start ,int count);

int main(){

    thread t1(TestThread1,10);
    t1.detach();
    thread t2(TestThread2,40,50);
    t2.detach();

    printf("主线程:你好帅!!!!\n");
    system("pause");


}

void TestThread1(int count){

    for (int i = 0; i < count; i++){

        printf("TestThread1:%d\n", i);
        Sleep(100);
    }
}
void TestThread2(int start,int count){

    for (int i = start; i < count; i++){

        printf("TestThread2:%d\n", i);
        Sleep(100);
    }
}

《C++》基础入门_22——多线程:一对一聊天实例_第3张图片

八、实战篇:一对一聊天

若想实现一个一对一聊天:

...
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
···
void RecvData(){
  ···
  while(1){
     recvBuf=recvMsg();
     cout<<recvBuf<<endl;
  }
  return ;
}
int main(int argc.char *argv[]){
    ···
    string sendBuf;
    thread Recv_Thread(RecvData);
    while(1){
        getline( cin,sendBuf);
        sendMsg(sendBuf);
    }
    ···
    return 0;
}

此时,进程由2个线程组成,一个主线程(main),一个子线程(Recv_Thread),主线程用来发送消息,子线程用来接收消息,2者并发执行,共用CPU,所以实现了我们想要的功能。然而,大功告成了吗?还记得上面提过的join() 和detach() 吗,我们并没有使用,所以结果必然是导致子线程终止之后不被回收,造成内存泄露,资源不被释放。那好,我们加上一个 join() 试试?

···
int main(){
  ···
  string sendbuf;
  thread Recv_Thread(RecvData);  //创建子线程以完成getMsg
  Recv_Thread.join();
   while(1){
        getline( cin,sendBuf);
        sendMsg(sendBuf);
    }
    ···
    return 0;
}

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

线程管理:
https://www.cnblogs.com/wangguchangqing/p/6134635.html

c++11并发之std::thread:
https://blog.csdn.net/liuker888/article/details/46848905

c++11 thread类简单使用:
https://blog.csdn.net/qq_22494029/article/details/79273127

C++多线程入门:
https://blog.csdn.net/AC_hell/article/details/53613940?utm_source=blogxgwz3

C++11多线程基本使用:
https://blog.csdn.net/wrx1721267632/article/details/52197849

你可能感兴趣的:(C++,多线程)