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.rhkb.cn/news/254268.html

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

相关文章

嵌入式学习之Linux入门篇笔记——7,Linux常用命令第二部分

配套视频学习链接&#xff1a;http://【【北京迅为】嵌入式学习之Linux入门篇】 https://www.bilibili.com/video/BV1M7411m7wT/?p4&share_sourcecopy_web&vd_sourcea0ef2c4953d33a9260910aaea45eaec8 目录 1.mkdir 命令 2.rmdir 3.rm 命令 4.touch 命令 5.clear…

【网络安全】2024年暗网威胁分析及发展预测

暗网因其非法活动而臭名昭著&#xff0c;现已发展成为一个用于各种非法目的的地下网络市场。 它是网络犯罪分子的中心&#xff0c;为被盗数据交易、黑客服务和邪恶活动合作提供了机会。为了帮助企业组织更好地了解暗网发展形势&#xff0c;近日&#xff0c;卡巴斯基的安全研究…

Python商业数据挖掘实战——爬取网页并将其转为Markdown

前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff1a;https://www.captainbed.cn/z ChatGPT体验地址 文章目录 前言前言正则表达式进行转换送书活动 前言 在信息爆炸的时代&#xff0c;互联网…

OpenCV/C++:点线面相关计算(二)

接续&#xff0c;继续更新 OpenCV/C:点线面相关计算_线面相交的点 代码计算-CSDN博客文章浏览阅读1.6k次&#xff0c;点赞2次&#xff0c;收藏12次。OpenCV处理点线面的常用操作_线面相交的点 代码计算https://blog.csdn.net/cd_yourheart/article/details/125626239 目录 1、…

BlueLotus 下载安装使用

说明 蓝莲花平台BlueLotus&#xff0c;是清华大学曾经的蓝莲花战队搭建的平台&#xff0c;该平台用于接收xss返回数据。 正常执行反射型xss和存储型xss&#xff1a; 反射型在执行poc时&#xff0c;会直接在页面弹出执行注入的poc代码&#xff1b;存储型则是在将poc代码注入用…

500mA High Voltage Linear Charger with OVP/OCP

一、General Description YHM2810 is a highly integrated, single-cell Li-ion battery charger with system power path management for space-limited portable applications. The full charger function features Trickle-charge, constant current fast charge and const…

JavaScript综合练习3

JavaScript 综合练习 3 1. 案例演示 2. 代码实现 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewpor…

物联网ARM开发-STM32之RTC浅谈

RTC 一.RTC简单介绍 RTC好比我们用来记录时间的一个钟表&#xff0c;他里面有年月日&#xff0c;还可以记录星期&#xff0c;小时&#xff0c;分钟等。是Real Time Clock的缩写&#xff0c;译为实时时钟&#xff0c;本质上是一个独立的定时器。 1. 1 与通用定时器的区别 可以…

基于 SpringBoot 和 Vue.js 的权限管理系统部署教程

大家后&#xff0c;我是 jonssonyan 在上一篇文章我介绍了我的新项目——基于 SpringBoot 和 Vue.js 的权限管理系统&#xff0c;本文主要介绍该系统的部署 部署教程 这里使用 Docker 进行部署&#xff0c;Docker 基于容器技术&#xff0c;它可以占用更少的资源&#xff0c;…

PyTorch 2.2 中文官方教程(七)

使用 torchtext 库进行文本分类 原文&#xff1a;pytorch.org/tutorials/beginner/text_sentiment_ngrams_tutorial.html 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 注意 点击这里下载完整示例代码 在本教程中&#xff0c;我们将展示如何使用 torchtext 库构建文…

【机器学习与自然语言处理】预训练 Pre-Training 各种经典方法的概念汇总

【机器学习与自然语言处理】预训练 Pre-Training 各种经典方法的概念汇总 前言请看此正文预训练 Pre-Training无监督学习 unsupervised learning概念&#xff1a;标签PCA 主成分分析&#xff08;Principal Component Analysis&#xff09;降维算法LSA 潜在语义分析&#xff08;…

《动手学深度学习(PyTorch版)》笔记7.2

注&#xff1a;书中对代码的讲解并不详细&#xff0c;本文对很多细节做了详细注释。另外&#xff0c;书上的源代码是在Jupyter Notebook上运行的&#xff0c;较为分散&#xff0c;本文将代码集中起来&#xff0c;并加以完善&#xff0c;全部用vscode在python 3.9.18下测试通过&…

sql求解连续两个以上的空座位

Q&#xff1a;查找电影院所有连续可用的座位。 返回按 seat_id 升序排序 的结果表。 测试用例的生成使得两个以上的座位连续可用。 结果表格式如下所示。 A:我们首先找出所有的空座位&#xff1a;1&#xff0c;3&#xff0c;4&#xff0c;5 按照seat_id排序&#xff08;上面已…

zer0pts-2020-memo:由文件偏移处理不正确--引发的堆溢出

启动脚本 #!/bin/sh qemu-system-x86_64 \-m 256M \-kernel ./bzImage \-initrd ./rootfs.cpio \-append "root/dev/ram rw consolettyS0 oopspanic panic1 kaslr quiet" \-cpu kvm64,smep,smap \-monitor /dev/null \-nographic -enable-kvm/ # dmesg | grep page …

jsp课程管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 课程管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

Django(十)

1. Ajax请求 浏览器向网站发送请求时&#xff1a;URL 和 表单的形式提交。 GETPOST 特点&#xff1a;页面刷新。 除此之外&#xff0c;也可以基于Ajax向后台发送请求&#xff08;偷偷的发送请求&#xff09;。 依赖jQuery编写ajax代码 $.ajax({url:"发送的地址"…

[MFC] MFC消息机制的补充

之前写了[MFC] 消息映射机制的使用和原理浅析&#xff0c;还有些需要补充的&#xff0c;都记在这里。 MFC 消息的分类 MFC消息分为系统消息和自定义消息。 图片来源&#xff1a;C语言/C教程 大型源码案例分析&#xff1a;MFC消息系统的代码解析 易道云编程 系统消息分为窗口…

个人IP塑造与短视频带货,人人都是吸金的网红博主

一、教程描述 网红带货&#xff0c;就是网络红人通过推荐和分享&#xff0c;间接销售产品的一种方式。网红带货并不是直接带货&#xff0c;而是需要打造自己&#xff0c;用时下热门的话讲叫塑造IP&#xff0c;一般通过旅行、工作、日常服装搭配等这些行为&#xff0c;输出自己…

龙芯开启ssh服务——使用Putty连接

本文采用龙芯3A6000处理器&#xff0c;Loongnix操作系统。 为了能使用其他电脑远程操控龙芯电脑&#xff0c;需要打开loongnix的ssh服务&#xff0c;并在其他电脑里使用putty连接loongnix。 1 修改ssh配置文件 命令行输入&#xff1a; sudo vim /etc/ssh/sshd_config按下i插…

Unity3d Shader篇(六)— BlinnPhong高光反射着色器

文章目录 前言一、BlinnPhong高光反射着色器是什么&#xff1f;1. BlinnPhong高光反射着色器的工作原理2. BlinnPhong高光反射着色器的优缺点优点缺点 3. 公式 二、使用步骤1. Shader 属性定义2. SubShader 设置3. 渲染 Pass4. 定义结构体和顶点着色器函数5. 片元着色器函数 三…