欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++学习笔记--多线程

程序员文章站 2022-07-05 22:47:52
线程与进程优缺点对比: 多线程开销小,但难于管理,且不能用于分布式系统; 多进程开销大,操作系统会进行一部分管理,因此用户管理就比较简单,可用于分布式; 通常多线程和多进程结...

线程与进程优缺点对比:
多线程开销小,但难于管理,且不能用于分布式系统
多进程开销大,操作系统会进行一部分管理,因此用户管理就比较简单,可用于分布式;
通常多线程和多进程结合使用。
代码实例:

1 最简单的多线程

#include <iostream>
#include <thread>

void function_1()
{
    std::cout <<"www.oxox.work"<<std::endl; false="" int="" pre="" return="" std::thread="">

2 主线程和子线程交叉运行

#include 
#include 

using namespace std;
void function_1()
{
    cout <<"www.oxox.work"< -100; --i)
        {
            cout << "from t1: " << i << endl;
        }
    }
};

int main()  //主线程
{
    //std::thread t1(function_1);   //使用函数创建并初始化一个线程,且线程开始运行
    Factor fct;
    std::thread t1(fct);    //使用函数对象创建并初始化一个线程

    try
    {
        for(int i = 0; i < 100; ++i)
        {
            cout << "from main: " << i << endl;
        }
    }
    catch(...)  //上面的for循环属于主线程,如果上面抛出异常,但是没有try catch,主线程终止,t1线程也终止了,这样是非线程安全的。添加try catch之后,即使主线程异常,t1线程也能正常执行结束
    {
        t1.join();  //主线程将等待t1线程结束后再运行
        throw;
    }
    return 0;
}

3 主线程和子线程之间实现内存共享

#include 
#include 
#include 

