静态方法与实例方法的区别,多线程,性能

8
您能帮助解释多个线程如何访问静态方法吗?多个线程能够同时访问静态方法吗?
对我而言,如果一个方法是静态的,那么它就是一个由所有线程共享的单一资源。因此,只有一个线程能够在同一时间使用它。我创建了一个控制台应用程序来测试这个假设。但从我的测试结果来看,我的假设是错误的。
在我的测试中,构造了许多“Worker”对象。每个“Worker”都有许多密码和密钥。每个“Worker”都有一个实例方法,用其密钥散列其密码。还有一个静态方法,其实现完全相同,唯一的区别是它是静态的。在创建所有“Worker”对象之后,开始时间被写入控制台。然后引发“DoInstanceWork”事件,并且所有“Worker”对象都将其“useInstanceMethod”排队到线程池中。当所有方法或所有“Worker”对象完成时,从开始时间计算出它们全部完成所需的时间,并将其写入控制台。然后,将开始时间设置为当前时间并引发“DoStaticWork”事件。这次,所有“Worker”对象都将其“useStaticMethod”排队到线程池中。当所有这些方法调用完成时,再次计算并写入控制台直到它们全部完成所需的时间。
我原本期望使用实例方法时所需的时间是使用静态方法时所需时间的1/8。1/8是因为我的机器有4个核心和8个虚拟线程。但实际上,使用静态方法所需的时间要稍微快一些。
这是为什么呢?在幕后发生了什么?每个线程是否都有自己的静态方法副本?
以下是控制台应用程序-
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading;

namespace bottleneckTest
{
    public delegate void workDelegate();

    class Program
    {
        static int num = 1024;
        public static DateTime start;
        static int complete = 0;
        public static event workDelegate DoInstanceWork;
        public static event workDelegate DoStaticWork;
        static bool flag = false;

        static void Main(string[] args)
        {
            List<Worker> workers = new List<Worker>();
            for( int i = 0; i < num; i++){
                workers.Add(new Worker(i, num));
            }
            start = DateTime.UtcNow;
            Console.WriteLine(start.ToString());
            DoInstanceWork();
            Console.ReadLine();
        }
        public static void Timer()
        {
            complete++;
            if (complete == num)
            {
                TimeSpan duration = DateTime.UtcNow - Program.start;
                Console.WriteLine("Duration:  {0}", duration.ToString());
                complete = 0;
                if (!flag)
                {
                    flag = true;
                    Program.start = DateTime.UtcNow;
                    DoStaticWork();
                }
            }
        }
    }
    public class Worker
    {
        int _id;
        int _num;
        KeyedHashAlgorithm hashAlgorithm;
        int keyLength;
        Random random;
        List<byte[]> _passwords;
        List<byte[]> _keys;
        List<byte[]> hashes;

        public Worker(int id, int num)
        {
            this._id = id;
            this._num = num;
            hashAlgorithm = KeyedHashAlgorithm.Create("HMACSHA256");
            keyLength = hashAlgorithm.Key.Length;
            random = new Random();
            _passwords = new List<byte[]>();
            _keys = new List<byte[]>();
            hashes = new List<byte[]>();
            for (int i = 0; i < num; i++)
            {
                byte[] key = new byte[keyLength];
                new RNGCryptoServiceProvider().GetBytes(key);
                _keys.Add(key);

                int passwordLength = random.Next(8, 20);
                byte[] password = new byte[passwordLength * 2];
                random.NextBytes(password);
                _passwords.Add(password);
            }
            Program.DoInstanceWork += new workDelegate(doInstanceWork);
            Program.DoStaticWork += new workDelegate(doStaticWork);
        } 
        public void doInstanceWork()
        {
            ThreadPool.QueueUserWorkItem(useInstanceMethod, new WorkerArgs() { num = _num, keys = _keys, passwords = _passwords });
        }
        public void doStaticWork()
        {
            ThreadPool.QueueUserWorkItem(useStaticMethod, new WorkerArgs() { num = _num, keys = _keys, passwords = _passwords });
        }
        public void useInstanceMethod(object args)
        {
            WorkerArgs workerArgs = (WorkerArgs)args;
            for (int i = 0; i < workerArgs.num; i++)
            {
                KeyedHashAlgorithm hashAlgorithm = KeyedHashAlgorithm.Create("HMACSHA256");
                hashAlgorithm.Key = workerArgs.keys[i];
                byte[] hash = hashAlgorithm.ComputeHash(workerArgs.passwords[i]);
            }
            Program.Timer();
        }
        public static void useStaticMethod(object args)
        {
            WorkerArgs workerArgs = (WorkerArgs)args;
            for (int i = 0; i < workerArgs.num; i++)
            {
                KeyedHashAlgorithm hashAlgorithm = KeyedHashAlgorithm.Create("HMACSHA256");
                hashAlgorithm.Key = workerArgs.keys[i];
                byte[] hash = hashAlgorithm.ComputeHash(workerArgs.passwords[i]);
            }
            Program.Timer();
        }
        public class WorkerArgs
        {
            public int num;
            public List<byte[]> passwords;
            public List<byte[]> keys;
        }
    }
}
5个回答

