请问在密集计算的任务中,能真正做到多线程“同时”运行计算么?

boyyao 2021-04-14 02:53:19
有个签名的函数。因为时常要同时签名多条数据。 假设每次签名需要10ms时间。我一次需要签名10条数据。顺序签名需要100ms。按理说我开10个TASK 同时签名 按理说如果cpu资源“足够”的情况下 TASK完成时间应该是10ms加上一点损耗。。 但是实际上 TASK完成时间有时候甚至能超过100ms(顺序运行的总和时间)。。后来去查了一下资料。发现很多语言都有线程锁。实际只是“并发”,而不是“并行”在计算。
为了验证是否硬件资源问题。我尝试用多个进程跑同样的连续单线程签名的程序。得到的结果也也似乎验证了 线程只是并发运行。(多个进程下。每个进程使用的签名时间都是10ms,当然有误差和cpu占用的损耗)。。


所以。我想请问2个问题

1,C# 能否自身做到真正的“并行”多线程执行?

2,如果无法并行处理。那么我打算用多进程方式解决。那么请问为了降低两个进程间的延迟。请问用那种多进程间通讯方式?大概查了一下。有ipc、共享内存等。。 。我是在.net core下用。。程序在windows下开发调试。在linux下运行。 也就是说最好能兼容两种系统。并且通讯延迟要越低越好。至少在毫秒内的。请问我应该着重去找哪方面的资料?

...全文
2116 点赞 收藏 22
写回复
22 条回复
shawn_yang 04月19日
单核是时间片 假并行
多核不是
回复 点赞
xiaoxiangqing 04月16日
回复 点赞
enaking 04月16日
这只是一种假并行,给人感觉是并行 ,就跟一个火星在夜晚划一条线一样的,人的感知误觉。
回复 点赞
wanghui0380 04月15日
而同步reslut,这个确有两个执行中的线程并且这两个线程还有同步关系 这个就相当于很多人最喜欢用的Thread.Join ,但是这东西的结果就是多出一个线程,而且这两线程还不是并行关系,这两线程是串行关系
回复 点赞
wanghui0380 04月15日
我觉着你最需要知道的线程本身,而不是语法本身。你好像总在绕圈 https://www.zhihu.com/question/42962803 你的知道你的线程到底在什么状态,result同步的情况他一直处于运行态,而async await 其实是本线程已死或者挂起(逻辑上我们认为他是挂起状态,但其实是线程已死,如果你在 await task 进入前,执行中,重新运行回来这3个地方输出线程id,你会发现进入前是一个id,重新回来后是另一个id,说明逻辑上好像在同一个大括号里,但实际上他被编辑器编译成了 传统的 这种回调方式) 线程1 { call线程2(参数上下文,回调方法)//call完了就死了 } 线程2 { 有结果了,修改参数上下文,Call回调方法(修改后的参数上下文) } 回调方法(参数上下文) { 对,我们这里拿到新结果了,但此时其实在线程2里,而不是线程1,线程1早就死了 } 这样你会发现就是我们说的,他拆成更小的任务,同时他在执行状态中的就1个线程。而同步reslut,这个确有两个执行中的线程并且这两个线程还有同步关系
回复 点赞
正怒月神 04月15日
引用 7 楼 boyyao 的回复:
感谢解答。。经过测试和找了一些粗浅的资料 尝试用 如下类似的方法来测试同时签名 正常情况下函数执行时间在5ms左右。我测试同时签名6条数据(cpu 6核,尽量减低cpu本身能力不足的原因,所以用6条测试)。实际 测试下来虽然比逐条运行有所改善,但是远没有达到期望的5ms+损耗就能出结果的需求。我粗浅的认为 这里的Parallel 并不是我想要的“并行”。所以想尝试多进程方案。所以想再次请教2个问题
1,我的测试是否正确?还是还有其他方案?
2,假设我想要尝试多进程方案。请推荐一种方案可以么?没接触过多进程。因为是想提高并发的签名速度,所以希望进程间的通讯延迟足够小,至少ms内其他进程能做出响应并且执行签名任务后主进程也能最快速度得到回馈。。需兼容Linux/windows平台。

Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 6 }, (i) =>
{
string tmp = 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result;
});


