Lawrence  Lesch

Lawrence Lesch

1657176540

Map-reduce: Async Map-reduce Functions for Nodejs

Map Reduce for leveldb (via levelup)

Incremental map-reduces and real-time results.

Waat?

An "incremental map reduce" means when you update one key, only a relevant portion of the data needs to be recalculated.

"real-time results" means that you can listen to the database, and recieve change notifications on the fly! a la level-live-stream

If you just want something very simple, like mapping the date a blog post is created to the blog, then level-index may be enough.

Example

create a simple map-reduce

var LevelUp   = require('levelup')
var SubLevel  = require('level-sublevel')
var MapReduce = require('map-reduce')

var db = SubLevel(LevelUp(file))

var mapDb = 
  MapReduce(
    db, //the parent db
    'example',  //name.
    function (key, value, emit) {
      //perform some mapping.
      var obj = JSON.parse(value)
      //emit(key, value)
      //key may be an array of strings. 
      //value must be a string or buffer.
      emit(['all', obj.group], ''+obj.lines.length)
    },
    function (acc, value, key) {
      //reduce little into big
      //must return a string or buffer.
      return ''+(Number(acc) + Number(value))
    },
    //pass in the initial value for the reduce.
    //*must* be a string or buffer.
    '0'
  })
})

map-reduce uses level-trigger to make map reduces durable.

querying results.

  //get all the results in a specific group
  //start:[...] implies end:.. to be the end of that group.
  mapDb.createReadStream({range: ['all', group]}) 

  //get all the results in under a group.
  mapDb.createReadStream({range: ['all', true]}) 

  //get all the top level 
  mapDb.createReadStream({range: [true]})

complex aggregations

map-reduce with multiple levels of aggregation.

suppose we are building a database of all the street-food in the world. the data looks like this:

{
  country: USA | Germany | Cambodia, etc...
  state:   CA | NY | '', etc...
  city: Oakland | New York | Berlin | Phnom Penh, etc...
  type: taco | chili-dog | doner | noodles, etc...
}

We will aggregate to counts per-region, that look like this:

//say: under the key USA
{
  'taco': 23497,
  'chili-dog': 5643,
  etc...
}

first we'll map the raw data to ([country, state, city],type) tuples. then we'll count up all the instances of a particular type in that region!


var LevelUp   = require('levelup')
var SubLevel  = require('level-sublevel')
var MapReduce = require('map-reduce')

var db = SubLevel(LevelUp(file))
var mapDb = 
  MapReduce(
    db,
    'streetfood',
    function (key, value, emit) {
      //perform some mapping.
      var obj = JSON.parse(value)
      //emit(key, value)
      //key may be an array of strings. 
      //value must be a string or buffer.
      emit(
        [obj.country, obj.state || '', obj.city],
        //notice that we are just returning a string.
        JSON.stringify(obj.type)
      )
    },
    function (acc, value) {
      acc = JSON.parse(acc)
      value = JSON.parse(value)
      //check if this is top level data, like 'taco' or 'noodle'
      if('string' === typeof value) {
        //increment by one (remember to set as a number if it was undefined)
        acc[value] = (acc[value] || 0) ++
        return JSON.stringify(acc)
      }
      //if we get to here, we are combining two aggregates.
      //say, all the cities in a state, or all the countries in the world.
      //value and acc will both be objects {taco: number, doner: number2, etc...}

      for(var type in value) {
        //add the counts for each type together...
        //remembering to check that it is set as a value...
        acc[type] = (acc[type] || 0) + value[type]
      }
      //stringify the object, so that it can be written to disk!
      return JSON.stringify(acc)
    },
    '{}')

then query it like this:

mapDb.createReadStream({range: ['USA', 'CA', true]})
  .pipe(...)

retrive a specific result

pass db.get an array, and you can retrive a specific value, by group.

var userMapping = require("map-reduce")(
    db,
    "userPoints",
    function(key, value, emit){
        value = JSON.parse(value);
        var date = new Date(value.created);
        emit([value.user, date.getYear(), date.getMonth()], value.amount);
    },
    function(acc, value){
        return (Number(acc) + Number(value)).toString();
    },
    0
);

function getTotalPointsForUser(user, year, month, cb){
    userMapping.get([user, year, month], cb);
}

Author: Ddominictarr
Source Code: https://github.com/dominictarr/map-reduce 
License: MIT license

#node #map #async #javascript 

Map-reduce: Async Map-reduce Functions for Nodejs
Hoang Tran

Hoang Tran

1656861060

Bắt Đầu Với Chế Độ Xem Không Đồng Bộ Của Django

Viết mã không đồng bộ cung cấp cho bạn khả năng tăng tốc ứng dụng của mình mà không tốn nhiều công sức. Các phiên bản Django> = 3.1 hỗ trợ chế độ xem không đồng bộ, phần mềm trung gian và các thử nghiệm. Nếu bạn chưa thử nghiệm với chế độ xem không đồng bộ, bây giờ là thời điểm tuyệt vời để bạn có được chúng.

Hướng dẫn này xem xét cách bắt đầu với các chế độ xem không đồng bộ của Django.

Nếu bạn quan tâm đến việc tìm hiểu thêm về sức mạnh đằng sau mã không đồng bộ cùng với sự khác biệt giữa các luồng, đa xử lý và không đồng bộ trong Python, hãy xem bài viết Tăng tốc Python với Concurrency, Parallelism và asyncio của tôi .

Mục tiêu

Đến cuối hướng dẫn này, bạn sẽ có thể:

  1. Viết chế độ xem không đồng bộ trong Django
  2. Thực hiện một yêu cầu HTTP không chặn trong chế độ xem Django
  3. Đơn giản hóa các tác vụ nền cơ bản với chế độ xem không đồng bộ của Django
  4. Sử dụng sync_to_asyncđể thực hiện cuộc gọi đồng bộ bên trong chế độ xem không đồng bộ
  5. Giải thích khi nào bạn nên và không nên sử dụng chế độ xem không đồng bộ

Bạn cũng có thể trả lời các câu hỏi sau:

  1. Điều gì sẽ xảy ra nếu bạn thực hiện cuộc gọi đồng bộ bên trong chế độ xem không đồng bộ?
  2. Điều gì sẽ xảy ra nếu bạn thực hiện một cuộc gọi đồng bộ và không đồng bộ bên trong một chế độ xem không đồng bộ?
  3. Cần tây có còn cần thiết với các chế độ xem không đồng bộ của Django không?

Điều kiện tiên quyết

Miễn là bạn đã quen thuộc với chính Django, việc thêm chức năng không đồng bộ vào các chế độ xem không dựa trên lớp là cực kỳ đơn giản.

Sự phụ thuộc

  1. Python> = 3,10
  2. Django> = 4.0
  3. Uvicorn
  4. HTTPX

ASGI là gì?

ASGI là viết tắt của Asynchronous Server Gateway Interface. Đó là phiên bản tiếp theo không đồng bộ, hiện đại đối với WSGI , cung cấp một tiêu chuẩn để tạo các ứng dụng web dựa trên Python không đồng bộ.

Một điều đáng nói khác là ASGI tương thích ngược với WSGI, đây là lý do tốt để chuyển từ máy chủ WSGI như Gunicorn hoặc uWSGI sang máy chủ ASGI như Uvicorn hoặc Daphne ngay cả khi bạn chưa sẵn sàng chuyển sang viết ứng dụng không đồng bộ .

Tạo ứng dụng

Tạo một thư mục dự án mới cùng với một dự án Django mới:

$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject hello_async .

Hãy trao đổi virtualenv và Pip cho thơ hoặc Pipenv . Để biết thêm, hãy xem lại Môi trường Python hiện đại .

Django sẽ chạy các chế độ xem không đồng bộ của bạn nếu bạn đang sử dụng máy chủ phát triển tích hợp sẵn, nhưng nó sẽ không thực sự chạy chúng một cách không đồng bộ, vì vậy chúng tôi sẽ chạy Django với Uvicorn.

Cài đặt nó:

(env)$ pip install uvicorn

Để chạy dự án của bạn với Uvicorn, bạn sử dụng lệnh sau từ thư mục gốc của dự án:

uvicorn {name of your project}.asgi:application

Trong trường hợp của chúng tôi, đây sẽ là:

(env)$ uvicorn hello_async.asgi:application

Tiếp theo, hãy tạo chế độ xem không đồng bộ đầu tiên của chúng ta. Thêm tệp mới để giữ các dạng xem của bạn trong thư mục "hello_async", rồi thêm dạng xem sau:

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")

Tạo chế độ xem không đồng bộ trong Django cũng đơn giản như tạo chế độ xem đồng bộ - tất cả những gì bạn cần làm là thêm asynctừ khóa.

Cập nhật các URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]

Bây giờ, trong một thiết bị đầu cuối, trong thư mục gốc của bạn, hãy chạy:

(env)$ uvicorn hello_async.asgi:application --reload

Cờ --reloadyêu cầu Uvicorn xem các tệp của bạn để biết các thay đổi và tải lại nếu tìm thấy bất kỳ thay đổi nào. Điều đó có lẽ đã tự giải thích.

Mở http: // localhost: 8000 / trong trình duyệt web yêu thích của bạn:

Hello, async Django!

Không phải là điều thú vị nhất trên thế giới, nhưng, đây là một sự khởi đầu. Cần lưu ý rằng việc chạy chế độ xem này với máy chủ phát triển tích hợp sẵn của Django sẽ dẫn đến chức năng và đầu ra giống hệt nhau. Điều này là do chúng tôi không thực sự làm bất kỳ điều gì không đồng bộ trong trình xử lý.

HTTPX

Cần lưu ý rằng hỗ trợ không đồng bộ hoàn toàn tương thích ngược, vì vậy bạn có thể kết hợp chế độ xem không đồng bộ và đồng bộ hóa, phần mềm trung gian và thử nghiệm. Django sẽ thực thi từng thứ trong ngữ cảnh thực thi thích hợp.

Để chứng minh điều này, hãy thêm một vài chế độ xem mới:

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")

Cập nhật các URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Cài đặt HTTPX :

(env)$ pip install httpx

Khi máy chủ đang chạy, điều hướng đến http: // localhost: 8000 / async / . Bạn sẽ thấy ngay phản hồi:

Non-blocking HTTP request

Trong thiết bị đầu cuối của bạn, bạn sẽ thấy:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Tại đây, phản hồi HTTP được gửi lại trước lệnh gọi ngủ đầu tiên.

Tiếp theo, điều hướng đến http: // localhost: 8000 / sync / . Sẽ mất khoảng năm giây để nhận được phản hồi:

Blocking HTTP request

Chuyển sang thiết bị đầu cuối:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

Tại đây, phản hồi HTTP được gửi sau vòng lặp và yêu cầu https://httpbin.org/hoàn tất.

Hút một số loại thịt

Để mô phỏng thêm một kịch bản trong thế giới thực về cách bạn tận dụng tính năng không đồng bộ, hãy xem cách chạy nhiều hoạt động không đồng bộ, tổng hợp kết quả và trả lại chúng cho người gọi.

Quay lại URLconf của dự án của bạn, tạo một đường dẫn mới tại smoke_some_meats:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Quay lại dạng xem của bạn, tạo một hàm trợ giúp không đồng bộ mới được gọi là smoke. Hàm này nhận hai tham số: danh sách các chuỗi được gọi smokablesvà một chuỗi được gọi flavor. Những thứ này được mặc định là danh sách các loại thịt hun khói và "Sweet Baby Ray's", tương ứng.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    for smokable in smokables:
        print(f"Smoking some {smokable}...")
        print(f"Applying the {flavor}...")
        print(f"{smokable.capitalize()} smoked.")

    return len(smokables)

