Linux---线程

线程概念

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

在进程中,有一个task_struct的结构体,其中的一个指针指向地址空间。如果我在创建一个进程,这个进程不创建地址空间,不创建页表,也不再物理内存中创建属于这个进程的资源,它只需要指向某个已经创建好的进程的地址空间,页表,物理内存。并且地址空间中的代码区,堆区等区域都给这个新进程一部分,就可以让这两个进程都运行起来。如果我创建跟多新进程,都指向某个创建好进程的地址空间,这些新进程都是进程的执行分支,那么这些新进程就可以叫做线程。

那么Linux的实现方案是什么?

  1. 在Linux中,线程在进程内部执行,线程在进程的地址空间内运行,任何执行流都需要有资源,地址空间是进程的资源窗口。
  2. 线程的执行粒度要比进程更细,线程执行进程代码的一部分。

不同操作系统对线程的概念是一样的,但是实现方案可能会有不同。

在进程这里提到过,进程=内核数据结构(task_struct) + 代码和数据,从内核观点来说:进程是承担分配系统资源的基本实体。执行流是资源吗?是的,我们的线程是进程内部的执行流资源。操作系统就是以进程为单位,来进行的分配资源,而我们当前进程的内部,只有一个执行流。

当线程创建出来了,操作系统就要把线程管理起来,如何管理?跟进程一样吗?通过创建task_struct,调度算法,优先级算法等操作都需要搞一个,这样就会太复杂了,Linux的设计者直接用struct task_struct来模拟线程。用进程的数据结构进行复用,来模拟线程。这样就将线程管理了起来。简单来说,就是Linux没有真正的线程(因为并没有创建线程的PCB),而是用进程的内核数据结构来模拟的线程。


文章开头说,线程比进程更轻量化,这是为什么?

线程在切换的时候肯定会有自己的上下文,CPU内有大量的寄存器,线程在切换的时候要进程上下文保护,但是页表和地址空间是不需要切换的,所以切换效率就会提高。在CPU中,除了有寄存器,CPU所有以进程为载体,线程在执行的时候,本质就是进程在执行,线程是进程的一个执行分支。所以CPU内部,会有一个硬件级别的缓存,叫cache。

通过 cat /proc/cpuinfo可以查看,比如说你当前要访问第10行代码,它会将第10行到第50行(或n行)的代码全部弄到内存中去,这就叫做cache,所以进程在调度的时候,会越来越快。这个cache称为缓存的热数据,这部分数据高频被访问。一个进程内的多个线程切换的时候,上下文会变化,但是缓存的数据不会变化或者是做少量的更新。切换过程中,只需要切换,不需要做保存。如果要切换另一个进程,热数据就需要切换,重新缓存,所以线程要比进程更加轻量化。

线程的优缺点

优点

创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

缺点

  1. 性能损失
    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  2. 健壮性降低
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的 。
  3. 缺乏访问控制
    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响 。
  4. 编程难度提高
    编写与调试一个多线程程序比单线程程序困难得多 。

线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程用途

合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现 )


进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器

errno
信号屏蔽字
调度优先级


进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  1. 文件描述符表
  2. 每种信号的处理方式(SIG_IGN,SIG_DFL或者自定义的信号处理方式)
  3. 当前工作目录
  4. 用户id和组id


在内核中没有很明确的线程概念,但是有轻量级进程的概念,这就导致系统没有直接提供线程的系统调用,只提供了轻量级进程的系统调用。我们需要线程的接口,所以就有程序员在应用层开发了pthread线程库,以轻量级进程接口进行封装,为用户提供直接线程的接口。几乎所有Linux平台,都默认自带这个库 --- pthread库。

线程接口

跟线程有关的函数构成了一个完整的系列,大多数函数的名字都是以pthread_开头的,要使用这些函数,引入头文件<pthread.h>,链接这些线程函数库时要使用编译器命令的-lpthread选项。

创建线程

功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
thread:thread.ccg++ -o $@ $^ -g -lpthread -std=c++11
.PHONY:clean
clean:rm -rf thread

#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;void *handler(void *arg)
{while (1){cout << "handler " << getpid() << endl;sleep(1);}return nullptr;
}int main()
{pthread_t pt;pthread_create(&pt, nullptr, handler, nullptr);while (1){cout << "main " << getpid() << endl;sleep(1);}return 0;
}

通过ps -aL可以查看轻量级进程.每一个轻量级进程有一个pid,LWP(light weight process)就是这个id。仔细观察会发现,存在一个线程的LWP和PID是相等的,说明这个线程其实是最先创建出来的那个线程,也就是进程(单进程不就是只有一个线程吗)。

当线程是因为异常退出的时候,该进程会被杀死。


#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;void show()
{cout << "show" << endl;
}void *handler(void *arg)
{show();return nullptr;
}int main()
{pthread_t pt;pthread_create(&pt, nullptr, handler, nullptr);show();return 0;
}

一个函数可以被不同的执行流执行。

线程等待

