任务并行库(TPL)中的线程同步

时间:2016-08-25 07:27:11

标签: c# multithreading task-parallel-library

我正在学习TPL并坚持不懈。这只是为了学习目的,我希望人们能指导我正确的方向。

我希望一次只能有一个线程访问变量sum,这样就不会被覆盖。

我的代码如下。

using System;
using System.Threading.Tasks;


class ThreadTest
{
    private Object thisLock = new Object();
    static int sum = 0;
    public void RunMe()
    {
        lock (thisLock)
        {
            sum = sum + 1;
        }
    }

    static void Main()
    {
        ThreadTest b = new ThreadTest();
        Task t1 = new Task(()=>b.RunMe());
        Task t2= new Task(() => b.RunMe());
        t1.Start();
        t2.Start();
        Task.WaitAll(t1, t2);
        Console.WriteLine(sum.ToString());
        Console.ReadLine();
    }
}

问题 - 我在这段代码中是对的吗?

问题 - 我没有锁就可以了,因为我在某个地方读到它应该避免,因为它不允许任务彼此通信。我已经看到了一些async和await的例子但是我使用的是.Net 4.0。

由于

3 个答案:

答案 0 :(得分:2)

  

我在这段代码中是对的

明智的实施是的,但理解明智不,因为你在尝试实现逻辑时混淆了新旧世界,让我试着详细解释。

  • Task t1 = new Task(()=>b.RunMe());并不像Thread API那样每次都有新线程的意思
  • Task API将调用一个线程池线程,因此很可能是两个Task对象t1,t2,在大多数情况下都会在同一个线程上执行一个短时间运行的逻辑,而且从来没有竞争条件,在尝试更新lock
  • 时需要明确的shared object
  • 防止Sum object竞争条件的更好方法是Interlocked.Increment(ref sum),这是一种在primitive types上执行基本操作的线程安全机制
  • 对于你正在做的更好的API操作类型Parallel.For,而不是创建一个单独的Task,好处是你可以运行任意数量的increment操作很少,而不是创建一个单独的任务,它会自动阻止主线程,所以你的代码应该是这样的:

    using System;
    using System.Threading.Tasks;
    
    class ThreadTest
    {    
       public static int sum;
    }
    
    static void Main()
    {
    
     Parallel.For(0, 1, i =>
    {
        // Some thread instrumentation
        Console.WriteLine("i = {0}, thread = {1}", i,
        Thread.CurrentThread.ManagedThreadId);
    
        Interlocked.Increment(ref ThreadTest.sum);
    });
    
       Console.WriteLine(ThreadTest.sum.ToString());
       Console.ReadLine();
    }
    }
    
  • 使用线程检测时,您会发现两个循环的可能性0,1managed thread id相同,因此无需像前面所述那样需要线程安全

答案 1 :(得分:1)

回答1:

这是您发布的代码的线程安全。

然而,正如Sam指出的那样,在一般情况下,这当前不是线程安全的,因为增加的字段是静态的,但锁定对象不是静态的。

这意味着可以在两个单独的线程上创建两个单独的ThreadTest实例,然后可以从这些线程调用RunMe(),并且因为每个实例都有一个单独的锁定对象,所以锁定不会工作。

这里的解决方案是使锁定对象也是静态的。

回答2:

您无需使用Interlocked.Increment()明确锁定即可执行此操作:

public void RunMe()
{
    Interlocked.Increment(ref sum);
}

答案 2 :(得分:1)

现在,我正在添加一些关于downvotes的不满意的评论,这没有理由:)。

您的方案正常运行,是经典的多线程使用方式。 经典因为使用系统锁,即锁实际上是操作系统的WinAPI锁,所以为了进行同步,代码必须设法响应操作系统并返回,当然,由于某些争用,可能会因切换线程而浪费一些时间。特别是如果您在运行给定任务的每个线程中多次访问RunMe,或者创建更多并发任务,感谢2。

请尝试查看原子操作。 对于你的场景,它可以很好地工作,Interlocked.Increment(ref sum)。 从那里你必须限制自己直接访问总和,但这不是问题,因为Increment方法返回最新结果。

另一种选择是使用SpinLock,即如果您的操作非常快。 永远不会打开某些东西,比如Console.WriteLine或任何其他系统操作或长时间运行的计算等。

这里有许多例子:

using System;
using System.Threading;
using System.Threading.Tasks;

class ThreadTest
{
    /// <summary>  DO NOT TOUCH ME DIRECTLY  </summary>
    private static int sum/* = 0 zero is default*/;
    private static int Add(int add) => Interlocked.Add(ref sum, add);
    private static int Increment() => Interlocked.Increment(ref sum);
    private static int Latest() => Interlocked.Add(ref sum, 0);
    private static void RunMe() => Increment();

    static void Main()
    {
        Task t1 = new Task(RunMe);
        Task t2 = new Task(RunMe);
        t1.Start();
        t2.Start();
        Task.WaitAll(t1, t2);
        Console.WriteLine(Latest().ToString());
        Console.ReadLine();
    }
}