C++高阶-多线程开发thread

本文详细介绍了C++11中多线程编程的相关概念和使用,包括如何使用`<thread>`头文件创建线程,通过函数对象和Lambda表达式启动线程,以及线程间的数据传递。文章还讨论了如何使用future和promise简化线程返回值操作,介绍了线程的时间控制,如`sleep_for()`和`sleep_until()`函数。此外,还涵盖了互斥锁(mutex)的使用,包括`lock_guard`和`unique_lock`在管理互斥锁中的作用,以及OpenMP的并行编程概念。

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

为了简化多线程程序的设计与实现, C++11的标准库专门提供了<thread>头文件以支持多线程程序的开发。

利用thread创建线程

C++11 中的<thread>头文件提供了 thread、 mutex 以及 unique_lock 等基本对象来对多线程开发中最常用的线程、互斥以及锁等基本概念进行抽象与表达,为多线程程序的实现提供了一个较低抽象层次的编程模型。

线程是对程序中的某个执行或者计算过程的一种表述,而所谓的多线程程序,就是将原来的一个执行过程分成多个过程去执行。由此可见,线程的创建,是实现多线程的基础。

thread 类对线程概念进行了很好的抽象与实现,从而使得我们可以非常简单地使用一个函数指针(也包括函数对象和 Lambda 表达式)来构建一个 thread 对象。而一旦拥有了 thread 对象,就意味着我们创建了一个线程,也就可以利用 thread 对象所提供的成员函数对这个线程进行调度,启动、挂起或者停止这个线程,以操作线程完成某个执行过程。

#include <iostream>
// 引入定义 thread 类的头文件
#include <thread>
// 使用 thread 所在的名字空间
using namespace std;
// 定义需要线程执行的函数和函数对象
void ListenMusic()
{
	cout<<"听音乐"<<endl;
}
struct ReadBook
{
	void operator()()
	{
		cout<<"看书"<<endl;
	}
};

int main()
{
	// 使用函数对象创建线程对象 readthread,并立即启动线程的执行
	ReadBook read;
	thread readthread(read);
	// 使用函数指针创建线程对象 listenthread,并立即启动线程的执行
	thread listenthread(ListenMusic);
	// 处理其他业务逻辑…
	// 调用 join()函数,等待分支线程执行完毕
	readthread.join();
	listenthread.join();
	// 所有分支线程执行完毕,程序退出
	return 0;
}

在这段代码中,我们利用函数对象类 ReadBook 的一个 read 函数对象和指向 ListenMusic()函数的函数指针(也就是它的函数名)作为 thread 类构造函数的参数,分别创建了 readthread 和listenthread 这两个 thread 对象。
thread 对象的创建, 意味着它将创建新的线程,并开始执行作为构造函数参数传递给 thread 对象的函数对象或者是函数,通过简单的一个步骤,就完成了线程的创建与启动执行。

在多线程环境下,我们将执行主函数并负责创建其它线程的线程称为主线程,而那些被创建的线程则相应地被称为分支线程或工作者线程,其执行的函数则被称为线程函数。在执行的时候,主线程开始进入主函数执行,通过创建两个 thread 对象而创建了两个分支线程并立即启动执行其线程函数,而与此同时,执行主函数的主线程将继续向下执行。这样,主线程和两个分支线程同时都在执行,操作系统会将它们调度到 CPU 的多个运算核心去执行,以此达到对 CPU 多个运算核心的充分利用。当主线程遇到thread 对象调用的join()函数后,主线程将等待这个 thread 对象执行完毕之后,再继续往下执行,直到最后主函数执行完毕,退出整个程序。

线程中的数据传递

我们也需要向线程函数内传入数据以供其进行处理,或者是从线程函数中传出结果数据。要做到这一点,我们同样需要给线程函数加上参数,跟普通函数类似,如果只是需要向线程函数内传入数据,那就加上传值形式的参数,而如果加上传指针和传引用形式的参数,则既可以传入也可以传出数据

与普通函数在调用时将实际参数复制给函数的形式参数所不同的是,线程函数的形式参数的赋值是在这个函数被用于创建 thread 对象时完成的。当我们在使用某个带有参数的线程函数创建 thread 对象时,在 thread 构造函数的实际参数中,我们不仅要用这个函数指针或者函数对象做第一个参数,同时其后还要依次加上线程函数所需要的各个实际参数。在创建thread 对象的时候,这些实际参数会被拷贝复制给线程函数相应的形式参数,以此来实现数据的传递。

