14

When does a developer need to be concerned with the effects of garbage collection when using APIs and classes derived from the Task Parallel Library?

Can .NET Task instances go out of scope during run?, would seem to give a sense of security that you do not have to worry about keeping tasks in scope. However the question seems limited to Tasks running on the ThreadPool where they are then rooted by the ThreadPool. However, if I understand this MSDN blog post correctly, the advice from that SO question is not be generally applicable because Tasks from TaskCompletionSource are not similarly rooted.

Are direct use of TaskCompletionSource the only time of concern?
However, when consuming an API you do not know where the Task came from. Do you then need to worry about storing references to continuations in case the provided Task came from a TaskCompletionSource or some other non-rooted source?

This seems to get inconvenient and complex quickly from needing to consider whether the Task is rooted or not (are Async I/O Tasks rooted?). I am struggling to find much for information on topic but it a popular enough library I feel I should not need to be reading decompiled source code to determine if I need to worry about race-conditions with the garbage collector, so I figure I must be missing or misunderstanding something.

Community
  • 1
  • 1
vossad01
  • 11,552
  • 8
  • 56
  • 109
  • No, you don't have to worry about that. Looks to me you asked the wrong question and should have asked: "how does a Task object stay alive when I don't keep a reference myself?" – Hans Passant Aug 06 '13 at 22:01
  • 1
    Are you worried about your Task objects somehow going out of scope and getting killed before completion? Or are you worried about your Task objects hanging around forever and consuming memory unnecessarily? – Jim Mischel Aug 06 '13 at 22:15
  • @JimMischel I am more concerned about about out of scope "work" not being executed. – vossad01 Aug 06 '13 at 22:20
  • @HansPassant What is the proper approach here then? Edit this question? Close it and ask the question you state state separately? – vossad01 Aug 07 '13 at 12:57
  • The proper approach is to run a memory profiler and make a recording of your problem. Then ask a question about it and document it with the recording. My prediction is that you are not going to ask a question about it. – Hans Passant Aug 07 '13 at 13:00

2 Answers2

8

When you have uncompleted TaskCompletionSource, then there are always two options:

  1. Something might complete that TCS in the future. That means that that something holds a reference to the TCS, which means it can't get GCed.

    Normal rules still apply to that something, so you might need to worry about keeping that rooted.

  2. Nothing will ever complete that TCS. That means the TCS and its Task will likely get GCed soon, but there is no risk of work not being done (because there is no work).

Community
  • 1
  • 1
svick
  • 236,525
  • 50
  • 385
  • 514
  • Well, that _something_ which holds the TCS and is going to complete it some lucky day, may be GC'ed as well. So the same precautions apply, right? – Vlad Jan 02 '20 at 11:21
7

The only concern is when the Task was provided by a TaskCompletionSource, and whatever is supposed to utilize the TaskCompletionSource to set the result is eligible for garbage collection. Unfortunately there is nothing the consumer of the API can do in this situation unless they have access to and can hold a reference to whatever that is. Thus this is also gotcha for the provider of the API implementer in needing to be aware of this when returning such a Task.

Lacking better resources, I had to determine the above by a combination of tests (trial an error) and reading source code. However, in the absence of documentation that these are probably implementation details and could be subject to change in future releases of the .NET Framework.

Further Explanation

The Task class is sealed and it appears that TaskCompletionSource works by using a non-public API. Thus, excluding other MS APIs which could potentially use the non-public API and assuming libraries are not reflectively using Task's internals, the only implementations of concern are Task and TaskCompletionSource.

Task (not from TaskCompletionSource)

Besides those created by TaskCompletionSource, Task are created using members on Task or TaskFactory. Any started Task created by either of these methods is bound to a TaskScheduler. Since according to the Task-Based Asynchronous Pattern guidelines (excerpt) any returned task should be started, non-started is not a case a consumer needs to worry about.

According to the documentation for TaskScheduler.QueueTask on MSDN (emphasis mine):

A typical implementation would store the task in an internal data structure, which would be serviced by threads that would execute those tasks at some time in the future.

Thus, so long as the utilized TaskScheduler implementation adheres to that, the scheduler causes a reference to be maintained to the Task. This should keep the Task alive so long as the data structure used by the scheduler is alive.

The two TaskScheduler implementations built into the framework should be safe with respect to the storage of queued Tasks. One is a singleton and the other is backed by the SynchronizationContext so the queued tasks will be rooted so long as the context exists.

The base constructor for TaskScheduler registers all created TaskScheduler instances in a static list of active implementations, which should prevent any custom implementation from being garbage collected when it otherwise may have been eligible for collection. No issues should arise related to the scope of custom TaskSchedulers, unless the TaskScheduler does something uncouth in queuing tasks.

Overall, there is nothing really to worry about here.

TaskCompletionSource

TaskCompletionSources are not guaranteed to be rooted by anything.[1] Thus, there does exist potential for the TaskCompletionSource to be garbage collected before it sets the result.

Maintaining a reference to the object in which you called the Task-returning method could make a difference if the relevant objects for ensuring completion of the TaskCompletionSource are members of the object. While I cannot find any guideline for the TAP/TPL that such situations should be avoided, I hope they are clearly documented when they occur.

The Task returned by a TaskCompletionSource does not maintain a reference to the originating TaskCompletionSource, let alone whatever else is supposed to reference the TaskCompletionSource to set the result. So whether the consumer maintains a reference to the returned Task does not affect this issue.

In situations in which the objects needed for completion are scoped only to the task returning method, there is really nothing an API consumer can do to ensure correctness, and such situations should be considered a bug in the providing API.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
vossad01
  • 11,552
  • 8
  • 56
  • 109