C++11 线程与同步总结

本文详细介绍了C++11的线程与同步机制,包括线程创建、互斥体、锁类、条件变量、信号量、线程异步调用及返回值和原子操作。重点讲解了线程库的使用,如通过全局函数、仿函数、lambda和成员函数创建线程,以及std::this_thread的功能,如yield()和sleep_for()。此外,还讨论了互斥体、锁类(如std::lock_guard和std::unique_lock)以及条件变量和线程异步调用的相关方法,如std::async、std::future和std::packaged_task。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

while(true) {
  if(pool.try_get_work()) {
    // do work
  }
  else {
    std::this_thread::yield(); // other threads can push work to the queue now
  }
}

std::this_thread::yield() 的目的是避免一个线程(that should be used in a case where you are in a busy waiting state)频繁与其他线程争抢CPU时间片, 从而导致多线程处理性能下降.

std::this_thread::yield() 是让当前线程让渡出自己的CPU时间片(给其他线程使用) 
std::this_thread::sleep_for() 是让当前休眠”指定的一段”时间.

sleep_for()也可以起到 std::this_thread::yield()相似的作用, (即:当前线程在休眠期间, 自然不会与其他线程争抢CPU时间片)但两者的使用目的是大不相同的: 
std::this_thread::yield() 是让线程让渡出自己的CPU时间片(给其他线程使用) 
sleep_for() 是线程根据某种需要, 需要等待若干时间.

一、线程库(thread)

C++11中线程较轻量级,线程的创建和执行的消耗较小,但是同时C++中线程没有明确的状态信息,如果不怕麻烦可以直接使用linux下的posix线程库,这对底层开发者还是很有用的。
**c++标准线程库是c++11/14/17才支持的,很多编译器的版本还并没有完全支持他们的标准

c++11线程特点: 
1. thread创建后,C++始终尝试在一个新线程中执行任务,如果失败返回std::system_error 
2. 没有线程运行结果的接口,无法获得执行结果。(后面会讨论其他方法) 
3. 如果有异常在thread中发生,并且没有被捕获,std::terminate将会被调用 
4. 只能调用join去结束线程,或者detach去转成后台线程执行。否则当thread被析构时,程序会被abort。 
5. 如果线程detach并且main函数执行结束,所有的detach线程会被终止. 
6. 线程只可以通过std::move转移,不可以复制。

1. 通过全局函数创建线程

线程类的构造函数是变参构造函数,第一个参数是线程函数,后面的参数为线程函数的参数(参数通过值传递方式,若要引用传递须加std::ref())。

#include <thread>
#include <iostream>
void hello(int i)
{
    std::cout << "Hello from thread :" << i<<std::endl;
}
int main()
{
    std::thread t1(hello,1);
    t1.join();
    return 0;
}

2. 通过仿函数创建线程

//class Counter 实现 operator()
//1) thread   t1{Counter(1, 20)};   //c++统一推荐方法
//2) Counter  c(1, 20);
//    thread   t2(c);
//3) thread   t3(Counter(1,20));
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

class thread_c
{
public:
    void operator () ()
    {
        cout << "hello from class member function" << endl;
    }
private:
};
int main()
{
    thread_c  c;
    thread t(c);
    //thread t1{thread_c()};
    this_thread::sleep_for(chrono::seconds(1));
    return 0;
}

thread t3(Counter());是不行的,因为编译器会认为你在声明一个函数,函数名为t3,此时只能用第一种构造方式。

3. 通过lambda表达式创建线程

#include <thread>
#include <iostream>
#include <vector>
int main()
{
    std::vector<std::thread> threads;
    for(int i = 0; i < 5; ++i)
    {
        threads.push_back(std::thread([]()
        {
            std::cout << std::this_thread::get_id() << std::endl;
        }
        ));
    }
    
    for(auto& thread : threads)
    {
        thread.join();
    }
    return 0;
}

4. 通过成员函数创建线程

