作者:leafjia,騰訊WXG客戶端開發工程師
你真的了解Android的線程優先級嗎? 看似平平無奇的三行代碼卻隱藏著巨大的陷阱!
Android上如果在主線程執行下面的代碼:
Thread t = new Thread();
t.start();
t.setPriority(3);
我們的預期應該是子線程t的優先級被設置為了低優先級。
但真正運行后,我們驚奇的發現,不只是子線程t,主線程的優先級同樣會被設置為低優先級!事實上,這三行代碼甚至導致了Android微信客戶端的一次線上故障!這是為什么?背后有怎樣秘密?又如何管控和避免?我們來一起深入分析、研究下這個問題。
(傳送門:如果不想深入了解這其中的原理,和一波三折的故事,可以直接跳到最后的 5.3 小節,那里提供了一些設置線程優先級的正確和錯誤的典型例子)
一、案件發生
微信Android客戶端某個新版本發布之后,馬上就收到了很多用戶的反饋:公眾號里的視頻卡頓/音畫不同步;朋友圈里的視頻卡頓掉幀。看現象,初步可能會懷疑到是不是播放器有什么問題?甚至懷疑是不是CDN有什么問題?然而事情遠沒有想象的那么簡單,在測試同學的幫助下,可以確定是Matrix的版本升級引起的。離譜!Matrix作為性能監控的工具,居然會影響到視頻的播放?
二、案發現場
瀏覽器的同事先有了發現:音視頻不同步,是因為pthread_cond_timedwait方法比設置的時間要長40ms左右導致。再經過測試,好家伙!不止是這個方法,新版本微信里,所有的sleep/wait方法,都要比原本設置的時間多出了40ms左右。可以用下面的代碼,非常容易的復現:
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","duration = " + (System.currentTimeMillis() - startTime));
//結果為:duration = 50
一開始我們完全沒有頭緒,根本不清楚到底是哪里的代碼引起的,好吧,最蠢的辦法,在新舊兩個Matrix版本的commit中,不斷二分,找到第一次出現問題的commit。終于找到了案發現場,定位到了類似這樣的一個修改:
啊?只是設置線程優先級與啟動線程的順序調換,況且設置的也只是一個特定子線程的優先級,居然會有這么大的破壞力?沒錯,讀者有興趣可以在Android上運行這樣一段代碼:
Thread t = new Thread();
t.start();
t.setPriority(3);
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","duration = " + (System.currentTimeMillis() - startTime));
// 結果為duration = 50ms
非常離奇的現象就是:如果在Thread的start后馬上調用setPriority,并且設置一個小于默認優先級(默認優先級為5)的優先級,就會出現sleep/wait等方法會比設置的時間多幾十毫秒的現象。
而由于很多視頻播放的邏輯中,都會通過系統的sleep/wait/pthread_cond_timedwait等方法來實現音視頻的同步,會有非常頻繁的調用,如果每次都多出幾十毫秒,就會直接引起視頻播放卡頓,音畫不同步等問題,也就直接導致了這次微信的線上故障。
三、分析推理
離譜!只是想設置一個特定子線程的優先級,居然就直接影響了主線程和主線程創建的所有子線程的sleep時間?這里絕對隱藏著什么秘密。
(在一起來探秘之前,需要補充一個小的背景(如果你已經充分了解linux線程的nice值,可以直接跳過):不管是在JAVA層設置線程優先級,還是在Native層設置線程優先級,最終設置的,也是絕大部分情況下最終起到作用的,都是線程的nice值,nice值越高,也就說明這個線程的“脾氣”越好,就越不容易搶到CPU,也就意味著線程的優先級越低。)
3.1. 罪魁禍首:TimerSlackHigh
既然Thread的start和setPriority的順序改變會影響sleep/wait等方法的時間,我們就先看下setPriority做了什么事情,Thread.setPriority最終會調用到system/libartpalette/palette_android.cc里的PaletteSchedSetPriority方法:
palette_status_t PaletteSchedSetPriority(int32_t tid, int32_t managed_priority) {
if (managed_priority < art::palette::kMinManagedThreadPriority ||
managed_priority > art::palette::kMaxManagedThreadPriority) {
return PALETTE_STATUS_INVALID_ARGUMENT;
}
int new_nice = kNiceValues[managed_priority - art::palette::kMinManagedThreadPriority];
int curr_nice = getpriority(PRIO_PROCESS, tid);
if (curr_nice == new_nice) {
return PALETTE_STATUS_OK;
}
if (new_nice >= ANDROID_PRIORITY_BACKGROUND) {
SetTaskProfiles(tid, {"SCHED_SP_BACKGROUND"}, true);
} else if (curr_nice >= ANDROID_PRIORITY_BACKGROUND) {
SchedPolicy policy;
// Change to the sched policy group of the process.
if (get_sched_policy(getpid(), &policy) != 0) {
policy = SP_FOREGROUND;
}
SetTaskProfiles(tid, {get_sched_policy_profile_name(policy)}, true);
}
if (setpriority(PRIO_PROCESS, tid, new_nice) != 0) {
return PALETTE_STATUS_CHECK_ERRNO;
}
return PALETTE_STATUS_OK;
}
static const int kNiceValues[art::palette::kNumManagedThreadPriorities] = {
ANDROID_PRIORITY_LOWEST, // 1 (MIN_PRIORITY)
ANDROID_PRIORITY_BACKGROUND + 6,
ANDROID_PRIORITY_BACKGROUND + 3,
ANDROID_PRIORITY_BACKGROUND,
ANDROID_PRIORITY_NORMAL, // 5 (NORM_PRIORITY)
ANDROID_PRIORITY_NORMAL - 2,
ANDROID_PRIORITY_NORMAL - 4,
ANDROID_PRIORITY_URGENT_DISPLAY + 3,
ANDROID_PRIORITY_URGENT_DISPLAY + 2,
ANDROID_PRIORITY_URGENT_DISPLAY // 10 (MAX_PRIORITY)
};
該方法會根據kNiceValues數組,把Java層Thread的優先級映射為Linux的線程nice值,上面的例子中Java層priority是3,kMinManagedThreadPriority的值是1,所以經過映射后,得到的nice值為13,而ANDROID_PRIORITY_BACKGROUND是10,表示后臺優先級,原來我們設置了比后臺優先級的nice值更高的值(即比后臺優先級更低),此時系統會把該線程設置為后臺線程,具體做了什么呢?接著看SetTaskProfiles:
bool SetTaskProfiles(int tid, const std::vector<std::string>& profiles, bool use_fd_cache) {
return TaskProfiles::GetInstance().SetTaskProfiles(tid, profiles, use_fd_cache);
}
TaskProfiles::TaskProfiles() {
// load system task profiles
if (!Load(CgroupMap::GetInstance(), TASK_PROFILE_DB_FILE)) {
LOG(ERROR) << "Loading " << TASK_PROFILE_DB_FILE << " for [" << getpid() << "] failed";
}
//省略
......
}
static constexpr const char* TASK_PROFILE_DB_FILE = "/etc/task_profiles.json";
這里會去讀取/etc/task_profiles.json這個配置文件,我們在這個配置文件里搜索這里傳入的參數SCHED_SP_BACKGROUND,找到了這樣的配置:
{
"Name": "SCHED_SP_BACKGROUND",
"Profiles": [ "HighEnergySaving", "LowIoPriority", "TimerSlackHigh" ]
}
{
"Name": "TimerSlackHigh",
"Actions": [
{
"Name": "SetTimerSlack",
"Params":
{
"Slack": "40000000"
}
}
]
},
{
"Name": "TimerSlackNormal",
"Actions": [
{
"Name": "SetTimerSlack",
"Params":
{
"Slack": "50000"
}
}
]
}
被設置為后臺線程后,會設置三個profile:"HighEnergySaving", "LowIoPriority", "TimerSlackHigh"。
聰明的你應該敏銳的發現了這個TimerSlackHigh,“懶惰定時器”,名字看起來就非常可疑,很有可能跟我們在查的sleep/wait延遲有關,趕快查一下TimerSlack是什么:
原來真兇就是他!
TimerSlack是Linux系統為了降低系統功耗,避免timer時間參差不齊,過于的頻繁的喚醒cpu,而設置的一種對齊策略。
總而言之:如果系統設置了大于等于10的nice值,即設置了比后臺優先級還要低的優先級,即把線程設置成了后臺線程,那么系統就會設置一個比較高的TimerSlack,從默認的50微秒,提高到40毫秒,從而導致wait/sleep等掛起的時間多了40ms左右。
但是,還是不對勁啊,我設置的明明只是特定子線程的優先級,按道理說只會影響該子線程的TimerSlack才對啊,為什么看起來影響了所有的線程呢?確實,這個問題還有很多疑點,我們接著分析。
3.2. 大誤會:糊涂的setPriority
我們首先就會懷疑Java層調用setPriority,設置錯了線程,那么找到 art/runtime/thread.cc:
void Thread::SetNativePriority(int new_priority) {
palette_status_t status = PaletteSchedSetPriority(GetTid(), new_priority);
CHECK(status == PALETTE_STATUS_OK || status == PALETTE_STATUS_CHECK_ERRNO);
}
在這里之后才會調用之前提到的PaletteSchedSetPriority方法,我們發現這里傳遞的參數也就是設置優先級的對象線程是GetTid(),繼續找到GetTid():
pid_t GetTid() const {
return tls32_.tid;
}
tls32_是用來描述native線程的數據結構,繼續找下tls32_.tid是在哪里賦值的,art/runtime/thread.cc:
void Thread::InitTid() {
tls32_.tid = ::art::GetTid();
}
bool Thread::Init(ThreadList* thread_list, JavaVMExt* java_vm, JNIEnvExt* jni_env_ext) {
//......
InitTid();
//......
}
void* Thread::CreateCallback(void* arg) {
//......
CHECK(self->Init(runtime->GetThreadList(), runtime->GetJavaVM(), self->tlsPtr_.tmp_jni_env));
//......
}
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
//......
pthread_create_result = pthread_create(&new_pthread,
&attr,
Thread::CreateCallback,
child_thread);
//......
}
一層一層往上找,來到CreateNativeThread方法,原來是在線程創建的時候在pthread_create的回調方法CreateCallback里面調用的InitTid,來設置的。而CreateNativeThread是Thread.start調用來創建native線程的地方。
對!你可能已經想到了,這里是有時序問題的,pthread_create的回調CreateCallback是異步執行的,所以start執行完畢,并不能確保setNativePriority的tid參數已經賦值,而一旦tid還未被賦值,那么這個時候調用setPriority,tid就是默認值0。
我們再看如果Native的系統調用setpriority的參數如果是0的話,會出現什么情況:
來了,如果第二個參數tid是0的話,就會設置調用線程的優先級!
那TimerSlack呢?在tid為0的情況下的表現是什么樣呢?我們找到最終設置TimerSlack的地方,system/core/libprocessgroup/task_profiles.cpp:
bool SetTimerSlackAction::ExecuteForTask(int tid) const {
//......
if (tid == 0 || tid == GetThreadId()) {
if (prctl(PR_SET_TIMERSLACK, slack_) == -1) {
PLOG(ERROR) << "set_timerslack_ns prctl failed";
}
}
return true;
}
TimerSlack最終是通過系統調用prctl方法設置的,如果tid是0,同樣會設置當前線程的TimerSlack。
所以結論是,如果GetTid為0,那么無論是nice值還是TimerSlack都會對當前的調用線程設置。
居然有這么詭異的陷阱!我們再回頭看引起問題的代碼,似乎就說得過去了:
Thread t = new Thread();
t.start();
t.setPriority(3);
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","duration = " + (System.currentTimeMillis() - startTime));
t.start()調用之后,在native的tid還沒有被設置好的時候,就執行了下面的t.setPriority,這時GetTid返回值是0,native的setpriority的tid參數為0,就會把調用線程(Calling Thread)即主線程的nice設置為13,同時TimerSlack也被設置為40ms。有這樣的結果就說得通了。
然而,結束了嗎?因為時序問題,主線程被設置了高nice和高TimerSlack,理所當然的,主線程創建的子線程的nice值和TimerSlack理所當然地繼承了父線程,也就是主線程,所以自然也被影響了,是這樣嗎?
用這樣一段代碼測試:
Thread t = new Thread();
t.start();
t.setPriority(3);
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","MainThread duration = " + (System.currentTimeMillis() - startTime));
//結果為50
new Thread(new Runnable() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","Thread duration = " + (System.currentTimeMillis() - startTime));
//結果為10
}
}).start();
我們發現,主線程被錯誤設置nice值和TimerSlack后,創建的子線程并沒有繼承主線程的nice值和TimerSlack,這又是為什么呢?我們繼續分析。
在native層的線程,的確子線程會繼承父線程也就是主線程的nice值和TimerSlack,但是,start和priority的時序問題,只會錯誤地設置主線程的native的nice值和TimerSlack,并不會影響主線程Java層的priority變量,而子線程同樣會在Java層繼承父線程Java層的priority,而在native層創建線程的時候,有這樣的邏輯:
void* Thread::CreateCallback(void* arg) {
//......
ArtField* priorityField = jni::DecodeArtField(WellKnownClasses::java_lang_Thread_priority);
self->SetNativePriority(priorityField->GetInt(self->tlsPtr_.opeer));
//......
}
在native創建線程調用到CreateCallback時,會根據Java層的priority,重新設置native層的priority/nice值和TimerSlack,而在上面的情況中,主線程的Java層priority并沒有被設置,是默認的5,對應的nice值為0,所以子線程的nice值就被再次設置為了0,也就重新被設置為了前臺線程,TimerSlack就又被設置為了低TimerSlack。
那么,問題又來了,既然主線程因為時序問題被錯誤地設置后臺優先級后,并不影響其創建的子線程的nice值和TimerSlack,而線上故障中,引起音畫不同步和視頻掉幀的線程,卻又都是在主線程創建的子線程中產生的。那又是什么讓本來只會影響主線程的問題,進一步污染了它創建的全部子線程呢?
3.3. 幫兇:WebView推波助瀾
再經過進一步排查,發現一個非常奇怪的現象,來看下面的代碼:
Thread t = new Thread();
t.start();
t.setPriority(3);
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","MainThread duration = " + (System.currentTimeMillis() - startTime));
//結果為50
new WebView(context);
new Thread(new Runnable() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
Thread.sleep(10);
Log.i("Matrix","Thread duration = " + (System.currentTimeMillis() - startTime));
//結果為50
}
}).start();
考驗眼力的時候來了,發現了跟之前的測試代碼有什么區別了嗎?是的,只要在創建子線程前加一句new WebView(context),那么子線程的nice和TimerSlack就也被影響了!
事實上,微信在某些頁面的確會有預加載webview的邏輯,我們也會發現,也的確在new了WebView之后,設置優先級的時序錯誤,就不止會影響主線程,而是其創建的所有的子線線程就都會被污染,都有了高TimerSlack,這是為什么呢?
我們在new Webview前后打印一下主線程nice值就會發現,主線程nice值在執行new WebView之前是13,之后變成了-4。
哦?new WebView居然會設置主線程的優先級?
找到Chromium的源碼content/browser/browser_main_loop.cc:
// Up the priority of the UI thread unless it was already high (since mac
// and recent versions of Android (O+) do this automatically).
if (base::FeatureList::IsEnabled(
features::kBrowserUseDisplayThreadPriority) &&
base::PlatformThread::GetCurrentThreadPriority() <
base::ThreadPriority::DISPLAY) {
base::PlatformThread::SetCurrentThreadPriority(
base::ThreadPriority::DISPLAY);
}
其中ThreadPriority::DISPLAY的值就是-4,是的,Chromium有這樣的邏輯:如果當前主線程的nice值大于-4(即優先級較低),就會提高主線程的優先級,并且這里設置優先級是直接調用了native的setpriority,所以并不會同時修改TimerSlack。
這個操作其實也很容易理解,是Chromium的保護措施,它不希望渲染UI的主線程處在一個較低的優先級,不過卻陰差陽錯地成為了幫兇。
我們仔細看下,native線程在創建的時候根據Java層的priority設置nice值的時候的邏輯:
if (new_nice >= ANDROID_PRIORITY_BACKGROUND) {
SetTaskProfiles(tid, {"SCHED_SP_BACKGROUND"}, true);
} else if (curr_nice >= ANDROID_PRIORITY_BACKGROUND) {
SchedPolicy policy;
// Change to the sched policy group of the process.
if (get_sched_policy(getpid(), &policy) != 0) {
policy = SP_FOREGROUND;
}
SetTaskProfiles(tid, {get_sched_policy_profile_name(policy)}, true);
}
如果新設置的nice值大于等于10,直接判斷為后臺線程,設置高的TimerSlack;但另一方面,只有當前的nice值大于10,新的nice值小于10,才會把線程設置為前臺線程,設置低的TimerSlack。
而執行new WebView之后,主線程nice值是-4,而子線程繼承了主線程的nice值也是-4,-4 < 10,系統認為你已經是前臺線程了,就不會走到else if的邏輯,也就不會重新把線程設置為前臺線程,也就不會設置低TimerSlack,而子線程從主線程繼承的TimerSlack就是高TimerSlack。從而,主線程創建的全部子線程就都被污染了,也就引起了,音畫不同步和掉幀的故障。
至此才真正破案了,所有的疑問和“奇怪”現象也都可以解釋通了。
該問題一開始是由Thread的start和setPriority的時序問題,導致主線程被設置為后臺線程,同時被設置了高TimerSlack;而幫兇Chromium又在初始化WebView的時候默默地把主線程的nice值設置成了較低的nice值(較高的優先級),但又沒有設置回低TimerSlack,從而主線程創建的子線程繼承了主線程的nice值和高TimerSlack后,卻認為自己已經是前臺線程,所以也沒有機會根據Java層的priority重新設置低TimerSlack,最后就導致了主線程連同其創建的所有子線程的TimerSlack全部都被設置為了高TimerSlack,從而產生了一系列的問題,導致了這次故障。
四、監控機制:
原理已經搞清楚后,我們需要建立一個監控機制,避免之后再出現這種情況。
首先我們需要確保住主線程優先級不被設置的過低,hook系統調用setpriority,如果對主線程設置過低的優先級(過高的nice值),則直接報錯:
int (*original_setpriority)(int __which, id_t __who, int __priority);
int my_setpriority(int __which, id_t __who, int __priority) {
if (__priority <= 0) {
return original_setpriority(__which, __who, __priority);
}
if (__who == 0 && getpid() == gettid()) {
// Throw Crash Here
} else if (__who == getpid()) {
// Throw Crash Here
}
return original_setpriority(__which, __who, __priority);
}
另外,我們還hook了設置TimerSlack的prctl方法,確保主線程的TimerSlack值不被設置的過大:
int (*original_prctl)(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5);
int my_prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5) {
if(option == PR_SET_TIMERSLACK) {
if (gettid()==getpid() && arg2 > 50000) {
// Throw Crash Here
}
}
return original_prctl(option, arg2, arg3, arg4, arg5);
}
監控會在微信各種體驗版和debug的時候打開,確保當再次出現主線程的優先級或者TimerSlack被錯誤設置的情況時,能夠提前發現,避免這類問題被帶到線上。
五、額外的結論:
最后,我們再討論下,在設置優先級的時候我們容易出現的一些錯誤。
5.1. 線程優先級的“雙標”
Thread在Java層的優先級與Native層或者說Linux系統層的線程優先級,也就是nice值,是兩套不同的標準,數字大小的意義甚至也是相反的,容易產生混淆和誤用。
通過Thread.setPriority方法設置的優先級是在Java層的優先級,數字從0到10,數字越大表優先級越高,默認是5,Android主線程默認是5。Java層的優先級通過native層的kNiceValues數組,映射為nice值,再起到作用。
通過android.os.Process.setThreadPriority方法設置的優先級是Native層的線程優先級/nice值,數字從-20到20,數字越大代表優先級越低,默認是0,Android主線程默認的nice值是-10。另外,native層的系統調用setpriority當然也是直接設置的nice值。
5.2. 正確設置HandlerThread的優先級
怎么設置HandlerThread的優先級呢?第一反應可能會這樣寫
HandlerThread ht = new HandlerThread("leafjia-thread");
ht.setPriority(3);
ht.start();
似乎也沒什么問題哦?但是這樣設置,其實除了設置了Java層Thread對象的成員變量priority,并不會起到任何其他的效果。
我們看HandlerThread的源碼:
public class HandlerThread extends Thread {
int mPriority;
//......
public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
}
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}
//......
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
//......
}
調用ht.serPriority(3)其實設置的是其父類Thread的priority成員變量,而HandlerThread本身有自己的mPriority成員變量,start之后,會在創建Native Thread的時候,在調用run回調方法前,根據Java層Thread的priority(我們已經設置為了3)設置Native的nice值,這時的確優先級能夠設置成功。但是HandlerThread自己重寫了run方法,在之后執行的run方法中,又再次通過Process.setThreadPriority(mPriority)設置了自己的優先級為mPriority,而mPriority并不能通過Thread.setPriority方法設置。所以上面的代碼并不生效。
正確的方法也顯而易見:
HandlerThread ht = new HandlerThread("My-Thread", 13);
ht.start();
需要注意的是,因為線程優先級最終是通過Process.setThreadPriority方法實現的,所以priority使用的是-20到20的nice值的優先級體系。
5.3. Some Cases
進一步,可以總結出下面幾種設置線程優先級的case,如果我們的目的是只設置thread線程的優先級為3(而不想改變調用線程的優先級),即nice值為13,那么:
//Case1: 正確,thread線程的nice值被設置為13
Thread thread = new Thread();
thread.setPriority(3);
thread.start();
//Case2: 正確,thread線程的nice值被設置為13
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setPriority(3);
// 或:
// Process.setThreadPriority(13);
}
});
thread.start();
//Case3: 錯誤,thread線程的nice值一定被設置為13
//(可能直接被設置或從當前線程繼承而來,視時序而定),
// 當前線程的nice值很有可能被設置為13
Thread thread = new Thread();
thread.start();
thread.setPriority(3);
//Case4: 錯誤,同上
HandlerThread thread = new HandlerThread("My-Thread");
thread.start();
thread.setPriority(3);
//Case5: 錯誤,thread線程的nice值被設置為默認的0
HandlerThread thread = new HandlerThread("My-Thread");
thread.setPriority(3);
thread.start();
//Case6: 正確,thread線程的nice值被設置為13
HandlerThread thread = new HandlerThread("My-Thread", 13);
thread.start();
至此,就是本文的全部內容。線程優先級的相關問題,大家平時可能關注的并不多,或者會認為線程優先級的影響并不會特別大。而看起來平平無奇的設置線程優先級的三行代碼,就真的引起了一次線上故障。這也提醒大家,一旦涉及到多線程,就一定要對時序問題特別謹慎。
如果這篇文章對你有幫助,歡迎分享,收藏,點贊!