[原]使用C++11进行多线程开发

吕子健 18/09/30 16:27:30


创建线程

C++11 增加了线程以及线程相关的类, 而之前并没有对并发编程提供语言级别的支持

std::thread 类

使用 std::thread 类来创建线程, 我们需要提供的只是线程函数, 或者线程对象, 同时提供必要的参数
std::thread 表示单个执行的线程, 使用thread 类首先会构造一个线程对象, 然后开始执行线程函数,

#include <iostream>
#include <thread> //需要包含的头

using namespace std;

void func(int a, double b)  //有参数, 参数数量不限
{
    cout << a << ' ' << b << endl;
}

void func2() //无参数
{
    cout << "hello!\n";
}

int main() 
{
    thread t1(func, 1, 2); //提供参数
    thread t2(func2);

    //可以使用 lambda表达式
    thread t3([](int a, double b){cout << a << ' ' << b << endl;}, 3, 4);

    cout << t1.get_id()  << "****" << endl;  //可以使用 get_id() 获取线程 id
    t1.join();
    t2.join();
    t3.join();

    return 0;
}

使用join()

我们知道, 上例中如果主线程 (main) 先退出, 那些还未完成任务的线程将得不到执行机会, 因为 main 会在执行完调用 exit(), 然后整个进程就结束了, 那它的"子线程" (我们知道线程是平级的, 这里只是, 形象一点) 自然也就 over 了
所以就像上例中, 线程对象调用 join() 函数, join() 会阻塞当前线程, 直到线程函数执行结束, 如果线程有返回值, 会被忽略

使用 detach()

对比于 join(), 我们肯定有不想阻塞当前线程的时候, 这时可以调用 detach(), 这个函数会分离线程对象和线程函数, 让线程作为后台线程去执行, 当前线程也不会被阻塞了, 但是分离之后, 也不能再和线程发生联系了, 例如不能再调用 get_id() 来获取线程 id 了, 或者调用 join() 都是不行的, 同时也无法控制线程何时结束

#include <thread>
void func() 
{
	//...
}

int main() 
{
	std::thread t(func);
	t.detach();
	// 可以做其他事了, 并不会被阻塞
	return 0;
}

程序终止后, 不会等待在后台执行的其余分离线程, 而是将他们挂起, 并且本地对象被破坏

警惕作用域

std::thread 出了作用域之后就会被析构, 这时如果线程函数还没有执行完就会发生错误, 因此, 要注意保证线程函数的生命周期在线程变量 std::thread 之内

线程不能复制

std::thread 不能复制, 但是可以移动
也就是说, 不能对线程进行复制构造, 复制赋值, 但是可以移动构造, 移动赋值

#include <iostream>
#include <thread>

void func() 
{
    std::cout << "here is func" << std::endl;
}

int main() 
{
    std::thread t1(func);
    std::thread t2;
    t2 = t1; //error

    t2 = std::move(t1); //right, 将 t1 的线程控制权转移给 t2
    
    std::cout << t1.get_id() << std::endl;  //error,t1已经失去了线程控制权

    t1 = std::thread(func); //right, 直接构造, 创建的是临时对象,所以隐式调用move

    t1 = std::move(t2); //error, 不能通过赋值一个新值来放弃一个已有线程, 这样会直接导致程序崩溃
}

std::thread= 重载了, 调用 operator= 是移动构造函数, 复制被禁用了,

给线程传参

传递指针

#include <iostream>
#include <thread>

void func(int* a)
{
    *a += 10;
}

int main()
{
    int x = 10;
    std::thread t1(func, &x);
    t1.join();
    std::cout << x << std::endl;

    return 0;
}

上例代码, 可以如愿改变 x 的值, 但是看下面的代码, 当我们传递引用时, 却好像并不能如我们所想

传递引用

#include <iostream>
#include <thread>

void func(int& a)
{
    a += 10;
}

int main()
{
    int x = 10;
    std::thread t1(func, x);
    t1.join();
    std::cout << x << std::endl;

    return 0;
}

我们想让 func 函数对 x 进行更新, 但是实际上给线程传参会以拷贝的形式复制到线程空间, 所以即使是引用, 引用的实际上是新线程堆栈中的临时值, 为了解决这个问题, 我们需要使用引用包装器 std::ref()
改成:
std::thread t1(func, std::ref(x));

实际上, 我的编译器对于这段代码直接给出了编译错误…

以类成员函数为线程函数

因为类内成员涉及 this 指针, 就和所需的线程函数参数不同了

#include <iostream>
#include <thread>

using namespace std;

class A 
{
public:
    void func1()  
    {
        cout << "here is class A`s func 1" << endl;
    }
    static void func2() 
    {
        cout << "here is class A`s func 2" << endl;
    }

