电商平台搭建构思/宜昌seo
进程在前面已经讲过了,所以这次我们来讨论一下多线程。
前言:线程的背景
进程是Linux中资源及事物管理的基本单位,是系统进行资源分配和调度的一个独立单位。但是实现进程间通信需要借助操作系统中专门的通信机制,但是只这些机制将占用大量的空间资源,特别是少量数据传递时显得庞大而欠灵活。因此才出现了线程。
线程和进程一样,具有创建,退出,取消,等待等基本操作,可以独立完成特定事物的处理。并且线程占用的资源更少。
1.线程的基本概念?
1.1什么是线程:
在一个程序里的一个执行路线叫做线程。一切进程至少有一个执行路线。
1.2线程与进程的对比
1.21用户空间资源对比
一个进程的创建包含着进程控制块(task_struct),进程地址空间(mm_struct),以及页表的创建。

线程是当前进程内的一个执行流,并且在进程地址空间里运行,这个进程申请的所有资源都被线程共享。
但是如果用fork函数创建一个子进程,而线程用ptread_creat()函数创建一个新线程,一起对比一下所占资源情况!

总结:
每个进程在创建是额外申请了新的内存和空间以及存储代码,数据段,堆,栈空间。并且初始化为父进程的值,父子进程在创建后不能互访对方资源。
每个创建的新线程在用户阶段仅申请自己的栈空间,并且与同进程的其他线程共享其他地址空间,这使得同进程下的个线程共享数据很方便。但是带来的问题也是同步的,接下来我们会讲到。
1.22内核空间资源对比
1.前面的进程中我们说了,每一个进程在内核中都有自己的进程控制块PCB来识别当前进程所能够访问的系统资源。通过该进程的PCB可以访问到进程的所有资源。
目前在Linux下的进程统称为轻量级进程,甚至很多的书中都说LINUX并不区分进程与线程,如果我们站在内核的角度想:在创建线程时Linux内核仍然创建一个新的PCB来表示这个线程,而内核对他俩的认识都来自PCB,所以内核并不认为他俩有区别。
2.在Linux下每一个进程的PCB的mm_struct都用来描述地址空间。而父子进程间的地址空间是分开的;而同一个进程下的线程共享这个地址空间,所以才在用户的角度说两者是有区别的。但是从调度的角度来看,操作系统是基于线程调度的,及内核并不认为他俩有区别。
3.一个进程如果不创建新的线程,那他就是只有一个线程的进程;如果创建了额外的线程,原来的进程也称为主线程。
总结一下:
4.进程在使用时占用了大量的内存,特别是进程间通信,这使得进程不够灵活,而且耗费资源,而线程占用资源少,使用灵活,且同进程下的线程间数据交互不需要经过OS,所以很多应用程序都使用了大量的进程,但是线程不能脱离进程而存在。
线程引进能做啥事?
如果说进程中有10个函数,但是只有一个线程,那这10个函数一定是按照顺序一次进行,但是如果有多个线程被创建,那这几个线程就可以分别实现这几个函数。
单线程执行流被调度:

多个执行流被调度:

linxu下没有真正的多线程
1.操作系统中存在大量的进程,一个进程内又存在一个或多个线程,因此线程的数量一定比进程的数量多,当线程的数量足够多的时候,很明显线程的执行粒度要比进程更细。
2.如果一款操作系统要支持真的线程,那么就需要对这些线程进行管理。比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,这个和进程的绝大多数功能重复,所以委员会决定在内核层面没必要去区分他俩,都是用test_struct去表示。
3.因此,如果要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来,描述线程的控制块和描述进程的控制块是类似的,因此Linux并没有重新为线程设计数据结构,而是直接复用了进程控制块,所以我们说Linux中的所有执行流都叫做轻量级进程。
既然linxu中没有真正意义上的线程,所以就没有真正意义上的线程调用了。但是Linux可以提供创建轻量级进程的接口,也就是创建进程,共享空间,其中最典型的代表就是vfork函数。
提到vfork函数就不得不提到他的老大哥fork函数,这个vfork()函数其实就是fork的阉割版,fork创建的子进程是完全1:1模仿的父进程,但是操作者创建的进程如果是只想实现一些小功能函数,就不需要完全复制父进程。
fork():父进程的一个副本(代码+数据)
vfork():共享父进程的代码与数据
vfork()原型:
pid_t vfork(void);
vfork函数的返回值与fork函数的返回值相同:
给父进程返回子进程的PID。
给子进程返回0。
例如在下面的代码中,父进程使用vfork函数创建子进程,子进程将全局变量g_val由100改为了200,父进程休眠3秒后再读取到全局变量g_val的值。


