C++11(下)线程库

参考文档:https://zh.cppreference.com/w/cpp/thread

线程

构造

C++11把线程相关的系统调用封装成了 std::thread 。我们看下构造函数

thread() noexcept;							//构造一个空线程
thread( thread&& other ) noexcept;			//支持移动构造
template< class F, class... Args >			
explicit thread( F&& f, Args&&... args );	//传参构造
thread( const thread& ) = delete;			//禁用拷贝构造

线程对象会在底层关联一个线程,如果构造了一个空线程,那么该对象在底层不会关联任何线程。

传参构造的第一个参数是可调用对象,参数包是可调用对象的参数包。

传参构造是以值拷贝的方式传递给底层的系统调用,所以,如果传引用传参就会变成传值传参,所以我们需要借助 std::ref 来解决该问题,如下代码

#include 
void ThreadFunc1(int& x) { x += 10; }
void ThreadFunc2(int* x) { *x += 10; }
int main()
{
 int a = 10;
 // 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
 thread t1(ThreadFunc1, a);
 t1.join();
 cout << a << endl;
 // 如果想要通过形参改变外部实参时,必须借助std::ref()函数
 thread t2(ThreadFunc1, std::ref(a);
 t2.join();
 cout << a << endl;
 // 地址的拷贝
 thread t3(ThreadFunc2, &a);
 t3.join();
 cout << a << endl;
 return 0;
}

std::thread 类禁掉了拷贝构造,说明一个线程不允许被另一个线程拷贝。

但是 std::thread 类支持移动构造,这是为什么呢?这是为了方便空线程转移资源。如下代码

int sum(int a, int b) { return a + b; }
thread t1;
t1 = thread(sum, 1, 3);

先创建空线程 t1 。thread(sum, 1, 3) 是匿名对象,具有右值属性,所以该线程的资源会被转移给 t1。支持移动构造是为了我们方便写线程池。

注:线程也支持移动赋值

thread& operator=( thread&& other ) noexcept;

如果我们向先创建一批线程但不执行任何函数,可以这样写

vector<thread> threads(10);

当我们想用线程池中的某个线程时,只需用传参构造匿名对象,然后把匿名对象的资源转移给要执行的线程。

成员函数

get_id() :获取线程id

jionable() :线程是否还在执行

jion() :该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行

detach():该函数调用后,线程的“死活”就与主线程无关了

原子操作

https://zh.cppreference.com/w/cpp/atomic/atomic

我们看如下程序,两个线程对同一个整数加加一万次

int main() {
    int x = 0, n = 10000;
    thread t1([&, n]()mutable { while(n--) x++; });        
    thread t2([&, n]()mutable { while(n--) x++; });     
    t1.join(); 
    t2.join(); 
    cout << x << endl;  
    return 0;
}

结果如下

很明显有线程安全, 如果我们不想加锁,用 std::atomic 可以让类型的属性具有原子性。

atomic<int> x = 0;

这样在加加 x 时,永远是线程安全的

互斥量

https://zh.cppreference.com/w/cpp/thread/mutex

某一时刻有且只有一个线程可以锁住互斥量,互斥量所保护的临界区的资源就是原子的

成员函数

lock 锁定互斥体,若互斥体不可用则阻塞
try_lock 尝试锁定互斥体,若互斥体不可用则返回
unlock 解锁互斥体
void task(int& x, mutex& lock) {     
    int n = 100000; 
    lock.lock();
    while (n--) x++;
    lock.unlock();
}

int main() {
    mutex lock;
    int x = 0;
    thread t1(task, ref(x), ref(lock));
    thread t2(task, ref(x), ref(lock));        
    t1.join();
    t2.join();
    cout << x << endl;     
    return 0;
}

用对象生命周期管理锁

这个思想和智能指针类似,用类的构造函数和析构函数管理资源。

我们可以用构造函数加锁,析构函数解锁,这样会很大程度避免死锁问题。

C++11 的线程库提供了 std::lock_guard 和 std::unique_lock,这两个类模板可以用构造和析构管理加锁和解锁

如下代码,可以用 {} 指定局部作用域

void task(int& x, mutex& lock) {     
    int n = 100000; 
    
    {
        lock_guard<mutex> lock_guard(lock);   
        while (n--) x++;
    }
}

int main() {
    mutex lock;     
    int x = 0;
    thread t1(task, ref(x), ref(lock)); 
    thread t2(task, ref(x), ref(lock));         
    t1.join();
    t2.join();
    cout << x << endl;     
    return 0;
}

条件变量

https://zh.cppreference.com/w/cpp/thread/condition_variable

条件变量需要被互斥量保护,所以条件变量需要关联一个互斥量,如果某个条件成立,对互斥量解锁并阻塞到条件变量上。

下面是一个很经典的问题:创建两个线程,一个线程打印基数,一个线程打印偶数,并且交替打印

代码如下

int main() {

    condition_variable cv;
    mutex m;
    bool falg = true;       
    int count = 0, n = 10;

    thread t1([&]() {
        int i = 1;
        while (i < n) {
            unique_lock<mutex> lock(m); 
            cv.wait(lock, [&]() { return falg == true; });   
            cout << "打印基数的线程" << std::this_thread::get_id() << ":  " << i << endl;
            i += 2;
            falg = false;
            cv.notify_one();
        }
        });

    thread t2([&]() {
        int i = 0;
        while (i < n) {
            unique_lock<mutex> lock(m); 
            cv.wait(lock, [&]() { return falg == false; }); 
            cout << "打印偶数的线程" << std::this_thread::get_id() << ":  " << i << endl;
            i += 2; 
            falg = true; 
            cv.notify_one(); 
        }
        });
        
    t1.join();
    t2.join();

    return 0;
}

C++11(下)线程库_第1张图片

你可能感兴趣的:(C++,c++,C++11,线程库)