C++2.0 —— 多线程的使用之std::thread

基本概念

线程状态:

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

  • 就绪:参与调度,等待被执行,一旦被调度选中,立即开始执行
  • 运行:占用CPU,正在运行中
  • 休眠:暂不参与调度,等待特定事件发生
  • 中止:已经运行完毕,等待回收线程资源

线程环境:

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

  • 代码区:这意味着当前进程空间内所有的可见的函数代码,对于每个线程来说,也是可见的
  • 静态存储区:全局变量,静态空间
  • 动态存储区:堆空间

线程内典型的局部资源:

  • 本地栈空间:存放本线程的函数调用栈,函数内部的局部变量等
  • 部分寄存器变量:线程下一步要执行代码的指针偏移量

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

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

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

  • 可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
  • 相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

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

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

C++11多线程构成

C++11 多线程相关的头文件

  • <atomic>:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
  • <thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
  • <mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
  • <condition_variable>:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
  • <future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

std::thread

类 thread 表示单个执行线程。线程允许多个函数同时执行。线程在构造关联的线程对象时立即开始执行(等待任何OS调度延迟),从提供给作为构造函数参数的顶层函数开始。顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用 std::terminate 。顶层函数可以通过 std::promise 或通过修改共享变量(可能需要同步,见 std::mutex 与 std::atomic )将其返回值或异常传递给调用方。

std::thread 对象也可能处于不表示任何线程的状态(默认构造、被移动、 detach 或 join 后),并且执行线程可能与任何 thread 对象无关( detach 后)。

没有两个 std::thread 对象会表示同一执行线程; std::thread 不是可复制构造 (CopyConstructible) 可复制赋值 (CopyAssignable) 的,尽管它可移动构造 (MoveConstructible) 可移动赋值 (MoveAssignable) 

下面是gcc-4.9.0的源码,其中去掉了内部class id 和 一些struct。

 class thread
  {
  public:
    typedef __gthread_t            native_handle_type;
    struct _Impl_base;
    typedef shared_ptr<_Impl_base>    __shared_base_type;

  private:
    id                _M_id;

  public:
    thread() noexcept = default;//无参的默认构造函数
    // _GLIBCXX_RESOLVE_LIB_DEFECTS
    // 2097.  packaged_task constructors should be constrained
    thread(thread&) = delete;//禁止copy constructor
    thread(const thread&) = delete;// 禁止copy constructor

    thread(thread&& __t) noexcept { swap(__t); } //支持move copy

    template<typename _Callable, typename... _Args>
      explicit 
      thread(_Callable&& __f, _Args&&... __args) //支持传参,参数形式为&& 右值引用
      {
        _M_start_thread(_M_make_routine(std::__bind_simple(
                std::forward<_Callable>(__f),
                std::forward<_Args>(__args)...)));
      }

    ~thread()
    {
      if (joinable())
    std::terminate();
    }

    thread& operator=(const thread&) = delete;//禁止copy assignment 

    thread& operator=(thread&& __t) noexcept //支持move copy assignment
    {
      if (joinable())
    std::terminate();
      swap(__t);
      return *this;
    }

    void
    swap(thread& __t) noexcept
    { std::swap(_M_id, __t._M_id); }//支持swap

    bool
    joinable() const noexcept
    { return !(_M_id == id()); }

    void
    join();

    void
    detach();

    thread::id
    get_id() const noexcept
    { return _M_id; }

    /** @pre thread is joinable
     */
    native_handle_type
    native_handle()
    { return _M_id._M_thread; }

    // Returns a value that hints at the number of hardware thread contexts.
    static unsigned int
    hardware_concurrency() noexcept;

  private:
    void
    _M_start_thread(__shared_base_type);

    template<typename _Callable>
      shared_ptr<_Impl<_Callable>>
      _M_make_routine(_Callable&& __f)
      {
    // Create and allocate full data structure, not base.
    return std::make_shared<_Impl<_Callable>>(std::forward<_Callable>(__f));
      }
  };

  • 默认构造函数,创建一个空的 thread 执行对象。
  • 初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
  • 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
  • 支持move copy构造函数,move copy assignment 函数