Vòng lặp for áp dụng một cách không đồng bộ hương vị (đọc là: Sweet Baby Ray's) cho các loại thịt hun khói (đọc là: thịt hun khói).

Đừng quên nhập:

from typing import List

Listđược sử dụng cho khả năng đánh máy bổ sung. Điều này là không bắt buộc và có thể dễ dàng bị bỏ qua (chỉ cần bỏ qua phần : List[str]khai báo tham số "smokingables" sau đây).

Tiếp theo, thêm hai trình trợ giúp không đồng bộ khác:

async def get_smokables():
    print("Getting smokeables...")

    await asyncio.sleep(2)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning smokeable")
        return [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]


async def get_flavor():
    print("Getting flavor...")

    await asyncio.sleep(1)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning flavor")
        return random.choice(
            [
                "Sweet Baby Ray's",
                "Stubb's Original",
                "Famous Dave's",
            ]
        )

Đảm bảo thêm nhập:

import random

Tạo dạng xem không đồng bộ sử dụng các chức năng không đồng bộ:

# hello_async/views.py

async def smoke_some_meats(request):
    results = await asyncio.gather(*[get_smokables(), get_flavor()])
    total = await asyncio.gather(*[smoke(results[0], results[1])])
    return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

Dạng xem này gọi hàm get_smokablesget_flavorđồng thời. Vì smokephụ thuộc vào kết quả từ cả hai get_smokablesget_flavor, chúng tôi đã từng gatherđợi mỗi tác vụ không đồng bộ hoàn thành.

Hãy nhớ rằng điều đó trong chế độ xem đồng bộ hóa thông thường get_smokablesget_flavorsẽ được xử lý từng cái một. Ngoài ra, chế độ xem không đồng bộ sẽ mang lại hiệu quả thực thi và cho phép các yêu cầu khác được xử lý trong khi các tác vụ không đồng bộ được xử lý, cho phép nhiều yêu cầu hơn được xử lý bởi cùng một quy trình trong một khoảng thời gian cụ thể.

Cuối cùng, một phản hồi được trả lại để cho người dùng biết rằng bữa ăn BBQ ngon của họ đã sẵn sàng.

Tuyệt quá. Lưu tệp, sau đó quay lại trình duyệt của bạn và điều hướng đến http: // localhost: 8000 / khói_some_meats / . Sẽ mất vài giây để nhận được phản hồi:

Smoked 6 meats with Sweet Baby Ray's!

Trong bảng điều khiển của mình, bạn sẽ thấy:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO:     127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Hãy lưu ý thứ tự của các câu lệnh in sau:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Đây là sự không đồng bộ khi làm việc: Khi get_smokableshàm ở chế độ ngủ, get_flavorhàm sẽ kết thúc quá trình xử lý.

Thịt cháy

Đồng bộ hóa cuộc gọi

Hỏi: Điều gì sẽ xảy ra nếu bạn thực hiện cuộc gọi đồng bộ bên trong chế độ xem không đồng bộ?

Điều tương tự sẽ xảy ra nếu bạn gọi một hàm không đồng bộ từ một chế độ xem không đồng bộ.

--

Để minh họa điều này, hãy tạo một hàm trợ giúp mới trong views.py của bạn được gọi là oversmoke:

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")

Rất đơn giản: Chúng tôi chỉ đợi đồng bộ trong năm giây.

Tạo dạng xem gọi hàm này:

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")

Cuối cùng, sắp xếp lộ trình trong URLconf của dự án của bạn:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Truy cập tuyến đường trong trình duyệt tại http: // localhost: 8000 / burn_some_meats :

Burned some meats.

Lưu ý rằng phải mất năm giây để cuối cùng nhận được phản hồi từ trình duyệt. Bạn cũng nên nhận được đầu ra bảng điều khiển cùng một lúc:

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

Có thể đáng lưu ý rằng điều tương tự sẽ xảy ra bất kể máy chủ bạn đang sử dụng, có thể là WSGI hoặc dựa trên ASGI.

Đồng bộ hóa và không đồng bộ hóa cuộc gọi

Hỏi: Điều gì sẽ xảy ra nếu bạn thực hiện một cuộc gọi đồng bộ và không đồng bộ bên trong một chế độ xem không đồng bộ?

Đừng làm điều này.

Chế độ xem đồng bộ và không đồng bộ có xu hướng hoạt động tốt nhất cho các mục đích khác nhau. Nếu bạn có chức năng chặn trong chế độ xem không đồng bộ, thì tốt nhất nó sẽ không tốt hơn là chỉ sử dụng chế độ xem đồng bộ.

Đồng bộ với không đồng bộ

Nếu bạn cần thực hiện cuộc gọi đồng bộ bên trong dạng xem không đồng bộ (ví dụ như tương tác với cơ sở dữ liệu thông qua Django ORM), hãy sử dụng sync_to_async làm trình bao bọc hoặc trình trang trí.

Thí dụ:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync, thread_sensitive=False)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

Bạn có nhận thấy rằng chúng tôi đã đặt thread_sensitivethông số thành Falsekhông? Điều này có nghĩa là hàm đồng bộ http_call_sync, sẽ được chạy trong một luồng mới. Xem lại các tài liệu để biết thêm thông tin.

Thêm nhập vào đầu:

from asgiref.sync import sync_to_async

Thêm URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Kiểm tra nó trong trình duyệt của bạn tại http: // localhost: 8000 / sync_to_async / .

Trong thiết bị đầu cuối của bạn, bạn sẽ thấy:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Bằng cách sử dụng sync_to_async, cuộc gọi đồng bộ chặn đã được xử lý trong một chuỗi nền, cho phép phản hồi HTTP được gửi lại trước cuộc gọi ngủ đầu tiên.

Chế độ xem cần tây và không đồng bộ

Q: Celery có còn cần thiết với các chế độ xem không đồng bộ của Django không?

Nó phụ thuộc.

Các chế độ xem không đồng bộ của Django cung cấp chức năng tương tự như một tác vụ hoặc hàng đợi tin nhắn mà không phức tạp. Nếu bạn đang sử dụng (hoặc đang cân nhắc) Django và muốn làm điều gì đó đơn giản (và không quan tâm đến độ tin cậy), chế độ xem không đồng bộ là một cách tuyệt vời để thực hiện điều này một cách nhanh chóng và dễ dàng. Nếu bạn cần thực hiện các quy trình nền chạy lâu và nặng hơn nhiều, bạn sẽ vẫn muốn sử dụng Celery hoặc RQ.

Cần lưu ý rằng để sử dụng các chế độ xem không đồng bộ một cách hiệu quả, bạn chỉ nên có các lệnh gọi không đồng bộ trong chế độ xem. Mặt khác, hàng đợi tác vụ sử dụng công nhân trên các quy trình riêng biệt và do đó có khả năng chạy các lệnh gọi đồng bộ trong nền, trên nhiều máy chủ.

Nhân tiện, bạn không phải chọn giữa chế độ xem không đồng bộ và hàng đợi tin nhắn - bạn có thể dễ dàng sử dụng chúng song song. Ví dụ: Bạn có thể sử dụng chế độ xem không đồng bộ để gửi email hoặc thực hiện sửa đổi cơ sở dữ liệu một lần, nhưng hãy yêu cầu Celery dọn dẹp cơ sở dữ liệu của bạn vào thời gian đã định mỗi đêm hoặc tạo và gửi báo cáo khách hàng.

Khi nào sử dụng

Đối với các dự án greenfield, nếu không đồng bộ là sở thích của bạn, hãy tận dụng các chế độ xem không đồng bộ và viết các quy trình I / O của bạn theo cách không đồng bộ càng nhiều càng tốt. Điều đó nói rằng, nếu hầu hết các chế độ xem của bạn chỉ cần thực hiện lệnh gọi đến cơ sở dữ liệu và thực hiện một số xử lý cơ bản trước khi trả lại dữ liệu, bạn sẽ không thấy sự gia tăng nhiều (nếu có) so với việc chỉ gắn bó với các chế độ xem đồng bộ.

Đối với các dự án brownfield, nếu bạn có ít hoặc không có quá trình I / O thì hãy gắn với chế độ xem đồng bộ. Nếu bạn có một số quy trình I / O, hãy kiểm tra xem bạn sẽ dễ dàng viết lại chúng theo cách không đồng bộ như thế nào. Viết lại I / O đồng bộ hóa thành không đồng bộ không dễ dàng, vì vậy có thể bạn sẽ muốn tối ưu hóa I / O đồng bộ hóa và chế độ xem của mình trước khi cố gắng viết lại thành không đồng bộ. Ngoài ra, không bao giờ là một ý tưởng hay khi kết hợp các quy trình đồng bộ hóa với các chế độ xem không đồng bộ của bạn.

Trong quá trình sản xuất, hãy đảm bảo sử dụng Gunicorn để quản lý Uvicorn nhằm tận dụng lợi thế của cả đồng thời (thông qua Uvicorn) và song song (thông qua Gunicorn worker):

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Sự kết luận

Tóm lại, mặc dù đây là một trường hợp sử dụng đơn giản, nhưng nó sẽ cung cấp cho bạn một ý tưởng sơ bộ về các khả năng mà các khung nhìn không đồng bộ của Django mở ra. Một số điều khác cần thử trong chế độ xem không đồng bộ của bạn là gửi email, gọi API của bên thứ ba và đọc / ghi vào tệp.

Nguồn:  https://testdriven.io

#django #async 

Bắt Đầu Với Chế Độ Xem Không Đồng Bộ Của Django

Как начать работу с асинхронными представлениями Django

Написание асинхронного кода дает вам возможность ускорить ваше приложение без особых усилий. Версии Django >= 3.1 поддерживают асинхронные представления, промежуточное ПО и тесты. Если вы еще не экспериментировали с асинхронными представлениями, сейчас самое время попробовать их.

В этом руководстве показано, как начать работу с асинхронными представлениями Django.

Если вам интересно узнать больше о силе асинхронного кода, а также о различиях между потоками, многопроцессорностью и асинхронностью в Python, ознакомьтесь с моей статьей «Ускорение Python с помощью параллелизма, параллелизма и асинхронности ».

Цели

К концу этого урока вы должны уметь:

  1. Напишите асинхронное представление в Django.
  2. Сделать неблокирующий HTTP-запрос в представлении Django
  3. Упростите основные фоновые задачи с помощью асинхронных представлений Django.
  4. Используйте sync_to_asyncдля синхронного вызова внутри асинхронного представления.
  5. Объясните, когда вы должны и не должны использовать асинхронные представления

Вы также должны быть в состоянии ответить на следующие вопросы:

  1. Что, если вы сделаете синхронный вызов внутри асинхронного представления?
  2. Что, если вы сделаете синхронный и асинхронный вызов внутри асинхронного представления?
  3. Нужен ли Celery с асинхронными представлениями Django?

Предпосылки

Если вы уже знакомы с самим Django, добавление асинхронной функциональности к представлениям, не основанным на классах, чрезвычайно просто.

Зависимости

  1. Питон >= 3.10
  2. Джанго >= 4.0
  3. Увикорн
  4. HTTPX

Что такое АСГИ?

ASGI означает асинхронный интерфейс шлюза сервера. Это современное асинхронное продолжение WSGI , предоставляющее стандарт для создания асинхронных веб-приложений на основе Python.

Еще одна вещь, о которой стоит упомянуть, это то, что ASGI обратно совместим с WSGI, что делает его хорошим предлогом для перехода с сервера WSGI, такого как Gunicorn или uWSGI, на сервер ASGI, такой как Uvicorn или Daphne , даже если вы не готовы переключиться на написание асинхронных приложений . .

Создание приложения

Создайте новый каталог проекта вместе с новым проектом Django:

$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject hello_async .

Не стесняйтесь менять virtualenv и Pip на Poetry или Pipenv . Подробнее см. в Modern Python Environments .

Django будет запускать ваши асинхронные представления, если вы используете встроенный сервер разработки, но на самом деле он не будет запускать их асинхронно, поэтому мы запустим Django с Uvicorn.

Установите его:

(env)$ pip install uvicorn

Чтобы запустить ваш проект с помощью Uvicorn, вы используете следующую команду из корня вашего проекта:

uvicorn {name of your project}.asgi:application

В нашем случае это будет:

(env)$ uvicorn hello_async.asgi:application

Далее давайте создадим наше первое асинхронное представление. Добавьте новый файл для хранения представлений в папку «hello_async», а затем добавьте следующее представление:

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")

Создание асинхронных представлений в Django так же просто, как создание синхронных представлений — все, что вам нужно сделать, это добавить asyncключевое слово.

Обновите URL-адреса:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]

Теперь в терминале в корневой папке запустите:

(env)$ uvicorn hello_async.asgi:application --reload

Флаг --reloadуказывает Uvicorn следить за изменениями в ваших файлах и перезагружать их, если они будут обнаружены. Это, наверное, было само собой разумеющимся.

Откройте http://localhost:8000/ в своем любимом веб-браузере:

Hello, async Django!

Не самая захватывающая вещь в мире, но, эй, это начало. Стоит отметить, что запуск этого представления со встроенным сервером разработки Django приведет к точно такой же функциональности и результату. Это потому, что мы на самом деле не делаем ничего асинхронного в обработчике.

HTTPX

Стоит отметить, что асинхронная поддержка полностью совместима с предыдущими версиями, поэтому вы можете комбинировать асинхронные и синхронные представления, промежуточное ПО и тесты. Django выполнит каждый в соответствующем контексте выполнения.

Чтобы продемонстрировать это, добавьте несколько новых представлений:

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")

Обновите URL-адреса:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Установите HTTPX :

(env)$ pip install httpx

Когда сервер запущен, перейдите по адресу http://localhost:8000/async/ . Вы должны сразу увидеть ответ:

Non-blocking HTTP request

В вашем терминале вы должны увидеть:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Здесь ответ HTTP отправляется обратно перед первым вызовом сна.

Затем перейдите по адресу http://localhost:8000/sync/ . Получение ответа должно занять около пяти секунд:

