c++11 多线程 – 基本使用
前言:这篇文章仅针对没有使用过c++11线程库的童鞋来快速入门,也是自己的一个简单记录,内容比较基础。
- 1.线程的基本使用
- 2.互斥量
- 3.条件变量
- 4.原子变量
- 补充
1.线程的基本使用
代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <thread>
#include <iostream>
int k = 0;
void fun(void)
{
//线程休眠,chrono是c++11的时间相关库。
std::this_thread::sleep_for(std::chrono::seconds(3));
for(int i = 0; i < 10; ++i)
{
std::cout << "hello world" << std::endl;
k++;
}
}
int main(int argc, char *argv[])
{
//创建线程对象
std::thread t1(fun);
//输出线程id和cpu核数
std::cout << "ID:" << t1.get_id() << std::endl;
std::cout << "CPU:" << std::thread::hardware_concurrency() << std::endl;
//主函数阻塞等待线程结束
t1.join();
//主函数和线程函数分离执行,线程变为后台线程
//t1.detach();
std::cout << k << std::endl;
return EXIT_SUCCESS;
}
注意:
1.linux下用gcc或clang必须加-pthread连接到线程库,否则会出错。
2.主线程函数不能提前结束于新创建的线程函数,因为在c++11中,线程也是对象,主函数结束线程对象即销毁。
3.t.join()是主函数阻塞等待线程结束才能结束,主函数会继续执行,并阻塞在return处
t.detach()主函数和线程函数分离,各自执行各自的,线程变为后台线程。
4.可通过bind和lambda创建线程
可以将线程保存在容器中,以保证线程对象的声明周期。
但是注意线程没有拷贝构造函数,有移动构造函数。
图上可以看出拷贝构造函数为delete。
2.互斥量
分为4种
std::mutex 独占的互斥量,不能递归使用
std::timed_mutex 带超时的独占的互斥量,不能递归使用
std::recursive_mutex 递归互斥量,不带超时功能
std::recursive_timed_mutex 带超时的递归互斥量
代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <iostream>
#include <thread>
#include <mutex>
std::mutex g_lock;
int i = 0;
void func(void)
{
//使用RAII手法,在离开作用域时自动释放
std::lock_guard<std::mutex>locker(g_lock);
//正常的互斥锁上锁
//g_lock.lock();
i++;
std::cout << i << std::endl;
//互斥锁解锁
//g_lock.unlock();
}
int main(int argc, char *argv[])
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
t1.join();
t2.join();
t3.join();
return EXIT_SUCCESS;
}
注意:
1.多次获取互斥量可能会发生死锁,所以我们调用std::recursive_mutex递归锁,允许同一线程多次获得该锁,一般不要使用递归锁,原因:<1.用到递归锁会使得程序的逻辑变复杂,使用到递归锁的程序一般可以简化。<2.递归锁比非递归锁效率低。<3.递归锁的可重入次数是有限的,超过也会报错。
2.可以使用带超时时间的互斥锁,避免阻塞在等待互斥锁上。
3.unique_lock: 是一个通用的互斥量封装类。与lock_guard不同,它还支持延迟加锁、时间锁、递归锁、锁所有权的转移并且还支持使用条件变量。这也是一个不可复制的类,但它是可以移动的类。
3.条件变量
阻塞一个或多个线程,直到收到另外一个线程发来的通知或者超时,才会唤醒当前阻塞的进程
条件变量需要和互斥量配合使用
c++11提供了两种条件变量
1.std::condition_variable,配合std::unique_lock进行wait操作
2.std::condition_variable_any,和任意带有lock,unlock的mutex进行搭配使用,比较灵活但效率略低。
条件变量的wait还有一个重载的方法,可以设置一个条件,条件变量会先检查判断式是否满足条件。
原理:
当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。
代码:用c++11多线程实现同步队列
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <iostream>
#include <list>
#include <vector>
#include <memory>
#include <unistd.h>
template<typename T>
class SynQueue
{
public:
//构造函数
SynQueue(int MaxSize):
m_maxsize(MaxSize) { }
//将T类型对象放入队列
void Put(const T&x)
{
std::lock_guard<std::mutex>locker(m_mutex);
while(isFull())
{
//如果满了,等待
m_notFull.wait(m_mutex);
}
m_queue.push_back(x);
//通过条件变量唤醒一个线程,也可以所有线程
m_notEmpty.notify_one();
}
//将T类型对象从队列取出
void Take(T&x)
{
std::lock_guard<std::mutex> locker(m_mutex);
while(isEmpty())
{
std::cout << "no resource... please wait" << std::endl;
m_notEmpty.wait(m_mutex);
}
x = m_queue.front();
m_queue.pop_front();
m_notFull.notify_one();
}
//判断队列是否为空
bool Empty()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.empty();
}
//判断队列是否为满
bool Full()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.size() == m_maxsize;
}
//返回队列大小
size_t Size()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.size();
}
private:
//判断空或满,内部使用不需要加锁
bool isFull() const
{
return m_queue.size() == m_maxsize;
}
bool isEmpty() const
{
return m_queue.empty();
}
private:
//队列
std::list<T>m_queue;
//互斥锁
std::mutex m_mutex;
//不为空时的条件变量
std::condition_variable_any m_notEmpty;
//不为满时的条件变量
std::condition_variable_any m_notFull;
//队列最大长度
int m_maxsize;
};
void func(SynQueue<int> *sq)
{
int ret;
sq->Take(ret);
std::cout << ret << std::endl;
}
int main(int argc, char *argv[])
{
//创建线程队列,长度最大为20
SynQueue<int>syn(20);
//放置数据对象
for(int i = 0; i < 10; i++)
{
syn.Put(i);
}
std::cout << syn.Size() << std::endl;
//线程不能拷贝,用容器和智能指针来管理线程生存
std::vector<std::shared_ptr<std::thread>> tvec;
//多循环一次,资源不足,阻塞最后一个线程,在后面添加一个资源,看该线程是否会被唤醒执行。
for(int i = 0; i < 11; i++)
{
//创建线程并且将管理线程的智能指针保存到容器中
tvec.push_back(std::make_shared<std::thread>(func, &syn));
//变为后台线程
tvec[i]->detach();
}
sleep(10);
//添加一个资源
syn.Put(11);
sleep(10);
return EXIT_SUCCESS;
}
运行结果:
4.原子变量
原子变量,为原子操作,不需要加锁
std::atomic<T>
详情可参考,这里仅简单举例用法
cppreference atomic
代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
//创建int类型的原子变量
std::atomic<int>atc(0);
void func()
{
std::cout << atc << std::endl;
原子变量自增
atc++;
}
int main(int argc, char *argv[])
{
std::vector<std::thread>tvec;
for(int i = 0; i < 10; i++)
{
std::thread t(func);
//线程对象移动语义
tvec.push_back(std::move(t));
tvec[i].join();
}
return EXIT_SUCCESS;
}
补充:
1.线程创建函数可以为各种可调用类型,函数指针,仿函数,lambda表达式。但是注意,线程参数会以默认的方式被复制到内部存储空间中,即便函数中的相应参数期待引用,线程执行函数内部可以访问它们。
可能我们想传递引用在线程执行函数内部修改并返回结果,但复制会导致达不到预期的效果,解决方案是传递引用参数时用std::ref来包装或者传递指针。
2.线程初始化执行函数也可以传递一个成员函数
std::thread tid(&x::func, &my_func)
3.线程不支持拷贝,但支持所有权移动。
4.如果线程执行函数使用了临时变量可能会出现问题,线程调用了detach在后台运行时,临时变量可能已经销毁,那么线程会访问已经被销毁的变量。join能保证。