首页 > 编程语言 > 详细

C++ 11中的多线程

时间:2021-09-08 21:45:00      阅读:59      评论:0      收藏:0      [点我收藏+]

C++11多线程简单使用

开篇介绍

C++11中引入了多线程头文件<thread>,让我们能更方便的使用多线程进行编程

void TestThread(int index)
{
    std::cout << "Child Thread" << index << " id " << std::this_thread::get_id() << std::endl;
    std::cout << "Child Thread" << index << "Stop" << std::endl;
}

int main()
{
    std::thread newThread1(TestThread, 1);
    std::thread newThread2(TestThread, 2);
    std::cout << "Main Thread id " << std::this_thread::get_id() << std::endl;
    std::cout << "Main Thread Stop" << std::endl;
    if (newThread1.joinable())
        newThread1.join();
    if (newThread2.joinable())
        newThread2.join();
}

众所周知,线程具有异步性,也就是说这道程序的推进的方向是不确定的,而事实也正是如此

技术分享图片 技术分享图片

有两句话说得好,汇总一下就是:join()detach()总要调用一个,并且调用之前最好是要进行joinable()检查

Never call join() or detach() on std::thread object with no associated executing thread

Never forget to call either join or detach on a std::thread object with associated executing thread

如果对一个子线程执行两次join()操作,那么会抛出异常

void TestThread(int index)
{
    std::cout << "Child Thread" << index << " id " << std::this_thread::get_id() << std::endl;
    std::cout << "Child Thread" << index << "Stop" << std::endl;
}

int main()
{
    std::thread newThread(TestThread, 1);
    newThread.join();
    newThread.join();
}
技术分享图片

cppreference中提到,当joinable()的线程被赋值或析构的时候,会调用std::terminate(),而一般std::terminate()意味着std::abort(),也就是说如果对一个线程既不执行join()操作也不执行detach()操作,而它却被析构了,那么“it will cause the program to Terminate(终止)”

技术分享图片
void TestThread(int index)
{
    std::cout << "Child Thread" << index << " id " << std::this_thread::get_id() << std::endl;
    std::cout << "Child Thread" << index << "Stop" << std::endl;
}

int main()
{
    std::thread newThread(TestThread, 1);
    newThread = std::thread(TestThread, 2);

    std::cout << "Main Thread Sleep Begin" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "Main Thread Sleep End" << std::endl;
    if (newThread.joinable())
        newThread.join();
}

抛开Bug不谈,由于线程的异步性,谁也说不清是先输出Thread1还是输出Thread2

针对这个Bug,可以这么理解:线程1由对象newThread管理,当C++底层告知操作系统去创建一个新线程并给它分配一些任务后,却马上创建了一个线程2交给newThread,这样子产生了一个覆盖操作,导致线程1被析构,而它又是可结合的,所以程序被terminate

技术分享图片

正确的做法是

int main()
{
    std::thread newThread(TestThread, 1);
    if (newThread.joinable())
        newThread.join();
    newThread = std::thread(TestThread, 2);
    if (newThread.joinable())
        newThread.join();
}

线程的构造

通过前文中的示范,我们了解到可以通过传入一个函数指针来给一个线程分配它的“任务”

除了函数指针外,还可以通过lambda表达式,std::functionstd::bind,仿函数对象等来解决

struct Handle
{
    void operator()(int data) { std::cout << data << std::endl; }
};

int main()
{
    std::thread t(Handle(), 10);
    if (t.joinable())
        t.join();
}

join与detach

  • newThread.join()操作代表当前线程将阻塞,直到newThread完成它的任务
  • newThread.detach()操作代表newThread与当前线程分开(即当前线程结束销毁后并不会同时销毁newThread),但是程序结束后newThread仍会被终止(即使它还没运行完,也会被操作系统强行叫停,这也可能会导致资源没有正确的释放)
class TestClass
{
public:
    TestClass() { cout << "create" << endl; }
    TestClass(const TestClass& t) { cout << "copy" << endl; }
    void print(int num)
    {
        for (int i = 0; i < num; i++)
            cout << i << ends;
        cout << endl;
    }
};

void Func2()
{
    cout << "start" << endl;
    TestClass t;
    std::thread newThread(&TestClass::print, &t, 10);
    // 让当前线程等待newThread完成
    newThread.join();
    cout << "end" << endl;
}
int main()
{
    std::thread t(Func2);
    if (t.joinable())
        t.join();
    return 0;
}

程序的执行结果为

技术分享图片

void repeat1000(int index, int time)
{
    for (int i = 0; i < time; i++)
        cout << index;
}

void detach_test()
{
    thread newThread(repeat1000, 1, 100000);
    newThread.detach();
    newThread = thread(repeat1000, 2, 1000);
    newThread.join();
}

int main()
{
    {
        thread t(detach_test);
        t.join();
    }
    cout << endl << "start wait" << endl;
    this_thread::sleep_for(chrono::seconds(5));
    return 0;
}

