线程本地存储工作原理

时间:2017-08-15 21:35:56

标签: c# multithreading data-sharing

这是关于Apress并行编程书中的线程本地存储(TLS)的示例。我知道如果我们有4核计算机4线程可以同时并行运行。在这个例子中,我们创建10个任务,我们假设有4个核心计算机。每个Thread本地存储都存在于线程上,因此当启动10个任务并行时只执行4个线程。我们有4个TLS所以10个任务尝试更改4个本地存储对象。我想问一下,当线程数<时,Tls如何防止数据竞争问题任务计数??

using System;
using System.Threading;
using System.Threading.Tasks;
namespace Listing_04
{
    class BankAccount
    {
        public int Balance
        {
            get;
            set;
        }
    }
    class Listing_04
    {
        static void Main(string[] args)
        {
            // create the bank account instance
            BankAccount account = new BankAccount();
            // create an array of tasks
            Task<int>[] tasks = new Task<int>[10];
            // create the thread local storage
            ThreadLocal<int> tls = new ThreadLocal<int>();
            for (int i = 0; i < 10; i++)
            {
                // create a new task
                tasks[i] = new Task<int>((stateObject) =>
                {
                    // get the state object and use it
                    // to set the TLS data
                    tls.Value = (int)stateObject;
                    // enter a loop for 1000 balance updates
                    for (int j = 0; j < 1000; j++)
                    {
                        // update the TLS balance
                        tls.Value++;
                    }
                    // return the updated balance
                    return tls.Value;
                }, account.Balance);
                // start the new task
                tasks[i].Start();
            }
            // get the result from each task and add it to
            // the balance
            for (int i = 0; i < 10; i++)
            {
                account.Balance += tasks[i].Result;
            }
            // write out the counter value
            Console.WriteLine("Expected value {0}, Balance: {1}",
            10000, account.Balance);
            // wait for input before exiting
            Console.WriteLine("Press enter to finish");
            Console.ReadLine();
        }
    }
}

1 个答案:

答案 0 :(得分:3)

  

我们有4个TLS所以10个任务尝试更改4个本地存储对象

在您的示例中,您可以拥有1到10个TLS插槽。这是因为a)您没有显式管理线程,因此使用线程池执行任务,以及b)线程池根据需求随时间创建和销毁线程。

只有1000次迭代的循环几乎是瞬间完成的。因此,在线程池确定工作项已经等待足够长的时间以证明添加任何新线程之前,您的所有10个任务都可能通过线程池。但是没有保证

the documentation的一些重要部分包括以下陈述:

  

默认情况下,最小线程数设置为系统上的处理器数

  

当需求低时,线程池线程的实际数量可能低于最小值。

换句话说,在您的四核系统上,默认的最小线程数是4,但线程池中活动的实际线程数实际上可能小于该数。如果任务需要足够长的时间来执行,活动线程的数量可能会超过这个数量。

这里要记住的最重要的事情是在线程池的上下文中使用TLS几乎肯定是错误的。

当您控制线程时,您使用TLS,并且您希望线程能够将某些数据维护为该线程的私有或唯一。当你使用线程池时发生的事情的相反。即使在最简单的情况下,多个任务也可以使用相同的线程,因此最终会共享TLS。在更复杂的场景中,例如使用await时,单个任务可能会在不同的线程中执行,因此一个任务最终可能会使用不同的TLS值,具体取决于在该任务中分配给该任务的线程时刻。

  

当线程数&lt;时,Tls如何防止数据争用问题任务计数??

这取决于您正在谈论的&#34;数据竞争问题&#34;

事实是,你发布的代码充满了至少奇怪的问题,如果不是完全错误的话。例如,您传递account.Balance作为每个任务的初始值。但为什么?在创建任务之前评估此值,之后可以对其进行修改,那么传递它的重点是什么?

如果您认为在任务开始时传递的是当前值,那么这似乎也是错误的。为什么使给定任务的起始值根据已完成的任务数量以及后续循环中的计算结果而有效? (要明确的是:发生了什么......但即使是这样,也很奇怪。)

除此之外,我们还不清楚你在这里使用TLS会想到什么。当每个任务开始时,您将TLS值重新初始化为0(即您传递给account.Balance构造函数的Task<int>的值)。因此,在执行任何给定任务的上下文中,任何涉及的线程都不会看到0以外的值。一个局部变量可以完成同样的事情,没有TLS的开销,也没有混淆任何读取代码的人,并试图弄清楚为什么在没有为代码添加值时使用TLS。

