C++ 并发操作(多线程)


标准线程库

C++11 新标准中引入了几个头文件来支持多线程编程:

< thread > :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在 中声明.

< atomic > :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。

< mutex > :包含了与互斥量相关的类以及其他类型和函数

< future > :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。

< condition_variable > :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。

std::thread

在C++11中可以简单地使用std:thread类来创建线程,构造thread对象时传入一个可调用对象作为参数(如果带参,则将参数也同时传入),可调用对象包括函数、函数指针、lambda表达式、bind创建的对象或重载了函数调用运算符的对象,例如:

/*无参*/
void thread01(){
    /*....*/
}
/*含参*/
void thread02(int i,int j){
    /*...*/
}

...

int main(){
    /*...*/
    thread first(thread01);/*构造thread对象完成后新的线程立马被创建。同时可调用对象被调用*/
    thread second(thread02,10,10);/*传入参数,可调用对象被调用*/
}

若采用std:thread的默认构造函数来构造对象,则该thread不关联任何线程,可以利用joinable()来查看一个thread对象是否关联某个线程

int main(){
   
    /*...*/
    thread first(thread01);/*构造thread对象完成后新的线程立马被创建。同时该可调用对象立马被调用*/
    thread second(thread02,10,10);/*传入参数*/
    thread third;/*没有绑定*/
    cout<<first.joinable()<< " "<<second.joinable()<<" "<<third.joinable()<<endl;
}

运行结果:1 1 0

join 汇聚

thread类调用join()函数后,原始线程会等待新线程执行完毕之后再去销毁线程对象。这样,如果新线程使用了原始线程中的某个共享对象不会发生异常,在新线程结束后才会析构共享对象;若新线程和主线程分离,主线程析构了某个对象或变量后,新线程再去调用的话则会产生异常。

thread::join()还会在进程结束后清理子线程相关的内存空间,此后该thread对象不再与该子线程相关,即thread::joinable() = false ,故join对一个线程而言只能调用一次

detach 分离

thread::detach将线程从thread对象分离出来,运行线程独立执行,此时即使主线程结束了子线程还有可能在后台运行。通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。

由于detach不像join一样会等待子线程与主线程汇聚,故要注意以下几点:

  • 如果传递int这种简单类型,推荐使用值传递,不要用引用
  • 如果传递类对象,避免使用隐式类型转换,全部都在创建线程这一行就创建出临时对象,然后在函数参数里,用引用来接,否则还会创建出一个对象。

std::mutex

考虑到多个线程对同一个对象进行操作,若全为只读操作,则数据是安全的,若有读有写,则很容易发生错误,因此要使用互斥量(mutex)来保护数据。

互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么线程将阻塞在lock()这里不断尝试去锁定,直到lock成功。

lock()&unlock()

实例分析:
未加锁状态

#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
int num = 0;
mutex mtx;
void thread01()
{
   
    for (int i = 0; i < 1000000; i++)
    {
      //  mtx.lock();
        ++num;
       // mtx.unlock();
    }
   
}
void thread02()
{
    
    for (int i = 0; i < 1000000; i++)
    {
      //  mtx.lock();
        ++num;
      //  mtx.unlock();
    }
    
}

int main()
{
    thread task01(thread01);
    thread task02(thread02);
    task01.join();
    task02.join();
    cout << "The num is  " << num << endl;
    system("pause");
    return 0;
}

多次运行程序结果:


可以看到每次运行的结果都与预期的结果不一样,数据未加锁则会引起数据冲突。下面是加锁状态:

void thread01()
{
   
    for (int i = 0; i < 1000000; i++)
    {
        mtx.lock();//加锁
        ++num;
        mtx.unlock();//解锁
    }
   
}
void thread02()
{
    
    for (int i = 0; i < 1000000; i++)
    {
        mtx.lock();
        ++num;
        mtx.unlock();
    }
    
}
//其他与上面的例子相同

程序运行结果:

这次程序运行的结果是正确的,可以看出数据加锁的重要性。不过由于加锁会导致阻塞,因此保护数据多了会影响程序运行的效率。

try_lock()

try_lock()会试图去lock mutex对象,但是不会造成阻塞,通常有以下几种情况:

  1. 如果mutex没有被任何线程lock,则调用线程会lock
  2. 如果mutex被其他线程lock,则该函数会失败,返回false,但是不会造成阻塞。
  3. 如果mutex被与调用该函数的线程相同的线程lock,则会产生一个死锁,因为mutex不支持递归,一般情况下会崩溃。

std::lock_guard

std::lock_guard用于管理mutex,很显然对于mutex的lock()和unlock()要成对调用,如果lock()之后忘记unlock()会出现死锁或其他很严重的错误,而有时程序会有很多出口,例如break continue return ,有时还会有异常抛出,在这些出口前都加上unlock()很麻烦。

std::lock_guard是mutex的封装器,创建lock_guard对象时,它会试图接收给定mutex的所有权。当线程离开创建lock_guard对象所在的作用域时,lock_guard对象被自动析构并释放mutex,我们可以将上述thread01()改为:

void thread01()
{
   
    for (int i = 0; i < 1000000; i++)
    {
        std::lock_guard<std::mutex> guard(mtx);//利用lock_guard管理mutex
        ++num;
        
    }
   
}

std::unique_lock

std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock要比std::lock_guard更灵活,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。此处不展开说明。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM