【Linux】線程封裝與互斥(萬字)

提示:文章寫完后,目錄可以自動生成,如何生成可參考右邊的幫助文檔

目錄

文章目錄

前言

C++多線程的用法

對原生線程進行一次封裝

理解pthread線程

Linux線程互斥

進程線程間的互斥相關背景概念

互斥量mutex

操作共享變量會有問題的售票系統代碼

互斥量的接口

初始化互斥量

銷毀互斥量

互斥量加鎖和解鎖

改進上面的售票系統:

方法一:定義一個靜態或全局的鎖變量gmutex

方法二:定義一個局部的鎖

方法三:?臨時對象, RAII風格的加鎖和解鎖(構造加鎖,析構解鎖)

互斥量實現原理探究

可重入VS線程安全

概念

常見的線程不安全的情況

常見的線程安全的情況

常見不可重入的情況

常見可重入的情況

可重入與線程安全聯系

可重入與線程安全區別

總結



前言

世上有兩種耀眼的光芒,一種是正在升起的太陽,一種是正在努力學習編程的你!一個愛學編程的人。各位看官,我衷心的希望這篇博客能對你們有所幫助,同時也希望各位看官能對我的文章給與點評,希望我們能夠攜手共同促進進步,在編程的道路上越走越遠!


提示:以下是本篇文章正文內容,下面案例可供參考

C++多線程的用法

#include <thread> // C++多線程所對應的頭文件
#include <unistd.h>void threadrun(int num)
{while(num){std::cout << "I am a thread, num: " << num << std::endl;sleep(1);}
}
int main()
{std::thread t1(threadrun, 10);std::thread t2(threadrun, 10);std::thread t3(threadrun, 10);std::thread t4(threadrun, 10);std::thread t5(threadrun, 10);while(true){std::cout << "I am a main thread "<< std::endl;sleep(1);}t1.join();t2.join();t3.join();t4.join();t5.join();return 0;
}

對原生線程進行一次封裝

C++11的多線程,是對原生線程的封裝,所以在編譯時,要鏈接上 -lpthread原生線程庫。

為什么要封裝呢?

  • 語言的跨平臺性。在Linux當中,我們所使用的C++11的多線程,用的是Linux的pthread庫;如果是在Windows當中,用的是Windows的原始對應的系統調用創建線程的接口,C++在給Linux和Windows當中提供的標準庫是不一樣的,C++給我們提供的標準庫編譯出來,在Windows中是Windows版本的,在Linux中是Linux版本的,所以對應的庫不一樣,但是代碼是一樣的。

Windows當中還要不要包含pthread庫呢?

  • 不需要。語言具有跨平臺性。

其它語言呢?

  • 大部分的語言要在Linux下跑多線程,必須要用原生線程庫,因為pthread庫是Linux提供多線程的底層唯一方式。
thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>// C++線程的頭文件namespace ThreadModule
{// using就相當于typedef,using定義了一個新類型std::function<void(T&)>,是一個新語法template<typename T>using func_t = std::function<void(T)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data);}public:Thread(func_t<T> func, T data, const std::string& name = "none-name"): _func(func), _data(data), _threadname(name), _stop(true){}// 方法:static修飾的函數中的參數是沒有this指針的// 因為沒有this指針,所以該函數里面也無法調用該類的成員對象了static void* threadroutine(void* args) // 類成員函數,形參是有this指針的!!{Thread<T>* self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){// pthread_create()函數中的參數3的函數指針,要求的參數類型是void*,// 而threadroutine()函數是類成員函數,有一個this指針,所以調不了該函數int n = pthread_create(&_tid, nullptr, threadroutine, this);// 把當前對象this傳threadroutine()if (!n){_stop = false;return true;}else{return false;}}void Detach(){if (!_stop){pthread_detach(_tid);}}void Join(){if (!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;  // 模板的參數類型T就直接是指針了func_t<T> _func;bool _stop;};
} #endif
testThread.cc
using namespace ThreadModule;void print(int &cnt)
{while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}
}const int num = 10;int main()
{std::vector<Thread<int> > threads;// 1. 創建一批線程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, 10, name);}// 2. 啟動 一批線程for (auto &thread : threads){thread.Start();}// 3. 等待一批線程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}// Thread<int> t1(print, 10);// t1.Start();// std::cout << "name: " << t1.name() << std::endl;// t1.Join();return 0;
}

