大師兄的Python源碼學(xué)習(xí)筆記(四十一): Python的多線程機制(三)

大師兄的Python源碼學(xué)習(xí)筆記(四十): Python的多線程機制(二)
大師兄的Python源碼學(xué)習(xí)筆記(四十二): Python的多線程機制(四)

四郎嫁、創(chuàng)建線程

1. 創(chuàng)建子線程
  • 在建立多線程環(huán)境后幅恋,Python會開始創(chuàng)建底層平臺的原生線程,也可以稱為子進程讲岁。
  • 這還要從調(diào)用thread_PyThread_start_new_thread的主線程開始:
Modules\_threadmodule.c

static PyObject *
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
{
    PyObject *func, *args, *keyw = NULL;
    struct bootstate *boot;
    unsigned long ident;

    if (!PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3,
                           &func, &args, &keyw))
        return NULL;
   ... ...
    boot = PyMem_NEW(struct bootstate, 1);
    if (boot == NULL)
        return PyErr_NoMemory();
    boot->interp = PyThreadState_GET()->interp;
    boot->func = func;
    boot->args = args;
    boot->keyw = keyw;
    boot->tstate = _PyThreadState_Prealloc(boot->interp);
    if (boot->tstate == NULL) {
        PyMem_DEL(boot);
        return PyErr_NoMemory();
    }
    Py_INCREF(func);
    Py_INCREF(args);
    Py_XINCREF(keyw);
    PyEval_InitThreads(); /* Start the interpreter's thread-awareness */
    ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
    if (ident == PYTHREAD_INVALID_THREAD_ID) {
        PyErr_SetString(ThreadError, "can't start new thread");
        Py_DECREF(func);
        Py_DECREF(args);
        Py_XDECREF(keyw);
        PyThreadState_Clear(boot->tstate);
        PyMem_DEL(boot);
        return NULL;
    }
    return PyLong_FromUnsignedLong(ident);
}
  • 主線程在創(chuàng)建多線程環(huán)境后听隐,調(diào)用PyThread_start_new_thread創(chuàng)建子線程:
Python\thread_nt.h

unsigned long
PyThread_start_new_thread(void (*func)(void *), void *arg)
{
    HANDLE hThread;
    unsigned threadID;
    callobj *obj;

    dprintf(("%lu: PyThread_start_new_thread called\n",
             PyThread_get_thread_ident()));
    if (!initialized)
        PyThread_init_thread();

    obj = (callobj*)HeapAlloc(GetProcessHeap(), 0, sizeof(*obj));
    if (!obj)
        return PYTHREAD_INVALID_THREAD_ID;
    obj->func = func;
    obj->arg = arg;
    PyThreadState *tstate = PyThreadState_GET();
    size_t stacksize = tstate ? tstate->interp->pythread_stacksize : 0;
    hThread = (HANDLE)_beginthreadex(0,
                      Py_SAFE_DOWNCAST(stacksize, Py_ssize_t, unsigned int),
                      bootstrap, obj,
                      0, &threadID);
    if (hThread == 0) {
        /* I've seen errno == EAGAIN here, which means "there are
         * too many threads".
         */
        int e = errno;
        dprintf(("%lu: PyThread_start_new_thread failed, errno %d\n",
                 PyThread_get_thread_ident(), e));
        threadID = (unsigned)-1;
        HeapFree(GetProcessHeap(), 0, obj);
    }
    else {
        dprintf(("%lu: PyThread_start_new_thread succeeded: %p\n",
                 PyThread_get_thread_ident(), (void*)hThread));
        CloseHandle(hThread);
    }
    return threadID;
}

  • 觀察PyThread_start_new_thread函數(shù)的參數(shù):

func:函數(shù)t_bootstrap。
arg: boot饰躲,也就是保存了線程信息的bootstate結(jié)構(gòu)盗扇。

  • PyThread_start_new_thread實際將funcarg打包到一個類型為callobj的結(jié)構(gòu)體中:
Python\thread_nt.h

/*
 * Thread support.
 */

typedef struct {
    void (*func)(void*);
    void *arg;
} callobj;
  • 有一點值得注意,_beginthreadex是Win32下用于創(chuàng)建線程的API寥假。
Include\10.0.18362.0\ucrt\process.h

