使用线程本地数据将一个简单的算法移植到TPL

本文关键字:简单 一个 算法 TPL 线程 数据 | 更新日期: 2023-09-27 17:58:33

我有一个非常简单的算法,可以根据斑点之间的xy距离对其进行聚类。我移植了同样的方法,将Parallel.For与线程本地数据一起使用,但结果不正确。换句话说,我可能没有正确地使用同步来隔离每个线程。

根本无法理解为什么这两种实现的结果不同。任何想法都将不胜感激。

我想发布完全可编译的代码,但所使用的对象与项目上下文的集成过于紧密。由于该算法非常简单,希望不会成为障碍。

类级别下降

/// <summary>
/// Contains the master blobl collection to be clustered.
/// </summary>
public List<Blob> Blobs { get; private set; }
/// <summary>
/// List of clusters to be computed.
/// </summary>
public List<Cluster> Clusters { get; private set; }

线性示例(工作良好)

Cluster cluster = null;
for (int i = 0; i < this.Blobs.Count; i++)
{
    cluster = new Cluster();
    cluster.Id = i;
    if (this.Blobs [i].ClusterId == 0)
    {
        cluster.Blobs.Add(this.Blobs [i], i);
        for (int j = 0; j < this.Blobs.Count; j++)
        {
            if (this.Blobs [j].ClusterId == 0)
            {
                if (this.Blobs [i].Rectangle.IntersectsWith(this.Blobs [j].Rectangle))
                {
                    cluster.Blobs.Add(this.Blobs [j], i);
                }
                else if (this.Blobs [i].Rectangle.IsCloseTo(this.Blobs [j].Rectangle, distanceThreshold))
                {
                    cluster.Blobs.Add(this.Blobs [j], i);
                }
            }
        }
    }
    if (cluster.Blobs.Count > 2)
    {
        this.Clusters.Add(cluster);
    }
}

并行端口(不正确的集群)

System.Threading.Tasks.Parallel.For<Cluster>
(
    0,
    this.Blobs.Count,
    new ParallelOptions() { MaxDegreeOfParallelism = degreeOfParallelism },
    () => new Cluster(),
    (i, loop, cluster) =>
    {
        cluster.Id = i;
        if (this.Blobs [i].ClusterId == 0)
        {
            cluster.Blobs.Add(this.Blobs [i], i);
            for (int j = 0; j < this.Blobs.Count; j++)
            {
                if (this.Blobs [j].ClusterId == 0)
                {
                    if (this.Blobs [i].Rectangle.IntersectsWith(this.Blobs [j].Rectangle))
                    {
                        cluster.Blobs.Add(this.Blobs [j], i);
                    }
                    else if (this.Blobs [i].Rectangle.IsCloseTo(this.Blobs [j].Rectangle, distanceThreshold))
                    {
                        cluster.Blobs.Add(this.Blobs [j], i);
                    }
                }
            }
        }
        return (cluster);
    },
    (cluster) =>
    {
        lock (this.Clusters)
        {
            if (cluster.Blobs.Count > 2)
            {
                this.Clusters.Add(cluster);
            }
        }
    }
);

使用线程本地数据将一个简单的算法移植到TPL

我认为您的问题是对"线程本地数据"的误解。根据Parallel.For()的文件,它是:

[…]可以在同一线程上执行的迭代之间共享的一些局部状态。

这意味着循环的某些迭代将共享相同的Cluster对象,这将导致不正确的结果。如果每次迭代都执行localInitlocalFinally,那么它们将毫无用处,因为您可以通过将它们的代码移动到循环的开始和结束来做完全相同的事情。

代理之所以存在,是因为您可以将它们用于优化。有了它们,您就不必经常访问共享状态(在本例中为this.Clusters),这可以提高性能。

如果你不需要这种优化,不要使用这两个委托,而是像这样写循环的主体:

i =>
{
    var cluster = new Cluster { Id = i };
    // rest of the loop here
    if (cluster.Blobs.Count > 2)
    {
        lock (this.Clusters)
        {
            this.Clusters.Add(cluster);
        }
    }
}

(在上面的代码中,作为优化,我还将lockif进行了切换。)

如果你认为使用线程本地数据的优化对你有用(即它实际上会加快速度),你可以使用它。但有问题的数据必须是Cluster的列表,而不仅仅是单个Cluster。类似于:

() => new List<Cluster>(),
(i, loop, clusters) =>
{
    var cluster = new Cluster { Id = i };
    // rest of the loop here
    if (cluster.Blobs.Count > 2)
        clusters.Add(cluster);
    return clusters;
},
clusters =>
{
    lock (this.Clusters)
    {
        this.Clusters.AddRange(clusters);
    }
}