理解pthread線程

  • 二進制代碼剛運行時,pthread_t tid;只有一個進程,等執行到pthread_create()代碼時,才創建出了新線程,從操作層面上:創建、終止、等待線程的接口都是在庫當中實現的,庫是將輕量級進程做了封裝,所以給上層用戶提供的就是庫當中的方法,所以把我們用的線程叫做用戶級線程。
  • 線程庫首先要映射到當前進程的地址空間中(堆棧之間的共享區)!
  • 線程的管理工作要由庫來進行管理!

那么庫要如何管理線程呢?

  • 先描述,再組織!
  • 庫里面要有描述線程的結構體,以及把所有的線程都組織在一起,線程的控制塊:struct_pthread,一般我們喜歡將線程的控制塊叫做struct_tcb,只不過Linux不提供struct_tcb,創建一個線程就為我們在庫當中維護一個控制塊結構,而每一個控制塊結構的起始地址,就叫做線程的tid。tid的本質就是一個堆棧之間共享區的線程庫中的控制塊結構的起始地址(虛擬地址)。

  • 線程的整體結構:struct_pthread、線程局部存儲、線程棧。
  • 動態庫是共享庫,多個進程,每一個進程都創建多個線程,每個進程的進程地址空間堆棧之間的共享區都是同一個動態庫。
  • 全局變量在進程地址空間當中的已初始化數據區。
  • 線程局部存儲:不能用來存儲stl的容器數據,只能用來存儲內置類型,因為它是一個C語言的庫,不認識其它容器的數據。

線程庫是不是磁盤當中的一個普通文件呢?

  • 線程庫是一個動態庫,也是磁盤當中的一個文件。

執行流(task_struct)是如何找到線程的線程棧的?

man clone

輕量級進程是Linux當中線程實現的底層方案,但真正線程實現是在庫當中實現的。

Linux線程互斥

進程線程間的互斥相關背景概念

  • 臨界資源:多線程執行流共享的資源就叫做臨界資源
  • 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區
  • 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用
  • 原子性(后面討論如何實現):不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完成
  • 當共享資源做了保護,就叫做臨界資源。

互斥量mutex

  • 大部分情況,線程使用的數據都是局部變量,變量的地址空間在線程棧空間內,這種情況,變量歸屬單個線程,其他線程無法獲得這種變量。
  • 但有時候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數據的共享,完成線程之間的交互。
  • 多個線程并發的操作共享變量,會帶來一些問題。

