多线程学习 c
摘要 :以非常详细的方式介绍了C ++ 14多线程结构的速成课程
新的C ++多线程结构非常易于学习。 如果您熟悉C或C ++并想开始编写多线程程序,那么本文适合您!
我使用C ++ 14作为参考,但是C ++ 17也支持我所描述的内容。 我只介绍常见的构造。 阅读本文之后,您应该能够编写自己的多线程程序。
更新(2020年3月):
我为此制作了一个视频。 您可以在这里观看:
创建线程
可以通过几种方式创建线程:
- 使用函数指针
- 使用函子
- 使用lambda函数
这些方法非常相似,只有很小的区别。 接下来,我将解释每种方法及其差异。
使用函数指针
考虑以下函数,该函数采用向量参考v
,对结果acm
的参考以及向量v
中的两个索引。 该函数在beginIndex
和endIndex
之间添加所有元素。
void accumulator_function2 ( const std :: vector < int > &v, unsigned long long &acm,
unsigned int beginIndex, unsigned int endIndex)
{
acm = 0 ;
for ( unsigned int i = beginIndex; i < endIndex; ++i)
{
acm += v[i];
}
}
A function calculating the sum of all elements between beginIndex and endIndex in a vector v
现在假设您要将向量分为两部分,并在单独的线程t1
和t2
计算每个部分的总和:
//Pointer to function
{
unsigned long long acm1 = 0 ;
unsigned long long acm2 = 0 ;
std :: thread t1 (accumulator_function2, std ::ref(v),
std ::ref(acm1), 0 , v.size() / 2 ) ;
std :: thread t2 (accumulator_function2, std ::ref(v),
std ::ref(acm2), v.size() / 2 , v.size()) ;
t1.join();
t2.join();
std :: cout << "acm1: " << acm1 << endl ;
std :: cout << "acm2: " << acm2 << endl ;
std :: cout << "acm1 + acm2: " << acm1 + acm2 << endl ;
}
Creating threads using function pointers
您需要带走什么?
-
std::thread
创建一个新线程。 第一个参数是函数指针accumulator_function2
的名称。 因此,每个线程将执行此功能。 - 传递给
std::thread
构造函数的其余参数是我们需要传递给accumulator_function2
的参数。 - 重要说明:除非您将它们包装在
std::ref.
否则所有传递给accumulator_function2
参数都将按值传递std::ref.
这就是为什么我们将v
,acm1
和acm2
在std::ref
。 -
std::thread
创建的std::thread
没有返回值。 如果要返回某些内容,则应将其存储在引用传递的参数之一中,即acm
。 - 每个线程一创建就启动。
- 我们使用
join()
函数等待线程完成
使用函子
您可以使用函子来做完全相同的事情。 以下是使用函子的代码:
class CAccumulatorFunctor3
{
public :
void operator () ( const std :: vector < int > &v,
unsigned int beginIndex, unsigned int endIndex)
{
_acm = 0 ;
for ( unsigned int i = beginIndex; i < endIndex; ++i)
{
_acm += v[i];
}
}
unsigned long long _acm;
};
Functor Definition
创建线程的代码是:
//Creating Thread using Functor
{
CAccumulatorFunctor3 accumulator1 = CAccumulatorFunctor3();
CAccumulatorFunctor3 accumulator2 = CAccumulatorFunctor3();
std :: thread t1 ( std ::ref(accumulator1),
std ::ref(v), 0 , v.size() / 2 ) ;
std :: thread t2 ( std ::ref(accumulator2),
std ::ref(v), v.size() / 2 , v.size()) ;
t1.join();
t2.join();
std :: cout << "acm1: " << accumulator1._acm << endl ;
std :: cout << "acm2: " << accumulator2._acm << endl ;
std :: cout << "accumulator1._acm + accumulator2._acm : " <<
accumulator1._acm + accumulator2._acm << endl ;
}
Creating threads using functors
您需要带走什么?
一切都与函数指针非常相似,除了:
- 第一个参数是仿函数对象。
- 无需传递对函子的引用来存储结果,我们可以将其返回值存储在函子内部的成员变量中,即
_acm
。
使用Lambda函数
作为第三种选择,我们可以在lambda函数中定义每个线程,如下所示:
{unsigned long long acm1 = 0 ;
unsigned long long acm2 = 0 ;
std :: thread t1 ([&acm1, &v] {
for ( unsigned int i = 0 ; i < v.size() / 2 ; ++i)
{
acm1 += v[i];
}
}) ;
std :: thread t2 ([&acm2, &v] {
for ( unsigned int i = v.size() / 2 ; i < v.size(); ++i)
{
acm2 += v[i];
}
}) ;
t1.join();
t2.join();
std :: cout << "acm1: " << acm1 << endl ;
std :: cout << "acm2: " << acm2 << endl ;
std :: cout << "acm1 + acm2: " << acm1 + acm2 << endl ;
}
Creating threads using lambda functions
同样,所有内容都与函数指针非常相似,除了:
- 作为传递参数的替代方法,我们可以使用lambda捕获将引用传递给lambda函数。
任务,期货和承诺
作为std::thread
的替代方案,您可以使用任务。
任务的工作与线程非常相似,但是主要区别在于它们可以返回值。 因此,您可以将它们记住为定义线程的更抽象的方式,并在线程返回值时使用它们。
下面是使用任务编写的相同示例:
# include <future>
//Tasks, Future, and Promises
{
auto f1 = []( std :: vector < int > &v,
unsigned int left, unsigned int right) {
unsigned long long acm = 0 ;
for ( unsigned int i = left; i < right; ++i)
{
acm += v[i];
}
return acm;
};
auto t1 = std ::async(f1, std ::ref(v),
0 , v.size() / 2 );
auto t2 = std ::async(f1, std ::ref(v),
v.size() / 2 , v.size());
//You can do other things here!
unsigned long long acm1 = t1.get();
unsigned long long acm2 = t2.get();
std :: cout << "acm1: " << acm1 << endl ;
std :: cout << "acm2: " << acm2 << endl ;
std :: cout << "acm1 + acm2: " << acm1 + acm2 << endl ;
}
您需要带走什么?
- 使用
std::async
定义和创建任务(而不是使用std::thread
创建的std::thread
) - 从
std::async
返回的值称为std::future
。 不要被它的名字吓到。 这仅表示t1
和t2
是变量,其值将在将来分配给它们。 我们通过调用t1.get()
和t2.get()
获得它们的值 - 如果将来的值尚未准备好,则在调用
get()
之前主线程将阻塞,直到将来的值准备就绪为止(类似于join()
)。 - 请注意,我们传递给
std::async
的函数返回一个值。 该值通过称为std :: promise的类型传递。 同样,不要被它的名字吓到。 在大多数情况下,您不需要了解std::promise
细节或定义任何类型为std::promise
变量。 C ++库在后台执行此操作。 - 默认情况下,每个任务都会在创建后立即开始(有一种更改方法,我不会介绍)。
创建线程的摘要
你有它。 创建线程和我上面解释的一样简单。 您可以使用std::thread
:
- 使用函数指针
- 使用函子
- 使用lambda函数
或者,您可以使用std::async
创建task
并在std::future
获取返回值。 任务也可以使用函数指针,函子或lambda函数获得。
共享内存和共享资源
简而言之,线程在读取/写入共享内存和资源(例如文件)时应格外小心,以避免出现竞争状况。
C ++ 14提供了几种同步线程的构造,以避免此类竞争情况。
使用Mutex,lock,()和unlock()(不推荐)
以下代码显示了我们如何创建一个关键部分,以便每个线程专有地访问std::cout
:
std ::mutex g_display_mutex;
thread_function()
{
g_display_mutex.lock();
std ::thread::id this_id = std ::this_thread::get_id();
std :: cout << "My thread id is: " << this_id << endl ;
g_display_mutex.unlock();
}
您需要带走什么?
- 创建互斥量
std::mutex
- 使用
lock()
创建一个关键部分(即保证每次只能由一个线程运行lock()
- 关键部分在调用
unlock()
结束 - 每个线程都在
lock()
处等待,并且只有在该部分中没有其他线程时才进入关键部分。
尽管上述方法有效,但不建议这样做,因为:
- 这不是异常安全的方法:如果锁之前的代码生成异常,则将不会执行
unlock()
,并且我们绝不会释放可能导致死锁的互斥量 - 我们总是要小心,不要忘记调用
unlock()
使用std :: lock_guard(推荐)
不要被它的名字lock_guard
吓到。 这只是创建关键部分的一种更抽象的方式。
以下是使用lock_guard的同一关键部分:
std ::mutex g_display_mutex;
thread_function()
{
std ::lock_guard< std ::mutex> guard(g_display_mutex);
std ::thread::id this_id = std ::this_thread::get_id();
std :: cout << "From thread " << this_id << endl ;
}
critical section using lock_guard
您需要带走什么?
- 创建std :: lock_guard之后的代码将自动锁定。 不需要显式的
lock()
和unlock()
函数调用。 - 当
std::lock_guard
超出范围时,关键部分自动结束。 这使得异常安全,并且我们也不需要记住调用unlock()
-
lock_guard
仍然需要在其构造函数中使用类型为std::mutex
的变量。
我们应该创建多少个线程?
您可以根据需要创建任意数量的线程,但是如果活动线程的数量大于可用CPU核心的数量,则可能毫无意义。
为了获得最大数量的内核,您可以调用: std::thread::hardware_cuncurrency()
,如下所示:
{unsigned int c = std ::thread::hardware_concurrency();
std :: cout << " number of cores: " << c << endl ;;
}
我没有涵盖的内容
我介绍了创建线程所需的大部分内容。 还有其他一些不太常见的细节,我在这里不包括,但是您可以自己研究它们:
- std :: move
- std :: promise的详细信息
- std :: packaged_task
- 条件变量
希望这可以帮助您快速学习C ++多线程。
如果您喜欢这篇文章,请单击拍手并给我反馈。
翻译自: https://hackernoon.com/learn-c-multi-threading-in-5-minutes-8b881c92941f
多线程学习 c