C#

C#

C# is a high level, statically typed, object oriented programming language developed by Microsoft. C# code usually targets Microsoft's .NET family of tools and runtimes, which include the .NET Framework and .NET Core.
Best of Crypto

Best of Crypto

1656614160

Binance Public API Connector in DotNET

This is a lightweight library that works as a connector to Binance public API

  • Supported APIs:
    • /api/*
    • /sapi/*
    • Spot Websocket Market Stream
    • Spot User Data Stream
  • Test cases and examples
  • Customizable base URL, request timeout and HTTP proxy
  • Response Metadata

Installation

dotnet add package Binance.Spot

RESTful APIs

Usage example

using System;
using System.Threading.Tasks;
using Binance.Spot;

class Program
{
    static async Task Main(string[] args)
    {
        Wallet wallet = new Wallet();

        string status = await wallet.SystemStatus();

        Console.WriteLine(status);
    }
}

Please find more examples folder to check for more endpoints.

Websocket

using System;
using System.Threading;
using System.Threading.Tasks;
using Binance.Spot;

class Program
{
    static async Task Main(string[] args)
    {
        var websocket = new MarketDataWebSocket("btcusdt@aggTrade");

        websocket.OnMessageReceived(
            (data) =>
        {
            Console.WriteLine(data);

            return Task.CompletedTask;
        }, CancellationToken.None);

        await websocket.ConnectAsync(CancellationToken.None);
    }
}

More websocket examples are available in the Examples folder

Heartbeat

Once connected, the websocket server sends a ping frame every 3 minutes and requires a response pong frame back within a 10 minutes period. This package handles the pong responses automatically.

Testnet

While /sapi/* endpoints don't have testnet environment yet, /api/* endpoints can be tested in Spot Testnet.

using Binance.Spot;

Wallet wallet = new Wallet(baseUrl: "https://testnet.binance.vision");

Base URL

If baseUrl is not provided, it defaults to https://api.binance.com.

It's recommended to pass in the baseUrl parameter, even in production as Binance provides alternative URLs in case of performance issues:

  • https://api1.binance.com
  • https://api2.binance.com
  • https://api3.binance.com

RecvWindow parameter

Additional parameter recvWindow is available for endpoints requiring signature.

It defaults to 5000 (milliseconds) and can be any value lower than 60000(milliseconds). Anything beyond the limit will result in an error response from Binance server.

using Binance.Spot;

Wallet wallet = new Wallet();

await wallet.AccountStatus(recvWindow=4000)

Timeout

The default timeout is 100,000 milliseconds (100 seconds).

Usage Example

using System;
using System.Net.Http;
using Binance.Spot;

HttpClient httpClient = new HttpClient() { 
    Timeout = TimeSpan.FromSeconds(10)
}

Wallet wallet = new Wallet(httpClient: httpClient);

Proxy

Usage Example

using System;
using System.Net;
using System.Net.Http;
using Binance.Spot;

WebProxy proxy = new WebProxy(new Uri("http://1.2.3.4:8080"));
HttpClientHandler proxyHandler = new HttpClientHandler { Proxy = proxy };
HttpClient httpClient = new HttpClient(handler: proxyHandler);

Wallet wallet = new Wallet(httpClient: httpClient);

Exceptions

There are 2 types of exceptions returned from the library:

  • Binance.Common.BinanceClientException
    • This is thrown when server returns 4XX, it's an issue from client side.
    • Properties:
      • Code - Server's error code, e.g. -1102
      • Message - Server's error message, e.g. Unknown order sent.
  • Binance.Common.BinanceServerException
    • This is thrown when server returns 5XX, it's an issue from server side.

Both exceptions inherit Binance.Common.BinanceHttpException along with the following properties:

  • StatusCode - Response http status code, e.g. 401
  • Headers - Dictionary with response headers

Logging

This library implements the .NET logging API that works with a variety of built-in and third-party logging providers.

For more information on how to configure logging in .NET, visit Microsoft's logging article

Usage Example

using System;
using System.Net;
using System.Net.Http;
using Binance.Spot;

public async Task LoggingExample(ILogger logger) {
    BinanceLoggingHandler loggingHandler = new BinanceLoggingHandler(logger: logger);
    HttpClient httpClient = new HttpClient(handler: loggingHandler);

    Wallet wallet = new Wallet(httpClient: httpClient);

    await wallet.SystemStatus();
}

Sample Output

Method: GET, RequestUri: 'https://www.binance.com/?timestamp=1631525776809&signature=f07558c98cb82bcb3556a6a21b8a8a2582bae93d0bb9604a0df72cae8c1c6642', Version: 1.1, Content: <null>, Headers: { }
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: <null>, Headers: {}
{"status": 0,"msg": "normal"}

Test Cases

dotnet test

Limitations

Futures and Vanilla Options APIs are not supported:

  • /fapi/*
  • /dapi/*
  • /vapi/*
  • Associated Websocket Market and User Data Streams

Contributing

Contributions are welcome.

If you've found a bug within this project, please open an issue to discuss what you would like to change.

If it's an issue with the API, please open a topic at Binance Developer Community

Download Details:
Author: binance
Source Code: https://github.com/binance/binance-connector-dotnet
License: MIT license

#Binance #blockchain #dotnet #csharp 

Binance Public API Connector in DotNET
Best of Crypto

Best of Crypto

1656584820

Binance API Signature Examples

Binance API document has the details of how to hash the signature. However we are still seeing users spending hours to find out why server still complains about bad signatures. In this repo, we give the example script on how to do signature.

Code in this repository should NOT be used in production.

How it works

In each language, the script will try to hash following string and should return same signature

# hashing string
timestamp=1578963600000
# and return
d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4

# hashing string
symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559
# and return
c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71

The secret is the same as NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j

Should I look into this repo

  • if you don't know what's signature
  • if you don't know how to do sha256 hashing in a language
  • if your signature can't pass the server's validation

Which language included

  • C++
  • Dotnet
  • Go
  • Java
  • NodeJS
  • PHP
  • Python
  • Ruby
  • Shell

Download Details:
Author: binance
Source Code: https://github.com/binance/binance-signature-examples
License:

#Binance #blockchain #python #csharp 

Binance API Signature Examples

Ant Design Blazor: A Set Of Enterprise-class UI Components

Ant Design Blazor

rich set of enterprise-class UI components based on Ant Design and Blazor.

✨ Features

  • 🌈 Enterprise-class UI designed for web applications.
  • 📦 A set of high-quality Blazor components out of the box.
  • 💕 Supports WebAssembly-based client-side and SignalR-based server-side UI event interaction.
  • 🎨 Supports Progressive Web Applications (PWA).
  • 🛡 Build with C#, a multi-paradigm static language for an efficient development experience.
  • ⚙️ .NET Standard 2.1/.NET 5/.NET 6 based, with direct reference to the rich .NET ecosystem.
  • 🎁 Seamless integration with existing ASP.NET Core MVC and Razor Pages projects.

🌈 Online Examples

WebAssembly static hosting examples:

🖥 Environment Support

  • Compatible with .NET Core 3.1 / .NET 5 / .NET 6.
  • Blazor WebAssembly 3.2 / .NET 5 / .NET 6 Release.
  • Supports two-way binding on the server side.
  • Supports WebAssembly static file deployment.
  • Support 4 major browsers engines, and Internet Explorer 11+ (Blazor Server only)
  • Run directly on .NET MAUI / WPF / Windows Forms and other Blazor Hybrid workloads.
  • Run directly on Electron and other Web standards-based environments.
IE / Edge
Edge / IE
Firefox
Firefox
Chrome
Chrome
Safari
Safari
Opera
Opera
Electron
Electron
Edge 16 / IE 11†522571144Chromium 57

Due to WebAssembly restriction, Blazor WebAssembly doesn't support IE browser, but Blazor Server supports IE 11† with additional polyfills. See official documentation.

From .NET 5, IE 11 is no longer officially supported. See Blazor: Updated browser support. Unofficial support is provided by Blazor.Polyfill community project.

💿 Demo

🎨 Design Specification

Regularly synchronize with Official Ant Design specifications, you can check the sync logs online.

Therefore, you can use the custom theme styles of Ant Design directly.

📦 Installation Guide

Create a new project from the dotnet new template AntDesign.Templates

We have provided the dotnet new template to create a Boilerplate project out of the box:

Pro Template

Install the template

$ dotnet new --install AntDesign.Templates

Create the Boilerplate project with the template

$ dotnet new antdesign -o MyAntDesignApp

Options for the template:

OptionsDescriptionTypeDefault
-f | --fullIf specified, generates all pages of Ant Design Proboolfalse
-ho | --hostSpecify the hosting model'wasm' | 'server' | 'hosted''wasm'
--stylesWhether use NodeJS and Less to compile your custom themes.css | lesscss
--no-restoreIf specified, skips the automatic restore of the project on createboolfalse

Import Ant Design Blazor into an existing project

Go to the project folder of the application and install the Nuget package reference

$ dotnet add package AntDesign

Register the services in Program.cs (client-side WebAssembly Blazor)

builder.Services.AddAntDesign();

or Startup.cs (server-side Blazor)

services.AddAntDesign();

Link the static files in wwwroot/index.html (client-side WebAssembly Blazor) or Pages/_Host.cshtml (server-side Blazor)

<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>

Add namespace in _Imports.razor

@using AntDesign

To display the pop-up component dynamically, you need to add the <AntContainer /> component in App.razor.

<Router AppAssembly="@typeof(MainLayout).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <Result Status="404" />
        </LayoutView>
    </NotFound>
</Router>

<AntContainer />   <-- add this component ✨

Finally, it can be referenced in the .razor component!

<Button Type="@ButtonType.Primary">Hello World!</Button>

⌨️ Local Development

  • Install .NET Core SDK 6.0.100 or later.
  • Install Node.js (only for building style files and interoperable TypeScript files)
  • Clone to local development
$ git clone git@github.com:ant-design-blazor/ant-design-blazor.git
$ cd ant-design-blazor
$ npm install
$ dotnet build ./site/AntDesign.Docs.Build/AntDesign.Docs.Build.csproj
$ npm start

Visual Studio 2022 is recommended for development.

🔗 Links

🗺 Roadmap

Check out this issue to learn about our development plans for 2020.

Download Details:
Author: ant-design-blazor
Source Code: https://github.com/ant-design-blazor/ant-design-blazor
License: MIT, Unknown licenses found

#dotnet #aps.net #csharp #blazor 

Ant Design Blazor: A Set Of Enterprise-class UI Components

Bootstrap Blazor: A Set Of Enterprise-class UI Components

Bootstrap Blazor Component

A set of enterprise-class UI components based on Bootstrap and Blazor.

Features

  • Enterprise-class UI designed for web applications.
  • A set of high-quality Blazor components out of the box.
  • Supports WebAssembly-based client-side and SignalR-based server-side UI event interaction.
  • Supports Progressive Web Applications (PWA).
  • Build with C#, a multi-paradigm static language for an efficient development experience.
  • .NET Standard 2.1 based, with direct reference to the rich .NET ecosystem.
  • Supports NET5. (Server-Side, WASM)
  • Seamless integration with existing ASP.NET Core MVC and Razor Pages projects.

Online Examples

website website

Installation Guide

Create a new project from the dotnet new template

  1. Install the template

dotnet new -i Bootstrap.Blazor.Templates::*

  1. Create the Boilerplate project with the template

dotnet new bbapp

Install Bootstrap Blazor Project Template

  1. Download Project Template

Microsoft Market link

  1. Double Click BootstrapBlazor.UITemplate.vsix

Import Bootstrap Blazor into an existing project

  1. Go to the project folder of the application and install the Nuget package reference

dotnet add package BootstrapBlazor

  1. Add the stylesheet javascripts file to your main index file - Pages/_Host.cshtml (Server) or wwwroot/index.html (WebAssembly)

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    . . .
    <link rel="stylesheet" href="_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css">
</head>
<body>
    . . .
    <script src="_framework/blazor.server.js"></script>
    <script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
</body>
</html>
  1. Open the ~/Startup.cs file in the and register the Bootstrap Blazor service:

C#

namespace BootstrapBlazorAppName
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //more code may be present here
            services.AddBootstrapBlazor();
        }

        //more code may be present here
    }
}

Visual Studio Integration

To create a new Bootstrap Blazor UI for Blazor application, use the Create New Project Wizard. The wizard detects all installed versions of Bootstrap Blazor for Blazor and lists them in the Version combobox—this enables you to start your project with the desired version. You can also get the latest version to make sure you are up to date.

  1. Get the Wizard

To use the Create New Project Wizard, install the Bootstrap Blazor UI for Blazor Visual Studio Extensions. You can get it from the:

  • Visual Studio Marketplace (for Windows)
  1. Start the Wizard

To start the wizard, use either of the following approaches

Using the Project menu:

  • Click File > New > Project.
  • Find and click the C# Blazor Application option (you can use the search, or filter by Blazor templates).
  • Follow the wizard.

Supported browsers

Mobile devices  

 ChromeFirefoxSafariAndroid Browser & WebViewMicrosoft Edge
iOSSupportedSupportedSupportedN/ASupported
AndroidSupportedSupportedN/AAndroid v5.0+ supportedSupported
Windows 10 MobileN/AN/AN/AN/ASupported

Desktop browsers

 ChromeFirefoxInternet ExplorerMicrosoft EdgeOperaSafari
MacSupportedSupportedN/AN/ASupportedSupported
LinuxSupportedSupportedN/AN/AN/AN/A
WindowsSupportedSupportedSupported, IE11+SupportedSupportedNot supported

Screenshots

Toggle Toast Upload Upload2 Bar Pei Doughnut

Contribution

  1. Fork
  2. Create Feat_xxx branch
  3. Commit
  4. Create Pull Request

Download Details:
Author: dotnetcore
Source Code: https://github.com/dotnetcore/BootstrapBlazor
License: Apache-2.0 license

#dotnet #aps.net #csharp #blazor #bootstrap

Bootstrap Blazor: A Set Of Enterprise-class UI Components

Logibit Hawk: A F# Implementation Of The Hawk Authentication Protocol

Logibit Hawk

A F# implementation of the Hawk authentication protocol. Few dependencies. No cruft. No thrown exceptions.

If this library throws an exception, report an issue - instead it uses return values that are structured instead.

paket add nuget Hawk
paket add nuget Hawk.Suave

Dependencies: { Aether, FSharp.Core, NodaTime }, nugets Hawk and Hawk.Suave.

For all API methods implemented, the full test suite for those methods has also been translated.

Sponsored by qvitoo – A.I. bookkeeping.

Usage (Suave Example)

open Logibit.Hawk
open Logibit.Hawk.Types
open Logibit.Hawk.Server

open Suave
open Suave.Http // houses submodule 'Hawk'
open Suave.Http.Successful
open Suave.Http.RequestErrors
open Suave.Types

// your own user type
type User =
  { homepage  : Uri
    realName : string }

// this is the structure that is the 'context' for Logibit.Hawk
let settings =
  // this is what the lib is looking for to verify the request
  let sampleCreds =
    { id        = "haf"
      key       = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn"
      algorithm = SHA256 }

  // the generic type param allows you to implement a generic user repository
  // for your own user type (above)
  { Settings.empty<User>() with
     // sign: UserId -> Choice<Credentials * 'a, CredsError>
     credsRepo = fun id ->
       (sampleCreds,
        { homepage = Uri("https://qvitoo.com"); realName = "Henrik" }
       )
       // no error:
       |> Choice1Of2 }

// You can compose this into the rest of the app, as it's a web part. In this
// case you're doing a Authorization header authentication
let sampleApp settings : WebPart =
  Hawk.authenticate
    settings
    Hawk.bindHeaderReq
    // in here you can put your authenticated web parts
    (fun (attr, creds, user) -> OK (sprintf "authenticated user '%s'" user.realName))
    // on failure to authenticate the request
    (fun err -> UNAUTHORIZED (err.ToString()))

// Similarly for bewits, where you want to authenticate a portion of the query
// string:
let sampleApp2 settings : WebPart =
  Hawk.authenticateBewit
    settings Hawk.bindQueryRequest
    // in here you can put your authenticated web parts
    (fun (attr, creds, user) -> OK (sprintf "authenticated user '%s'" user.realName))
    // on failure to authenticate the request
    (fun err -> UNAUTHORIZED (err.ToString()))

Currently the code is only fully documented - but not outside the code, so have a browse to the source code that you are interested in to see how the API composes.

Usage from client:

Use the .js file from src/vendor/hawk.js/lib, then you can wrap your ajax calls like this:

request.js: (using CommonJS module layout, which you can use to require it and get a function in return).

var Auth   = require('./auth.js'),
    Hawk   = require('./lib/hawk.js'),
    Logger = require('./logger.js'),
    jQuery = require('jquery');

var qt = function(str) {
  return "'" + str + "'";
}

var jqSetHawkHeader = function(opts, creds, jqXHR, settings) {
  if (typeof opts.contentType == 'undefined') {
    throw new Error('missing contentType from options');
  }

  var opts = jQuery.extend({ credentials: creds, payload: settings.data }, opts),
      // header(uri, method, options): should have options values for
      // - contentType
      // - credentials
      // - payload
      header = Hawk.client.header(settings.url, settings.type, opts); // type = HTTP-method

  if (typeof header.err !== 'undefined') {
    Logger.error('(1/2) Hawk error:', qt(header.err), 'for', method, qt(settings.url));
    Logger.error('(2/2) Using credentials', opts.credentials);
    return;
  }

  Logger.debug('(1/3)', settings.type, settings.url);
  Logger.debug('(2/3) opts:', opts);
  Logger.debug('(3/3) header:', header.field);

  jqXHR.setRequestHeader('Authorization', header.field);
};

module.exports = function (method, resource, data, opts) {
  var origin    = window.location.origin,
      creds     = Auth.getCredentials(),
      url       = origin + resource,
      opts      = jQuery.extend({
        contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
        dataType: 'html'
      }, (typeof opts !== 'undefined' ? opts : {})),
      jqOpts    = jQuery.extend({
        type:       method,
        data:       data,
        url:        url,
        beforeSend: function(xhr, s) { jqSetHawkHeader(opts, creds, xhr, s) }
      }, opts);

  return jQuery.ajax(jqOpts);
};

Changelog

Please have a look at Releases.

API

This is the public API of the library. It mimics the API of Hawk.js - the reference implementation.

Logibit.Hawk.Bewit

These functions are available to creating and verifying Bewits.

  •  generate - generate a new bewit from credentials, a uri and an optional ext field.
  •  generate' - generate a new bewit from credentials, a string uri and an optional ext field.
  •  authenticate - verify a given bewit

authenticate details

TBD - docs, in the meanwhilse, see Server authenticate (superset is validated).

Logibit.Hawk.Client

These functions are available, checked functions are implemented

  •  header - generate a request header for server to authenticate
  •  bewit - delegates to Bewit.generate
  •  authenticate - test that server response is authentic, see Response Payload Validation.
  •  message - generate an authorisation string for a message

Logibit.Hawk.Server

  •  authenticate - authenticate a request
  •  authenticatePayload - authenticate the payload of a request - assumes you first have called authenticate to get credentials. Payload Validation
  •  authenticatePayloadHash
  •  header - generate a server-header for the client to authenticate
  •  authenticateBewit - authenticate a client-supplied bewit, see Bewit Usage Example.
  •  authenticateMessage - authenticate a client-supplied message

authenticate details

How strictly does the server validate its input? Compared to reference implementation. This part is important since it will make or break the usability of your api/app. Just throwing SecurityException for any of these is not granular enough.

  •  server cannot parse header -> FaultyAuthorizationHeader
  •  server cannot find Hawk scheme in header -> FaultyAuthorizationHeader
  •  id, ts, nonce and mac (required attrs) are supplied -> MissingAttribute
  •  credential function errors -> CredsError
  •  mac doesn't match payload -> BadMac
  •  missing payload hash if payload -> MissingAttribute
  •  payload hash not matching -> BadPayloadHash of hash_given * hash_calculated
  •  nonce reused -> NonceError AlreadySeen, with in-memory cache
  •  stale timestamp -> StaleTimestamp

Hints when not under attack (in dev)

If you see CredsError, it's most likely a problem that you can't find the user with your repository function.

If you see BadMac, it means probably means you haven't fed the right parameters to authenticate. Log the input parameters, verify that host and port match (are you behind a reverse proxy?) and check that the length of the content is the same on the client as on the server.

The BadMac error comes from hashing a normalised string of these parameters:

  • hawk header version
  • type of normalisation ('header' in this case)
  • timestamp
  • nonce
  • method
  • resource (aka PathAndQuery for the constructed Uri)
  • host
  • port
  • hash value
  • ext if there is one
  • app if there is one
  • dlg if there is app and if there is one

If you see PadPayloadHash, it means that the MAC check passed, so you're probably looking at an empty byte array, or your Content-Type isn't being passed to the server properly, or the server implementation doesn't feed the correct Content-Type header (e.g. it doesn't trim the stuff after the first MimeType declaration, before the semi-colon ;).

Logibit.Hawk.Crypto

The crypto module contains functions for validating the pieces of the request.

  •  genNormStr - generate a normalised string for a request/auth data
  •  calcPayloadHash - calculates the payload hash from a given byte[]
  •  calcPayloadHash - calculates the payload hash from a given string
  •  calcHmac - calculates the HMAC for a given string

Logibit.Hawk.Types

This module contains the shared types that you should use for interacting with the above modules.

  • HttpMethod - discriminated union type of HTTP methods
  • Algo - The supported hash algorithms
  • Credentials - The credentials object used in both client and server
  • HawkAttributes - Recognised attributes in the Hawk header
  • FullAuth - A structure that represents the fully calculated hawk request data structure

This module also contains a module-per-type with lenses for that type. The lenses follow the same format as Aether recommends.

Logibit.Hawk.Logging

Types:

  • LogLevel - the level of the LogLine.
  • LogLine - this is the data structure of the logging module, this is where you feed your data.
  • Logger interface - the main interface that we can log to/into.
  • Logger module - a module that contains functions equiv. to the instance methods of the logger interface.
  • NoopLogger : Logger - the default logger, you have to replace it yourself

It's good to know that you have to construct your LogLine yourself. That LogLines with Verbose or Debug levels should be sent to the debug or verbose functions/methods of the module/interface Logger, which in turn takes functions, which are evaluated if it's the case that the logging infrastructure is indeed logging at that level.

This means that logging at that level, and computing the log lines, needs only be done if we can really do something with them.

Other APIs

There are some modules that are currently internal as to avoid conflicting with existing code. If these are made 'more coherent' or else moved to external libraries, they can be placed on their own and be made public. The modules like this are Random, Prelude, Parse.

Download Details:
Author: logibit
Source Code: https://github.com/logibit/logibit.hawk/
License: View license

#dotnet #aps.net #csharp

Logibit Hawk: A F# Implementation Of The Hawk Authentication Protocol

AspNetSaml: Very Simple SAML 2.0 Consumer Module for ASP.NET/C#

AspNetSaml

Very simple SAML 2.0 "consumer" implementation in C#.

It's a SAML client library, not a SAML server, allows adding SAML single-sign-on to your ASP.NET app, but not to provide auth services to other apps.

Installation

Consists of one short C# file you can throw into your project (or install via nuget) and start using it. It works with both ASP.NET Core and the "old" ASP.NET Framework

Usage

How SAML works?

SAML workflow has 2 steps:

  1. User is redirected to the SAML provider (where he authenticates)
  2. User is redirected back to your app, where you validate the payload

Here's how you do it (this example is for ASP.NET MVC):

1. Redirecting the user to the saml provider:

//this example is an ASP.NET MVC action method
public ActionResult Login()
{
    //TODO: specify the SAML provider url here, aka "Endpoint"
    var samlEndpoint = "http://saml-provider-that-we-use.com/login/";

    var request = new AuthRequest(
        "http://www.myapp.com", //TODO: put your app's "entity ID" here
        "http://www.myapp.com/SamlConsume" //TODO: put Assertion Consumer URL (where the provider should redirect users after authenticating)
        );

    //redirect the user to the SAML provider
    return Redirect(request.GetRedirectUrl(samlEndpoint));
}

2. User has been redirected back

User is sent back to your app - you need to validate the SAML response ("assertion") that you recieved via POST.

Here's an example of how you do it in ASP.NET MVC

//ASP.NET MVC action method... But you can easily modify the code for Web-forms etc.
public ActionResult SamlConsume()
{
    // 1. TODO: specify the certificate that your SAML provider gave you
    string samlCertificate = @"-----BEGIN CERTIFICATE-----
BLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAH123543==
-----END CERTIFICATE-----";

    // 2. Let's read the data - SAML providers usually POST it into the "SAMLResponse" var
    var samlResponse = new Response(samlCertificate, Request.Form["SAMLResponse"]);

    // 3. We're done!
    if (samlResponse.IsValid())
    {
        //WOOHOO!!! user is logged in
        username = samlResponse.GetNameID();
    }
}

Reading more attributes from the provider

SAML providers usually send more data with their response: username, first/last names etc. Here's how to get it:

if (samlResponse.IsValid())
{
    //WOOHOO!!! user is logged in

    //Some more optional stuff for you
    //let's extract username/firstname etc
    string username, email, firstname, lastname;
    try
    {
        username = samlResponse.GetNameID();
        email = samlResponse.GetEmail();
        firstname = samlResponse.GetFirstName();
        lastname = samlResponse.GetLastName();
    }
    catch(Exception ex)
    {
        //insert error handling code
        //no, really, please do
        return null;
    }

    //user has been authenticated, put your code here, like set a cookie or something...
    //or call FormsAuthentication.SetAuthCookie()
    //or call context.SignInAsync() in ASP.NET Core
    //or do something else
}

Dependencies

Depending on your .NET version, your Project should reference System.Security for .NET Framework and System.Security.Cryptography.Xml for .NET Core.

(NEW!) Nuget

I've published this to Nuget.

Install-Package AspNetSaml

This will simply add the cs-file to the root of your project.

A version of this library has been used for years in production in our helpdesk app.

Download Details:
Author: jitbit
Source Code: https://github.com/jitbit/AspNetSaml
License: Apache-2.0 license

#dotnet #aps.net #csharp 

AspNetSaml: Very Simple SAML 2.0 Consumer Module for ASP.NET/C#

Bundle Transformer: A Modular Extension for System.Web.Optimization

Bundle Transformer

Bundle Transformer - a modular extension for the System.Web.Optimization (also known as the Microsoft ASP.NET Web Optimization Framework). StyleTransformer and ScriptTransformer classes, included in the core of Bundle Transformer and implement the IBundleTransform interface. They are intended to replace the standard classes: CssMinify and JsMinify.

The main differences of the StyleTransformer and ScriptTransformer classes from a standard implementations: ability to exclude unnecessary assets when adding assets from a directory, does not produce the re-minification of pre-minified assets, support automatic transformation of relative paths to absolute in CSS code (by using the UrlRewritingCssPostProcessor), etc. These classes do not produce the minification of code in runtime, but this feature can be added by installing of minifier-modules (now available modules based on Microsoft Ajax Minifier, YUI Compressor for .NET, NUglify, Google Closure Compiler, Douglas Crockford's JSMin, Dean Edwards' Packer, Mihai Bazon's UglifyJS, Sergey Kryzhanovsky's CSSO (CSS Optimizer), WebGrease and Clean-css). In addition, you can also install translator-modules, that implement the translation of code on intermediate languages (LESS, Sass, SCSS, CoffeeScript, TypeScript, Mustache (by using Hogan) and Handlebars). Apart from this, in the Bundle Transformer there is a third type of modules - postprocessors. Postprocessors runs after translators and before minifiers. Now available following postprocessors: URL rewriting CSS postprocessor (included in core) and postprocessor-module based on the Andrey Sitnik's Autoprefixer.

This extension will help your web applications successfully pass a most part of the tests in Google PageSpeed.

NuGet Packages

Core

Translators

Postprocessors

Minifiers

Unofficial modules

Documentation

Documentation is located on the wiki of this Repo.

Release History

See the changelog.

Who's Using Bundle Transformer

If you use Bundle Transformer in some project, please send me a message so I can include it in this list:

Software

Websites

Bundle Transformer was created and is maintained by Andrey Taritsyn.

Download Details:
Author: Taritsyn
Source Code: https://github.com/Taritsyn/BundleTransformer
License: Apache-2.0 license

#dotnet #aps.net #csharp 

Bundle Transformer: A Modular Extension for System.Web.Optimization
Best of Crypto

Best of Crypto

1656507369

Binance DotNet: C# Wrapper for The Binance Exchange API

BinanceDotNet

C# Wrapper for the official Binance exchange API

Compatible with .NET 4.5.1, .NET 4.5.2, .NET 4.6.1, .NETSTANDARD2.0

This repository provides a C# wrapper for the official Binance API, and provides rate limiting features (set to 10 by 10 out the box), a IAPICacheManager interface to allow users to provide their own cache implementations, all REST endpoints covered, and a best practice solution coupled with strongly typed responses and requests. It is built on the latest .NET Framework and in .NET Core

Feel free to raise issues and Pull Request to help improve the library. If you found this API useful, and you wanted to give back feel free to sign up to Binance via my referral link here.

Documentation

Installation

The package is available in NuGet, or feel free to download: https://www.nuget.org/packages/BinanceDotNet/

Nuget PM

Install-Package BinanceDotNet

dotnet cli

dotnet add package BinanceDotNet

Donations and Contribution

Upkeep of this API takes a lot of time from answering issues and PR's on the Repository, as well as tweets and direct emails. You can donate cryptocurrency of any amount to the following wallets:

ETH: 0xd5775e2dee4b9fa9a3be5222970c04ac574e8412

Want to send something else? Just tweet me! @Glitch100

Or you can help maintain the repository! Donations of time are welcome, just hit me up and we can work on it. From answering issues, to contributing code anyone can assist.

git clone git@github.com:glitch100/BinanceDotNet.git
  • Navigate to ExampleProgram.cs
  • Add your own Private and Secret keys
  • Play around with the API

Features

  • Simple, Configurable, Extendable
  • Rate limiting, with 10 requests in 10 seconds (disabled by default)
  • log4net Interfaces supported
  • dotnet standard, dotnet core, 4.5.1, 4.6.1 support
  • Binance WebSockets
  • Unit test coverage (in progress)
  • IAPICacheManager abstraction for providing your own cache or using the build in concrete implementation. (Currently only one endpoint has caching)
  • Console app with examples ready to launch (provide API keys)

Examples

More examples are available to play around with within the repositorys Console application which can be found here. Otherwise there are some examples around utilising both WebSockets and REST API in the Usage section below.

Roadmap

Work will continue on this API wrapper over the coming months adding and extending out the number of features that the BinanceDotNet library has. Please raise issues for new features desired

  • Start building out Unit Test support - >~2.1.0
  • Provide Builder support for queries - 2.5.0~
  • Abstract out the HttpClient - 3.0.0~

Usage

Code examples below, or clone the repository and run the BinanceExchange.Console project. This repository is built off dotnet core, and runs against C# 7.1

Creating a Client

General usage just requires setting up the client with your credentials, and then calling the Client as necessary.

// Build out a client, provide a logger, and more configuration options, or even your own APIProcessor implementation
var client = new BinanceClient(new ClientConfiguration()
{
    ApiKey = "YOUR_API_KEY",
    SecretKey = "YOUR_SECRET_KEY",
});

//You an also specify symbols like this.
var desiredSymbol = TradingPairSymbols.BNBPairs.BNB_BTC,

IReponse response = await client.GetCompressedAggregateTrades(new GetCompressedAggregateTradesRequest(){
  Symbol = "BNBBTC",
  StartTime = DateTime.UtcNow().AddDays(-1),
  EndTime = Date.UtcNow(),
});

Creating a WebSocket Client

For WebSocket endpoints, just instantiate the BinanceClient, and provide it into the BinanceWebSocketClient You can use a using block or manual management.

var client = new BinanceClient(new ClientConfiguration()
{
    ApiKey = "YOUR_API_KEY",
    SecretKey = "YOUR_SECRET_KEY",
});


// Manual management
var manualWebSocketClient = new InstanceBinanceWebSocketClient(client);
var socketId = binanceWebSocketClient.ConnectToDepthWebSocket("ETHBTC", data =>
{
    System.Console.WriteLine($"DepthCall: {JsonConvert.SerializeObject(data)}");
});
manualWebSocketClient.CloseWebSocketInstance(socketId);


// Disposable and managed
using (var binanceWebSocketClient = new DisposableBinanceWebSocketClient(client))
{
    binanceWebSocketClient.ConnectToDepthWebSocket("ETHBTC", data =>
    {
        System.Console.WriteLine($"DepthCall: {JsonConvert.SerializeObject(data)}");
    });

    Thread.Sleep(180000);
}

Error Handling

The Binance API provides rich exceptions based on different error types. You can decorate calls like this if you would like to handle the various exceptions.

// Firing off a request and catching all the different exception types.
try
{
    accountTrades = await client.GetAccountTrades(new AllTradesRequest()
    {
        FromId = 352262,
        Symbol = "ETHBTC",
    });
}
catch (BinanceBadRequestException badRequestException)
{

}
catch (BinanceServerException serverException)
{

}
catch (BinanceTimeoutException timeoutException)
{

}
catch (BinanceException unknownException)
{
}

Building out a local cache per symbol from the depth WebSocket

The example is mainly 'all in one' so you can see a full runthrough of how it works. In your own implementations you may want to have a cache of only the most recent bids/asks, or perhaps will want the empty quanity/price trades.

You can also calculate volume and more from this cache. The following code is partial from the ExampleProgram.cs.

private static async Task<Dictionary<string, DepthCacheObject>> BuildLocalDepthCache(IBinanceClient client)
{
    // Code example of building out a Dictionary local cache for a symbol using deltas from the WebSocket
    var localDepthCache = new Dictionary<string, DepthCacheObject> {{ "BNBBTC", new DepthCacheObject()
    {
        Asks = new Dictionary<decimal, decimal>(),
        Bids = new Dictionary<decimal, decimal>(),
    }}};
    var bnbBtcDepthCache = localDepthCache["BNBBTC"];

    // Get Order Book, and use Cache
    var depthResults = await client.GetOrderBook("BNBBTC", true, 100);
    //Populate our depth cache
    depthResults.Asks.ForEach(a =>
    {
        if (a.Quantity != 0.00000000M)
        {
            bnbBtcDepthCache.Asks.Add(a.Price, a.Quantity);
        }
    });
    depthResults.Bids.ForEach(a =>
    {
        if (a.Quantity != 0.00000000M)
        {
            bnbBtcDepthCache.Bids.Add(a.Price, a.Quantity);
        }
    });

    // Store the last update from our result set;
    long lastUpdateId = depthResults.LastUpdateId;
    using (var binanceWebSocketClient = new DisposableBinanceWebSocketClient(client))
    {
        binanceWebSocketClient.ConnectToDepthWebSocket("BNBBTC", data =>
        {
            if (lastUpdateId < data.UpdateId)
            {
                data.BidDepthDeltas.ForEach((bd) =>
                {
                    CorrectlyUpdateDepthCache(bd, bnbBtcDepthCache.Bids);
                });
                data.AskDepthDeltas.ForEach((ad) =>
                {
                    CorrectlyUpdateDepthCache(ad, bnbBtcDepthCache.Asks);
                });
            }
            lastUpdateId = data.UpdateId;
            System.Console.Clear();
            System.Console.WriteLine($"{JsonConvert.SerializeObject(bnbBtcDepthCache, Formatting.Indented)}");
            System.Console.SetWindowPosition(0, 0);
        });

        Thread.Sleep(8000);
    }
    return localDepthCache;
}

Result Transformations

You can use the data returned from above to utilise the ResultTransformations static class, to transform data returned from the API into more meaningful, known shapes, such as Volume etc.

// This builds a local depth cache from an initial call to the API and then continues to fill
// the cache with data from the WebSocket
var localDepthCache = await BuildLocalDepthCache(client);
// Build the Buy Sell volume from the results
var volume = ResultTransformations.CalculateTradeVolumeFromDepth("BNBBTC", localDepthCache);

Download Details:
Author: binance-exchange
Source Code: https://github.com/binance-exchange/BinanceDotNet
License: MIT license

#Binance #blockchain #dotnet #csharp 

Binance DotNet: C# Wrapper for The Binance Exchange API

DotNetty: Asynchronous Event-driven Network Application Framework

DotNetty Project

DotNetty is a port of Netty, asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Use

  • Official releases are on NuGet.
  • Nightly builds are available on MyGet.

Contribute

We gladly accept community contributions.

Download Details:
Author: Azure
Source Code: https://github.com/Azure/DotNetty
License: View license

#dotnet #aps.net #csharp 

DotNetty: Asynchronous Event-driven Network Application Framework
Corey Brooks

Corey Brooks

1656386666

How to Adds an API to Your Website with ASP.NET Core

Learn C# with CSharpFritz - Add an API to your Website with C#

Fritz has built a website to manage your collection, but how can we open up the website with an API to allow other integrations?  In this session, Fritz adds an API to our website using ASP.NET Core

#aspnetcore #api #dotnet #csharp #webdev 

How to Adds an API to Your Website with ASP.NET Core
Thierry  Perret

Thierry Perret

1656378000

Qu'est-ce qu'Async, Await et Task en C#

Les performances sont primordiales lorsque vous essayez de publier sur le Web, sur mobile, sur des consoles et même sur certains PC bas de gamme. Un jeu ou une application fonctionnant à moins de 30 FPS peut être source de frustration pour les utilisateurs. Jetons un coup d'œil à certaines des choses que nous pouvons utiliser pour augmenter les performances en réduisant la charge sur le processeur.

Dans cet article, nous expliquerons ce que sont async, awaitet Tasken C # et comment les utiliser dans Unity pour gagner en performance dans votre projet. Ensuite, nous examinerons certains des packages intégrés à Unity : les coroutines, le système de tâches C# et le compilateur en rafale. Nous verrons ce qu'ils sont, comment les utiliser et comment ils augmentent les performances de votre projet.

Pour démarrer ce projet, j'utiliserai Unity 2021.3.4f1. Je n'ai testé ce code sur aucune autre version de Unity ; tous les concepts ici devraient fonctionner sur n'importe quelle version d'Unity après Unity 2019.3. Vos résultats de performances peuvent différer si vous utilisez une version plus ancienne, car Unity a apporté des améliorations significatives avec le modèle de programmation async/wait en 2021. En savoir plus à ce sujet dans le blog Unity d'Unity et .NET, quelle est la prochaine étape , en particulier la section intitulée "Modernizing the Exécution de l'unité.

J'ai créé un nouveau projet Core 2D (URP), mais vous pouvez l'utiliser dans n'importe quel type de projet que vous aimez.

J'ai un sprite que j'ai obtenu de Space Shooter (Redux, plus polices et sons) de Kenney Vleugels .

J'ai créé un préfabriqué ennemi qui contient un Sprite Render et un composant ennemi. Le composant ennemi est un MonoBehaviourqui a un Transformet un floatpour garder une trace de la position et de la vitesse de déplacement sur l'axe y :

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Qu'est -ce que async, await, et Tasksont en C#

Qu'est-ce que c'est async?

En C#, les méthodes peuvent avoir un mot- asyncclé devant elles, ce qui signifie que les méthodes sont des méthodes asynchrones. C'est juste une façon de dire au compilateur que nous voulons pouvoir exécuter du code à l'intérieur et permettre à l'appelant de cette méthode de continuer l'exécution en attendant que cette méthode se termine.

Un exemple de ceci serait la préparation d'un repas. Vous commencerez à cuire la viande, et pendant que la viande cuit et que vous attendez qu'elle se termine, vous commencerez à faire les côtés. Pendant que les aliments cuisent, vous commencez à mettre la table. Un exemple de ceci dans le code serait static async Task<Steak> MakeSteak(int number).

Unity possède également toutes sortes de méthodes intégrées que vous pouvez appeler de manière asynchrone ; voir les docs Unity pour une liste des méthodes. Avec la façon dont Unity gère la gestion de la mémoire, il utilise soit des coroutines , AsyncOperationsoit le système de tâches C# .

Qu'est-ce que c'est awaitet comment l'utiliser ?

En C#, vous pouvez attendre la fin d'une opération asynchrone en utilisant le mot- awaitclé. Ceci est utilisé à l'intérieur de toute méthode qui a le mot- asyncclé pour attendre qu'une opération continue :

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consultez les documents Microsoft pour en savoir plus sur await.

Qu'est-ce qu'un Tasket comment l'utiliser ?

A Taskest une méthode asynchrone qui effectue une seule opération et ne renvoie pas de valeur. Pour a Taskqui renvoie une valeur, nous utiliserions Task<TResult>.

Pour utiliser une tâche, nous la créons comme créer n'importe quel nouvel objet en C# : Task t1 = new Task(void Action). Ensuite, nous commençons la tâche t1.wait. Enfin, nous attendons que la tâche se termine avec t1.wait.

Il existe plusieurs façons de créer, démarrer et exécuter des tâches. Task t2 = Task.Run(void Action)va créer et démarrer une tâche. await Task.Run(void Action)créera, démarrera et attendra la fin de la tâche. Nous pouvons utiliser la méthode alternative la plus courante avec Task t3 = Task.Factory.Start(void Action).

Il existe plusieurs façons d'attendre que la tâche soit terminée. int index = Task.WaitAny(Task[])attendra la fin de toute tâche et nous donnera l'index de la tâche terminée dans le tableau. await Task.WaitAll(Task[])attendra que toutes les tâches soient terminées.

Pour plus d'informations sur les tâches, consultez les documents Microsoft .

Un exemple simpletask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Comment la tâche affecte les performances

Comparons maintenant les performances d'une tâche par rapport aux performances d'une méthode.

J'aurai besoin d'une classe statique que je peux utiliser dans toutes mes vérifications de performances. Il aura une méthode et une tâche qui simulent une opération gourmande en performances. La méthode et la tâche effectuent exactement la même opération :

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Maintenant, j'ai besoin d'un MonoBehaviourque je peux utiliser pour tester l'impact des performances sur la tâche et la méthode. Juste pour que je puisse voir un meilleur impact sur les performances, je vais prétendre que je veux exécuter cela sur dix objets de jeu différents. Je garderai également une trace du temps Updatenécessaire à l'exécution de la méthode.

Dans Update, j'obtiens l'heure de début. Si je teste la méthode, je parcours tous les objets de jeu simulés et j'appelle la méthode intensive en performances. Si je teste la tâche, je crée une nouvelle Taskboucle de tableau à travers tous les objets de jeu simulés et j'ajoute la tâche gourmande en performances au tableau de tâches. Je puis awaitpour toutes les tâches à accomplir. En dehors de la vérification du type de méthode, je mets à jour l'heure de la méthode, en la convertissant en ms. Je le connecte également.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

La méthode intensive prend environ 65 ms pour se terminer avec le jeu fonctionnant à environ 12 FPS.

Méthode intensiveMéthode intensiveMéthode intensive

La tâche intensive prend environ 4 ms et le jeu tourne à environ 200 FPS.

Tâche intensive

Essayons ceci avec mille ennemis :

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

L'affichage et le déplacement d'un millier d'ennemis avec la méthode ont pris environ 150 ms avec une fréquence d'images d'environ 7 FPS.

Mille ennemis

Afficher et déplacer un millier d'ennemis avec une tâche a pris environ 50 ms avec une fréquence d'images d'environ 30 FPS.

Affichage des ennemis en mouvement

Pourquoi pas useTasks?

Les tâches sont extrêmement performantes et réduisent la pression sur les performances de votre système. Vous pouvez même les utiliser dans plusieurs threads à l'aide de la bibliothèque parallèle de tâches (TPL).

Il y a cependant quelques inconvénients à les utiliser dans Unity. Le principal inconvénient de l'utilisation Taskdans Unity est qu'ils s'exécutent tous sur le Mainthread. Oui, nous pouvons les faire fonctionner sur d'autres threads, mais Unity fait déjà sa propre gestion des threads et de la mémoire, et vous pouvez créer des erreurs en créant plus de threads que de cœurs de processeur, ce qui entraîne une concurrence pour les ressources.

Les tâches peuvent également être difficiles à exécuter correctement et à déboguer. Lors de l'écriture du code d'origine, je me suis retrouvé avec toutes les tâches en cours d'exécution, mais aucun des ennemis ne s'est déplacé à l'écran. Il a fini par être que j'avais besoin de retourner le Task[]que j'ai créé dans le fichier Task.

Les tâches créent beaucoup de déchets qui affectent les performances. Ils n'apparaissent pas non plus dans le profileur, donc si vous en avez un qui affecte les performances, il est difficile de le retrouver. De plus, j'ai remarqué que parfois mes tâches et mes fonctions de mise à jour continuent de s'exécuter à partir d'autres scènes.

Coroutines d'unité

Selon Unity , "Une coroutine est une fonction qui peut suspendre son exécution (rendement) jusqu'à ce que l' instruction YieldInstruction donnée se termine."

Cela signifie que nous pouvons exécuter du code et attendre la fin d'une tâche avant de continuer. Cela ressemble beaucoup à une méthode asynchrone. Il utilise un type de retour IEnumeratoret we yield returnau lieu de await.

Unity a plusieurs types d' instructions de rendement que nous pouvons utiliser, c'est-à-dire , WaitForSeconds, ou .WaitForEndOfFrameWaitUntilWaitWhile

Pour démarrer les coroutines, nous avons besoin de a MonoBehaviouret utilisons le MonoBehaviour.StartCoroutine.

Pour arrêter une coroutine avant qu'elle ne se termine, nous utilisons MonoBehaviour.StopCoroutine. Lorsque vous arrêtez des coroutines, assurez-vous d'utiliser la même méthode que celle que vous avez utilisée pour la démarrer.

Les cas d'utilisation courants des coroutines dans Unity consistent à attendre que les actifs se chargent et à créer des temporisateurs de temps de recharge.

Exemple : Un chargeur de scène utilisant une coroutine

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Vérifier l'impact d'une coroutine sur les performances

Voyons comment l'utilisation d'une coroutine impacte les performances de notre projet. Je ne vais le faire qu'avec la méthode intensive en performances.

J'ai ajouté le Coroutineà l' MethodTypeénumération et aux variables pour garder une trace de son état:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

J'ai créé la coroutine. Ceci est similaire à la tâche et à la méthode gourmandes en performances que nous avons créées précédemment avec du code ajouté pour mettre à jour l'heure de la méthode :

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

Dans la Updateméthode, j'ai ajouté la vérification de la coroutine. J'ai également modifié l'heure de la méthode, mis à jour le code et ajouté du code pour arrêter la coroutine si elle était en cours d'exécution et nous avons changé le type de méthode :

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

La coroutine intensive prend environ 6 ms pour se terminer avec le jeu fonctionnant à environ 90 FPS.

Coroutine intensive

Le système de tâches C# et le compilateur en rafale

Qu'est-ce que le système de tâches C# ?

Le système de tâches C# est l'implémentation par Unity de tâches faciles à écrire, qui ne génèrent pas les déchets générés par les tâches et utilisent les threads de travail que Unity a déjà créés. Cela corrige tous les inconvénients des tâches.

Unity compare les travaux en tant que threads, mais ils disent qu'un travail effectue une tâche spécifique. Les tâches peuvent également dépendre d'autres tâches à terminer avant de s'exécuter ; cela résout le problème avec la tâche que j'avais qui ne s'est pas correctement déplacée Unitsparce qu'elle dépendait d'une autre tâche à terminer en premier.

Les dépendances de travail sont automatiquement prises en charge pour nous par Unity. Le système d'emploi dispose également d'un système de sécurité intégré principalement pour se protéger contre les conditions de course . Une mise en garde avec les travaux est qu'ils ne peuvent contenir que des variables membres qui sont soit des types blittables, soit des types NativeContainer ; c'est un inconvénient du système de sécurité.

Pour utiliser le système de travail, vous créez le travail, planifiez le travail, attendez que le travail se termine, puis utilisez les données renvoyées par le travail. Le système de tâches est nécessaire pour utiliser la pile technologique orientée données (DOTS) de Unity.

Pour plus de détails sur le système de tâches, consultez la documentation Unity .

Création d'un emploi

Pour créer un travail, vous créez un stuctqui implémente l'une des IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobest un travail de base. IJobForet IJobForParallelpermettent d'effectuer la même opération sur chaque élément d'un conteneur natif ou pour plusieurs itérations. La différence entre eux est que le IJobFor s'exécute sur un seul thread où le IJobForParallelsera divisé entre plusieurs threads.

Je vais l'utiliser IJobpour créer un travail d'opération intensive IJobForet IJobForParallelpour créer un travail qui déplacera plusieurs ennemis; c'est juste pour que nous puissions voir les différents impacts sur les performances. Ces tâches seront identiques aux tâches et méthodes que nous avons créées précédemment :

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Ajoutez les variables membres. Dans mon cas, mon IJobn'en a pas besoin. Le IJobForet IJobParallelForont besoin d'un flottant pour le temps delta actuel car les travaux n'ont pas de concept de cadre ; ils opèrent en dehors de Unity MonoBehaviour. Ils ont également besoin d'un tableau de float3pour la position et d'un tableau pour la vitesse de déplacement sur l'axe y :

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

La dernière étape consiste à mettre en œuvre la Executeméthode requise. Le IJobForet IJobForParallelles deux nécessitent un intpour l'index de l'itération actuelle que le travail est en train d'exécuter.

La différence est qu'au lieu d'accéder à l'ennemi transformet de se déplacer, nous utilisons le tableau qui se trouve dans le travail :

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Planification d'un travail

Tout d'abord, nous devons établir le travail et remplir les données des travaux :

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Ensuite, nous planifions le travail avec JobHandle jobHandle = jobData.Schedule();. La Scheduleméthode retourne un JobHandlequi peut être utilisé plus tard.

Nous ne pouvons pas planifier une tâche à partir d'une tâche. Cependant, nous pouvons créer de nouvelles tâches et remplir leurs données à partir d'une tâche. Une fois qu'une tâche a été planifiée, elle ne peut pas être interrompue.

Le travail à haute performance

J'ai créé une méthode qui crée un nouveau travail et le planifie. Il renvoie le descripteur de travail que je peux utiliser dans ma updateméthode :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

J'ai ajouté le travail à mon énumération. Ensuite, dans la Updateméthode, j'ajoute le caseà la switchsection. J'ai créé un tableau de JobHandles. Je parcours ensuite tous les objets de jeu simulés, en ajoutant une tâche planifiée pour chacun au tableau :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

Le MoveEnemyetMoveEnemyParallelJob

Ensuite, j'ai ajouté les emplois à mon énumération. Puis dans la Updateméthode, j'appelle une nouvelle MoveEnemyJobméthode, en passant le temps delta. Normalement, vous utiliseriez soit le JobForou le JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

La première chose que je fais est de définir un tableau pour les positions et un tableau pour les moveYque je transmettrai aux tâches. Je remplis ensuite ces tableaux avec les données des ennemis. Ensuite, je crée le travail et définit les données du travail en fonction du travail que je souhaite utiliser. Après cela, je planifie le travail en fonction du travail que je veux utiliser et du type de planification que je veux faire :

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Récupérer les données d'une tâche

Nous devons attendre que le travail soit terminé. Nous pouvons obtenir le statut à partir du JobHandleque nous avons utilisé lorsque nous avons programmé le travail pour le terminer. Cela attendra que le travail soit terminé avant de poursuivre l'exécution : > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Une fois le travail terminé, le NativeContainerque nous avons utilisé pour configurer le travail aura toutes les données que nous devons utiliser. Une fois que nous en avons récupéré les données, nous devons en disposer correctement.

Le travail à haute performance

C'est assez simple puisque je ne suis pas en train de lire ou d'écrire des données dans le travail. J'attends que tous les travaux programmés soient terminés, puis je supprime le Nativetableau :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

Le travail intensif prend environ 6 ms et le jeu tourne à environ 90 FPS.

Travail intensif

Le MoveEnemytravail

J'ajoute les vérifications complètes appropriées :

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Après les vérifications du type de méthode, je parcoure tous les ennemis, en définissant leurs transformpositions et moveYles données qui ont été définies dans le travail. Ensuite, je dispose correctement des tableaux natifs :

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

L'affichage et le déplacement d'un millier d'ennemis avec le travail ont pris environ 160 ms avec une fréquence d'images d'environ 7 FPS sans gain de performances.

Aucun gain de performances

L'affichage et le déplacement d'un millier d'ennemis avec un travail parallèle ont pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Travail parallèle

Qu'est-ce que le compilateur de rafales dans Unity ?

Le compilateur en rafale est un compilateur qui traduit du bytecode en code natif. L'utiliser avec le système de tâches C # améliore la qualité du code généré, vous donnant une augmentation significative des performances ainsi qu'une réduction de la consommation de la batterie sur les appareils mobiles.

Pour l'utiliser, il vous suffit d'indiquer à Unity que vous souhaitez utiliser la compilation en rafale sur le travail avec l' [BurstCompile]attribut :

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Ensuite, dans Unity, sélectionnez Jobs > Burst > Enable Completion

Activer l'achèvement

Burst est juste à temps (JIT) dans l'éditeur, ce qui signifie qu'il peut être désactivé en mode lecture. Lorsque vous construisez votre projet, c'est Ahead-Of-Time (AOT), ce qui signifie que cela doit être activé avant de construire votre projet. Vous pouvez le faire en modifiant la section Burst AOT Settings dans la fenêtre Project Settings .

Paramètres AOT en rafale

Pour plus de détails sur le compilateur de rafale, consultez la documentation Unity .

Un travail gourmand en performances avec le compilateur burst

Un travail intensif avec rafale prend environ 3 ms pour se terminer avec le jeu fonctionnant à environ 150 FPS.

Travail intensif avec rafale

Affichage et déplacement d'un millier d'ennemis, le travail avec rafale a pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Rafale 30 ms

Affichage et déplacement d'un millier d'ennemis, le travail parallèle à la rafale a pris environ 6 ms avec une fréquence d'images d'environ 80 à 90 FPS.

6 millisecondes

Conclusion

Nous pouvons utiliser Taskpour augmenter les performances de nos applications Unity, mais leur utilisation présente plusieurs inconvénients. Il est préférable d'utiliser les éléments fournis dans Unity en fonction de ce que nous voulons faire. Utilisez des coroutines si nous voulons attendre que quelque chose finisse de se charger de manière asynchrone ; nous pouvons démarrer la coroutine et ne pas arrêter l'exécution du processus de notre programme.

Nous pouvons utiliser le système de tâches C # avec le compilateur en rafale pour obtenir un gain de performances considérable sans avoir à nous soucier de toutes les tâches de gestion des threads lors de l'exécution de tâches gourmandes en processus. En utilisant les systèmes intégrés, nous sommes sûrs que cela est fait d'une manière sûre qui ne cause pas d'erreurs ou de bugs indésirables.

Les tâches s'exécutaient un peu mieux que les travaux sans utiliser le compilateur en rafale, mais cela est dû au peu de surcharge supplémentaire dans les coulisses pour tout configurer en toute sécurité pour nous. Lors de l'utilisation du compilateur de rafale, nos travaux ont effectué nos tâches. Lorsque vous avez besoin de toutes les performances supplémentaires que vous pouvez obtenir, utilisez le système de tâches C# avec burst.

Les fichiers de projet pour cela peuvent être trouvés sur mon GitHub .

Source : https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

 #csharp #async #await 

Qu'est-ce qu'Async, Await et Task en C#
曾 俊

曾 俊

1656375600

C# 中的 Async、Await 和 Task 是什么

当您尝试发布到 Web、移动设备、控制台甚至一些低端 PC 时,性能就是一切。以低于 30 FPS 的速度运行的游戏或应用程序可能会让用户感到沮丧。让我们看一下可以通过减少 CPU 负载来提高性能的一些方法。

在这篇文章中,我们将介绍 C# 中的 、 和 是什么async以及await如何Task在 Unity 中使用它们来提高项目的性能。接下来,我们将看一下 Unity 的一些内置包:协程、C# 作业系统和突发编译器。我们将了解它们是什么、如何使用它们以及它们如何提高项目的性能。

为了启动这个项目,我将使用 Unity 2021.3.4f1。我没有在任何其他版本的 Unity 上测试过这段代码;这里的所有概念都应该适用于 Unity 2019.3 之后的任何 Unity 版本。如果使用旧版本,您的性能结果可能会有所不同,因为 Unity 在 2021 年确实对 async/await 编程模型进行了一些重大改进。在 Unity 的博客Unity and .NET, what's next中阅读有关它的更多信息,特别是标有“现代化Unity 运行时。”

我创建了一个新的 2D (URP) Core 项目,但您可以在任何您喜欢的项目中使用它。

我有一个来自Kenney Vleugels的 Space Shooter(Redux,加上字体和声音)的精灵。

我创建了一个包含 Sprite Render 和 Enemy 组件的敌人预制件。Enemy 组件是MonoBehaviour具有 aTransform和 a 的 afloat来跟踪在 y 轴上移动的位置和速度:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

C# 中的async,await​​ 和是什么Task

是什么async

在 C# 中,方法async前面可以有一个关键字,表示方法是异步方法。这只是告诉编译器我们希望能够在其中执行代码并允许该方法的调用者在等待该方法完成时继续执行的一种方式。

这方面的一个例子是做饭。您将开始烹饪肉,当肉在烹饪并且您正在等待它完成时,您将开始制作侧面。当食物在烹饪时,你会开始摆桌子。代码中的一个示例是static async Task<Steak> MakeSteak(int number).

Unity 还有各种可以异步调用的内置方法;有关方法列表,请参阅Unity 文档。通过 Unity 处理内存管理的方式,它使用协程AsyncOperationC# Job System

它是什么await以及如何使用它?

await在 C# 中,您可以使用关键字等待异步操作完成。这在任何具有async关键字等待操作继续的方法中使用:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

有关更多信息,请参阅Microsoft 文档await

什么是 aTask以及如何使用它?

ATask是一种异步方法,它执行单个操作并且不返回值。对于Task返回值的 a,我们将使用Task<TResult>.

要使用任务,我们创建它就像在 C# 中创建任何新对象一样:Task t1 = new Task(void Action)。接下来,我们开始任务t1.wait。最后,我们等待任务完成t1.wait

有多种方法可以创建、启动和运行任务。Task t2 = Task.Run(void Action)将创建并启动一个任务。await Task.Run(void Action)将创建、启动并等待任务完成。我们可以使用最常见的替代方式Task t3 = Task.Factory.Start(void Action)

我们可以通过多种方式等待 Task 完成。int index = Task.WaitAny(Task[])将等待任何任务完成并为我们提供数组中已完成任务的索引。await Task.WaitAll(Task[])将等待所有任务完成。

有关任务的更多信息,请参阅Microsoft 文档

一个简单的task例子

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

任务如何影响绩效

现在让我们比较一个任务的性能和一个方法的性能。

我需要一个可以在所有性能检查中使用的静态类。它将具有模拟性能密集型操作的方法和任务。方法和任务都执行相同的操作:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

现在我需要一个MonoBehaviour可以用来测试对任务和方法的性能影响。为了更好地看到对性能的影响,我将假装我想在十个不同的游戏对象上运行它。我还将跟踪Update方法运行所需的时间。

Update中,我得到了开始时间。如果我正在测试该方法,我会遍历所有模拟的游戏对象并调用性能密集型方法。如果我正在测试任务,我会创建一个Task遍历所有模拟游戏对象的新数组循环,并将性能密集型任务添加到任务数组中。然后我await为所有的任务完成。在方法类型检查之外,我更新方法时间,将其转换为ms. 我也记录下来。

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

强化方法大约需要 65 毫秒才能完成,游戏以大约 12 FPS 的速度运行。

强化方法

密集型任务大约需要 4 毫秒才能完成,游戏以大约 200 FPS 的速度运行。

密集任务

让我们用一千个敌人试试这个:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

使用该方法显示和移动一千个敌人大约需要 150 毫秒,帧速率约为 7 FPS。

千敌

显示和移动一千个敌人的任务大约需要 50 毫秒,帧速率约为 30 FPS。

显示移动的敌人

为什么不useTasks呢?

任务非常高效,可以减少系统性能的压力。您甚至可以使用任务并行库 (TPL) 在多个线程中使用它们。

然而,在 Unity 中使用它们有一些缺点。在 Unity 中使用的主要缺点Task是它们都在Main线程上运行。是的,我们可以让它们在其他线程上运行,但是 Unity 已经做了自己的线程和内存管理,你可以通过创建比 CPU Cores 更多的线程来创建错误,这会导致资源竞争。

任务也很难正确执行和调试。在编写原始代码时,我结束了所有任务都在运行,但没有一个敌人在屏幕上移动。结果是我需要返回Task[]我在Task.

任务会产生大量影响性能的垃圾。它们也不会出现在分析器中,所以如果你有一个影响性能的,很难追踪。另外,我注意到有时我的任务和更新功能会继续从其他场景运行。

统一协程

根据Unity的说法,“协程是一个可以暂停执行(yield)直到给定的YieldInstruction完成的函数。”

这意味着我们可以运行代码并等待任务完成后再继续。这很像一个异步方法。它使用返回类型IEnumerator和 weyield return而不是await.

Unity 有几种不同类型的yield 指令可供我们使用,即WaitForSecondsWaitForEndOfFrameWaitUntilWaitWhile

要启动协程,我们需要 aMonoBehaviour并使用MonoBehaviour.StartCoroutine.

要在协程完成之前停止协程,我们使用MonoBehaviour.StopCoroutine. 停止协程时,请确保使用与启动协程相同的方法。

Unity 中协程的常见用例是等待资产加载并创建冷却计时器。

示例:使用协程的场景加载器

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

检查协程对性能的影响

让我们看看使用协程如何影响我们项目的性能。我只会使用性能密集型方法来做到这一点。

我添加CoroutineMethodType枚举和变量中以跟踪其状态:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

我创建了协程。这类似于我们之前创建的性能密集型任务和方法,添加了代码来更新方法时间:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

Update方法中,我添加了对协程的检查。我还修改了方法时间,更新了代码,并添加了代码来停止协程(如果它正在运行)并且我们更改了方法类型:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

密集的协程大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。

强化协程

C# 作业系统和突发编译器

什么是 C# 作业系统?

C# Job System 是 Unity 对易于编写的任务的实现,不会像任务那样产生垃圾,并利用Unity 已经创建的工作线程。这解决了任务的所有缺点。

Unity 将作业比作线程,但他们确实说作业执行一项特定任务。作业也可以在运行前依赖其他作业完成;这解决了我没有正确移动我的任务的问题,Units因为它依赖于首先完成的另一个任务。

Unity 会自动为我们处理作业依赖项。工作系统还内置了一个安全系统,主要用于防止竞争条件。对作业的一个警告是,它们只能包含blittable 类型NativeContainer类型的成员变量。这是安全系统的一个缺点。

要使用作业系统,您需要创建作业、安排作业、等待作业完成,然后使用作业返回的数据。需要作业系统才能使用 Unity 的面向数据的技术堆栈 (DOTS)。

有关作业系统的更多详细信息,请参阅Unity 文档

创建工作

要创建作业,您需要创建一个stuct实现其中一个IJob接口(IJobIJobForIJobParallelForUnity.Engine.Jobs.IJobParallelForTransform)的作业。IJob是一项基本工作。IJobForIJobForParallel用于对本机容器的每个元素执行相同的操作或进行多次迭代。它们之间的区别在于 IJobFor 在单个线程上运行,其中IJobForParallel将在多个线程之间拆分。

我将用于IJob创建一个密集的操作工作,IJobForIJobForParallel创建一个可以移动多个敌人的工作;这只是为了让我们可以看到对性能的不同影响。这些作业将与我们之前创建的任务和方法相同:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

添加成员变量。就我而言,我IJob不需要任何东西。由于作业没有框架的概念,IJobFor并且需要当前增量时间的浮点数;IJobParallelFor他们在 Unity 之外运行MonoBehaviour。他们还需要一个float3用于位置的数组和一个用于 y 轴移动速度的数组:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

最后一步是实现所需的Execute方法。和IJobForIJobForParallel需要int作业正在执行的当前迭代的索引。

transform不同之处在于,我们使用工作中的数组,而不是访问敌人的和移动:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

安排工作

首先,我们需要设置作业并填充作业数据:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

然后我们用JobHandle jobHandle = jobData.Schedule();. 该Schedule方法返回一个JobHandle可以在以后使用的。

我们无法从作业中安排作业。但是,我们可以创建新的工作并从工作中填充他们的数据。一旦安排了作业,就不能中断。

性能密集型工作

我创建了一个创建新作业并安排它的方法。它返回我可以在我的update方法中使用的作业句柄:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

我将这份工作添加到我的枚举中。然后,在Update方法中,我将 添加case到该switch部分。我创建了一个数组JobHandles。然后,我循环遍历所有模拟的游戏对象,为每个对象添加一个预定作业到数组中:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

和_MoveEnemyMoveEnemyParallelJob

接下来,我将作业添加到我的枚举中。然后在Update方法中,我调用一个新MoveEnemyJob方法,传递增量时间。通常你会使用JobForJobParallelFor

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

我要做的第一件事是为职位设置一个数组,并为moveY我将传递给工作的数组设置一个数组。然后我用来自敌人的数据填充这些数组。接下来,我创建作业并根据我要使用的作业设置作业的数据。之后,我根据要使用的作业和要执行的调度类型来安排作业:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

从作业中取回数据

我们必须等待工作完成。我们可以从JobHandle我们安排作业完成时使用的状态获取状态。这将在继续执行之前等待作业完成: >handle.Complete();JobHandle.CompleteAll(jobHandles). 作业完成后,NativeContainer我们用来设置作业的那个将拥有我们需要使用的所有数据。一旦我们从它们那里检索到数据,我们就必须妥善处理它们。

性能密集型工作

这非常简单,因为我没有在作业中读取或写入任何数据。我等待所有计划完成的作业,然后处理Native数组:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

密集的工作大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。

密集工作

工作_MoveEnemy

我添加了适当的完整检查:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

在方法类型检查之后,我遍历所有敌人,设置他们的transform位置和moveY作业中设置的数据。接下来,我正确处理原生数组:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

显示和移动一千个有工作的敌人大约需要 160 毫秒,帧速率约为 7 FPS,而没有性能提升。

没有性能提升

以约 30 FPS 的帧速率显示和移动 1000 个并行作业的敌人大约需要 30 毫秒。

并行作业

Unity 中的突发编译器是什么?

突发编译器是一种将字节码转换为本机代码的编译器。将此与 C# 作业系统一起使用可提高生成代码的质量,从而显着提高性能并减少移动设备上的电池消耗。

要使用它,您只需告诉 Unity 您想在具有以下[BurstCompile]属性的作业上使用突发编译:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

然后在 Unity 中,选择Jobs > Burst > Enable Completion

启用完成

Burst在编辑器中是即时 (JIT),这意味着在播放模式下可以关闭。当您构建项目时,它是 Ahead-Of-Time (AOT),这意味着需要在构建项目之前启用它。您可以通过编辑Project Settings Window中的Burst AOT Settings部分来实现。

突发 AOT 设置

有关突发编译器的更多详细信息,请参阅Unity 文档

使用突发编译器的性能密集型工作

在游戏以大约 150 FPS 的速度运行时,一个带有突发的密集工作大约需要 3 毫秒才能完成。

密集的工作与突发

显示和移动一千个敌人,爆发的工作大约需要 30 毫秒,帧速率约为 30 FPS。

突发 30 毫秒

显示和移动一千个敌人,与爆发并行的工作大约需要 6 毫秒,帧速率约为 80 到 90 FPS。

6 毫秒

结论

我们可以使用它Task来提高 Unity 应用程序的性能,但使用它们有几个缺点。根据我们想要做的事情,最好使用 Unity 中打包的东西。如果我们想等待某些东西完成异步加载,请使用协程;我们可以启动协程,而不是停止程序进程的运行。

我们可以使用 C# 作业系统和突发编译器来获得巨大的性能提升,同时在执行进程密集型任务时不必担心所有线程管理问题。使用内置系统,我们确信它以安全的方式完成,不会导致任何不必要的错误或错误。

在不使用突发编译器的情况下,任务确实比作业运行得更好,但这是由于在幕后为我们安全地设置一切而产生的额外开销。使用突发编译器时,我们的工作执行了我们的任务。当您需要可以获得的所有额外性能时,请使用 C# Job System with burst。

这个项目文件可以在我的 GitHub 上找到

来源:https ://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

#csharp #async #await 

C# 中的 Async、Await 和 Task 是什么
Hong  Nhung

Hong Nhung

1656374400

Async, Await Và Task Là Gì Trong C #

Hiệu suất là tất cả mọi thứ khi bạn đang cố gắng xuất bản lên web, thiết bị di động, bảng điều khiển và thậm chí một số PC cấp thấp hơn. Một trò chơi hoặc ứng dụng chạy ở tốc độ dưới 30 FPS có thể gây ra sự thất vọng cho người dùng. Chúng ta hãy xem xét một số thứ chúng ta có thể sử dụng để tăng hiệu suất bằng cách giảm tải cho CPU.

Trong bài đăng này, chúng tôi sẽ đề cập đến những gì async, awaitTasktrong C # là gì và cách sử dụng chúng trong Unity để đạt được hiệu suất trong dự án của bạn. Tiếp theo, chúng ta sẽ xem xét một số gói có sẵn của Unity: coroutines, C # Job System và trình biên dịch bùng nổ. Chúng tôi sẽ xem xét chúng là gì, cách sử dụng chúng và cách chúng tăng hiệu suất trong dự án của bạn.

Để bắt đầu dự án này, tôi sẽ sử dụng Unity 2021.3.4f1. Tôi chưa thử nghiệm mã này trên bất kỳ phiên bản Unity nào khác; tất cả các khái niệm ở đây sẽ hoạt động trên bất kỳ phiên bản Unity nào sau Unity 2019.3. Kết quả hiệu suất của bạn có thể khác nếu sử dụng phiên bản cũ hơn vì Unity đã thực hiện một số cải tiến đáng kể với mô hình lập trình async / await vào năm 2021. Đọc thêm về nó trong blog Unity và .NET của Unity, phần tiếp theo là phần có nhãn “Hiện đại hóa Thời gian chạy thống nhất. ”

Tôi đã tạo một dự án 2D (URP) Core mới, nhưng bạn có thể sử dụng dự án này trong bất kỳ loại dự án nào bạn thích.

Tôi có một sprite mà tôi nhận được từ Space Shooter (Redux, cùng với phông chữ và âm thanh) của Kenney Vleugels .

Tôi đã tạo một nhà lắp ghép của kẻ thù có chứa Sprite Render và một Thành phần của kẻ thù. Thành phần địch là một thành phần MonoBehaviourcó a Transformvà a floatđể theo dõi vị trí và tốc độ di chuyển trên trục y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Cái gì async, awaitTaskcó trong C #

Là gì async?

Trong C #, các phương thức có thể có một asynctừ khóa phía trước, nghĩa là các phương thức đó là phương thức không đồng bộ. Đây chỉ là một cách để nói với trình biên dịch rằng chúng ta muốn có thể thực thi mã bên trong và cho phép người gọi phương thức đó tiếp tục thực thi trong khi chờ phương thức này kết thúc.

Một ví dụ về điều này sẽ là nấu một bữa ăn. Bạn sẽ bắt đầu nấu thịt, và trong khi thịt đang nấu và bạn chờ nó hoàn thành, bạn sẽ bắt đầu làm các mặt. Trong khi thức ăn đang nấu, bạn sẽ bắt đầu dọn bàn ăn. Một ví dụ về điều này trong mã sẽ là static async Task<Steak> MakeSteak(int number).

Unity cũng có tất cả các loại phương thức có sẵn mà bạn có thể gọi không đồng bộ; xem tài liệu Unity để biết danh sách các phương pháp. Với cách Unity xử lý việc quản lý bộ nhớ, nó sử dụng coroutines hoặcAsyncOperation Hệ thống công việc C # .

Bạn awaitsử dụng nó như thế nào và nó là gì?

Trong C #, bạn có thể đợi hoạt động không đồng bộ hoàn tất bằng cách sử dụng awaittừ khóa. Điều này được sử dụng bên trong bất kỳ phương thức nào có asynctừ khóa để chờ một thao tác tiếp tục:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Xem các tài liệu của Microsoft để biết thêm về await.

A là gì Taskvà bạn sử dụng nó như thế nào?

A Tasklà một phương thức không đồng bộ thực hiện một thao tác đơn lẻ và không trả về giá trị. Đối với một Tasktrả về một giá trị, chúng tôi sẽ sử dụng Task<TResult>.

Để sử dụng một tác vụ, chúng ta tạo nó giống như tạo bất kỳ đối tượng mới nào trong C # Task t1 = new Task(void Action):. Tiếp theo, chúng ta bắt đầu nhiệm vụ t1.wait. Cuối cùng, chúng tôi đợi nhiệm vụ hoàn thành với t1.wait.

Có một số cách để tạo, bắt đầu và chạy tác vụ. Task t2 = Task.Run(void Action)sẽ tạo và bắt đầu một nhiệm vụ. await Task.Run(void Action)sẽ tạo, bắt đầu và đợi tác vụ hoàn thành. Chúng tôi có thể sử dụng cách thay thế phổ biến nhất với Task t3 = Task.Factory.Start(void Action).

Có một số cách mà chúng ta có thể đợi Tác vụ hoàn thành. int index = Task.WaitAny(Task[])sẽ đợi bất kỳ nhiệm vụ nào hoàn thành và cung cấp cho chúng tôi chỉ số của nhiệm vụ đã hoàn thành trong mảng. await Task.WaitAll(Task[])sẽ đợi tất cả các nhiệm vụ hoàn thành.

Để biết thêm về các tác vụ, hãy xem Tài liệu Microsoft .

Một taskví dụ đơn giản

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Nhiệm vụ ảnh hưởng như thế nào đến hiệu suất

Bây giờ chúng ta hãy so sánh hiệu suất của một tác vụ so với hiệu suất của một phương pháp.

Tôi sẽ cần một lớp tĩnh mà tôi có thể sử dụng trong tất cả các lần kiểm tra hiệu suất của mình. Nó sẽ có một phương thức và một nhiệm vụ mô phỏng một hoạt động đòi hỏi nhiều hiệu suất. Cả phương pháp và tác vụ đều thực hiện cùng một hoạt động chính xác:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Bây giờ tôi cần một MonoBehaviourcái mà tôi có thể sử dụng để kiểm tra tác động của hiệu suất đối với tác vụ và phương pháp. Để tôi có thể thấy tác động tốt hơn đến hiệu suất, tôi sẽ giả vờ rằng tôi muốn chạy điều này trên mười đối tượng trò chơi khác nhau. Tôi cũng sẽ theo dõi khoảng thời gian mà Updatephương thức cần để chạy.

Trong Update, tôi nhận được thời gian bắt đầu. Nếu tôi đang thử nghiệm phương pháp này, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng và gọi phương pháp tăng cường hiệu suất. Nếu tôi đang thử nghiệm tác vụ, tôi sẽ tạo một Taskvòng lặp mảng mới thông qua tất cả các đối tượng trò chơi được mô phỏng và thêm nhiệm vụ chuyên sâu về hiệu suất vào mảng tác vụ. Sau đó tôi awaitcho tất cả các nhiệm vụ để hoàn thành. Bên ngoài kiểm tra loại phương thức, tôi cập nhật thời gian phương thức, chuyển đổi nó thành ms. Tôi cũng ghi lại nó.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

Phương pháp chuyên sâu mất khoảng 65 mili giây để hoàn thành trò chơi chạy ở khoảng 12 FPS.

Phương pháp chuyên sâuPhương pháp chuyên sâuPhương pháp chuyên sâu

Nhiệm vụ chuyên sâu mất khoảng 4ms để hoàn thành với trò chơi chạy ở khoảng 200 FPS.

Nhiệm vụ chuyên sâu

Hãy thử điều này với hàng nghìn kẻ thù:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Hiển thị và di chuyển một nghìn kẻ thù với phương pháp này mất khoảng 150ms với tốc độ khung hình khoảng 7 FPS.

Ngàn kẻ thù

Hiển thị và di chuyển hàng nghìn kẻ thù trong một nhiệm vụ mất khoảng 50ms với tốc độ khung hình khoảng 30 FPS.

Hiển thị kẻ thù di chuyển

Tại sao không useTasks?

Các tác vụ cực kỳ thành thạo và giảm bớt căng thẳng về hiệu suất trên hệ thống của bạn. Bạn thậm chí có thể sử dụng chúng trong nhiều chủ đề bằng cách sử dụng Thư viện song song tác vụ (TPL).

Tuy nhiên, có một số hạn chế khi sử dụng chúng trong Unity. Hạn chế lớn khi sử dụng Tasktrong Unity là tất cả chúng đều chạy trên Mainluồng. Có, chúng tôi có thể làm cho chúng chạy trên các luồng khác, nhưng Unity đã thực hiện quản lý luồng và bộ nhớ của riêng mình, và bạn có thể tạo lỗi bằng cách tạo nhiều luồng hơn Lõi CPU, điều này gây ra sự cạnh tranh về tài nguyên.

Các tác vụ cũng có thể khó thực hiện chính xác và gỡ lỗi. Khi viết mã ban đầu, tôi kết thúc với tất cả các nhiệm vụ đang chạy, nhưng không có kẻ thù nào di chuyển trên màn hình. Kết quả là tôi cần trả lại cái Task[]mà tôi đã tạo trong Task.

Tác vụ tạo ra nhiều rác ảnh hưởng đến hiệu suất. Chúng cũng không hiển thị trong hồ sơ, vì vậy nếu bạn có một cái đang ảnh hưởng đến hiệu suất, thật khó để theo dõi. Ngoài ra, tôi nhận thấy rằng đôi khi các tác vụ và chức năng cập nhật của tôi tiếp tục chạy từ các cảnh khác.

Các quy trình thống nhất

Theo Unity , “Một quy trình đăng ký là một hàm có thể tạm dừng việc thực thi của nó (lợi nhuận) cho đến khi kết thúc YieldInstruction đã cho .”

Điều này có nghĩa là chúng ta có thể chạy mã và đợi một tác vụ hoàn thành trước khi tiếp tục. Điều này giống như một phương pháp không đồng bộ. Nó sử dụng kiểu trả về IEnumeratorvà chúng tôi yield returnthay vì await.

Unity có một số loại hướng dẫn lợi nhuận khác nhau mà chúng tôi có thể sử dụng, chẳng hạn như WaitForSeconds, hoặc .WaitForEndOfFrameWaitUntilWaitWhile

Để bắt đầu coroutines, chúng ta cần một MonoBehaviourvà sử dụng MonoBehaviour.StartCoroutine.

Để dừng một quy trình đăng ký trước khi hoàn tất, chúng tôi sử dụng MonoBehaviour.StopCoroutine. Khi dừng coroutines, hãy đảm bảo rằng bạn sử dụng cùng một phương pháp như bạn đã sử dụng để bắt đầu nó.

Các trường hợp sử dụng phổ biến cho coroutines trong Unity là đợi tải nội dung và tạo bộ định thời gian hồi chiêu.

Ví dụ: Trình tải cảnh sử dụng chương trình điều tra

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Kiểm tra tác động của quy trình đăng ký đối với hiệu suất

Hãy xem việc sử dụng một quy trình điều tra tác động như thế nào đến hiệu suất của dự án của chúng tôi. Tôi sẽ chỉ làm điều này với phương pháp chuyên sâu về hiệu suất.

Tôi đã thêm vào Coroutineenum MethodTypevà các biến để theo dõi trạng thái của nó:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Tôi đã tạo quy trình đăng quang. Điều này tương tự với tác vụ và phương thức chuyên sâu về hiệu suất mà chúng tôi đã tạo trước đó với mã được bổ sung để cập nhật thời gian của phương thức:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

Trong Updatephương thức này, tôi đã thêm dấu kiểm cho quy trình đăng quang. Tôi cũng đã sửa đổi thời gian phương thức, cập nhật mã và thêm mã để dừng chương trình đăng quang nếu nó đang chạy và chúng tôi đã thay đổi loại phương thức:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

Quá trình đăng quang chuyên sâu mất khoảng 6 mili giây để hoàn thành khi trò chơi chạy ở khoảng 90 FPS.

Chương trình điều trị chuyên sâu

Hệ thống công việc C # và trình biên dịch liên tục

Hệ thống công việc C # là gì?

Hệ thống công việc C # là việc Unity thực hiện các tác vụ dễ viết, không tạo ra rác mà các tác vụ thực hiện và sử dụng các chuỗi công nhân mà Unity đã tạo. Điều này khắc phục tất cả các nhược điểm của nhiệm vụ.

Unity so sánh các công việc như các chủ đề, nhưng họ nói rằng một công việc thực hiện một nhiệm vụ cụ thể. Các công việc cũng có thể phụ thuộc vào các công việc khác để hoàn thành trước khi chạy; điều này khắc phục sự cố với nhiệm vụ mà tôi đã không di chuyển đúng cách của tôi Unitsvì nó phụ thuộc vào một nhiệm vụ khác để hoàn thành trước.

Các phụ thuộc công việc sẽ được Unity tự động giải quyết cho chúng tôi. Hệ thống công việc cũng có một hệ thống an toàn được tích hợp chủ yếu để bảo vệ khỏi các điều kiện của cuộc đua . Một lưu ý với các công việc là chúng chỉ có thể chứa các biến thành viên là kiểu blittable hoặc kiểu NativeContainer ; đây là một nhược điểm của hệ thống an toàn.

Để sử dụng hệ thống công việc, bạn tạo công việc, lên lịch công việc, đợi công việc hoàn thành, sau đó sử dụng dữ liệu trả về của công việc. Hệ thống công việc là cần thiết để sử dụng Ngăn xếp Công nghệ Định hướng Dữ liệu (DOTS) của Unity.

Để biết thêm chi tiết về hệ thống công việc, hãy xem tài liệu Unity .

Tạo một công việc

Để tạo một công việc, bạn tạo một công stuctviệc triển khai một trong các IJobgiao diện ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJoblà một công việc cơ bản. IJobForIJobForParallelđược sử dụng để thực hiện cùng một thao tác trên mỗi phần tử của vùng chứa gốc hoặc cho một số lần lặp. Sự khác biệt giữa chúng là IJobFor chạy trên một luồng duy nhất, nơi IJobForParallelsẽ được chia thành nhiều luồng.

Tôi sẽ sử dụng IJobđể tạo ra một công việc hoạt động chuyên sâu IJobForIJobForParallelđể tạo ra một công việc có thể di chuyển nhiều kẻ thù xung quanh; điều này chỉ để chúng ta có thể thấy các tác động khác nhau đến hiệu suất. Các công việc này sẽ giống với các tác vụ và phương pháp mà chúng tôi đã tạo trước đó:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Thêm các biến thành viên. Trong trường hợp của tôi, tôi IJobkhông cần bất kỳ. Và cần một IJobForphao IJobParallelForcho thời gian đồng bằng hiện tại vì các công việc không có khái niệm về khung; họ hoạt động bên ngoài Unity's MonoBehaviour. Họ cũng cần một mảng float3cho vị trí và một mảng cho tốc độ di chuyển trên trục y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

Bước cuối cùng là thực hiện Executephương thức được yêu cầu. Cả IJobForhai IJobForParallelđều yêu cầu một intchỉ mục của lần lặp hiện tại mà công việc đang thực thi.

Sự khác biệt là thay vì truy cập kẻ thù transformvà di chuyển, chúng tôi sử dụng mảng có trong công việc:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Lên lịch công việc

Đầu tiên, chúng ta cần cài đặt công việc và điền dữ liệu công việc:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Sau đó, chúng tôi lên lịch công việc với JobHandle jobHandle = jobData.Schedule();. Phương Schedulethức trả về một JobHandlecó thể được sử dụng sau này.

Chúng ta không thể sắp xếp một công việc từ bên trong một công việc. Tuy nhiên, chúng tôi có thể tạo công việc mới và điền dữ liệu của họ từ bên trong công việc. Một khi công việc đã được lên lịch, nó không thể bị gián đoạn.

Công việc đòi hỏi hiệu suất cao

Tôi đã tạo ra một phương pháp tạo một công việc mới và lên lịch cho nó. Nó trả về công việc xử lý mà tôi có thể sử dụng trong updatephương thức của mình:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Tôi đã thêm công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi thêm phần casevào switchphần. Tôi đã tạo một mảng JobHandles. Sau đó, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng, thêm một công việc đã lên lịch cho từng đối tượng vào mảng:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

MoveEnemy_MoveEnemyParallelJob

Tiếp theo, tôi đã thêm các công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi gọi một MoveEnemyJobphương thức mới, vượt qua thời gian delta. Thông thường, bạn sẽ sử JobFordụng JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

Điều đầu tiên tôi làm là thiết lập một mảng cho các vị trí và một mảng cho vị trí moveYmà tôi sẽ chuyển cho các công việc. Sau đó, tôi điền vào các mảng này với dữ liệu từ kẻ thù. Tiếp theo, tôi tạo công việc và thiết lập dữ liệu của công việc tùy thuộc vào công việc mà tôi muốn sử dụng. Sau đó, tôi lên lịch công việc tùy thuộc vào công việc mà tôi muốn sử dụng và loại lập lịch mà tôi muốn thực hiện:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Lấy lại dữ liệu từ một công việc

Chúng ta phải đợi cho công việc được hoàn thành. Chúng ta có thể lấy trạng thái từ trạng thái JobHandlemà chúng ta đã sử dụng khi lên lịch để hoàn thành công việc. Thao tác này sẽ đợi công việc hoàn tất trước khi tiếp tục thực hiện:> handle.Complete();hoặc JobHandle.CompleteAll(jobHandles). Sau khi công việc hoàn tất, công việc NativeContainermà chúng tôi đã sử dụng để thiết lập công việc sẽ có tất cả dữ liệu mà chúng tôi cần sử dụng. Khi chúng tôi lấy dữ liệu từ chúng, chúng tôi phải xử lý chúng đúng cách.

Công việc đòi hỏi hiệu suất cao

Điều này khá đơn giản vì tôi không đọc hoặc ghi bất kỳ dữ liệu nào cho công việc. Tôi đợi cho tất cả các công việc đã được lên lịch hoàn thành sau đó xử lý Nativemảng:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

Công việc chuyên sâu mất khoảng 6ms để hoàn thành với trò chơi chạy ở khoảng 90 FPS.

Công việc chuyên sâu

Công MoveEnemyviệc

Tôi thêm các séc hoàn chỉnh thích hợp:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Sau khi kiểm tra loại phương pháp, tôi lặp qua tất cả kẻ thù, thiết lập transformvị trí của chúng và moveYđến dữ liệu đã được thiết lập trong công việc. Tiếp theo, tôi xử lý đúng cách các mảng gốc:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Việc hiển thị và di chuyển hàng nghìn kẻ thù với công việc mất khoảng 160ms với tốc độ khung hình khoảng 7 FPS mà không tăng hiệu suất.

Không tăng hiệu suất

Hiển thị và di chuyển hàng nghìn kẻ thù song song với công việc mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Công việc song song

Trình biên dịch bùng nổ trong Unity là gì?

Trình biên dịch liên tục là trình biên dịch chuyển từ mã bytecode sang mã gốc. Việc sử dụng tính năng này với Hệ thống công việc C # sẽ cải thiện chất lượng của mã được tạo, giúp bạn tăng hiệu suất đáng kể cũng như giảm mức tiêu thụ pin trên thiết bị di động.

Để sử dụng điều này, bạn chỉ cần nói với Unity rằng bạn muốn sử dụng trình biên dịch liên tục trong công việc với [BurstCompile]thuộc tính:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Sau đó, trong Unity, chọn Jobs > Burst> Enable Completion

Cho phép hoàn thành

Burst là Just-In-Time (JIT) khi ở trong Trình chỉnh sửa, có nghĩa là điều này có thể ngừng hoạt động khi ở Chế độ phát. Khi bạn xây dựng dự án của mình, nó là Ahead-Of-Time (AOT), có nghĩa là điều này cần được kích hoạt trước khi xây dựng dự án của bạn. Bạn có thể làm như vậy bằng cách chỉnh sửa phần Cài đặt Burst AOT trong Cửa sổ Cài đặt Dự án .

Cài đặt Burst AOT

Để biết thêm chi tiết về trình biên dịch cụm, hãy xem tài liệu Unity .

Một công việc chuyên sâu về hiệu suất với trình biên dịch liên tục

Một công việc chuyên sâu với liên tục mất khoảng 3ms để hoàn thành với trò chơi chạy ở khoảng 150 FPS.

Công việc chuyên sâu với Burst

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc với liên tục mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Burst 30 mili giây

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc song song với liên tục mất khoảng 6ms với tốc độ khung hình khoảng 80 đến 90 FPS.

6 mili giây

Sự kết luận

Chúng tôi có thể sử dụng Taskđể tăng hiệu suất của các ứng dụng Unity của mình, nhưng có một số hạn chế khi sử dụng chúng. Tốt hơn là sử dụng những thứ được đóng gói trong Unity tùy thuộc vào những gì chúng ta muốn làm. Sử dụng coroutines nếu chúng ta muốn đợi một thứ gì đó kết thúc tải một cách không đồng bộ; chúng tôi có thể bắt đầu quy trình đăng ký và không dừng quá trình chạy chương trình của chúng tôi.

Chúng ta có thể sử dụng Hệ thống công việc C # với trình biên dịch liên tục để đạt được hiệu suất lớn trong khi không phải lo lắng về tất cả nội dung quản lý luồng khi thực hiện các tác vụ đòi hỏi nhiều quy trình. Sử dụng các hệ thống có sẵn, chúng tôi chắc chắn rằng nó được thực hiện một cách an toàn và không gây ra bất kỳ lỗi hoặc lỗi không mong muốn nào.

Các công việc đã chạy tốt hơn một chút so với các công việc không sử dụng trình biên dịch liên tục, nhưng đó là do chi phí bổ sung đằng sau hậu trường ít hơn để thiết lập mọi thứ một cách an toàn cho chúng tôi. Khi sử dụng trình biên dịch bùng nổ, các công việc của chúng tôi đã thực hiện các nhiệm vụ của chúng tôi. Khi bạn cần tất cả hiệu suất bổ sung mà bạn có thể nhận được, hãy sử dụng Hệ thống công việc C # với liên tục.

Các tệp dự án cho việc này có thể được tìm thấy trên GitHub của tôi .

Nguồn: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

#csharp  #async  #await 

Async, Await Và Task Là Gì Trong C #

O que Async, Await e Task estão em C #

Desempenho é tudo quando você está tentando publicar na web, dispositivos móveis, consoles e até mesmo alguns dos PCs de baixo custo. Um jogo ou aplicativo rodando a menos de 30 FPS pode causar frustração para os usuários. Vamos dar uma olhada em algumas das coisas que podemos usar para aumentar o desempenho reduzindo a carga na CPU.

Neste post, abordaremos o que são async, await, e Taskem C# e como usá-los no Unity para obter desempenho em seu projeto. Em seguida, vamos dar uma olhada em alguns dos pacotes embutidos do Unity: corrotinas, o C# Job System e o compilador de intermitência. Veremos o que são, como usá-los e como eles aumentam o desempenho em seu projeto.

Para iniciar este projeto, usarei o Unity 2021.3.4f1. Não testei este código em nenhuma outra versão do Unity; todos os conceitos aqui devem funcionar em qualquer versão do Unity após o Unity 2019.3. Seus resultados de desempenho podem diferir se você usar uma versão mais antiga, pois o Unity fez algumas melhorias significativas com o modelo de programação async/await em 2021. Leia mais sobre isso no blog do Unity Unity and .NET, o que vem a seguir , em particular a seção intitulada “Modernizing the Tempo de execução da unidade.”

Criei um novo projeto 2D (URP) Core, mas você pode usá-lo em qualquer tipo de projeto que desejar.

Eu tenho um sprite que peguei do Space Shooter (Redux, além de fontes e sons) de Kenney Vleugels .

Eu criei um prefab inimigo que contém um Sprite Render e um Enemy Component. O Componente Inimigo é um MonoBehaviourque tem a Transforme a floatpara acompanhar a posição e a velocidade para se mover no eixo y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

O que async, awaite Taskestão em C#

O que é async?

Em C#, os métodos podem ter uma palavra- asyncchave na frente deles, o que significa que os métodos são métodos assíncronos. Esta é apenas uma maneira de dizer ao compilador que queremos poder executar o código e permitir que o chamador desse método continue a execução enquanto aguarda a conclusão desse método.

Um exemplo disso seria cozinhar uma refeição. Você começará a cozinhar a carne e, enquanto a carne estiver cozinhando e você estiver esperando que ela termine, você começará a fazer os lados. Enquanto a comida está cozinhando, você deve começar a colocar a mesa. Um exemplo disso no código seria static async Task<Steak> MakeSteak(int number).

O Unity também tem todos os tipos de métodos embutidos que você pode chamar de forma assíncrona; consulte os documentos do Unity para obter uma lista de métodos. Com a maneira como o Unity lida com o gerenciamento de memória, ele usa coroutines , AsyncOperation, ou o C# Job System .

O que é awaite como você usa?

Em C#, você pode aguardar a conclusão de uma operação assíncrona usando a palavra- awaitchave. Isso é usado dentro de qualquer método que tenha a asyncpalavra-chave para aguardar a continuação de uma operação:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consulte os documentos da Microsoft para saber mais sobre await.

O que é um Taske como você o usa?

A Taské um método assíncrono que executa uma única operação e não retorna um valor. Para um Taskque retorna um valor, usaríamos Task<TResult>.

Para usar uma tarefa, nós a criamos como criar qualquer novo objeto em C#: Task t1 = new Task(void Action). Em seguida, iniciamos a tarefa t1.wait. Por fim, esperamos que a tarefa seja concluída com t1.wait.

Há várias maneiras de criar, iniciar e executar tarefas. Task t2 = Task.Run(void Action)irá criar e iniciar uma tarefa. await Task.Run(void Action)irá criar, iniciar e aguardar a conclusão da tarefa. Podemos usar a maneira alternativa mais comum com Task t3 = Task.Factory.Start(void Action).

Existem várias maneiras pelas quais podemos esperar que a tarefa seja concluída. int index = Task.WaitAny(Task[])aguardará a conclusão de qualquer tarefa e nos fornecerá o índice da tarefa concluída na matriz. await Task.WaitAll(Task[])aguardará a conclusão de todas as tarefas.

Para obter mais informações sobre tarefas, consulte os Documentos da Microsoft .

Um exemplo simplestask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Como a tarefa afeta o desempenho

Agora vamos comparar o desempenho de uma tarefa versus o desempenho de um método.

Vou precisar de uma classe estática que possa usar em todas as minhas verificações de desempenho. Ele terá um método e uma tarefa que simula uma operação de alto desempenho. Tanto o método quanto a tarefa executam a mesma operação exata:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Agora eu preciso de um MonoBehaviourque eu possa usar para testar o impacto do desempenho na tarefa e no método. Só para que eu possa ver um melhor impacto no desempenho, vou fingir que quero rodar isso em dez objetos de jogo diferentes. Também acompanharei a quantidade de tempo que o Updatemétodo leva para ser executado.

Em Update, recebo a hora de início. Se estou testando o método, percorro todos os objetos de jogo simulados e chamo o método de alto desempenho. Se estou testando a tarefa, crio um novo Taskloop de matriz por meio de todos os objetos de jogo simulados e adiciono a tarefa de alto desempenho à matriz de tarefas. Eu, então, awaitpara que todas as tarefas sejam concluídas. Fora da verificação do tipo de método, atualizo o tempo do método, convertendo-o para ms. Eu também registro.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

O método intensivo leva cerca de 65ms para ser concluído com o jogo rodando a cerca de 12 FPS.

Método Intensivo

A tarefa intensiva leva cerca de 4ms para ser concluída com o jogo rodando a cerca de 200 FPS.

Tarefa Intensiva

Vamos tentar isso com mil inimigos:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Exibir e mover mil inimigos com o método levou cerca de 150ms com uma taxa de quadros de cerca de 7 FPS.

Mil Inimigos

Exibir e mover mil inimigos com uma tarefa levou cerca de 50ms com uma taxa de quadros de cerca de 30 FPS.

Exibindo Inimigos em Movimento

Por que não useTasks?

As tarefas são extremamente eficientes e reduzem a pressão sobre o desempenho do seu sistema. Você pode até usá-los em vários threads usando a Biblioteca Paralela de Tarefas (TPL).

No entanto, existem algumas desvantagens em usá-los no Unity. A principal desvantagem de usar Taskno Unity é que todos eles são executados no Mainencadeamento. Sim, podemos fazê-los rodar em outros threads, mas o Unity já faz seu próprio gerenciamento de thread e memória, e você pode criar erros criando mais threads do que CPU Cores, o que causa competição por recursos.

As tarefas também podem ser difíceis de executar corretamente e depurar. Ao escrever o código original, acabei com as tarefas todas em execução, mas nenhum dos inimigos se moveu na tela. Acabou sendo que precisei retornar o Task[]que criei no Task.

As tarefas criam muito lixo que afeta o desempenho. Eles também não aparecem no criador de perfil, portanto, se você tiver um que esteja afetando o desempenho, é difícil rastrear. Além disso, notei que às vezes minhas tarefas e funções de atualização continuam sendo executadas em outras cenas.

Corrotinas de unidade

De acordo com Unity , “Uma corrotina é uma função que pode suspender sua execução (yield) até que o YieldInstruction termine”.

O que isso significa é que podemos executar o código e esperar que uma tarefa seja concluída antes de continuar. Isso é muito parecido com um método assíncrono. Ele usa um tipo de retorno IEnumeratore nós yield returnem vez de await.

O Unity tem vários tipos diferentes de instruções de rendimento que podemos usar, ou seja, WaitForSeconds, WaitForEndOfFrame, WaitUntilou WaitWhile.

Para iniciar as corrotinas, precisamos de um MonoBehavioure usamos o MonoBehaviour.StartCoroutine.

Para parar uma corrotina antes que ela seja concluída, usamos MonoBehaviour.StopCoroutine. Ao parar as corrotinas, certifique-se de usar o mesmo método usado para iniciá-las.

Casos de uso comuns para corrotinas no Unity são aguardar o carregamento dos ativos e criar temporizadores de resfriamento.

Exemplo: Um carregador de cena usando uma corrotina

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Verificando o impacto de uma corrotina no desempenho

Vamos ver como o uso de uma corrotina afeta o desempenho do nosso projeto. Eu só vou fazer isso com o método de desempenho intensivo.

Eu adicionei Coroutineao MethodTypeenum e variáveis ​​para acompanhar seu estado:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Eu criei a corrotina. Isso é semelhante à tarefa e ao método de alto desempenho que criamos anteriormente com código adicionado para atualizar o tempo do método:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

No Updatemétodo, adicionei a verificação da corrotina. Também modifiquei o tempo do método, atualizei o código e adicionei código para parar a corrotina se estivesse em execução e alteramos o tipo do método:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

A corrotina intensiva leva cerca de 6ms para ser concluída com o jogo rodando a cerca de 90 FPS.

Corrotina Intensiva

O sistema de trabalho C# e o compilador de intermitência

O que é o sistema de trabalho C#?

O C# Job System é a implementação do Unity de tarefas que são fáceis de escrever, não geram o lixo que as tarefas fazem e utilizam os threads de trabalho que o Unity já criou. Isso corrige todas as desvantagens das tarefas.

O Unity compara jobs como threads, mas eles dizem que um job faz uma tarefa específica. Os trabalhos também podem depender de outros trabalhos para serem concluídos antes de serem executados; isso corrige o problema com a tarefa que eu tinha que não moveu minha corretamente Unitsporque dependia de outra tarefa para ser concluída primeiro.

As dependências de trabalho são automaticamente cuidadas para nós pelo Unity. O sistema de trabalho também possui um sistema de segurança integrado principalmente para proteção contra condições de corrida . Uma ressalva com os jobs é que eles só podem conter variáveis ​​de membro que sejam tipos blittable ou tipos NativeContainer ; esta é uma desvantagem do sistema de segurança.

Para usar o sistema de trabalho, você cria o trabalho, agende o trabalho, aguarde a conclusão do trabalho e use os dados retornados pelo trabalho. O sistema de trabalho é necessário para usar o Data-Oriented Technology Stack (DOTS) da Unity.

Para obter mais detalhes sobre o sistema de tarefas, consulte a documentação do Unity .

Criando um trabalho

Para criar um trabalho, você cria um stuctque implementa uma das IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobé um trabalho básico. IJobFore IJobForParallelsão usados ​​para executar a mesma operação em cada elemento de um contêiner nativo ou em várias iterações. A diferença entre eles é que o IJobFor é executado em um único thread, onde IJobForParallelserá dividido entre vários threads.

Eu usarei IJobpara criar um trabalho de operação intensiva IJobFore IJobForParallelpara criar um trabalho que moverá vários inimigos ao redor; isso é apenas para que possamos ver os diferentes impactos no desempenho. Esses trabalhos serão idênticos às tarefas e métodos que criamos anteriormente:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Adicione as variáveis ​​de membro. No meu caso, o meu IJobnão precisa de nenhum. O IJobFore IJobParallelForprecisa de um float para o tempo delta atual, pois os trabalhos não têm um conceito de quadro; eles operam fora do Unity MonoBehaviour. Eles também precisam de uma matriz de float3para a posição e uma matriz para a velocidade de movimento no eixo y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

A última etapa é implementar o Executemétodo necessário. O IJobFore IJobForParallelambos exigem um intpara o índice da iteração atual que o trabalho está executando.

A diferença é que ao invés de acessar o inimigo transforme se mover, usamos o array que está no trabalho:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Agendando um trabalho

Primeiro, precisamos instalar o trabalho e preencher os dados dos trabalhos:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Em seguida, agendamos o trabalho com JobHandle jobHandle = jobData.Schedule();. O Schedulemétodo retorna um JobHandleque pode ser usado posteriormente.

Não podemos agendar um trabalho de dentro de um trabalho. Podemos, no entanto, criar novos trabalhos e preencher seus dados de dentro de um trabalho. Depois que um trabalho é agendado, ele não pode ser interrompido.

O trabalho de alto desempenho

Eu criei um método que cria um novo trabalho e o agenda. Ele retorna o identificador de trabalho que posso usar no meu updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Eu adicionei o trabalho ao meu enum. Então, no Updatemétodo, eu adiciono o caseà switchseção. Eu criei uma matriz de JobHandles. Em seguida, faço um loop por todos os objetos de jogo simulados, adicionando um trabalho agendado para cada um ao array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

O MoveEnemyeMoveEnemyParallelJob

Em seguida, adicionei os trabalhos ao meu enum. Então, no Updatemétodo, chamo um novo MoveEnemyJobmétodo, passando o tempo delta. Normalmente você usaria o JobForou o JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

A primeira coisa que faço é definir um array para as posições e um array para o moveYque vou passar para os jobs. Eu então preencho essas matrizes com os dados dos inimigos. Em seguida, crio o trabalho e defino os dados do trabalho dependendo de qual trabalho quero usar. Depois disso, agendo o trabalho dependendo do trabalho que quero usar e do tipo de agendamento que quero fazer:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Obtendo os dados de volta de um trabalho

Temos que esperar que o trabalho seja concluído. Podemos obter o status do JobHandleque usamos quando agendamos o trabalho para concluí-lo. Isso aguardará a conclusão do trabalho antes de continuar a execução: > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Quando o trabalho estiver concluído, o NativeContainerque usamos para configurar o trabalho terá todos os dados que precisamos usar. Uma vez que recuperamos os dados deles, temos que descartá-los adequadamente.

O trabalho de alto desempenho

Isso é bem simples, pois não estou lendo ou gravando nenhum dado no trabalho. Aguardo todos os trabalhos que foram agendados para serem concluídos e descarto a Nativematriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

O trabalho intensivo leva cerca de 6ms para ser concluído com o jogo rodando a cerca de 90 FPS.

Trabalho intensivo

O MoveEnemytrabalho

Eu adiciono as verificações completas apropriadas:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Após a verificação do tipo de método, eu percorro todos os inimigos, definindo suas transformposições e moveYos dados que foram definidos no trabalho. Em seguida, descarto adequadamente os arrays nativos:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Exibir e mover mil inimigos com trabalho levou cerca de 160ms com uma taxa de quadros de cerca de 7 FPS sem ganhos de desempenho.

Sem Ganhos de Desempenho

Exibir e mover mil inimigos com trabalho paralelo levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.

Trabalho paralelo

O que é o compilador burst no Unity?

O compilador de intermitência é um compilador que traduz de bytecode para código nativo. Usar isso com o C# Job System melhora a qualidade do código gerado, proporcionando um aumento significativo no desempenho, além de reduzir o consumo da bateria em dispositivos móveis.

Para usar isso, basta informar ao Unity que deseja usar a compilação de rajada no trabalho com o [BurstCompile]atributo:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Em seguida, no Unity, selecione Jobs > Burst > Enable Completion

Ativar conclusão

Burst é Just-In-Time (JIT) enquanto estiver no Editor, o que significa que isso pode ser desativado enquanto estiver no modo Play. Quando você compila seu projeto, ele é Ahead-Of-Time (AOT), o que significa que isso precisa ser ativado antes de compilar seu projeto. Você pode fazer isso editando a seção Burst AOT Settings na janela Project Settings .

Configurações AOT de rajada

Para obter mais detalhes sobre o compilador de intermitência, consulte a documentação do Unity .

Um trabalho de alto desempenho com o compilador de intermitência

Um trabalho intensivo com rajada leva cerca de 3ms para ser concluído com o jogo rodando a cerca de 150 FPS.

Trabalho intensivo com explosão

Exibindo e movendo mil inimigos, o trabalho com burst levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.

Explosão 30 ms

Exibindo e movendo mil inimigos, o trabalho paralelo com burst levou cerca de 6ms com uma taxa de quadros de cerca de 80 a 90 FPS.

6 ms

Conclusão

Podemos usar Taskpara aumentar o desempenho de nossos aplicativos Unity, mas há várias desvantagens em usá-los. É melhor usar as coisas que vêm empacotadas no Unity dependendo do que queremos fazer. Use corrotinas se quisermos esperar que algo termine de carregar de forma assíncrona; podemos iniciar a corrotina e não interromper a execução do processo do nosso programa.

Podemos usar o C# Job System com o compilador de intermitência para obter um enorme ganho de desempenho sem precisar se preocupar com todas as coisas de gerenciamento de threads ao executar tarefas de processo intenso. Usando os sistemas embutidos, temos certeza de que é feito de maneira segura que não causa erros ou bugs indesejados.

As tarefas foram executadas um pouco melhor do que os trabalhos sem usar o compilador de rajadas, mas isso se deve à pequena sobrecarga extra nos bastidores para configurar tudo com segurança para nós. Ao usar o compilador de intermitência, nossos trabalhos executavam nossas tarefas. Quando você precisar de todo o desempenho extra que pode obter, use o C# Job System com burst.

Os arquivos de projeto para isso podem ser encontrados no meu GitHub .

Fonte: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

 #csharp #async #await 

O que Async, Await e Task estão em C #

What Async, Await, and Task are in C#

Performance is everything when you are trying to publish to the web, mobile, consoles, and even some of the lower-end PCs. A game or application running at less than 30 FPS can cause frustration for the users. Let’s take a look at some of the things we can use to increase the performance by reducing the load on the CPU.

In this post, we will be covering what async, await, and Task in C# are and how to use them in Unity to gain performance in your project. Next, we will take a look at some of Unity’s inbuilt packages: coroutines, the C# Job System, and the burst compiler. We will look at what they are, how to use them, and how they increase the performance in your project.

To start this project off, I will be using Unity 2021.3.4f1. I have not tested this code on any other version of Unity; all concepts here should work on any Unity version after Unity 2019.3. Your performance results may differ if using an older version as Unity did make some significant improvements with the async/await programming model in 2021. Read more about it in Unity’s blog Unity and .NET, what’s next, in particular the section labeled “Modernizing the Unity runtime.”

I created a new 2D (URP) Core project, but you can use this in any type of project that you like.

I have a sprite that I got from Space Shooter (Redux, plus fonts and sounds) by Kenney Vleugels.

I created an enemy prefab that contains a Sprite Render and an Enemy Component. The Enemy Component is a MonoBehaviour that has a Transform and a float to keep track of the position and the speed to move on the y axis:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

What async, await, and Task are in C#

What is async?

In C#, methods can have an async keyword in front of them, meaning that the methods are asynchronous methods. This is just a way of telling the compiler that we want to be able to execute code within and allow the caller of that method to continue execution while waiting for this method to finish.

An example of this would be cooking a meal. You will start cooking the meat, and while the meat is cooking and you are waiting for it to finish, you would start making the sides. While the food is cooking, you would start setting the table. An example of this in code would be static async Task<Steak> MakeSteak(int number).

Unity also has all kinds of inbuilt methods that you can call asynchronously; see the Unity docs for a list of methods. With the way Unity handles memory management, it uses either coroutines, AsyncOperation, or the C# Job System.

What is await and how do you use it?

In C#, you can wait for an asynchronous operation to complete by using the await keyword. This is used inside any method that has the async keyword to wait for an operation to continue:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

See the Microsoft documents for more on await.

What is a Task and how do you use it?

A Task is an asynchronous method that performs a single operation and does not return a value. For a Task that returns a value, we would use Task<TResult>.

To use a task, we create it like creating any new object in C#: Task t1 = new Task(void Action). Next, we start the task t1.wait. Lastly, we wait for the task to complete with t1.wait.

There are several ways to create, start, and run tasks. Task t2 = Task.Run(void Action) will create and start a task. await Task.Run(void Action) will create, start, and wait for the task to complete. We can use the most common alternative way with Task t3 = Task.Factory.Start(void Action).

There are several ways that we can wait for Task to complete. int index = Task.WaitAny(Task[]) will wait for any task to complete and give us the index of the completed task in the array. await Task.WaitAll(Task[]) will wait for all of the tasks to complete.

For more on tasks, see the Microsoft Documents.

A simple taskexample

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

How the task affects performance

Now let’s compare the performance of a task versus the performance of a method.

I will need a static class that I can use in all of my performance checks. It will have a method and a task that simulates a performance-intensive operation. Both the method and the task perform the same exact operation:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Now I need a MonoBehaviour that I can use to test the performance impact on the task and the method. Just so I can see a better impact on the performance, I will pretend that I want to run this on ten different game objects. I will also keep track of the amount of time the Update method takes to run.

In Update, I get the start time. If I am testing the method, I loop through all of the simulated game objects and call the performance-intensive method. If I am testing the task, I create a new Task array loop through all of the simulated game objects and add the performance-intensive task to the task array. I then await for all of the tasks to complete. Outside of the method type check, I update the method time, converting it to ms. I also log it.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

The intensive method takes around 65ms to complete with the game running at about 12 FPS.

Intensive MethodIntensive MethodIntensive Method

The intensive task takes around 4ms to complete with the game running at about 200 FPS.

Intensive Task

Let’s try this with a thousand enemies:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Displaying and moving a thousand enemies with the method took around 150ms with a frame rate of about 7 FPS.

Thousand Enemies

Displaying and moving a thousand enemies with a task took around 50ms with a frame rate of about 30 FPS.

Displaying Moving Enemies

Why not useTasks?

Tasks are extremely perficient and reduce the strain on performance on your system. You can even use them in multiple threads using the Task Parallel Library (TPL).

There are some drawbacks to using them in Unity, however. The major drawback with using Task in Unity is that they all run on the Main thread. Yes, we can make them run on other threads, but Unity already does its own thread and memory management, and you can create errors by creating more threads than CPU Cores, which causes competition for resources.

Tasks can also be difficult to get to perform correctly and debug. When writing the original code, I ended up with the tasks all running, but none of the enemies moved on screen. It ended up being that I needed to return the Task[] that I created in the Task.

Tasks create a lot of garbage that affect the performance. They also do not show up in the profiler, so if you have one that is affecting the performance, it is hard to track down. Also, I have noticed that sometimes my tasks and update functions continue to run from other scenes.

Unity coroutines

According to Unity, “A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes.”

What this means is that we can run code and wait for a task to complete before continuing. This is much like an async method. It uses a return type IEnumerator and we yield return instead of await.

Unity has several different types of yield instructions that we can use, i.e., WaitForSeconds, WaitForEndOfFrame, WaitUntil, or WaitWhile.

To start coroutines, we need a MonoBehaviour and use the MonoBehaviour.StartCoroutine.

To stop a coroutine before it completes, we use MonoBehaviour.StopCoroutine. When stopping coroutines, make sure that you use the same method as you used to start it.

Common use cases for coroutines in Unity are to wait for assets to load and to create cooldown timers.

Example: A scene loader using a coroutine

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Checking a coroutine’s impact on performance

Let’s see how using a coroutine impacts the performance of our project. I am only going to do this with the performance-intensive method.

I added the Coroutine to the MethodType enum and variables to keep track of its state:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

I created the coroutine. This is similar to the performance-intensive task and method that we created earlier with added code to update the method time:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

In the Update method, I added the check for the coroutine. I also modified the method time, updated code, and added code to stop the coroutine if it was running and we changed the method type:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

The intensive coroutine takes around 6ms to complete with the game running at about 90 FPS.

Intensive Coroutine

The C# Job System and burst compiler

What is the C# Job System?

The C# Job System is Unity’s implementation of tasks that are easy to write, do not generate the garbage that tasks do, and utilize the worker threads that Unity has already created. This fixes all of the downsides of tasks.

Unity compares jobs as threads, but they do say that a job does one specific task. Jobs can also depend on other jobs to complete before running; this fixes the issue with the task that I had that did not properly move my Units because it was depending on another task to complete first.

The job dependencies are automatically taken care of for us by Unity. The job system also has a safety system built in mainly to protect against race conditions. One caveat with jobs is that they can only contain member variables that are either blittable types or NativeContainer types; this is a drawback of the safety system.

To use the job system, you create the job, schedule the job, wait for the job to complete, then use the data returned by the job. The job system is needed in order to use Unity’s Data-Oriented Technology Stack (DOTS).

For more details on the job system, see the Unity documentation.

Creating a job

To create a job, you create a stuct that implements one of the IJob interfaces (IJob, IJobFor, IJobParallelFor, Unity.Engine.Jobs.IJobParallelForTransform). IJob is a basic job. IJobFor and IJobForParallel are used to perform the same operation on each element of a native container or for a number of iterations. The difference between them is that the IJobFor runs on a single thread where the IJobForParallel will be split up between multiple threads.

I will use IJob to create an intensive operation job, IJobFor and IJobForParallel to create a job that will move multiple enemies around; this is just so we can see the different impacts on performance. These jobs will be identical to the tasks and methods that we created earlier:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Add the member variables. In my case, my IJob does not need any. The IJobFor and IJobParallelFor need a float for the current delta time as jobs do not have a concept of a frame; they operate outside of Unity’s MonoBehaviour. They also need an array of float3 for the position and an array for the move speed on the y axis:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

The last step is to implement the required Execute method. The IJobFor and IJobForParallel both require an int for the index of the current iteration that the job is executing.

The difference is instead of accessing the enemy’s transform and move, we use the array that are in the job:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Scheduling a job

First, we need to instate the job and populate the jobs data:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Then we schedule the job with JobHandle jobHandle = jobData.Schedule();. The Schedule method returns a JobHandle that can be used later on.

We can not schedule a job from within a job. We can, however, create new jobs and populate their data from within a job. Once a job has been scheduled, it cannot be interrupted.

The performance-intensive job

I created a method that creates a new job and schedules it. It returns the job handle that I can use in my update method:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

I added the job to my enum. Then, in the Update method, I add the case to the switch section. I created an array of JobHandles. I then loop through all of the simulated game objects, adding a scheduled job for each to the array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

The MoveEnemy and MoveEnemyParallelJob

Next, I added the jobs to my enum. Then in the Update method, I call a new MoveEnemyJob method, passing the delta time. Normally you would use either the JobFor or the JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

The first thing I do is set an array for the positions and an array for the moveY that I will pass on to the jobs. I then fill these arrays with the data from the enemies. Next, I create the job and set the job’s data depending on which job I want to use. After that, I schedule the job depending on the job that I want to use and the type of scheduling I want to do:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Getting the data back from a job

We have to wait for the job to be completed. We can get the status from the JobHandle that we used when we scheduled the job to complete it. This will wait for the job to be complete before continuing execution: >handle.Complete(); or JobHandle.CompleteAll(jobHandles). Once the job is complete, the NativeContainer that we used to set up the job will have all the data that we need to use. Once we retrieve the data from them, we have to properly dispose of them.

The performance-intensive job

This is pretty simple since I am not reading or writing any data to the job. I wait for all of the jobs that were scheduled to be completed then dispose of the Native array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

The intensive job takes around 6ms to complete with the game running at about 90 FPS.

Intensive Job

The MoveEnemy job

I add the appropriate complete checks:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

After the method type checks, I loop through all of the enemies, setting their transform positions and moveY to the data that was set in the job. Next, I properly dispose of the native arrays:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Displaying and moving a thousand enemies with job took around 160ms with a frame rate of about 7 FPS with no performance gains.

No Performance Gains

Displaying and moving a thousand enemies with job parallel took around 30ms with a frame rate of about 30 FPS.

Job Parallel

What is the burst compiler in Unity?

The burst compiler is a compiler that translates from bytecode to native code. Using this with the C# Job System improves the quality of the code generated, giving you a significant boost in performance as well as reducing the consumption of the battery on mobile devices.

To use this, you just tell Unity that you want to use burst compile on the job with the [BurstCompile] attribute:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Then in Unity, select Jobs > Burst > Enable Completion

Enable Completion

Burst is Just-In-Time (JIT) while in the Editor, meaning that this can be down while in Play Mode. When you build your project it is Ahead-Of-Time (AOT), meaning that this needs to be enabled before building your project. You can do so by editing the Burst AOT Settings section in the Project Settings Window.

Burst AOT Settings

For more details on the burst compiler, see the Unity documentation.

A performance-intensive job with the burst compiler

An intensive job with burst takes around 3ms to complete with the game running at about 150 FPS.

Intensive Job With Burst

Displaying and moving a thousand enemies, the job with burst took around 30ms with a frame rate of about 30 FPS.

Burst 30 ms

Displaying and moving a thousand enemies, the job parallel with burst took around 6ms with a frame rate of about 80 to 90 FPS.

6 ms

Conclusion

We can use Task to increase the performance of our Unity applications, but there are several drawbacks to using them. It is better to use the things that come packaged in Unity depending on what we want to do. Use coroutines if we want to wait for something to finish loading asynchronously; we can start the coroutine and not stop the process of our program from running.

We can use the C# Job System with the burst compiler to get a massive gain in performance while not having to worry about all of the thread management stuff when performing process-intensive tasks. Using the inbuilt systems, we are sure that it is done in a safe manner that does not cause any unwanted errors or bugs.

Tasks did run a little better than the jobs without using the burst compiler, but that is due to the little extra overhead behind the scenes to set everything up safely for us. When using the burst compiler, our jobs performed our tasks. When you need all of the extra performance that you can get, use the C# Job System with burst.

The project files for this can be found on my GitHub.

Source: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

#csharp #async #await 

What Async, Await, and Task are in C#