Blocking HTTP request

Повернитесь к терминалу:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

Здесь ответ HTTP отправляется после завершения цикла и завершения запроса https://httpbin.org/.

Курение мяса

Чтобы смоделировать более реальный сценарий использования асинхронности, давайте рассмотрим, как асинхронно выполнять несколько операций, агрегировать результаты и возвращать их вызывающему объекту.

Вернувшись в URLconf вашего проекта, создайте новый путь по адресу smoke_some_meats:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Вернувшись в представления, создайте новую вспомогательную асинхронную функцию с именем smoke. Эта функция принимает два параметра: список вызываемых строк smokablesи строку с именем flavor. По умолчанию это список копченого мяса и «Sweet Baby Ray's» соответственно.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    for smokable in smokables:
        print(f"Smoking some {smokable}...")
        print(f"Applying the {flavor}...")
        print(f"{smokable.capitalize()} smoked.")

    return len(smokables)

Цикл for асинхронно применяет вкус (читай: Sweet Baby Ray's) к куримым (читай: копченостям).

Не забудьте импорт:

from typing import List

Listиспользуется для дополнительных возможностей набора текста. Это не требуется и может быть легко опущено (просто : List[str]отмените следующее объявление параметра "smokables").

Затем добавьте еще два асинхронных помощника:

async def get_smokables():
    print("Getting smokeables...")

    await asyncio.sleep(2)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning smokeable")
        return [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]


async def get_flavor():
    print("Getting flavor...")

    await asyncio.sleep(1)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning flavor")
        return random.choice(
            [
                "Sweet Baby Ray's",
                "Stubb's Original",
                "Famous Dave's",
            ]
        )

Обязательно добавьте импорт:

import random

Создайте асинхронное представление, использующее асинхронные функции:

# hello_async/views.py

async def smoke_some_meats(request):
    results = await asyncio.gather(*[get_smokables(), get_flavor()])
    total = await asyncio.gather(*[smoke(results[0], results[1])])
    return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

Это представление вызывает функции get_smokablesи get_flavorодновременно. Поскольку smokeэто зависит от результатов как от , так get_smokablesи от get_flavor, мы обычно gatherждали завершения каждой асинхронной задачи.

Имейте в виду, что в обычном представлении синхронизации get_smokablesи get_flavorбудут обрабатываться по одному. Кроме того, асинхронное представление приведет к выполнению и позволит обрабатывать другие запросы во время обработки асинхронных задач, что позволяет обрабатывать большее количество запросов одним и тем же процессом за определенное время.

Наконец, возвращается ответ, информирующий пользователя о том, что вкусная еда для барбекю готова.

Большой. Сохраните файл, затем вернитесь в браузер и перейдите по адресу http://localhost:8000/smoke_some_meats/ . Получение ответа должно занять несколько секунд:

Smoked 6 meats with Sweet Baby Ray's!

В консоли вы должны увидеть:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO:     127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Обратите внимание на порядок следующих операторов печати:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Это асинхронность в действии: пока get_smokablesфункция засыпает, get_flavorфункция завершает обработку.

Пригоревшее мясо

Синхронизация вызова

Вопрос. Что делать, если вы делаете синхронный вызов внутри асинхронного представления?

То же самое произойдет, если вы вызовете неасинхронную функцию из неасинхронного представления.

--

Чтобы проиллюстрировать это, создайте новую вспомогательную функцию в файле views.py с именем oversmoke:

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")

Очень просто: мы просто синхронно ждем пять секунд.

Создайте представление, которое вызывает эту функцию:

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")

Наконец, подключите маршрут в URLconf вашего проекта:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Посетите маршрут в браузере по адресу http://localhost:8000/burn_some_meats :

Burned some meats.

Обратите внимание, что потребовалось пять секунд, чтобы наконец получить ответ от браузера. Вы также должны были получить вывод консоли одновременно:

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

Возможно, стоит отметить, что одно и то же произойдет независимо от используемого вами сервера, будь то WSGI или ASGI.

Синхронные и асинхронные вызовы

Вопрос. Что делать, если вы выполняете синхронный и асинхронный вызов внутри асинхронного представления?

Не делай этого.

Синхронные и асинхронные представления лучше всего подходят для разных целей. Если у вас есть функция блокировки в асинхронном представлении, в лучшем случае это будет не лучше, чем просто использование синхронного представления.

Синхронизировать с асинхронным

Если вам нужно сделать синхронный вызов внутри асинхронного представления (например, для взаимодействия с базой данных через ORM Django), используйте sync_to_async в качестве оболочки или декоратора.

Пример:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync, thread_sensitive=False)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

Вы заметили, что мы установили для thread_sensitiveпараметра значение False? Это означает, что синхронная функция http_call_syncбудет выполняться в новом потоке. Просмотрите документы для получения дополнительной информации.

Добавьте импорт вверху:

from asgiref.sync import sync_to_async

Добавьте URL-адрес:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Проверьте это в своем браузере по адресу http://localhost:8000/sync_to_async/ .

В вашем терминале вы должны увидеть:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

С помощью sync_to_asyncблокирующий синхронный вызов обрабатывался в фоновом потоке, что позволяло отправить ответ HTTP перед первым вызовом сна.

Celery и асинхронные представления

В: Нужен ли Celery для асинхронных представлений Django?

Это зависит.

Асинхронные представления Django предлагают функциональность, аналогичную задаче или очереди сообщений, но без сложности. Если вы используете (или рассматриваете) Django и хотите сделать что-то простое (и не заботитесь о надежности), асинхронные представления — отличный способ сделать это быстро и легко. Если вам нужно выполнять более тяжелые и длительные фоновые процессы, вы все равно захотите использовать Celery или RQ.

Следует отметить, что для эффективного использования асинхронных представлений в представлении должны быть только асинхронные вызовы. Очереди задач, с другой стороны, используют рабочие процессы в отдельных процессах и поэтому могут выполнять синхронные вызовы в фоновом режиме на нескольких серверах.

Кстати, вы ни в коем случае не должны выбирать между асинхронными представлениями и очередью сообщений — вы можете легко использовать их в тандеме. Например: вы можете использовать асинхронное представление для отправки электронной почты или внесения одноразовых изменений в базу данных, но заставить Celery очищать вашу базу данных в запланированное время каждую ночь или создавать и отправлять отчеты о клиентах.

Когда использовать

Для новых проектов, если вам нужна асинхронность, используйте асинхронные представления и пишите процессы ввода-вывода максимально асинхронно. Тем не менее, если большинству ваших представлений просто нужно сделать вызовы к базе данных и выполнить некоторую базовую обработку перед возвратом данных, вы не увидите большого увеличения (если оно вообще будет) по сравнению с простым использованием синхронизированных представлений.

Для старых проектов, если у вас практически нет процессов ввода-вывода, придерживайтесь синхронизированных представлений. Если у вас есть несколько процессов ввода-вывода, оцените, насколько легко будет переписать их асинхронно. Переписать синхронный ввод-вывод в асинхронный непросто, поэтому вы, вероятно, захотите оптимизировать свой синхронный ввод-вывод и представления, прежде чем пытаться переписать в асинхронный. Кроме того, никогда не рекомендуется смешивать процессы синхронизации с вашими асинхронными представлениями.

В рабочей среде обязательно используйте Gunicorn для управления Uvicorn, чтобы воспользоваться преимуществами как параллелизма (через Uvicorn), так и параллелизма (через воркеры Gunicorn):

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Вывод

В заключение, хотя это был простой вариант использования, он должен дать вам общее представление о возможностях, которые открывают асинхронные представления Django. Некоторые другие вещи, которые можно попробовать в асинхронных представлениях, — это отправка электронных писем, вызов сторонних API и чтение из/запись в файлы.

Источник:  https://testdriven.io

#django #async 

Как начать работу с асинхронными представлениями Django

如何開始使用 Django 的異步視圖

編寫異步代碼使您能夠毫不費力地加快應用程序的速度。Django 版本 >= 3.1支持異步視圖、中間件和測試。如果您還沒有嘗試過異步視圖,那麼現在是掌握它們的好時機。

本教程著眼於如何開始使用 Django 的異步視圖。

如果您有興趣了解有關異步代碼背後的強大功能以及 Python 中線程、多處理和異步之間的區別的更多信息,請查看我的使用並發、並行和異步加速 Python文章。

目標

在本教程結束時,您應該能夠:

  1. 在 Django 中編寫異步視圖
  2. 在 Django 視圖中發出非阻塞 HTTP 請求
  3. 使用 Django 的異步視圖簡化基本的後台任務
  4. 用於sync_to_async在異步視圖中進行同步調用
  5. 解釋什麼時候應該和不應該使用異步視圖

您還應該能夠回答以下問題:

  1. 如果您在異步視圖中進行同步調用怎麼辦?
  2. 如果您在異步視圖中進行同步和異步調用怎麼辦?
  3. Django 的異步視圖仍然需要 Celery 嗎?

先決條件

只要您已經熟悉 Django 本身,向非基於類的視圖添加異步功能就非常簡單。

依賴項

  1. 蟒蛇> = 3.10
  2. Django >= 4.0
  3. 優維康
  4. HTTPX

什麼是 ASGI?

ASGI代表異步服務器網關接口。它是WSGI的現代異步後續版本,為創建基於 Python 的異步 Web 應用程序提供了標準。

另一件值得一提的是,ASGI 向後兼容 WSGI,這使得它成為從 Gunicorn 或 uWSGI 等 WSGI 服務器切換到UvicornDaphne等 ASGI 服務器的好藉口,即使你還沒有準備好切換到編寫異步應用程序.

創建應用程序

創建一個新的項目目錄以及一個新的 Django 項目:

$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject hello_async .

隨意將 virtualenv 和 Pip 換成PoetryPipenv。有關更多信息,請查看現代 Python 環境

如果您使用內置的開發服務器,Django 將運行您的異步視圖,但它實際上不會異步運行它們,因此我們將使用 Uvicorn 運行 Django。

安裝它:

(env)$ pip install uvicorn

要使用 Uvicorn 運行項目,請從項目的根目錄中使用以下命令:

uvicorn {name of your project}.asgi:application

在我們的例子中,這將是:

(env)$ uvicorn hello_async.asgi:application

接下來,讓我們創建我們的第一個異步視圖。添加一個新文件以在“hello_async”文件夾中保存您的視圖,然後添加以下視圖:

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")

在 Django 中創建異步視圖就像創建同步視圖一樣簡單——您需要做的就是添加async關鍵字。

更新網址:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]

現在,在終端的根文件夾中,運行:

(env)$ uvicorn hello_async.asgi:application --reload

--reload標誌告訴 Uvicorn 監視您的文件是否有更改,如果發現任何更改則重新加載。這可能是不言自明的。

在您喜歡的 Web 瀏覽器中打開http://localhost:8000/ :

Hello, async Django!

這不是世界上最令人興奮的事情,但是,嘿,這是一個開始。值得注意的是,使用 Django 的內置開發服務器運行此視圖將產生完全相同的功能和輸出。這是因為我們實際上並沒有在處理程序中執行任何異步操作。

HTTPX

值得注意的是,異步支持完全向後兼容,因此您可以混合使用異步和同步視圖、中間件和測試。Django 將在正確的執行上下文中執行每一個。

為了證明這一點,添加一些新視圖:

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")

更新網址:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

安裝HTTPX

(env)$ pip install httpx

在服務器運行的情況下,導航到http://localhost:8000/async/。您應該立即看到響應:

Non-blocking HTTP request

在您的終端中,您應該看到:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

在這裡,HTTP 響應在第一次睡眠調用之前被發回。

接下來,導航到http://localhost:8000/sync/。獲得響應大約需要五秒鐘:

Blocking HTTP request

轉到終端:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

在這裡,HTTP 響應在循環之後發送並且請求https://httpbin.org/完成。

抽一些肉

為了模擬更多關於如何利用異步的真實場景,讓我們看看如何異步運行多個操作、聚合結果並將它們返回給調用者。

回到項目的 URLconf,在以下位置創建一個新路徑smoke_some_meats

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

回到您的視圖中,創建一個名為smoke. 這個函數有兩個參數:一個被調用的字符串列表smokables和一個被調用的字符串flavor。這些默認分別為可吸煙肉類和“Sweet Baby Ray's”列表。

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    for smokable in smokables:
        print(f"Smoking some {smokable}...")
        print(f"Applying the {flavor}...")
        print(f"{smokable.capitalize()} smoked.")

    return len(smokables)

