<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

協程的作用、結構及原理

科技綠洲 ? 來源:Linux開發架構之路 ? 作者:Linux開發架構之路 ? 2023-11-08 16:39 ? 次閱讀

本文介紹了協程的作用、結構、原理,并使用C++匯編實現了64位系統下的協程池。文章內容避免了協程晦澀難懂的部分,用大量圖文來分析原理,適合新手閱讀學習。

GitHub源碼

1. Web服務器問題

現代分布式Web后臺服務邏輯通常由一系列RPC請求組成,若串行則耗時比較長。

圖片

此時一般都會使用線程池并行運行RPC請求,如圖中GetData函數

圖片

假設請求數據包不大,那么可假設GetData耗時組成如下圖所示。在非阻塞讀情況下,CPU將在Wait環節空轉浪費資源(不斷地read,得到返回碼-1)。

圖片

  1. 協程的引入

有沒有辦法只用一個線程并行執行GetData呢?答案是:可以!我們假設有3個并行的GetData任務,下圖線程1通過跳轉控制流,減少CPU資源浪費。執行流為①⑦,在Wait階段則跳到其他任務如①⑤。運行結束后也跳到其他任務如⑥~⑦。通過這種方式,3個GetData能用一個線程以52ms的耗時并行執行。

圖片

如果GetData任務可以被這樣分配,則可以減少線程切換的消耗。因為協程的調度是線程內用戶態執行的,CPU消耗非常小。

圖片

  1. 協程的原理

**從上文可知,協程之間的切換本質是函數的跳轉,即如何讓正在執行的函數跳轉到另一個新的函數上,以及下次如何又跳轉回來。**如下面代碼所示:

void func1() {
printf("① 跳轉到func2");
Coroutine::CoYield(); // 通過該函數跳到func2
printf("③ func2跳轉回func1");
}

void func2() {
printf("② func2執行完畢");
}

要實現這種能力,需要結合匯編知識。首先研究如下簡單函數的匯編語言

#include
using namespace std;

class Object {
public:
int val[12];
};

int func(Object *pObj1, Object *pObj2) {
pObj1->val[0] = 1;
pObj1->val[11] = 11;
pObj2->val[0] = 2;
pObj2->val[11] = 12;
int arr[100];
arr[0] = 3;
arr[99] = 99;
return pObj1->val[0];
}

int main() {
Object obj, obj2;
int a = func(&obj, &obj2);
return a;
}

下面看看在64位系統匯編中,func函數是如何執行的。push %rbp是進入func函數執行的第一個指令,作用是把rbp的地址壓到棧頂。因為rsp始終指向棧頂,所以壓棧后,rsp的地址下移8字節。rdi和rsi相差48個字節,該空間被class Object內的int val[12]占用。

圖片

前兩個指令讓rbp指向rsp往下296字節的位置。后面兩個指令把rdi和rsi地址保存在最下面。

圖片

為什么rsp下移296字節?首先,上述代碼使用了臨時變量int arr[100],需要有400個字節的??臻g;其次,x64系統存有128字節的紅色區域可使用;最后,rdi和rsi地址共占16字節。因此,rbp到紅色區域底部的空間一共是 288 + 8 + 104 + 8 + 8 = 416字節。接下來才開始執行func函數第一行代碼,給val[0]賦值。

圖片

然后分別給pObj1和pObj2的成員變量賦值

圖片

接下來給臨時變量arr賦值

圖片

最后讓eax指向返回值,恢復函數棧的棧底和棧頂。

圖片

  1. 協程的結構

從前面我們知道,每個函數在內存中都有棧頂rsp和棧底rbp。這兩個值決定了函數可操作的內存范圍,如下圖所示

圖片

既然協程切換是從一個函數切換到另一個函數,那么就需要知道兩個函數的rbp和rsp。然而,函數的rbp和rsp是執行時設定的,代碼層面難以獲得。既然如此,我們可以實現騰出空間,讓函數在預期的rbp和rsp內。定義一個類如下:

