C++多线程详解 | 线程创建 | 互斥锁 | 条件变量 | 线程池

目录

前言

1.线程创建

2.互斥锁

3.lock_guard与std::unique_lock

4.condition_variable

 5.线程池

前言

在说线程之前,先说说进程和线程的关系,以及什么是多线程(为了方便理解就用大白话来说)

进程:进程就是运行中的程序,比如说一个微信的程序,你双击它,它运行起来了就是一个进程,在还没有运行之间,就是要一个可执行的程序文件exe。

线程:线程可以理解为进程中的进程,就有人把进程比喻为一台火车,而线程就是每一个车厢。其实说的就是一个进程里可以有多个线程。

那我们为什么要使用多线程呢

 在回答这个问题之前,我们先假设如果没有多线程,只有单线程的话,整个任务的运行就是串行的,只能依次的执行,这也就是我们所说的多进程并发,这种模式的缺点就是使用复杂,系统开销大,所以就引入了多线程。

多线程就是在同一个进程中执行多个线程,也称为多线程并发,也就是任务并行,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。但是缺少操作系统提供的保护机制,所以在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。

1.线程创建

要创建线程,我们需要一个可调用的函数或函数对象,作为线程的入口点。在C++11中,我们可以使用函数指针、函数对象或lambda表达式来实现。创建线程的基本语法如下: 

#include <thread>
std::thread t(function_name, args...);    //创建语法
  • function_name是线程入口点的函数或可调用对象
  • args...是传递给函数的参数

创建线程后,我们可以

  • 使用t.join()等待线程完成(为了避免主线程运行结束 但是子线程还行运行完的情况),或者
  • 使用t.detach()分离线程,让它在后台运行(也就是主线程结束了 但是子线程让他继续运行),注意一旦线程被分离,就不能再使用`t.join()`方法等待它完成。而且,我们需要确保线程不会在主线程结束前退出,否则可能会导致未定义行为。
  • 使用joinable()方法判断能否使用join或detach,该方法返回一个布尔值,如果线程可以被join()或detach(),则返回true,否则返回false。如果我们试图对一个不可加入的线程调用join()或detach(),则会抛出一个std::system_error异常。
#include<iostream>
#include<thread>
#include<string>

void printHelloWorld(std::string msg )
{
    std::cout << msg << std::endl;
}

int  main()
{
    //创建线程
    std::thread thread1(printHelloWorld,"hello world!");

    thread1.join(); //主程序等待所有线程执行结束

    thread1.detach(); //一般都不会用这个
    
    bool isJoin = thread1.joinable(); //判断这个线程可不可以用join或detach, 他返回的是bool值
    
    if(isJoin)
    {
        thread1.join(); 
    }

    return 0;
}

2.互斥锁

前面就提到 多线程虽然轻量 方便 但是没有系统提供保护机制,需要我们程序员来掌控他的安全,那么如何来保证他的安全呢,下面详细说明:

在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。

为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量条件变量原子操作等。

 简单理解就是在多线程运行时,当一个线程要访问一个变量的时候,就采用一个机制手段不让其他的线程访问,这样就不会产生数据竞争的问题。这个手段就是所谓的互斥量(mutex) 

互斥量提供了两个基本操作:lock() 和 unlock()。当一个线程调用 lock() 函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock() 函数的线程会被阻塞,直到该互斥量被释放为止

 #include<mutex>

std::mutex mtx;        //定义互斥锁的语法

mtx.lock();        //加锁

mtx.unlock();        //解锁

#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 0;
std::mutex mtx;     //定义一个互斥锁

void func(int n) 
{
    for (int i = 0; i < 100000; ++i) 
    {
        mtx.lock();    //锁住,单个线程访问的时候,其他线程不能进行访问

        shared_data++;

        std::cout << "thread" << n << "incremnet sharad_data" << sharad_data << std::endl;
       
        mxt.ubnlock();    //解锁,访问结束进行解锁,这样其他的线程进行访问
    }
}