for 循環將風味(閱讀:Sweet Baby Ray's)異步應用於可吸煙(閱讀:熏肉)。

不要忘記導入:

from typing import List

List用於額外的打字功能。這不是必需的,可以很容易地省略(只需: List[str]在“smokables”參數聲明後面加上)。

接下來,再添加兩個異步助手:

async def get_smokables():
    print("Getting smokeables...")

    await asyncio.sleep(2)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning smokeable")
        return [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]


async def get_flavor():
    print("Getting flavor...")

    await asyncio.sleep(1)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning flavor")
        return random.choice(
            [
                "Sweet Baby Ray's",
                "Stubb's Original",
                "Famous Dave's",
            ]
        )

確保添加導入:

import random

創建使用異步函數的異步視圖:

# hello_async/views.py

async def smoke_some_meats(request):
    results = await asyncio.gather(*[get_smokables(), get_flavor()])
    total = await asyncio.gather(*[smoke(results[0], results[1])])
    return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

此視圖同時調用get_smokablesget_flavor函數。由於smoke依賴於 和 的結果get_smokablesget_flavor我們過去常常gather等待每個異步任務完成。

請記住,在常規同步視圖中,get_smokablesget_flavor一次處理一個。此外,異步視圖將產生執行並允許在處理異步任務時處理其他請求,這允許同一進程在特定時間內處理更多請求。

最後,返回一個響應,讓用戶知道他們準備好了美味的燒烤餐。

偉大的。保存文件,然後返回瀏覽器並導航到http://localhost:8000/smoke_some_meats/。需要幾秒鐘才能得到響應:

Smoked 6 meats with Sweet Baby Ray's!

在您的控制台中,您應該看到:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO:     127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

請注意以下打印語句的順序:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

這是工作中的異步性:當get_smokables函數休眠時,get_flavor函數完成處理。

燒焦的肉

同步通話

問:如果您在異步視圖中進行同步調用怎麼辦?

如果您從非異步視圖調用非異步函數,也會發生同樣的事情。

--

為了說明這一點,在您的views.py中創建一個新的輔助函數,名為oversmoke

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")

非常簡單:我們只是同步等待五秒鐘。

創建調用此函數的視圖:

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")

最後,在項目的 URLconf 中連接路由:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

在瀏覽器中訪問http://localhost:8000/burn_some_meats中的路由:

Burned some meats.

請注意最終從瀏覽器返迴響應花了五秒鐘。您還應該同時收到控制台輸出:

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

可能值得注意的是,無論您使用哪種服務器,無論是基於 WSGI 還是基於 ASGI,都會發生同樣的事情。

同步和異步調用

問:如果您在異步視圖中進行同步和異步調用怎麼辦?

不要這樣做。

同步和異步視圖往往最適合不同的目的。如果您在異步視圖中具有阻塞功能,那麼充其量不會比僅使用同步視圖更好。

同步到異步

如果您需要在異步視圖中進行同步調用(例如,通過 Django ORM 與數據庫交互),請使用sync_to_async作為包裝器或裝飾器。

例子:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync, thread_sensitive=False)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

您是否注意到我們將thread_sensitive參數設置為False?這意味著同步函數http_call_sync將在新線程中運行。查看文檔以獲取更多信息。

將導入添加到頂部:

from asgiref.sync import sync_to_async

添加網址:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

在瀏覽器中測試它http://localhost:8000/sync_to_async/

在您的終端中,您應該看到:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

使用sync_to_async,阻塞同步調用在後台線程中處理,允許 HTTP 響應在第一次睡眠調用之前被發回。

Celery 和異步視圖

問: Django 的異步視圖仍然需要 Celery 嗎?

這取決於。

Django 的異步視圖提供了與任務或消息隊列類似的功能,但並不復雜。如果您正在使用(或正在考慮)Django 並且想做一些簡單的事情(並且不關心可靠性),那麼異步視圖是快速輕鬆地完成此任務的好方法。如果您需要執行更繁重、長時間運行的後台進程,您仍然需要使用 Celery 或 RQ。

應該注意的是,要有效地使用異步視圖,您應該只在視圖中進行異步調用。另一方面,任務隊列在單獨的進程上使用工作人員,因此能夠在後台、多台服務器上運行同步調用。

順便說一句,您不必在異步視圖和消息隊列之間進行選擇——您可以輕鬆地串聯使用它們。例如:您可以使用異步視圖發送電子郵件或進行一次性數據庫修改,但讓 Celery 在每晚的預定時間清理您的數據庫或生成並發送客戶報告。

何時使用

對於新建項目,如果您喜歡異步,請利用異步視圖並儘可能以異步方式編寫您的 I/O 流程。也就是說,如果您的大多數視圖只需要在返回數據之前調用數據庫並進行一些基本處理,那麼您不會看到與僅僅堅持同步視圖相比有很大的增加(如果有的話)。

對於棕地項目,如果您幾乎沒有 I/O 進程,請堅持使用同步視圖。如果您確實有許多 I/O 進程,請評估以異步方式重寫它們的難易程度。將同步 I/O 重寫為異步並不容易,因此您可能希望在嘗試重寫為異步之前優化同步 I/O 和視圖。另外,將同步進程與異步視圖混合絕不是一個好主意。

在生產中,一定要使用 Gunicorn 來管理 Uvicorn,以便同時利用並發性(通過 Uvicorn)和並行性(通過 Gunicorn worker):

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

結論

總之,雖然這是一個簡單的用例,但它應該讓您大致了解 Django 的異步視圖打開的可能性。在您的異步視圖中嘗試的其他一些事情是發送電子郵件、調用第三方 API 以及讀取/寫入文件。

來源:  https ://testdriven.io

#django #async 

如何開始使用 Django 的異步視圖

How to Get Started with Django's Asynchronous Views

Writing asynchronous code gives you the ability to speed up your application with little effort. Django versions >= 3.1 support async views, middleware, and tests. If you haven't already experimented with async views, now's a great time to get them under your belt.

This tutorial looks at how to get started with Django's asynchronous views.

Source: https://testdriven.io

#django #async 

How to Get Started with Django's Asynchronous Views
Noelia  Graham

Noelia Graham

1656842520

Comment Démarrer Avec Les Vues Asynchrones De Django

L'écriture de code asynchrone vous donne la possibilité d'accélérer votre application avec peu d'effort. Les versions de Django >= 3.1 prennent en charge les vues asynchrones, le middleware et les tests. Si vous n'avez pas encore expérimenté les vues asynchrones, c'est le moment idéal pour les mettre sous votre ceinture.

Ce didacticiel explique comment démarrer avec les vues asynchrones de Django.

Si vous souhaitez en savoir plus sur la puissance du code asynchrone ainsi que sur les différences entre les threads, le multitraitement et l'async en Python, consultez mon article Accélérer Python avec la concurrence, le parallélisme et l'asyncio .

Objectifs

À la fin de ce didacticiel, vous devriez être en mesure de :

  1. Écrire une vue asynchrone dans Django
  2. Faire une requête HTTP non bloquante dans une vue Django
  3. Simplifiez les tâches de base en arrière-plan avec les vues asynchrones de Django
  4. Utiliser sync_to_asyncpour effectuer un appel synchrone dans une vue asynchrone
  5. Expliquez quand vous devriez et ne devriez pas utiliser les vues asynchrones

Vous devez également être en mesure de répondre aux questions suivantes :

  1. Que se passe-t-il si vous effectuez un appel synchrone dans une vue asynchrone ?
  2. Que se passe-t-il si vous effectuez un appel synchrone et asynchrone dans une vue asynchrone ?
  3. Celery est-il toujours nécessaire avec les vues asynchrones de Django ?

Conditions préalables

Tant que vous êtes déjà familiarisé avec Django lui-même, l'ajout de fonctionnalités asynchrones à des vues non basées sur des classes est extrêmement simple.

Dépendances

  1. Python >= 3.10
  2. Django >= 4.0
  3. Uvicorne
  4. HTTPX

Qu'est-ce qu'ASGI ?

ASGI signifie Asynchronous Server Gateway Interface. Il s'agit de la suite moderne et asynchrone de WSGI , fournissant une norme pour la création d'applications Web asynchrones basées sur Python.

Une autre chose à mentionner est qu'ASGI est rétrocompatible avec WSGI, ce qui en fait une bonne excuse pour passer d'un serveur WSGI comme Gunicorn ou uWSGI à un serveur ASGI comme Uvicorn ou Daphne même si vous n'êtes pas prêt à passer à l'écriture d'applications asynchrones .

Création de l'application

Créez un nouveau répertoire de projet avec un nouveau projet Django :

$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject hello_async .

N'hésitez pas à échanger virtualenv et Pip contre Poetry ou Pipenv . Pour en savoir plus, consultez Environnements Python modernes .

Django exécutera vos vues asynchrones si vous utilisez le serveur de développement intégré, mais il ne les exécutera pas réellement de manière asynchrone, nous allons donc exécuter Django avec Uvicorn.

Installez-le:

(env)$ pip install uvicorn

Pour exécuter votre projet avec Uvicorn, vous utilisez la commande suivante depuis la racine de votre projet :

uvicorn {name of your project}.asgi:application

Dans notre cas, ce serait :

(env)$ uvicorn hello_async.asgi:application

Ensuite, créons notre première vue asynchrone. Ajoutez un nouveau fichier pour contenir vos vues dans le dossier "hello_async", puis ajoutez la vue suivante :

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")

La création de vues asynchrones dans Django est aussi simple que la création d'une vue synchrone -- tout ce que vous avez à faire est d'ajouter le mot- asyncclé.

Mettez à jour les URL :

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]

Maintenant, dans un terminal, dans votre dossier racine, exécutez :

(env)$ uvicorn hello_async.asgi:application --reload

Le --reloaddrapeau indique à Uvicorn de surveiller vos fichiers pour les modifications et de recharger s'il en trouve. Cela allait probablement de soi.

Ouvrez http://localhost:8000/ dans votre navigateur Web préféré :

Hello, async Django!

Ce n'est pas la chose la plus excitante au monde, mais bon, c'est un début. Il convient de noter que l'exécution de cette vue avec un serveur de développement intégré à Django donnera exactement les mêmes fonctionnalités et résultats. C'est parce que nous ne faisons rien d'asynchrone dans le gestionnaire.

HTTPX

Il convient de noter que la prise en charge asynchrone est entièrement rétrocompatible, vous pouvez donc mélanger des vues asynchrones et synchronisées, des intergiciels et des tests. Django exécutera chacun dans le contexte d'exécution approprié.

Pour illustrer cela, ajoutez quelques nouvelles vues :

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")

Mettez à jour les URL :

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Installez HTTPX :

(env)$ pip install httpx

Avec le serveur en cours d'exécution, accédez à http://localhost:8000/async/ . Vous devriez immédiatement voir la réponse :

Non-blocking HTTP request

Dans votre terminal, vous devriez voir :

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Ici, la réponse HTTP est renvoyée avant le premier appel de veille.

Ensuite, accédez à http://localhost:8000/sync/ . Cela devrait prendre environ cinq secondes pour obtenir la réponse :

Blocking HTTP request

Tournez-vous vers le terminal :

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

Ici, la réponse HTTP est envoyée après la boucle et la requête se https://httpbin.org/termine.

Fumer des viandes

Pour simuler davantage un scénario réel de la façon dont vous tireriez parti de l'asynchronisme, examinons comment exécuter plusieurs opérations de manière asynchrone, agréger les résultats et les renvoyer à l'appelant.

De retour dans l'URLconf de votre projet, créez un nouveau chemin àsmoke_some_meats :

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

De retour dans vos vues, créez une nouvelle fonction d'assistance asynchrone appelée smoke. Cette fonction prend deux paramètres : une liste de chaînes appelée smokableset une chaîne appelée flavor. Ceux-ci sont par défaut une liste de viandes à fumer et de "Sweet Baby Ray's", respectivement.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    for smokable in smokables:
        print(f"Smoking some {smokable}...")
        print(f"Applying the {flavor}...")
        print(f"{smokable.capitalize()} smoked.")

    return len(smokables)

