摘要

本篇文章围绕以下几个问题展开:

  1. 进程和线程的区别
  2. 何为并发?C++中如何解决并发问题?C++中多线程的基本操作
  3. 同步互斥原理以及多进程和多线程中实现同步互斥的两种方法
  4. Qt中的多线程应用

c++的多线程可以充分利用计算机资源,提高代码运行效率。在这里总结了一些多线程应用过程中的基本概念和用法。   

一,进程和线程的区别

  • 进程是一个应用程序被操作系统拉起来加载到内存之后从开始执行到执行结束的这样一个过程。简单来说,进程是程序(应用程序,可执行文件)的一次执行。比如双击打开一个桌面应用软件就是开启了一个进程。
  • 线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也有说,线程是CPU可执行调度的最小单位。也就是说,进程本身并不能获取CPU时间,只有它的线程才可以。

引入线程之后,将传统进程的两个基本属性分开了,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。

我对这句话的理解是:线程参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行。而进程负责获取操作系统分配的资源,如内存。

二,何为并发?

简单来说,并发指的是两个或多个独立的活动在同一时段内发生

与并发相近的另一个概念是并行。它们两者存在很大的差别,图示如下:

  • 并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。

  • 并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。

在传统的单核CPU中,CPU通过极快的速度不停的切换不同应用程序的命令,而让我们看起来感觉计算机在同时执行很多个应用程序。比如,一边听歌,一边聊天,还能同时打游戏,我们误以为这是并发,其实只是一种伪并发的假象。

在出现多核处理器以后,使得并发真正的实现。C++中采用多线程实现并发。

三,C++11线程基本操作

一个多线程C++程序是什么样子的?它看上去和其他所有C++程序一样,通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行,所以你需要确保共享数据的并发访问是安全的。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。

C++11新标准多线程支持库

C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API(比如用CreateThread进行创建)
C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程,是<atomic>/<thread>/<mutex>/<condition_variable>/<future>

  • < thread > : 提供线程创建及管理的函数或类接口;
  • < mutex > : C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题
  • < condition_variable > : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行;
  • < future > : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常
  • < atomic >:为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。

std::thread类成员函数:

  • get_id:获取线程ID,返回一个类型为std::thread::id的对象。
  • joinable:检查线程是否可被join。检查thread对象是否标识一个活动(active)的可行性线程。缺省构造的thread对象、已经完成join的thread对象、已经detach的thread对象都不是joinable。
  • join:通过join()函数关联并阻塞线程,等待该线程执行完毕后继续;
  • detach:通过detach()函数解除关联使线程可以与主线程并发执行,但若主线程执行完毕退出后,detach()接触关联的线程即便没有执行完毕,也将自动退出。
  • native_handle:该函数返回与std::thread具体实现相关的线程句柄。
  • swap:交换两个线程对象所代表的底层句柄。
  • operator:移动线程对象

有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11里面专门定义了一个命名空间this_thread,此命名空间也声明在<thread>头文件中,其中包括:

  • get_id()函数用来获取当前调用者线程的ID;
  • yield()函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,即当前线程放弃执行,操作系统调度另一线程继续执行;
  • sleep_until()函数是将线程休眠至某个指定的时刻(time point),该线程才被重新唤醒;
  • sleep_for()函数是将线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠实际可能比sleep_duration所表示的时间片更长。

std::thread的关键总结:

  1. C++ 11中创建线程非常简单,使用std::thread类就可以,thread类定义于thread头文件,构造thread对象时传入一个可调用对象作为参数(如果可调用对象有参数,把参数同时传入),这样构造完成后,新的线程马上被创建,同时执行该可调用对象
  2. 用std::thread默认的构造函数构造的对象不关联任何线程;判断一个thread对象是否关联某个线程,使用joinable()接口,如果返回true,表明该对象关联着某个线程(即使该线程已经执行结束);
  3. "joinable"的对象析构前,必须调用join()接口等待线程结束,或者调用detach()接口解除与线程的关联,否则会抛异常;
  4. 正在执行的线程从关联的对象detach后会自主执行直至结束,对应的对象变成不关联任何线程的对象,joinable()将返回false;
  5. std::thread没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但是可以move),也就是说,没有两个 std::thread对象会表示同一执行线程;
  6. 容易知道,如下几种情况下,std::thread对象是不关联任何线程的(对这种对象调用join或detach接口会抛异常):

    默认构造的thread对象;

    被移动后的thread对象;

    detach 或 join 后的thread对象;

C++中多线程创建:

1️⃣简单使用

#include <iostream>
#include <thread>
using namespace std; void f()
{
cout<<"thread 1 is running"<<endl;
this_thread::sleep_for(chrono::seconds(1));
}
int main()
{
thread t1(f); //创建线程,一旦创建完毕,马上开始运行。
t1.join();
}