string tmp = 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result;
这个写法,就是同步。
改成下面的试试看
Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 6 }, async (i) =>
{
string tmp = await 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1);
});
回复 点赞
wanghui0380 04月15日
引用 15 楼 boyyao 的回复:
[quote=引用 11 楼 JDD1997 的回复:]Parallel在多核(多线程)cpu上就是真正的并行,只不过每启动其中一个任务也是需要时间的, 你别用5ms的任务试, 换用5s的试试.
额。那么按照我的要求。我只能去寻求多进程的解决方法了么?总觉得多进程可能会有点麻烦。。。Parallel的启动耗时挺多的么?。另外。请教。假设我task的数量是确定的。能否指定.Parallel的起始数量? 默认有个参数是指定最大并行数。。[/quote] 你太过纠结语法,先去看线程本身。你现在这样问其实就是线程状态问题。线程的第一,第2步状态是,建立---就绪 然后是等待操作系统调度,等待cpu调度。不是什么启动耗时挺多,而是由系统根据优先级调度,你用的task.run其实也是多核调度,他是线程池,你先进池,然后线程池调度器给你调度,线程池调度器也是支持多核的。(只是线程池比多核出的早,所以线程池对多核利用不太充分,所以后面才有Paralle,你不需要要纠结什么Paralle核线程,因为Paralle其实依旧是线程池,只是对新的多核指令集进行了优化,对,硬件厂家出多核方案后调整了指令集的,对多核特性上出来专用指令集,当然这个都不关你的事情,你只需要做好自己的事情) 做好你自己的事情 1.划分小任务 2.区分IO任务,cpu任务 3.对于IO任务,请及时释放对象和内存 4.cpu任务,在满足要求的情况,适当控制并发数量。(池 或者 带数量控制的phore信号量-------也就是12306的卖票机制,我设计目标达产多少,满产多少就发多少票,不要超卖,把东西一股脑全扔到线程) System.Threading.SemaphoreSlim SemaphoreSlim=new SemaphoreSlim(100,200); //达产100,满产200 还是小学题,一进一出,问多长时间能放完。我们说这取决与你进多少,出多少。当然我得限制进入口径,不做限制,你进水量是1000,我出水量最多190,结果当然是悲剧
回复 点赞
千梦一生 04月15日
如果是个CPU的计算机,我记得好像也需要一些特定的代码才能让线程分别并行工作起来,也就是也需要对应代码形式,否则不一定完全在并发。 以前学习时了解的,不知道有没有记错
回复 点赞
千梦一生 04月15日
我记得吧。对于单CPU来说,不存在并行,这与语言是无关的。线程的增加反而占用了计算资源。 烧水的时候同时扫地正是习以为常的并发,人是cpu,烧水是线程、扫地是线程。 *傻等水开了再去扫地、和烧水时扫地实际上人这个CPU的工作量是没有变化的。 你这个工作如果各个任务不存在阻塞/等待/...情况的话。同样算法、代码下,单CPU下不存在提高效率,反而只会增加损耗【线程切换...】 思路有三个: 增加CPU/强化CPU 优化算法 优化代码 具体我没经验,不懂,但方向是这个不会错
回复 点赞
boyyao 04月15日
引用 11 楼 JDD1997 的回复:
Parallel在多核(多线程)cpu上就是真正的并行,只不过每启动其中一个任务也是需要时间的, 你别用5ms的任务试, 换用5s的试试.
额。那么按照我的要求。我只能去寻求多进程的解决方法了么?总觉得多进程可能会有点麻烦。。。Parallel的启动耗时挺多的么?。另外。请教。假设我task的数量是确定的。能否指定.Parallel的起始数量? 默认有个参数是指定最大并行数。。
回复 点赞
boyyao 04月15日
非常抱歉。。 .Result是在发帖的时候我给特意加上了。理解不够深。因为我发现 用如下代码 会造成 Debug.WriteLine($"并行总时间{DateTime.Now - d}"); 执行的过早。。也就是没能等 Parallel.ForEach 全部执行完成就执行了这个debug输出。。 因为是测试。所以没去学习这门等待Parallel.ForEach完整完成的方法。就想当然的改成.Result 发帖询问了。。
 Debug.WriteLine("并行 Test start");
                Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 6 }, async  (i) =>
                {
                    Debug.WriteLine($"并行 {i} 开始 {DateTime.Now.ToString("mm:ss:ffff")}");
                    var tmp = await TestAsync1(1);
                });
                Debug.WriteLine($"并行总时间{DateTime.Now - d}");
