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

相关文章
|
7天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
21 2
|
11天前
|
存储 算法 C++
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
文章详细探讨了C++中的泛型编程与STL技术,重点讲解了如何使用模板来创建通用的函数和类,以及模板在提高代码复用性和灵活性方面的作用。
27 2
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
|
3天前
|
程序员 C++
C++编程:While与For循环的流程控制全解析
总结而言,`while`循环和 `for`循环各有千秋,它们在C++编程中扮演着重要的角色。选择哪一种循环结构应根据具体的应用场景、循环逻辑的复杂性以及个人的编程风格偏好来决定。理解这些循环结构的内在机制和它们之间的差异,对于编写高效、易于维护的代码至关重要。
9 1
|
12天前
|
Java 调度 开发者
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java多线程编程的核心概念和实际应用,通过浅显易懂的语言解释多线程的基本原理,并结合实例展示如何在Java中创建、控制和管理线程。我们将从简单的线程创建开始,逐步深入到线程同步、通信以及死锁问题的解决方案,最终通过具体的代码示例来加深理解。无论您是Java初学者还是希望提升多线程编程技能的开发者,本文都将为您提供有价值的见解和实用的技巧。
15 2
|
14天前
|
Java 数据处理
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java中的多线程编程,涵盖其基本概念、创建方法、同步机制及实际应用。通过对多线程基础知识的介绍和具体示例的演示,希望帮助读者更好地理解和应用Java多线程编程,提高程序的效率和性能。
19 1
|
18天前
|
Java
深入理解Java中的多线程编程
本文将探讨Java多线程编程的核心概念和技术,包括线程的创建与管理、同步机制以及并发工具类的应用。我们将通过实例分析,帮助读者更好地理解和应用Java多线程编程,提高程序的性能和响应能力。
20 4
|
26天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
17天前
|
安全 Java 调度
Java 并发编程中的线程安全和性能优化
本文将深入探讨Java并发编程中的关键概念,包括线程安全、同步机制以及性能优化。我们将从基础入手,逐步解析高级技术,并通过实例展示如何在实际开发中应用这些知识。阅读完本文后,读者将对如何在多线程环境中编写高效且安全的Java代码有一个全面的了解。
|
27天前
|
算法 Java 数据处理
Java并发编程:解锁多线程的力量
在Java的世界里,掌握并发编程是提升应用性能和响应能力的关键。本文将深入浅出地探讨如何利用Java的多线程特性来优化程序执行效率,从基础的线程创建到高级的并发工具类使用,带领读者一步步解锁Java并发编程的奥秘。你将学习到如何避免常见的并发陷阱,并实际应用这些知识来解决现实世界的问题。让我们一起开启高效编码的旅程吧!
|
7天前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。
下一篇
无影云桌面