当前位置: 首页 > news >正文

linux篇【11】:linux下的线程<后序>

目录

一.线程互斥

1.三个概念

2.互斥

(1)在执行语句的任何地方,线程可能被切换走

(3)抢票场景中的问题

(4)解决方案

3.加锁

(1)加锁介绍

(2)定义/释放 互斥锁

(3)加锁、解锁(使用锁)

(4)使用锁代码

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

(2)既传name又传锁

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

2.在我被切走的时候,绝对不会有线程进入临界区!

3.加锁是原子的

(1)xchgb 交换是原子的

(2)加锁原理

(3)C++ 加锁

Makefile

Lock.hpp

mythread.cc

(4)C++ RAII加锁

4.可重入VS线程安全

5. 常见锁概念

(1)死锁


一.线程互斥

1.三个概念

1.临界资源:多个执行流都能看到并能访问的资源,临界资源
2.临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区
3.互斥特性:当我们访问某种资源的时候,任何时刻。都只有一个执行流在进行访问,这个就叫做:互斥特性

4.线程互斥:线程互斥 指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

2.互斥

没有互斥时,以抢票为例,一抢票 票数减1: int tickets;   tickets--;为例

(1)在执行语句的任何地方,线程可能被切换走

int tickets;
tickets--;tickets--是由3条语句完成的:

tickets--:有三步
① load tickets to reg
② reg-- ;
③ write reg to tickets

(2)CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。 

(3)抢票场景中的问题

情况1:线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存,A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时B的时间片到了,又切回线程A,又把9999写入内存,就错误了。

        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
            usleep(123); //模拟其他业务逻辑的执行
        }

情况2:或者在抢最后一张时,线程A先抢最后一张票,if (tickets > 0)为真,进入if语句,此时A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,此时显示票数仍是1,if (tickets > 0)为真,进入if语句,并执行tickets--;,tickets变为0,此时B的时间片到了,又切回线程A,线程A又继续执行tickets--;,此时直接把票数减到了负数,就出错了。

(4)解决方案

原子性:一件事要么不做,要么全做完

把tickets--这个临界区设为原子的,使不想被打扰,加锁

3.加锁

(1)加锁介绍

加锁范围:临界区,只要对临界区加锁,而且加锁的力度越细越好

加锁本质:加锁的本质是让线程执行临界区代码串行化

加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加

锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了

pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。

难度在加锁的临界区里面,就没有线程切换了吗????

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

  • 0表示已经有执行流加锁成功,资源处于不可访问,
  • 1表示未加锁,资源可访问。

(2)定义/释放 互斥锁

man pthread_mutex_init

① pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 定义全局/静态的互斥锁,可以用这个宏初始化

② int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);         mutex:锁的地址。attr:锁的属性设为空             

③ int pthread_mutex_destroy(pthread_mutex_t *mutex); 释放锁

(3)加锁、解锁(使用锁)

man pthread_mutex_lock

① int pthread_mutex_lock(pthread_mutex_t *mutex);  加阻塞式锁

线程1正在用锁住的代码,那线程2就要阻塞式等待线程1执行完才能使用这个锁(即执行锁住的代码)

② int pthread_mutex_trylock(pthread_mutex_t *mutex); 加非阻塞式锁

线程1正在用这个非阻塞锁(即执行锁住的代码),那线程2就直接返回,只有当没有别的线程用这个锁,自己才能用。

③ int pthread_mutex_unlock(pthread_mutex_t *mutex);  解锁

如果不解锁,比如线程1使用锁后没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

(4)使用锁代码

else那里也要解锁,否则会阻塞:线程1走else使用锁后如果没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

跟上面代码一样(可以忽略):

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h> // 仅仅是了解

// __thread int global_value = 100;

