C++ 多线程系统编程入门

引言

在 C++11 发布之前,Linux程序员都用过Pthread来实现多线程,但有了C++ 11 的std::thread以后,便可以在语言层面编写多线程程序,直接的好处就是多线程程序的可移植性得到了很大的提高。

另外,在开发C++程序时,一般在吞吐量、并发、实时性上有较高的要求。在语言层面上实现的多线程,也可以实现从并发、异步、缓存 方面的性能的提高。

与C++ 11多线程相关的头文件

  • <thread>: 主要声明了std::thread类,另外std::this_thread命名空间也在该头文件中
  • <mutex>: 主要声明了与互斥量(mutex)相关的类,包括std::mutex系列类,std::lock_guard,std::unique_lock,以及其它的类型和函数

线程的创建

在每个c++应用程序中,都有一个默认的主线程,即main函数,在c++11中,我们可以通过创建std::thread类的对象来创建其他线程,每个std :: thread对象都可以与一个线程相关联,只需包含头文件< thread>。可以使用std :: thread对象附加一个回调,当这个新线程启动时,它将被执行。 这些回调可以为 函数指针、函数对象、Lambda函数。 

线程对象可通过std::thread thObj(< CALLBACK>)来创建,新线程将在创建新对象后立即开始,并且将与已启动的线程并行执行传递的回调。此外,任何线程可以通过在该线程的对象上调用join()函数来等待所创建的新线程退出。 

1.使用函数指针创建新线程

#include <iostream>
#include <thread>

void thread_function() {
    for (int i = 0; i < 5; i++)
        std::cout << "thread function excuting" << std::endl;
}

int main() {
    std::thread threadObj(thread_function);    //创建threadObj线程对象并绑定thread_function函数
    for (int i = 0; i < 5; i++)
        std::cout << "Display from MainThread" << std::endl;
    threadObj.join();                          //等待threadObj线程结束
    std::cout << "Exit of Main function" << std::endl;
    return 0;
}

2.使用函数对象创建线程

#include <iostream>
#include <thread>

class DisplayThread {
public:
    void operator ()() {
        for (int i = 0; i < 100; i++)
            std::cout << "Display Thread Excecuting" << std::endl;
    }
};

int main() {
    std::thread threadObj((DisplayThread()));        //创建threadObj线程对象并绑定DisplayThread函数对象
    for (int i = 0; i < 100; i++)
        std::cout << "Display From Main Thread " << std::endl;
    std::cout << "Waiting For Thread to complete" << std::endl;
    threadObj.join();
    std::cout << "Exiting from Main Thread" << std::endl;

    return 0;
}

每个std::thread对象都有一个相关联的id,std::thread::get_id() —-成员函数中给出对应线程对象的id; 
std::this_thread::get_id()—-给出当前线程的id,如果std::thread对象没有关联的线程,get_id()将返回默认构造的std::thread::id对象:“not any thread”,std::thread::id也可以表示id。

线程的 join 和 detach

启动了线程,你需要明确是要等待线程结束(加入式),还是让其自主运行(分离式),一个是通过调用std::thread对象上调用join()函数等待这个线程执行完毕:

std::thread threadObj(funcPtr); 
threadObj.join();

例如,主线程启动10个线程,启动完毕后,main函数等待他们执行完毕,join完所有线程后,main函数继续执行:

#include <iostream>
#include <thread>
#include <algorithm>

class WorkerThread
{
public:
    void operator()(){
        std::cout<<"Worker Thread "<<std::this_thread::get_id()<<"is Excecuting"<<std::endl;
    }
};

int main(){
    std::vector<std::thread> threadList;
    for(int i = 0; i < 10; i++){
        threadList.push_back(std::thread(WorkerThread()));
    }
    // 等待所有 worker thread 线程运行完毕 i.e.
    // 为每个 thread 对象调用 join() function
    std::cout<<"Wait for all the worker thread to finish"<<std::endl;
    std::for_each(threadList.begin(), threadList.end(), std::mem_fn(&std::thread::join));
    std::cout<<"Exiting from Main Thread"<<std::endl;

    return 0;
} 

另一个是detach可以将线程与线程对象分离,让线程作为后台线程执行,当前线程也不会阻塞了。但是detach之后就无法再和线程发生联系了,如果线程执行函数使用了临时变量可能会出现问题,线程调用了detach在后台运行,临时变量可能已经销毁,那么线程会访问已经被销毁的变量:

