C++11的 thread多线程

本文详细介绍了C++11标准下多线程编程的基础知识,包括std::thread类的使用方法,线程互斥与同步通信机制,以及基于CAS的原子类操作。通过实例代码,阐述了线程的创建、执行、同步和通信过程。

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

一、C++11的多线程类thread

C++11之前,C++库中没有提供和线程相关的类或者接口,因此在编写多线程程序时,Windows上需要调用CreateThread创建线程,Linux下需要调用clone或者pthread_create来创建线程,但是这样是直接调用了系统相关的API函数,编写的代码,无法做到跨平台编译运行。

C++11之后提供了thread线程类,可以很方便的编写多线程的程序。

std::thread定义了四种构造函数:

thread() noexcept;

template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );

thread( thread&& other ) noexcept;

thread( const thread& ) = delete;
  • 默认构造函数,创建一个空的 std::thread 执行对象。
  • 初始化构造函数,创建一个 std::thread 对象,可以传入任意函数对象,以及函数参数。在创建thread对象时,std::thread构建函数中的所有参数均会按值并以副本的形式保存成一个tuple对象。
  • 移动构造函数,调用成功之后该线程对象会转移。
  • 拷贝构造函数(被禁用),意味着线程无法拷贝。

随便提一下,当你创建了一个带参(非空的)线程对象时,该线程就会立即执行,不需要显式的调用start或者run 

举个例子,下面展示了四个构造函数的使用

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
using namespace std;

void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 1 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
};

void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
};

class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 3 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};

class baz
{
public:
    void operator()()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 4 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};

int main()
{
    int n = 0;
    foo f;
    baz b;
    std::thread t1([] {
        cout << "Hello World from lambda thread." << endl;
    });                  // 线程会立即执行

    std::thread t2(f1, n + 1); // 值传递
    std::thread t3(f2, std::ref(n)); //引用传递
    std::thread t4(std::move(t3)); // 移动构造函数,t4 is now running f2(). t3 is no longer a thread
    std::thread t5(&foo::bar, &f); // 在类的成员函数,t5 runs foo::bar() on object f
    std::thread t6(b); // 使用仿函数,t6 runs baz::operator() on a copy of object b
    //std::thread t6(std::ref(b));

    // 等待线程t2和t4,t5,t6执行完,main线程再继续运行
    t2.join();
    t4.join();
    t5.join();
    t6.join();
    std::cout << "Final value of n is " << n << '\n';
    std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';
    std::cout << "Final value of b.n (baz::n) is " << b.n << '\n'; // 因为是复制对象,原对象的n不改变

	return 0;
}

  注解:

 join:等待子线程,调用线程main处于阻塞模式。直到由 子线程线程执行完毕, join 才返回,join()执行完成之后,底层线程id被设置为0,即joinable()变为false。

joinable:检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。

detach:  将当前线程对象所代表的执行实例与该线程对象分离,不会阻塞当前main线程,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

 void foo(int n)
 {
	 std::cout << "start run  " << endl;
	 std::cout << "run  " << n << endl;
 }

 int main()
 {
	 std::thread t1(foo, 1);
	 if (t1.joinable())
	 {
		 std::cout << "thread joinable" << std::endl;
	 }

	 std::cout <<"this_thread调用get_id  "<< std::this_thread::get_id() << std::endl;
	 std::cout << "t1调用get_id  " << t1.get_id() << std::endl;
	 t1.detach();

	 // detach之后,joinable返回false,get_id返回0x0
	 if (!t1.joinable())
	 {
		 std::cout << "t1 can not joinable" << std::endl;
	 }

	 std::cout << "t1调用get_id " <<t1.get_id() << std::endl;
     return 0;
}

运行结果

二、线程互斥

在多线程环境中运行的代码段,需要考虑是否存在竞态条件。如果存在竞态条件,我们就说该代码段不是线程安全的,是不能直接运行在多线程环境当中。对于这样的代码段,我们经常称之为临界区。临界区又称关键代码段,指的是一小段代码在代码执行前,需要独占一些资源资源,比如访问CPU,打印机等。对于临界区资源,多线程环境下需要保证它以原子操作执行,要保证临界区的原子操作,就需要用到线程间的互斥操作-锁机制。

int g_i = 0;
mutex g_mutex;
void c()
{
	for (int i = 0; i < 3; ++i)
	{
		g_mutex.lock();
		cout << std::this_thread::get_id() << "  " << g_i++ << endl;
		g_mutex.unlock();
	}
}
 
int main()
{
	thread t1(c);
	thread t2(c);
 
	if (t1.joinable())
		t1.join();//调用该函数会阻塞当前线程,直到该t1线程执行完成
	if (t2.joinable())
		t2.join();
    return 0;
}

三、基于CAS的原子类 

