C++11 std::thread 多线程框架

       C++11标准在标准库中为多线程提供了组件,这意味着使用C++编写与平台无关的多线程程序成为可能,而C++程序的可移植性也得到了有力的保证。

并发与并行的区别:

       并发指的是两个或者多个独立的活动在同一时间段内发生, 例如在跑步的时候你可能同时在听音乐;在看电脑显示器的同时你的手指在敲击键盘。这时我们称我们大脑并发地处理这些事件,只不过我们大脑的处理是有次重点的:有时候你会更关注你呼吸的频率,而有时候你更多地被美妙的音乐旋律所吸引。这时我们可以说大脑是一种并发设计的结构。这种次重点在计算机程序设计中,体现为某一个时刻只能处理一个操作。

       与并发相近的另一个概念是并行。它们两者存在很大的差别。并行就是同时执行,计算机在同一时刻,在某个时间点上处理两个或以上的操作。判断一个程序是否并行执行,只需要看某个时刻上是否多两个或以上的工作单位在运行。一个程序如果是单线程的,那么它无法并行地运行。利用多线程与多进程可以使得计算机并行地处理程序(当然 ,前提是该计算机有多个处理核心)。
 

并发:同一时间段内可以交替处理多个操作

并行:同一时间段内同时处理多个操作

       第一张图中两个任务队列同时等待一个处理器处理,两个队列可能约定交替着进行被处理,也可能是大家同时竞争被处(通信)。后一种方式可能引起冲突:因为一个处理器无法同时进行两步操作。但在逻辑上看来,这个处理器是同时处理这两个队列。而第二张图中两个任务队列是并行处理,每个队列都有自己的独立处理器(或者核),两个队列中间没有竞争关系,队列中的某个排队者只需等待队列前面的消息处理完成,然后再轮到自己被处理。在物理上,如果是两个核的CPU宏观上看是同时处理这两个任务队列。

     并发的程序设计,提供了一种方式让我们能够设计出一种方案将问题(非必须地)并行地解决。如果我们将程序的结构设计为可以并发执行的,那么在支持并行的机器上,我们可以将程序并行地执行。因此,并发重点指的是程序的设计结构,而并行指的是程序运行的状态。并发编程,是一种将一个程序分解成小片段独立执行的程序设计方法。

并发的两种模式 -------多线程并发与多进程并发

多进程并发
      多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。
多线程并发
      在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。

    原来的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
注:以上内容参考:https://blog.csdn.net/godqiao/article/details/81075576

std::thread

      thread 类代表着单线程执行,Threads 允许同时执行多个函数。要执行的函数会作为构造函数的参数传递给thread 对象,

所以在thread 对象构造的时候,线程就开始执行了。函数的返回值被忽略,如果函数在执行过程中发生了异常,那么会调用

 std::terminate 方法。通过 std::promise 方法,函数也可以和调用者进行通信。
 

#include <iostream>
#include <thread>
using namespace std;
 
void task_one() {
    for (int i = 0; i < 10; i++) {
        cout << this_thread::get_id() << '\t' << i << endl;
        this_thread::sleep_for(chrono::milliseconds(5));    // 休眠5ms
    }
}
 
void task_two(int n) {
    for (int i = 0; i < n; i++) {
        cout << this_thread::get_id() << '\t' << i << endl;
        this_thread::sleep_for(chrono::milliseconds(10));   //休眠10ms
    }
}
 
int main() {
    int n = 20;
 
    thread t1(task_one);
    thread t2(task_two, n);
 
    t1.join();
    t2.join();
 
    return 0;
}

std::thread::joinable 

判断一个线程对象是否是活跃的正在执行的线程。如果

get_id() != std::thread::id() 那么,返回true。

Check if joinable
Returns whether the thread object is joinable.

返回线程对象是否是joinable的。


A thread object is joinable if it represents a thread of execution.

如果是一个正在执行的线程,那么它是joinable的。

A thread object is not joinable in any of these cases:

下列任一情况都是非joinable

    if it was default-constructed.
    默认构造器构造的。
    if it has been moved from (either constructing another thread object, or assigning to it).
    通过移动构造获得的。
    if either of its members join or detach has been called.
    调用了join或者detach方法的。
 

#include <iostream>
#include <thread>
#include <ctime>
using namespace std;

void delay(double sec)    
{    
    time_t start_time, cur_time; // 变量声明    
    time(&start_time);    
    do {    
           time(&cur_time);    
       }while((cur_time - start_time) < sec );    
}; 
 
void show(int n){
    cout<<"n="<<n<<endl;
}
thread returnThread(){
    thread tt(show,10);	
    return tt;
}
 
int main()
{
 
    thread t(show,18);
    cout<<"t is joinable? "<<t.joinable()<<endl;
 
    thread t1(returnThread());
    cout<<"t1 is joinable? "<<t1.joinable()<<endl;
	
    thread t2(show,3);
    cout<<"t2 is joinable? "<<t2.joinable()<<endl;
    t2.join();
    cout<<"after t2.join(),t2 is joinable? "<<t2.joinable()<<endl;
	
    thread t3(show,5);
    cout<<"t3 is joinable? "<<t3.joinable()<<endl;
    t3.detach();
    cout<<"after t3.detach(),t3 is joinable? "<<t3.joinable()<<endl;
}

运行结果

 

std::thread::join() 

阻塞当前线程,直到线程(通过this标记)结束。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    // simulate expensive operation
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void bar()
{
    // simulate expensive operation
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::cout << "starting first helper...\n";
    std::thread helper1(foo);
 
    std::cout << "starting second helper...\n";
    std::thread helper2(bar);
 
    std::cout << "waiting for helpers to finish..." << std::endl;
    helper1.join();
    helper2.join();
 
    std::cout << "done!\n";
}
 
