C++多线程编程(上)

简介: C++多线程编程

多线程学习笔记

前言:这周学习学习了多线程并发的相关知识,写一个读书笔记以作记录。学习的教程是网易云课堂的:https://study.163.com/course/courseMain.htm?courseId=1006067356&trace_c_p_k2=217aa888da5741698cfb97e1e70009cd


更新:在最近的项目中使用到了多线程技术,发现有些知识遗漏或是不准确,已对下文进行更正和重新排版。2020/11/3


一、进程、线程、并发

  • 进程:简而言之就是一个运行的程序,如点开一个exe文件就打开了一个进程;

线程:进程中的不同执行路径,即在一个进程中运行了多个功能;(每一个进程都至少包含有一个线程,即主线程,主线程与进程的关系是相互依存)


并发:分为多线程和多进程,顾名思义就是多进程同时运行和多线程同时运行,如同时打开两个QQ客户端就是多进程,VStudio中多个窗口线程就是多线程并发;

二、thread库

以前由于系统环境的不同,如windows、linux,就需要选择不同的线程库进行代码编写,可移植性不高。

C++11之后有了标准的线程库:std::thread

除此之外,C++还有另外三个库支持多线程编程,分别是**,,和**,之后我会对其一一介绍,现在先来看看Thread库。

看看thread类,它是thread库多线程实现的基础。

构造函数

从构造函数可以窥见“多线程”的一些实现思想。

thread()默认构造函数,创建一个空的 std::thread 执行对象(在线程池的实现中就需要提前创建一定数量的线程对象);

thread(Fn&& fn, Args&&…)初始化构造函数,创建一个 std::thread 对象,该 std::thread 对象可被 joinable,新产生的线程会调用 fn 函数(即可调用对象),该函数的参数由 args 给出。


thread(const thread&) = delete拷贝构造函数(被禁用),意味着 std::thread 对象不可拷贝构造,这也可以理解,“线程”这个概念在同一时刻仅能由一个线程对象运行,所以不存在拷贝赋值之类的;

**thread(thread&& x)**转移/移动构造函数,,调用成功之后 x 不代表任何 std::thread 执行对象,相当于将“线程”的所有权转给了另外的线程对象。

下面这个例子就展示了如何创建线程:

//参考并稍加修改自博客:  https://blog.csdn.net/coolwriter/article/details/79883253
//example 1_1
#include <iostream>       // std::cout  
#include <thread>         // std::thread  
void func1()  
{  
    for (int i = 0; i != 10; ++i)  
    {  
        std::cout << "thread 1 print " << i << std::endl;  
    }  
}  

void func2(int n)  
{  
    std::cout << "thread 2 print " << n << std::endl;  
}  

int main()  
{  
    std::thread t1(func1);     
    std::thread t2(func2, 123);   
    std::cout << "main, foo and bar now execute concurrently...\n";  
    // synchronize threads:  
    t1.join();                // pauses until first finishes  
    t2.join();               // pauses until second finishes  
    std::cout << "thread 1 and htread 2 completed.\n";  
    system("pause");
    return 0;  
}  

结果:

861693d5a5b3c9e88e3435cd72456759.png

上述代码中,使用两个重要成员函数。

重要函数

join,它可以阻塞主线程直到子线程执行完毕,即必须等待A.join()执行完毕,才会接着执行之后的代码;

detach,表示该线程和主线程分离,该线程被运行时库给接管,若是在linux环境下运行,会发现即便Ctrl+C退出了主线程,但是子线程依旧还在运行。

一旦线程执行完毕,它所分配的资源将会被释放。

另外,调用 detach 函数之后this不再代表任何的线程执行实例。

PS:值得一提的是,多线程的调度机制可能会造成先创建的线程还未执行,而后面的线程就已经开始执行了,比如上述例子在代码中的顺序应该是thread1 thread2 而后打印出main,但实际效果却并非如此,因此在多线程程序的编写要注意变量的互斥性,以及代码的鲁棒性。

至于其他的成员函数:

get_id:获取线程 ID,返回一个类型为 std::thread::id 的对象。


0f54de0ec5ef6159cf8fe763714f6aaa.png

joinable():检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程由默认构造函数创建的线程是不能被 join 的

swap():Swap 线程,交换两个线程对象所代表的底层句柄即ID等资源,见下:

#include <iostream>
#include <thread>
#include <chrono>

void foo()
{
  std::this_thread::sleep_for(std::chrono::seconds(1));
}

void bar()
{
  std::this_thread::sleep_for(std::chrono::seconds(1));
}

int main()
{
  std::thread t1(foo);
  std::thread t2(bar);

  std::cout << "thread 1 id: " << t1.get_id() << std::endl;
  std::cout << "thread 2 id: " << t2.get_id() << std::endl;

  std::swap(t1, t2);

  std::cout << "after std::swap(t1, t2):" << std::endl;
  std::cout << "thread 1 id: " << t1.get_id() << std::endl;
  std::cout << "thread 2 id: " << t2.get_id() << std::endl;

  t1.swap(t2);

  std::cout << "after t1.swap(t2):" << std::endl;
  std::cout << "thread 1 id: " << t1.get_id() << std::endl;
  std::cout << "thread 2 id: " << t2.get_id() << std::endl;

  t1.join();
  t2.join();
}

结果如下:


0c2acc2d1c1d98a916518a1efdd813d0.png

yield: 当前线程放弃执行,操作系统调度另一线程继续执行。