// void *startRoutine(void *args)
// {
//     // pthread_detach(pthread_self());
//     // cout << "线程分离....." << endl;
//     while (true)
//     {
//         // 临界区,不是所有的线程代码都是临界区
//         cout << "thread " << pthread_self() << " global_value: "
//              << global_value << " &global_value: " << &global_value
//              << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid)<<endl;
//         sleep(1);
//         break;
//     }
//     // 退出进程,任何一个线程调用exit,都表示整个进程退出
//     //exit(1);
//     // pthread_exit()
// }

using namespace std;

// int 票数计数器
// 临界资源
int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。
pthread_mutex_t mutex;

void *getTickets(void *args)
{
    const char *name = static_cast<const char *>(args);

    while (true)
    {
        // 临界区,只要对临界区加锁,而且加锁的粒度约细越好
        // 加锁的本质是让线程执行临界区代码串行化
        // 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
        // 锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
        // 这把锁,本身不就也是临界资源吗?锁的设计者早就想到了
        // pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!
        // 难度在加锁的临界区里面,就没有线程切换了吗????
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mutex);

            //other code
            usleep(123); //模拟其他业务逻辑的执行
        }
        else
        {
            // 票抢到几张,就算没有了呢?0
            cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    return nullptr;
}

// 如何理解exit?
int main()
{
    pthread_mutex_init(&mutex, nullptr);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
    pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");

    // sleep(1);
    // 倾向于:让主线程,分离其他线程

    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);

    // 1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。4. 线程退出的第四种方式,延后退出
    // 2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main thread一般不要退出(常驻内存的进程)
    // sleep(1);

    int n = pthread_join(tid1, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid2, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid3, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid4, nullptr);
    cout << n << ":" << strerror(n) << endl;

    pthread_mutex_destroy(&mutex);

    return 0;
}

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

#include <unistd.h>
#include <pthread.h>
#include <iostream>
using namespace std;
int tickets = 1000;
void *startRoutine(void *args)
{
    pthread_mutex_t* mutex_p= static_cast<pthread_mutex_t*>(args);
    while (true)
    {
        pthread_mutex_lock(mutex_p);//如果申请不到,线程阻塞等待
        if (tickets > 0)
        {
            usleep(1000);
            cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(mutex_p);
            //做其他的事
            usleep(500);
        }
        else
        {
            pthread_mutex_unlock(mutex_p);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;

    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

 

 

(2)既传name又传锁

完整版:

#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
using namespace std;
int tickets = 1000;
#define NAMESIZE 64
typedef struct threadData
{
    char name[NAMESIZE];
    pthread_mutex_t* mutexp;
}threadData;
void *startRoutine(void *args)
{
    threadData* td= static_cast<threadData*>(args);
    while (true)
    {
        pthread_mutex_lock(td->mutexp);//如果申请不到,线程阻塞等待
        if (tickets > 0)
        {
            usleep(1000);
            cout << "thread: " << td->name << "get a ticket: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(td->mutexp);
            //做其他的事
            usleep(500);
        }
        else
        {
            pthread_mutex_unlock(td->mutexp);
            break;
        }
    }
    return nullptr;
}

int main()
{
    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_t t1, t2, t3, t4;

    threadData *td1=new threadData();
    threadData *td2=new threadData();
    threadData *td3=new threadData();
    threadData *td4=new threadData();
    strcpy(td1->name,"thread 1");
    strcpy(td2->name,"thread 2");
    strcpy(td3->name,"thread 3");
    strcpy(td4->name,"thread 4");
    td1->mutexp=&mutex;
    td2->mutexp=&mutex;
    td3->mutexp=&mutex;
    td4->mutexp=&mutex;
    pthread_create(&t1, nullptr, startRoutine, (void *)td1);
    pthread_create(&t2, nullptr, startRoutine, (void *)td2);
    pthread_create(&t3, nullptr, startRoutine, (void *)td3);
    pthread_create(&t4, nullptr, startRoutine, (void *)td4);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

 

 

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

我在临界资源对应的临界区中上锁了,临界区还是多行代码,是多行代码就可以被切换。加锁 不等于 不会被切换。加锁后仍然可以切换进程,因为线程执行的加锁解锁等对应的也是代码,线程在任意代码处都可以被切换,只是线程加锁是原子的——要么你拿到了锁,要么没有

2.在我被切走的时候,绝对不会有线程进入临界区!

——因为每个线程进入临界区都必须先申请锁! !假设当前的锁被A申请走了,即便当前的线程A没有被调度,因为它是被切走的时候是抱着锁走的,其他线程想进入临界区需要先申请锁,但是已经有线程A持有锁了,则其他线程在申请时会被阻塞。即:一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态
,才对其他线程有意义!即:对于其他线程而言,线程A访问临界区具有一定的原子性
注意:尽量不要在临界区内做耗时的事情!因为只有持有锁的线程能访问,其他线程都会阻塞等待。

3.加锁是原子的

①每一个CPU任何时刻只能有一个线程在跑

②单独的一条汇编代码是具有原子性的

(1)xchgb 交换是原子的

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题 为了实现互斥锁操作,大多数体系结构(芯片体系结构)都提供了swap或exchange指令,该指令的作用是使用一条汇编代码把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

(2)加锁原理

mutex中的值默认是1
%al :CPU中的寄存器( 凡是在寄存器中的数据,全部都是线程的内部上下文! !
mutex :内存中的一个变量

 

加锁原理解释:线程A执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是1)  的值,交换后 寄存器%al中是1, 变量mutex中是0。还未执行判断,此时突然进程切换,线程A会自动带走%al中的上下文数据1,线程B开始执行:线程B执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是0)  的值,交换后 寄存器%al 和 变量mutex中都是0。再判断——>因为%al是0,不大于0就挂起。此时线程B挂起,该线程A继续执行,线程A会把自己上下文数据恢复到%al中,此时%al=1,该执行判断了——>因为%al是1,就返回。这样就成功做到:多个线程看起来同时在访问寄存器,但是互不影响

lock和unlock的伪代码:

(3)C++ 加锁

Makefile

mythread:mythread.cc
	g++ -o $@ $^ -lpthread -std=c++11

.PHONY:clean
clean:
	rm -f mythread

Lock.hpp

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    void lock()
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&lock_);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&lock_);
    }