引用 9 楼 wanghui0380 的回复:
string tmp = 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result; result 是一个同步阻断操作,他和真异步的区别是,此时你的线程还在运行(线程有执行绪,这个result就是说该该步操作依然在线程调度中轮询) 真异步是 await 另一个task 或者await 驱动级别的IO信号,此时本线程处于挂起状态或者完成状态,不参与线程调度轮询,直到他收到信号量或者回调才会从新执行 其实真await task 是一个状态机迁移动作 他真实描述是 (i) => { //这个新线程1 string tmp = 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result; //另启动线程,但本线程不退出,阻塞拿结果 还是这个新线程1 }); async(i) => { //这个新线程1 string tmp =await 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1). // 另启动线程2,状态迁移,当前线程退出或挂起 ,此刻实际运行 线程2,线程1已退出 或挂起 //另外线程执行完毕,状态迁移,线程2回调到此或者信号量释放线程1恢复 }); 对比真异步的化你就知道,同步拿结果。他其实是一个阻止更多并行的操作
回复 点赞
JDD1997 04月14日
Parallel在多核(多线程)cpu上就是真正的并行,只不过每启动其中一个任务也是需要时间的, 你别用5ms的任务试, 换用5s的试试.
回复 点赞
boyyao 04月14日
Sign 以上调用的是第三方库。当时以为sign中会有什么其他操作。我单独吧Sign抽离出来也是同样结果。抽离出来的代码如下。也就是sign的大概流程


public static string Sign(byte[] message, ECDsaSigner _ECDsaSigner, byte[] uncompressedPublicKey/*, ECKey _ecKey*/)
        {
            DateTime d = DateTime.Now;
            DateTime d2 = DateTime.Now;
            var byteList = new List<byte>();
            var bytePrefix = "0x19".HexToByteArray();
            var textBytePrefix = Encoding.UTF8.GetBytes("Ethereum Signed Message:\n" + message.Length);
            byteList.AddRange(bytePrefix);
            byteList.AddRange(textBytePrefix);
            byteList.AddRange(message);
            var plainMessage = byteList.ToArray();
            Console.WriteLine($"步骤:1 {(DateTime.Now - d)}");
            d = DateTime.Now;

            KeccakDigest digest = new KeccakDigest(256);
            byte[] hash = new byte[digest.GetDigestSize()];
            digest.BlockUpdate(plainMessage, 0, plainMessage.Length);
            digest.DoFinal(hash, 0);
            Console.WriteLine($"步骤:2 {(DateTime.Now - d)}");
            d = DateTime.Now;

            var tmp = _ECDsaSigner.GenerateSignature(hash);
            Console.WriteLine($"步骤:3 {(DateTime.Now - d)}");
            d = DateTime.Now;

            var tmp2 = new ECDSASignature(tmp);
            Console.WriteLine($"步骤:4 {(DateTime.Now - d)}");
            d = DateTime.Now;

            var byte_tmp = tmp2.ToDER();
            Console.WriteLine($"步骤:5 {(DateTime.Now - d)}");
            d = DateTime.Now;

            var sig = ECDSASignature.FromDER(byte_tmp);
            Console.WriteLine($"步骤:6 {(DateTime.Now - d)}");
            d = DateTime.Now;

            var ECDSAsignature = sig.MakeCanonical();
            Console.WriteLine($"步骤:7 {(DateTime.Now - d)}");
            d = DateTime.Now;

            var recId = -1;
            for (var i = 0; i < 4; i++)
            {
                ECKey rec = ECKey.RecoverFromSignature(i, ECDSAsignature, hash, false);
                if (rec != null)
                {
                    var k = rec.GetPubKey(false);
                    Console.WriteLine($"步骤:8-{i} {(DateTime.Now - d)}");
                    if (k != null && k.SequenceEqual(uncompressedPublicKey))
                    {
                        recId = i;
                        break;
                    }
                }
            }
            if (recId == -1)
                throw new Exception("Could not construct a recoverable key. This should never happen.");
            Console.WriteLine($"步骤:8 {(DateTime.Now - d)}");
            d = DateTime.Now;

            ECDSAsignature.V = new[] { (byte)(recId + 27) };

            var t2 = "0x" + ECDSAsignature.R.ToByteArrayUnsigned().ToHex().PadLeft(64, '0') +
                   ECDSAsignature.S.ToByteArrayUnsigned().ToHex().PadLeft(64, '0') +
                   ECDSAsignature.V.ToHex();


            Console.WriteLine($"步骤:9 {(DateTime.Now - d)}");
            Debug.WriteLine($"{DateTime.Now.ToString("mm:ss:ffff")} 签名总耗时 {(DateTime.Now - d2)}");
            return t2;
        }
代码中有一些检查签名耗时的一些婴儿代码。别见怪。。~~为了速度还吧一些对象提前实例化,静态化。随意参数有所改动。但是实际这些耗时都不高。 最耗时的是var tmp = _ECDsaSigner.GenerateSignature(hash); 和 ECKey rec = ECKey.RecoverFromSignature(i, ECDSAsignature, hash, false); 这两条语句。是个椭圆DSA算法。。也已经独立抽离出来。但是没能力简化和提高运算速度。。
回复 点赞
wanghui0380 04月14日
string tmp = 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result; result 是一个同步阻断操作,他和真异步的区别是,此时你的线程还在运行(线程有执行绪,这个result就是说该该步操作依然在线程调度中轮询) 真异步是 await 另一个task 或者await 驱动级别的IO信号,此时本线程处于挂起状态或者完成状态,不参与线程调度轮询,直到他收到信号量或者回调才会从新执行 其实真await task 是一个状态机迁移动作 他真实描述是 (i) => { //这个新线程1 string tmp = 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result; //另启动线程,但本线程不退出,阻塞拿结果 还是这个新线程1 }); async(i) => { //这个新线程1 string tmp =await 纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1). // 另启动线程2,状态迁移,当前线程退出或挂起 ,此刻实际运行 线程2,线程1已退出 或挂起 //另外线程执行完毕,状态迁移,线程2回调到此或者信号量释放线程1恢复 }); 对比真异步的化你就知道,同步拿结果。他其实是一个阻止更多并行的操作
回复 点赞
boyyao 04月14日
很是奇怪。我之前就用task。whenAll做的测试。测试结果不但没有减少签名时间。反而偶尔有可能比顺序总时间还增加了。 我调用的签名如下:不知道您能否帮助看下问题所在。。 using Nethereum.Signer 签名代码 static EthereumMessageSigner signer = new EthereumMessageSigner(); byte[] array = Encoding.ASCII.GetBytes(message); string tmp = signer.Sign(array, _ethPrivateKey);
引用 6 楼 wanghui0380 的回复:
 await Task.WhenAll(getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x)
我加这么多跑,结果75ms,可见他当然是并行的
回复 点赞
boyyao 04月14日
感谢解答。。经过测试和找了一些粗浅的资料 尝试用 如下类似的方法来测试同时签名 正常情况下函数执行时间在5ms左右。我测试同时签名6条数据(cpu 6核,尽量减低cpu本身能力不足的原因,所以用6条测试)。实际 测试下来虽然比逐条运行有所改善,但是远没有达到期望的5ms+损耗就能出结果的需求。我粗浅的认为 这里的Parallel 并不是我想要的“并行”。所以想尝试多进程方案。所以想再次请教2个问题 1,我的测试是否正确?还是还有其他方案? 2,假设我想要尝试多进程方案。请推荐一种方案可以么?没接触过多进程。因为是想提高并发的签名速度,所以希望进程间的通讯延迟足够小,至少ms内其他进程能做出响应并且执行签名任务后主进程也能最快速度得到回馈。。需兼容Linux/windows平台。

 Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 6 }, (i) =>
                {
                    string tmp =  纯cpu运算操作签名异步函数,是一个椭圆ECDSA算法(1).Result;
                });
