C++ thread库

C++11中提供了thread线程库,它本质上和pthread库差不多,只不过被封装了,同时它还是可以跨平台的

thread

构造函数

thread() noexcept;
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

注意

  1. 无参构造就只是开一个线程,但是不会工作,不会执行
  2. thread的第一个元素是可调用对象
  • lambda表达式
  • 函数指针
  • 类对象的仿函数
  1. 后面的是可变参数列表,可以传入任意的参数
  2. 第一个参数是一个模板参数,所以它是万能引用,既可以传左值也可以传右值

函数指针

void print(int x,int y)//线程函数
{
	cout<<x<<y<<endl;
}


void threads()
{

    //线程库
    thread t1;//这里可以创建一个无参的,即线程不执行
    thread t2(print, 10,2);//第一个参数因为是模板,所以它是一个万能引用,func它不一定是一个函数,可调用对象就可以了,即可以传左值也可以传右值
    //第二个参数是可变的模板参数,可以传0-n个参数
    //第一个可以
    t2.join();//这和c的线程库也是一样的
    
    //不像c语言要使用一个结构体传进去,
    //原来在c语言我们是使用线程id来控制这些线程的
}

Lambda表达式

int main()
{
	thread t([](){cout<<"hello world"<<endl;});//使用lambda表达式进行传参
}

int main()
{
	int x = 0;
    mutex mtx;
    int N = 10000;
    atomic<int> costtime1(0);
    thread t1([&]
              {
        int begin1 = clock();
        mtx.lock();
        for (int i = 0; i < N; i++)
        {
            x++;
        }
        mtx.unlock();
        cout<<x<<endl;
        int end1 = clock();
        costtime1 += (end1 - begin1);
        cout<<costtime1<<endl;});//lambda表达式可以去处理一些小函数
    cout << x << ":" << costtime1 << endl;

    // costtime1就是调用花费的时间 }); //这里用一个可调用对象就可以了,我们这里用lambda表达式,&全部捕获 }); });
    //项目里面,我们还是用原子的,相对更好一点

    int costtime2 = 0;
    thread t2([&]
              {
                  int begin2 = clock();
        mtx.lock();

                  for (int i = 0; i < N; i++)
                  {
                      x++;
                  }
        mtx.unlock();
                  int end2 = clock();
                  costtime2 = end2 - begin2; }); // costtime1就是调用花费的时间 });
    t1.join();
    t2.join();
    cout << x << ":" << costtime1 << endl;
	return 0;
}

仿函数

class Func
{

void operator()
{
        cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
}
int main()
{
	Func fc;
	thread ss(fc);//使用类对象
	thread s((Func()));//使用仿函数进行传参,就要这样弄,
}

使用类对象成员函数

