同时使用多线程和异步。

3
我有以下代码:
myObject object1 = null;
Thread obj1Thread = new Thread(() => { object1 = _myService.GetMethod(variable1, variable2); });
obj1Thread.Start();
obj1Thread.Join();


myObject object2 = null;
Thread obj2Thread = new Thread(() => { object2 = _myService.GetMethod2(variable3, variable4); });
obj2Thread.Start();
obj2Thread.Join();

据我所知,这段代码将创建两个新线程,运行指定的方法,暂停主线程直到这两个线程完成,然后继续执行。
假设我的说法是正确的,到目前为止一切都很好。
接下来我想试试这个:
myObject object1 = null;
Thread obj1Thread = new Thread(async () => { object1 = await _myService.GetMethod(variable1, variable2); });
obj1Thread.Start();
obj1Thread.Join();


myObject object2 = null;
Thread obj2Thread = new Thread(async () => { object2 = await _myService.GetMethod2(variable3, variable4); });
obj2Thread.Start();
obj2Thread.Join();

基本上是为每个线程添加异步和等待。

编译器接受这个更改,它在本地运行似乎没有问题,但是这段代码是否正确?它是否可能在后面的过程中引起任何问题?例如,线程会混淆、无法等待、混淆结果等。

我对异步有相当好的理解,对多线程有基本的了解,我想不出为什么这不会起作用。

代码在本地运行,但我担心在服务器负载较重时会出现在本地版本中不存在的问题...


1
你为什么需要线程?异步不够吗? - tkausl
2
你的起始示例感觉不对 - 开启一个新线程来运行一些代码,然后立即暂停原始线程直到新线程退出。然后用第二个线程重复这个过程。这与直接编写两个GetMethod调用并且没有涉及线程的行为完全相同。 - Damien_The_Unbeliever
@Damien_The_Unbeliever,你能解释一下为什么吗?据我所知,新线程将执行它们的代码,直到它们达到I/O点,在此时它们将释放线程,直到I/O完成,然后完成它们的任务。你描述的情况听起来非常不同.... - Alex
1
线程在CPU密集型活动中很有意义,特别是当您尝试提供某种程度的“公平性”时。如果您的工作负载是I/O绑定(网络、磁盘、数据库),则线程可能无法帮助,反而可能会使情况变得更糟。这些类型的工作负载受益于异步结构。 - eddiewould
@Damien_The_Unbeliever 如果我不使用线程运行3个方法,它们平均需要0.85秒。如果我使用线程运行它们,平均需要0.45秒 - 都是使用相同的数据进行10次调用。如果这些调用没有在新线程上运行,这是如何解释的? - Alex
显示剩余5条评论
3个回答

3

编译器接受了这个更改并且似乎在本地运行,但是这段代码是否正确?在后面的开发中是否会出现问题,例如线程混乱、无法等待、混淆结果等等。

我对异步有相当好的理解,对多线程有基本的了解,我认为这段代码没有任何问题。

不,这段代码会给你带来问题。线程不会按照你期望的方式等待。你将一个 async void lambda 传递给了 Thread 构造函数,而该线程将在 lambda 中的 await 命令执行之前就退出了,在设置 object1/object2 变量之前。因此在 Join 后这些变量可能仍然是 null

适当的解决方案是使用异步并发,如FCin所述。(我避免在此处使用“并行”一词,以减少与Parallel类型和任务并行库的混淆)。异步并发使用Task.WhenAll

// Start both methods concurrently
var task1 = _myService.GetMethod(variable1, variable2);
var task2 = _myService.GetMethod2(variable3, variable4);

// (Asynchronously) wait for them both to complete.
await Task.WhenAll(task1, task2);

// Retrieve results.
myObject object1 = await task1;
myObject object2 = await task2;

2
您的代码可以使用一行代码实现并行和异步等待:
await Task.WhenAll(_myService.GetMethod(variable1, variable2), _myService.GetMethod2(variable3, variable4)). 

这就是你需要的全部内容。没有线程,没有加入。如果这些方法真正是I/O操作,那么就不需要线程。

一如既往,必须阅读: https://blog.stephencleary.com/2013/11/there-is-no-thread.html

如果你的方法返回不同的结果并需要赋值给变量,那么你可以这样做:

Task<int> task1 = _myService.GetMethod(variable1, variable2);
Task<string> task2 = _myService.GetMethod2(variable3, variable4);
// Both tasks started.

int a = await task1; // Wait for first task
string b = await task2; // If this already finished, it will just return the result

如果我的代码在 lambda 函数内为变量赋值,怎么办? - Alex
@Alex 然后你可以启动这两个方法,让它们并行执行,然后等待每个方法完成并将结果分配给相应的变量。 - FCin

0

你上面的代码没有利用多线程,它会阻塞第一个线程直到它完成才启动第二个线程。

obj1Thread.Join();指示你的主线程在继续之前等待obj1Thread退出。这意味着它会启动obj1Thread然后等待它完成,这意味着你将会:

  1. 创建线程1
  2. 运行线程1
  3. 退出线程1
  4. 创建线程2
  5. 运行线程2
  6. 退出线程2

你需要进行以下操作:

myObject object1 = null;
Thread obj1Thread = new Thread(async () => { object1 = await _myService.GetMethod(variable1, variable2); });
obj1Thread.Start();
myObject object2 = null;
Thread obj2Thread = new Thread(async () => { object2 = await _myService.GetMethod2(variable3, variable4); });
obj2Thread.Start();
obj1Thread.Join();
obj2Thread.Join();

你是正确的,但一般来说最好将它们作为子线程进行处理,因为几乎总会有一些可以在主线程中处理的任务。只是在这种特定情况下没有显示出来。 - Prodigle
你基本上是创建了两个新线程,然后几乎立即将它们挂起。 - FCin
取决于每个服务完成所需的时间。对于单个IO读取来说可能有点多,但如果在异步读取后涉及处理,则非常值得。再次强调,这是从一般角度回答OP的问题。 - Prodigle

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