Limity wątków AsyncTask w systemie Android?

95

Tworzę aplikację, w której muszę aktualizować jakieś informacje za każdym razem, gdy użytkownik loguje się do systemu, korzystam również z bazy danych w telefonie. Do wszystkich tych operacji (aktualizacje, pobieranie danych z bazy danych itp.) Używam zadań asynchronicznych. Do tej pory nie widziałem, dlaczego nie powinienem ich używać, ale ostatnio doświadczyłem, że jeśli wykonam jakieś operacje, niektóre z moich zadań asynchronicznych po prostu zatrzymują się przed wykonaniem i nie skaczą do doInBackground. To było zbyt dziwne, żeby tak to zostawić, więc opracowałem kolejną prostą aplikację, aby sprawdzić, co jest nie tak. Co dziwne, zachowuję się tak samo, gdy liczba wszystkich zadań asynchronicznych osiąga 5, a szósta zatrzymuje się przed wykonaniem.

Czy Android ma limit asyncTasks w aktywności / aplikacji? A może to tylko jakiś błąd i należy go zgłosić? Czy ktoś napotkał ten sam problem i może znalazł sposób obejścia tego problemu?

Oto kod:

Po prostu utwórz 5 z tych wątków, aby pracować w tle:

private class LongAsync extends AsyncTask<String, Void, String>
{
    @Override
    protected void onPreExecute()
    {
        Log.d("TestBug","onPreExecute");
        isRunning = true;
    }

    @Override
    protected String doInBackground(String... params)
    {
        Log.d("TestBug","doInBackground");
        while (isRunning)
        {

        }
        return null;
    }

    @Override
    protected void onPostExecute(String result)
    {
        Log.d("TestBug","onPostExecute");
    }
}

A potem utwórz ten wątek. Wejdzie do preExecute i zawiesi się (nie przejdzie do wykonania InBackground).

private class TestBug extends AsyncTask<String, Void, String>
{
    @Override
    protected void onPreExecute()
    {
        Log.d("TestBug","onPreExecute");

        waiting = new ProgressDialog(TestActivity.this);
        waiting.setMessage("Loading data");
        waiting.setIndeterminate(true);
        waiting.setCancelable(true);
        waiting.show();
    }

    @Override
    protected String doInBackground(String... params)
    {
        Log.d("TestBug","doInBackground");
        return null;
    }

    @Override
    protected void onPostExecute(String result)
    {
        waiting.cancel();
        Log.d("TestBug","onPostExecute");
    }
}
Mindaugas Svirskas
źródło

Odpowiedzi:

207

Wszystkie AsyncTasks są wewnętrznie kontrolowane przez udostępniony (statyczny) ThreadPoolExecutor i LinkedBlockingQueue . Gdy wywołasz executeAsyncTask, ThreadPoolExecutorwykona go, gdy będzie gotowy w przyszłości.

Pytanie „kiedy będę gotowy?” zachowanie a ThreadPoolExecutorjest kontrolowane przez dwa parametry, rozmiar puli rdzeniowej i maksymalny rozmiar puli . Jeśli obecnie aktywnych jest mniej niż rdzeń wątków puli i pojawia się nowe zadanie, moduł wykonawczy utworzy nowy wątek i natychmiast go wykona. Jeśli są uruchomione wątki o rozmiarze co najmniej rdzenia puli, spróbuje ustawić zadanie w kolejce i zaczeka, aż będzie dostępny bezczynny wątek (tj. Do zakończenia innego zadania). Jeśli nie można umieścić zadania w kolejce (kolejka może mieć maksymalną pojemność), utworzy nowy wątek (o maksymalnej wielkości puli), w którym zadania będą działać. Wątki bezczynne inne niż rdzeniowe mogą ostatecznie zostać wycofane. zgodnie z parametrem limitu czasu utrzymywania aktywności.

Przed Androidem 1.6 rozmiar puli podstawowej wynosił 1, a maksymalny - 10. Od Androida 1.6 rozmiar puli podstawowej to 5, a maksymalny rozmiar puli to 128. Rozmiar kolejki to 10 w obu przypadkach. Limit czasu utrzymywania aktywności wynosił 10 sekund przed 2,3 i 1 sekundę od tego czasu.

Mając to wszystko na uwadze, staje się jasne, dlaczego AsyncTaskwydaje się, że wykonuje tylko 5/6 Twoich zadań. Szóste zadanie jest ustawiane w kolejce, dopóki jedno z pozostałych nie zostanie ukończone. Jest to bardzo dobry powód, dla którego nie należy używać AsyncTasks do długotrwałych operacji - zapobiegnie to uruchomieniu innych AsyncTasks.

Dla kompletności, jeśli powtórzysz swoje ćwiczenie z więcej niż 6 zadaniami (np. 30), zobaczysz, że wejdzie więcej niż 6, gdy doInBackgroundkolejka zostanie zapełniona, a wykonawca zostanie popchnięty, aby utworzyć więcej wątków roboczych. Jeśli trzymałeś się długotrwałego zadania, powinieneś zobaczyć, że 20/30 stało się aktywne, a 10 nadal znajduje się w kolejce.