可以看到,父进程读取到g_val的值是子进程修改后的值,也就证明了vfork创建的子进程与其父进程是共享地址空间的。
1.3从新理解啥是进程
在用户视角:
内核数据结构+该进程对应的代码和数据

内核视角:
承担系统资源分配的基本实体
刚开始创建一个进程时,该进程就会向操作系统要资源(内核区+栈+堆...),他是以进程的身份向系统要资源。当资源要完了,又创建新的线程时,线程就不在伸手向系统要了,而是向你这个进程要了。换句话说,创建新进程时,向操作系统要资源的不是线程,而是以进程为单位要的。既然是进程要的,那系统分配资源时不就是以进程为基本单位进行分配的吗?
怎样看我们写的代码呢?
原来我们写的代码都是一个task_struct,但是现在有多个task_struct,如果说以前的叫做单进程多线程的话,那现在的就叫做多进程多线程了,也就是说原来的是现在多个task_struct的一个子集。
也就是说一个进程就是一个地址空间,而一个线程就是一个task_struct。
CPU视角:
其实CPU不关心到底系统是咋区分进程和线程的,他只看task_struct,只要运行队列中有task_struct,就直接执行其中的代码和数据。CPU不关心到底代码和数据是和谁共享的,只要能执行就行。
1.4进程和线程之间的关系

线程的创建
2.1线程创建函数
一般我们都是用pthread_create()来创建一个线程的
man pthread_create //查看其函数声明

第一个参数:线程ID,没错进程有进程ID,线程有线程ID
第二个参数:设置线程属性,没毛病知道我们学完线程这个默认也没关系,不用改。
第三个参数:设置线程运行的代码起始地址是个函数指针。因为线程是进程一部分,所以用函数指针来接受进程的地址
第四个参数:运行函数的参数地址。
2.2创建线程
先创建一个Makefile文件:
mythread:mythread.ccg++ -o $@ $^ -lpthread
.PHONY:clean
clean:rm -f mythread
那个lpthread是引入线程库,否则要是新创建一个线程的话就会报错
然后在创建一个thread.cc来存放代码:
#include <iostream>
#include <unistd.h> //这个getpid函数头文件
#include <pthread.h>
#include <cstdio>
#include <string.h>using namespace std; // 这是C++中的命名空间void *threadRun(void *args)
{const string name = (char *)args;while (true){cout << name << ", pid:" << getpid() << "\n"<< endl;sleep(1);}
}int main()
{pthread_t tid[5]; // 一次创建5个线程char name[64]; // 这是线程名,但是可能有点小问题for (int i = 0; i < 5; i++){snprintf(name, sizeof name, "%s-%d", "thread", i); // 给线程重新命名pthread_create(tid + i, NULL, threadRun, (void *)name);sleep(1);}while (true){cout << "main thread, pid:" << getpid() << endl;sleep(3); // 避免和上面的混淆}
}
ldd mythread
确认一下有没有用到这里的库

运行一下mythread--->./mythread

接下来我们创建一个分屏:然后用ps axj查看命令来查看:
ps axj | head -1 && ps axj |grep mythread
但是这里却只有一个进程

不是说系统可以创建轻量级进程吗?在哪呢?
输入以下命令:
ps -aL
ps -aL | head -1 && ps -aL |grep mythread

看见PID后面那个LWP没那个就是轻量级进程对应的PID,19,20,26...
而第一个线程的PID和LWP是一样的,所以他叫做主线程。
所以操作系统调度线程时看的是LWP,因为PID和线程是一对多的关系。
我们输入一下kill -9 PID

当我们结束一个进程,那他其中的所有线程就都结束了。
代码区的共享
我再新写一个函数
void show(const string &name)
{cout << name << ", pid:" << getpid() << "\n" << endl;
}void *threadRun(void *args)
{const string name = (char *)args;while (true){show(name); sleep(1);}
}int main()
{pthread_t tid[5]; // 一次创建5个线程char name[64]; // 这是线程名,但是可能有点小问题for (int i = 0; i < 5; i++){snprintf(name, sizeof name, "%s-%d", "thread", i); // 给线程重新命名pthread_create(tid + i, NULL, threadRun, (void *)name);sleep(1);}while (true){cout << "main thread, pid:" << getpid() << endl;sleep(3); // 避免和上面的混淆}
}