class Coroutine {
void* m_pRegister[14];
char m_pStack[1024];
std::function m_func;
};()>

那么在內存模型中,該類的布局如下所示

圖片

這樣的協程在能被使用前需要做初始化,如下圖所示

圖片

在其他協程切換過來時,cpu寄存器可按m_pRegister預設的地址賦值,開始執行DoWork函數,函數代碼如下:

static void Coroutine::DoWork(Coroutine *pThis) {
pThis->m_func();
pThis->Yield(); // 轉讓控制流給同線程的其他協程
}

由于是靜態函數,需令參數pThis為協程地址。所以,初始化時需要設置m_pRegister中的rdi為this。上述第二行代碼執行時,rbp會設為this。所以執行m_func時,如下圖所示:

圖片

  1. 協程間的切換

下面以Coroutine1切換到Coroutine2為例。主要分為兩步:1. 保存Coroutine1的上下文

圖片

  1. 加載Coroutine2的上下文

圖片

切換代碼可見源代碼Coroutine::Switch## 6. 協程池的實現

本文實現協程池比較簡單,初始化創建線程并設置thread_local變量以保存協程隊列狀態。并且,每個線程額外創建一個main協程用作Guard。在執行時,每個線程通過輪詢的方式切換協程,若協程無任務則嘗試CAS獲取Job,否則直接執行已有Job。當Job執行完或主動CoYield時,切換到下一個協程。為了避免CAS空轉,在沒有任務時會阻塞休眠。當任務來臨時則Notify所有線程的協程。

圖片

  1. 源代碼

example.cpp

/**
* @file example.cpp
* @author souma
* @brief 使用協程池的示例,編譯命令如下
* g++ example.cpp coroutine.cpp -lpthread -O3
* @version 0.1
* @date 2023-06-06
*
* @copyright Copyright (c) 2023
*
*/
#include
#include
#include "coroutine.h"

using namespace std;
using namespace comm;

void func(const string &sTaskName, uint32_t uWaitSeconds) {
printf("[%ld] [%s start], wait seconds[%u]n", time(nullptr), sTaskName.c_str(), uWaitSeconds);
time_t iStartSec = time(nullptr);
// 默認可用65535字節的棧內存,具體可看CO_STACK_SIZE
uint32_t uArrSize = 65535/4;
int arr[uArrSize];
while (time(nullptr) - iStartSec < uWaitSeconds) {
// 操作棧內存
for (int i = 0; i < uArrSize; ++i) {
arr[i] = i;
}

// 切換控制流
printf("[%ld] [%s] -> [協程池]n", time(nullptr), sTaskName.c_str());
usleep(100);
Coroutine::CoYield(); // 只需這一個函數即可切換控制流
printf("[%ld] [協程池] -> [%s]n", time(nullptr), sTaskName.c_str());
}

// 檢查棧內存是否正確
for (int i = 0; i < uArrSize; ++i) {
if (arr[i] != i) {
printf("棧內存錯誤n");
exit(-1);
}
}
printf("[%ld] [%s end], expect_timecost[%d], real_timecost[%ld]n", time(nullptr), sTaskName.c_str(), uWaitSeconds, time(nullptr) - iStartSec);
}

int main() {
// 如果想當線程池用,可以令第一個參數為線程數,第二個參數為1。
// 在該場景下,使用小線程大協程不僅CPU消耗低,整體耗時也很低,可以自行測試。
CoroutinePool oPool(2, 300);
oPool.Run();

time_t iStartTime = time(nullptr);
const int iTaskCnt = 400;
vector> vecFuture;
for (int i = 0; i < iTaskCnt; ++i) {
// 模擬GetData中的Wait環節, 1 ~ 5秒等待
shared_ptr pFuture = oPool.Submit([i](){func("Task" + to_string(i), random() % 5 + 1);});
if (pFuture != nullptr) {
vecFuture.emplace_back(pFuture);
}
}

// 阻塞等待所有Task完成
for (auto it = vecFuture.begin(); it != vecFuture.end(); ++it) {
(*it)->Get();
}

printf("demo's finished, time cost[%ld]n", time(nullptr) - iStartTime);
return 0;
}