using namespace std;
void function_1()
{
    cout <<"www.oxox.work"<

4 线程移动与线程ID

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

using namespace std;
void function_1()
{
    cout <<"www.oxox.work"<<endl; cout="" factor="" from="" i="" id="" int="" love="" main:="" pre="" public:="" return="" s="" std::thread="" string="" t1:="" t2="std::move(t1);" void="">

5 线程安全

下面的代码是非线程安全的,主线程和t1线程将竞争资源cout,只要竞争到资源就随时可以将内容写入到输出流cout,使得输出看起来是下面这样的:

from t1: 0 from t1: -1 from t1: -2 from main: 0 from main: 1 from main: 2 from main: 3 from main: 4 from main: 5 from main: 6 from main: 7 from t1: -3 from t1: -4 from t1: -5

#include 
#include 
#include 

using namespace std;

void function_1()
{
    for(int i = 0; i > -100; --i)
    {
        cout 

可以加mutex锁,有线程正在使用cout,其他线程就不能使用,这样cout在当前程序中是线程安全的,能使得输出是有序的,是下面这样的:

from main: 0 from t1: 0 from main: 1 from t1: -1 from main: 2 from t1: -2 from main: 3 from t1: -3 from main: 4 from t1: -4 from main: 5 from t1: -5

#include 
#include 
#include 
#include 

using namespace std;

std::mutex mtx;
void shared_print(string s, int id)
{
    mtx.lock();
    cout  -100; --i)
    {
        shared_print("from t1: ",i);
    }
}

int main()  //主线程
{
    std::thread t1(function_1);
    for(int i = 0; i 

上面的代码中,当shared_print函数中cout那一行抛出异常时,mtx.unclock()不会被执行,mtx将被永远地锁住。这时可以使用std::lock_guard来保证mtx会被解锁。shared_print函数修改如下:

void shared_print(string s, int id)
{
   std::lock_guard locker(mtx);    //lock_guard对象创建时会自动对mtx加锁,离开作用域被析构时,mtx会被自动解锁,这样即使cout这行发生异常,mtx也能被解锁了 
   cout 

但是上面的代码仍然不是安全的,因为cout是个全局变量,并没有完全在mtx的保护下,其他线程仍然可以在不加锁的情况下使用cout。为了完整地保护资源,必须使资源和互斥对象进行绑定。代码如下:

#include 
#include 
#include 
#include 
#include 

using namespace std;

class LofFile
{
public:
    LofFile()
    {
        f.open("log.txt");
    }
    void shared_print(string s, int id)
    {
        std::lock_guard locker(m_mutex); 
        f  -100; --i)
    {
        log.shared_print("from t1: ",i);
    }
}

int main()  //主线程
{
       LofFile log; 
       std::thread t1(function_1, std::ref(log));
    for(int i = 0; i 

上面的代码将资源std::ofstream f和互斥对象std::mutex m_mutex定义在LogFile类中,类外的线程不可访问资源f,使用类对象的shared_print函数的线程也能保证资源f必定有一个互斥对象m_mutex来保护。简单地讲,就是资源和互斥对象必定成对地出现在同一个作用域中,因此资源一定会受互斥对象保护。注意,这里的代码使用了资源std::ofstream f,而不是cout,是因为cout是全局的资源。

6 避免死锁

死锁是指两个线程互相锁住,互相等待释放,却不能释放,下面的代码发生了死锁,程序的输出可能是这样的(程序被暂停在某处,并没有成功执行完毕):

from t1: 0 from main: 0 from t1: -1 from main: 1

#include 
#include 
#include 
#include 
#include 

using namespace std;

class LofFile
{
public:
    LofFile()
    {
        f.open("log.txt");
    }
    void shared_print(string s, int id)    //函数被t1线程调用
    {
        std::lock_guard locker(m_mutex); 
        std::lock_guard locker2(m_mutex2); 
        cout  locker2(m_mutex2); 
        std::lock_guard locker(m_mutex); 
        cout  -100; --i)
    {
        log.shared_print("from t1: ",i);
    }
}

int main()  //主线程
{
       LofFile log; 
       std::thread t1(function_1, std::ref(log));
    for(int i = 0; i 

上面的代码中,m_mutex和m_mutex2在两个线程中加锁的顺序是相反的,如果将语句的顺序改成一致就不会发生死锁。在C++标准库中提供了std::lock,是规范的处理死锁问题的方法,把上面的两个函数改成下面这样:

void shared_print(string s, int id)    //函数被t1线程调用
{
    std::lock(m_mutex, m_mutex2);   //std::lock可以指定锁的顺序,参数为lock1,lock2,...,lockn,它的参数个数是不固定的,有多少个锁就可以使用多少个参数
    std::lock_guard locker(m_mutex, std::adopt_lock);    //这里添加std::adopt_lock是告知locker,m_mutex已经被锁住,locker要做的只是获得m_mutex的所有权,然后在析构时将其解锁即可
    std::lock_guard locker2(m_mutex2, std::adopt_lock); 
    cout  locker2(m_mutex2, std::adopt_lock); 
    std::lock_guard locker(m_mutex, std::adopt_lock); 
    cout 

上面的代码中,即使locker和locker2的顺序是相反的,但是m_mutex和m_mutex2加锁的顺序是相同的,因为std::lock指定了加锁的顺序。 为了避免程序设计中出现死锁,可以遵循以下几条规则: (1)使用一个mutex即可满足要求的场合,绝不使用两个mutex; (2)如果某个作用域中已经使用了一个mutex,那么要小心该作用域中的函数调用,因为该函数调用中可能包括其他mutex; (3)无法避免地需要使用两个以上mutex时,尽量使用std::lock指定锁的顺序,但是在某些极端情况下std::lock无法使用,就要小心地保证加锁的语句顺序。

7 Unique Lock和call_once

#include 
#include 
#include 
#include 
#include 

using namespace std;

class LogFile
{
public:
    LogFile()
    {
        f.open("log.txt");
    }
    void shared_print(string s, int id)
    {
        //std::lock_guard locker(m_mutex);   //lock_guard对象在构造时自动m_mutex加锁,在析构时自动对m_mutex解锁,用户无法*控制何时加锁解锁
        //std::unique_lock locker(m_mutex);   //unique_lock可以起到与lock_guard一样的功能,默认情况下是构造自动加锁,析构自动解锁
        std::unique_lock locker(m_mutex, std::defer_lock);  //还可以使用defer_lock告知locker,不要在构造时自动加锁m_mutex
        locker.lock();   //然后用户自行给m_mutex加锁   
        f  locker2 = std::move(locker);    
    }
private:
    std::mutex m_mutex;
    std::ofstream f;
};

void function_1(LogFile& log)
{
    for(int i = 0; i > -100; --i)
    {
        log.shared_print("from t1: ",i);
    }
}

int main()  //主线程
{
       LogFile log; 
       std::thread t1(function_1, std::ref(log));
    for(int i = 0; i 

上面使用的所有示例代码中,LogFile类都是在构建函数中打开log.txt文件,如果我们想只在调用shared_print函数的时候才打开文件,可以做如下修改:

class LogFile
{
public:
    LogFile()
    {
        //f.open("log.txt");
    }
    void shared_print(string s, int id)
    {
        //if(!f.is_open())    //这段代码不是线程安全的,因为两个线程可能同时执行到f.open处,两次打开文件,将会运行报错
        //{
        //    f.open("log.txt");
        //}

        //if(!f.is_open())    //这段代码仍然不是线程安全的,假设a线程尚未执行完f.open,b线程正好检测到文件未打开,然后发现m_mutex_fopen被锁住,于是等待,接着a线程打开了文件,解锁m_mutex_fopen,此时b线程发现解锁了,立即执行f.open,这样就两次打开文件,将会运行报错
        //{
        //    std::unique_lock locker(m_mutex_fopen, std::defer_lock);
        //    f.open("log.txt");
        //}

        //{   //这段代码是线程安全的,但是这存在一个性能上的问题,即每次函数调用都要对m_mutex_fopen进行加锁和解锁,这会无意义地消耗计算机资源
        //    std::unique_lock locker(m_mutex_fopen, std::defer_lock);
        //    if(!f.is_open())  f.open("log.txt");
        //}
        std::call_once(m_flag, [&](){f.open("log.txt")})    //这行代码能确保后面的lambda函数只被一个线程调用一次,是C++标准库的推荐用法
        std::unique_lock locker(m_mutex, std::defer_lock);  
        f 

8 条件变量

条件变量适用于a线程需要等待b线程触发某种条件,a线程才能执行的场合

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;
std::deque q;
std::mutex mu;
std::condition_variable cond;

void function_1()
{
    int count = 10;
    while (count > 0)
    {
        std::unique_lock locker(mu);
        q.push_front(count);
        locker.unlock();
        cond.notify_one();    //激活条件变量cond
        cond.notify_all();    //notify_one只能激活一个正在等待cond被激活的线程
                                     //notify_all可以激活所有正在等待cond被激活的线程
        std::this_thread::sleep_for(chrono::seconds(1));
        count--;
    }
}

void function_2()
{
    int data = 0;
    while (data != 1)
    {
        std::unique_lock locker(mu);
        /*if (!q.empty())
        {
            data = q.back();
            q.pop_back();
            locker.unlock();
            cout 

9 Future, Promise和async()

std::future类可以从子线程获取返回值,然后在父线程中使用

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int factorial(int N)
{
    int res = 1;
    for (int i = N; i > 1; --i)
    {
        res *= i;
    }
    cout  fu = std::async(factorial, 4);
    std::future fu = std::async(std::launch::async | std::launch::deferred, factorial, 4);
    x = fu.get();    //get将会等待子线程结束,并取回子线程返回的结果,get只能被调用一次,调用两次程序会运行报错
    return 0;
}

std::promise可以从父线程获取值到子线程中使用

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int factorial(std::future& f)
{
    int N = f.get();
    int res = 1;
    for (int i = N; i > 1; --i)
    {
        res *= i;
    }
    cout  p;
    std::future f = p.get_future();
    std::future fu = std::async(std::launch::async, factorial, std::ref(f));
    p.set_value(4);    //这里必须进行set_value,否则在factorial函数的int N = f.get()这行会抛出std::future_error::broken_promise的异常
    x = fu.get();
    cout 

如果需要创建多个都调用factorial线程,每个线程都需要一个f参数,但是std::future不能被拷贝,此时可以使用std::shared_future,它可以被拷贝,可将int factorial(std::future& f)改为int factorial(std::shared_future f),同时main函数做如下修改:

int main()
{
    int x;
    std::promise p;
    std::future f = p.get_future();
    std::shared_future sf = f.share();
    std::future fu = std::async(std::launch::async, factorial, sf);
    std::future fu2 = std::async(std::launch::async, factorial, sf);
    std::future fu3 = std::async(std::launch::async, factorial, sf);   
    p.set_value(4);    
    return 0;
}

10 创建线程的不同方式

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

class A
{
public:
    void f(int x, char c){}
    int operator()(int N){return 0;}
};

void foo(int x){}

int main()
{
    A a;
    std::thread t1(a, 6);    //传递a的拷贝给子线程
    std::thread t2(std::ref(a), 6);    //传递a的引用给子线程
    std::thread t3(std::move(a), 6);    //从主线程移动a到子线程,a在主线程中不再有效
    std::thread t4(A(), 6);   //传递临时创建的a对象给子线程
    std::thread t5(foo, 6);   //传递自定义的函数给子线程
    std::thread t6([](int x){return x*x;}, 6);   //传递lambda函数给子线程
    std::thread t7(&A::f, a, 8, 'w');   //传递a的拷贝的成员函数f给子线程
    std::thread t8(&A::f, &a, 8, 'w');   //传递a的地址的成员函数f给子线程
    std::async(std::launch::async, a, 6);    //上面的八种方式同样适用于async
    return 0;
}

11 Packaged_task

#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int factorial(int N)
{
    int res = 1;
    for (int i = N; i > 1; --i)
    {
        res *= i;
    }
    cout  > task_q;
std::mutex mu;
std::condition_variable cond;
void thread_1()
{
    std::packged_task t;
    {
       std::unique_lock locker(mu);
       cond.wait(locker, []{return !task_q.empty();});
       t = std::move(task_q.front());
    }
    t();
}

int main()
{
    std::thread t1(thread_1);
    std::packaged_task t(std::bind(factorial, 6));   //packaged_task只能传递一个参数,如果还要传递参数6,可以使用bind函数
    std::future ret = t.get_future();    //获得与packaged_task共享状态相关联的future对象
    {
       std::unique_lock locker(mu);
       task_q.push_back(std::move(t));
    }    
    cond.notify_one();
    int value = ret.get();    //等待任务完成并获取结果
    t1.join()

    //这两句可以实现和packaged_task类似的功能,但是packaged_task特点是可以将一个可调用对象关联到一个future变量,然后异步获取可调用对象的返回结果
    //auto t = std::bind(factorial, 6);  
    //t();

    //std::future fu = std::async(factorial, 4);
    x = fu.get();

    std::packaged_task t(factorial);   //以一个可调用对象为参数,并且可以异步获取该调用对象的返回结果
    std::future ret = t.get_future();    //获得与packaged_task共享状态相关联的future对象
    int value = ret.get();    //等待任务完成并获取结果  
}

12 时间约束

可以让某个类对象休眠一段时间,或者到达指定的时间点才停止休眠

#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int factorial(int N)
{
    int res = 1;
    for (int i = N; i > 1; --i)
    {
        res *= i;
    }
    cout  locker(mu);
    locker.try_lock_for(chrono::milliseconds(3));
    locker.try_lock_until(tp);

    std::condition_variable cond;
    cond.wait_for(locker, chrono::milliseconds(3));
    cond.wait_until(locker, tp);

    std::promise p;
    std::future f = p.get_future();
    f.wait_for(chrono::milliseconds(3));
    f.wait_until(tp);
}