当一个线程创建出来的时候,谁先运行?谁先退出呢?

谁先运行不清楚,但主线程应该最后一个退出,当主线程提前退出的时候,其他线程就不能运行了。所以线程是需要等待的。对于已经退出的线程来说,其空间没有被释放,仍然在进程的地址空间内。

pthread_join

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
 

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread cancel异常终掉,value ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;void *handler(void *arg)
{int cnt = 0;while (cnt++ != 10){cout << "handler" << endl;}return nullptr;
}int main()
{pthread_t pt;pthread_create(&pt, nullptr, handler, nullptr);pthread_join(pt, nullptr);cout << "main quit" << endl;return 0;
}

使用线程等待,可以保证主线程是在线程执行完毕之后,在退出。


如何获取线程的返回值?

通过 pthread_join来获取。

#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;void *handler(void *arg)
{int cnt = 0;while (cnt++ != 10){cout << "handler" << endl;}return (void*)1;
}int main()
{pthread_t pt;pthread_create(&pt, nullptr, handler, nullptr);void* retval;pthread_join(pt, &retval);cout << (long long int)retval << endl;return 0;
}

这就拿到了线程的返回值。

pthread_exit

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;void *handler(void *arg)
{int cnt = 0;while (cnt++ != 10){cout << "handler" << endl;}pthread_exit((void*)100);
}int main()
{pthread_t pt;pthread_create(&pt, nullptr, handler, nullptr);void* retval;pthread_join(pt, &retval);cout << (long long int)retval << endl;return 0;
}

pthread_cancel

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;void *handler(void *arg)
{int cnt = 0;while (cnt++ != 10){cout << "handler" << endl;}pthread_exit((void*)100);
}int main()
{pthread_t pt;pthread_create(&pt, nullptr, handler, nullptr);pthread_cancel(pt);cout << "main quit" << endl;return 0;
}

pt线程并没有执行handler函数,而是主线程运行到cancel函数的时候,线程直接取消了。


了解上面函数之后,可以自己尝试写一个用线程来计算区间和。

#include <pthread.h>
#include <unistd.h>
#include <iostream>using namespace std;class Request
{
public:Request(int start, int end, const string &threadname):_start(start),_end(end),_threadname(threadname){}public:int _start;int _end;string _threadname;
};class Response
{
public:Response(int result, int exitcode):_result(result),_exitcode(exitcode){}
public:int _result;int _exitcode;
};void *sumCount(void *args)
{Request *rq = static_cast<Request*>(args);Response *rsp = new Response(0,0);for (int i = rq->_start; i <= rq->_end; i++){rsp->_result += i;}return rsp;}int main()
{pthread_t tid;Request *rq = new Request(1, 100, "thread 1");pthread_create(&tid, nullptr, sumCount, rq);void *ret;pthread_join(tid, &ret);Response *rsp = static_cast<Response*>(ret);cout << "result " << rsp->_result << endl;cout << "exitcode " << rsp->_exitcode << endl;return 0;
}

pthread_detach

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放
资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线
程资源。

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());


joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

线程的互斥