coroutine.h

/**
* @file coroutine.h
* @author souma
* @brief 多線程無棧式協程池,請不要用-O0編譯否則會產生coredump
* @version 0.1
* @date 2023-06-06
*
* @copyright Copyright (c) 2023
*
*/
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

namespace comm {
class Future;
class CoroutinePool;
class Coroutine;
struct CoroutinePoolCtx;
struct CoroutineTaskCtx;


struct CoroutinePoolCtx {
std::vector> m_vecCoroutine;
std::shared_ptr m_pMainCoroutine;
uint32_t m_uCursor;
uint32_t m_uWorkCnt;
};

struct CoroutineTaskCtx {
std::function m_userFunc;
std::shared_ptr m_pFuture;
};

// class ArraySyncQueue start
template
class ArraySyncQueue {
public:
ArraySyncQueue(uint32_t uCapacity, uint32_t uSleepUs = 100, uint32_t uRetryTimes = 3);
bool Push(T *pObj);
T* Pop();
inline bool IsFull() const { return m_uPushCursor == m_uPopCursor - 1 || (m_uPopCursor == 0 && m_uPushCursor == m_vecQueue.size() - 1); }
bool IsEmpty() const { return m_uPopCursor == m_uPushCursor; }

~ArraySyncQueue();

private:
uint32_t GetNextCursor(uint32_t uCursor);
private:
std::vector m_vecQueue;
uint32_t m_uPushCursor = 0;
uint32_t m_uPopCursor = 0;
uint32_t m_uSleepUs;
uint32_t m_uRetryTimes;
};
// class ArraySyncQueue end

// class Coroutine start
class Coroutine {
public:

friend class CoroutinePool;

/**
* @brief 調用該函數將執行流交給其他協程,僅在協程池環境下有效
*
* @return true:協程切換成功, false:不在協程池環境中運行
*/
static bool CoYield();

Coroutine(const Coroutine &) = delete;
Coroutine(Coroutine &&) = delete;
Coroutine & operator=(const Coroutine &) = delete;
Coroutine & operator=(Coroutine &&) = delete;

private:
// 4096是預留給庫使用的棧內存大小,后者是留給用戶使用的棧內存大小
constexpr static uint32_t CO_STACK_SIZE = 4096 + 65535;

Coroutine();

/**
* @brief 當前協程是否綁定了任務
*
* @return true:是
*/
inline bool HasTask() const { return m_pTaskCtx != nullptr; }

/**
* @brief 兩個協程切換,從pPrev切換到pNext
*/
static void Switch(Coroutine *pPrev, Coroutine *pNext);

/**
* @brief 將控制流轉給同線程的其他協程
*/
void Yield();

/**
* @brief 這個是給main協程用的
*/
void Register();

/**
* @brief 這個是給執行用戶任務的協程用的
*/
void Register(std::shared_ptr pTaskCtx);

/**
* @return CoroutinePoolCtx& 當前線程的協程上下文
*/
static CoroutinePoolCtx & GetCtx();

/**
* @brief 讓當前線程的cursor往后移,輪詢協程
*/
static void MoveCursor();

/**
* @brief 協程包一層的函數
*/
static void DoWork(Coroutine *pThis);

/**
*
* @return void* 獲得自建rsp地址
*/
void* GetRsp();

/**
* 保存寄存器的值到m_pStack中
*/
void SaveReg();

private:
void* m_pRegister[14];
char m_pStack[CO_STACK_SIZE];
std::shared_ptr m_pTaskCtx;
};
// class Coroutine end

// class CoroutinePool start
class CoroutinePool {
public:
friend class Coroutine;
/**
* @brief 建立一個多線程協程池,即創建uThreadCnt個線程,每個線程含有uCoroutineCnt個協程
調用Run開始運行,調用Stop或直接析構結束
* @param uThreadCnt 線程數,小于1則為1
* @param uCoroutineCnt 每個線程的協程數,小于1則為1
* @param uJobQueueSize 總任務隊列大小,小于1則為1
*/
CoroutinePool(uint32_t uThreadCnt, uint32_t uCoroutineCnt, uint32_t uJobQueueSize = 1024000);

/**
* @brief 線程安全,可重入
* @return true:正常
*/
bool Run();

/**
* @brief 停止協程池 (會先保證池中任務完成再停止),線程安全可重入
*
*/
void Stop();

/**
* @param userFunc 用戶函數
* @return std::shared_ptr nullptr:協程池隊列滿了,提交不了
*/
std::shared_ptr Submit(const std::function &userFunc);

~CoroutinePool();
CoroutinePool(const CoroutinePool &) = delete;
CoroutinePool(CoroutinePool &&) = delete;
CoroutinePool & operator=(const CoroutinePool &) = delete;
CoroutinePool & operator=(CoroutinePool &&) = delete;

private:
static void LoopWork(CoroutinePool &oPool);

private:
bool m_bStarted;
uint32_t m_uThreadCnt;
uint32_t m_uRoutineCnt;
ArraySyncQueue m_queueJob;
std::vector> m_vecThread;
std::mutex m_oMutex;
std::condition_variable m_oCondition;
};
// class CoroutinePool end

// class Future start
class Future {
public:
/**
* @brief 阻塞獲得結果
*
* @param uTimeoutMs 超時時間
* @return true:成功, false:超時
*/
bool Get(uint32_t uTimeoutMs = -1);

/**
* @brief 設置狀態為完成
*/
void SetFinished();

Future();

Future(const Future&) = delete;
Future(Future&&) = delete;

Future & operator=(const Future&) = delete;
Future & operator=(Future&&) = delete;

private:
std::mutex m_oMutex;
std::condition_variable m_oCondition;
bool m_bFinished;
};
// class Future end
}()>*>()>