如果线程函数的参数是传引用形式,那么在创建 thread 对象的时候,我们需要使用 ref()函数获得实际参数的引用才行,否则,即使这个参数是引用形式,它也会被拷贝复制而在线程函数和本地函数间形成两个副本,起不到传出数据的效果。

// 需要传递数据的线程函数
// 传值形式的 strSong 负责向线程函数内传入数据
// 传引用形式的 vecEar 负责向线程函数外传出数据
void ListenMusic(string strSong,vector<string>& vecEar)
{
	cout<<"我正在听"<<strSong<<endl; // 使用外部数据
	vecEar.push_back(strSong); // 将结果数据存入外部容器
}
// …

int main()
{
	// 准备需要传递的数据
	vector<string> vecEar; // 用于保存结果数据的容器
	string strSong = "歌唱祖国"; // 传入线程函数的数据
	// 在创建 thread 对象时传递数据
	// 第一个参数是线程函数指针,其后依次是线程函数所需要的参数
	thread listenthread(ListenMusic,strSong,ref(vecEar));
	// …
	listenthread.join();
	// 输出结果数据
	for(string strName : vecEar)
		cout<<strName<<endl;
	return 0;
}

线程的时间控制

在利用线程执行某个任务的时候,我们往往要对线程的执行时间进行控制,让线程在等待一定时间之后再继续执行,或者是在某个事先设定的固定时间点之后执行。这时,我们就需要用到std::this_thread名字空间下的 sleep_for()函数和sleep_until()函数来完成对线程执行状态的时间控制了。

  • sleep_for()函数可以让当前线程(也就是调用这个函数的线程)暂停执行一段时间,等过了这段时间之后再继续恢复执行;
  • sleep_until()函数则是让当前线程一直暂停,直到某个固定时间点的到来才会继续恢复执行。它们就像两条瞌睡虫,一条可以让线程瞌睡一整天(固定时间段),而另一条更厉害,可以让线程一直瞌睡到天明(固定时间点)。
#include <iostream>
#include <thread> // 引入线程相关的头文件
#include <chrono> // 引入时间相关的头文件
using namespace std;
using namespace std::chrono; // 使用时间相关的名字空间
int main()
{
	// 构造一个固定时间点: 2012 年 12 月 21 日零时
	tm timeinfo = tm();
	timeinfo.tm_year = 112; // 年: 2012 = 1900 + 112
	timeinfo.tm_mon = 11; // 月: 12 = 1 + 11
	timeinfo.tm_mday = 21; // 21 日
	time_t tt = mktime(&timeinfo);
	// 利用 time_t 类型的变量 tt 创建一个表示世界末日固定时间点的 time_point 对象 tp
	system_clock::time_point tp = system_clock::from_time_t (tt);
	// 当前线程一直瞌睡到 tp 表示的 2012 年 12 月 21 日零时
	this_thread::sleep_until(tp);
	// 世界末日到了,程序继续恢复执行,响铃 10 次发出警报
	for(int i = 0; i < 10; ++i)
	{
		cout<<'\a'; // 输出一个计算机响铃
		// 当前线程瞌睡一秒钟,然后继续恢复执行下一次循环
		this_thread::sleep_for(seconds(1));
	}
	return 0;
}

利用 future 和 promise 简化线程的返回值操作

更多时候,为了及时地得到结果数据,往往是还未等分支线程执行完毕,主线程就会试图访问用于保存结果数据的两个线程之间的共享变量。这时有可能发生的状况是,分支线程正在写入结果数据到共享变量,而主线程却也正在从共享变量读取结果数据,其结果往往是主线程读取到错误的结果数据。

futurepromise是一种可以在线程之间传递数据的较高层次的编程机制,它可以在线程之间传递数值数据,也可以传递异常,更方便的是,在分支线程的结果数据尚未准备完成的情况下,它还可以让主线程一直等待而不用去不断地查询结果数据是否已经准备完成。而一旦分支线程的结果数据准备完成,它又会及时地通知主线程来读取结果数据。