成员函数

(构造函数)

构造新的 thread 对象
(公开成员函数)

(析构函数)

析构 thread 对象,必须合并或分离底层线程
(公开成员函数)

operator=

移动 thread 对象
(公开成员函数)

观察器

joinable

检查线程是否可合并,即潜在地运行于平行环境中
(公开成员函数)

get_id

返回线程的 id
(公开成员函数)

native_handle

返回底层实现定义的线程句柄
(公开成员函数)

hardware_concurrency

[静态]

返回实现支持的并发线程数
(公开静态成员函数)

操作

join

等待线程完成其执行
(公开成员函数)

detach

容许线程从线程句柄独立开来执行
(公开成员函数)

swap

交换二个 thread 对象
(公开成员函数)

join

阻塞当前线程直至 *this 所标识的线程结束其执行。*this 所标识的线程的完成同步于对应的从 join() 成功返回。*this 自身上不进行同步。同时从多个线程在同一 thread 对象上调用 join() 构成数据竞争,导致未定义行为;join可能出现的问题:

detach 和 join 的区别

当thread::join()函数被调用后,调用它的线程会被block,直到线程的执行被完成。基本上,这是一种可以用来知道一个线程已结束的机制。当thread::join()返回时,OS的执行的线程已经完成,C++线程对象可以被销毁。

当thread::detach()函数被调用后,执行的线程从线程对象中被分离,已不再被一个线程对象所表达--这是两个独立的事情。C++线程对象可以被销毁,同时OS执行的线程可以继续。如果程序想要知道执行的线程何时结束,就需要一些其它的机制。join()函数在那个thread对象上不能再被调用,因为它已经不再和一个执行的线程相关联。

去销毁一个仍然可以“joinable”的C++线程对象会被认为是一种错误。为了销毁一个C++线程对象,约么join()函数需要被调用(并结束),要么detach()函数被调用。如果一个C++线程对象当销毁时仍然可以被join,异常会被抛出。

C++线程对象不被表达为执行的线程的其它的情况(也就是unjoinable):

  • 默认构造的线程对象不表达为执行的线程,所以是unjoinable。
  • 被移开的线程将不表达为执行的线程,所以是unjoinable。

在std::thread的析构函数中,std::terminate会被调用如果:

  • 线程没有被Joined(用t.join())
  • 线程也没有被detached(用t.detach())

因此,你应该在执行流程到析构函数前总是要么join,要么detach一个线程。当一个程序终止时(比如main返回),剩下的在后台的detached线程执行不会再等待;相反它们的执行会被挂起并且它们的本地线程对象会被销毁。关键地,这意味着这些线程的栈不是完好无损的,因此一些析构函数不会被执行。依赖于这些行为,一些析构函数假象会被承担,这可能是一种坏情形,好像程序已经Crash或者已经被kill。希望OS会释放加在这些文件上的锁。Depending on the actions those destructors were supposed to undertake, this might be as bad a situation as if the program had crashed or had been killed. Hopefully the OS will release the locks on files, etc... but you could have corrupted shared memory, half-written files, and the like.所以,你应该使用join还是detached?

  • 使用join
  • 除非你需要更灵活并且想要独立地提供一种同步机制来等待线程完成,在这种情况下你应该使用detach

简单的栗子:

#include <iostream>
#include <thread>

using namespace std;

void myprint() 
{
	cout << "this is a test thread_id: " << this_thread::get_id() << endl;
	
}

int main()
{
	cout << " this is main thread id: " << this_thread::get_id() << endl;

	thread t1(myprint);
	t1.join();

	cout << "main thread end" << endl;

	return system("pause");
}

 

 

以上参考:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值