mutex互斥锁毕竟比较重,对于系统消耗有些大,C++11的thread类库提供了针对简单类型的原子操作类,如std::atomic_int,atomic_long,atomic_bool等,它们值的增减都是基于CAS操作的,既保证了线程安全,效率还非常高

下面代码示例开启10个线程,每个线程对整数增加1000次,保证线程安全的情况下,应该加到10000次,这种情况下,可以用atomic_int来实现,代码示例如下:

#include <iostream>
#include <atomic> // C++11线程库提供的原子类
#include <thread> // C++线程类库的头文件
#include <vector>

// 原子整形,CAS操作保证给sum自增自减的原子操作
std::atomic_int sum = 0;

// 线程函数
void sumTask()
{
    // 每个线程给sum加1000次
    for (int i = 0; i < 1000; ++i)
    {
        sum++;
    }
};

int main()
{
    // 创建10个线程放在容器当中
    std::vector<std::thread> vec;
    for (int i = 0; i < 10; ++i)
    {
        vec.push_back(std::thread(sumTask));
    }

    // 等待线程执行完成
    for (int i = 0; i < vec.size(); ++i)
    {
        vec[i].join();
    }

    // 所有子线程运行结束,sum的结果每次运行应该都是10000
    std::cout << "sum : " << sum << std::endl;

	return 0;
}

四、线程的同步通信

多线程在运行过程中,各个线程都是随着操作系统的调度算法,占用CPU时间片来执行指令做事情,每个线程的运行完全没有顺序可言。但是在某些应用场景下,一个线程需要等待另外一个线程的运行结果,才能继续往下执行,这就需要涉及线程之间的同步通信机制。

线程间同步通信最典型的例子就是生产者-消费者模型,生产者线程生产出产品以后,会通知消费者线程去消费产品;如果消费者线程去消费产品,发现还没有产品生产出来,它需要通知生产者线程赶快生产产品,等生产者线程生产出产品以后,消费者线程才能继续往下执行。

C++11 线程库提供的条件变量condition_variable,就是Linux平台下的Condition Variable机制,用于解决线程间的同步通信问题,下面通过代码演示一个生产者-消费者线程模型,仔细分析代码:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <vector>

// 定义互斥锁(条件变量需要和互斥锁一起使用)
std::mutex mtx;
// 定义条件变量(用来做线程间的同步通信)
std::condition_variable cv;
// 定义vector容器,作为生产者和消费者共享的容器
std::vector<int> vec;

// 生产者线程函数
void producer()
{
	// 生产者每生产一个,就通知消费者消费一个
	for (int i = 1; i <= 10; ++i)
	{
		// 获取mtx互斥锁资源
		std::unique_lock<std::mutex> lock(mtx);

		// 如果容器不为空,代表还有产品未消费,等待消费者线程消费完,再生产
		while (!vec.empty())
		{
			// 判断容器不为空,进入等待条件变量的状态,释放mtx锁,
			// 让消费者线程抢到锁能够去消费产品
			cv.wait(lock);
		}
		vec.push_back(i); // 表示生产者生产的产品序号i
		std::cout << "producer生产产品:" << i << std::endl;

		/* 
		生产者线程生产完产品,通知等待在cv条件变量上的消费者线程,
		可以开始消费产品了,然后释放锁mtx
		*/
		cv.notify_all();

		// 生产一个产品,睡眠100ms
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}
// 消费者线程函数
void consumer()
{
	// 消费者每消费一个,就通知生产者生产一个
	for (int i = 1; i <= 10; ++i)
	{
		// 获取mtx互斥锁资源
		std::unique_lock<std::mutex> lock(mtx);

		// 如果容器为空,代表还有没有产品可消费,等待生产者生产,再消费
		while (vec.empty())
		{
			// 判断容器为空,进入等待条件变量的状态,释放mtx锁,
			// 让生产者线程抢到锁能够去生产产品
			cv.wait(lock);
		}
		int data = vec.back(); // 表示消费者消费的产品序号i
		vec.pop_back();
		std::cout << "consumer消费产品:" << data << std::endl;

		/*
		消费者消费完产品,通知等待在cv条件变量上的生产者线程,
		可以开始生产产品了,然后释放锁mtx
		*/
		cv.notify_all();

		// 消费一个产品,睡眠100ms
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}
int main()
{
	// 创建生产者和消费者线程
	std::thread t1(producer);
	std::thread t2(consumer);

	// main主线程等待所有子线程执行完
	t1.join();
	t2.join();

	return 0;
}

参考:

C++11 - thread多线程编程,线程互斥和同步通信,死锁问题分析解决_大秦坑王的专栏-优快云博客

《深入应用C++11》笔记-线程std::thread_WizardtoH-优快云博客

std::thread详解 - _yanghh - 博客园

多线程 - 从 pthread 转换到 std::thread_个人文章 - SegmentFault 思否

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值