C++11并发编程基础(⼀):并发、并⾏与C++多线程
正⽂
C++11标准在标准库中为多线程提供了组件,这意味着使⽤C++编写与平台⽆关的多线程程序成为可能,⽽C++程序的可移植性也得到了有⼒的保证。另外,并发编程可提⾼应⽤的性能,这对对性能锱铢必较的C++程序员来说是值得关注的。
1. 何为并发
并发指的是两个或多个独⽴的活动在同⼀时段内发⽣。⽣活中并发的例⼦并不少,例如在跑步的时候你可能同时在听⾳乐;在看电脑显⽰器的同时你的⼿指在敲击键盘。这时我们称我们⼤脑并发地处理这些事件,只不过我们⼤脑的处理是有次重点的:有时候你会更关注你呼吸的频率,⽽有时候你更多地被美妙的⾳乐旋律所吸引。这时我们可以说⼤脑是⼀种并发设计的结构。这种次重点在计算机程序设计中,体现为某⼀个时刻只能处理⼀个操作。
与并发相近的另⼀个概念是并⾏。它们两者存在很⼤的差别。并⾏就是同时执⾏,计算机在同⼀时刻,在某个时间点上处理两个或以上的操作。判断⼀个程序是否并⾏执⾏,只需要看某个时刻上是否多两个或以上的⼯作单位在运⾏。⼀个程序如果是单线程的,那么它⽆法并⾏地运⾏。利⽤多线程与多进程可以使得计算机并⾏地处理程序(当然,前提是该计算机有多个处理核⼼)。
并发:同⼀时间段内可以交替处理多个操作:
图中整个安检系统是⼀个并发设计的结构。两个安检队列队⾸的⼈竞争这⼀个安检窗⼝,两个队列可能约定交替着进⾏安检,也可能是⼤家同时竞争安检窗⼝(通信)。后⼀种⽅式可能引起冲突:因为⽆法同时进⾏两个安检操作。在逻辑上看来,这个安检窗⼝是同时处理这两个队列。
并⾏:同⼀时刻内同时处理多个操作:
图中整个安检系统是⼀个并⾏的系统。在这⾥,每个队列都有⾃⼰的安检窗⼝,两个队列中间没有竞争关系,队列中的某个排队者只需等待队列前⾯的⼈安检完成,然后再轮到⾃⼰安检。在物理上,安检窗⼝同时处理这两个队列。
并发的程序设计,提供了⼀种⽅式让我们能够设计出⼀种⽅案将问题(⾮必须地)并⾏地解决。如果我们将程序的结构设计为可以并发执⾏的,那么在⽀持并⾏的机器上,我们可以将程序并⾏地执⾏。因此,并发重点指的是程序的设计结构,⽽并⾏指的是程序运⾏的状态。并发编程,是⼀种将⼀个程序分解成⼩⽚段独⽴执⾏的程序设计⽅法。
2.并发的基本⽅式途径
多线程与多进程是并发的两种途径。
想象两个场景:
场景⼀:你和⼩伙伴要开发⼀个项⽬,但⼩伙伴们放寒假都回家了,你们只能通过QQ聊天、⼿机通话、发送思维导图等⽅式来进⾏交流,总之你们⽆法很⽅便地进⾏沟通。好处是你们各⾃⼯作时可以互不打扰。
场景⼆:你和⼩伙伴放假都呆在学校实验室中开发项⽬,你们可以聚在⼀起使⽤头脑风暴,可以使⽤⽩板进⾏观点的阐述,总之你们沟通变得更⽅便有效了。有点遗憾的是你在思考时可能有⼩伙伴过来问你问题,你受到了打扰。
这两个场景描绘了并发的两种基本途径。每个⼩伙伴代表⼀个线程,⼯作地点代表⼀个处理器。场景
⼀中每个⼩伙伴是⼀个单线程的进程,他们拥有独⽴的处理器,多个进程同时执⾏;场景⼆中只有⼀个处理器,所有⼩伙伴都是属于同⼀进程的线程。
2.1 多进程并发
多个进程独⽴地运⾏,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,⽂件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免⼀个进程去修改另⼀个进程,操作系统在进程间提供了⼀定的保护措施,当然,这也使得编写安全的并发代码更容易。
运⾏多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。
2.2 多线程并发
在当个进程中运⾏多个线程也可以并发。线程就像轻量级的进程,每个线程相互独⽴运⾏,但它们共享地址空间,所有线程访问到的⼤部分数据如指针、对象引⽤或其他数据可以在线程之间进⾏传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建⽴且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是⼀致的。
3. C++中的并发与多线程
C++标准并没有提供对多进程并发的原⽣⽀持,所以C++的多进程并发要靠其他API——这需要依赖相关平台。
C++11 标准提供了⼀个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原⼦操作等各种类。标准极⼤地提⾼了程序的可移植性,以前的多线程依赖于具体的平台,⽽现在有了统⼀的接⼝进⾏实现。
C++11 新标准中引⼊了⼏个头⽂件来⽀持多线程编程:
< thread > :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在中声明.
< atomic > :包含std::atomic和std::atomic_flag类,以及⼀套C风格的原⼦类型和与C兼容的原⼦操作的函数。
< mutex > :包含了与互斥量相关的类以及其他类型和函数
< future > :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。
< condition_variable > :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。
3.1 初试多线程
我们从⼀个hello开始。在单线程时:
1 # include<iostream>
2using namespace std;
3int main()
4 {
5 cout<<"hello world"<<endl;
6 }
在这⾥,进⾏由⼀个线程组成,该线程的初始函数是main。我们启动第⼆个线程来打印hello world:
一个线程可以包含多个进程1 # include<iostream>
2 # include<thread>
3using namespace std;
4void hello()
5 {
6 cout<<"hello world"<<endl;
7 }
8int main()
9 {
10 thread t (hello);
11 t.join();
12 }
在这⾥,我们将打印hello world的语句放在函数hello中。每个线程都必须有⼀个初始函数,新线程的
执⾏开始于初始函数。对于第⼀段程序来说,它的初始函数是main,对于我们新创建的线程,可以在std::thread()对象的构造函数中指定。
在第⼆段程序⾥,程序由两个线程组成:初始线程始于main,新线程始于hello。这⾥将新线程t的初始函数指定为hello。
新线程启动之后会与初始进程⼀并运⾏,初始线程可以等待或不等待新进程的运⾏结束——如果需要等待线程,则新线程实例需要使⽤join(),否则可以使⽤detach()。如果不等待新线程,则初始线程⾃顾⾃地运⾏到main()结束。
关于< thread > 我们将在下⼀篇中进⾏详解。
由于我们的初始线程并没有做什么事情,启动新线程后,新线程将打印出hello world。
这就是我们编写出的第⼀个多线程的程序,⼀般来说并不值得为了如此简单的任务⽽使⽤多线程,尤其是在这期间初始线程并没做什么。在下⼀篇⽂章⾥,我们将继续探索< thread >头⽂件的内容,编写更复杂的并发程序。
⽂章链接:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论