这个函数能被所有的线程进行访问,再重新make一下,再编译也能跑。

当然全局变量也可以被共享


2.3线程的缺点
说了这么多,我们就来聊一聊写线程时遇到的缺点:
1.性能损失
一个很少被外部事件阻塞的计算机密集型线程往往无法与其他线程共享同一个处理器,如果密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的损失指的是增加了额外的同步和调度开销,而可以资源不变。
2.健壮性降低
编写多线程需要更全面,更深入的考虑, 在一个多线程程序中,因时间分配上的细微差别或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程间是缺乏保护的。
3.缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4.编程难度提高
编写与调试一个多线程程序比单线程困难的多。
2.4线程异常
这个后面会做示范,这里就提一下:
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随之崩溃。
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程1,进程终止,该进程中的所有线程也就随即退出。
这里我们再重新创建一个新线程跑一跑试试
makefile文件和上面一样,为了方便继续用mythread.cc
#include <iostream>
#include <unistd.h> //这个getpid函数头文件
#include <pthread.h>
#include <cstdio>
#include <string.h>using namespace std;void *threadRoutine(void *args)
{ while(true){cout << "新线程: " << (char *)args << "running..." << endl;sleep(1);}
}
int main()
{pthread_t tid; // 创建线程pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");while (true){cout << "main线程: "<< " running ..." << endl;sleep(1);}return 0;
}
这两个新线程都可以跑

上面不是说如果一个线程崩溃其他线程也不能跑了,我们试一下吧。
我们把代码改一下:

这里的\=0代表 硬件异常,CPU中的状态标记为被置为0,这里我们来模拟线程出错。
重新make一下,报错不管,然后编译。

出现这个错误,然后在输入kill -l
又出现这个信号


这里就不存在我们创建的mythread线程了。
所以线程谁先运行与调度器有关。
如果一个线程出现异常,都可能导致整个进程退出。
线程在创建和执行的时候也是需要等待的。如果主线程不等待,就会导致
3.线程等待
3.1线程等待函数pthread_join()
man pthread_join

thread:被等待线程的ID。
retval:线程退出时的退出码信息。
void *threadRoutine(void *args)
{int i = 0;while (true){cout << "新线程: " << (char *)args << "running..." << endl;sleep(1);if (i++ == 10){break;}}cout << " new thread quit..." << endl;
}
int main()
{pthread_t tid; // 创建线程pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");pthread_join(tid, NULL); //进程等待函数cout << "main thread wait down ...." << endl;while (true){cout << "main线程: "<< " running ..." << endl;sleep(1);}return 0;
}
如果发生进程等待就是先打印新线程,后打印main线程

一点毛病没有。
总结一下:
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
3.2pthread_join第二个参数
我们写的功能函数的返回值是void*, 这个返回类型可以自己设置。
就比如我们让threadRoutine函数返回10,但是必须要强制类型转换成void*, 说白了就是把10当做一个指针数据,也就是说有一个地址,这个地址是10。

返回值但是返回给谁呢?
当然是返回给主线程了,主线程创建分线程就是办事的,所以必须要知道事办的咋样。但是主线程咋接收?所以此时就用到pthread_join()的第二个参数了。

指针就是一个地址,地址说白了就是一个数据,我们就把他看做自变常量。
void* 10看做一个数据。下面的ret看做空间。取指针的地址,所以第二个参数就是二级指针。
void *threadRoutine(void *args)
{int i = 0;while (true){cout << "新线程: " << (char *)args << "running..." << endl;sleep(1);if (i++ == 3){break;}}cout << " new thread quit..." << endl;return (void*) 10;
}
int main()
{pthread_t tid; // 创建线程pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");void* ret = NULL;pthread_join(tid, &ret); // 进程等待函数,默认阻塞等待新线程退出cout << "main thread wait down .... new thread init:" <<(long long)ret<<"\n"<< endl;return 0;
}

新线程退出以后,主线程就活动新线程的返回值了。
我们在来个新玩法:
void *threadRoutine(void *args)
{int i = 0;int *date = new int[10];while (true){cout << "新线程: " << (char *)args << "running..." << endl;sleep(1);date[i]=i;if (i++ == 10){break;}}cout << " new thread quit..." << endl;// return (void*) 10;return (void*)date;
}
int main()
{pthread_t tid; // 创建线程pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");int *ret = NULL;pthread_join(tid, (void**)&ret); // 进程等待函数,默认阻塞等待新线程退出cout << "main thread wait down .... new thread init:"<<"\n"<< endl;for(int i=0;i<10;i++){cout<<ret[i]<<endl;}return 0;
}