private:
    pthread_mutex_t lock_;
};

class LockGuard
{
public:
    LockGuard(Mutex *mutex) : mutex_(mutex)
    {
        mutex_->lock();
        std::cout << "加锁成功..." << std::endl;
    }

    ~LockGuard()
    {
        mutex_->unlock();
        std::cout << "解锁成功...." << std::endl;
    }

private:
    Mutex *mutex_;
};

mythread.cc



int tickets = 1000;
Mutex mymutex;

// 函数本质是一个代码块, 会被多个线程同时调用执行,该函数被重复进入 - 被重入了
bool getTickets()
{
    bool ret = false; // 函数的局部变量,在栈上保存,线程具有独立的栈结构,每个线程各自一份
    LockGuard lockGuard(&mymutex); //局部对象的声明周期是随代码块的!
    if (tickets > 0)
    {
        usleep(1001); //线程切换了
        cout << "thread: " << pthread_self() << " get a ticket: " << tickets << endl;
        tickets--;
        ret = true;
    }
    cnt++;
    return ret;
}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        if(!getTickets())
        {
            break;
        }
        cout << name << " get tickets success" << endl;
        //其他事情要做
        sleep(1);
    }
}

int cnt = 10000;

int main()
{
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, nullptr, startRoutine, (void *)"thread 1");
    pthread_create(&t2, nullptr, startRoutine, (void *)"thread 2");
    pthread_create(&t3, nullptr, startRoutine, (void *)"thread 3");
    pthread_create(&t4, nullptr, startRoutine, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
}

(4)C++ RAII加锁

通过RAII思想,创建对象时加锁,出代码块时解锁

    {
        //临界资源
        LockGuard LockGuard(&mymutex);
        cnt++;
        ...
        ...
        ...
    }