coroutine.cpp

/**
* @file coroutine.cpp
* @author souma
* @brief 協程池的具體實現
* @version 0.1
* @date 2023-06-06
*
* @copyright Copyright (c) 2023
*
*/

#include "coroutine.h"
#include

using namespace std;
namespace comm {

// class Coroutine start
Coroutine::Coroutine() {
m_pTaskCtx = nullptr;
}

void Coroutine::Register() {
m_pTaskCtx = make_shared();
m_pTaskCtx->m_userFunc = [](){};
m_pTaskCtx->m_pFuture = nullptr;
SaveReg();
}

void Coroutine::Register(shared_ptr pTaskCtx) {
m_pTaskCtx = pTaskCtx;
SaveReg();
}

inline void Coroutine::Yield() {
Coroutine::Switch(this, Coroutine::GetCtx().m_pMainCoroutine.get());
}

bool Coroutine::CoYield() {
if (GetCtx().m_vecCoroutine.size() == 0) {
return false;
}
GetCtx().m_vecCoroutine[GetCtx().m_uCursor]->Yield();
return true;
}

CoroutinePoolCtx & Coroutine::GetCtx() {
thread_local CoroutinePoolCtx coroutinePoolCtx;
return coroutinePoolCtx;
}

void Coroutine::MoveCursor() {
GetCtx().m_uCursor = GetCtx().m_uCursor == GetCtx().m_vecCoroutine.size() - 1 ? 0 : GetCtx().m_uCursor + 1;
}

extern "C" __attribute__((noinline, weak))
void Coroutine::Switch(Coroutine *pPrev, Coroutine *pNext) {
// 1.保存pPrev協程的上下文, rdi和pPrev同指向
// 2.加載pNext協程的上下文, rsi和pNext同指向
asm volatile(R"(
movq %rsp, %rax
movq %rbp, 104(%rdi)
movq %rax, 96(%rdi)
movq %rbx, 88(%rdi)
movq %rcx, 80(%rdi)
movq %rdx, 72(%rdi)
movq 0(%rax), %rax
movq %rax, 64(%rdi)
movq %rsi, 56(%rdi)
movq %rdi, 48(%rdi)
movq %r8, 40(%rdi)
movq %r9, 32(%rdi)
movq %r12, 24(%rdi)
movq %r13, 16(%rdi)
movq %r14, 8(%rdi)
movq %r15, (%rdi)

movq (%rsi), %r15
movq 8(%rsi), %r14
movq 16(%rsi), %r13
movq 24(%rsi), %r12
movq 32(%rsi), %r9
movq 40(%rsi), %r8
movq 48(%rsi), %rdi
movq 64(%rsi), %rax
movq 72(%rsi), %rdx
movq 80(%rsi), %rcx
movq 88(%rsi), %rbx
movq 96(%rsi), %rsp
movq 104(%rsi), %rbp
movq 56(%rsi), %rsi
movq %rax, (%rsp)
xorq %rax, %rax
)");
}

void Coroutine::DoWork(Coroutine *pThis) {
pThis->m_pTaskCtx->m_userFunc();
pThis->m_pTaskCtx->m_pFuture->SetFinished();
pThis->m_pTaskCtx.reset();
Coroutine::GetCtx().m_uWorkCnt--;
pThis->Yield();
}

void* Coroutine::GetRsp() {
// m_pRegister和m_pStack中間預留一個指針空間
auto sp = std::end(m_pStack) - sizeof(void*);
// 預定Rsp的地址保證能夠整除8字節
sp = decltype(sp)(reinterpret_cast(sp) & (~0xF));
return sp;
}

void Coroutine::SaveReg() {
void *pStack = GetRsp();
memset(m_pRegister, 0, sizeof m_pRegister);
void **pRax = (void**)pStack;
*pRax = (void*) DoWork;
// rsp
m_pRegister[12] = pStack;
// rax
m_pRegister[8] = *pRax;
// rdi
m_pRegister[6] = this;
}
// class Coroutine end

// class CoroutinePool start
CoroutinePool::CoroutinePool(uint32_t uThreadCnt, uint32_t uCoroutineCnt, uint32_t uJobQueueSize) : m_queueJob(uJobQueueSize) {
m_bStarted = false;
m_uThreadCnt = max(uThreadCnt, 1u);
m_uRoutineCnt = max(uCoroutineCnt, 1u);
}

bool CoroutinePool::Run() {
if (!__sync_bool_compare_and_swap(&m_bStarted, false, true)) {
return false;
}

for (decltype(m_uThreadCnt) i = 0; i < m_uThreadCnt; ++i) {
m_vecThread.emplace_back(make_shared(CoroutinePool::LoopWork, ref(*this)));
}
return true;
}

void CoroutinePool::Stop() {
if (!__sync_bool_compare_and_swap(&m_bStarted, true, false)) {
return;
}

m_oCondition.notify_all();
for (auto it = m_vecThread.begin(); it != m_vecThread.end(); ++it) {
(*it)->join();
}
m_vecThread.clear();
}

shared_ptr CoroutinePool::Submit(const function &userFunc) {
shared_ptr pNewFuture = make_shared();
CoroutineTaskCtx *pTaskCtx = new CoroutineTaskCtx;
pTaskCtx->m_pFuture = pNewFuture;
pTaskCtx->m_userFunc = userFunc;

if (!m_queueJob.Push(pTaskCtx)) {
delete pTaskCtx, pTaskCtx = nullptr;
return nullptr;
}
m_oCondition.notify_all();
return pNewFuture;
}

CoroutinePool::~CoroutinePool() {
Stop();
}

void CoroutinePool::LoopWork(CoroutinePool &oPool) {
Coroutine::GetCtx().m_uCursor = 0;
Coroutine::GetCtx().m_uWorkCnt = 0;
Coroutine::GetCtx().m_pMainCoroutine = shared_ptr(new Coroutine);
Coroutine::GetCtx().m_pMainCoroutine->Register();

Coroutine::GetCtx().m_vecCoroutine.clear();
for (decltype(oPool.m_uRoutineCnt) i = 0; i < oPool.m_uRoutineCnt; ++i) {
Coroutine::GetCtx().m_vecCoroutine.emplace_back(shared_ptr(new Coroutine));
}

Coroutine *pMainCoroutine, *pCurCoroutine;
while (oPool.m_bStarted || Coroutine::GetCtx().m_uWorkCnt > 0 || !oPool.m_queueJob.IsEmpty()) {

pMainCoroutine = Coroutine::GetCtx().m_pMainCoroutine.get();
pCurCoroutine = Coroutine::GetCtx().m_vecCoroutine[Coroutine::GetCtx().m_uCursor].get();

if (pCurCoroutine->HasTask()) {
Coroutine::Switch(pMainCoroutine, pCurCoroutine);
Coroutine::MoveCursor();
continue;
}

CoroutineTaskCtx *pTaskCtx = oPool.m_queueJob.Pop();
if (pTaskCtx == nullptr) {
if (Coroutine::GetCtx().m_uWorkCnt > 0) {
Coroutine::MoveCursor();
continue;
}
unique_lock oLock(oPool.m_oMutex);
oPool.m_oCondition.wait(oLock);
continue;
}

pCurCoroutine->Register(shared_ptr(pTaskCtx));
++Coroutine::GetCtx().m_uWorkCnt;
Coroutine::Switch(pMainCoroutine, pCurCoroutine);
Coroutine::MoveCursor();
}
}
// class CoroutinePool end

// class Future start
Future::Future() {
m_bFinished = false;
}

bool Future::Get(uint32_t uTimeoutMs) {
unique_lock oLock(m_oMutex);
if (m_bFinished) {
return true;
}
return m_oCondition.wait_for(oLock, chrono::milliseconds(uTimeoutMs)) == cv_status::no_timeout;
}

void Future::SetFinished() {
{
unique_lock oLock(m_oMutex);
m_bFinished = true;
}
m_oCondition.notify_all();
}
// class Future end

// class ArraySyncQueue start
template
ArraySyncQueue::ArraySyncQueue(uint32_t uCapacity, uint32_t uSleepUs, uint32_t uRetryTimes) {
for (uint32_t i = 0; i < std::max(uCapacity, 1u); ++i) {
m_vecQueue.emplace_back(nullptr);
}
m_uSleepUs = uSleepUs;
m_uRetryTimes = uRetryTimes;
}

template
bool ArraySyncQueue::Push(T *pObj) {
if (pObj == nullptr) {
return false;
}
uint32_t uRetryTimes = 0;
while (uRetryTimes <= m_uRetryTimes) {
uint32_t uPushCursor = m_uPushCursor;
if (uPushCursor == m_uPopCursor - 1 || (m_uPopCursor == 0 && uPushCursor == m_vecQueue.size() - 1)) {
// 隊列滿了
return false;
}

if (!__sync_bool_compare_and_swap(&m_vecQueue[uPushCursor], nullptr, pObj)) {
uRetryTimes++;
usleep(m_uSleepUs);
continue;
}

m_uPushCursor = GetNextCursor(uPushCursor);
return true;
}
// 競爭失敗
return false;
}

template
T* ArraySyncQueue::Pop() {
uint32_t uRetryTimes = 0;
while (uRetryTimes <= m_uRetryTimes) {
uint32_t uPopCursor = m_uPopCursor;
if (uPopCursor == m_uPushCursor) {
return nullptr;
}

T* pToReturn = m_vecQueue[uPopCursor];
if (pToReturn == nullptr || !__sync_bool_compare_and_swap(&m_vecQueue[uPopCursor], pToReturn, nullptr)) {
usleep(m_uSleepUs);
uRetryTimes++;
continue;
}
m_uPopCursor = GetNextCursor(uPopCursor);
return pToReturn;
}
return nullptr;
}

template
uint32_t ArraySyncQueue::GetNextCursor(uint32_t uCursor) {
if (uCursor == m_vecQueue.size() - 1) {
return 0;
}
return uCursor + 1;
}

template
ArraySyncQueue::~ArraySyncQueue() {
m_uRetryTimes = -1;
do {
T *pObj = Pop();
if (pObj == nullptr) {
return;
}
delete pObj, pObj = nullptr;
} while (true);
}
// class ArraySyncQueue end
}()>