La boucle for applique de manière asynchrone la saveur (lire : Sweet Baby Ray's) aux produits à fumer (lire : viandes fumées).

N'oubliez pas l'importation :

from typing import List

Listest utilisé pour des capacités de frappe supplémentaires. Ce n'est pas obligatoire et peut être facilement omis (nixez simplement la : List[str]déclaration de paramètre "smokables" suivante).

Ensuite, ajoutez deux autres assistants asynchrones :

async def get_smokables():
    print("Getting smokeables...")

    await asyncio.sleep(2)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning smokeable")
        return [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]


async def get_flavor():
    print("Getting flavor...")

    await asyncio.sleep(1)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning flavor")
        return random.choice(
            [
                "Sweet Baby Ray's",
                "Stubb's Original",
                "Famous Dave's",
            ]
        )

Assurez-vous d'ajouter l'importation :

import random

Créez la vue asynchrone qui utilise les fonctions asynchrones :

# hello_async/views.py

async def smoke_some_meats(request):
    results = await asyncio.gather(*[get_smokables(), get_flavor()])
    total = await asyncio.gather(*[smoke(results[0], results[1])])
    return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

Cette vue appelle les fonctions get_smokableset get_flavorsimultanément. Étant donné que smokedépend des résultats de get_smokableset get_flavor, nous avions l'habitude gatherd'attendre la fin de chaque tâche asynchrone.

Gardez à l'esprit que, dans une vue de synchronisation régulière, get_smokablesils get_flavorseraient traités un à la fois. De plus, la vue asynchrone produira l'exécution et permettra de traiter d'autres demandes pendant que les tâches asynchrones sont traitées, ce qui permet de traiter plus de demandes par le même processus dans un laps de temps donné.

Enfin, une réponse est renvoyée pour informer l'utilisateur qu'un délicieux repas barbecue est prêt.

Super. Enregistrez le fichier, puis revenez à votre navigateur et accédez à http://localhost:8000/smoke_some_meats/ . Cela devrait prendre quelques secondes pour obtenir la réponse :

Smoked 6 meats with Sweet Baby Ray's!

Dans votre console, vous devriez voir :

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO:     127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Prenez note de l'ordre des déclarations d'impression suivantes :

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

C'est l'asynchronicité au travail : pendant que la get_smokablesfonction dort, la get_flavorfonction termine le traitement.

Viandes brûlées

Appel synchronisé

Q : Que se passe-t-il si vous effectuez un appel synchrone dans une vue asynchrone ?

La même chose qui se produirait si vous appeliez une fonction non asynchrone à partir d'une vue non asynchrone.

--

Pour illustrer cela, créez une nouvelle fonction d'assistance dans votre views.py appeléeoversmoke :

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")

Très simple : nous attendons de manière synchrone cinq secondes.

Créez la vue qui appelle cette fonction :

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")

Enfin, câblez la route dans l'URLconf de votre projet :

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Visitez l'itinéraire dans le navigateur à http://localhost:8000/burn_some_meats :

Burned some meats.

Remarquez qu'il a fallu cinq secondes pour enfin obtenir une réponse du navigateur. Vous devriez également avoir reçu la sortie de la console en même temps :

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

Il est peut-être intéressant de noter que la même chose se produira quel que soit le serveur que vous utilisez, qu'il soit basé sur WSGI ou ASGI.

Appels synchronisés et asynchrones

Q : Que se passe-t-il si vous effectuez un appel synchrone et un appel asynchrone dans une vue asynchrone ?

Ne fais pas ça.

Les vues synchrones et asynchrones ont tendance à mieux fonctionner à des fins différentes. Si vous avez une fonctionnalité de blocage dans une vue asynchrone, au mieux, ce ne sera pas mieux que d'utiliser simplement une vue synchrone.

Synchroniser avec asynchrone

Si vous devez effectuer un appel synchrone à l'intérieur d'une vue asynchrone (comme pour interagir avec la base de données via l'ORM de Django, par exemple), utilisez sync_to_async comme wrapper ou comme décorateur.

Exemple:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync, thread_sensitive=False)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

Avez-vous remarqué que nous avons défini le thread_sensitiveparamètre sur False? Cela signifie que la fonction synchrone, http_call_sync, sera exécutée dans un nouveau thread. Consultez la documentation pour plus d'informations.

Ajoutez l'importation en haut :

from asgiref.sync import sync_to_async

Ajoutez l'URL :

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Testez-le dans votre navigateur à http://localhost:8000/sync_to_async/ .

Dans votre terminal, vous devriez voir :

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

À l' aide sync_to_asyncde , l'appel synchrone bloquant était traité dans un thread d'arrière-plan, permettant à la réponse HTTP d'être renvoyée avant le premier appel de mise en veille.

Céleri et vues asynchrones

Q : Le céleri est-il toujours nécessaire avec les vues asynchrones de Django ?

Ça dépend.

Les vues asynchrones de Django offrent des fonctionnalités similaires à une file d'attente de tâches ou de messages sans la complexité. Si vous utilisez (ou envisagez) Django et que vous voulez faire quelque chose de simple (et que vous ne vous souciez pas de la fiabilité), les vues asynchrones sont un excellent moyen d'accomplir cela rapidement et facilement. Si vous devez effectuer des processus d'arrière-plan beaucoup plus lourds et de longue durée, vous voudrez toujours utiliser Celery ou RQ.

Il convient de noter que pour utiliser efficacement les vues asynchrones, vous ne devez avoir que des appels asynchrones dans la vue. Les files d'attente de tâches, en revanche, utilisent des travailleurs sur des processus distincts et sont donc capables d'exécuter des appels synchrones en arrière-plan, sur plusieurs serveurs.

Soit dit en passant, vous ne devez en aucun cas choisir entre des vues asynchrones et une file d'attente de messages - vous pouvez facilement les utiliser en tandem. Par exemple : vous pouvez utiliser une vue asynchrone pour envoyer un e-mail ou effectuer une modification ponctuelle de la base de données, mais demander à Celery de nettoyer votre base de données à une heure programmée chaque nuit ou de générer et d'envoyer des rapports client.

Quand utiliser

Pour les nouveaux projets, si l'asynchrone est votre truc, tirez parti des vues asynchrones et écrivez vos processus d'E/S de manière asynchrone autant que possible. Cela dit, si la plupart de vos vues ont juste besoin d'appeler une base de données et d'effectuer un traitement de base avant de renvoyer les données, vous ne verrez pas beaucoup d'augmentation (le cas échéant) par rapport aux vues synchronisées.

Pour les projets brownfield, si vous avez peu ou pas de processus d'E/S, restez fidèle aux vues de synchronisation. Si vous avez un certain nombre de processus d'E/S, évaluez à quel point il sera facile de les réécrire de manière asynchrone. Réécrire les E/S de synchronisation en asynchrone n'est pas facile, vous voudrez donc probablement optimiser vos E/S de synchronisation et vos vues avant d'essayer de réécrire en asynchrone. De plus, ce n'est jamais une bonne idée de mélanger les processus de synchronisation avec vos vues asynchrones.

En production, assurez-vous d'utiliser Gunicorn pour gérer Uvicorn afin de tirer parti à la fois de la concurrence (via Uvicorn) et du parallélisme (via les travailleurs Gunicorn) :

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Conclusion

En conclusion, bien qu'il s'agisse d'un cas d'utilisation simple, cela devrait vous donner une idée approximative des possibilités qu'ouvrent les vues asynchrones de Django. D'autres choses à essayer dans vos vues asynchrones sont l'envoi d'e-mails, l'appel d'API tierces et la lecture/écriture de fichiers.

Source:  https://testdrive.io

#django #async 

Comment Démarrer Avec Les Vues Asynchrones De Django

Cómo Comenzar Con Las Vistas Asíncronas De Django

Escribir código asíncrono le brinda la capacidad de acelerar su aplicación con poco esfuerzo. Las versiones de Django >= 3.1 admiten vistas asíncronas, middleware y pruebas. Si aún no ha experimentado con vistas asíncronas, ahora es un buen momento para obtenerlas en su haber.

Este tutorial analiza cómo comenzar con las vistas asíncronas de Django.

Si está interesado en obtener más información sobre el poder detrás del código asíncrono junto con las diferencias entre subprocesos, multiprocesamiento y asíncrono en Python, consulte mi artículo Acelerando Python con concurrencia, paralelismo y asyncio .

Objetivos

Al final de este tutorial, debería ser capaz de:

  1. Escribir una vista asíncrona en Django
  2. Realice una solicitud HTTP sin bloqueo en una vista de Django
  3. Simplifique las tareas básicas en segundo plano con las vistas asíncronas de Django
  4. Úselo sync_to_asyncpara realizar una llamada síncrona dentro de una vista asíncrona
  5. Explique cuándo debe y no debe usar vistas asíncronas

También debe ser capaz de responder a las siguientes preguntas:

  1. ¿Qué sucede si realiza una llamada síncrona dentro de una vista asíncrona?
  2. ¿Qué sucede si realiza una llamada síncrona y asíncrona dentro de una vista asíncrona?
  3. ¿Sigue siendo necesario Celery con las vistas asíncronas de Django?

requisitos previos

Siempre que esté familiarizado con Django, agregar funcionalidad asíncrona a vistas no basadas en clases es extremadamente sencillo.

dependencias

  1. Pitón >= 3.10
  2. Django >= 4.0
  3. uvicornio
  4. HTTPX

¿Qué es ASGI?

ASGI significa Interfaz de puerta de enlace de servidor asíncrono. Es el seguimiento moderno y asíncrono de WSGI , que proporciona un estándar para crear aplicaciones web asíncronas basadas en Python.

Otra cosa que vale la pena mencionar es que ASGI es compatible con versiones anteriores de WSGI, lo que lo convierte en una buena excusa para cambiar de un servidor WSGI como Gunicorn o uWSGI a un servidor ASGI como Uvicorn o Daphne , incluso si no está listo para cambiar a escribir aplicaciones asincrónicas. .

Crear la aplicación

Cree un nuevo directorio de proyectos junto con un nuevo proyecto de Django:

$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject hello_async .

Siéntete libre de cambiar virtualenv y Pip por Poetry o Pipenv . Para obtener más información, revise Entornos modernos de Python .

Django ejecutará sus vistas asíncronas si está utilizando el servidor de desarrollo integrado, pero en realidad no las ejecutará de forma asíncrona, por lo que ejecutaremos Django con Uvicorn.

Instalarlo:

(env)$ pip install uvicorn

Para ejecutar su proyecto con Uvicorn, use el siguiente comando desde la raíz de su proyecto:

uvicorn {name of your project}.asgi:application

En nuestro caso, esto sería:

(env)$ uvicorn hello_async.asgi:application

A continuación, creemos nuestra primera vista asíncrona. Agregue un nuevo archivo para mantener sus vistas en la carpeta "hello_async" y luego agregue la siguiente vista:

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")

Crear vistas asíncronas en Django es tan simple como crear una vista síncrona: todo lo que necesita hacer es agregar la asyncpalabra clave.

Actualice las URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]

Ahora, en una terminal, en su carpeta raíz, ejecute:

(env)$ uvicorn hello_async.asgi:application --reload

La --reloadbandera le dice a Uvicorn que mire sus archivos en busca de cambios y que los vuelva a cargar si encuentra alguno. Eso probablemente se explica por sí mismo.

Abra http://localhost:8000/ en su navegador web favorito:

Hello, async Django!

No es lo más emocionante del mundo, pero bueno, es un comienzo. Vale la pena señalar que ejecutar esta vista con un servidor de desarrollo incorporado de Django dará como resultado exactamente la misma funcionalidad y salida. Esto se debe a que en realidad no estamos haciendo nada asíncrono en el controlador.

HTTPX

Vale la pena señalar que el soporte asíncrono es totalmente compatible con versiones anteriores, por lo que puede combinar vistas asíncronas y sincronizadas, middleware y pruebas. Django ejecutará cada uno en el contexto de ejecución adecuado.

Para demostrar esto, agregue algunas vistas nuevas:

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")

Actualice las URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Instalar HTTPX :

(env)$ pip install httpx

Con el servidor en ejecución, vaya a http://localhost:8000/async/ . Inmediatamente debería ver la respuesta:

Non-blocking HTTP request

En tu terminal deberías ver:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Aquí, la respuesta HTTP se devuelve antes de la primera llamada de suspensión.

A continuación, navegue hasta http://localhost:8000/sync/ . Debería tomar alrededor de cinco segundos obtener la respuesta:

Blocking HTTP request

Gire a la terminal:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

Aquí, la respuesta HTTP se envía después del ciclo y la solicitud se https://httpbin.org/completa.

Fumar algunas carnes

Para simular más un escenario del mundo real de cómo aprovecharía la sincronización, veamos cómo ejecutar varias operaciones de forma asíncrona, agregar los resultados y devolverlos a la persona que llama.

De vuelta en la URLconf de su proyecto, cree una nueva ruta en smoke_some_meats:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

De vuelta en sus vistas, cree una nueva función auxiliar asincrónica llamada smoke. Esta función toma dos parámetros: una lista de cadenas llamadas smokablesy una cadena llamada flavor. Estos por defecto son una lista de carnes fumables y "Sweet Baby Ray's", respectivamente.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    for smokable in smokables:
        print(f"Smoking some {smokable}...")
        print(f"Applying the {flavor}...")
        print(f"{smokable.capitalize()} smoked.")

    return len(smokables)