我认为这是一个很好的解释join()detach()的例子,下面来分析一下

  • 首先创建了线程t,给它分配了detach_test()任务
  • 主线程阻塞 等待t完成它的任务
  • 同时操作系统完成了对t的分配,开始执行detach_test()
  • t进程再创建子线程newThread,并给他分配(repeat1000, 1, 100000)任务
  • 操作系统在分配newThread的“同时”,t线程将其和newThread分离
  • 此时(repeat1000, 1, 100000)仍然在运行,同时t线程给newThread赋了一个新线程,任务为(repeat1000, 2, 1000)
  • newThread调用join()操作,t进程阻塞,等待newThread(repeat1000, 2, 1000)工作完成
  • newThread(repeat1000, 2, 1000)完成,也意味着t进程的任务完成,此时主线程不再受到阻塞
  • t线程离开作用域,遭到销毁。但此时(repeat1000, 1, 100000)操作因为比较耗时所以仍然还在运行
  • 主程序输出"start wait"后开始休眠5秒钟,此时线程(repeat1000, 1, 100000)仍然在运行
  • 最后程序结束,各种线程被操作系统销毁(即可能(repeat1000, 1, 100000)执行到第八万次的时候就突然被终止了)
技术分享图片 技术分享图片

在一般情况下,为了避免detachjoin使用不当造成的程序错误,可以创建一个线程类,使用析构函数执行线程的分离或合并(join)

线程传参时应该避免的操作

应该避免传入栈上对象的指针

void newThreadCallback(int* p)
{
    std::cout << "Inside Thread: " << *p << std::endl;
    // 等一秒钟 让startNewThread()执行结束 使i的内存空间被回收
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // 抛出异常
    *p = 19;
}

void startNewThread()
{
    int i = 10;
    std::cout << "Inside Main Thread: " << i << std::endl;
    std::thread t(newThreadCallback, &i);
    t.detach();
    std::cout << "Inside Main Thread: " << i << std::endl;
}

int main()
{
    startNewThread();
    // 等两秒钟 让所有线程和方法都执行完毕再结束程序
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

堆上的数据同理

因为一般堆对象需要使用delete来销毁,所以也无法确定别的线程访问到指针的时候,它所指的内存是否有效

线程的引用传参

使用std::ref或者std::cref,使用方法几乎和std::bind中使一致的,所以不再赘述

多线程中的竞争

操作系统应该学过,竞争就是多个线程同时访问一块内存区域,导致不可预估的结果

class Wallet
{
    int money;
public:
    Wallet() : money(0) {}

    int getMoney() const { return money; }
    void addMoney(int increase)
    {
        for (int i = 0; i < increase; ++i)
            money++;
    }
};

int testMultiThreadWallet()
{
    Wallet walletObject;
    std::vector<std::thread> threads;
    // 创建五条线程异步访问Wallet "理应"得到的结果为5000
    threads.reserve(5);
    // reserve对应_back而resize对应[i]
    for (int i = 0; i < 5; ++i)
        threads.emplace_back(&Wallet::addMoney, &walletObject, 1000);

    // 等所有线程执行完再结束
    for (auto& thread : threads)
        thread.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;
}

某次程序执行的结果为,这种结果是不确定的,可能1000次实验,一次都不会出错,也可能出现多至十多次错误

技术分享图片

这一行短短的代码其实发生了三件事

money++;
  • money的值加载进寄存器中
  • 在寄存器中进行计算,即++操作
  • 将寄存器中的结果存回money所在的内存中

而当由于线程具有异步性,在不加锁的情况下我们无法控制多条线程对money的访问顺序,那么就可能出现以下这种情况

技术分享图片

  • 假设money的初始值是43
  • money的值被线程1取出放入寄存器1中
  • money的值被线程2取出放入寄存器2中
  • 寄存器1进行计算得到结果44
  • 寄存器2进行计算得到结果44
  • 寄存器1将结果放入money所在内存中,money为44
  • 寄存器2将结果放入money所在内存中,money为44

解决线程中的竞争

操作系统中的PV操作与之类似,关键就是加锁

std::mutex

使用互斥锁解锁上文中的钱包问题

#include<mutex>
class Wallet
{
    int money;
    mutex mutexLock;
public:
    Wallet() : money(0) {}

    int getMoney() const { return money; }
    void addMoney(int increase)
    {
        mutexLock.lock();
        for (int i = 0; i < increase; ++i)
            money++;
        mutexLock.unlock();
    }
};

但是如果一个线程在加锁后并没有解锁,那么所有其他线程将会一直等待,导致程序无法结束(当使用join的情况下)

相反的,如果没上锁就解锁

技术分享图片

因此可以在此基础上做一层简单的封装

class SmartMutex
{
private:
    mutex mutexLock;
public:
    void AutoLock(const function<void()>& func)
    {
        mutexLock.lock();
        func();
        mutexLock.unlock();
    }
};


class Wallet
{
    int money;
    SmartMutex smartMutex;
public:
    Wallet() : money(0) {}

    int getMoney() const { return money; }
    void addMoney(int increase)
    {
        smartMutex.AutoLock([this, &increase]()
        {
            for (int i = 0; i < increase; ++i)
                this->money++;
        });
    }
};

或者可以使用std::lock_guard

std::lock_guard

std::lock_guard有点像一个智能指针,在它的作用域结束后,会调用析构函数,完成对互斥锁的解锁操作

class Wallet
{
    int money;
    mutex mutexLock;
public:
    Wallet() : money(0) {}

    int getMoney() const { return money; }

    void addMoney(int increase)
    {
        // 在lockGuard的构造函数中会自动上锁
        lock_guard<mutex> lockGuard(mutexLock);
        for (int i = 0; i < increase; ++i)
            this->money++;
        // 离开作用域 lockGuard析构函数调用 自动解锁
    }
};

C++ 11中的多线程

原文:https://www.cnblogs.com/tuapu/p/15242237.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!