future 提供的是 get 操作(通过 get()函数实现),与之相对应的, promise 提供的是 set 操作(通过 set_value()函数或 set_exception()函数实现)。

  • 首先将future 对象和 promise 对象配对,然后将 promise 对象传递给分支线程。
  • 当一个分支线程完成结果数据后,它就把这个数据通过 set_value()函数放到 promise 对象中;
  • 之后,这个结果数据就会出现在和此 promise 对象关联的 future 对象中。
  • 最终通过这个 future 对象的 get()成员函数,我们就可以读取到从线程函数中传递出来的结果数据。
#include <thread>
#include <future> // 引入 future 所在的头文件
#include <string>
#include <iostream>
using namespace std;
// 需要从线程函数传递出来的数据
class Food
{
	public:
		Food(){} // 默认构造函数
		// 通过菜名构建 Food 对象
		Food(string strName) : m_strName(strName){}
		// 获取菜名
		string GetName() const
		{
			return m_strName;
		}
	private:
		string m_strName; // 菜名
};
// 线程函数
// 根据菜名创建 Food 对象,并通过 promise 对象返回结果数据
void Cook(const string strName,promise<Food>& prom)
{
	// 做菜…
	Food food(strName);
	// 将创建完成的 food 对象放到 promise 传递出去
	prom.set_value(food);
}
int main()
{
	// 用于存放结果数据的 promise 对象
	promise<Food> prom;
	// 获得 promise 所关联的 future 对象
	future<Food> fu = prom.get_future();
	// 创建分支线程执行 Cook()函数
	// 同时将菜名和用于存放结果数据的 promise 对象传递给 Cook()函数
	// ref()函数用于获取 promise 对象的引用
	thread t(Cook,"回锅肉",ref(prom));
	// 等待分支线程完成 Food 对象的创建,一旦完成,立即获取完成的 Food 对象
	Food food = fu.get();
	// 上菜
	cout<<"客官,你点的"<<food.GetName()<<"来了,请慢用! "<<endl;
	t.join(); // 等待分支线程最终完成
	return 0;
}

future 和 promise 实际上是两个类模板,在这里,我们首先根据它们所处理的结果数据,使用了相应的数据类型 Food 对其进行了特化而得到模板类future<Food>promise<Food>,之后就是创建用于存放结果数据的 promise 对象 prom,并通过 get_future()函数从 prom 对象获得与之关联的future对象fu。这样,prom对象和fu对象就配对成功,也就意味着它们之间达成了某种约定(promise):
在分支线程中,我们可以将结果数据 set_value()到 prom 对象中,而在主线程中,我们可以从 fu 对象中 get()到结果数据

接下来的工作,就是在创建线程执行线程函数的同时,将用于存放结果数据的 prom 对象传递给线程函数。而在线程函数的执行中,如果结果数据已经准备完成,它就会通过set_value()函数将结果数据存放到 prom 对象中。于此同时,主线程正在通过 fu 对象的 get()函数等待分支线程的结果数据准备完成,一旦分支线程将准备完成的结果数据 set_value()到prom 对象,get()函数就会获取到分支线程存放在 prom 中的结果数据并返回。整个过程不需要主线程去不断查询,而分支线程一旦完成结果数据主线程就会立刻得到,从而很好地做到了结果数据的及时返回。

一旦主线程开始调用 future 对象的 get()函数等待分支线程返回结果数据,主线程的执行就会暂停下来,直到分支线程的结果数据准备完成并被 set_value()到 promise 中,get()函数获得结果数据返回而主线程才会继续往下执行。

wait_for()

分支线程的执行有可能会出现问题而导致结果数据迟迟没有准备完成。使用 future 对象的 wait_for()成员函数让主线程等待一段时间,如果在这个时间内分支线程的结果数据没有准备好,它就会结束等待继续执行后面的代码。

#include <chrono> // 为了使用 minutes 类
// …
using namespace std::chrono; // 使用 minutes 所在的名字空间
//
// 等待十分钟
if (fu.wait_for(minutes(10)))
{
	// 如果十分钟内结果数据准备完成,则从 future 对象中获取结果数据
	Food food = fu.get();
	cout<<"客官,你点的"<<food.GetName().c_str()<<"来了,请慢用! "<<endl;
}
else // 如果十分钟内结果数据尚未到达
{
	cout<<"等不下去了,换个餐馆"<<endl;
}

packaged_task