一般常见的是一个类自己创建一个后台处理线程:thread t{&Counter::process, this};

class thread_c
{
public:
    void thread_1(int a)
    {
        std::cout <<a<< std::endl;
    }
};
int main()
{
    thread_c b;
    std::thread t1(&thread_c::thread_1, b,1);
    t1.join();
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

线程本地存储 thread_local 
thread_local int n; 
n作为线程参数传递给线程,那么每个线程有一个n的副本,在线程整个生命周期中存在,且只初始化一次,如同static局部变量.

互斥体类

1) 非定时互斥体类

std::mutex std::recursive_mutex 
方法: 
  lock() : 尝试获取锁,并且阻塞直到获取锁。 
  try_lock() : 尝试获取锁,并立即返回,成功获取返回true,否则false。 
  unlock() : 释放锁。 
mutex与recursive_mutex的区别在于,前者获得锁后不能再获取,这会死锁,后者能递归获取,注意释放次数应与获取次数相等。

2)定时互斥锁类

std::timed_mutex std::recursive_timed_mutex 
方法: 
  lock() , try_lock() , unlock() 
  try_lock_for(rel_time) : 指定相对时间内获得返回true, 超时返回false。 
  try_lock_until(abs_time) : 指定系统绝对时间内获得返回true, 超时返回false。 
timed_mutex与recursive_timed_mutex区别同上。

timed_mutex mtx;
while (!mtx.try_lock_for(std::chrono::milliseconds(200))

2. 锁类

锁类是一个包装器,析构函数会自动释放关联的互斥体。使用C++的RAII(Res Acquisition is init 资源获取即初始化),利用destructor做unlock处理。其构造函数会要求获得互斥体,并阻塞直到获得锁。

1) 简单锁 std::lock_guard

std::mutex mutex;
{
    std::lock_guard<std::mutex> lg(mutex);
}

2) 复杂锁 std::unique_lock 

explict unique_lock( mutex_type& m); //阻塞直到获得锁。

unique_lock(mutex_type& m, defer_lock_t) noexcept; //保存一个互斥体引用,不会立即尝试获得锁。锁可以在以后获得。之后可以用std::lock() 进行加锁 

unique_lock(mutex_type& m, try_to_lock_t); //尝试获得引用的互斥锁,未能获得也不阻塞。

unique_lock(mutex_type& m, adopt_lock_t); //该锁假定线程已获得引用的互斥锁,并负责管理这个锁。   

template<class Clock, class Duration>   

unique_lock(mutex& m, const chrono::time_point<Clock, Duration>& abs_time); //尝试获取该锁,直到超过给定的绝对时间。

template<class Rep, class Period>   

unique_lock(mutex& m, const chrono::duration<Rep, Period>& rel_time); //尝试获取该锁,直到超过给定的相对时间。 unique_lock类还支持lock(), try_lock(), try_lock_for(), try_lock_until()等方法。 通过owns_lock()查看是否获得了这个锁;也可以用if对unique_lock对象直接判断是否获得锁,因为它定义了bool()运算符。

四、条件变量

1. condition_variable

只能等待unique_lock<mutex>的条件变量
 notify_one();  //唤醒等待这个条件变量的线程之一
 notify_all();    //唤醒所有等待这个条件变量的线程

 // 1)前提是已经获得lk的锁
 // 2)调用wait会unlock  lk,然后等待
 // 3)当被唤醒后 lock  lk
以下是重载版本,非重载版本没有pred
void wait (unique_lock<mutex>& lck, Predicate pred);
wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, std::move(pred));
wait_until (lck, chrono::system_clock::now() + rel_time, std::move(pred));

 另外,wait_for 的最后一个参数 pred 表示 wait_for 的预测条件,只有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞,因此相当于如下代码:(超时返回cv_status::timeout)

#include <iostream>  
#include <thread>  
#include <chrono>  
#include <mutex>  
#include <condition_variable>  