El ciclo for aplica asincrónicamente el sabor (léase: Sweet Baby Ray's) a los ahumables (léase: carnes ahumadas).

No olvides la importación:

from typing import List

Listse utiliza para capacidades adicionales de escritura. Esto no es obligatorio y se puede omitir fácilmente (simplemente elimine la : List[str]siguiente declaración del parámetro "smokables").

A continuación, agregue dos ayudantes asíncronos más:

async def get_smokables():
    print("Getting smokeables...")

    await asyncio.sleep(2)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning smokeable")
        return [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]


async def get_flavor():
    print("Getting flavor...")

    await asyncio.sleep(1)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning flavor")
        return random.choice(
            [
                "Sweet Baby Ray's",
                "Stubb's Original",
                "Famous Dave's",
            ]
        )

Asegúrate de agregar la importación:

import random

Cree la vista asíncrona que usa las funciones asíncronas:

# hello_async/views.py

async def smoke_some_meats(request):
    results = await asyncio.gather(*[get_smokables(), get_flavor()])
    total = await asyncio.gather(*[smoke(results[0], results[1])])
    return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

Esta vista llama a las funciones get_smokablesy al get_flavormismo tiempo. Dado smokeque depende de los resultados de ambos get_smokablesy get_flavor, solíamos gatheresperar a que se completara cada tarea asíncrona.

Tenga en cuenta que en una vista de sincronización regular, get_smokablesy get_flavorse manejaría uno a la vez. Además, la vista asincrónica producirá la ejecución y permitirá que se procesen otras solicitudes mientras se procesan las tareas asincrónicas, lo que permite que el mismo proceso maneje más solicitudes en un período de tiempo determinado.

Finalmente, se devuelve una respuesta para informar al usuario que su deliciosa comida BBQ está lista.

Excelente. Guarde el archivo, luego regrese a su navegador y navegue hasta http://localhost:8000/smoke_some_meats/ . Debería tomar unos segundos obtener la respuesta:

Smoked 6 meats with Sweet Baby Ray's!

En su consola, debería ver:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO:     127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Tome nota del orden de las siguientes declaraciones impresas:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Esta es la asincronía en el trabajo: mientras la get_smokablesfunción duerme, la get_flavorfunción termina de procesar.

Carnes Quemadas

sincronizar llamada

P: ¿Qué sucede si realiza una llamada síncrona dentro de una vista asíncrona?

Lo mismo que sucedería si llamara a una función no asíncrona desde una vista no asíncrona.

--

Para ilustrar esto, cree una nueva función de ayuda en sus vistas.py llamada oversmoke:

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")

Muy sencillo: solo estamos esperando sincrónicamente durante cinco segundos.

Cree la vista que llama a esta función:

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")

Por último, conecte la ruta en la URLconf de su proyecto:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Visite la ruta en el navegador en http://localhost:8000/burn_some_meats :

Burned some meats.

Observe cómo tardó cinco segundos en obtener finalmente una respuesta del navegador. También debería haber recibido la salida de la consola al mismo tiempo:

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

Posiblemente valga la pena señalar que sucederá lo mismo independientemente del servidor que esté utilizando, ya sea basado en WSGI o ASGI.

Llamadas sincronizadas y asíncronas

P: ¿Qué sucede si realiza una llamada síncrona y asíncrona dentro de una vista asíncrona?

No hagas esto.

Las vistas sincrónicas y asincrónicas tienden a funcionar mejor para diferentes propósitos. Si tiene la funcionalidad de bloqueo en una vista asíncrona, en el mejor de los casos no será mejor que simplemente usar una vista síncrona.

Sincronizar a asíncrono

Si necesita realizar una llamada síncrona dentro de una vista asíncrona (como interactuar con la base de datos a través de Django ORM, por ejemplo), use sync_to_async ya sea como contenedor o decorador.

Ejemplo:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync, thread_sensitive=False)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

¿Te diste cuenta de que configuramos el thread_sensitiveparámetro en False? Esto significa que la función síncrona http_call_sync, se ejecutará en un nuevo subproceso. Revise los documentos para obtener más información.

Agregue la importación a la parte superior:

from asgiref.sync import sync_to_async

Agregue la URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Pruébelo en su navegador en http://localhost:8000/sync_to_async/ .

En tu terminal deberías ver:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Con sync_to_async, la llamada síncrona de bloqueo se procesó en un subproceso en segundo plano, lo que permitió que la respuesta HTTP se devolviera antes de la primera llamada de suspensión.

Vistas de apio y asíncronas

P: ¿Sigue siendo necesario Celery con las vistas asíncronas de Django?

Eso depende.

Las vistas asíncronas de Django ofrecen una funcionalidad similar a una tarea o cola de mensajes sin la complejidad. Si está usando (o está considerando) Django y quiere hacer algo simple (y no le importa la confiabilidad), las vistas asíncronas son una excelente manera de lograr esto de manera rápida y sencilla. Si necesita realizar procesos en segundo plano mucho más pesados ​​​​y de larga duración, aún querrá usar Celery o RQ.

Cabe señalar que para usar las vistas asíncronas de manera efectiva, solo debe tener llamadas asíncronas en la vista. Las colas de tareas, por otro lado, usan trabajadores en procesos separados y, por lo tanto, son capaces de ejecutar llamadas síncronas en segundo plano, en múltiples servidores.

Por cierto, de ninguna manera debe elegir entre vistas asíncronas y una cola de mensajes; puede usarlas fácilmente en conjunto. Por ejemplo: podría usar una vista asíncrona para enviar un correo electrónico o hacer una modificación única de la base de datos, pero hacer que Celery limpie su base de datos a una hora programada todas las noches o generar y enviar informes de clientes.

Cuándo usar

Para proyectos totalmente nuevos, si lo suyo es asincrónico, aproveche las vistas asincrónicas y escriba sus procesos de E/S de la forma más asincrónica posible. Dicho esto, si la mayoría de sus vistas solo necesitan hacer llamadas a una base de datos y hacer un procesamiento básico antes de devolver los datos, no verá un gran aumento (si lo hay) en comparación con las vistas sincronizadas.

Para proyectos brownfield, si tiene pocos o ningún proceso de E/S, quédese con las vistas sincronizadas. Si tiene varios procesos de E/S, calcule lo fácil que será reescribirlos de forma asíncrona. Reescribir la E/S sincronizada a asíncrona no es fácil, por lo que probablemente querrá optimizar su E/S sincronizada y sus vistas antes de intentar reescribir a asíncrona. Además, nunca es una buena idea mezclar procesos de sincronización con sus vistas asíncronas.

En producción, asegúrese de usar Gunicorn para administrar Uvicorn con el fin de aprovechar tanto la simultaneidad (a través de Uvicorn) como el paralelismo (a través de los trabajadores de Gunicorn):

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Conclusión

En conclusión, aunque este fue un caso de uso simple, debería darle una idea aproximada de las posibilidades que abren las vistas asincrónicas de Django. Algunas otras cosas que puede probar en sus vistas asíncronas son enviar correos electrónicos, llamar a API de terceros y leer o escribir en archivos.

Fuente:  https://testdriven.io

#django #async 

Cómo Comenzar Con Las Vistas Asíncronas De Django

A Few Utils to Make Working with Streams and Futures Easier

flutter_async_utils

A few utils to make working with Streams and Futures easier. See the running example at: https://hpoul.github.io/flutter_async_utils/ (example/lib/main.dart)

StreamSubscriberMixin

Right now not much more than an extracted version of the StreamSubscriberMixin of https://github.com/flutter/plugins/blob/570932f/packages/firebase_database/lib/ui/utils/stream_subscriber_mixin.dart

FutureTaskStateMixin

Useful for providing asynchronous callbacks to show progress and prevent double-taps.

Work In Progress

Under heavy development. You might take a look at AuthPass which makes heavy use of those simple classes. For example _SelectFileWidgetState.

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add flutter_async_utils

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  flutter_async_utils: ^1.0.0+1

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:flutter_async_utils/flutter_async_utils.dart'; 

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_async_utils/flutter_async_utils.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

Future<void> _wait(int seconds) async {
  await Future<void>.delayed(Duration(seconds: seconds));
}

class _MyAppState extends State<MyApp> with FutureTaskStateMixin {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Container(
          width: double.infinity,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              const SizedBox(height: 160),
              const Text('Example App ;-)\n'),
              ElevatedButton(
                onPressed: asyncTaskCallback((progress) async {
                  await _wait(1);
                  progress.progressLabel = 'Doing more work';
                  await _wait(2);
                  progress.progressLabel = 'And even more.';
                  await _wait(3);
                }),
                child: const Text('Tap me.'),
              ),
              ...?withTask((task) => [
                    const CircularProgressIndicator(),
                    Text(task.progressLabel ?? ''),
                  ]),
            ],
          ),
        ),
      ),
    );
  }
} 

Download Details:

Author: hpoul

Source Code: https://github.com/hpoul/flutter_async_utils

#flutter #async 

A Few Utils to Make Working with Streams and Futures Easier
Neil  Morgan

Neil Morgan

1656831540

Como Começar Com As Visualizações Assíncronas Do Django

Escrever código assíncrono oferece a capacidade de acelerar seu aplicativo com pouco esforço. As versões do Django >= 3.1 suportam visualizações assíncronas, middleware e testes. Se você ainda não experimentou visualizações assíncronas, agora é um ótimo momento para colocá-las em prática.

Este tutorial mostra como começar com as visualizações assíncronas do Django.

Se você estiver interessado em aprender mais sobre o poder por trás do código assíncrono, juntamente com as diferenças entre threads, multiprocessamento e assíncrono em Python, confira meu artigo Acelerando o Python com simultaneidade, paralelismo e assíncrono .

Objetivos

Ao final deste tutorial, você deverá ser capaz de:

  1. Escreva uma visão assíncrona no Django
  2. Faça uma solicitação HTTP sem bloqueio em uma visualização do Django
  3. Simplifique as tarefas básicas em segundo plano com as visualizações assíncronas do Django
  4. Use sync_to_asyncpara fazer uma chamada síncrona dentro de uma visualização assíncrona
  5. Explique quando você deve e não deve usar visualizações assíncronas

Você também deve ser capaz de responder às seguintes perguntas:

  1. E se você fizer uma chamada síncrona dentro de uma visualização assíncrona?
  2. E se você fizer uma chamada síncrona e assíncrona dentro de uma visualização assíncrona?
  3. O aipo ainda é necessário com as visualizações assíncronas do Django?

Pré-requisitos

Contanto que você já esteja familiarizado com o próprio Django, adicionar funcionalidade assíncrona a visualizações não baseadas em classe é extremamente simples.

Dependências

  1. Python >= 3,10
  2. Django >= 4.0
  3. Uvicorn
  4. HTTPX

O que é ASGI?

ASGI significa Interface de Gateway de Servidor Assíncrono. É a continuação moderna e assíncrona do WSGI , fornecendo um padrão para a criação de aplicativos Web assíncronos baseados em Python.

Outra coisa que vale a pena mencionar é que ASGI é compatível com WSGI, tornando-se uma boa desculpa para mudar de um servidor WSGI como Gunicorn ou uWSGI para um servidor ASGI como Uvicorn ou Daphne , mesmo se você não estiver pronto para mudar para escrever aplicativos assíncronos .

Criando o aplicativo

Crie um novo diretório de projeto junto com um novo projeto Django:

$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject hello_async .

Sinta-se à vontade para trocar virtualenv e Pip por Poetry ou Pipenv . Para saber mais, revise Ambientes Python Modernos .

O Django executará suas visualizações assíncronas se você estiver usando o servidor de desenvolvimento integrado, mas na verdade não as executará de forma assíncrona, então executaremos o Django com o Uvicorn.

Instale-o:

(env)$ pip install uvicorn

Para executar seu projeto com o Uvicorn, você usa o seguinte comando da raiz do seu projeto:

uvicorn {name of your project}.asgi:application

No nosso caso seria:

(env)$ uvicorn hello_async.asgi:application

Em seguida, vamos criar nossa primeira visualização assíncrona. Adicione um novo arquivo para manter suas visualizações na pasta "hello_async" e adicione a seguinte visualização:

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")

Criar visualizações assíncronas no Django é tão simples quanto criar uma visualização síncrona -- tudo que você precisa fazer é adicionar a palavra- asyncchave.

Atualize os URLs:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]

Agora, em um terminal, na sua pasta raiz, execute:

(env)$ uvicorn hello_async.asgi:application --reload

O --reloadsinalizador diz ao Uvicorn para observar seus arquivos em busca de alterações e recarregar se encontrar algum. Isso foi provavelmente auto-explicativo.

Abra http://localhost:8000/ em seu navegador favorito:

Hello, async Django!

Não é a coisa mais excitante do mundo, mas, ei, é um começo. Vale a pena notar que executar esta visão com um servidor de desenvolvimento embutido do Django resultará exatamente na mesma funcionalidade e saída. Isso ocorre porque, na verdade, não estamos fazendo nada assíncrono no manipulador.

HTTPX

Vale a pena notar que o suporte assíncrono é totalmente compatível com versões anteriores, para que você possa misturar exibições assíncronas e sincronizadas, middleware e testes. O Django executará cada um no contexto de execução apropriado.

Para demonstrar isso, adicione algumas novas visualizações:

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")

Atualize os URLs:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Instale o HTTPX :