future 和 promise 的相互配合,是可以从线程函数及时返回结果数据,但是这个过程中我们既要完成它们之间的配对,又要传递 promise 对象给线程函数并自己动手完成结果数据的存放,整个过程稍显繁琐。 为了简化这一过程, C++11 特别地提供了 packaged_task 类模板。

首先用这个线程函数的返回值和参数类型对 packaged_task 类模板进行特化以形成特定类型的 packaged_task 模板类,然后用这个线程函数作为其构造函数参数就可以创建得到一个 packaged_task 对象。而在这个对象中,就已经完成了 future 和 promise 的配对工作,我们只需要通过它的 get_future()成员函数就可以得到配对完成的 future 对象,进而可以用它来获得线程函数的返回值。

// …
// 线程函数的返回值成了我们所需要的结果数据
// 不再需要向线程函数传递 promise 对象
Food Cook(const string strName)
{
	// 做菜…
	Food food(strName);
	// 直接将结果数据通过返回值返回
	return food;
}
int main()
{
	// 使用线程函数的返回值和参数类型特化 packaged_task 类模板
	// 利用其构造函数,将线程函数打包成一个 packaged_task 对象
	packaged_task<Food(string)> cooker(Cook);
	// 从 packaged_task 对象获得与之关联的 future 对象
	future<Food> fu = cooker.get_future();
	// 创建线程执行 packaged_task 对象,实际上执行的是 Cook()函数
	// 这里也不再需要传递 promise 对象
	thread t(move(cooker),"回锅肉");
	// 同样地获得结果数据
	Food food = fu.get();
	cout<<"客官,你点的"<<food.GetName()<<"来了,请慢用! "<<endl;
	t.join();
	return 0;
}

async()

使用 packaged_task 类模板,确实起到了简化 future 和 promise 配对工作的目的,可是在这个过程中,我们仍然需要创建 packaged_task 对象,仍然需要从 packaged_task 对象中获得future 对象,仍然需要创建线程来执行这个 packaged_task 对象,整过过程仍然显得比较繁琐。

C++11 提供了 async()函数,通过简单的一个函数调用,一次性地完成上面这些繁琐的过程。通过 async()函数,我们只需要提供一个线程函数,它就会创建并启动相应的分支线程来执行这个线程函数,更关键的是,它还会完成 future 和 promise 的配对,并直接返回一个可以获取线程函数返回值的 future 对象。而主线程只需要通过调用它的 get()函数等待分支线程执行结束,就可以直接得到线程函数的返回值。

// 将 Cook()函数异步( async)执行
future<Food> fu = async(bind(Cook,"回锅肉"));
cout<<"客官,你点的"<<fu.get().GetName()<<"来了,请慢用! "<<endl;

首先利用 bind()函数将线程函数 Cook()和它的参数共同打包成一个匿名的函数对象,然后作为参数交给 async()函数去异步执行。 async()函数会根据情况创建新的线程或者是复用已有的线程来执行 Cook()线程函数,并返回一个可以在将来( future)取得线程函数返回值的 future 对象,而通过它的 get()函数,主线程就可以在线程函数执行完毕后及时得到它的返回值。

随着过程的减少,它同样也减少了对整个过程进行更多控制以适应更复杂需求的机会。比如,通过 packaged_task 的方法,我们无法控制结果数据返回的时机通过 async()函数的方法,我们无法控制线程的执行。

利用 mutex 处理线程之间的共享资源竞争

mutex

前面我们所遇到的多线程应用场景,都只是各个线程各自独立地访问自己的资源,各个线程之间并没有共享资源,也就不存在共享资源的竞争。然而在更多时候,多个线程之间往往需要共享资源,比如它们都需要访问某个共享的容器,这时就存在一个共享资源竞争的问题。当多个线程各自独立地同时访问某个共享资源时,将导致未定义的结果

C++11 专门提出了多种措施来处理线程之间共享资源的竞争,以保证在任何时刻都只有唯一的一个线程对共享资源进行操作,从而确保其操作行为结果的确定性和唯一性。在这些措施当中,最简单也是最常用的就是互斥机制互斥机制主要通过 mutex 类来实现

首先在程序中创建一个全局的 mutex对象,然后通过在线程函数中先后调用这个对象的 lock()成员函数和 unlock()成员函数来形成一个代码区域,通常称为临界区。而互斥机制保证了在任何时刻, 最多只能有一个线程进入到 lock()函数和 unlock()函数之间的临界区执行其中的代码