操作共享變量會有問題的售票系統代碼

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <pthread.h> 
int ticket = 100;
void* route(void* arg)
{char* id = (char*)arg;while (1) {if (ticket > 0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else {break;}}
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

一次執行結果:
thread 4 sells ticket : 100
...
thread 4 sells ticket : 1
thread 2 sells ticket : 0
thread 1 sells ticket : -1
thread 3 sells ticket : -2

搶票的結構最終出現了負數,造成了數據不一致,為什么?

  • 因為g_tickets是一個全局的變量,這個全局的變量是沒有被保護起來的,并且對全局變量_tickets的判斷不是原子的。
  • 當_tickets == 1時,多個線程并發的判斷,讓很多線程都進入搶票邏輯。
  • 全局變量g_tickets是在內存當中的,當線程1執行if語句時,要進行票數的判斷,判斷是邏輯運算,必須在CPU內部運行,此時內存中的票數數據拷貝到CPU的寄存器當中,執行到usleep語句時,線程1被切換了出去,因為寄存器只有一套,所以為了保存線程1的上下文數據,數據被線程1帶走了;
  • 此時切換到線程2執行判斷邏輯,與線程1的情況一樣也被切換走了,線程3和4都是如此;
  • 那么當線程1再次切換回來的時候,要重新在內存中讀取數據,打印并--操作;
  • _tickets--(不是原子的)等價于_tickets = _tickets - 1;--操作數據改變,會影響原生內存中的數據,因為會寫回內存;
  • 那么其它3個線程也都進入了搶票邏輯,所以會讀取內存中的數據,打印并--操作,所以會打印出了負數的情況。

-- 操作并不是原子操作,而是對應三條匯編指令:

  • load :將共享變量ticket從內存加載到寄存器中
  • update : 更新寄存器里面的值,執行-1操作
  • store :將新值,從寄存器寫回共享變量ticket的內存地址

要解決以上問題,需要做到三點:

  • 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他線程進入該臨界區。
  • 如果多個線程同時要求執行臨界區的代碼,并且臨界區沒有線程在執行,那么只能允許一個線程進入該臨界區。
  • 如果線程不在臨界區中執行,那么該線程不能阻止其他線程進入臨界區。

要做到這三點,本質上就是需要一把鎖。Linux上提供的這把鎖叫互斥量。

互斥量的接口

初始化互斥量

初始化互斥量有兩種方法:

  • 方法1,如果你定義的鎖是靜態的或者是全局的:那么這個鎖可以不用init初始化和destroy銷毀;你可以直接定義一個鎖,并用PTHREAD_ MUTEX_ INITIALIZER宏對其進行初始化。
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 
  • 方法2,動態分配:

如果這把鎖是一個局部的:建議init初始化和destroy銷毀

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict 
attr); 
參數: 
mutex:要初始化的互斥量 
attr:NULL 

嘗試的去申請鎖:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
嘗試的去申請鎖,跟申請鎖成功和函數調用失敗是一樣的,但是申請鎖失敗了,不會阻塞,會立馬出錯返回。

銷毀互斥量

銷毀互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要銷毀
  • 不要銷毀一個已經加鎖的互斥量
  • 已經銷毀的互斥量,要確保后面不會有線程再嘗試加鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex); 

互斥量加鎖和解鎖

int pthread_mutex_lock(pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
返回值:成功返回0,失敗返回錯誤號 

調用 int pthread_mutex_lock(pthread_mutex_t *mutex)?時,可能會遇到以下情況:

  • 申請成功:函數就會返回,允許你繼續向后運行;
  • 申請鎖失敗:函數就會阻塞,不允許你繼續向后運行;
  • 函數調用失敗:出錯返回,比如:申請鎖的對象已經被釋放了

改進上面的售票系統:

方法一:定義一個靜態或全局的鎖變量gmutex

// 搶票邏輯
#include <iostream>
#include <vector>
#include <mutex> // C++11里面鎖的頭文件
#include "Thread.hpp"using namespace ThreadModule;// 數據不一致
int g_tickets = 10000; // 共享資源,沒有保護的
// 線程的數據類型
class ThreadData
{
public:ThreadData(int& tickets, const std::string& name): _tickets(tickets), _name(name), _total(0)){}~ThreadData(){}public:int& _tickets; // 所有的線程,最后都會引用同一個全局的g_ticketsstd::string _name;int _total;
};// 方法一:定義一個靜態或全局的鎖變量gmutex
// gmutex中的g表示globle的意思
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;// 定義一個鎖// 線程執行的方法void route(ThreadData *td){// 加鎖while (true){// 訪問臨界資源的代碼,叫做臨界區!// 我們加鎖,本質就是把多線程的并行執行變為串行執行 --- 加鎖的力度要越細越好pthread_mutex_lock(&gmutex); // 加鎖 : 競爭鎖是自由競爭的,競爭鎖的能力太強的線程,會導致其他線程搶不到鎖 --- 造成了其他線程的饑餓問題!if (td->_tickets > 0)     {// 模擬一次搶票的邏輯usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--;  pthread_mutex_unlock(&gmutex); // 解鎖 法一:td->_total++;}else{pthread_mutex_unlock(&gmutex); // 解鎖 法一:break;}}// 解鎖}const int num = 4;
int main()
{std::cout << "main: &tickets: " << &g_tickets << std::endl;// std::mutex mutex;// C++11的做法,不用初始化,因為它有構造函數std::vector<Thread<ThreadData*>> threads;std::vector<ThreadData*> datas;// 每個線程搶了多少張票// 1. 創建一批線程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData* td = new ThreadData(g_tickets, name);threads.emplace_back(route, td, name);datas.emplace_back(td);}// 2. 啟動 一批線程for (auto& thread : threads){thread.Start();}// 3. 等待一批線程for (auto& thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}sleep(1);// 4. 輸出統計數據for (auto data : datas){std::cout << data->_name << " : " << data->_total << std::endl;delete data;}// pthread_mutex_destroy(&mutex);return 0;
}