这里创建传入的函数f,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:

  • lambda表达式
for (int i = 0; i < 4; i++)
{
thread t([i]{
cout << i << endl;
});
t.detach();
}
  • 重载了()运算符的类的实例
#include <iostream>
#include <thread>
using namespace std;
class Task
{
public :
void operator()(int i) //()重载
{
cout << i << endl;
}
}; int main()
{
for (int i = 0; i < 4; i++)
{
Task task;
thread t(task, i);
t.detach();
}
}

当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。因此执行的结果就多种多样了。

这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程是没有问题的。但是控制台(资源)却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。

  • join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。

2️⃣带函数参数的线程

当需要向线程函数传递参数时,直接在创建线程时,同时也把参数作为入参传递给线程函数。

注意当调用函数的参数为引用参数时,线程调用需要加上ref关键字表示引用。并且线程函数会改变引用的变量值。

void f1(int n)
{
n++;
cout<<"n = "<< n <<endl;
} void f2(int &n)//引用参数
{
n++;
cout<<"n = "<<n<<endl;
} int main()
{
int n = 0; thread t1(f1, n);
t1.join();
cout<<"n = "<<n<<endl; thread t2(f2, ref(n));
t2.join();
cout<<"n = "<<n<<endl; }

运行结果为:

3️⃣转移线程的所有权

thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。

void f2(int &n)
{
n++;
cout<<"n = "<<n<<endl;
} int main()
{
int n = 0; thread t3(f2, ref(n));
thread t4(move(t3)); //此时t4正在运行f2(),t3不再是一个线程了。
t4.join(); }

运行结果:

将线程从t3转移给t4,这时候t3就不再拥有线程的所有权,调用t3.joint3.detach会出现异常,要使用t4来管理线程。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。

4️⃣线程暂停

如果让线程从外部暂停会引发很多并发问题,这也是为什么std::thread没有直接提供pause函数的原因。

如果线程在运行过程中,确实需要停顿,就可以用this_thread::sleep_for。

 this_thread::sleep_for(chrono::seconds(3)); //此处线程停顿3秒。

5️⃣获取当前线程号

线程的标识类型为std::thread::id,有两种方式获得到线程的id。

  • 通过thread的实例调用get_id()直接获取
  • 在当前线程上调用this_thread::get_id()获取
thread::id main_threadId =  this_thread::get_id();

小结:

本结主要介绍了C++11引入的标准多线程库的一些基本操作。有以下内容:

  • 线程的创建
  • 线程的执行方式,join或者detach
  • 向线程函数传递参数,需要注意的是线程默认是以拷贝的方式传递参数的,当期望传入一个引用时,要使用std::ref进行转换
  • 线程是movable的,可以在函数内部或者外部进行传递
  • 线程在运行过程中,如果需要停顿,可以用this_thread::sleep_for实现。
  • 每个线程都一个标识,可以调用get_id获取。

线程管理的示例代码

创建线程,并观察线程的并发执行与阻塞等待

#include <iostream>
#include <thread>
#include <chrono> using namespace std; void thread_function(int n)
{
std::thread::id this_id = std::this_thread::get_id(); //获取线程ID for (int i = 0; i < 5; i++) {
cout << "子线程 " << this_id << " 运行 : " << i + 1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
} class Thread_functor
{
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n)
{
std::thread::id this_id = std::this_thread::get_id(); for (int i = 0; i < 5; i++) {
cout << "子仿函数线程" << this_id << " 运行: " << i + 1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}
}; int main()
{
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if (mythread1.joinable()) //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕 Thread_functor thread_functor; //函数对象实例化一个对象
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if (mythread2.joinable())
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程 auto thread_lambda = [](int n) { //lambda表达式格式:[捕获列表](参数列表)可变异常->返回类型{函数体}
std::thread::id this_id = std::this_thread::get_id();
for (int i = 0; i < 5; i++)
{
cout << "子lambda线程" << this_id << " 运行: " << i + 1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(n)); //进程睡眠n秒
}
}; thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if (mythread3.joinable())
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕 std::thread::id this_id = std::this_thread::get_id();
for (int i = 0; i < 5; i++) {
cout << "主线程" << this_id << " 运行: " << i + 1 << endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
} getchar();
return 0;
}

运行结果:

上面的代码分别用三种函数对象创建了三个线程,其中第一个线程mythread1阻塞等待其执行完后继续往下执行,第二个线程mythread2不阻塞等待在后台与后面的第三个线程mythread3并发执行,第三个线程继续阻塞等待其完成后再继续往下执行主线程任务。为了便于观察并发过程,对三个线程均用了睡眠延时this_thread::sleep_for(duration)函数,且延时时间作为参数传递给该函数。