当第一个线程正在临界区执行时, 临界区处于锁定状态。如果后续有执行到 lock()函数临界区开始位置的线程将会被阻塞,进入线程队列等待,直到第一个线程执行到 unlock()函数临界区结束离开临界区,解除了临界区的锁定,其他处于线程队列中等待的线程才会按照先进先出( FIFO,First In, First Out)的规则进入临界区执行,而一旦有线程进入临界区,临界区又会被再次锁定。其他未进入临界区的线程只有在线程队列中继续等待,直到它的机会到来。由于互斥对象所确定的临界区每次只能有一个线程进入执行,如果我们将对共享资源的访问放到临界区来进行,这样就能保证每次只有一个线程在临界区对共享资源进行访问,也就避免了共享资源被多个线程同时访问的问题。

#include <mutex> // 引入 mutex 所在的头文件
#include <queue> // 引入 queue 容器所在的头文件
// 全局的互斥对象
mutex m;
// 全局的 queue 容器对象 quFoods
// 线程函数会将炒好的菜 push()到 quFoods 容器,所以它表示服务员
queue<Food> quFoods;
// 线程函数,创建临界区访问共享资源 quFoods
void Cook(string strName)
{
	// 炒菜…
	// 这些不涉及共享资源的动作是可以放在临界区之外多个线程同时进行的
	Food food(strName);
	m.lock(); // 临界区开始
	// 对共享资源的操作
	quFoods.push(food);// 将 food 对象添加到共享的容器中
	m.unlock(); // 临界区结束
}
int main()
{
	thread coWang(Cook,"回锅肉"); // 王厨子炒回锅肉
	thread coChen(Cook,"盐煎肉"); // 陈厨子炒盐煎肉
	// 等待厨子炒完菜…
	coWang.join();
	coChen.join();
	// 输出结果
	cout<<"两位厨子炒出了"<<endl;
	// 输出 quFoods 容器中所有 Food 对象的名字
	// 这里只有主线程会执行,所以对共享资源的访问不需要放在临界区
	while(0 != quFoods.size() )
	{
		cout<<quFoods.front().GetName()<<endl;
		quFoods.pop(); // 从容器中弹出最先进入队列的 Food 对象
	}
	return 0;
}

首先创建了一个全局的 mutex 对象 m 以及一个共享资源 quFoods 容器,然后在线程函数 Cook()中,我们用 m 的 lock()函数和 unlock()函数形成了一个临界区。因为 Food 对象的创建不涉及共享资源,各个线程可以各自独立地进行,所以我们把 Food 对象的创建工作放在临界区之外进行。当 Food 对象创建完成需要添加到 quFoods 容器时,就涉及到了对共享资源 quFoods 的操作,就需要放到临界区来进行以保证任何时刻只有唯一的线程对 quFoods 进行操作。

互斥对象 mutex 的使用非常简单。我们只需要创建一个全局的 mutex 对象,然后利用其 lock()函数和 unlock()函数划定一个临界区,同时将那些对共享资源的访问放到临界区就可以保证同一时刻只有唯一的一个线程对共享资源进行访问了

try_lock()

如果一个线程只执行了 lock()函数锁定了临界区,但因为某种原因(出现异常或者是长时间被阻塞)而没有执行相应的 unlock()函数解除临界区的锁定,那么其他线程将再也无法进入临界区,从而整个程序都会被阻塞而失去响应。

利用 try_lock()函数,我们可以在锁定临界区之前进行一定的尝试,如果当前临界区可以锁定,则锁定临界区而进入临界区执行。如果当前临界区已经被其他线程锁定而无法再次锁定,则可以采取一定的措施来避免线程一直等待而形成线程的死锁

void Cook(string strName)
{
	Food food(strName);
	// 尝试锁定临界区
	if(m.try_lock())
	{
		quFoods.push(food);
		m.unlock(); // 解除锁定
	}
	else
	{
		// 在无法锁定临界区时采取的补救措施
		cout<<"服务员这会儿太忙了,炒好的菜先放放"<<endl;
	}
}

recursive_mutex

recursive_mutex 互斥对象,它可以让同一线程多次进入某一临界区,从而巧妙地解决了函数递归调用中对同一个互斥对象多次执行 lock()操作的问题。

timed_mutex