方法二:定義一個局部的鎖

// 搶票邏輯
#include <iostream>
#include <vector>
#include <mutex> // C++11里面鎖的頭文件
#include "Thread.hpp"using namespace ThreadModule;// 數據不一致
int g_tickets = 10000; // 共享資源,沒有保護的
// 線程的數據類型
class ThreadData
{
public:ThreadData(int& tickets, const std::string& name, pthread_mutex_t &mutex): _tickets(tickets), _name(name), _total(0), _mutex(mutex){}~ThreadData(){}public:int& _tickets; // 所有的線程,最后都會引用同一個全局的g_ticketsstd::string _name;int _total;pthread_mutex_t& _mutex;
};// 線程執行的方法void route(ThreadData *td){// 加鎖while (true){       // 方法二:加鎖pthread_mutex_lock(&td->_mutex);if (td->_tickets > 0)      {// 模擬一次搶票的邏輯usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); td->_tickets--;  pthread_mutex_unlock(&td->_mutex); // 解鎖 法二:td->_total++;}else{pthread_mutex_unlock(&td->_mutex); // 解鎖 法二:break;}}// 解鎖}const int num = 4;
int main()
{// 方法二:定義一個局部的鎖pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);// 鎖的屬性設為nullptr// std::mutex mutex;// C++11的做法,不用初始化,因為它有構造函數std::vector<Thread<ThreadData*>> threads;std::vector<ThreadData*> datas;// 每個線程搶了多少張票// 1. 創建一批線程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData* td = new ThreadData(g_tickets, name, mutex);// 把局部的鎖,以參數的形式傳遞到線程內部,而不是以全局的形式threads.emplace_back(route, td, name);datas.emplace_back(td);}// 2. 啟動 一批線程for (auto& thread : threads){thread.Start();}// 3. 等待一批線程for (auto& thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}sleep(1);// 4. 輸出統計數據for (auto data : datas){std::cout << data->_name << " : " << data->_total << std::endl;delete data;}pthread_mutex_destroy(&mutex);return 0;
}

方法三:?臨時對象, RAII風格的加鎖和解鎖(構造加鎖,析構解鎖)

