Search Unity

[Solved] Schedule C# job from different (non-job) thread

Discussion in 'Data Oriented Technology Stack' started by Aithoneku, Aug 20, 2018.

  1. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Currently, we cannot schedule C# job from other than main thread. I was asked to ask you whether this feature could be implemented (edit: but we do not need to schedule jobs from a job, only from a different non-job, non-main-Unity threads) in order to simplify migration from custom (managed) job system to C# job system. Or is there some supported way to do that? Edit: see end of this post for solution.

    Edit (more details about the problem):
    For clarification: I do not want to schedule job from a job, but I want to schedule job from a thread which is not job thread and which is not Unity main thread. See my post bellow for code example.

    So my use-case is:
    • Main thread: start separate thread for an Operation
    • Separate thread, executing Operation:
      • Do work which requires managed code, so it cannot be implemented as a job.
      • Do work which doesn't require managed code, so it can be implemented as a job. But currently, I cannot schedule job from this thread, because it's not main Unity thread, even if it's not job thread.
      • Do another work which requires managed code, so it cannot be implemented as a job.
    Notes:
    • This document explains why scheduling job from a job is a problem and not supported. One of the reasons is determinism (which is IMHO extremely needed for many kinds of games, so I do not argue with that) and deadlocks (which is also huge problem). So my suggestion is: job can be scheduled from any thread which is not job thread. The job can be completed only in this thread and can depend only on jobs which are scheduled from same thread. This way it's like each thread has it's own job system, but sharing actual threads for the job execution and - as far as jobs are concerned - determinism is preserved and deadlocks should be avoided.
    Edit (solution):
    Scheduling jobs from other threads is not correct solution. The correct solution is to divide the task into sub-tasks, each represented either by managed task (standard class) or job (structure deriving from IJob or similar), then schedule them and use C# job dependency system to assure that each sub-task is executed when previous one is finished. This thread discusses how to execute managed code from a job.
     
    Last edited: Aug 31, 2018
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,413
    Technically Unity Joibs manages threads utilization by it self.
    Is there any reason, you want change thread manually?
     
  3. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    331
    Last edited: Aug 20, 2018
    Aithoneku likes this.
  4. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    First, thank you for your answers.

    I guess I didn't made myself clear, so I'm try to explain myself with an example (not working code);

    Code (CSharp):
    1. public void MakeSomeWork(string file)
    2. {
    3.     Thread thread = new Thread(() => SomeWork(file));
    4.     thread.Start();
    5.  
    6.     // This is just for the example, in real application, thread instance is managed and re-used.
    7. }
    8.  
    9. private void SomeWork(string file)
    10. {
    11.     using(var stream = File.Open(file, FileMode.Open, FileAccess.Read))
    12.     {
    13.         // This is heavy operation, so it cannot be done in main thread. It uses managed code (file stream),
    14.         // so it cannot be migrated to a job.
    15.         Data loadedData = LoadData();
    16.      
    17.         // DataJob processes some data and uses burst optimization, so it cannot be executed directly (burst
    18.         // optimizations would go away).
    19.         DataJob job = new DataJob(loadedData);
    20.         job.Schedule();
    21.      
    22.         // Store job handle somewhere and fetch the results later, for example in main thread
    23.     }
    24. }
    Please note that "MakeSomeWork", "SomeWork" and "LoadData" can represent in real application complex code of thousands lines of code and many kinds of managed objects, not just file. It would be expensive (in case of file and network operations impossible) to rewrite all the used managed code for burst-compliant code.

    Thank you, I was searching for this kind of page and couldn't find it. (Even thought I do not want to schedule job from a job, but schedule job from a different thread that is not job thread nor main Unity thread.)

    That's the first solution which comes to the mind when solving this issue. However this solution was dismissed. And even if I could decide that, it adds some delay, in worst-case scenario whole frame (current project targets 30fps, so it's up to 33ms) which actually in some cases makes whole operation to take longer than solution without jobs (note: not measured with actual implementation, only estimated).
     
  5. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,413
    Other than just jobs (IJob), you have also IJobParallelFor, which allows utilize multi threading effectively. Apparently most preferable method.
     
  6. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    I apologize for being so unclear. I know about IJobParallelFor and it doesn't solve the problem. The problem is: I have an operation, which takes long time, so it cannot be done on main thread. But this operation MUST use managed code (like file, networking, etc.) so it CANNOT be implemented using jobs. But part of this operation (not always output, but for example in middle of the processing) CAN be implemented using jobs (either IJob or IJobParallelFor) and would benefit from burst optimizations.

    So I would like to implement this operation and standard C# thread which does managed-code work, then schedule job (either IJob or IJobParallelFor) and then continues. In other words to implement this:

    Code (CSharp):
    1. // This takes long time, so it's executed in a thread (System.Thread).
    2. void SomeOperation()
    3. {
    4.    // Do work using managed code, so it CANNOT be implemented as a job.
    5.    
    6.    // Do work which doesn't need managed code, so it CAN be implemented as a job.
    7.    // And this work could actually really benefit from Burst compiler optimizations.
    8.    // So create the job instance and schedule it.
    9.    
    10.    // Do some other work, which uses managed code, so it CANNOT be implemented as a job.
    11. }
    12.  
     
  7. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,413
    Thx, I got the concept now.
    I think you best bet is to wait for other guys response (if they willing). They have better expertise and may assist with the problem.
     
    Aithoneku likes this.
  8. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Okay. Anyway, thank you for your time.
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,666
    I'm not really seeing the problem, do you really need to schedule a job from a different thread to do what you want.

    If you've got data that for whatever reason must be managed, do the work in a separate thread. Wait for the callback on the main thread in a System, then process the data and create your jobs from the main thread. Wait or complete the job, and just pass the results back to the managed thread.

    In the worst case scenario, it'll be delayed 1 frame before you start the job, but if you're asynchronously waiting for your results from another thread the results can't be that time dependent so it shouldn't really matter.
     
  10. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    More like I have to.

    As I wrote earlier: that's the first solution which comes to the mind when solving this issue. However this solution was dismissed.

    Let's say the job takes little less than 33ms.

    When scheduling with jobs being able to be scheduled from any non-job thread:
    • Frame #1: schedule work
    • Work thread: do the first part in ~10ms and schedule job.
    • Job thread: do the job in ~10ms.
    • Work thread: fetch job result and do the last part in ~10ms.
    • Frame #2: fetch results.
    Using solution that job is scheduled only in main thread:
    • Frame #1: schedule work.
    • Work thread: do the first part in ~10ms and add job to the queue to be scheduled.
    • Frame #2: check the job queue and schedule the job (second part of the work).
    • Job thread: do the job in ~10ms.
    • Frame #3: fetch job result and send it to the work thread
    • Work thread: do the last part in ~10 ms.
    • Frame #4: fetch results from work thread.
    Now imagine if there are actually two jobs and between them is managed code (which is actually my case).
     
  11. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    2,370
    So first off if you have existing code that runs in managed threads, it's going to run fine in jobs also. You won't get burst and some other features but migrating large existing systems you need to do this a step at a time.

    The first step I would refactor the current code to use jobs and the job schedular. The schedular is what you want to refactor to primarily at this stage. Your jobs will likely just be one liners, calling a static method or singleton that kicks off work you are now doing in manually created threads. Your goal is leave your existing code as is as much as you can.

    Doing this will work through any areas of your code that are simply not job compatible in any way, and get it on the job schedular. Now you can start refactoring for bang for the buck stuff. Like maybe you have one area that could benefit a lot from burst and you want to refactor that.

    Bouncing back and forth from managed threads to jobs, I just wouldn't. That takes you in the opposite direction from where you want to go. At the very least don't make your situation worse. Your last post there would make it much worse.

    Another issue here is knowledge of idiomatic .Net concurrency. Starting threads manually like that example, just don't. If your code was using Task helper methods like Task.Run and continuations and even async/await, this would likely all be simpler. It sounds like you are stuck with a lot of existing code that creates threads manually. What you can't get rid of by refactoring to jobs, refactor to something like Task.Run and continuations.
     
  12. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Actually, there is little bit better solution than static method/variable; see this thread which is quite nicely solving "how to execute managed operation from job". I tested it and it works. So this part is no problem.

    And this is the issue. There is one operation which could benefit from burst a lot in one part, but this operation has following properties:
    • It's long managed code.
    • Rewriting it whole is too expensive.
    • There is one part - only this part - which would benefit from burst. Rewriting it to job structure is easy. My current task is to optimize this part - only this part - and there is not allocated time and budget for making huge changes across the current codebase.
    • In other words: there will probably NEVER be time and money to rewrite whole operation.
    That's why I created this thread - to request support of scheduling jobs from other non-job threads. My options are actually very, very simple: if the support (for scheduling jobs from any non-job thread) will be there, I implement the mentioned part as a job and start migration to job system. If it's not, the project is not going to use jobs at all, because it would be too expensive. Instead, we might to write native plugins as that would be much cheaper. That's all there is to it.

    Again, I apologize for being unclear. When I wrote this part:

    Code (CSharp):
    1. public void MakeSomeWork(string file)
    2. {
    3.     Thread thread = new Thread(() => SomeWork(file));
    4.     thread.Start();
    5.     // This is just for the example, in real application, thread instance is managed and re-used.
    6. }
    the comment means: "thread is not actually allocated each time we need to run some async operation but instead there is custom implementation using thread pool and task scheduling system which distributes operations over these threads so the whole system works similarly like task helper or C# job system with managed tasks but this is not important for the point of this (forum) thread so I write just simple example with comment that this is not the important part and the only point of the code snippet is to say that there is something to be done on separate thread".
     
  13. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    First, I apologize if I offended someone or if I was rude; I definitely didn't intent to.

    Second, I apologize for bumping; should I interpret lack of answer from Unity staff as "it's not going to happen"?
     
  14. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,671
    This discussion here:
    https://forum.unity.com/threads/c-job-system-vs-managed-threaded-code.545360/#post-3602938

    The result seemed to be that you can schedule the majority of the code via C# job system but running with all .NET code classes / reference types and the whole thing.

    You could then schedule a job on the main thread that runs part of the code in burst as a dependency to have the innerloops run faster.

    Why is that not possible to do in your case?
     
    Aithoneku likes this.
  15. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Yes, that's exactly right.

    First thing, I have to confess I absolutely didn't realize I can separate the task into three parts (managed / burst-compliant job / managed) and use the dependency system to schedule them when the whole task needs to be scheduled. My (huge) bad, I apologize for that. With that approach, it seems actually solvable.

    Second thing is, I was directly asked to make this request; the task description I received is (almost) literally: "make a request on Unity forum to implement official support of job scheduling from different threads" (and not, for example, something like "try to find the solution with smallest modification of our code").

    Anyway, thank you very much for you time and help.
     
  16. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,671
    The answer to that is, we have no intention of supporting it. It may not seem totally obvious at first. Following our principles of performance by, but it is the wrong solution to the problem, and we are not aware of any situations where it is the right solution.

    I am happy the actaul problem is solved.
     
    Aithoneku likes this.
  17. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Again, thank you very much for your time and help.

    - - -

    And to conclude this thread (for people jumping to the end): scheduling jobs from other threads is not correct solution. The correct solution is to divide the task into sub-tasks, each represented either by managed task (standard class) or job (structure deriving from IJob or similar), then schedule them and use C# job dependency system to assure that each sub-task is executed when previous one is finished. This thread discusses how to execute managed code from a job.
     
    Joachim_Ante likes this.