std::thread threadObj(funcPtr)
threadObj.detach();

线程的参数传递

要将参数传递给线程的可关联对象或函数,只需将参数传递给std::thread构造函数,默认情况下,所有的参数都将复制到新线程的内部存储中。 

给线程传递参数:

#include <iostream>
#include <string>
#include <thread>

void threadCallback(int x, std::string str) {
  std::cout << "Passed Number = " << x << std::endl;
  std::cout << "Passed String = " << str << std::endl;
}
int main() {
  int x = 10;
  std::string str = "Sample String";
  std::thread threadObj(threadCallback, x, str);
  threadObj.join();
  return 0;
}

给线程传递引用:

#include <iostream>
#include <thread>

void threadCallback(const int & x) {
  int& y = const_cast<int&>(x);
  y++;
  std::cout << "Inside Thread x = " << x << std::endl;
}

int main() {
  int x = 9;
  std::cout << "In Main Thread : Before Thread Start x = " << x << std::endl;
  std::thread threadObj(threadCallback, x);
  threadObj.join();
  std::cout << "In Main Thread : After Thread Joins x = " << x << std::endl;
  return 0;
} 

输出结果为: 


  即使threadCallback接受参数作为引用,但是并没有改变main中x的值,在线程外引用是不可见的。这是因为线程函数threadCallback中的x是引用复制在新线程的堆栈中的临时值,使用 std::ref 可进行修改 std::thread threadObj(threadCallback, std::ref(x))。

指定一个类的成员函数的指针作为线程函数,将指针传递给成员函数作为回调函数,并将指针指向对象作为第二个参数:

#include <iostream>
#include <thread>

class DummyClass {
 public:
  DummyClass() { }
  DummyClass(const DummyClass& obj) { }
  void sampleMemberfunction(int x) {
    std::cout << "Inside sampleMemberfunction " << x << std::endl;
  }
};

int main() {
  DummyClass dummyObj;
  int x = 10;
  std::thread threadObj(&DummyClass::sampleMemberfunction, &dummyObj, x);
    // 此处类的成员函数的指针作为线程函数,必须使用 "&" 来创建指向成员的指针
  threadObj.join();

  return 0;
}

 

使用 Mutex 保护多线程的共享数据

 为了处理多线程环境中的竞争条件,我们需要mutex互斥锁,在修改或读取共享数据前,需要对数据加锁,修改完成后,对数据进行解锁。在c++11的线程库中,mutex在< mutex >头文件中,表示互斥体的类是std::mutex。

在下面的例子中,Wallet类提供了在Wallet中增加money的方法,并且在不同的线程中使用相同的Wallet对象,所以我们需要对Wallet的addMoney()方法加锁。在增加Wallet中的money前加锁,并且在离开该函数前解锁:Wallet类内部维护money,并提供函数addMoney(),这个成员函数首先获取一个锁,然后给wallet对象的money增加指定的数额,最后释放锁。

 

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

class Wallet {
    int mMoney;
    std::mutex mutex;
public:
    Wallet() : mMoney(0) { }
    int getMoney() { return mMoney;}
    void addMoney(int money) {
        mutex.lock();
        for (int i = 0; i < money; i++) {
            mMoney++;
        }
        mutex.unlock();
    }
};

int testMultithreadWallet() {
    Wallet walletObject;
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
        // 使用类方法作为创建进程的参数时,必须将方法名和对象加上引用
    }

    for (int i = 0; i < threads.size(); i++) {
        threads.at(i).join();
    }

    return walletObject.getMoney();
}

int main() {
    int val = 0;
    for (int k = 0; k < 1000; k++) {
        if ((val = testMultithreadWallet()) != 5000) {
            std::cout << "Error at count= " << k << " money in wallet" << val << std::endl;
        }
    }

    return 0;
}

使用std::lock_guard:这种情况保证了钱包里的钱不会出现少于5000的情况,因为addMoney()中的互斥锁确保了只有在一个线程修改完成money后,另一个线程才能对其进行修改,但是,如果我们忘记在函数结束后对锁进行释放会怎么样?这种情况下,一个线程将退出而不释放锁,其他线程将保持等待,为了避免这种情况,我们应当使用std::lock_guard,这是一个template class,它为mutex实现RALL,它将mutex包裹在其对象内,并将附加的mutex锁定在其构造函数中,当其析构函数被调用时,它将释放互斥体。