#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>#define N 10struct threadData
{std::string tid;std::string threadname;
}void *threadRoutine(void *args)
{int i = 0;while (i < 10){std::cout << "pid: " << getpid() << std::endl;}return nullptr;
}int main()
{std::vector<pthread_t> tids;for (int i = 0; i < N; i++){threadDatta td;td.threadname = td.tid = pthread_create(&tid, nullptr, threadRoutine, &td);tids.push_back(tid);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;}

利用struct结构体创建在for循环中创建出一批线程,然后这些线程干其他的事情,这样写可不可以?不可以,因为threadDatta td;这个代码块是在for循环内部更是在主线程的栈中,而且还是一个临时变量。但是我们可以用指针,然后new出一块空间(threadData *td = new threadData;),这样这块空间是位于堆区。在执行for循环的时候,会new出来多个空间,每个线程访问的也是不同的堆空间,就算主线程中,for循环结束了,线程中把new出来的空间给保存下来。所有的线程,是共享堆空间的。


#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>#define N 10struct threadData
{std::string tid;std::string threadname;
};void InitThreadData(threadData* td, int number, pthread_t tid)
{td->threadname = "thread-" + std::to_string(number);char buf[128];snprintf(buf, sizeof(buf), "0x%x", tid);td->tid = buf;
}void *threadRoutine(void *args)
{threadData *td = static_cast<threadData*>(args);int i = 0;while (i < 10){std::cout << "pid: " << getpid() << ", tid : " << td->tid << ", threadname : " << td->threadname << std::endl;sleep(1);i++;}delete td;return nullptr;
}int main()
{std::vector<pthread_t> tids;for (int i = 0; i < N; i++){pthread_t tid;threadData *td = new threadData;InitThreadData(td, i, tid);pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;}

代码不难理解,这样就创建出来了一批线程。

所有的线程执行的都是同一个函数。


#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>#define N 10struct threadData
{std::string tid;std::string threadname;
};void InitThreadData(threadData* td, int number, pthread_t tid)
{td->threadname = "thread-" + std::to_string(number);char buf[128];snprintf(buf, sizeof(buf), "0x%x", tid);td->tid = buf;
}void *threadRoutine(void *args)
{threadData *td = static_cast<threadData*>(args);int i = 0;int test = 0;while (i < 10){std::cout << "pid: " << getpid() << ", tid : " << td->tid << " test " << &test << std::endl;sleep(1);i++;}delete td;return nullptr;
}int main()
{std::vector<pthread_t> tids;for (int i = 0; i < N; i++){pthread_t tid;threadData *td = new threadData;InitThreadData(td, i, tid);pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;}

在一个函数中定义了一个test变量,然后每个线程都打印这个变量的地址。

每一个线程都会有自己独立的栈结构。其实线程和线程之间,几乎没有秘密,线程的栈上数据,也是可以被其他线程看到并访问的。


#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>#define N 10
int test = 0;
struct threadData
{std::string tid;std::string threadname;
};void InitThreadData(threadData* td, int number, pthread_t tid)
{td->threadname = "thread-" + std::to_string(number);char buf[128];snprintf(buf, sizeof(buf), "0x%x", tid);td->tid = buf;
}void *threadRoutine(void *args)
{threadData *td = static_cast<threadData*>(args);int i = 0;while (i < 10){std::cout << "pid: " << getpid() << ", tid : " << td->tid << " test " << &test << std::endl;sleep(1);i++;}delete td;return nullptr;
}int main()
{std::vector<pthread_t> tids;for (int i = 0; i < N; i++){pthread_t tid;threadData *td = new threadData;InitThreadData(td, i, tid);pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;}

将test定义为全局变量的话,各个线程打印出来的地址都是一样的。全局变量是被所有的线程同时看到并访问的。

线程可以要一个私有的全局变量吗?

可以

#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>#define N 10
__thread int test = 0;
struct threadData
{std::string tid;std::string threadname;
};void InitThreadData(threadData* td, int number, pthread_t tid)
{td->threadname = "thread-" + std::to_string(number);char buf[128];snprintf(buf, sizeof(buf), "0x%x", tid);td->tid = buf;
}void *threadRoutine(void *args)
{threadData *td = static_cast<threadData*>(args);int i = 0;while (i < 10){std::cout << "pid: " << getpid() << ", tid : " << td->tid << " test " << &test << std::endl;sleep(1);i++;}delete td;return nullptr;
}int main()
{std::vector<pthread_t> tids;for (int i = 0; i < N; i++){pthread_t tid;threadData *td = new threadData;InitThreadData(td, i, tid);pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;}

在全局变量的类型前面加上 __thread,线程就可以私有一份全局变量了,这种技术叫做线程的局部存储。__thread不是C++提供的东西,而是一个编译选项。只能定义内置类型,不能用来修饰自定义类型。


新电影出来了,电影院要卖票,我们可以模拟一下卖票的过程。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;#define N 4int tickets = 1000; // 用多线程,模拟抢票class threadData
{
public:threadData(int number){threadname = "thread-" + to_string(number);}
public:string threadname;
};void *getTicket(void *args)
{threadData* td = static_cast<threadData*>(args);const char* name = td->threadname.c_str();while (true){if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;}else {break;}}printf("%s ... quit\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData*> thread_datas;for (int i = 1; i <= N; i++){pthread_t tid;threadData* td = new threadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}

在程序退出的时候,票抢到了-2。我们的代码写的是,>0才能抢票,但是居然出现了<0的数字。

为什么会出现这样的问题?

在所有线程执行的时候,tickets是一个共享数据,这个数据在多线程的并发访问下,造成了数据不一致问题。这个问题肯定和多线程并发访问是有关系的。那么就会出现,一个进程正在进行tickets--操作的时候,另一个进程读取到了--之前的数字,这就造成了数据不一致的问题。

想一下,对一个全局变量进行多线程并发--/++操作是否安全?

线程1读取数据,在读取的时候,线程可能会被切换,在任意时间,任意地点都可以切换。当线程1刚读取数据,就被切换了,要知道,寄存器不等于寄存器的内容。线程在执行的时候,将共享数据加载到CPU寄存器的本质是把数据的内存,变成了自己的上下文(这个变量的数据,以拷贝的形式,给自己单独拿了一份),此时线程2一直对tickets做--操作,在此过程中并没有切换线程。tickets已经被减到10了,再要切换线程的时候,线程2读取了当前tickets的数10并保存到自己的上下文中,切换到线程1,切换回来的时候并不会进行--操作,先恢复上下文,然后做--操作,此时线程1进行完--操作后,tickets由1000变成了999,然后写回到内存当中。之前线程2将tickets--到了10,经过线程1之后,票数回到了999,这就造成了数据不一致的问题。

假设票数为1,我们的判断是票数大于0就可以买票,当线程1判断之后,进入买票情况,此时切换到其他线程,tickets的数量没有改变,完成判断后,也进入了买票情况。票数就出现了不合理的情况。

如何解决??

解决这个问题,要做到3点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。


互斥量的接口

前提知识

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。

初始化互斥量和释放

可以将锁定义为全局变量,然后用pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 这个方式来初始化,就可以不用初始化锁和释放锁了。

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

不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
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,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

只有申请锁成功,才能往后执行,不成功,则阻塞。

解决数据不一致问题

加锁的本质其实就是用时间换安全。

加锁的表现:线程对于临界区代码串行执行。

加锁原则:尽量要保证临界区代码越少越好。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;#define N 4int tickets = 1000; // 用多线程,模拟抢票class threadData
{
public:threadData(int number, pthread_mutex_t *mutex){threadname = "thread-" + to_string(number);lock = mutex;}
public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData* td = static_cast<threadData*>(args);const char* name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock);if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;}else {pthread_mutex_unlock(td->lock);break;}pthread_mutex_unlock(td->lock);}printf("%s ... quit\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr);vector<pthread_t> tids;vector<threadData*> thread_datas;for (int i = 1; i <= N; i++){pthread_t tid;threadData* td = new threadData(i, &lock);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

通过加锁就解决了数据不一致问题,但是只有thread-1这个线程在执行,没有切换到其他线程。


#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;#define N 4int tickets = 1000; // 用多线程,模拟抢票class threadData
{
public:threadData(int number, pthread_mutex_t *mutex){threadname = "thread-" + to_string(number);lock = mutex;}
public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData* td = static_cast<threadData*>(args);const char* name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock);if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);}else {pthread_mutex_unlock(td->lock);break;}usleep(13);}printf("%s ... quit\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr);vector<pthread_t> tids;vector<threadData*> thread_datas;for (int i = 1; i <= N; i++){pthread_t tid;threadData* td = new threadData(i, &lock);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

添上一个usleep即可。


第一个代码只有一个线程执行(该线程的竞争能力比较强),这就导致锁分配不够合理,容易导致其他线程的饥饿问题。我们可以考虑让所有的线程获取锁,按照一定的顺序。按照一定顺序性获取资源就叫做同步。

当程序执行的时候,所有线程就要竞争锁这个资源,所以,锁本身也是共享资源。既然是共享资源,谁来保证锁的安全? 因此,申请锁和释放锁本身就被设计成了原子性操作。

在临界区中,线程可以被切换吗?可以切换。在线程被切出去的时候,是持有锁被切走的。我不在期间,照样没有线程能进入临界区访问临界资源。通过加锁保证我在访问临界区打的时候,对其他线程是原子的。


也可以先封装一个锁

#pragma once#include <pthread.h>class Mutex{
public:Mutex(pthread_mutex_t *lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t *_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex _mutex;
};

利用类来完成锁的操作。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "mutex.hpp"using namespace std;#define N 4int tickets = 1000; // 用多线程,模拟抢票class threadData
{
public:threadData(int number, pthread_mutex_t *mutex){threadname = "thread-" + to_string(number);lock = mutex;}
public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData* td = static_cast<threadData*>(args);const char* name = td->threadname.c_str();while (true){{LockGuard lg(td->lock); // 先调用构造函数,出了作用域在调用析构函数if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);}else {pthread_mutex_unlock(td->lock);break;}}usleep(13);}printf("%s ... quit\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr);vector<pthread_t> tids;vector<threadData*> thread_datas;for (int i = 1; i <= N; i++){pthread_t tid;threadData* td = new threadData(i, &lock);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

锁的原理

tickets-- 不是原子的,会变成三条汇编语句。原子:一条汇编语句就是原子的。

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。


常见锁概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态 。


连续申请锁就会造成死锁。

死锁的四个必要条件

互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配

线程同步

在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
在上面的代码中,出现了一种情况,就是某一个线程的竞争能力很强,导致其他线程处于空闲状态导致的饥饿问题。这样的问题可以用条件变量来解决。条件变量必须依赖锁的使用

条件变量的接口

初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL销毁
int pthread_cond_destroy(pthread_cond_t *cond)等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

#include <iostream>
#include <pthread.h>
#include <unistd.h>
int cnt = 0;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 使用这个参数就不需要初始化和销毁了。void *Count(void *args)
{uint64_t number = (uint64_t)args;pthread_detach(pthread_self());while (true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex); // 等待条件满足为什么写在这里?// 因为当进行等待的时候,会自动释放锁。所以这个等待条件中会存在大量线程。// 在main函数中的唤醒线程中,会唤醒线程(一般是第一个),如果用的是broadcast则唤醒一批线程。std::cout << "pthread: " << number << " , cnt: " << cnt++ << std::endl;pthread_mutex_unlock(&mutex);sleep(3);}return nullptr;
}int main()
{for (uint64_t i = 0; i < 5; i++){pthread_t tid;pthread_create(&tid, nullptr, Count, (void*)i);}sleep(3);std::cout << "main thread ctrl begin: " << std::endl;while (true){sleep(1);pthread_cond_signal(&cond);//唤醒一个线程std::cout << "signal one thread ... " << std::endl;}return 0;
}


换成 pthread_cond_broadcast。

唤醒一批线程。


要知道,让一个线程去休眠,也就是临界资源不就绪,没错,临界资源也是有状态的。怎么知道临界资源是就绪还是不就绪?判断出来的,判断就是访问临界资源,也就是判断必须在加锁之后。等待在加锁和解锁之间。

CP问题

生产者消费者(consumer producter)模型

为什么要有仓库,消费者不可以直接找到生产者吗?那么生产者不就变成了生产者+仓库吗?仓库的存在是能提高效率的。快该过年了,生产者可以把仓库给塞满,让消费者进行消费,当仓库中的货快消费完的时候,生产者可以继续生产,不会出现仓库没货,消费者等着消费,生产者生产一个消费掉一个这种情况。

这个仓库就是一个共享资源,会存在一些问题

  1. 生产者vs生产者:互斥
  2. 消费者vs消费者:互斥
  3. 生产者vs消费者:互斥,同步

总结一下就是

  1. 三种关系
  2. 两种角色 --- 生产和消费
  3. 一个交易场所---特定结构的内存空间

优点

  1. 解耦
  2. 支持并发
  3. 支持忙闲不均

实现CP

BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

C++queue模拟阻塞队列的生产消费模型

利用C++STL中的queue来完成这个模型。

#pragma once#include <iostream>
#include <queue>
#include <pthread.h>template <typename T>
class BlockQueue
{static const int defalutnum = 5;
public:BlockQueue(int maxcap = defalutnum):_maxcap(maxcap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&c_cond, nullptr);pthread_cond_init(&p_cond, nullptr);}T pop(){pthread_mutex_lock(&_mutex);if (_q.size() == 0){pthread_cond_wait(&c_cond, &_mutex); // 等待之后谁来唤醒呢?,生产之后就有了数据,有了数据就可以消费了。}T out = _q.front();_q.pop();pthread_cond_signal(&p_cond);pthread_mutex_unlock(&_mutex);return out;}void push(const T& in){/*并不是你想生产就生产,当队列中的数量超过maxcap的时候,就需要进行等待*/pthread_mutex_lock(&_mutex);if (_q.size() == _maxcap){pthread_cond_wait(&p_cond, &_mutex); // 通过判断确定是否要进行等待}_q.push(in);pthread_cond_signal(&c_cond);pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&c_cond);pthread_cond_destroy(&p_cond);}private:std::queue<T> _q; // 共享资源// int _mincap;int _maxcap; // 极值pthread_mutex_t _mutex; // C++中的STL是线程不安全的,所以需要一把锁来保护它pthread_cond_t c_cond;pthread_cond_t p_cond;
};
#include "BlockQueue.hpp"
#include <unistd.h>void *Consumer(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);while (true){int data = bq->pop();std::cout << "消费了一个数据 " << data << std::endl;}
}void *Productor(void *args)
{BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);int data = 0;while (true){data++;bq->push(data);std::cout << "生产了一个数据 " << data << std::endl;}
}int main()
{BlockQueue<int> *bq = new BlockQueue<int>();pthread_t c, p;pthread_create(&p, nullptr, Productor, bq);pthread_create(&c, nullptr, Consumer, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);delete bq;return 0;
}

这是单线程版本的。生产五个数据,消费五个数据,也可以定义一个low和up变量,当作下界和上界,当队列中的数据 == low的时候,生产数据,当队列中的数据位于 low 和 up之间,就让消费者来消费。

多线程伪唤醒

多线程伪唤醒问题通常是指在多线程编程中,由于竞争条件或者错误使用条件变量等原因,导致线程在没有实际被唤醒的情况下似乎被唤醒了。这可能会引起程序逻辑错误或性能问题。

在这个pop函数当中,多个线程在wait中进行等待,生产者生产了一个数据在释放锁后,pop中在等待的线程获得了一个锁,然后通知生产者生产数据,并释放锁,此时,pop中wait中的线程会与生产者中的线程竞争这个锁资源,如果pop中的线程竞争到了这个资源,会继续往下执行代码,删除队列中的数据,此时上一个线程已经把队列中的数据给删除了,再次进行删除,就会删除错误的数据,如果队列中没有数据的话,再次进行pop操作,就会出现错误。 上面的代码中,是单生产单消费,几乎不会有这种问题,如果是多线程,可能就会出现这种问题了。


int main()
{BlockQueue<int> *bq = new BlockQueue<int>();pthread_t c[3], p[5];for (int i = 0; i < 3; i++){pthread_create(p + i, nullptr, Productor, bq);}for (int i = 0; i < 5; i++){pthread_create(c + i, nullptr, Consumer, bq);}for (int i = 0; i < 3; i++){pthread_create(p + i, nullptr, Productor, bq);}for (int i = 0; i < 5; i++){pthread_join(p[i], nullptr);}for (int i = 0; i < 3; i++){pthread_join(c[i], nullptr);}delete bq;return 0;
}

这样就变成了多生产,多消费了。队列是所有生产者消费者的共享资源。

注意要将pop和push中的if改成while,避免出现伪唤醒的情况。

POSIX 信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。


初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()


 

基于环形队列的生产消费模型

环形队列采用数组模拟,用模运算来模拟环状特性。

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态 。

基于环形队列的生产消费模型有三个原则

  1. 指向同一个位置,只能一个人访问
    该位置为空:生产
    该位置满:消费
  2. 消费者不能超过生产者
  3. 生产者不能把消费者套圈

#pragma once
#include  <iostream>
#include <vector>
#include <semaphore.h>const static int defaultcap = 5;template <typename T>
class RingQueue
{// 对PV操作进行一个封装
private:void P(sem_t &sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}public:RingQueue(int cap = defaultcap):_ringqueue(cap),_cap(cap),_c_step(0),_p_step(0){sem_init(&_cdata_sem, 0, 0);sem_init(&_pspace_sem, 0, cap);}void push(const T &in) // 生产{P(_pspace_sem);_ringqueue[_p_step++] = in;V(_cdata_sem);_p_step %= _cap; //维持环形特征}void pop(T *out) // 消费{P(_cdata_sem);*out = _ringqueue[_c_step++];V(_pspace_sem);_c_step %= _cap;}~RingQueue(){sem_destroy(&_cdata_sem);sem_destroy(&_pspace_sem);}private:std::vector<T> _ringqueue;int _cap;int _c_step; // 消费者下标int _p_step; // 生产者下标sem_t _cdata_sem; // 消费者关注的数据资源sem_t _pspace_sem; // 生产者关注的空间资源
};
#include <iostream>
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>using namespace std;void *Productor(void *args)
{RingQueue<int> *rq = static_cast<RingQueue<int>*>(args);while (true){int data = rand() % 10 + 1;rq->push(data);cout << "Productor data : " << data << endl;}return nullptr;
}void *Consumer(void* args)
{RingQueue<int> *rq = static_cast<RingQueue<int>*>(args);while (true){int data = 0;rq->pop(&data);cout << "Consumer data : " << data << endl;sleep(1);}return 0;
}int main()
{srand(time(nullptr));RingQueue<int> *rq = new RingQueue<int>();pthread_t c, p;pthread_create(&c, nullptr, Productor, rq);pthread_create(&p, nullptr, Consumer ,rq);pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}

这是一个单生产,单消费的例子。


现在改成多生产,多消费的例子。

#pragma once
#include  <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>const static int defaultcap = 5;template <typename T>
class RingQueue
{// 对PV操作进行一个封装
private:void P(sem_t &sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}void Lock(pthread_mutex_t &mutex){pthread_mutex_lock(&mutex);}void Unlock(pthread_mutex_t &mutex){pthread_mutex_unlock(&mutex);}public:RingQueue(int cap = defaultcap):_ringqueue(cap),_cap(cap),_c_step(0),_p_step(0){sem_init(&_cdata_sem, 0, 0);sem_init(&_pspace_sem, 0, cap);pthread_mutex_init(&_c_mutex, nullptr);pthread_mutex_init(&_p_mutex, nullptr);}void push(const T &in) // 生产{P(_pspace_sem);Lock(_p_mutex);_ringqueue[_p_step++] = in;_p_step %= _cap; //维持环形特征Unlock(_p_mutex);V(_cdata_sem);}void pop(T *out) // 消费{P(_cdata_sem);Lock(_c_mutex);*out = _ringqueue[_c_step++];_c_step %= _cap;Unlock(_c_mutex);V(_pspace_sem);}~RingQueue(){sem_destroy(&_cdata_sem);sem_destroy(&_pspace_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}private:std::vector<T> _ringqueue;int _cap;int _c_step; // 消费者下标int _p_step; // 生产者下标sem_t _cdata_sem; // 消费者关注的数据资源sem_t _pspace_sem; // 生产者关注的空间资源pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;};
#include <iostream>
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>using namespace std;void *Productor(void *args)
{RingQueue<int> *rq = static_cast<RingQueue<int>*>(args);while (true){int data = rand() % 10 + 1;rq->push(data);cout << "Productor data : " << data << endl;}return nullptr;
}void *Consumer(void* args)
{RingQueue<int> *rq = static_cast<RingQueue<int>*>(args);while (true){int data = 0;rq->pop(&data);cout << "Consumer data : " << data << endl;sleep(1);}return 0;
}int main()
{srand(time(nullptr));RingQueue<int> *rq = new RingQueue<int>();pthread_t c[3], p[5];for (int i = 0; i < 3; i++){pthread_create(c + i, nullptr, Productor, rq);}for (int i = 0; i < 5; i++){pthread_create(p + i, nullptr, Consumer, rq);}for (int i = 0; i < 5; i++){pthread_join(p[i], nullptr);}for (int i = 0; i < 3; i++){pthread_join(c[i], nullptr);}return 0;
}

线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:

    1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
    1. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
    1. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
  • 线程池的种类:
  • 线程池示例:
    1. 创建固定数量线程池,循环从任务队列中获取任务对象,
    1. 获取到任务对象后,执行任务对象中的任务接口
#pragma once#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defaultnum = 5;template <typename T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}bool IsQueueEmpty(){return _task.empty();}public:ThreadPool(int num = defaultnum):_threads(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}static void *Handler(void* args){ThreadPool<T> *tp = static_cast<ThreadPool<T>*>(args);while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();}}T Pop(){T t = _task.front();_task.pop();return t;}void Start(){int num = _threads.size();for (int i = 0; i < num; i++){_threads[i].name = "thread-" + std::to_string(i + 1);pthread_create(&_threads[i].tid, nullptr, Handler, this);}}void Push(const T& t){Lock();_task.push(t);Wakeup();Unlock();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:std::vector<ThreadInfo> _threads;std::queue<T> _task;pthread_mutex_t _mutex;pthread_cond_t _cond;
};
#pragma once
#include <iostream>
#include <string>std::string opers="+-*/%";enum{DivZero=1,ModZero,Unknown
};class Task
{
public:Task(){}Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0){}void run(){switch (oper_){case '+':result_ = data1_ + data2_;break;case '-':result_ = data1_ - data2_;break;case '*':result_ = data1_ * data2_;break;case '/':{if(data2_ == 0) exitcode_ = DivZero;else result_ = data1_ / data2_;}break;case '%':{if(data2_ == 0) exitcode_ = ModZero;else result_ = data1_ % data2_;}            break;default:exitcode_ = Unknown;break;}}void operator ()(){run();}std::string GetResult(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=";r += std::to_string(result_);r += "[code: ";r += std::to_string(exitcode_);r += "]";return r;}std::string GetTask(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=?";return r;}~Task(){}private:int data1_;int data2_;char oper_;int result_;int exitcode_;
};
#include <iostream>
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <ctime>int main()
{srand(time(nullptr));ThreadPool<Task> *tp = new ThreadPool<Task>(5);tp->Start();while (true){int x = rand() % 100 + 1;int y = rand() % 100 + 1;char op = opers[rand() % opers.size()];Task t(x, y, op);tp->Push(t);std::cout << "main thread make task: " << t.GetTask() << std::endl;sleep(1);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/673493.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构第十二天(队列)

目录 前言 概述 源码&#xff1a; 主函数&#xff1a; 运行结果&#xff1a; 前言 今天和大家共享一句箴言&#xff1a;我本可以忍受黑暗&#xff0c;如果我不曾见过太阳。 概述 队列&#xff08;Queue&#xff09;是一种常见的数据结构&#xff0c;遵循先进先出&#…

25、数据结构/二叉树相关练习20240207

一、二叉树相关练习 请编程实现二叉树的操作 1.二叉树的创建 2.二叉树的先序遍历 3.二叉树的中序遍历 4.二叉树的后序遍历 5.二叉树各个节点度的个数 6.二叉树的深度 代码&#xff1a; #include<stdlib.h> #include<string.h> #include<stdio.h> ty…

UDP是什么,UDP协议及优缺点

UDP&#xff0c;全称 User Datagram Protocol&#xff0c;中文名称为用户数据报协议&#xff0c;主要用来支持那些需要在计算机之间传输数据的网络连接。 UDP 协议从问世至今已经被使用了很多年&#xff0c;虽然目前 UDP 协议的应用不如 TCP 协议广泛&#xff0c;但 UDP 依然是…

提速MySQL:数据库性能加速策略全解析

提速MySQL&#xff1a;数据库性能加速策略全解析 引言理解MySQL性能指标监控和评估性能指标索引优化技巧索引优化实战案例 查询优化实战查询优化案例分析 存储引擎优化InnoDB vs MyISAM选择和优化存储引擎存储引擎优化实例 配置调整与系统优化配置调整系统优化优化实例 实战案例…

Linux学习笔记(centOS)—— 文件系统

目录 一、Linux中的文件 打开方式 二、目录结构​ 三、相关命令 切换目录命令 列出当前目录下的文件和目录命令 一、Linux中的文件 “万物皆文件。” 图1.1 所有文件 打开方式 图形化界面左上角的位置→计算机&#xff0c;打开以后就可以看到Linux全部的文件了&#xf…

C语言:操作符详解

创作不易&#xff0c;给个三连吧&#xff01;&#xff01; 一、算术操作符 C语言中为了方便计算&#xff0c;提供了算数操作符&#xff0c;分别是:,-,*,/,% 由于这些操作符都是有两个操作数&#xff08;位于操作符两边&#xff09;&#xff0c;所以这种操作符也叫做双目操作…

【操作系统】MacOS虚拟内存统计指标

目录 命令及其结果 参数解读 有趣的实验 在 macOS 系统中&#xff0c;虚拟内存统计指标提供了对系统内存使用情况和虚拟内存操作的重要洞察。通过分析这些指标&#xff0c;我们可以更好地了解系统的性能状况和内存管理情况。 命令及其结果 >>> vm_stat Mach Virtu…

查看NodeJs版本和查看NPM版本

Windows10 Dos命令下 查看NodeJs版本和查看NPM版本 NodeJs的命令是&#xff1a;node -v Npm的命令是&#xff1a;npm -v 下图&#xff1a; 记录下&#xff01;~

Springboot 整合 Elasticsearch(三):使用RestHighLevelClient操作ES ①

&#x1f4c1; 前情提要&#xff1a; Springboot 整合 Elasticsearch&#xff08;一&#xff09;&#xff1a;Linux下安装 Elasticsearch 8.x Springboot 整合 Elasticsearch&#xff08;二&#xff09;&#xff1a;使用HTTP请求来操作ES 目录 一、Springboot 整合 Elasticsea…

DC-7靶机渗透详细流程

信息收集&#xff1a; 1.存活扫描&#xff1a; 由于靶机和kali都是nat的网卡&#xff0c;都在一个网段&#xff0c;我们用arp-scan会快一点&#xff1a; arp-scan arp-scan -I eth0 -l └─# arp-scan -I eth0 -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:dd:ee:6…

【蓝桥杯冲冲冲】[NOIP2017 提高组] 宝藏

蓝桥杯备赛 | 洛谷做题打卡day29 文章目录 蓝桥杯备赛 | 洛谷做题打卡day29[NOIP2017 提高组] 宝藏题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1样例 #2样例输入 #2样例输出 #2提示题解代码我的一些话[NOIP2017 提高组] 宝藏 题目背景 NOIP2017 D2T2 题目描…

机器人学、机器视觉与控制 上机笔记(2.1章节)

机器人学、机器视觉与控制 上机笔记&#xff08;2.1章节&#xff09; 1、前言2、本篇内容3、代码记录3.1、新建se23.2、生成坐标系3.3、将T1表示的变换绘制3.4、完整绘制代码3.5、获取点*在坐标系1下的表示3.6、相对坐标获取完整代码 4、结语 1、前言 工作需要&#xff0c;想同…

51单片机之数码管显示表白数字篇

朝菌不知晦朔 蟪蛄不知春秋 眼界决定境界 CSDN 请求进入专栏 是否进入《51单片机专栏》? 确定 目录 数码管的简介 数码管引脚定义 数码管的原理图 74HC245 代码实现 静态数码管的显示 动态数码管的显示 数码管实现表白画面 数码管的简介 L…

Pytorch 安装与配置

每次在服务器上配置环境都需要考虑 Pytorch 版本 / CUDA 版本 / GPU 驱动版本等诸多因素&#xff0c;因为 ⌈ \lceil ⌈Pytorch 只能下载指定 CUDA 版本的库 ⌋ \rfloor ⌋、 ⌈ \lceil ⌈GPU 只能使用特定版本的驱动 ⌋ \rfloor ⌋、 ⌈ \lceil ⌈GPU 有最高支持的 CUDA 版本…

国际物流数字化运输方式选择指南 | 箱讯科技

国际物流涉及多种运输方式&#xff0c;每种方式都有其独特的优势和适用场景。选择合适的运输方式对于确保货物安全、及时到达目的地并控制成本至关重要。以下是对六种主要国际运输方式的简要介绍和选择建议&#xff1a; 国际快递&#xff1a;适用于小件、高价值或急需的货物。…

基于Skywalking开发分布式监控(二)

续上篇&#xff0c;上一篇主要是讲了为啥选skywalking&#xff0c;以及怎么有针对性改造SW Agent&#xff0c;现在我们继续看看如何构建自定义Trace跟踪链 要对SW Agent插件做适当剪裁&#xff0c;原来包括customize插件在内SW 8.9有100多个插件&#xff0c;如果没有作用也就罢…

jvm几个常见面试题整理

1. Full GC触发机制有如下5种情况。 (1)调用System.gc()时&#xff0c;系统建议执行Full GC&#xff0c;但是不必然执行。(2)老年代空间不足。(3)方法区空间不足。(4)老年代的最大可用连续空间小于历次晋升到老年代对象的平均大小就会进行Full GC。(5)由Eden区、S0(From)区向S…

基于蒙特卡洛的电力系统可靠性分析matlab仿真,对比EDNS和LOLP

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 1.课题概述 电力系统可靠性是指电力系统按可接受的质量标准和所需数量不间断地向电力用户供应电力和电能量的能力的量度&#xff0c;包括充裕度和安全性两个方面。发电系统可靠性是指统一并网的全部发电机…

OOD分类项目训练

一、项目地址 GitHub - LooKing9218/UIOS 二、label制作 将训练、验证、测试数据的分类信息转换入.csv文件中&#xff0c;运行如下脚本即可&#xff1a; import os import csv#要读取的训练、验证、测试文件的目录&#xff0c;该文件下保存着以各个类别命名的文件夹和对应的分…

【网站项目】035家居商城系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…