12

方法是代码 - 线程并发访问该代码没有问题,因为在运行代码时不会对代码进行修改;它是只读资源(除了抖动之外)。在多线程情况下需要小心处理的是同时访问数据(更具体地说,当修改数据是可能的时候)。无论方法是静态还是实例方法,都与其是否需要以某种方式序列化使其线程安全无关。


谢谢您关于方法只读资源的解释。 我想我的理解中存在的巨大差距是同一资源如何可以被多个线程同时读取。 这对于基本的计算机理解来说可能是相当基础的。 我想我下意识地认为一个方法在每个时钟周期只能被读取一次,但实际上它可以被多个线程多次读取。 但是,这样说读取是否只是并发出现,因为它们都发生在一个时钟周期内? 另外,方法如何读取? 是一次性全部读取还是分段读取?谢谢 - Duncan Gravill
这是一个清晰而优美的解释。数据与代码的区别刚刚从我的脑海中溜走,经过这么多年,这个答案仍然帮助着其他人。谢谢你。 :) - Harshal

4

无论是静态方法还是实例方法,在任何情况下,除非您采取明确的措施来阻止它,否则任何线程都可以在任何时间访问任何方法。

例如,您可以创建锁定以确保仅单个线程可以访问给定的方法,但C#不会为您执行此操作。

可以将其比作看电视。电视不会防止多人同时观看,只要所有观看者都想看同一部节目,就没有问题。当然,你肯定不希望电视一次只允许一个人观看,因为可能有多个人想看不同的节目,对吧?所以如果人们想看不同的节目,他们需要一些外部机制(例如有一个单独的遥控器,当前观众保留它以观看他的节目)来确保一个人在另一个人正在看的时候不会更改频道到他的节目。


3

C# 方法是"可重入"的(与大多数语言一样;我最后一次听到真正不可重入的代码是 DOS 程序),每个线程都有自己的调用堆栈,当调用方法时,该线程的调用堆栈会更新以为返回地址、调用参数、返回值、本地值等留出空间。

假设 Thread1 和 Thread2 并发调用方法 M,而 M 有一个本地整数变量 n。Thread1 的调用堆栈与 Thread2 的调用堆栈是分开的,因此 n 在两个不同的堆栈上具有两种不同的实例化。只有当 n 不在堆栈中存储而在相同的寄存器中存储时(即在共享资源中存储)并发才会成为问题。CLR(或者是 Windows?)会小心地处理,不让它引起问题,并在切换线程时清理、存储和恢复寄存器。(在存在多个 CPU 时,如何分配寄存器,如何实现锁定。这些确实是困难的问题,让人们对编译器、操作系统编写人员产生尊重感)

可重入并不能证明当两个线程同时调用相同的方法时不会发生错误:仅仅证明如果该方法不访问和更新其他共享资源,则不会发生错误。


2
当您访问实例方法时,是通过对象引用进行访问的。
当您访问静态方法时,是直接进行访问的。
因此,静态方法稍微快一点。

0
当你实例化一个类时,你并不会创建代码的副本。你有一个指向类定义的指针,并且通过它来访问代码。因此,实例方法和静态方法的访问方式是相同的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接