class Wallet {
  int mMoney;
  std::mutex mutex;
 public:
  Wallet() : mMoney(0) { }
  int getMoney() { return mMoney;}
  void addMoney(int money) {
    std::lock_guard<std::mutex> lockGuard(mutex);

    for (int i = 0; i < mMoney; ++i) {
      //如果在此处发生异常,lockGuadr的析构函数将会因为堆栈展开而被调用
      mMoney++;
      //一旦函数退出,那么lockGuard对象的析构函数将被调用,在析构函数中mutex会被释放
    }
  }
}; 

只能用 mutex.lock() 保护以 join() 方式处理的多线程的共享数据,对 detach() 方式处理的多线程,因为detach之后实现了线程分离,后台运行,无法再和其他线程发生联系了。

条件变量

  • 条件变量阻塞一个或多个线程,直到收到另外一个线程发来的通知或者超时,才会唤醒当前阻塞的进程,条件变量需要和互斥量配合使用
  • C++11提供了两种条件变量
  1. std::condition_variable,配合std::unique_lock进行wait操作
  2. std::condition_variable_any,和任意带有lock,unlock的mutex进行搭配使用,比较灵活但效率略低。
  • 条件变量的使用过程如下:
  1. 拥有条件变量的线程获取互斥锁
  2. 循环检查某个条件,如果条件不满足,则阻塞直到条件满足,如果条件满足,则向下执行
  3. 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程

条件变量是一种用于在2个线程之间进行通信的事件,一个线程可以等待它得到信号,其他的线程可以给它发信号。在c++11中,条件变量需要头文件< condition_variable >,同时,条件变量还需要一个mutex锁。 
  条件变量是如何运行的: 
  ·线程1调用等待条件变量,内部获取mutex互斥锁并检查是否满足条件; 
  ·如果没有,则释放锁,并等待条件变量得到发出的信号(线程被阻塞),条件变量的wait()函数以原子方式提供这两个操作; 
  ·另一个线程,如线程2,当满足条件时,向条件变量发信号; 
  ·一旦线程1正等待其恢复的条件变量发出信号,线程1便获取互斥锁,并检查与条件变量相关关联的条件是否满足,或者是否是一个上级调用,如果多个线程正在等待,那么notify_one将只解锁一个线程; 
  ·如果是一个上级调用,那么它再次调用wait()函数。 
  条件变量的主要成员函数: 
Wait() 
它使得当前线程阻塞,直到条件变量得到信号或发生虚假唤醒; 
它原子性地释放附加的mutex,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中,当某线程在同样的条件变量上调用notify_one() 或者 notify_all(),线程将被解除阻塞; 
这种行为也可能是虚假的,因此,解除阻塞后,需要再次检查条件; 
一个回调函数会传给该函数,调用它来检查其是否是虚假调用,还是确实满足了真实条件; 
当线程解除阻塞后,wait()函数获取mutex锁,并检查条件是否满足,如果条件不满足,则再次原子性地释放附加的mutex,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中。 
notify_one() 
如果所有线程都在等待相同的条件变量对象,那么notify_one会取消阻塞其中一个等待线程。 
notify_all() 
如果所有线程都在等待相同的条件变量对象,那么notify_all会取消阻塞所有的等待线程。

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;

class Application {
    std::mutex m_mutex;
    std::condition_variable m_condVar;
    bool m_bDataLoaded;
public:
    Application() {
        m_bDataLoaded = false;
    }

    void loadData() {
        //使该线程sleep 1秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;

        //锁定数据
        std::lock_guard<std::mutex> guard(m_mutex);

        //flag设为true,表明数据已加载
        m_bDataLoaded = true;

        //通知条件变量
        m_condVar.notify_one();
    }

    bool isDataLoaded() {
        return m_bDataLoaded;
    }

    void mainTask() {
        std::cout << "Do some handshaking" << std::endl;

        //获取锁
        std::unique_lock<std::mutex> mlock(m_mutex);

        //开始等待条件变量得到信号
        //wait()将在内部释放锁,并使线程阻塞
        //一旦条件变量发出信号,则恢复线程并再次获取锁
        //然后检测条件是否满足,如果条件满足,则继续,否则再次进入wait
        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};

int main() {
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();

    return 0;
}


--------------------- 
作者:krais24 
来源:CSDN 
原文:https://blog.csdn.net/krais_wk/article/details/81095899 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值