是否需要 mutex() 来安全地同时访问具有 2 个线程的数组的不同元素?

Is mutex() needed to safely access different elements of an array with 2 threads at once?

本文关键字:线程 数组 元素 mutex 是否 安全 访问      更新时间:2023-10-16

我正在处理天气数据(从气象卫星检测到的闪电能量(。我编写了一个函数,该函数获取卫星数据(int(并在决定需要放置哪个元素后将其插入多维数组中。

数组为: 国际conus_grid[1180][520];

这完美地工作,但处理时间太长,所以我写了 2 个拆分数组的函数,这样我就可以使用 std::thread 运行 2 个线程。这就是麻烦发生的地方...我正在尽最大努力将我的例子保持在最低限度。

这是我访问数组的原始函数,并且工作正常。您可以看到我的两个循环来访问数组:一个是 0-1180 (x(,另一个是 0-520 (y(:

void writeCell(long double latitude, long double longitude, int energy)
{
double lat = latitude;
double lon = longitude;
for(int x=0;x<1180;x++)
{
for(int y=0;y<520;y++)
{

// Check every cell for one that matches current lat and lon selection, then write into that cell.
if(lon < conus_grid_west[x][y] && lon > conus_grid_east[x][y] && lat < conus_grid_north[x][y] && lat > conus_grid_south[x][y])
{
grid_used[x][y] = 1;
conus_grid[x][y] = conus_grid[x][y] +  energy;  // this is where it accesses the array
}       

}
}

} 

当我转换代码以利用多线程时,我创建了以下函数(基于上面的函数,替换它(。唯一的区别是它们各自只能访问阵列的一个特定部分。(正好各一半(

这首先处理 X... 0 到 590,以及 Y... 0 到 260

void writeCellT1(long double latitude, long double longitude, int energy)
{
double lat = latitude;
double lon = longitude;
for(int x=0;x<590;x++)
{
for(int y=0;y<260;y++)
{

// Check every cell for one that matches current lat and lon selection, then write into that cell.
if(lon < conus_grid_west[x][y] && lon > conus_grid_east[x][y] && lat < conus_grid_north[x][y] && lat > conus_grid_south[x][y])
{
grid_used[x][y] = 1;
conus_grid[x][y] = conus_grid[x][y] +  energy;  // this is where it accesses the array
}       

}
}

} 

第二个处理另一半 - X 是 590-1180,Y 是 260-520 :

void writeCellT2(long double latitude, long double longitude, int energy)
{
double lat = latitude;
double lon = longitude;
for(int x=590;x<1180;x++)
{
for(int y=260;y<520;y++)
{

// Check every cell for one that matches current lat and lon selection, then write into that cell.
if(lon < conus_grid_west[x][y] && lon > conus_grid_east[x][y] && lat < conus_grid_north[x][y] && lat > conus_grid_south[x][y])
{
grid_used[x][y] = 1;
conus_grid[x][y] = conus_grid[x][y] +  energy;  // this is where it accesses the array
}       

}
}

} 

程序不会崩溃,但一旦完成,数组中就会缺少数据 - 只有部分数据在那里。我很难跟踪它没有写入哪些元素,但很明显,当我有一个函数来完成此任务时,它可以工作,但是当我有多个线程访问具有 2 个函数的数组时,它不会将数据完全放入数组中。

我认为值得尝试像这样使用mutex((:

m.lock();
grid_used[x][y] = 1;
conus_grid[x][y] = conus_grid[x][y] +  energy; 
m.unlock();

但是,这也不起作用,因为它给出的结果与无法将数据写入数组相同。知道为什么会发生这种情况吗?这只是我工作的第 3 天,所以我希望这是我在教程中忽略的简单事情。

是否需要 mutex(( 来安全地同时访问具有 2 个线程的数组的不同元素?

如果不写入可能同时写入或由另一个线程读取的元素,则不需要互斥锁。

程序不会崩溃,但完成后数组中缺少数据

正如@G.M.所暗示的那样,您应该只在一个范围内拆分(在这种情况下是X(,否则您将只处理一半的单元格。一个线程处理 1/4,另一个线程处理 1/4。您应该在 X 上拆分,因为您希望每个线程处理的数据尽可能靠近。

请注意,2D 数组中的数据以行主顺序存储在内存中(这就是为什么人们通常使用[Y][X]表示法的原因(,但也可以像您一样做。在X上拆分会给一个线程一半的内存行,另一个线程提供另一半的内存行。


另一种方法是自己进行线程管理。C++17 添加了执行策略,允许您编写循环,其中循环主体可以在不同的线程中执行,通常从内部线程池中选取。将使用多少线程取决于C++实现和执行程序的硬件。

我做了一个例子,我交换了 X 和 Y,并对你正在使用的实际类型做了一些假设,我已经为它们创建了别名。

#include <algorithm> // std::for_each
#include <array>
#include <execution> // std::execution::par
#include <iostream>
#include <memory>
#include <type_traits>
// a class to keep everything together
struct conus {
static constexpr size_t y_size = 520, x_size = 1180;
// row aliases
using conus_int_row_t = std::array<int, x_size>;
using conus_bool_row_t = std::array<bool, x_size>;
using conus_real_row_t = std::array<double, x_size>;
// 2D array aliases
using conus_grid_int_t = std::array<conus_int_row_t, y_size>;
using conus_grid_bool_t = std::array<conus_bool_row_t, y_size>;
using conus_grid_real_t = std::array<conus_real_row_t, y_size>;
// a class to store the arrays
struct conus_data_t {
conus_grid_int_t conus_grid{};
conus_grid_bool_t grid_used{};
conus_grid_real_t conus_grid_west{}, conus_grid_east{},
conus_grid_north{}, conus_grid_south{};
// an iterator to be able to loop over the row number in the arrays
class iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = unsigned;
using difference_type = std::make_signed_t<value_type>;
using pointer = value_type*;
using reference = value_type&;
iterator(unsigned y = 0) : current(y) {}
iterator& operator++() {
++current;
return *this;
}
bool operator!=(const iterator& rhs) const {
return current != rhs.current;
}
unsigned operator*() { return current; }
private:
unsigned current;
};
// create iterators to use in loops
iterator begin() { return {0}; }
iterator end() { return {static_cast<unsigned>(conus_grid.size())}; }
};
// create arrays on the heap to save the stack
std::unique_ptr<conus_data_t> data = std::make_unique<conus_data_t>();
void writeCell(double lat, double lon, int energy) {
// Below is the std::execution::parallel_policy in use.
// A lambda, capturing its surrounding by reference, is called for each "y".
std::for_each(std::execution::par, data->begin(), data->end(), [&](unsigned y) {
// here we're most probably in a thread from the thread pool
// references to the rows
conus_int_row_t& row_grid = data->conus_grid[y];
conus_bool_row_t& row_used = data->grid_used[y];
conus_real_row_t& row_west = data->conus_grid_west[y];
conus_real_row_t& row_east = data->conus_grid_east[y];
conus_real_row_t& row_north = data->conus_grid_north[y];
conus_real_row_t& row_south = data->conus_grid_south[y];
for(unsigned x = 0; x < x_size; ++x) {
// Check every cell for one that matches current lat
// and lon selection, then write into that cell.
if(lon < row_west[x] && lon > row_east[x] &&
lat < row_north[x] && lat > row_south[x])
{
row_used[x] = true;
// this is where it accesses the array
row_grid[x] += energy;
}
}
});
}
};

如果在 Linux 上使用g++clang++,则必须与tbb链接(链接时添加-ltbb(。其他编译器可能具有其他库需求才能使用执行策略。Visual Studio 2019 会立即编译并链接它,如果您选择 C++17 作为您的语言。

我经常发现使用std::execution::par是一种快速且半简单的方法来加快速度,但您必须自己尝试一下,看看它是否会在您的目标机器上变得更快。