sleep_until: 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒,详见连接

sleep_for: 线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长。

#include <iostream>
#include <chrono>
#include <thread>

int main()
{
  std::cout << "Hello waiter" << std::endl;
  std::chrono::milliseconds dura( 2000 );
  std::this_thread::sleep_for( dura );
  std::cout << "Waited 2000 ms\n";
}

三、mutex库

互斥

在介绍mutex之间,先简要介绍一下何为互斥。

互斥是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。

最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

mutex主要是对临界区进行互斥操作,防止多线程同时访问该资源,造成系统错误。

常用函数

该库中所要使用的类及函数有以下这些:

mutex类4种
     std::mutex,最基本的 Mutex 类。 
     std::recursive_mutex,支持递归的 Mutex类。 
     std::time_mutex,定时 Mutex 类。 
     std::recursive_timed_mutex,定时递归Mutex 类。 

Lock 类(两种) 
     std::lock_guard,方便线程对互斥量上锁;
       std::unique_lock,比unique更灵活;
       std::try_to_lock_t ,尝试同时对多个互斥量上锁。
       std::lock,可以同时对多个互斥量上锁。 
       std::call_once,如果多个线程需要同时调用某个函数,可以保证多个线程对该函数只调用一次。

在介绍这些函数之前,先提一个概念——“锁”。

什么是锁?

——简单的理解就是,谁拿到了锁,谁就资源去拿屋子的东西!

体现在多线程之中则是:只有某个线程拿到锁时,它才能访问和修改锁对应的资源,其他的任何线程都只有等待!

mutex类

std::mutex 是最基本的互斥量,不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以。

mutxe的成员函数:

1、构造函数,std::mutex不允许拷贝构造,不允许 move 拷贝,开始产生的 mutex 对象是处于 unlocked 状态的

2、lock(),该线程将锁住互斥量。会发生下面 3 种情况:


  • 如果该互斥量不被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁。

PS:什么是死锁?

多个线程各占据了一部分资源的锁,但是需要所有资源才可以继续运行,但是大家都不放所,如此相持的局面。——就好比江湖人士各有一片神功秘籍残片,但是人人都不愿意交给其他人,所有人都死盯着对方放手。

3、unlock(), 解锁,释放对互斥量的所有权。

4、try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,**则当前线程也不会被阻塞。**线程调用该函数也会出现下面 3 种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁。

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的**多层所有权,**std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。


std::time_mutex 比 std::mutex 多了两个成员函数, try_lock_for(),try_lock_until()。


try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

C++多线程编程(下):https://developer.aliyun.com/article/1508312

相关文章
|
4天前
|
Java
【编程侦探社】追踪 Java 线程:一场关于生命周期的侦探故事!
【6月更文挑战第19天】在Java世界中,线程如同神秘角色,编程侦探揭示其生命周期:从新生(`new Thread()`)到就绪(`start()`),面临并发挑战如资源共享冲突。通过`synchronized`实现同步,处理阻塞状态(如等待锁`synchronized (lock) {...}`),最终至死亡,侦探深入理解并解决了多线程谜题,成为编程侦探社的传奇案例。
|
2天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
2天前
|
编译器 程序员 C++
C++一分钟之-模板基础:泛型编程
【6月更文挑战第21天】C++模板,泛型编程的关键,让代码跨类型工作,增强重用与灵活性。理解模板基础,如函数和类模板,注意避免特化与偏特化的混淆、编译时膨胀及复杂的错误调试。通过明确特化目的、限制模板使用及应用现代C++技术来优化。示例展示了模板如何自动或显式推导类型。模板元编程虽强大,但初学者宜从基础开始。正确使用模板,提升代码质量,同时保持简洁。
22 3
|
3天前
|
存储 安全 算法
Java并发编程中的线程安全性与性能优化
在Java编程中,特别是涉及并发操作时,线程安全性及其与性能优化是至关重要的问题。本文将深入探讨Java中线程安全的概念及其实现方式,以及如何通过性能优化策略提升程序的并发执行效率。
8 1
|
5天前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
18 3
|
4天前
|
安全 Java 调度
Java并发编程:优化多线程应用的性能与安全性
在当今软件开发中,多线程编程已成为不可或缺的一部分,尤其在Java应用程序中更是如此。本文探讨了Java中多线程编程的关键挑战和解决方案,重点介绍了如何通过合理的并发控制和优化策略来提升应用程序的性能和安全性,以及避免常见的并发问题。
10 1
|
4天前
|
Java
【编程炼金术】Java 线程:从一粒沙到一个世界,生命周期的奇妙转化!
【6月更文挑战第19天】Java线程生命周期始于`Thread`类或`Runnable`接口,经历创建、新生、就绪、运行、阻塞到死亡五态。调用`start()`使线程进入就绪,随后可能获得CPU执行权变为运行态。当阻塞后,线程返回就绪,等待再次执行。理解并管理线程生命周期是优化多线程程序的关键。
|
6天前
|
数据采集 安全 算法
Java并发编程中的线程安全与性能优化
在Java编程中,多线程并发是提升程序性能的关键之一。本文将深入探讨Java中的线程安全性问题及其解决方案,并介绍如何通过性能优化技术提升多线程程序的效率。
13 3
|
9天前
|
存储 安全 Java
Java多线程编程--JUC
Java多线程编程
|
9天前
|
算法 安全 编译器
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
22 1