C++11 多线程(std::thread)详解

C++11 多线程(std::thread)详解注 此教程以 VisualStudio 10 3 MSVC19 29 30038 1 为标准文章目录线程 进程 多线程 什么是多线程 进程与线程的区别 C 11 的 std threadstd thread 常用成员函数构造 amp 析构函数常用成员函数举个栗子例一 thread 的基本使用例二 thread 执行有参数的函数例三 thread 执行带有引用参数的函数注意事项 剩下的内容还没写完 明天再更 线程 进程 多线程 什么是多线程 百度百科中的解释 多

前方高能:本文字数接近2万

线程?进程?多线程?

什么是多线程?

百度百科中的解释:

进程与线程的区别

定义:

进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。

区别:

  • 一个程序有且只有一个进程,但可以拥有至少一个的线程。
  • 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。

C++11的std::thread

在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 (如果你认为句柄、回调式编程很实用,那请当我没说),所以c++11标准库中出现了一个叫作std::thread的东西。

std::thread常用成员函数

构造&析构函数
函数 类别 作用
thread() noexcept 默认构造函数 创建一个线程,
什么也不做

template


explicit thread(Fn&& fn, Args&&… args)



初始化构造函数 创建一个线程,
args为参数
执行fn函数




thread(const thread&) = delete复制构造函数(已删除)
thread(thread&& x) noexcept移动构造函数构造一个与x
相同的对象,会破坏x对象

~thread()析构函数析构对象
常用成员函数
函数作用
void join()等待线程结束并清理资源(会阻塞)
bool joinable()返回线程是否可以执行join函数
void detach()将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)
std::thread::id get_id()获取线程id
thread& operator=(thread &&rhs)见移动构造函数
(如果对象是joinable的,那么会调用std::terminate()结果程序)

举个栗子

例一:thread的基本使用
// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include       #include       using namespace std; void doit() {     cout << "World!" << endl; } int main() {     // 这里的线程a使用了 C++11标准新增的lambda函数 // 有关lambda的语法,请参考我之前的一篇博客 // https://blog.csdn.net/sjc_0910/article/details/ thread a([]{     cout << "Hello, " << flush; }), b(doit); a.join(); b.join(); return 0; } 

输出结果:

Hello, World! 

或者是

World! Hello, 

多线程运行时是以异步方式执行的,与我们平时写的同步方式不同。异步方式可以同时执行多条语句。

在上面的例子中,我们定义了2个thread,这2个thread在执行时并不会按照一定的顺序。打个比方,2个thread执行时,就好比赛跑,谁先跑到终点,谁就先执行完毕。

例二:thread执行有参数的函数
// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     using namespace std; void countnumber(int id, unsigned int n) { 
    for (unsigned int i = 1; i <= n; i++); cout << "Thread " << id << " finished!" << endl; } int main() { 
    thread th[10]; for (int i = 0; i < 10; i++) th[i] = thread(countnumber, i, ); for (int i = 0; i < 10; i++) th[i].join(); return 0; } 

你的输出有可能是这样

Thread 2 finished!Thread 3 finished! Thread 7 finished! Thread 5 finished! Thread 8 finished! Thread 4 finished! Thread 6 finished! Thread 0 finished! Thread 1 finished! Thread 9 finished! 

注意:我说的是有可能。你的运行结果可能和我的不一样,这是正常现象,在上一个例子中我们分析过原因。

这个例子中我们在创建线程时向函数传递了一些参数,但如果要传递引用参数呢?是不是像这个例子中直接传递就行了?让我们来看看第三个例子:

例三:thread执行带有引用参数的函数
// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     using namespace std; template<class T> void changevalue(T &x, T val) { 
    x = val; } int main() { 
    thread th[100]; int nums[100]; for (int i = 0; i < 100; i++) th[i] = thread(changevalue<int>, nums[i], i+1); for (int i = 0; i < 100; i++) { 
    th[i].join(); cout << nums[i] << endl; } return 0; } 

如果你尝试编译这个程序,那你的编译器一定会报错