那么,TLS是否解决了某种&#34;数据竞争问题&#34; ?不在这个例子中,它不会出现。因此,询问如何它是不可能回答的。它没有做到这一点,所以没有&#34;如何&#34;。


为了它的价值,我稍微修改了你的例子,以便它报告分配给任务的各个线程。我发现在我的机器上,使用的线程数在2到8之间变化。这与我的八核机器一致,由于在池初始化其他线程并为其分配任务之前,池中的第一个线程可以完成多少变化。最常见的是,我会看到第一个线程在三到五个任务之间完成,剩下的任务由剩余的单个线程处理。

在每种情况下,一旦任务启动,线程池就会创建八个线程。但大多数情况下,这些线程中至少有一个未使用,因为其他线程能够在池饱和之前完成任务。也就是说,线程池中只有管理任务的开销,在您的示例中,任务非常便宜,以至于此开销允许一个或多个线程池线程在线程池需要该线程之前完成另一个任务。

我已在下面复制了该版本。请注意,我还在试验迭代之间添加了一个延迟,以允许线程池终止它创建的线程(在我的机器上,这需要20秒,因此延迟时间硬编码...你可以看到线程在调试器中被终止输出)。

static void Main(string[] args)
{
    while (_PromptContinue())
    {
        // create the bank account instance
        BankAccount account = new BankAccount();
        // create an array of tasks
        Task<int>[] tasks = new Task<int>[10];
        // create the thread local storage
        ThreadLocal<int> tlsBalance = new ThreadLocal<int>();
        ThreadLocal<(int Id, int Count)> tlsIds = new ThreadLocal<(int, int)>(
            () => (Thread.CurrentThread.ManagedThreadId, 0), true);
        for (int i = 0; i < 10; i++)
        {
            int k = i;
            // create a new task
            tasks[i] = new Task<int>((stateObject) =>
            {
                // get the state object and use it
                // to set the TLS data
                tlsBalance.Value = (int)stateObject;
                (int id, int count) = tlsIds.Value;
                tlsIds.Value = (id, count + 1);
                Console.WriteLine($"task {k}: thread {id}, initial value {tlsBalance.Value}");
                // enter a loop for 1000 balance updates
                for (int j = 0; j < 1000; j++)
                {
                    // update the TLS balance
                    tlsBalance.Value++;
                }
                // return the updated balance
                return tlsBalance.Value;
            }, account.Balance);
            // start the new task
            tasks[i].Start();
        }

        // Make sure this thread isn't busy at all while the thread pool threads are working
        Task.WaitAll(tasks);

        // get the result from each task and add it to
        // the balance
        for (int i = 0; i < 10; i++)
        {
            account.Balance += tasks[i].Result;
        }

        // write out the counter value
        Console.WriteLine("Expected value {0}, Balance: {1}", 10000, account.Balance);
        Console.WriteLine("{0} thread ids used: {1}",
            tlsIds.Values.Count,
            string.Join(", ", tlsIds.Values.Select(t => $"{t.Id} ({t.Count})")));
        System.Diagnostics.Debug.WriteLine("done!");
        _Countdown(TimeSpan.FromSeconds(20));
    }
}

private static void _Countdown(TimeSpan delay)
{
    System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    TimeSpan remaining = delay - sw.Elapsed,
        sleepMax = TimeSpan.FromMilliseconds(250);
    int cchMax = $"{delay.TotalSeconds,2:0}".Length;
    string format = $"\r{{0,{cchMax}:0}}", previousText = null;

    while (remaining > TimeSpan.Zero)
    {
        string nextText = string.Format(format, remaining.TotalSeconds);

        if (previousText != nextText)
        {
            Console.Write(format, remaining.TotalSeconds);
            previousText = nextText;
        }
        Thread.Sleep(remaining > sleepMax ? sleepMax : remaining);
        remaining = delay - sw.Elapsed;
    }

    Console.Write(new string(' ', cchMax));
    Console.Write('\r');
}

private static bool _PromptContinue()
{
    Console.Write("Press Esc to exit, any other key to proceed: ");
    try
    {
        return Console.ReadKey(true).Key != ConsoleKey.Escape;
    }
    finally
    {
        Console.WriteLine();
    }
}