上面的示例假如多重复运行几次,有很大可能会出现某行与其他行交叠错乱的情况(如下图所示),为何会出现这种情况呢?

这就涉及到多线程资源竞争的问题了,即一个线程对某一资源(这里指显示终端)的访问还未完成,另一线程抢夺并访问了该资源,导致该资源数据混乱情况的出现。

解决方案详见下一篇文章:

相关博文:(2条消息) C++多线程并发(一)--- 线程创建与管理_流云-CSDN博客_c++多线程并发

  

浅谈C++11中的多线程(一)的更多相关文章

  1. 浅谈C++11中的多线程(三)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.c ...

  2. 浅谈C++11中的多线程(二)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.c ...

  3. c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程

    c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...

  4. 转: 浅谈C/C++中的指针和数组(二)

    转自:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242419.html 浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组 ...

  5. 转:浅谈C/C++中的指针和数组(一)

    再次读的时候实践了一下代码,结果和原文不一致 error C2372: 'p' : redefinition; different types of indirection 不同类型的间接寻址 /// ...

  6. 浅谈 Swift 2 中的 Objective-C 指针

    浅谈 Swift 2 中的 Objective-C 指针 2015-09-07  499 文章目录 1. 在 Swift 中读 C 指针 2. 在 Swift 中创建 C 指针 3. 总结 作者:Ja ...

  7. 转载 浅谈C/C++中的static和extern关键字

    浅谈C/C++中的static和extern关键字 2011-04-21 16:57 海子 博客园 字号:T | T   static是C++中常用的修饰符,它被用来控制变量的存贮方式和可见性.ext ...

  8. 浅谈C语言中的强符号、弱符号、强引用和弱引用

    摘自http://www.jb51.net/article/56924.htm 浅谈C语言中的强符号.弱符号.强引用和弱引用 投稿:hebedich 字体:[增加 减小] 类型:转载 时间:2014- ...

  9. 【sql注入】浅谈sql注入中的Post注入

    [sql注入]浅谈sql注入中的Post注入 本文来源:i春秋学院 00x01在许多交流群中,我看见很多朋友对于post注入很是迷茫,曾几何,我也是这样,因为我们都被复杂化了,想的太辅助了所以导致现在 ...

随机推荐

  1. unity texture 占用内存大小对比

    打包多种类型的项目,空项目和10张放在Resources文件夹中的图为比较案例.以下是比较数据. IPHONE: 1.空项目----空间占用量42.3MB----IPA大小10MB 2.10张1200 ...

  2. 用virtualenv建立Python独立开发环境

    1.用pip安装virtualenv sudo apt-get install python-virtualenv 2.1 创建python2的虚拟环境,进入要创建虚拟环境的目录下,我是放在/home ...

  3. springboot对mybatis的整合

    1. 导入依赖 首先新建一个springboot项目,勾选组件时勾选Spring Web.JDBC API.MySQL Driver pom.xml配置文件导入依赖 <!--mybatis-sp ...

  4. Linux将一个文件夹或文件夹下的所有内容复制到另一个文件夹

    Linux将一个文件夹或文件夹下的所有内容复制到另一个文件夹     1.将一个文件夹下的所有内容复制到另一个文件夹下 cp -r /home/packageA/* /home/cp/packageB ...

  5. openresty 学习笔记番外篇:python访问RabbitMQ消息队列

    openresty 学习笔记番外篇:python访问RabbitMQ消息队列 python使用pika扩展库操作RabbitMQ的流程梳理. 客户端连接到消息队列服务器,打开一个channel. 客户 ...

  6. redis主从架构宕机问题手动解决

    1    主机宕机 1.  设置端口6379是主机,端口6380是从机,全部都正常启动 2.  验证在6379写入数据,在6380也能得到数据 3.  现在将6379主机停掉,模拟主机宕机 4.  由 ...

  7. Go语言网络通信---一个简单的UDP编程

    Server端: package main import ( "fmt" "net" ) func main() { //创建udp地址 udpAddr, _ ...

  8. 从性能角度帮你理解HTTP协议

    因为做性能测试分析的人来说,HTTP 协议可能是绕不过去的一个槛.在讲 HTTP 之前,我们得先知道一些基本的信息. HTTP(HyperText Transfer Protocol,超文本传输协议) ...

  9. Python分析离散心率信号(上)

    Python分析离散心率信号(上) 一些理论和背景 心率包含许多有关信息.如果拥有心率传感器和一些数据,那么当然可以购买分析包或尝试一些可用的开源产品,但是并非所有产品都可以满足需求.也是这种情况.那 ...

  10. QByteArray使用方法大全

    QByteArray 在Qt中QByteArray可以看做是c语言中 char*的升级版本.我们在使用这种类型的时候可通过这个类的构造函数申请一块动态内存,用于存储我们需要处理的字符串数据. 下面给大 ...