E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(55): error C2672: “std::invoke”: 未找到匹配的重载函数 E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(61): note: 查看对正在编 译的函数 模板 实例化“unsigned int std::thread::_Invoke<_Tuple,0,1,2>(void *) noexcept”的引用 with [ _Tuple=_Tuple ] E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(66): note: 查看对正在编 译的函数 模板 实例化“unsigned int (__cdecl *std::thread::_Get_invoke<_Tuple,0,1,2>(std::integer_sequence 
  
    ) noexcept)(void *) noexcept”的引用 E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(89): note: 查看对正在编 译的函数 模板 实例化“void std::thread::_Start 
   
     (_Fn,int &,_Ty &&)”的引用 with [ T=int, _Ty=int, _Fn=void (__cdecl &)(int &,int) ] main.cpp(11): note: 查看对正在编译的函数 模板 实例化“std::thread::thread 
    
      (_Fn,int &,int &&)” 的引用 with [ T=int, _Fn=void (__cdecl &)(int &,int) ] E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): error C2893: 未能使 函数模板“unknown-type std::invoke(_Callable &&,_Ty1 &&,_Types2 &&...) noexcept( 
     
       )”专用化 E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\type_traits(1589): note: 参见“std::invoke”的声明 E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: 用下列模板参 数: E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Callable=void (__cdecl *)(T &,T)” E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Ty1=int” E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Types2={int}” E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): error C2780: “unknown-type std::invoke(_Callable &&) noexcept( 
      
        )”: 应输入 1 个参数,却提供了 3 个 E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\type_traits(1583): note: 参见“std::invoke”的声明 
       
      
     
    
  

这是怎么回事呢?原来thread在传递参数时,是以右值传递的:

template 
  
    explicit thread(Fn&& fn, Args&&... args) 
  

划重点:Args&&... args
很明显的右值引用,那么我们该如何传递一个左值呢?std::refstd::cref很好地解决了这个问题。
std::ref 可以包装按引用传递的值。
std::cref 可以包装按const引用传递的值。
针对上面的例子,我们可以使用以下代码来修改:








// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     using namespace std; template<class T> void changevalue(T &x, T val) { 
    x = val; } int main() { 
    thread th[100]; int nums[100]; for (int i = 0; i < 100; i++) th[i] = thread(changevalue<int>, ref(nums[i]), i+1); for (int i = 0; i < 100; i++) { 
    th[i].join(); cout << nums[i] << endl; } return 0; } 

这次编译可以成功通过,你的程序输出的结果应该是这样的:

1 2 3 4 ... 99 100 

(中间省略了一堆数)

注意事项

  • 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
  • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
  • 没有执行joindetach的线程在程序结束时会引发异常

C++11中的std::atomic和std::mutex

我们现在已经知道如何在c++11中创建线程,那么如果多个线程需要操作同一个变量呢?