using namespace std;  

condition_variable cv;  
mutex mtx;  

void thread_1()  
{  
    unique_lock<mutex> ulock(mtx);  
    cv.wait(ulock);  
    cout << "hello from thread_1" << endl;  
}  
int main(int argc, char **argv)  
{  
    thread t1(thread_1);  
    this_thread::sleep_for(chrono::seconds(1));  
    cv.notify_one();  
    t1.join();  
    return 0;  
} 

2.condition_variable_any

// condition_variable_any::wait (with predicate)
#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex
#include <condition_variable> // std::condition_variable_any

std::mutex mtx;
std::condition_variable_any cv;

int cargo = 0;
bool shipment_available() {return cargo!=0;}

void consume (int n)
{
    for (int i=0; i<n; ++i)
    {
        mtx.lock();
        cv.wait(mtx,shipment_available);
        // consume:
        std::cout << cargo << '\n';
        cargo=0;
        mtx.unlock();
    }
}

int main ()
{
    std::thread consumer_thread (consume,10);

    // produce 10 items when needed:
    for (int i=0; i<10; ++i)
    {
        while (shipment_available())
            std::this_thread::yield();
        mtx.lock();
        cargo = i+1;
        cv.notify_one();
        mtx.unlock();
    }

    consumer_thread.join();

    return 0;
}

五、信号量类

很遗憾c++并没有提供信号量( 信号灯)机制的库。所以可以自己设计一个,下面的程序是google的,只供参考。

#include<mutex>
#include<condition_variable>

class CSemaphore
{
private:
    std::condition_variable cv;
    std::mutex mutex;
    int value;
public:
    CSemaphore(int init) :value(init)
    {
    }
    
    void wait()
    {//类似P操作
        std::unique_lock<std::mutex> lock(mutex);
        while (value < 1)
        {
            cv.wait(lock);
        }
        value--;
    }
    
    bool try_wait() 
    {
        std::unique_lock<std::mutex> lock(mutex);
        if (value < 1)
            return false;
        value--;
        return true;
    }
    
    void post() 
    {//类似V操作
        {
            std::unique_lock<std::mutex> lock(mutex);
            value++;
        }
        cv.notify_one();
    }
};

六、线程异步调用和返回值

1.std::async

异步调用一个callable的对象,但是不保证调用时机。可以通过传入std::launch::async/deferred来制定是否立即调用和延迟(由系统决定调用时机)。async不保证内部使用线程池来实现,但是肯定使用一个后台线程执行任务。

2.std::future

类似于Java的future,获得任务执行结果,对于无返回值的结果使用future,如果调用future的get方法,可以保证async的会立即执行。async()方法返回值是future. 
int sum(int a,int b){return a+b;} 
//如果async的返回值没有被使用,那么async将一直阻塞 
//编译器可以优化为 std::async(sum,1,1).get(); 阻塞调用 
std::future f = std::async(sum,1,1); 
int result = f.get();//获得结果,如果有异常还会抛出异常 
//f.wait() 等运行完成 
//f.wait_for(dur) 等待一定时间或运行完成 
//f.wait_until(fp) 等待某一时刻或运行完成 
//f.share() 生成一个shared_future 见下文

3.std::shared_future

类似于future,但是future的get方法只能被调用一次,二次调用可能出现未定义的行为(不一定抛出异常或出错)。shared_future的二次调用可以保证一样,并且如有异常,可以保证异常一样。 
std::shared_future f = std::async(sum,1,1).share();//需要使用share来获得。

4.std::packaged_task

类似于async,但是可以自己控制调用时机,也可以使用线程去调用任务执行。
std::packaged_task<int(int,int)> task(sum);//封装函数
std::future<int> f = task.get_future();//获得future
task(1,1); //直接调用
//std::thread t{std::move(task),5,5}; //线程方式调用
//t.join();//need join or detach thread
int result = f.get();//获得结果