LockGuard.hpp
#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__#include <iostream>
#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex) :_mutex(mutex){pthread_mutex_lock(_mutex); // 構造加鎖}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t* _mutex;
};#endif
void route(ThreadData* td)
{while (true){{   // 擔心就用這個LockGuard guard(&td->_mutex); // 臨時對象, RAII風格的加鎖和解鎖(構造加鎖,析構解鎖)// td->_mutex.lock();C++11的做法// std::lock_guard<std::mutex> lock(td->_mutex);C++11中也封裝了lock_guardif (td->_tickets > 0) // 1{usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); // 2td->_tickets--;                                                           // 3td->_total++;// td->_mutex.unlock();C++11}else{// td->_mutex.unlock();break;}}}
}

什么是原子的?

  • 一條語句將來被匯編之后,只有一條匯編。

互斥量實現原理探究

  • 經過上面的例子,大家已經意識到單純的 i++ 或者 ++i 都不是原子的,有可能會有數據一致性問題
  • 為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把寄存器和內存單元的數據相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺,訪問內存的總線周期也有先后,一 個處理器上的交換指令執行時另一個處理器的交換指令只能等待總線周期。 現在我們把lock和unlock的偽代碼改一下

互斥的底層實現:

  1. 假設CPU中有一個寄存器%al,鎖相當于內存當中的整型變量;
  2. 假設剛開始把鎖初始化為1,線程1此時要申請鎖,它把0放入%al的寄存器中,再把寄存器中的值和鎖變量中的值進行交換,就完成了加鎖,若線程1加鎖成功,那么線程1就執行它的代碼;
  3. 假設線程1在執行完第二條語句時,線程1被切換成線程2,線程1被切換走時,會把寄存器中的數據帶走;
  4. 線程2開始申請鎖,線程2調用pthread_mutex_lock()函數從0開始申請鎖,將0放入寄存器中,再與內存中的值做交換,因為都是0,所以申請鎖失敗,掛起等待,將數據帶走;
  5. 等線程1回來時,將之前帶走的數據恢復到寄存器中,加鎖成功;
  6. 成功了之后,還要解鎖,將mutex變量重新置為1。

  • 寄存器內部的數據不屬于CPU,它屬于當前線程的硬件上下文
  • 臨界區內部,正在訪問臨界區的線程,可以被OS切換調度,被切換出去的時候,把鎖也帶走了。申請鎖成功的線程1正在訪問臨界區,即使線程1被掛起了,其它任何線程都進不來臨界區,那么臨界區對于其它的線程來說就是原子的,該線程是安全的。
  • 互斥是為了解決數據安全的問題;同步是為了解決資源被充分利用的問題。
  • 線程被切換的時機是隨機的。
  • 交換的本質:不是拷貝到寄存器,而是所有線程在爭鎖的時候,只有一個1。
  • 交換的時候,只有一條匯編 --- 原子的。
  • CPU寄存器硬件只有一套,但是CPU寄存器內部的數據是線程的硬件上下文。
  • 數據在內存里,所有線程都能訪問,屬于共享的。但是如果轉移到CPU內部寄存器中,就屬于一個線程私有了。
  • 互斥:任何時刻只允許一個線程進行訪問。

線程互斥:

  • 保護并不是把臨界資源怎么樣,而是保護多個線程都會執行訪問臨界資源的代碼,我們要保護的是臨界區。

可重入VS線程安全

概念

  • 線程安全:多個線程并發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作, 并且沒有鎖保護的情況下,會出現該問題。
  • 重入:同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之為重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數,否則,是不可重入函數。

常見的線程不安全的情況

  • 不保護共享變量的函數
  • 函數狀態隨著被調用,狀態發生變化的函數
  • 返回指向靜態變量指針的函數
  • 調用線程不安全函數的函數

常見的線程安全的情況

  • 每個線程對全局變量或者靜態變量只有讀取的權限,而沒有寫入的權限,一般來說這些線程是安全的
  • 類或者接口對于線程來說都是原子操作
  • 多個線程之間的切換不會導致該接口的執行結果存在二義性

常見不可重入的情況

  • 調用了malloc/free函數,因為malloc函數是用全局鏈表來管理堆的
  • 調用了標準I/O庫函數,標準I/O庫的很多實現都以不可重入的方式使用全局數據結構
  • 可重入函數體內使用了靜態的數據結構

常見可重入的情況

  • 不使用全局變量或靜態變量
  • 不使用用malloc或者new開辟出的空間
  • 不調用不可重入函數
  • 不返回靜態或全局數據,所有數據都有函數的調用者提供
  • 使用本地數據,或者通過制作全局數據的本地拷貝來保護全局數據

可重入與線程安全聯系

  • 函數是可重入的,那就是線程安全的
  • 函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題
  • 如果一個函數中有全局變量,那么這個函數既不是線程安全也不是可重入的

可重入與線程安全區別

  • 可重入函數是線程安全函數的一種
  • 線程安全不一定是可重入的,而可重入函數則一定是線程安全的。
  • 如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數若鎖還未釋放則會產生 死鎖,因此是不可重入的。


總結

好了,本篇博客到這里就結束了,如果有更好的觀點,請及時留言,我會認真觀看并學習。
不積硅步,無以至千里;不積小流,無以成江海。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/40029.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/40029.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/40029.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[go-zero] goctl 生成api和rpc

文章目錄 1.goctl 概述2.go-zero 需要安裝的組件3.生成 api4.生成 rpc 1.goctl 概述 goctl支持多種rpc&#xff0c;較為流行的是google開源的grpc&#xff0c;這里主要介紹goctl rpc protoc的代碼生成與使用。protoc是grpc的命令&#xff0c;作用是將proto buffer文件轉化為相…

探討命令模式及其應用

目錄 命令模式命令模式結構命令模式適用場景命令模式優缺點練手題目題目描述輸入描述輸出描述題解 命令模式 命令模式是一種行為設計模式&#xff0c; 它可將請求轉換為一個包含與請求相關的所有信息的獨立對象。 該轉換讓你能根據不同的請求將方法參數化、 延遲請求執行或將其…

《亞馬遜搬運亞馬遜產品》配合跟賣采集爬取跟賣店鋪高質量

亞馬遜高質量產品如何搬運&#xff1f;亞馬遜采集亞馬遜。 哈嘍大家好&#xff0c;大家講一下做亞馬遜是發貨、鋪貨這塊的功能。目前這款軟件做跟賣大家都知道&#xff0c;同時也支持做鋪貨。鋪貨可以采集國內的1688、淘寶、京東都可以采&#xff0c;采完之后也可以采速賣通&a…

周周星分享7.3—基于氣象大數據的自動站實況聯合預測

賽題 2024中國高校計算機大賽 — 大數據挑戰賽 經驗分享 大家好&#xff0c;我是掃地僧團隊的隊長&#xff0c;以前參加這樣打榜的比賽比較少&#xff0c;了解的打榜技巧不是太多&#xff0c;所以想從科研的角度給大家一點分享。 這次比賽主要從以下五個步驟進行&#xff1a…

Linux Doxygen快速生成文檔

此前寫過一篇編寫Doxygen格式的注釋以用于生成文檔,點擊以查閱, Doxygen常用語法與字段記錄,但是當時用的windows桌面版的doxygen,最近使用ubuntu編寫代碼想直接使用doxygen生成,故寫下此博客 Doxygen Doxygen是一個用于生成軟件文檔的工具&#xff0c;它可以從代碼中提取注釋…

(四)opengl函數加載和錯誤處理

#include <glad/glad.h>//glad必須在glfw頭文件之前包含 #include <GLFW/glfw3.h> #include <iostream>void frameBufferSizeCallbakc(GLFWwindow* window, int width, int height) {glViewport(0, 0, width, height);std::cout << width << &qu…

PHP多線程爬蟲:高效解析電商網頁內容

如何使用php多線程編寫高效的網頁爬蟲 隨著互聯網的發展和數據的不斷增長&#xff0c;網頁爬蟲成為了一種非常重要的工具。通過網頁爬蟲&#xff0c;我們可以自動地從各種網站上獲取大量的數據&#xff0c;并進行進一步的處理和分析。而PHP作為一種廣泛使用的編程語言&#xf…

Android高級面試_6_性能優化

Android 高級面試-7&#xff1a;網絡相關的三方庫和網絡協議等 1、網絡框架 問題&#xff1a;HttpUrlConnection, HttpClient, Volley 和 OkHttp 的區別&#xff1f; HttpUrlConnection 的基本使用方式如下&#xff1a; URL url new URL("http://www.baidu.com")…

SwanLinkOS首批實現與HarmonyOS NEXT互聯互通,軟通動力子公司鴻湖萬聯助力鴻蒙生態統一互聯

在剛剛落下帷幕的華為開發者大會2024上&#xff0c;伴隨全場景智能操作系統HarmonyOS Next的盛大發布&#xff0c;作為基于OpenHarmony的同根同源系統生態&#xff0c;軟通動力子公司鴻湖萬聯全域智能操作系統SwanLinkOS首批實現與HarmonyOS NEXT互聯互通&#xff0c;率先攻克基…

大模型與機器人精彩碰撞-7月5日晚上八點不見不散!

在瞬息萬變的科技時代&#xff0c;新興人工智能和機器人技術的結合正在引領新一輪的創新浪潮。你是否想成為未來科技的領航者&#xff1f;你是否想了解最前沿的AI與機器人技術&#xff1f;行麥科技重磅推出的“AIGC時代的生存法則”AI系列課&#xff0c;將為你揭開大模型與機器…

創建kset

1、kset介紹 2、相關結構體和api介紹 2.1 struct kset 2.2 kset_create_and_add kset_create_and_addkset_createkset_registerkobject_add_internalkobject_add_internal2.3 kset_unregister kset_unregisterkobject_delkobject_put3、實驗操作 #include<linux/module.…

【leetcode64-69二分查找、70-74棧、75-77堆】

二分查找[64-69] 時間復雜度O(log n)&#xff0c;要想到二分排序 35.搜索插入位置 class Solution:def searchInsert(self, nums: List[int], target: int) -> int:left 0right len(nums)-1while left < right: #左閉右閉mid (leftright)//2if nums[mid] < target…

【算法訓練記錄——Day39】

Day39——動態規劃Ⅱ 1.leetcode_62不同路徑2.leetcode_63不同路徑Ⅱ3.leetcode_343整數拆分4.leetcode_96不同的二叉樹搜索 1.leetcode_62不同路徑 思路&#xff1a;經典的動態規劃問題&#xff1a; dp[i][j]表示到達&#xff08;i&#xff0c;j&#xff09;位置時的不同路徑…

運維鍋總淺析云原生DevOps工具

本文從Tekton與Kubevela、Jenkins、GitLab CI的區別與聯系對常見的云原生DevOps工具進行對比分析&#xff0c;最后給出DevOps工具選型思路。希望對您有所幫助&#xff01; 一、DevOps簡介 DevOps是一種結合了軟件開發&#xff08;Development&#xff09;和IT運維&#xff08…

怎么在windows、linux、mac上安裝pnpm呢?

怎么在windows、linux、mac上安裝pnpm呢&#xff1f; 前言 如果您不使用獨立腳本或 pnpm/exe 來安裝 pnpm&#xff0c;則需要在系統上安裝 Node.js&#xff08;至少 v16.14&#xff09;。 原址&#xff1a;https://pnpm.io/zh/installation 使用獨立腳本安裝 即使沒有安裝…

登錄功能和校驗

基礎版 controller package com.web.management.controller;import com.web.management.pojo.Emp; import com.web.management.pojo.Result; import com.web.management.service.EmpService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.anno…

Ignis 應用: 社交 + 游戲 + 工業4.0,Ignis 構建Web3生態圈

引言 在數字經濟快速發展的今天&#xff0c;Web3技術為我們帶來了前所未有的變革。作為Ardor平臺的主要子鏈&#xff0c;Ignis公鏈在推動Web3生態系統建設中扮演了重要角色。本文將通過介紹Vessel Chain、Mythical Beings和Bridge Champ等應用&#xff0c;探討Ignis公鏈如何通…

GB/T 43566-2023中小學人造草面層足球場地檢測

人造草面層是指以類似天然草的合成纖維經機械編織固定于底布上形成人造草&#xff0c;至現場粘接并與彈性墊層等必要的其他材料組裝成整體的面層。 GB/T 43566-2023中小學人造草面層足球場地檢測項目&#xff1a; 測試項目 測試方法 人造草物理性能 GB/T 20394 人造草有害…

html+css+js文章模板

圖片 源代碼在圖片后面&#xff0c;點贊加關注&#xff0c;謝謝&#x1f604; 源代碼 <!DOCTYPE html> <html lang"zh"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width,…

redis的數據類型對應的使用場景

Redis提供了多種數據類型&#xff0c;每種數據類型都有其特定的適用場景。以下是Redis主要數據類型及其典型應用場景&#xff1a;1. 字符串(String) 應用場景&#xff1a;適用于存儲簡單的鍵值對數據&#xff0c;如用戶基本信息、計數器&#xff08;如網頁訪問次數&…