int main() 
{
    std::thread t1(func,1);
    std::thread t2(func,2);

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

    std::cout << " Final shared_data = " << shared_data << std::endl; 
   
    return 0;
}

 上面的代码中,定义了一个名为 shared_data 的全局变量,并使用互斥量 mtx 来确保多个线程对其进行访问时的线程安全。在两个线程中,分别调用了 func 函数,并传递了不同的参数。在 func 函数中,先获取互斥量的所有权,然后对 shared_data 变量进行累加操作,并输出变量的当前值。最后再释放互斥量的所有权。

3.lock_guard与std::unique_lock

 上述的互斥锁他们都是成对存在的,有lock加锁必须就有unlock解锁。需要手动 加锁和解锁

 而 lock_guard无需手工解锁,当我们进行初始化的时候,他就会自动加锁,运行结束后 ,也会自动解锁, lock_guard与std::unique_lock都是C++标准库提供的互斥量封装类,在类里面的构造函数写好了加锁,析构函数里写好了解锁,只需要调用就好了。

  • 当构造函数被调用时,该互斥量会被自动锁定。

  • 当析构函数被调用时,该互斥量会被自动解锁。

  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用

 代码演示:

#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 0;
std::mutex mtx;     //定义一个互斥锁

void func(int n) 
{
    for (int i = 0; i < 100000; ++i) 
    {
        //mtx.lock();   不在需要手动的加锁

        std::lock_guard<std::mutex> lg(mtx);    //使用 lock_guard 定义一个互斥锁
        shared_data++;

        std::cout << "thread" << n << "incremnet sharad_data" << sharad_data << std::endl;
       
        //mxt.ubnlock();    不需要手动解锁
    }
}

int main() 
{
    std::thread t1(func,1);
    std::thread t2(func,2);

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

    std::cout << " Final shared_data = " << shared_data << std::endl; 
   
    return 0;
}

 std::unique_lock除了能够自动加锁解锁以外,它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁条件变量超时

 std::unique_lock提供以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。

  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true

  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。

  • try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。

  • unlock():对互斥量进行解锁操作。

 除了上述成员函数外,std::unique_lock 还提供了以下几个构造函数:

  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象。

  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。

  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁

 代码演示:

​#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 0;
std::mutex mtx;     //定义一个互斥锁

void func(int n) 
{
    for (int i = 0; i < 100000; ++i) 
    {
        //mtx.lock();   不在需要手动的加锁

        std::unique_lock<std::mutex> lg(mtx);    //使用 unique_lock 定义一个互斥锁

        shared_data++;

        std::cout << "thread" << n << "incremnet sharad_data" << sharad_data << std::endl;
       
        //mxt.ubnlock();    不需要手动解锁
    }
}

int main() 
{
    std::thread t1(func,1);
    std::thread t2(func,2);

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

    std::cout << " Final shared_data = " << shared_data << std::endl; 
   
    return 0;
}

4.condition_variable

 在说这个条件变量之前,先说明一下与之相关联的的生产者与消费者模型,

生产者消费者模式可以理解为在生产者和消费者之间添加一个缓冲区,生产者只负责向缓冲区添加元素,而消费者只负责从缓冲区提取元素并使用。如果没有任务了消费者就进行等待,等到生产者通知。如下图所示

 

这么做可以对生产者与消费者进行解耦,这样一来消费者不直接调用生产者,使得生产者的不会因为生产者的具体处理而阻塞,充分利用资源。 

​​#include <iostream>
#include <thread>
#include <mutex>
#include <condition_vairable>
#include <queue >

std::queue<int> g_queue;    //创建一个队列
std::condition_vairable g_cv;    //创建一个条件变量
std::mutex mtx;     //定义一个互斥锁

//构建一个生产者 往队列里面加任务
void Producer()    
{
    
    for (int i = 0; i < 10; ++i) 
    {
       std::unique_lock<std::mutex> lock(mtx);    //使用 unique_lock 定义一个互斥锁

       // 生产者往队列里加任务,通知消费者来取任务
       g_cv.notify_one();
       g_queue.push(i);
        
    }
}