为什么要有atomic和mutex

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     using namespace std; int n = 0; void count10000() { 
    for (int i = 1; i <= 10000; i++) n++; } int main() { 
    thread th[100]; // 这里偷了一下懒,用了c++11的foreach结构 for (thread &x : th) x = thread(count10000); for (thread &x : th) x.join(); cout << n << endl; return 0; } 

我的2次输出结果分别是:

  

std::mutex

std::mutex是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特性,我们可以修改一下上一个例子中的代码:

例四:std::mutex的使用

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     #include  
     using namespace std; int n = 0; mutex mtx; void count10000() { 
    for (int i = 1; i <= 10000; i++) { 
    mtx.lock(); n++; mtx.unlock(); } } int main() { 
    thread th[100]; for (thread &x : th) x = thread(count10000); for (thread &x : th) x.join(); cout << n << endl; return 0; } 

执行了好几次,输出结果都是,说明正确。

mutex的常用成员函数

(这里用mutex代指对象

函数 作用
void lock() 将mutex上锁。
如果mutex已经被其它线程上锁,
那么会阻塞,直到解锁;
如果mutex已经被同一个线程锁住,
那么会产生死锁。








void unlock()解锁mutex,释放其所有权。
如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;
如果mutex不是被此线程上锁,那么会引发未定义的异常。




bool try_lock()尝试将mutex上锁。
如果mutex未被上锁,则将其上锁并返回true;
如果mutex已被锁则返回false。




std::atomic

例五:std::atomic的使用

根据atomic的定义,我又修改了例四的代码:

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     // #include 
   
     //这个例子不需要mutex了 
    #include  
     using namespace std; atomic_int n = 0; void count10000() { 
    for (int i = 1; i <= 10000; i++) { 
    n++; } } int main() { 
    thread th[100]; for (thread &x : th) x = thread(count10000); for (thread &x : th) x.join(); cout << n << endl; return 0; } 

输出结果:,正常

代码解释

可以看到,我们只是改动了n的类型(int->std::atomic_int),其他的地方一点没动,输出却正常了。
有人可能会问了:这个std::atomic_int是个什么玩意儿?其实,std::atomic_int只是std::atomic
的别名罢了。
atomic,本意为原子,官方 (我不确定是不是官方,反正继续解释就对了) 对其的解释是




原子操作是最小的且不可并行化的操作。

这就意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。

std::atomic常用成员函数

构造函数

对,atomic没有显式定义析构函数

函数 类型 作用
atomic() noexcept = default 默认构造函数 构造一个atomic对象(未初始化,可通过atomic_init进行初始化)
constexpr atomic(T val) noexcept 初始化构造函数 构造一个atomic对象,用val的值来初始化
atomic(const atomic&) = delete 复制构造函数 (已删除)
常用成员函数

atomic能够直接当作普通变量使用,成员函数貌似没啥用,所以这里就不列举了,想搞明白的点这里 (英语渣慎入,不过程序猿中应该没有英语渣吧)

C++11中的std::async

注:std::async定义在future头文件中。

为什么大多数情况下使用async而不用thread

std::async参数

不同于thread,async是一个函数,所以没有成员函数。

重载版本 作用
template


  future

::type>

    async (Fn&& fn, Args&&… args)







异步或同步(根据操作系统而定)以args为参数执行fn
同样地,传递引用参数需要std::refstd::cref

template


  future

::type>

    async (launch policy, Fn&& fn, Args&&… args);







异步或同步(根据policy参数而定(见下文))以args为参数执行fn,引用参数同上
std::launch强枚举类(enum class)

std::launch有2个枚举值和1个特殊值:

标识符 实际值(以Visual Studio 2019为标准) 作用
枚举值:launch::async 0x1(1) 异步启动
枚举值:launch::deferred 0x2(2) 在调用future::get、future::wait时同步启动(std::future见后文)
特殊值:launch::async | launch::defereed 0x3(3) 同步或异步,根据操作系统而定

例六:std::async的使用

暂且不管它的返回值std::future是啥,先举个例再说。

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     #include  
     using namespace std; int main() { 
    async(launch::async, [](const char *message){ 
    cout << message << flush; }, "Hello, "); cout << "World!" << endl; return 0; } 

你的编译器可能会给出一条警告:

warning C4834: 放弃具有 "nodiscard" 属性的函数的返回值 
Hello, World! 

不过如果你输出的是

World! Hello, 

也别慌,正常现象,多线程嘛!反正我执行了好几次也没出现这个结果。

C++11中的std::future

我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。

例七:使用std::future获取线程的返回值

在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     // #include 
   
     // 这里我们用async创建线程 
    #include  
     // std::async std::future using namespace std; template<class ... Args> decltype(auto) sum(Args&&... args) { 
    // C++17折叠表达式 // "0 +"避免空参数包错误 return (0 + ... + args); } int main() { 
    // 注:这里不能只写函数名sum,必须带模板参数 future<int> val = async(launch::async, sum<int, int, int>, 1, 10, 100); // future::get() 阻塞等待线程结束并获得返回值 cout << val.get() << endl; return 0; } 

输出:

111 
代码解释

我们定义了一个函数sum,它可以计算多个数字的和,之后我们又定义了一个对象val,它的类型是std::future
,这里的int代表这个函数的返回值是int类型。在创建线程后,我们使用了future::get()来阻塞等待线程结束并获取其返回值。至于sum函数中的折叠表达式(fold expression),不是我们这篇文章的重点。

std::future常用成员函数

构造&析构函数
函数 类型 作用
future() noexcept 默认构造函数 构造一个空的、无效的future对象,但可以移动分配到另一个future对象
future(const future&) = delete 复制构造函数 (已删除)
future (future&& x) noexcept 移动构造函数 构造一个与x相同的对象并破坏x
~future() 析构函数 析构对象
常用成员函数
函数 作用
一般:T get()
当类型为引用:R& future

::get()

当类型为void:void future::get()





阻塞等待线程结束并获取返回值。
若类型为void,则与future::wait()相同。
只能调用一次。




void wait() const阻塞等待线程结束
template


  future_status wait_for(const chrono::duration

& rel_time) const;




阻塞等待rel_timerel_time是一段时间),
若在这段时间内线程结束则返回future_status::ready
若没结束则返回future_status::timeout
若async是以launch::deferred启动的,则不会阻塞并立即返回future_status::deferred






不知道std::chrono::duration的点这里

std::future_status强枚举类

见上文future::wait_for解释

为啥要有void特化的std::future?

std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。

例八:void特化std::future
// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     using namespace std; void count_big_number() { 
    // C++14标准中,可以在数字中间加上单 // 引号 ' 来分隔数字,使其可读性更强 for (int i = 0; i <= 10'0000'0000; i++); } int main() { 
    future<void> fut = async(launch::async, count_big_number); cout << "Please wait" << flush; // 每次等待1秒 while (fut.wait_for(chrono::seconds(1)) != future_status::ready) cout << '.' << flush; cout << endl << "Finished!" << endl; return 0; } 

如果你运行一下这个代码,你也许就能搞懂那些软件的加载画面是怎么实现的。

C++11中的std::promise

std::thread th(func); std::future<int> return_value = th.join(); 

例九:引用传递返回值

这个例子中我们先不牵扯多线程的问题。假如你写一个函数,需要返回3个值,那你会怎么办呢?vector?嵌套pair?不不不,都不需要,3个引用参数就可以了。

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     using namespace std; constexpr long double PI = 3.; // 给定圆的半径r,求圆的直径、周长及面积 void get_circle_info(double r, double &d, double &c, double &s) { 
    d = r * 2; c = PI * d; s = PI * r * r; } int main() { 
    double r; cin >> r; double d, c, s; get_circle_info(r, d, c, s); cout << d << ' ' << c << ' ' << s << endl; return 0; } 