(env)$ pip install httpx

Com o servidor em execução, navegue até http://localhost:8000/async/ . Você deve ver imediatamente a resposta:

Non-blocking HTTP request

No seu terminal você deverá ver:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Aqui, a resposta HTTP é enviada de volta antes da primeira chamada de suspensão.

Em seguida, navegue até http://localhost:8000/sync/ . Deve levar cerca de cinco segundos para obter a resposta:

Blocking HTTP request

Ligue para o terminal:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

Aqui, a resposta HTTP é enviada após o loop e a solicitação é https://httpbin.org/concluída.

Fumar algumas carnes

Para simular mais um cenário do mundo real de como você aproveitaria a assíncrona, vejamos como executar várias operações de forma assíncrona, agregar os resultados e devolvê-los ao chamador.

De volta ao URLconf do seu projeto, crie um novo caminho em smoke_some_meats:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

De volta às suas visualizações, crie uma nova função auxiliar assíncrona chamada smoke. Esta função recebe dois parâmetros: uma lista de strings chamadas smokablese uma string chamada flavor. Estes padrões para uma lista de carnes defumadas e "Sweet Baby Ray's", respectivamente.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    for smokable in smokables:
        print(f"Smoking some {smokable}...")
        print(f"Applying the {flavor}...")
        print(f"{smokable.capitalize()} smoked.")

    return len(smokables)

O loop for aplica o sabor de forma assíncrona (leia-se: Sweet Baby Ray's) aos fumáveis ​​(leia-se: carnes defumadas).

Não esqueça da importação:

from typing import List

Listé usado para recursos extras de digitação. Isso não é necessário e pode ser facilmente omitido (apenas nix a : List[str]seguir a declaração do parâmetro "smokables").

Em seguida, adicione mais dois auxiliares assíncronos:

async def get_smokables():
    print("Getting smokeables...")

    await asyncio.sleep(2)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning smokeable")
        return [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]


async def get_flavor():
    print("Getting flavor...")

    await asyncio.sleep(1)
    async with httpx.AsyncClient() as client:
        await client.get("https://httpbin.org/")

        print("Returning flavor")
        return random.choice(
            [
                "Sweet Baby Ray's",
                "Stubb's Original",
                "Famous Dave's",
            ]
        )

Certifique-se de adicionar a importação:

import random

Crie a visualização assíncrona que usa as funções assíncronas:

# hello_async/views.py

async def smoke_some_meats(request):
    results = await asyncio.gather(*[get_smokables(), get_flavor()])
    total = await asyncio.gather(*[smoke(results[0], results[1])])
    return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

Essa exibição chama as funções get_smokablese get_flavorsimultaneamente. Como smokedepende dos resultados de get_smokablese get_flavor, costumávamos gatheraguardar a conclusão de cada tarefa assíncrona.

Lembre-se de que, em uma exibição de sincronização regular, get_smokablesseria get_flavortratado um de cada vez. Além disso, a visualização assíncrona produzirá a execução e permitirá que outras solicitações sejam processadas enquanto as tarefas assíncronas são processadas, o que permite que mais solicitações sejam tratadas pelo mesmo processo em um determinado período de tempo.

Por fim, uma resposta é retornada para informar ao usuário que sua deliciosa refeição de churrasco está pronta.

Excelente. Salve o arquivo, volte ao seu navegador e navegue até http://localhost:8000/smoke_some_meats/ . Deve levar alguns segundos para obter a resposta:

Smoked 6 meats with Sweet Baby Ray's!

Em seu console, você deverá ver:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO:     127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Observe a ordem das seguintes instruções de impressão:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Isso é assincronicidade no trabalho: enquanto a get_smokablesfunção dorme, a get_flavorfunção termina o processamento.

Carnes Queimadas

Sincronizar chamada

P: E se você fizer uma chamada síncrona dentro de uma visualização assíncrona?

A mesma coisa que aconteceria se você chamasse uma função não assíncrona de uma exibição não assíncrona.

--

Para ilustrar isso, crie uma nova função auxiliar em seu views.py chamada oversmoke:

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")

Muito simples: estamos apenas aguardando de forma síncrona por cinco segundos.

Crie a view que chama esta função:

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")

Por fim, conecte a rota no URLconf do seu projeto:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Visite a rota no navegador em http://localhost:8000/burn_some_meats :

Burned some meats.

Observe como levou cinco segundos para finalmente obter uma resposta do navegador. Você também deve ter recebido a saída do console ao mesmo tempo:

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

Possivelmente vale a pena notar que a mesma coisa acontecerá independentemente do servidor que você estiver usando, seja ele baseado em WSGI ou ASGI.

Chamadas sincronizadas e assíncronas

P: E se você fizer uma chamada síncrona e assíncrona dentro de uma visualização assíncrona?

Não faça isso.

Visualizações síncronas e assíncronas tendem a funcionar melhor para diferentes propósitos. Se você tiver a funcionalidade de bloqueio em uma exibição assíncrona, na melhor das hipóteses não será melhor do que apenas usar uma exibição síncrona.

Sincronizar com assíncrono

Se você precisar fazer uma chamada síncrona dentro de uma visão assíncrona (como interagir com o banco de dados através do Django ORM, por exemplo), use sync_to_async como wrapper ou decorador.

Exemplo:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync, thread_sensitive=False)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

Você notou que definimos o thread_sensitiveparâmetro para False? Isso significa que a função síncrona, http_call_sync, será executada em um novo thread. Revise os documentos para obter mais informações.

Adicione a importação ao topo:

from asgiref.sync import sync_to_async

Adicione o URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]

Teste-o em seu navegador em http://localhost:8000/sync_to_async/ .

No seu terminal você deverá ver:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Usando sync_to_async, a chamada síncrona de bloqueio foi processada em um thread em segundo plano, permitindo que a resposta HTTP fosse enviada de volta antes da primeira chamada de suspensão.

Aipo e visualizações assíncronas

P: O Celery ainda é necessário com as visualizações assíncronas do Django?

Depende.

As visualizações assíncronas do Django oferecem funcionalidade semelhante a uma fila de tarefas ou mensagens sem a complexidade. Se você está usando (ou está considerando) o Django e quer fazer algo simples (e não se importa com confiabilidade), as visualizações assíncronas são uma ótima maneira de fazer isso de forma rápida e fácil. Se você precisar executar processos em segundo plano muito mais pesados ​​e de longa duração, ainda precisará usar o Celery ou o RQ.

Deve-se observar que, para usar exibições assíncronas de maneira eficaz, você deve ter apenas chamadas assíncronas na exibição. As filas de tarefas, por outro lado, usam trabalhadores em processos separados e, portanto, são capazes de executar chamadas síncronas em segundo plano, em vários servidores.

A propósito, você não deve escolher entre exibições assíncronas e uma fila de mensagens - você pode usá-las facilmente em conjunto. Por exemplo: você pode usar uma exibição assíncrona para enviar um email ou fazer uma modificação única no banco de dados, mas fazer com que o Celery limpe seu banco de dados em um horário agendado todas as noites ou gere e envie relatórios de clientes.

Quando usar

Para projetos greenfield, se você gosta de assíncrona, aproveite as visualizações assíncronas e escreva seus processos de E/S de maneira assíncrona o máximo possível. Dito isso, se a maioria de suas visualizações precisar apenas fazer chamadas para um banco de dados e fazer algum processamento básico antes de retornar os dados, você não verá muito aumento (se houver) sobre apenas manter as visualizações de sincronização.

Para projetos brownfield, se você tiver pouco ou nenhum processo de E/S, fique com as visualizações de sincronização. Se você tiver vários processos de E/S, avalie como será fácil reescrevê-los de maneira assíncrona. Reescrever a E/S de sincronização para assíncrona não é fácil, portanto, você provavelmente desejará otimizar sua E/S de sincronização e exibições antes de tentar reescrever para assíncrona. Além disso, nunca é uma boa ideia misturar processos de sincronização com suas visualizações assíncronas.

Na produção, certifique-se de usar o Gunicorn para gerenciar o Uvicorn para aproveitar a simultaneidade (via Uvicorn) e o paralelismo (via trabalhadores do Gunicorn):

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Conclusão

Concluindo, embora este seja um caso de uso simples, ele deve lhe dar uma ideia aproximada das possibilidades que as visões assíncronas do Django abrem. Algumas outras coisas para tentar em suas visualizações assíncronas são enviar e-mails, chamar APIs de terceiros e ler/gravar em arquivos.

Fonte:  https://testdrive.io

#django #async 

Como Começar Com As Visualizações Assíncronas Do Django
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#

Qué son Async, Await y Task en C#

El rendimiento lo es todo cuando intenta publicar en la web, dispositivos móviles, consolas e incluso algunas de las PC de gama baja. Un juego o aplicación que se ejecuta a menos de 30 FPS puede causar frustración a los usuarios. Echemos un vistazo a algunas de las cosas que podemos usar para aumentar el rendimiento al reducir la carga en la CPU.

En esta publicación, cubriremos qué son async, awaity Tasken C# y cómo usarlos en Unity para obtener rendimiento en su proyecto. A continuación, veremos algunos de los paquetes integrados de Unity: rutinas, el sistema de trabajo de C# y el compilador de ráfagas. Veremos qué son, cómo usarlos y cómo aumentan el rendimiento en su proyecto.

Para comenzar este proyecto, usaré Unity 2021.3.4f1. No he probado este código en ninguna otra versión de Unity; todos los conceptos aquí deberían funcionar en cualquier versión de Unity posterior a Unity 2019.3. Sus resultados de rendimiento pueden diferir si usa una versión anterior, ya que Unity realizó algunas mejoras significativas con el modelo de programación async/await en 2021. Obtenga más información al respecto en el blog de Unity Unity and .NET, what's next , en particular, la sección denominada "Modernizing the Tiempo de ejecución de la unidad”.

Creé un nuevo proyecto Core 2D (URP), pero puede usarlo en cualquier tipo de proyecto que desee.

Tengo un sprite que obtuve de Space Shooter (Redux, además de fuentes y sonidos) de Kenney Vleugels .

Creé un prefabricado enemigo que contiene un Sprite Render y un Enemy Component. El Enemy Component es un MonoBehaviourque tiene a Transformy a floatpara realizar un seguimiento de la posición y la velocidad para moverse en el eje y:

using UnityEngine;

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

Qué async, awaity Taskson en C#

¿Qué es async?

En C#, los métodos pueden tener una asyncpalabra clave delante de ellos, lo que significa que los métodos son métodos asincrónicos. Esta es solo una forma de decirle al compilador que queremos poder ejecutar el código interno y permitir que la persona que llama a ese método continúe con la ejecución mientras espera que finalice este método.

Un ejemplo de esto sería cocinar una comida. Comenzarás a cocinar la carne, y mientras la carne se cocina y esperas a que termine, comenzarás a hacer los lados. Mientras se cocina la comida, empezarías a poner la mesa. Un ejemplo de esto en código sería static async Task<Steak> MakeSteak(int number).

Unity también tiene todo tipo de métodos incorporados a los que puede llamar de forma asíncrona; consulte los documentos de Unity para obtener una lista de métodos. Con la forma en que Unity maneja la administración de la memoria, utiliza corrutinas oAsyncOperation el sistema de trabajo de C# .

¿Qué es awaity cómo se usa?

En C#, puede esperar a que se complete una operación asíncrona usando la awaitpalabra clave. Esto se usa dentro de cualquier método que tenga la asyncpalabra clave para esperar a que continúe una operación:

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 los documentos de Microsoft para obtener más información sobre await.

¿Qué es un Tasky cómo se usa?

A Taskes un método asíncrono que realiza una única operación y no devuelve ningún valor. Para a Taskque devuelve un valor, usaríamos Task<TResult>.

Para usar una tarea, la creamos como crear cualquier objeto nuevo en C#: Task t1 = new Task(void Action). A continuación, comenzamos la tarea t1.wait. Por último, esperamos a que la tarea se complete con t1.wait.

Hay varias formas de crear, iniciar y ejecutar tareas. Task t2 = Task.Run(void Action)creará y comenzará una tarea. await Task.Run(void Action)creará, iniciará y esperará a que se complete la tarea. Podemos usar la forma alternativa más común con Task t3 = Task.Factory.Start(void Action).

Hay varias formas en que podemos esperar a que se complete la tarea. int index = Task.WaitAny(Task[])esperará a que se complete cualquier tarea y nos dará el índice de la tarea completada en la matriz. await Task.WaitAll(Task[])esperará a que se completen todas las tareas.

Para obtener más información sobre las tareas, consulte los documentos de Microsoft .

un ejemplo sencillotask

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");
}

Cómo afecta la tarea al rendimiento

Ahora comparemos el rendimiento de una tarea con el rendimiento de un método.