这就显出来了
4.线程终止
进程可以终止,线程可以终止吗?


他自动停止了。
在线程中绝对不能调用exit,他是终止线程的。
4.1pthread_exit()
线程有他自己的结束函数:pthread_exit()


当然return也可以线程退出,但是这里就不过多的介绍了。
4.2pthread_cancel
线程取消函数也可以退出线程
man pthread_cancel
//q键退出

取消那个线程就把它的id传入就行了。
主线程可以取消新线程,取消成功的线程的退出码一般是-1,我们让新线程进入死循环,并且把新线程id传给取消函数。
using namespace std;void *threadRoutine(void *args)
{int i = 0;int *date = new int[10];while (true){cout << "新线程: " << (char *)args << "running..." << endl;sleep(1);date[i]=i;if (i++ == 5){break;}}cout << " new thread quit..." << endl;
}
int main()
{pthread_t tid; // 创建线程pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");int count=0;while(true){cout<<"main()线程:"<<"running..."<<endl;sleep(1);count++;if(count>=5)break;}pthread_cancel(tid);cout<<"pthread_cancel:"<<tid<<endl;int *ret = NULL;pthread_join(tid, (void**)&ret); // 进程等待函数,默认阻塞等待新线程退出cout << "main thread wait down .... new thread init:"<<(long long)ret<<"\n"<< endl;sleep(3); //让主线程多活几秒
return 0;
}

接下来执行命令来监视一波:
while :; do ps -aL | head -1 && ps -aL | grep mythread ; sleep 1 ; done

既然主线程可以取消新线程,那反过来其实也可以,但是有啥实际意义呢,主线程取消,谁来帮忙管理其他新线程呢?而且内容比较凌乱,这里就不过多介绍了。
4.3线程id
刚才我们看到了一大串的数字,他们就被称作线程id,但是有些老铁认为这个线程id和线程的lwp其实是一个东西,但是他俩真的一样吗?


其实他俩没啥关系。id后面那一串数字是它的64位。而且id代表的是一个地址,所以它才是这一串数字。其实当我们调用pthread函数来实现一些对线程的操作时,我们调用的是pthread库而不是调用linux系统。当使用pthread库时,这个库就被加载到内存上面,而线程通过一系列操作最终被页表映射到内存上面。所以id其实就代表线程被映射到内存上的地址。
另外,主线程被系统存在cpu的栈里,而新线程则被存放在共享区新开辟的栈里。当时单执行流就调用cpu里主线程栈来操作。

获取自身线程id的函数:pthread_self
5.线程分离
默认情况下,新创建的线程时joinable的,线程退出后,pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
分离函数pthread_detach
int pthread_detach(pthread_t thread);
这里就不在细讲了,各位有兴趣去搜一下资料吧,饿死了!!!
相关文章:

【Linux】多线程---线程控制
进程在前面已经讲过了,所以这次我们来讨论一下多线程。前言:线程的背景进程是Linux中资源及事物管理的基本单位,是系统进行资源分配和调度的一个独立单位。但是实现进程间通信需要借助操作系统中专门的通信机制,但是只这些机制将占…...

秒杀高并发解决方案
秒杀高并发解决方案 1.秒杀/高并发方案-介绍 秒杀/高并发 其实主要解决两个问题,一个是并发读,一个是并发写并发读的核心优化理念是尽量减少用户到 DB 来"读"数据,或者让他们读更少的数据, 并 发写的处理原则也一样针对秒杀系统需…...

【每日一题】蓝桥杯加练 | Day07
文章目录一、三角回文数1、问题描述2、解题思路3、AC代码一、三角回文数 原题链接:三角回文数 1、问题描述 对于正整数 n, 如果存在正整数 k 使得n123⋯k k(k1)2\frac{k(k1)}{2}2k(k1) , 则 n 称为三角数。例如, 66066 是一个三角数, 因为 66066123⋯363 。 如果一…...

条件语句(分支语句)——“Python”
各位CSDN的uu们你们好呀,最近总是感觉特别特别忙,但是却又不知道到底干了些什么,好像啥也没有做,还忙得莫名其妙,言归正传,今天,小雅兰的内容还是Python呀,介绍一些顺序结构的知识点…...