// packaged_task example
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) 
{
    for (int i=from; i!=to; --i) 
    {
        std::cout << i << '\n';
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "Lift off!\n";
    return from-to;
}

int main ()
{
    std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task
    std::future<int> ret = tsk.get_future();            // get future
    
    std::thread th (std::move(tsk),10,0);   // spawn thread to count down from 10 to 0
    // ...
    int value = ret.get(); // wait for the task to finish and get result
    std::cout << "The countdown lasted for " << value << " seconds.\n";
    
    th.join();
    
    return 0;
}

4. call_once 和once_flag

call_once是c++11中引入的新特性,用于保证某个函数只调用一次,即使是多线程环境下,它也可以可靠地完成一次函数调用。特别适用于某个初始化只执行一次的场景。

  1. 若调用call_once一切顺利,将会翻转once_flag变量的内部状态,再次调用该函数时,所对应的目标函数不会被执行。
  2. 若调用call_once中发生异常,不会翻转once_flag变量的内部状态,再次调用该函数时,目标函数仍然尝试执行。
struct once_flag
{
    once_flag() _NOEXCEPT : __state_(0) {}
private:
    once_flag(const once_flag&); // = delete;
    once_flag& operator=(const once_flag&); // = delete;
    
    unsigned long __state_;
    template<class _Callable>
    friend void call_once(once_flag&, _Callable);
};
template<class Callable, class ...Args>
void call_once(once_flag& flag, Callable&& func, Args&&... args);

第一个参数是std::once_flag的对象(once_flag是不允许修改的,其拷贝构造函数和operator=函数都声明为delete),第二个参数可调用实体,即要求只执行一次的代码,后面可变参数是其参数列表。 

call_once保证函数fn只被执行一次,如果有多个线程同时执行函数fn调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态)——不会直接返回,直到活动线程对fn调用结束才返回。对于所有调用函数fn的并发线程,数据可见性都是同步的(一致的)。 
如果活动线程在执行fn时抛出异常,则会从处于”passive execution”状态的线程中挑一个线程成为活动线程继续执行fn,依此类推。一旦活动线程返回,所有”passive execution”状态的线程也返回,不会成为活动线程。

#include<iostream>
#include<mutex>

//在单例模式中建立对象,最好使用call_once或者进行加锁
class singleton
{
public:
    static singleton* instance()
    {
        std::call_once(_flag, [&](){_instance = new singleton();});
        return _instance;
    }

private:
    static std::once_flag _flag;
    static singleton* _instance;
    singleton() {}
    ~singleton() {}
};

singleton* singleton::_instance = nullptr;
std::once_flag singleton::_flag;

int main(int argc, char* argv[])
{
    std::cout<<singleton::instance()<<std::endl;
    std::cout<<singleton::instance()<<std::endl;
    std::cout<<singleton::instance()<<std::endl;

    return 0;
}

注意: once_flag的生命周期,它必须要比使用它的线程的生命周期要长。所以通常定义成全局变量比较好。

原子操作库(atomic)

atomic<int> counter(0);  //等效于 atomic_int counter(0); 

#include<atomic>
#include<thread>
#include<iostream>
void func(std::atomic<int>& counter)
{
    for(int i=0; i<1000; ++i)
        ++counter;
}
int main()
{
    std::atomic<int> counter(0);
    std::vector<std::thread>   threads;
    for(int i=0; i<10; ++i)
        threads.push_back(std::thread{func, std::ref(counter)});
    for(auto& t : threads)
        t.join();
    std::cout<<"Result="<<counter<<std::endl;
    return 0;
}

七、this_thread

this_thread::get_id: 返回当前线程的id 
this_thread::yield: 让调度器先运行其它的线程,这在忙于等待状态时很有用 
this_thread::sleep_for: 将当前线程置于阻塞状态,时间不少于参数所指定的时间段 
this_thread::sleep_util: 在指定的时刻来临前,一直将当前的线程置于阻塞状态 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值