output
starting first helper...
starting second helper...
waiting for helpers to finish...
done!

std::thread::detach()

线程状态:

在一个线程的生存期内,可以在多种状态之间转换,不同的操作系统可以实现不同的线程模型,定义许多不同的线程状态,每个状态还可以包含多个子状态,但大体来说,如下几种状态是通用的:

1)就绪:参与调度,等待被执行,一旦被调度选中,立即开始执行

2)运行:占用CPU,正在运行中

3)休眠:暂不参与调度,等待特定事件发生

4)中止:已经运行完毕,等待回收线程资源

线程环境:

线程存在于进程之中,进程内所有全局资源对于内部每个线程都是可见的。

进程内典型全局资源如下:

1)代码区:这意味着当前进程空间内所有的可见的函数代码,对于每个线程来说,也是可见的

2)静态存储区:全局变量,静态空间

3)动态存储区:堆空间

线程内典型的局部资源:

1)本地栈空间:存放本线程的函数调用栈,函数内部的局部变量等

2)部分寄存器变量:线程下一步要执行代码的指针偏移量

一个进程发起后,会首先生成一个缺省的线程,通常称这个线程为主线程,C/C++程序中,主线程就是通过main函数进入的线程,由主线程衍生的线程成为从线程,从线程也可以有自己的入口函数,相当于主线程的main函数,这个函数由用户指定。通过thread构造函数中传入函数指针实现,在指定线程入口函数时,也可以指定入口函数的参数。就像main函数有固定的格式要求一样,线程的入口函数也可以有固定的格式要求,参数通常都是void*类型,返回类型根据协议的不同也不同,pthread中是void*,winapi中是unsigned int,而且都是全局函数。

最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。

无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:

1、可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。

2、相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

在任何一个时间点上,线程是可结合(joinable)或者是可分离的(detached),一个可结合的线程能够被其他线程回收资源和杀死,在被其他线程回收之前,它的存储器资源如栈,是不释放的,相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
 

其实重点就是理解线程的join,detach,joinable三者之间的关系:

我们从thread的析构函数~thread()入手分析

~thread() _NOEXCEPT {	    // 析构函数
    if (joinable())         // 线程是可结合的,析构异常(也就是说只能析构不可结合的线程)
        _XSTD terminate();  // terminate会调用abort()来终止程序
}

        其实析构函数在这里只进行了判断,并没有析构什么,因为thread成员变量不存在用new或者是malloc进行内存分配的指针或者数组,所以在析构函数里不做资源释放的工作,那为什么只能析构不可结合的线程呢?

(可结合或者是不可结合都是指的是线程的状态,正在执行的线程和没有执行的线程)

我们还是以博客最开始的源代码进行分析,主线程main以及t1,t2两个线程:

int main() {
    int n = 20;
 
    thread t1(task_one);
    thread t2(task_two, n);
 
    t1.join();
    t2.join();
 
    cout << "main thread" << endl;
    return 0;
}

我们总结一下线程为不可结合(即joinable()为false)的几种情况:

    空线程
    move后的线程,即move(t),则t是不可结合的
    join后的线程
    detach后的线程

 在实例化了t1、t2对象之后,它们的状态默认都是可结合的,如果现在直接调用它们的析构函数来析构它们,那么在析构的时候线程处于什么状态呢?是执行完了吗?还是正在执行呢?注意,如果一个在没有结合(join)的情况下,就算它先于主线程执行完毕,其id依然是不为0的。所以我们是不能确定其状态的,所以我们只能析构明确了id为0的线程。因为id为0的线程要么已经执行完毕,要么是空线程,要么是分离后的线程。

另外,一个线程分离(detech)后,该线程对象便不能控制该线程,而是交由系统接管。
 

std::stread 小例子

在C++11中引入了一个用于多线程操作的thread类,简单的多线程实例:

#include <iostream>
#include <thread>
#include <Windows.h>
 
using namespace std;
 
void thread01()
{
    for (int i = 0; i < 5; i++) {
	cout << "Thread 01 is working !" << endl;
	Sleep(100);
    }
}
void thread02()
{
    for (int i = 0; i < 5; i++) {
	cout << "Thread 02 is working !" << endl;
	Sleep(200);
    }
}
 
int main()
{
    thread task01(thread01);
    thread task02(thread02);
    task01.join();
    task02.join();

    for (int i = 0; i < 5; i++) {
	cout << "Main thread is working !" << endl;
	Sleep(200);
    }
    system("pause");
}

简单分析一下,正如前面所说,join()的作用是阻塞主线程,只有在thread01与cthread02两个线程执行完成之后,才会继续执行主线程里面的东西,输出结果如下:

当然,我们也可以使用detach将子线程从主流程中分离,独立运行,不会阻塞主线程:

#include <iostream>
#include <thread>
#include <Windows.h>
 
using namespace std;
 
void thread01()
{
    for (int i = 0; i < 5; i++) {
	cout << "Thread 01 is working !" << endl;
	Sleep(100);
    }
}
void thread02()
{
    for (int i = 0; i < 5; i++) {
	cout << "Thread 02 is working !" << endl;
	Sleep(200);
    }
}
 
int main()
{
    thread task01(thread01);
    thread task02(thread02);
    task01.detach();
    task02.detach();
 
    for (int i = 0; i < 5; i++) {
	cout << "Main thread is working !" << endl;
	Sleep(200);
    }
    system("pause");
}

 这样的话,两个子线程就与主线程并行执行了,输出结果如下:

也就是说,即使主线程main函数结束了,那两个子线程还是照样运行的。 

 

注:以上内容参考:https://blog.csdn.net/godqiao/article/details/81075576

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breakpoints_

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值