输入5,输出:

10 31.4159 78.5398 

如果你和我输出有一些误差,是正常现象,不同编译器、不同机器处理精度也有所不同

std::promise到底是啥

例十:std::future的值不能改变,那么如何利用引用传递返回值
constexpr int a = 1; 
constexpr int a = 1; constexpr int b = 2; 

future的值不能改变,promise的值可以改变。

std::promise常用成员函数

构造&析构函数
函数 类型 作用
promise() 默认构造函数 构造一个空的promise对象
template

promise(allocator_arg_t aa, const Alloc& alloc)
构造函数 与默认构造函数相同,但使用特定的内存分配器alloc构造对象
promise (const promise&) = delete 复制构造函数 (已删除)
promise (promise&& x) noexcept 移动构造函数 构造一个与x相同的对象并破坏x
~promise() 析构函数 析构对象
常用成员函数
函数 作用
一般:
void set_value (const T& val)
void set_value (T&& val)
当类型为引用:void promise

::set_value (R& val)

当类型为void:void promise::set_value (void)








设置promise的值并将共享状态设为ready(将future_status设为ready)
void特化:只将共享状态设为ready

future get_future()构造一个future对象,其值与promise相同,status也与promise相同

例十一:std::promise的使用

以例七中的代码为基础加以修改:

// Compiler: MSVC 19.29.30038.1 // C++ Standard: C++17 #include  
     #include  
     #include  
     // std::promise std::future using namespace std; template<class ... Args> decltype(auto) sum(Args&&... args) { 
    return (0 + ... + args); } template<class ... Args> void sum_thread(promise<long long> &val, Args&&... args) { 
    val.set_value(sum(args...)); } int main() { 
    promise<long long> sum_value; thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100); cout << sum_value.get_future().get() << endl; get_sum.join(); // 感谢评论区 未来想做游戏 的提醒 return 0; } 

输出:

111 

C++11中的std::this_thread

std::this_thread常用函数