_Success_(return != 0)
_ACRTIMP uintptr_t __cdecl _beginthreadex(
    _In_opt_  void*                    _Security,
    _In_      unsigned                 _StackSize,
    _In_      _beginthreadex_proc_type _StartAddress,
    _In_opt_  void*                    _ArgList,
    _In_      unsigned                 _InitFlag,
    _Out_opt_ unsigned*                _ThrdAddr
    );
  • 這是一個關(guān)鍵的轉(zhuǎn)折市框,因為在此之前,我們一直在主線程的執(zhí)行路徑上昧旨;而現(xiàn)在我們通過_beginthreadex創(chuàng)建了一個子線程拾给,并將之前打包的callobj結(jié)構(gòu)體obj作為參數(shù)傳遞給了子線程。
  • 梳理Python當(dāng)前的狀態(tài):
  • Python當(dāng)前實際上由兩個Win32下的原生線程組成兔沃,一個是執(zhí)行Python程序時操作系統(tǒng)創(chuàng)建的主線程蒋得;另一個是通過_beginthreadex創(chuàng)建的子線程。
  • 主線程在在執(zhí)行PyEval_InitThreads的過程中乒疏,獲得了GIL额衙,并將自己掛起等待子線程。
  • 子線程的線程過程是bootstrap怕吴,為了訪問Python解釋器窍侧,必須首先獲得GIL
Python\thread_nt.h

/* thunker to call adapt between the function type used by the system's
thread start function and the internally used one. */
static unsigned __stdcall
bootstrap(void *call)
{
    callobj *obj = (callobj*)call;
    void (*func)(void*) = obj->func;
    void *arg = obj->arg;
    HeapFree(GetProcessHeap(), 0, obj);
    func(arg);
    return 0;
}
  • bootstrap中转绷,子線程完成了三個動作:

1. 獲得線程id伟件;
2. 喚醒主線程;
3. 調(diào)用t_bootstrap议经。

  • 主線程之所以需要等待子線程斧账,是因為主線程調(diào)用的PyThread_start_new_thread需要返回所創(chuàng)建子線程的線程id谴返,一旦在子線程中獲得了線程id,就會設(shè)法喚醒主線程咧织。
  • 到這里嗓袱,主線程和子線程開始分道揚鑣,主線程在返回子線程id并獲得GIL后习绢,會繼續(xù)執(zhí)行后續(xù)字節(jié)碼指令渠抹;而子線程則將進入t_bootstrap,最終進入等待GIL的狀態(tài)闪萄。
Modules\_threadmodule.c

static void
t_bootstrap(void *boot_raw)
{
    struct bootstate *boot = (struct bootstate *) boot_raw;
    PyThreadState *tstate;
    PyObject *res;

    tstate = boot->tstate;
    tstate->thread_id = PyThread_get_thread_ident();
    _PyThreadState_Init(tstate);
    PyEval_AcquireThread(tstate);
    tstate->interp->num_threads++;
    res = PyObject_Call(boot->func, boot->args, boot->keyw);
    if (res == NULL) {
        if (PyErr_ExceptionMatches(PyExc_SystemExit))
            PyErr_Clear();
        else {
            PyObject *file;
            PyObject *exc, *value, *tb;
            PySys_WriteStderr(
                "Unhandled exception in thread started by ");
            PyErr_Fetch(&exc, &value, &tb);
            file = _PySys_GetObjectId(&PyId_stderr);
            if (file != NULL && file != Py_None)
                PyFile_WriteObject(boot->func, file, 0);
            else
                PyObject_Print(boot->func, stderr, 0);
            PySys_WriteStderr("\n");
            PyErr_Restore(exc, value, tb);
            PyErr_PrintEx(0);
        }
    }
    else
        Py_DECREF(res);
    Py_DECREF(boot->func);
    Py_DECREF(boot->args);
    Py_XDECREF(boot->keyw);
    PyMem_DEL(boot_raw);
    tstate->interp->num_threads--;
    PyThreadState_Clear(tstate);
    PyThreadState_DeleteCurrent();
    PyThread_exit_thread();
}
  • 子線程從這里開始了與主線程對GIL的競爭:
  • 首先子線程通過PyEval_AcquireThread申請GIL
Python\ceval.c

void
PyEval_AcquireThread(PyThreadState *tstate)
{
   if (tstate == NULL)
       Py_FatalError("PyEval_AcquireThread: NULL new thread state");
   /* Check someone has called PyEval_InitThreads() to create the lock */
   assert(gil_created());
   take_gil(tstate);
   if (PyThreadState_Swap(tstate) != NULL)
       Py_FatalError(
           "PyEval_AcquireThread: non-NULL old thread state");
}
  • 接下來子線程通過PyObject_Call調(diào)用字節(jié)碼執(zhí)行引擎:
Objects\call.c