Necesitaré una clase estática que pueda usar en todas mis comprobaciones de rendimiento. Tendrá un método y una tarea que simule una operación intensiva en rendimiento. Tanto el método como la tarea realizan exactamente la misma operación:

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));
           }
       });
   }
}

Ahora necesito uno MonoBehaviourque pueda usar para probar el impacto en el rendimiento de la tarea y el método. Solo para poder ver un mejor impacto en el rendimiento, fingiré que quiero ejecutar esto en diez objetos de juego diferentes. También realizaré un seguimiento de la cantidad de tiempo Updateque tarda en ejecutarse el método.

En Update, obtengo la hora de inicio. Si estoy probando el método, recorro todos los objetos del juego simulado y llamo al método intensivo en rendimiento. Si estoy probando la tarea, creo un nuevo Taskbucle de matriz a través de todos los objetos del juego simulado y agrego la tarea de rendimiento intensivo a la matriz de tareas. Entonces awaitpara todas las tareas para completar. Fuera de la verificación del tipo de método, actualizo el tiempo del método, convirtiéndolo a ms. También lo 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");
   }
}

El método intensivo tarda alrededor de 65 ms en completarse y el juego se ejecuta a unos 12 FPS.

La tarea intensiva tarda alrededor de 4 ms en completarse y el juego se ejecuta a unos 200 FPS.

Intentemos esto con mil enemigos:

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;
  }

Mostrar y mover mil enemigos con el método tomó alrededor de 150 ms con una velocidad de cuadro de aproximadamente 7 FPS.

Mostrar y mover mil enemigos con una tarea tomó alrededor de 50 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Por qué no useTasks?

Las tareas son extremadamente eficientes y reducen la tensión en el rendimiento de su sistema. Incluso puede usarlos en múltiples subprocesos usando la Biblioteca paralela de tareas (TPL).

Sin embargo, existen algunos inconvenientes al usarlos en Unity. El principal inconveniente de usar TaskUnity es que todos se ejecutan en el Mainsubproceso. Sí, podemos hacer que se ejecuten en otros subprocesos, pero Unity ya realiza su propia gestión de subprocesos y memoria, y puede crear errores al crear más subprocesos que núcleos de CPU, lo que genera competencia por los recursos.

Las tareas también pueden ser difíciles de realizar correctamente y depurar. Al escribir el código original, terminé con todas las tareas ejecutándose, pero ninguno de los enemigos se movió en la pantalla. Terminó siendo que necesitaba devolver el Task[]que creé en el archivo Task.

Las tareas crean mucha basura que afecta el rendimiento. Tampoco aparecen en el generador de perfiles, por lo que si tiene uno que afecta el rendimiento, es difícil rastrearlo. Además, he notado que a veces mis tareas y funciones de actualización continúan ejecutándose desde otras escenas.

Corrutinas de Unity

Según Unity , "una corrutina es una función que puede suspender su ejecución (rendimiento) hasta que finalice la instrucción de rendimiento dada " .

Lo que esto significa es que podemos ejecutar código y esperar a que se complete una tarea antes de continuar. Esto es muy parecido a un método asíncrono. Utiliza un tipo de retorno IEnumeratory we yield returnen lugar de await.

Unity tiene varios tipos diferentes de instrucciones de rendimiento que podemos usar, es decir, WaitForSeconds, WaitForEndOfFrame, WaitUntilo WaitWhile.

Para iniciar las corrutinas, necesitamos MonoBehavioury usamos el MonoBehaviour.StartCoroutine.

Para detener una rutina antes de que se complete, usamos MonoBehaviour.StopCoroutine. Al detener las rutinas, asegúrese de utilizar el mismo método que utilizó para iniciarlas.

Los casos de uso comunes para corrutinas en Unity son esperar a que se carguen los activos y crear temporizadores de enfriamiento.

Ejemplo: un cargador de escenas usando una corrutina

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);
   }
}

Comprobar el impacto de una rutina en el rendimiento

Veamos cómo el uso de una corrutina afecta el rendimiento de nuestro proyecto. Solo voy a hacer esto con el método intensivo en rendimiento.

Agregué Coroutinea la MethodTypeenumeración y las variables para realizar un seguimiento de su estado:

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

   ...

   private Coroutine m_performanceCoroutine;

Creé la rutina. Esto es similar a la tarea y el método de alto rendimiento que creamos anteriormente con código agregado para actualizar el tiempo del 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;
   }

En el Updatemétodo, agregué el cheque para la rutina. También modifiqué el tiempo del método, actualicé el código y agregué código para detener la rutina si se estaba ejecutando y cambiamos el tipo de 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;
   }

La rutina intensiva tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

El sistema de trabajos de C# y el compilador de ráfagas

¿Qué es el sistema de trabajo de C#?

El sistema de trabajo de C# es la implementación de Unity de tareas que son fáciles de escribir, no generan la basura que generan las tareas y utilizan los subprocesos de trabajo que Unity ya ha creado. Esto corrige todas las desventajas de las tareas.

Unity compara los trabajos como subprocesos, pero dicen que un trabajo realiza una tarea específica. Los trabajos también pueden depender de otros trabajos para completarse antes de ejecutarse; esto soluciona el problema con la tarea que tenía que no se movió correctamente Unitsporque dependía de que otra tarea se completara primero.

Unity se ocupa automáticamente de las dependencias laborales. El sistema de trabajo también tiene un sistema de seguridad incorporado principalmente para proteger contra las condiciones de carrera . Una advertencia con los trabajos es que solo pueden contener variables miembro que sean tipos blittables o tipos NativeContainer ; esto es un inconveniente del sistema de seguridad.

Para usar el sistema de trabajos, cree el trabajo, programe el trabajo, espere a que se complete y luego use los datos devueltos por el trabajo. El sistema de trabajo es necesario para usar la pila de tecnología orientada a datos (DOTS) de Unity.

Para obtener más detalles sobre el sistema de trabajo, consulte la documentación de Unity .

Creando un trabajo

Para crear un trabajo, cree uno stuctque implemente una de las IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobes un trabajo básico. IJobFory IJobForParallelse utilizan para realizar la misma operación en cada elemento de un contenedor nativo o para varias iteraciones. La diferencia entre ellos es que IJobFor se ejecuta en un solo subproceso donde IJobForParallelse dividirá entre varios subprocesos.

Lo usaré IJobpara crear un trabajo de operación intensiva IJobFory IJobForParallelpara crear un trabajo que moverá a múltiples enemigos; esto es solo para que podamos ver los diferentes impactos en el rendimiento. Estos trabajos serán idénticos a las tareas y métodos que creamos anteriormente:

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

Agregue las variables miembro. En mi caso, mi IJobno necesita ninguno. Los IJobFory IJobParallelFornecesitan un valor flotante para el tiempo delta actual, ya que los trabajos no tienen un concepto de marco; operan fuera de Unity MonoBehaviour. También necesitan una matriz float3para la posición y una matriz para la velocidad de movimiento en el eje 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;
}

El último paso es implementar el Executemétodo requerido. Los IJobFory IJobForParallelrequieren un intpara el índice de la iteración actual que se está ejecutando el trabajo.

La diferencia es que en lugar de acceder a los movimientos del enemigo transform, usamos la matriz que está en el trabajo:

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();
   }

Programar un trabajo

Primero, necesitamos instalar el trabajo y completar los datos del trabajo:

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

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

Luego programamos el trabajo con JobHandle jobHandle = jobData.Schedule();. El Schedulemétodo devuelve un JobHandleque se puede utilizar más adelante.

No podemos programar un trabajo desde dentro de un trabajo. Sin embargo, podemos crear nuevos trabajos y completar sus datos desde dentro de un trabajo. Una vez que se ha programado un trabajo, no se puede interrumpir.

El trabajo intensivo en rendimiento

Creé un método que crea un nuevo trabajo y lo programa. Devuelve el identificador de trabajo que puedo usar en mi updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

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

Agregué el trabajo a mi enumeración. Luego, en el Updatemétodo, agrego el casea la switchsección. Creé una matriz de JobHandles. Luego recorro todos los objetos del juego simulado y agrego un trabajo programado para cada uno a la matriz:

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;
       }

       ...
   }
}

el MoveEnemyyMoveEnemyParallelJob

A continuación, agregué los trabajos a mi enumeración. Luego, en el Updatemétodo, llamo a un nuevo MoveEnemyJobmétodo, pasando el tiempo delta. Normalmente usaría el JobForo el 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;
       }

       ...
   }

   ...

Lo primero que hago es establecer una matriz para las posiciones y una matriz para las moveYque pasaré a los trabajos. Luego lleno estas matrices con los datos de los enemigos. A continuación, creo el trabajo y configuro los datos del trabajo según el trabajo que quiero usar. Después de eso, programo el trabajo según el trabajo que quiero usar y el tipo de programación que quiero hacer:

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);

           }
   }

Recuperar los datos de un trabajo

Tenemos que esperar a que se complete el trabajo. Podemos obtener el estado del JobHandleque usamos cuando programamos el trabajo para completarlo. Esto esperará a que se complete el trabajo antes de continuar con la ejecución: > handle.Complete();o JobHandle.CompleteAll(jobHandles). Una vez que se completa el trabajo, el NativeContainerque usamos para configurar el trabajo tendrá todos los datos que necesitamos usar. Una vez que recuperamos los datos de ellos, tenemos que desecharlos adecuadamente.

El trabajo intensivo en rendimiento

Esto es bastante simple ya que no estoy leyendo ni escribiendo ningún dato en el trabajo. Espero a que se completen todos los trabajos que estaban programados y luego me deshago de la 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;
       }

       ...
   }
}

El trabajo intensivo tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

el MoveEnemytrabajo

Agrego los cheques completos apropiados:

   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();
       }
   }

Después de las comprobaciones del tipo de método, recorro a todos los enemigos, establezco sus transformposiciones y moveYlos datos que se establecieron en el trabajo. A continuación, me deshago adecuadamente de las matrices nativas:

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();
   }

Mostrar y mover mil enemigos con el trabajo tomó alrededor de 160 ms con una velocidad de cuadro de aproximadamente 7 FPS sin ganancias de rendimiento.

Mostrar y mover mil enemigos con el trabajo en paralelo tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Qué es el compilador de ráfagas en Unity?

El compilador de ráfagas es un compilador que traduce de bytecode a código nativo. Usar esto con C# Job System mejora la calidad del código generado, lo que le brinda un aumento significativo en el rendimiento y reduce el consumo de batería en los dispositivos móviles.

Para usar esto, simplemente dígale a Unity que desea usar la compilación en ráfaga en el trabajo con el [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
{
   ...
}

Luego, en Unity, seleccione Trabajos > Ráfaga > Habilitar finalización

Burst es Just-In-Time (JIT) mientras está en el Editor, lo que significa que esto puede estar inactivo mientras está en Modo de reproducción. Cuando construyes tu proyecto es Ahead-Of-Time (AOT), lo que significa que esto debe habilitarse antes de construir tu proyecto. Puede hacerlo editando la sección Configuración de AOT de ráfaga en la ventana Configuración del proyecto .

Para obtener más detalles sobre el compilador de ráfagas, consulte la documentación de Unity .

Un trabajo de alto rendimiento con el compilador de ráfagas

Un trabajo intensivo con ráfaga tarda alrededor de 3 ms en completarse y el juego se ejecuta a unos 150 FPS.

Mostrando y moviendo mil enemigos, el trabajo con ráfaga tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

Mostrando y moviendo mil enemigos, el trabajo paralelo con la ráfaga tomó alrededor de 6 ms con una velocidad de cuadro de aproximadamente 80 a 90 FPS.

Conclusión

Podemos utilizar Taskpara aumentar el rendimiento de nuestras aplicaciones de Unity, pero existen varios inconvenientes al usarlos. Es mejor usar las cosas que vienen empaquetadas en Unity dependiendo de lo que queramos hacer. Usar corrutinas si queremos esperar a que algo termine de cargarse de forma asíncrona; podemos iniciar la rutina y no detener la ejecución del proceso de nuestro programa.

Podemos usar el sistema de trabajo de C# con el compilador de ráfagas para obtener una ganancia masiva en el rendimiento sin tener que preocuparnos por todas las cuestiones de administración de subprocesos cuando se realizan tareas de procesos intensivos. Usando los sistemas incorporados, estamos seguros de que se hace de una manera segura que no causa errores o errores no deseados.

Las tareas se ejecutaron un poco mejor que los trabajos sin usar el compilador de ráfagas, pero eso se debe a la pequeña sobrecarga adicional detrás de escena para configurar todo de manera segura para nosotros. Al usar el compilador de ráfagas, nuestros trabajos realizaron nuestras tareas. Cuando necesite todo el rendimiento adicional que puede obtener, use el sistema de trabajo de C# con ráfaga.

Los archivos del proyecto para esto se pueden encontrar en mi GitHub .

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

#csharp #async #await 

Qué son Async, Await y Task en C#