std::this_thread是个命名空间,所以你可以使用using namespace std::this_thread;这样的语句来展开这个命名空间,不过我不建议这么做。

函数 作用
std::thread::id get_id() noexcept 获取当前线程id
template


void sleep_for( const std::chrono::duration

& sleep_duration )




等待sleep_durationsleep_duration是一段时间)
void yield() noexcept 暂时放弃线程的执行,将主动权交给其他线程
(放心,主动权还会回来)

例十二:std::this_thread中常用函数的使用

#include  
     #include  
     #include  
     using namespace std; atomic_bool ready = 0; // uintmax_t ==> unsigned long long void sleep(uintmax_t ms) { 
    this_thread::sleep_for(chrono::milliseconds(ms)); } void count() { 
    while (!ready) this_thread::yield(); for (int i = 0; i <= 20'0000'0000; i++); cout << "Thread " << this_thread::get_id() << " finished!" << endl; return; } int main() { 
    thread th[10]; for (int i = 0; i < 10; i++) th[i] = thread(::count); sleep(5000); ready = true; cout << "Start!" << endl; for (int i = 0; i < 10; i++) th[i].join(); return 0; } 

我的输出:

Start! Thread 8820 finished!Thread 6676 finished! Thread 13720 finished! Thread 3148 finished! Thread 13716 finished! Thread 16424 finished! Thread 14228 finished! Thread 15464 finished! Thread 3348 finished! Thread 6804 finished! 

你的输出几乎不可能和我一样,不仅是多线程并行的问题,而且每个线程的id也可能不同。

结尾

这篇文章到这里就结束了 (说不定以后还会写个c++20的std::jthread讲解)。 感谢各位在评论区提出的建议。 这是我第一篇接近2万字的文章。其实我刚开始写这篇文章时,也没想到这篇文章会吸引这么多人看,评论里还会有很多的好评,并且还上过一次热榜:
热榜
(厚颜无耻地给自己点赞)
又入选过C/C++领域内容榜:
C/C++领域内容榜
这着实是出乎我的意料的。在此也感谢评论区里各位的好评,我就不一一回复了。
如果你觉得这篇文章有不对、不标准之处,也可以在评论区里说一下,感谢支持。












版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/202233.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月20日 上午7:35
下一篇 2026年3月20日 上午7:35


相关推荐

  • Linux-awk数组

    Linux-awk数组linuxawk数组

    2022年7月19日
    17
  • gcc编译过程

    gcc编译过程1 gcc 编译器的安装工作中 在 Linux 环境下 有过量产项目的公司 gcc 一般都是已经安装好了的 如果需要安装 请从官网下载或网上找资源下载 gcc 官网 https gcc gnu org 此处不赘述安装过程了查看是否安装成功 gcc v 能查到 gcc 版本号 说明安装成功 2 编译过程介绍 最开始学习编程的时候 一般都是用 IDE 去编译的 不管是 IDE 还是 gcc 编译的时候都会有四个过程一个编译过程一般要经过四个过程 预处理编译汇编链接预处理

    2026年3月18日
    3
  • hdu 5685

    hdu 5685

    2021年5月26日
    109
  • 万文多图之Pycharm的使用图解

    万文多图之Pycharm的使用图解文章目录 1 新建项目 2 窗口介绍 3 Pycharm 设置 4 Pycharm 菜单中的功能 1 新建项目 Pycharm 是根据项目组织的 项目相关的配置文件存放在项目文件夹下的 idea 文件夹 隐藏文件夹 中 新建环境可以通过 Virtualenv 进行创建 也可以通过 Conda 进行创建 下方状态栏如下所示说明 Pycharm 正在对选定的 Python 解释器进行索引工作 在这个过程中 Pycharm 的自动补全和代码高亮都暂时无效 2 窗口介绍新建文件 File 空文件 Dir

    2026年3月27日
    3
  • 架构设计:微服务模式下,实现灰度发布模式

    架构设计:微服务模式下,实现灰度发布模式

    2020年11月20日
    191
  • 串口调试工具–SecureCRT的使用

    串口调试工具–SecureCRT的使用说明 SecureCRT 如何连接 调试 记录 log

    2026年3月26日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号