//构建一个消费者 在队列里面取任务
void Consumer()    
{
    
    while(1)
    {
        std::unique_lock<std::mutex> lock(mtx);    //使用 unique_lock 定义一个互斥锁

        //如果队列为空消费者就要等待,等待生产者下发任务,此时就用到条件变量
        g_cv.wait(lock,[] () {return !g_queue.empty();});    //如果为空 就阻塞 直到生产者加任务 通知了    

        int value = g_queue.front(); //取任务
        g_queue.pop();    //取完就pop
    }
}

int main() 
{
    std::thread t1(Produer);
    std::thread t2(Consumer);

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

使用 std::condition_variable可以实现线程的等待和通知机制,从而在多线程环境中实现同步操作。在生产者-消费者模型中,使用 std::condition_variable可以让消费者线程等待生产者线程生产数据后再进行消费,避免了数据丢失或者数据不一致的问题。

 5.线程池

一种线程的使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性。而线程池维护着多个线程,等待监督管理者分配可并行执行的任务。这样避免了在短时间内创建和销毁线程的代价。线程池不仅能够内核的充分利用,还能防止过分调度。

线程池由四部分组成;

  • 线程池管理器:创建一定数量的线程,启动线程,调配任务,管理着线程池。线程池目前只需要Start()启动方法,Stop()方法,AddTask()方法。Start():创建一定数量的线程,进入线程循环。Stop():停止线程循环,回收所有的线程。AddTask():添加任务。
  • 工作者线程:线程池中线程,在线程池中等待并执行任务。该文使用条件变量condition_variable实现等待和唤醒进制。
  • 任务接口:添加任务接口,以供工作线程的调度任务和执行。
  • 任务队列:用于存放没有处理的任务,提供一种缓存机制 

 下面用C++11构造一个线程池

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_vairable>
#include <string>
#include <vector>
#include <function>

class ThreadPool
{
    //构造函数来初始化线程池
    ThreadPool(int numThread):stop(false)
    {
        for(int i = o, i < numThread; i++)     //往线程里面加线程
        {
            threads.emplace([this]
            {
                while(1)
                {
                    std::unique_lock<std::mutex> lock(mtx);

                    condition.wait(lock, (this) {return !tasks.empty() || stop})

                    if(stop && tasks.empty())
                    {
                        return;
                    }
                    std::function<void()> task(std::move(tasks.front()))
                    tasks.pop();
                    lock.unlock();
                    task();
                 }
                 
             })   //使用这个函数的好处是可以节省资源
        }
    }

    //构造一个函数加任务,因为加的任务是不确定的 所以用模板
    template <class F,class...Arga>
    //&&是右值引用。在函数模板里 就是万能引用, & 是左值引用
    void enqueue(F && f, Args&&...args)   
    {
        //取任务
        std::function<void()>task = 
            std::bind(std::foreard<F>(f),std::forward<Args>((args)...);
        {
            std::unique_lock<std::mutex> mtx;

            //放任务
            tasks.empalce(std::move(ask))
        }
        //用条件变量进行通知
        condition.notify_one(); 
    }

    //析构函数
    ~ThreadPool()
    {
          std::unique_lock<std::mutex> mtx;
          stop = true;
          condition.notify_all();
          for(auto& t: threads)
          {
               t.join();
          }
    }
    
    private:
        std::vector<std::thread> threads;    //创建一个线程数组
        std::queue <std:: function<void()>> tasks;    // 创建一个任务队列
        std::mutex mtx;

        std::condition_vairable condition;

        bool stop;    //    线程池什么时候终止
}

int main()
{
    ThreadPool(5); //这个线程池维护5个线程
    for(int i = 0;i < 10;i++)
    {
        //加任务
        pool.enqueue([i]{std::cout << "task" << i << std::endl;
        std::this_tr=hread::sleep_for(std::chrono::seconds(1));    //暂停1秒,
        std::cout << "task :" << i << atd::endl;
        });
    }
}

 

 参考视频:​​​​​​​1.线程库的基本使用_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值