4.可重入VS线程安全

(1)概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。(我们写的不加锁的抢票函数就是线程不安全函数,因为可能抢票抢到-1)
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。(90%函数是不可重入函数,带_r是可重入函数,不带_r是不可重入函数)

(2)####常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
(3)常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
(4)常见不可重入的情况
调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的
调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
(5)常见可重入的情况
不使用全局变量或静态变量
不使用用 malloc 或者 new 开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
(6)可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
(7)可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

5. 常见锁概念

(1)死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
### 死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件 : 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件 : 若干执行流之间形成一种头尾相接的循环等待资源的关系
### 避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
### 避免死锁算法
死锁检测算法 ( 了解 )
银行家算法(了解)
##7. Linux 线程同步
### 条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情
况就需要用到条件变量。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"
#include <mutex>

using namespace std;

pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;

void *startRoutine1(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexA);
        sleep(1);
        pthread_mutex_lock(&mutexB);

        cout << "我是线程1,我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    }
}
void *startRoutine2(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexB);
        sleep(1);
        pthread_mutex_lock(&mutexA);

        cout << "我是线程2, 我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexB);
        pthread_mutex_unlock(&mutexA);
    }
}

int main()
{
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, startRoutine1, nullptr);
    pthread_create(&t2, nullptr, startRoutine2, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    return 0;
}

相关文章:

  • 让学前端不再害怕英语单词(二)
  • Java培训教程给bean的属性赋值
  • Socket套接字(Java)
  • 机器人工程考研难易主观感受和客观数据
  • Bio-Helix丨Bio-Helix艾美捷Ponceaus S染色液说明书
  • 【Spring(四)】Spring基于注解的配置方式
  • 【Git】一文带你入门Git分布式版本控制系统(简介,安装,Linux命令)
  • AWS EKS 创建k8s生产环境实例
  • java计算机毕业设计html5健身房信息管理系统源码+mysql数据库+系统+lw文档+部署
  • 面向开发者的开源低代码开发工具,强烈推荐!
  • 制作一个简单HTML宠物猫网页(HTML+CSS)
  • Python题库(含答案)
  • 有效 QA 过程测量的 10 个基本指标
  • HTML小游戏11 —— 横版恐龙大冒险游戏(附完整源码)
  • vscode插件开发(四)Webview(1)
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • 2017-09-12 前端日报
  • Android 控件背景颜色处理
  • Cookie 在前端中的实践
  • css的样式优先级
  • Electron入门介绍
  • express如何解决request entity too large问题
  • flask接收请求并推入栈
  • js写一个简单的选项卡
  • pdf文件如何在线转换为jpg图片
  • PHP面试之三:MySQL数据库
  • React中的“虫洞”——Context
  • Spark RDD学习: aggregate函数
  • SQLServer插入数据
  • tensorflow学习笔记3——MNIST应用篇
  • 从地狱到天堂,Node 回调向 async/await 转变
  • 给初学者:JavaScript 中数组操作注意点
  • 用jQuery怎么做到前后端分离
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • 交换综合实验一
  • ​Java并发新构件之Exchanger
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • $redis-setphp_redis Set命令,php操作Redis Set函数介绍
  • $分析了六十多年间100万字的政府工作报告,我看到了这样的变迁
  • (4)logging(日志模块)
  • (6)添加vue-cookie
  • (function(){})()的分步解析
  • (力扣)1314.矩阵区域和
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (转)机器学习的数学基础(1)--Dirichlet分布
  • (转)树状数组
  • (轉貼) VS2005 快捷键 (初級) (.NET) (Visual Studio)
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .a文件和.so文件
  • .net(C#)中String.Format如何使用
  • .NET/C# 使用 SpanT 为字符串处理提升性能
  • .NET的数据绑定
  • .sh
  • /usr/lib/mysql/plugin权限_给数据库增加密码策略遇到的权限问题