论文投稿指南——中文核心期刊推荐(国家财政)
【前言】 🚀 想发论文怎么办?手把手教你论文如何投稿!那么,首先要搞懂投稿目标——论文期刊 🎄 在期刊论文的分布中,存在一种普遍现象:即对于某一特定的学科或专业来说,少数期刊所含…...

面向数据安全共享的联邦学习研究综述
开放隐私计算 摘 要:跨部门、跨地域、跨系统间的数据共享是充分发挥分布式数据价值的有效途径,但是现阶段日益严峻的数据安全威胁和严格的法律法规对数据共享造成了诸多挑战。联邦学习可以联合多个用户在不传输本地数据的情况下协同训练机器学习模型&am…...

Redis经典五种数据类型底层实现原理解析
目录总纲redis的k,v键值对新的三大类型五种经典数据类型redisObject结构图示结构讲解数据类型与数据结构关系图示string数据类型三大编码格式SDS详解代码结构为什么要重新设计源码解析三大编码格式hash数据类型ziplist和hashtable编码格式ziplist详解结构剖析ziplist的优势(为什…...

Jackson 返回前端的 Response结果字段大小问题
目录 1、问题产生的背景 2、出现的现象 3、解决方案 4、成果展现 5、总结 6、参考文章 1、问题产生的背景 因为本人最近工作相关的对接外部项目,在我们国内有很多程序员都是使用汉语拼音或者部分字母加上英文复合体定义返回实体VO,这样为了能够符合…...

每天五分钟机器学习:你理解贝叶斯公式吗?
本文重点 贝叶斯算法是机器学习算法中非常经典的算法,也是非常古老的一个算法,但是它至今仍然发挥着重大的作用,本节课程及其以后的专栏将会对贝叶斯算法来做一个简单的介绍。 贝叶斯公式 贝叶斯公式是由联合概率推导而来 其中p(Y|X)称为后验概率,P(Y)称为先验概率…...

C++的入门
C的关键字 C总计63个关键字,C语言32个关键字 命名空间 我们C的就是建立在C语言之上,但是是高于C语言的,将C语言的不足都弥补上了,而命名空间就是为了弥补C语言的不足。 看一下这个例子。在C语言中会报错 #include<stdio.h>…...

数据的存储
类型的意义:使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间视角类型的基本归类整型家族浮点数家族构造类型指针类型空类型整型存储解构:整型在计算机中占用四个字节,整型分为无符号整型和有符号整型在计算机…...

Linux查看UTC时间
先了解一下几个时间概念。 GMT时间:Greenwich Mean Time,格林尼治平时,又称格林尼治平均时间或格林尼治标准时间。是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间。 GMT时间存在较大误差,因此不再被作为标准时间使用。现在…...

SpringBoot修改启动图标(详细步骤)
目录 一、介绍 二、操作步骤 三、介绍Java学习(题外话) 四、关于基础知识 一、介绍 修改图标就是在资源加载目录(resources)下放一个banner.txt文件。这样运行加载的时候就会扫描到这个文件,然后启动的时候就会显…...

【每日一题Day143】面试题 17.05. 字母与数字 | 前缀和+哈希表
面试题 17.05. 字母与数字 给定一个放有字母和数字的数组,找到最长的子数组,且包含的字母和数字的个数相同。 返回该子数组,若存在多个最长子数组,返回左端点下标值最小的子数组。若不存在这样的数组,返回一个空数组。…...

Go 内置运算符 if for switch
算数运算符fmt.Println("103", 103) //103 13 fmt.Println("10-3", 10-3) //10-3 7 fmt.Println("10*3", 10*3) //10*3 30 //除法注意:如果运算的数都是整数,那么除后,去掉小数部分,保留整数部分 f…...

C语言指针数组实际应用(嵌入式)
C语言指针数组详细学习 指针是C语言中非常重要的概念之一,它可以让我们直接访问内存中的数据。指针数组则是由多个指针组成的数组,每个指针都可以指向内存中的某个位置。以下是一些指针数组的实际代码应用: 字符串数组 char* names[] {&q…...

常用的Java注解详解
Java是一种常用的编程语言,而注解是Java语言中非常重要的一部分。在这篇文章中,我们将介绍一些常用的Java注解,以及它们的作用和使用方法。 Override override注解是用于表示一个方法是被覆盖的。在Java中,如果子类要覆盖父类的方…...

华为OD机试题 - 第 K 个最小码值的字母(JavaScript)| 机考必刷
更多题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 更多华为OD题库,搜 梦想橡皮擦 华为OD 👑👑👑 更多华为机考题库,搜 梦想橡皮擦华为OD 👑👑👑 华为OD机试题 最近更新的博客使用说明本篇题解:第 K 个最小码值的字母题目输入输出示例一输入输出说明示例一输…...

vscode环境配置(支持跳转,阅读linux kernel)
目录 1.卸载clangd插件 2.安装C插件 3. 搜索框内输入 “intell”,将 C_Cpp:Intelli Sense Engine 开关设置为 Default。 4.ubuntu安装global工具 5.vscode安装插件 6.验证是否生效 7.建立索引 1.卸载clangd插件 在插件管理中卸载clangd插件 2.安…...

zigbee学习笔记:IO操作
1、IAR新建工程 (1)Projetc→Create New Projetc→OK→选择位置,确定 (2)新建一个c文件,保存在路径中 (3)点击工程,右键→add→加入c文件 (4)…...

华为OD机试题 - 最少数量线段覆盖(JavaScript)| 机考必刷
更多题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 更多华为OD题库,搜 梦想橡皮擦 华为OD 👑👑👑 更多华为机考题库,搜 梦想橡皮擦华为OD 👑👑👑 华为OD机试题 最近更新的博客使用说明本篇题解:最少数量线段覆盖题目输入输出示例一输入输出说明Code解题思路版…...

python趣味编程-2048游戏
在上一期我们用Python实现了一个盒子追逐者的游戏,这一期我们继续使用Python实现一个简单的2048游戏,让我们开始今天的旅程吧~ 在 Python 免费源代码中使用 Tkinter 的简单 2048 游戏 使用 Tkinter 的简单 2048 游戏是一个用Python编程语言编码的桌面游…...

求解完全背包问题
题目描述实现一个算法求解完全背包问题。完全背包问题的介绍如下:已知一个容量为 totalweight 的背包,有不同重量不同价值的物品,问怎样在背包容量限制下达到利益最大化。完全背包问题的每个物品可以无限选用背包问题求解方法的介绍如下&…...

我们为什么使用docker 优点 作用
1. 我们为什么使用Docker? 当我们在工作中,一款产品从开发设计到上线运行,其中需要开发人员和运维工程师,开发人员负责代码编写,开发产品,运维工程师需要测试环境,产品部署。这之间就会有分歧。 就好比我…...

Python每日一练(20230311)
目录 1. 合并两个有序数组 2. 二叉树的右视图 3. 拼接最大数 🌟 每日一练刷题专栏 C/C 每日一练 专栏 Python 每日一练 专栏 1. 合并两个有序数组 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为…...

202109-3 CCF 脉冲神经网络 66分题解 + 解题思路 + 解题过程
解题思路 根据题意,脉冲源的阈值大于随机数时,会向其所有出点发送脉冲 神经元当v>30时,会向其所有出点发送脉冲,unordered_map <int, vector > ne; //存储神经元/脉冲源的所有出点集合vector 所有脉冲会有一定的延迟&am…...

Aurora简介
Amazon Aurora是一种兼容MySQL和PostgreSQL的商用级别关系数据库,它既有商用数据库的性能和可用性(比如Oracle数据库),又具有开源数据库的成本效益(比如MySQL数据库)。 Aurora的速度可以达到MySQL数据库的…...

【python实操】用python写软件弹窗
文章目录前言组件label 与 多行文本复选框组件Radiobutton单选组件Frame框架组件labelframe标签框架列表框Listboxscrollbar滚动条组件scale刻度条组件spinbox组件Toplevel子窗体组件PanedWindow组件Menu下拉菜单弹出菜单总结针对组件前言 python学习之路任重而道远࿰…...

Ubuntu 常用操作
版本22.04 1、开启 root # 输入新密码 sudo passwd rootUbuntu以root账号登录桌面 默认情况是不允许用root帐号直接登录图形界面的。 Ubuntu 默认使用 GNOME,GNOME 使用 GDM 显示管理器。 为了允许以 root 身份登录到 GNOME,你需要对位于 /etc/…...

井字棋--课后程序(Python程序开发案例教程-黑马程序员编著-第7章-课后作业)
实例2:井字棋 井字棋是一种在3 * 3格子上进行的连珠游戏,又称井字游戏。井字棋的游戏有两名玩家,其中一个玩家画圈,另一个玩家画叉,轮流在3 * 3格子上画上自己的符号,最先在横向、纵向、或斜线方向连成一条…...