回复 点赞
wanghui0380 04月14日
 await Task.WhenAll(getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x)
我加这么多跑,结果75ms,可见他当然是并行的
回复 点赞
wanghui0380 04月14日
话说,我实际测试一下,其实并没有发现你说情况 测试代码
 static async Task Main(string[] args)
        {

            string x = string.Join("", Enumerable.Repeat(1, 100));
            Stopwatch watch = new Stopwatch();
            watch.Start();


            await Task.WhenAll(getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x), getMD5(x),
                getMD5(x), getMD5(x)
            );
            watch.Stop();
            Console.WriteLine(watch.Elapsed.TotalMilliseconds);


            Console.ReadKey();
        }
        static MD5 md5 = MD5.Create();
        public static async Task<string> getMD5(string str)
        {

            using (Stream ms = await CreateStrem(str).ConfigureAwait(false))
            {

                var x = await Task<byte[]>.Run(() =>
                {

                    return md5.ComputeHash(ms);
                }).ConfigureAwait(false);
               
                return UTF8Encoding.UTF8.GetString(x);
            }

        }

        public static Task<Stream> CreateStrem(string x)
        {
            return Task<Stream>.Run(async () =>
            {
                MemoryStream ms = new MemoryStream();
                await ms.WriteAsync(UTF8Encoding.UTF8.GetBytes(x));
                ms.Seek(0, SeekOrigin.Begin);
                return (Stream) ms;


            });


        }