PyObject *
PyObject_Call(PyObject *callable, PyObject *args, PyObject *kwargs)
{
   ternaryfunc call;
   PyObject *result;

   /* PyObject_Call() must not be called with an exception set,
      because it can clear it (directly or indirectly) and so the
      caller loses its exception */
   assert(!PyErr_Occurred());
   assert(PyTuple_Check(args));
   assert(kwargs == NULL || PyDict_Check(kwargs));

   if (PyFunction_Check(callable)) {
       return _PyFunction_FastCallDict(callable,
                                       &PyTuple_GET_ITEM(args, 0),
                                       PyTuple_GET_SIZE(args),
                                       kwargs);
   }
   else if (PyCFunction_Check(callable)) {
       return PyCFunction_Call(callable, args, kwargs);
   }
   else {
       call = callable->ob_type->tp_call;
       if (call == NULL) {
           PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable",
                        callable->ob_type->tp_name);
           return NULL;
       }

       if (Py_EnterRecursiveCall(" while calling a Python object"))
           return NULL;

       result = (*call)(callable, args, kwargs);

       Py_LeaveRecursiveCall();

       return _Py_CheckFunctionResult(callable, result, NULL);
   }
}
  • 傳遞進PyObject_Callboot->func是一個PyFunctionObject對象梧却,對應(yīng)線程執(zhí)行的方法。
  • PyObject_Call結(jié)束后败去,子線程將釋放GIL篮幢,并完成銷毀線程的所有掃尾工作。
  • t_bootstrap代碼上看为迈,子線程應(yīng)該全部執(zhí)行完成,才會通過PyThreadState_DeleteCurrent釋放GIL缺菌。
  • 但實際情況正如前面章節(jié)提到的葫辐,Python會定時激活線程的調(diào)度機制,在子線程和主線程之間不斷切換伴郁,從而真正實現(xiàn)多線程機制耿战。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市焊傅,隨后出現(xiàn)的幾起案子剂陡,更是在濱河造成了極大的恐慌,老刑警劉巖狐胎,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸭栖,死亡現(xiàn)場離奇詭異,居然都是意外死亡握巢,警方通過查閱死者的電腦和手機晕鹊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來暴浦,“玉大人溅话,你說我怎么就攤上這事「杞梗” “怎么了飞几?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長独撇。 經(jīng)常有香客問我屑墨,道長躁锁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任绪钥,我火速辦了婚禮灿里,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘程腹。我一直安慰自己匣吊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布寸潦。 她就那樣靜靜地躺著色鸳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪见转。 梳的紋絲不亂的頭發(fā)上命雀,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天,我揣著相機與錄音斩箫,去河邊找鬼吏砂。 笑死,一個胖子當(dāng)著我的面吹牛乘客,可吹牛的內(nèi)容都是我干的狐血。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼易核,長吁一口氣:“原來是場噩夢啊……” “哼匈织!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起牡直,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缀匕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后碰逸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乡小,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年饵史,在試婚紗的時候發(fā)現(xiàn)自己被綠了劲件。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡约急,死狀恐怖零远,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厌蔽,我是刑警寧澤牵辣,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站奴饮,受9級特大地震影響纬向,放射性物質(zhì)發(fā)生泄漏择浊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一逾条、第九天 我趴在偏房一處隱蔽的房頂上張望琢岩。 院中可真熱鬧,春花似錦师脂、人聲如沸担孔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糕篇。三九已至,卻和暖如春酌心,著一層夾襖步出監(jiān)牢的瞬間拌消,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工安券, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留墩崩,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓侯勉,卻偏偏與公主長得像泰鸡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子壳鹤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容

  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    極客學(xué)院Wiki閱讀 7,247評論 0 3
  • 不知不覺易趣客已經(jīng)在路上走了快一年了芳誓,感覺也該讓更多朋友認識知道易趣客,所以就謝了這篇簡介啊鸭,已做創(chuàng)業(yè)記事锹淌。 易趣客...
    Physher閱讀 3,418評論 1 2
  • 雙胎妊娠有家族遺傳傾向,隨母系遺傳赠制。有研究表明赂摆,如果孕婦本人是雙胎之一,她生雙胎的機率為1/58钟些;若孕婦的父親或母...
    鄴水芙蓉hibiscus閱讀 3,701評論 0 2
  • 晴天政恍,擁抱陽光汪拥,擁抱你。雨天篙耗,想念雨滴迫筑,想念你宪赶。 我可以喜歡你嗎可以啊 我還可以喜歡你嗎可以,可是你要知道我們不可...
    露薇霜凝閱讀 1,216評論 1 2