8. 補充說明

8.1. 為什么不能-O0編譯?

在-O0的情況下,編譯器會給函數(coroutine.cpp:57)Coroutine::Switch包一層匯編指令,導致實際執行匯編指令不是期望的。具體可以分別用-O0和-O3在GDB下disassemble看到差異。

8.2. 如果函數使用棧很大怎么辦?

源碼中定義的協程棧為CO_STACK_SIZE=4096 + 65535KB,若用戶函數使用的棧超過該范圍會產生coredump。簡單可行的解法是:1.盡量使用堆變量;2.改大CO_STACK_SIZE。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Web服務器
    +關注

    關注

    0

    文章

    137

    瀏覽量

    24227
  • RPC
    RPC
    +關注

    關注

    0

    文章

    102

    瀏覽量

    11423
  • C++
    C++
    +關注

    關注

    21

    文章

    2066

    瀏覽量

    72900
  • 線程
    +關注

    關注

    0

    文章

    489

    瀏覽量

    19495
收藏 人收藏

    評論

    相關推薦

    聲表面波濾波器作用結構

    聲表面波濾波器是利用壓電效應和聲表面波傳播的物理特性制成的一種無源帶通濾波器。聲表面波濾波器是在一塊壓電晶體基片的兩端,各鍍上一個叉指形梳狀金屬電極(又叫換能器)。
    發表于 02-15 15:15 ?8094次閱讀
    聲表面波濾波器<b class='flag-5'>作用</b>與<b class='flag-5'>結構</b>

    低壓電器的作用與基本結構

    制電器按其工作電壓的高低,以交流1200V、直流1500V為界,可劃分為高壓控制電器和低壓控制電器兩大類。交流1200V及以下、直流1500V及以下的均稱為低壓電器。
    發表于 05-12 14:55 ?1.1w次閱讀

    PG1200系列數據存儲型控制器作用結構

    PG1200是能使用頂部面板簡單設定4個定位數據的控制器。安裝方式有DIN導軌安裝用于嵌入式安裝用兩種。PG1200可進行以下運行。 1、定位運行(INDEX運行)有STEP選擇定位運行、STEP順序定位運行2中方式 2、機械原點返回運行(HOME運行) 3、連續運行(SCAN運行) 4、單脈沖運行(微動運行:僅限測試模式)
    發表于 10-20 09:39 ?2次下載

    USS通訊的定義、作用、結構及使用過程圖文詳解

    在使用這個庫的時候切記一定要為該庫分配庫存儲區,具體的方法是:右鍵點擊庫,選擇庫存儲區,在建議地址一欄輸入一個地址區間,這個地址區間將作為USS指令庫的背景數據區,切記該數據區不得與程序當中其它的地址區沖突,否則會導致USS庫文件的使用不正常。
    的頭像 發表于 11-30 14:58 ?4.2w次閱讀
    USS通訊的定義、<b class='flag-5'>作用</b>、<b class='flag-5'>結構</b>及使用過程圖文詳解

    工字電感作用結構組成

    大部分電子器件都含有RF電感?!盀榱烁檮游?,在我們家養動物的皮膚中植入的玻璃管內部都含有一個電感”,普萊默公司的一位研發工程師 Maria del Mar Villarrubia說,“每次啟動汽車的時候兩個電感之間都會產生無線通信,一個在汽車內部,另一個在鑰匙內部?!?/div>
    發表于 03-03 14:52 ?1.1w次閱讀
    工字電感<b class='flag-5'>作用</b>及<b class='flag-5'>結構</b>組成

    電子皮膚的作用,基本結構及工作原理

    大小和指定點的微應力大小,將各個點的檢測數據輸出到外圍電路,以達到實時檢測作用于電子皮膚的界面應力分布的目的。
    的頭像 發表于 03-06 15:37 ?1.8w次閱讀

    熱繼電器的作用結構

    本文首先介紹了熱繼電器的作用,其次介紹了熱繼電器結構,最后闡述了熱繼電器選擇方法。
    發表于 06-24 15:15 ?7720次閱讀

    電源濾波器作用_電源濾波器結構

    本文主要闡述了電源濾波器作用結構。
    發表于 12-31 10:48 ?4284次閱讀

    電源設備中噪聲濾波器的作用及基本結構

    隨著現代科學技術的飛速發展,電子、電力電子、電氣設備應用越來越廣泛,它們在運行中產生的高密度、寬頻譜的電磁信號充滿整個空間,形成復雜的電磁環境。復雜的電磁環境要求電子設備及電源具有更高的電磁兼容性。
    的頭像 發表于 04-18 10:26 ?1190次閱讀

    簡述制動信號傳輸器 BST作用結構組成

    制動信號傳輸器(BST)的作用是將駕駛員的減速請求以電子方式傳輸到ECU,以便ECU以極快的速度控制前橋模塊和后橋模塊進行精確的制動壓力控制,并在制動過程中平衡各車軸之間的制動力。
    的頭像 發表于 03-02 09:45 ?1685次閱讀
    簡述制動信號傳輸器 BST<b class='flag-5'>作用</b>與<b class='flag-5'>結構</b>組成

    智能變電站的作用結構

     智能變電站是一種利用信息技術、自動化技術、通訊技術等高新技術來實現變電站更智能化、數字化、集成化運行的新型變電站。通常包括智能化監測、智能化控制、智能化調度、智能化維護等多個方面。
    發表于 04-23 18:25 ?888次閱讀

    獨石電容的作用結構

    阻小、允許脈動電流大、可靠性高、高頻阻抗低、耐高溫、絕緣性好、成本低等,但是獨石電容制造復雜,比一般電容制造要去要求嚴格。下面將來了解獨石電容的結構以及作用。
    的頭像 發表于 05-29 15:59 ?1811次閱讀
    獨石電容的<b class='flag-5'>作用</b>與<b class='flag-5'>結構</b>

    FFC排線技術特征、作用、結構

    【專業術語】FFC排線、FFC CABLE、柔性扁平電纜線、FLEXABLE FLAT CABLE 【專業作用】移動部件與主板之間、PCB板與PCB板之間、連接器與連接器之間、座子與座子之間、模組
    發表于 07-20 16:18 ?0次下載

    斷路器的作用、結構及異常處理方法

    正常情況下:切斷和接通正常情況下的空載電流和負荷電流
    的頭像 發表于 07-27 14:46 ?1173次閱讀
    斷路器的<b class='flag-5'>作用</b>、<b class='flag-5'>結構</b>及異常處理方法

    開關電源的優缺點、作用及基本結構

     開關電源是一種利用電子元器件(如晶體管、二極管、場效應管等)控制電流的開關狀態,實現電源輸出電壓的調節和穩定的一種電源。其基本工作原理是通過開關器件對輸入電壓進行切換,從而改變輸出電壓的大小和極性。
    發表于 09-08 18:25 ?2050次閱讀
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>