除了统计输出,我没有加入其他任何输出,因为控制台输出会干扰统计。 实际结果:单独运算一个和并发10个没有任何区别,我测试机跑一个22ms,跑10个还是22ms。其实22ms对于计算机来说是无压力的,进的快,走的也快(小学题:一个进水,一个出水。22ms对计算机来说近乎是进水等于出水的节奏,) ps:22ms是在控制台直接运行,不是在vs里跑的,vs跑的快要*10,因为vs启动诊断等等附加开销
回复 点赞
Eason0807 04月14日
parallel. invoke() 我记得可以这样并行执行
回复 点赞
ziqi0716 04月14日
并行计算是否可缩短时间,也要看任务本身。 单个任务如果已经将你的cpu吃满了,那么并行计算就没有什么意义了。 但是如果是以下情况,并行就有明显优势了: 任务本身不耗计算资源,但是耗时较长,如需要等待的任务,常见的浏览广告页面(比如90秒),送积分任务。假如你有1000个账号要做这个任务,顺序执行的话时间大概就是90*1000秒;并行计算必定会快非常多,优化后甚至可以无限接近90s。
回复 点赞
发动态
发帖子
C#
创建于2007-09-28

8.4w+

社区成员

64.0w+

社区内容

.NET技术 C#
社区公告
暂无公告