C++11 还提供了 timed_mutex 互斥对象,利用它的 try_lock_for()函数和 try_lock_until()函数,可以让线程只是在某段时间之内或某个固定时间点之内尝试锁定,这样就对线程等待锁定临界区的时间做了限制,从而避免了线程在异常情况下无法锁定临界区时还长时间地等待,也就有机会采取措施解决问题。

还有 recursive_timed_mutex

lock_guard

与其在出现临界区无法锁定的情况下采取补救措施解决问题,不如事先就管理好互斥对象,让它的 lock()和 unlock()完全配对,也就不会出现临界区无法锁定的情况了。为此, C++11 的标准库中专门地提供了锁( lock)对象用于互斥对象的管理,其中最简单也最常用的就是 lock_guard 类

lock_guard 类实际上是一个类模板,我们只需要在合适的地方(需要 mutex 互斥对象锁定的地方),首先使用需要管理的互斥对象类型(比如, mutex 或 timed_mutex 等)作为其类型参数特化这个类模板而得到一个特定类型的模板类,然后使用一个互斥对象作为其构造函数参数而创建一个 lock对象,从此这个 lock 对象与这个互斥对象建立联系, lock 对象开始对互斥对象进行管理。

当以互斥对象为参数创建 lock 对象的时候,它的构造函数会自动调用互斥对象的 lock()函数,锁定它所管理的互斥对象。而当代码执行离开 lock 对象的作用域时,作为局部变量的 lock 对象会被自动释放,而它的析构函数则会自动调用互斥对象的 unlock()函数,自动解除它所管理的互斥对象的锁定。通过 lock对象的帮助,借助其构造函数和析构函数的严格匹配,互斥对象的锁定( lock())和解锁( unlock())也同样做到了自动地严格匹配。

// 使用 lock_guard 来管理 mutex 对象
void Cook(string strName)
{
	Food food(strName);
	// 使用需要管理的 mutex 对象作为构造函数创建 lock 对象
	// 构造函数会调用 mutex 对象的 lock()函数锁定临界区
	lock_guard<mutex> lock(m);
	// m.lock(); // 不用直接调用 mutex 对象 lock()函数
	// 对共享资源的访问
	quFoods.push(food);
	// lock 对象被释放,其析构函数被自动调用,
	// 在其析构函数中,会调用 mutex 对象的 unlock()函数解除临界区的锁定
	// m.unlock();
}

在这里,我们创建了一个 lock_guard 对象 lock 对 mutex 对象 m 进行管理,当 lock 对象被创建的时候,其构造函数会调用 m 的 lock()函数锁定临界区,而当线程函数执行完毕后,作为局部变量的lock 对象会被自动释放,其析构函数也会被自动调用,而在其析构函数中, m 对象的 unlock()函数会被调用,从而随着 lock 对象的析构自动地解除了临界区的锁定。

这样,利用局部变量 lock 对象构造函数和析构函数相互匹配的特性,就自动完成了 mutex 对象的 lock()与 unlock()的匹配,很大程度上避免了因 lock()和 unlock()无法匹配而形成的线程死锁问题。

除了只提供构造函数和析构函数的 lock_guard 类之外, C++11 标准库还提供了拥有其他成员函数的 unique_lock 类,从而让我们可以对锁对象进行更多的控制。比如,我们可以利用它的 try_lock()函数尝试锁定它所管理的互斥对象,也可以用 try_lock_for()函数在某一段时间内尝试锁定等等,其使用方法与互斥对象相似。

无论是互斥对象还是锁对象,某种意义上它们都代表着某种共享资源的所有权,它们往往是一一对应的。另外对于互斥对象而言,只有当它对至少两个线程可见时,它才是有意义的,所以它往往是全局的。而对于锁对象,我们需要利用它的构造函数和析构函数来完成临界区的锁定与解锁,所以它往往是局部的。

因为共享资源的唯一性,也同样决定了互斥对象和锁对象的唯一性。所以它们都是不可以被复制的,因为共享资源只有一个,如果它们被复制,我们就无法确定到底哪一个副本应该拥有这唯一的共享资源,从而造成所属关系上的混乱。但是,它们是可以被移动的,也就是相当于我们将这个资源的所有权从一个局部环境转移或共享到了另外一个局部环境。

OpemMP