antonyt
źródło
2
„Jest to bardzo dobry powód, dla którego nie należy używać AsyncTasks do długotrwałych operacji”. Jakie są Twoje zalecenia dotyczące tego scenariusza? Ręczne odradzanie nowego wątku lub tworzenie własnej usługi wykonawczej?
user123321
2
Wykonawcy są w zasadzie abstrakcją na wierzchu wątków, zmniejszając potrzebę pisania złożonego kodu do zarządzania nimi. Oddziela Twoje zadania od sposobu ich wykonywania. Jeśli twój kod zależy tylko od modułu wykonawczego, łatwo jest zmienić w przejrzysty sposób liczbę używanych wątków, itp. Nie mogę wymyślić dobrego powodu, aby samemu tworzyć wątki, ponieważ nawet w przypadku prostych zadań ilość pracy z Egzekutor jest taki sam, jeśli nie mniejszy.
antonyt,
37
Pamiętaj, że od Androida 3.0+ domyślna liczba równoczesnych AsyncTasks została zmniejszona do 1. Więcej informacji: developer.android.com/reference/android/os/ ...
Kieran
Wow, wielkie dzięki za świetną odpowiedź. Wreszcie mam wyjaśnienie, dlaczego mój kod zawodzi tak sporadycznie i w tajemniczy sposób.
Chris Knight,
@antonyt, jeszcze jedna wątpliwość, anulowane AsyncTasks, czy będzie się liczyć do liczby AsyncTasks? tj. liczone w core pool sizelub maximum pool size?
nmxprime,
9

@antonyt ma właściwą odpowiedź, ale jeśli szukasz prostego rozwiązania, możesz sprawdzić Needle.

Dzięki niemu możesz zdefiniować niestandardowy rozmiar puli wątków i, w przeciwieństwie do AsyncTasktego, działa na wszystkich wersjach Androida tak samo. Dzięki niemu możesz powiedzieć takie rzeczy jak:

Needle.onBackgroundThread().withThreadPoolSize(3).execute(new UiRelatedTask<Integer>() {
   @Override
   protected Integer doWork() {
       int result = 1+2;
       return result;
   }

   @Override
   protected void thenDoUiRelatedWork(Integer result) {
       mSomeTextView.setText("result: " + result);
   }
});

lub takie rzeczy

Needle.onMainThread().execute(new Runnable() {
   @Override
   public void run() {
       // e.g. change one of the views
   }
}); 

Potrafi o wiele więcej. Sprawdź to na GitHub .

Zsolt Safrany
źródło
ostatnie zatwierdzenie miało miejsce 5 lat temu :(
ŁukaszTaraszka
5

Aktualizacja : od API 19 rozmiar puli wątków rdzenia został zmieniony, aby odzwierciedlić liczbę procesorów w urządzeniu, z minimum 2 i maksymalnie 4 na starcie, przy jednoczesnym wzroście do maksymalnego CPU * 2 +1 - odniesienie

// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

Należy również zauważyć, że chociaż domyślny moduł wykonawczy AsyncTask jest seryjny (wykonuje jedno zadanie na raz iw kolejności, w jakiej przychodzi), z metodą

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params)

możesz zapewnić wykonawcę do wykonywania zadań. Możesz dostarczyć THREAD_POOL_EXECUTORowi wykonawcę pod maską, ale bez serializacji zadań, lub możesz nawet stworzyć swój własny Executor i dostarczyć go tutaj. Jednak uważnie zwróć uwagę na ostrzeżenie w Javadocach.

Ostrzeżenie: Zezwolenie na równoległe uruchamianie wielu zadań z puli wątków zwykle nie jest tym, czego się chce, ponieważ kolejność ich operacji nie jest zdefiniowana. Na przykład, jeśli te zadania są używane do modyfikowania dowolnego wspólnego stanu (np. Zapisywanie pliku w wyniku kliknięcia przycisku), nie ma gwarancji kolejności modyfikacji. Bez starannej pracy w rzadkich przypadkach nowsza wersja danych może zostać nadpisana przez starszą, co prowadzi do niejasnych problemów z utratą danych i stabilnością. Takie zmiany najlepiej wykonywać seryjnie; aby zagwarantować, że taka praca jest serializowana niezależnie od wersji platformy, możesz użyć tej funkcji z SERIAL_EXECUTOR.

Jeszcze jedną rzeczą, na którą należy zwrócić uwagę, jest to, że zarówno platforma wykonawcza dostarczona przez środowisko wykonawcze THREAD_POOL_EXECUTOR, jak i jej wersja szeregowa SERIAL_EXECUTOR (która jest domyślna dla AsyncTask) są statyczne (konstrukcje na poziomie klasy), a zatem współdzielone przez wszystkie wystąpienia AsyncTask (s) w procesie aplikacji.

rnk
źródło