    void func3() 
    {
        thread t1(&A::func1, this);	//非静态成员函数
        thread t2(A::func2);		//静态成员函数
        
        t1.join();
        t2.join();
    }
};


int main()
{
    A a;
    thread t1(&A::func1, &a);	//非静态成员函数
    thread t2(A::func2);		//静态成员函数
    t1.join();
    t2.join();
    a.func3();

}

注意的是, 如果我们选择将成员函数变成静态的使用, 那我们就不能使用非静态的成员变量了, 解决办法也很简单, 给静态成员函数传递该对象的 this 指针就好了

可以参考一下我犯过的这个错误

以容器存放线程对象

我们可以用容器保存创建的多个线程对象, 而当我们像其中插入元素时, 建议使用 emplace_bcak() 而不是 push_back()

我们知道 push_back() 会创建一个临时对象然后拷贝, 当然自从有了移动语意这里出发都是移动, 如下例:

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

using namespace std;

class A 
{
public:
    void func1() 
    {
        cout << "here is class A`s func 1" << endl;
    }

    void func3() 
    {
        tmpThread.push_back(thread(&A::func1, this));		//(1)
        tmpThread.emplace_back(&A::func1, this);	//(2)
    }
    vector<thread> tmpThread;
};

比较上例中 (1) (2)两处, 明显发现emplace_back()push_back() 调用形式更加简洁, 他会自动推导直接根据你给出的参数初始化临时对象
emplace_back 不会触发复制构造和移动构造, 他会直接原地构造一个元素
所以使用 emplace_back 更加简洁效率也更加高

互斥量

std::mutex

mutex 类是保护共享数据, 避免多线程同时访问的同步原语
mutex 也不能复制, 他的operator=被禁用

  • lock
    上锁, 若失败则阻塞
  • try_lock
    尝试上锁, 失败则返回
  • unlock
    解锁

使用时注意死锁

std::lock_guard

通常不直接使用 mutex, lock_guard 更加安全, 更加方便
他简化了 lock/unlock 的写法, lock_guard 在构造时自动锁定互斥量, 而在退出作用域时会析构自动解锁, 保证了上锁解锁的正确操作, 正是典型的 RAII 机制

#include <thread>
#include <mutex>

std::mutex myLock;
void func() 
{
    {
        std::lock_guard<std::mutex> locker(myLock);   //出作用域自动解锁
        //do some things...
    }
    myLock.lock();
    myLock.unlock();
}


int main()
{
    std::thread t(func);
    t.join();
}

还有一些其他互斥量, 如std::recursive::mutex 是递归型互斥量, 可以让同一线程重复申请等等, 就不一一介绍了

条件变量

条件变量是C++11 提供的一种用于等待的同步机制, 可以阻塞一到多个线程, 直到收到另一个线程发出的通知或者超时, 才会唤醒当前阻塞的线程, 条件变量需要和互斥量配合起来使用

  • std::condition_variable
    该条件变量必须配合 std::unique_lock 使用
  • std::condition_variable_any
    可以和任何带 lock, unlock 的 mutex 配合使用. 他更加通用, 更加灵活, 但是效率比前者差一些, 使用时会有一些额外的开销

这两者具有相同的成员函数

通知

  • notify_one
    唤醒一个阻塞于该条件变量的线程

    如果有多个等待的线程, 并没有会优先唤醒谁的说法
    即, 没有唤醒顺序, 是随机的

  • notify_all
    唤醒所有阻塞于该条件变量的线程
    等待
  • wait
    让当前线程阻塞直至条件变量被通知唤醒
  • wait_for
    导致当前线程阻塞直至通知条件变量、超过指定时间长度
  • wait_until
    导致当前线程阻塞直至通知条件变量、抵达指定时间点

因为虚假唤醒的存在和为了避免丢失信号量 (在调用wait的时候, 在其之前发出的唤醒都不会对wait生效, 而系统不会保存这些条件变量, 调用完就丢掉了), 我们必须使用循环判断条件变量,所以我们使用条件变量必须结合 mutex 并且将判断条件放入 while 循环, 而不是使用 if

call_once

call_once可以保证在多线程环境中某一个函数仅仅被调用一次, 使用 call_once 需要同时使用其帮助结构体 once_flag

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

using namespace std;

once_flag onlyOnce;
mutex myMutex;

void func() //线程函数
{
    myMutex.lock();
    cout << "here is func" << endl;
    myMutex.unlock();

    call_once(onlyOnce, []{		//仅仅调用一次
        cout << "hello world!" << endl;
    });
}

int main() 
{
    thread t1(func);
    thread t2(func);
    thread t3(func);

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

    return 0;
}

这篇博客算是拖了好几个月才写的了, 写一半还没了, 以后写博客记得好好保存…

作者:weixin_36888577 发表于 2018/09/30 16:27:30 原文链接 https://blog.csdn.net/weixin_36888577/article/details/82891531
阅读:23