open multi-processing-是一套支持跨平台的、 用于共享内存并行系统的多线程程序设计的编译指令、函数和一些能够影响运行行为的环境变量

OpenMP 提供对并行算法的高层抽象的描述,程序员只需要通过在原始的串行代码中加入专用的编译指令来指明他们的意图, 编译器就会根据这些指令自动地创建线程、分配线程任务、处理共享资源竞争,从而几乎是全自动地完成程序的并行化。 当选择忽略代码中的这些编译指令时,或者是编译器不支持 OpenMP 时,程序又可退化为原始的串行代码

要想在程序中使用 OpenMP 非常简单,只需要在编译器选项中启用对 OpenMP 的支持(例如, gcc编译器使用-fopenmp, Visual C++编译器使用/openmp) ,并在代码中引入 OpenMP 的头文件,然后就可以在原始代码中那些可以并行执行的代码(比如,某个 for 循环,某个对数组的操作等)前加入相应的OpenMP 编译指令来将程序并行化。

// 引入 OpenMP 的头文件
#include <omp.h>
using namespace std;
void foo()
{
	// …
}
int main()
{
	// 用 pragma 指令指明这是一个可以并行执行的 for 循环
	// 编译器会根据这些指令自动创建多个线程,
	// 对 for 循环进行相应的并行处理
	#pragma omp parallel for
	for (int i = 0; i < 100; ++i)
		foo();
	return 0;
}

在这里, 我们只是简单地用一个 pragma 编译指令告诉编译器接下来的 for 循环是一个可以并行处理的for 循环,编译器就会根据程序员的这个意图表达自动地创建多个线程并行地执行这个 for 循环。

### C++ 高级编程技巧与高阶知识点 C++ 是一种功能强大且灵活的语言,其高级特性能够显著提升程序性能和开发效率。以下是几个重要的高级编程技巧和知识点: #### 1. **STL 容器优化** 标准模板库 (STL) 提供了许多高效的容器类,如 `vector`、`list` 和 `map` 等。对于 `vector` 的使用,可以通过预先分配内存来减少动态调整带来的开销。 ```cpp std::vector<int> v; v.reserve(100); // 预先分配足够的空间以避免频繁重新分配 for(int i = 0; i < 100; ++i){ v.push_back(i); } ``` 通过调用 `reserve()` 方法可以有效提高性能[^1]。 #### 2. **智能指针管理资源** 现代 C++ 推荐使用智能指针(如 `std::unique_ptr` 和 `std::shared_ptr`)代替原始指针,从而自动管理对象生命周期并防止内存泄漏。 ```cpp #include <memory> std::unique_ptr<int> ptr(new int(10)); // 当ptr超出作用域时会自动释放所指向的对象 ``` 这种方式不仅简化了代码逻辑还增强了安全性[^3]。 #### 3. **移动语义与右值引用** 引入移动语义后,可以更高效地处理临时对象或不再需要的数据结构实例转移所有权而无需复制数据本身。 ```cpp class MyClass { public: std::string data; // 移动构造函数 MyClass(MyClass&& other):data(std::move(other.data)){} }; MyClass createObject(){ return MyClass(); } auto obj = createObject(); // 使用移动而非拷贝初始化obj ``` 这减少了不必要的深拷贝操作提升了运行速度[^4]。 #### 4. **泛型编程与模板元编程** 利用模板实现通用算法或者类型无关的功能模块;进一步深入则涉及到了解编译期计算即所谓的“模板元编程”。 ```cpp template<typename T> T max(T a,T b){return (a>b)?a:b;} template<size_t N> struct Factorial{ enum{value=N*Factorial<N-1>::value}; }; template<> struct Factorial<0>{ enum{value=1}; }; static_assert(Factorial<5>::value==120,""); ``` 上述例子展示了如何定义简单的最大值比较以及基于整数常量表达式的阶乘求解过程[^4]。 #### 5. **多线程支持** 随着硬件发展单核CPU频率增长放缓转而增加核心数量成为趋势因此并发执行变得越来越重要。C++11 开始提供了内置的支持用于创建和同步多个线程之间的工作流。 ```cpp #include <thread> void threadFunction(){} int main(){ std::thread t(threadFunction); if(t.joinable())t.join();//等待子线程完成后再继续主线程流程 } ``` 此片段说明了一个基本的多线程应用案例其中包含了启动新线程及其正确关闭的方法[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值