  • 静态成员函数可以 直接使用对应的静态函数
  • 使用普通成员函数,除了 有成员函数还要有类对象 ,类对象可以是一个临时对象
class test
{
public:
    test()
    {
    }
    ~test()
    {
    }
    static void do_work1()
    {
        cout << "do work 1" << endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
    void do_work2()
    {
        cout << "do work 2" << endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
    void do_work3(string& arg, int x, int y)
    {
        cout << "do work 3 x=" << x << "y=" << y << endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
};

void test_thread(int &data)
{
    cout << "thread data=" << data << endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
}
int main()
{
    int data=10;
    thread t1(&test_thread,ref(data));
    thread t2(&test::do_work1);//静态成员函数可以直接使用对应的静态函数
    test t;
    thread t3(&test::do_work2,t);//使用普通成员函数,除了有成员函数,还要有类对象
    thread t3(&test::do_work3,t,std::ref("test"),10,20);//使用普通成员函数,除了有成员函数,还要有类对象
  

    return 0;
}

拷贝构造

thread线程库不允许进行拷贝构造,所以直接把它给删除掉

thread (const thread&) = delete;

赋值重载

不允许赋值一个左值对象,类比拷贝构造
但是可以用一个右值对象来赋值

thread& operator= (thread&& rhs) noexcept;
copy [deleted] (2)	
thread& operator= (const thread&) = delete;
void test()
{
    int n;
    cin >> n;
    vector<thread> vthds;
    vthds.resize(n);//提前开好n个线程

    //现在有任务来了,我们要让这些线程都跑起来
    for (auto& e : vthds)
    {
        e = thread(print,100,2);//这里构造一个匿名对象赋值给它,这个地方又利用了一个移动赋值,把右边的这个临时
        //对象传过去给e去执行,出了作用域就销毁了
        //右边是一个右值
        //这里的线程不支持拷贝构造,把一个线程拷贝给另一个线程,所以直接delete掉了
        //线程也不支持赋值,但是可以支持移动赋值
    }
}

获取id

 std::this_thread::get_id()
class Func
{

void operator()
{
        cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
}
int main()
{
	thread s((Func()));//使用仿函数进行传参,就要这样弄,
}
};

sleep

std::this_thread::sleep_for(std::chrono::milliseconds(100));//这里面是休眠的时间

join和detach

  • join :就是在一个线程还没处理完之前,主线程都要一直等着这个线程做,直到新线程处理完了,才会放开主线程,
  • detach: 就是会把主线程和新线程分离开来,新线程的事情不影响主线程做事,后台自动回收

void print(int x,int y)//线程函数
{
	cout<<x<<y<<endl;
}

int main()
{
	thread s(print,10,20);
	s.detach();
	if(s.joinable())//判断是否可以被join,如果detach和join之后就不能被join
	
	s.join();
	
	cout<<"hello"<<endl;//使用join要等新线程处理完才会打印
	
		
	return 0;
}

引用与传参

假如说我们在main函数里面定义了对象想要传到thread里面

  • 可以使用指针进行传参
  • 不能用左值来进行接收,但是可以使用std::ref( ),之后就能用左值接收

void func(int* x)//用指针肯定是可以的
{
    *x+=10;
}
void func(int &x) //绝对不能传左值引用,但是下面传参是用std::ref()就能接收,因为正常thread里面都是拷贝
{
    x += 10;
}


int main()
{
	int n = 10;
    // 严格来说thread的参数不能是左值引用,
    
    thread t1(func,&n);//这样子对n的加,不可以,传值拷贝
    thread t2(func, std::ref(n)); //这样弄就可以了
    t1.join();
    t2.join();
    cout << n << endl;
}

atomic

为了解决内置类型传参过去的线程安全的问题

atomic<T> s;
atomic<int> x(0); //这样对x的操作就变成了原子操作,不能用=
atomic_long m{0};//这两者是一样的
atomic<long> n(2);


void func()
{
	x++;//因为这里的x是atomic原子变量,所以是线程安全的,
}
int main()
{
	thread s(func);
	thread p(func);
	return 0;
}

void threadpool()
{
    //实现一个线程池
    atomic<int> x(0);
    //我们实现一个n个线程都对它进行加m次
    int n, m;
    cin >> n >> m;
    vector<thread> vthds;
    vthds.resize(n); //我们直接就开n个线程,用thread的默认构造函数进行初始化,无参的,就不是不运行
    //这里还有还可以用移动构造和移动赋值
    atomic<int> costtime(0);
    for (size_t i = 0; i < vthds.size(); i++)
    {
        vthds[i] = thread([m, &x, &costtime]()
                          {
            int begin=clock();
            for(int i=0;i<m;i++)
            {
                x++;//这里的x是原子变量
            }
            int end=clock();
            costtime+=(end-begin); }); //这里我们用了移动赋值,构造了一个线程对象,线程里面用的是lambda表达式
    }
    for (auto &e : vthds)
    {
        if (e.joinable()) //判断是否可被join,
            e.join();     //这里必须要用&,如果不用的话,就会去掉拷贝构造,这是不允许的
    }
    cout << x << endl;
    cout << costtime << endl;
}

mutex

线程安全里面的锁资源

  • lock就把临界区锁住了
  • unlock可以把解锁
  • try_lock:如果这个锁已近被别人用了,就啥也不干直接返回,如果这个锁是空闲的,就把对应的线程给锁住
int x = 0;
mutex mtx; //定义一个锁出来
void Func(int& n)
{
    //每个线程都有自己的栈,各自在执行自己的func,
    mtx.lock();
    //不能放在里面,放在里面的话,每一次都要去竞争这个锁资源,
    //加在外面变成了串行,运行,就没有意义了,理论上应该加在里面,这样就能交替并行运行
    for (int i = 0; i < n; i++)
    {
        //放在这里锁的事情和释放也有消耗,
        //对用户态的切换,要保存上下文

        cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台

        //抢到锁的人执行的指令太少了,导致另一个人刚离开回去休息又回来了,而是在这里循环等待,一直问,好了我就进去执行,(自旋锁)
        ++x;
    }
    mtx.unlock();
}

int main()
{
	int n=10;
	thread th(Func,std::ref(n));
	return 0;
}

lock_guard

我们使用锁会出现一种情况,一把锁锁住之后,但是里面就调用throw,抛异常之后,就会到catch里面,就把后续代码都跳过了,这个就会造成死锁的问题

所以我们就可以用一个RAII机制的锁,在调用的时候构造,上锁,在析构的时候解锁

lock_guard 只能在作用域结束后才能解锁

模拟实现lock_guard

template <class Lock>
class LockGuard
{
private:
    Lock& _lock;//&,const,和没有默认构造函数的变量,都必须在初始化列表进行初始化

public:
    LockGuard(Lock& lock)//在构造函数的时候就行加锁,但是互斥锁是不支持拷贝的,也要保持是同一把锁
    :_lock(lock)//这里的_lock是mtx的别名
    {
        _lock.lock();
    }
    ~LockGuard()
    {
        _lock.unlock();//在析构函数的时候进行解锁
    }
};

unique_lock

和lock_guard类似,但是可以支持在作用域结束之前解锁
所以更加推荐使用unique_lock

void vfunc(vector<int> &vt, int x, int base, mutex &mtx)
{
    try
    {
        /* code */
        if (base == 200)
        {
            //对应第一个线程就让他sleep一下
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }

        for (int i = 0; i < x; i++)
        {
            //用IO把速度降下来
            // mtx.lock(); //这样用锁有问题
            // LockGuard<mutex> lock(mtx);//在这个里面就加锁,出了for作用域就解锁了,抛异常也算出了作用域,也解锁了,调用析构函数,生命周期到了

            //lock_guard<mutex> lock(mtx);//这个是库里面提供的

            unique_lock<mutex> lockk(mtx);//这个效果也是一样的,除了提供构造和析构,中途解一下锁

            //这个push失败之后就会抛异常

            vt.push_back(i); //有线程安全的问题

            //抛异常之后unlock就不会被执行了,这样可能在上面push里面开空间也会出现问题,所以我们这里的锁可以写一个对象锁
            if (base == 100 && i == 3)
                throw bad_alloc();
            //这里就死锁了,
            // mtx.unlock();

            //会出现死锁,在
        }
    }
    catch (const std::exception &e)
    {
        std::cerr << e.what() << '\n';
        //捕捉到异常之后,把锁释放掉
        // mtx.unlock();
    }
}

void test()
{
    thread t1, t2;
    vector<int> vt;

    //两个线程要用同一个锁
    mutex mtx;
    //这里用的匿名对象,右值引用,线程要放在里面抛异常
    t1 = thread(vfunc, std::ref(vt), 5, 100, std::ref(mtx));  //这样是存在线程安全问题
    t2 = thread(vfunc, std::ref(vt), 10, 200, std::ref(mtx)); //
    //这种小程序用lambda就行了

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

    for (auto e : vt)
    {
        cout << e << " ";
    }
}

cond_variable

wait

 template <class Predicate>
 void wait (unique_lock<mutex>& lck, Predicate pred);

wait后面的参数是可调用对象,同理,也是函数指针,lambda表达式,仿函数,当返回为true时,才会唤醒,否则一直阻塞着

notify_one:唤醒一个线程

实战
交替打印奇偶数

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

//两个线程交替打印,一个打印奇数,一个打印偶数
void test1()
{
    int n = 100;
    // t1打印奇数
    int i = 0;
    mutex mtx;
    bool flag = false;
    condition_variable cv;
    thread t1([&]()
              {
        while(i<n)
        {
            
            //尽量不要单独用lock和unlock
            // lock_guard<mutex> lock(mtx);//这个是出了作用域才解锁
            unique_lock<mutex> lock(mtx);

            //wait后面的是可调用对象,函数,lambda,仿函数
            // cv.wait(lock,[&flag](){return flag;});//在里面如果是false,就会一直阻塞,直到变成true才会开始,唤醒之后flag为true,就打印,
            //这里的wait是直到条件为真才会去执行任务
            cv.wait(lock,[&](){return i%2==1;});
            //唤醒和里面条件都会挡住它
            cout<<this_thread::get_id()<<"->"<<i<<" "<<endl;
            i++;
            flag=!flag;
            cv.notify_one();//唤醒一个

        } });
    // t2打印偶数
    thread t2([&]()
              {
           while(i<n){
            unique_lock<mutex> lock(mtx);
            //!flag是true,这里获取到不会阻塞,就会运行了
            // cv.wait(lock,[&flag](){return !flag;});
            cv.wait(lock,[&](){return i%2==0;});
            cout<<this_thread::get_id()<<"->"<<i<<" "<<endl;;
            i++;
            flag=!flag;//保证下一个自己不会打印
            cv.notify_one();//唤醒
           } });
    t1.join();
    t2.join();
}

int main()
{
    test1();
    return 0;
}
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevin~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值