参考大佬专栏: Linux 教程 | 爱编程的大丙 及其B站视频
基础
文件IO
进程与线程
阿Q理解
操作系统(windows & Linux)区别
- windows
- 进程和线程的概念有明确定义,进程的概念对应于一个程序的运行实例(instance),而线程则是程序代码执行的最小单元
- 提供API,CreateThread()用于建立一个新的线程,传递线程函数的入口地址和调用参数给新建的线程,然后新线程就开始执行了
- 一个线程拥有自己的堆栈、寄存器(包括程序计数器PC,用于指向下一条应该执行的指令在内存中的位置),而代码段、数据段、打开文件这些进程级资源是同一进程内多个线程所共享的。因此同一进程的不同线程可以很方便的通过全局变量(数据段)进行通信
#include <windows.h>
#include <stdio.h>
// 全局变量(位于数据段),被所有线程共享(模拟进程级资源)
int sharedCounter = 0;
const int MAX_COUNT = 10;
// 线程函数声明
DWORD WINAPI IncrementThread(LPVOID lpParam);
int main() {
HANDLE hThread;
DWORD dwThreadId;
printf("主线程: 进程ID=%d, 初始共享值=%d\n\n", GetCurrentProcessId(), sharedCounter);
// 创建第一个线程(增加计数器)
hThread = CreateThread(
NULL, // 默认安全属性
0, // 默认堆栈大小
IncrementThread, // 线程函数
NULL, // 无参数传递
0, // 默认创建标志
&dwThreadId // 返回线程ID
);
// 等待子线程完成
WaitForSingleObject(hThread, INFINITE);
// 关闭线程句柄
CloseHandle(hThread);
printf("\n主线程: 最终共享值=%d\n", sharedCounter);
return 0;
}
// 增加共享计数器的线程函数
DWORD WINAPI IncrementThread(LPVOID lpParam) {
printf("增量线程[ID=%d]: 开始增加共享计数器...\n", GetCurrentThreadId());
for (int i = 0; i < MAX_COUNT; i++) {
sharedCounter++; // 直接修改全局变量
printf("增量线程: 设置 sharedCounter = %d\n", sharedCounter);
}
printf("增量线程: 完成增加操作\n");
return 0;
}
- Linux
- 只有进程而无线程,然而它的进程又可以表现得像windows下的线程
- linux利用fork()和exec函数族来操作多进程,fork()函数可以在进程执行的任何阶段被调用,一旦调用,当前进程就被叉分为父进程和子进程,两者拥有相同的代码段和暂时相同的数据段(虽然暂时相同,但从分叉开的时刻就是逻辑上的两个数据段了,之所以说是逻辑上的,是因为这里是“写时复制”机制,也就是,除非万不得已有一个进程对数据段进行了写操作,否则系统不去复制数据段,这样达到了负担最小),两者的区别在于fork()函数返回值,对于子进程来说返回为0,对于父进程来说返回的是子进程id,因此可以通过if(fork()==0)…else…来让父子进程执行不同的代码段,从而实现“分叉”
#include <stdio.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
pid_t fpid; // fpid表示fork函数返回的值
int count = 0;
printf("父进程开始, 进程ID: %d\n", getpid());
fpid = fork();
if (fpid < 0) {
printf("fork过程中发生错误!");
return 1;
} else if (fpid == 0) {
// 子进程代码
printf("我是子进程, 我的进程ID是: %d\n", getpid());
printf("父进程ID是: %d\n", getppid());
count++;
// 子进程可以执行额外操作
printf("子进程正在执行额外任务...\n");
for (int i = 0; i < 3; i++) {
printf("子进程计数: %d\n", i);
sleep(1);
}
} else {
// 父进程代码
printf("我是父进程, 我的进程ID是: %d\n", getpid());
printf("我创建的子进程ID是: %d\n", fpid);
count++;
// 父进程可以执行额外操作
printf("父进程正在等待子进程完成...\n");
// 父进程等待子进程结束
wait(NULL); // 可以使用waitpid()更精确的调控
printf("子进程已完成,父进程继续执行\n");
for (int i = 0; i < 3; i++) {
printf("父进程计数: %d\n", i);
sleep(1);
}
}
// 两进程共用代码
printf("进程 %d 的统计结果是: %d\n", getpid(), count);
return 0;
}

对上述代码进行建议修改,可以简单模拟一台简易版的Linux
#include <stdio.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
// main()可视为一台简易版的Linux
// count 可看作进程向系统申请的共享内存
int count = 0;
int simulateMain(){
// 模拟main()函数,可视为启动进程
pid_t fpid; // fpid表示fork函数返回的值
printf("父进程开始, 进程ID: %d\n", getpid());
fpid = fork();
if (fpid < 0) {
printf("fork过程中发生错误!");
return 1;
} else if (fpid == 0) {
// 子进程代码
printf("我是子进程, 我的进程ID是: %d\n", getpid());
printf("父进程ID是: %d\n", getppid());
count++;
// 子进程可以执行额外操作
printf("子进程正在执行额外任务...\n");
for (int i = 0; i < 3; i++) {
printf("子进程计数: %d\n", i);
sleep(1);
}
} else {
// 父进程代码
printf("我是父进程, 我的进程ID是: %d\n", getpid());
printf("我创建的子进程ID是: %d\n", fpid);
count++;
// 父进程可以执行额外操作
printf("父进程正在等待子进程完成...\n");
// 父进程等待子进程结束
wait(NULL); // 可以使用waitpid()更精确的调控
printf("子进程已完成,父进程继续执行\n");
for (int i = 0; i < 3; i++) {
printf("父进程计数: %d\n", i);
sleep(1);
}
}
// 两进程共用代码
printf("进程 %d 的统计结果是: %d\n", getpid(), count);
}
simulateMain();
return 0;
}
- vfork()函数与fork()函数相同,都是系统调用函数,两者的区别是在创建子进程时,fork()函数会复制所有的父进程的资源,包括进程环境、内存资源等,而vfork()函数在创建子进程时不会复制父进程的所有资源,父子进程共享地址空间。这样,在子进程中对虚拟内存空间中变量的修改,实际上是在修改父进程虚拟内存空间中的值
注意: 在使用vfork()函数时,父进程会被阻塞,需要在子进程中调用_exit()函数退出子进程,不能使用exit()退出函数
在exit系统调用中,函数exit()在终止进程时会关闭所有文件,清空缓冲区。因此,如果在fork()函数和vfork()函数中使用exit()函数终止子进程,会清空标准输入/输出流,可能造成临时文件丢失,并且vfork()函数是父子进程共享虚拟内存,如果在子进程中使用exit()函数会严重影响到父进程,所以在使用这两个创建进程的函数时,尽量都不要使用exit()函数终止子进程 - exec函数族的函数的作用则是启动另一个程序的新进程,然后完全用那个进程来代替自己(代码段被替换,数据段和堆栈被废弃,只保留原有进程id)
例子:在shell中执行命令时(如ls),shell会先fork()一个子进程,然后在子进程中exec()执行ls程序 - Linux进程不像windows线程那样方便通信,因为他们间无共享数据段、地址空间,他们间的通讯通过管道(无名管道用于父子进程通信, 命名管道用于任意两个进程间的通讯)、共享内存(一个进程向系统申请一块可以共享的内存, 其他进程通过标识符取到该内存, 并将其连接到自己的地址空间, 效果类似于windows下的多线程间的共享数据段、信号量、 信息队列、 套接字)
多线程
C
线程函数内容
#include<pthread.h>
// 线程函数
/*
线程ID类型: pthread_t (X86_64位架构中, 大小为8字节, unsigned long long)
函数:
1、返回当前线程的线程ID pthread_self
2、创建线程 int pthread_create(*thread, *attr(线程属性, 默认填NULL), void* (*start_routing)(void *), void *arg);
备注:子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,
如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。
但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。
3、线程退出 pthread_exit(void *retval) retval携带线程退出时的数据
4、线程回收(阻塞函数, 子线程运行函数对应的主线程就会被阻塞)
int pthread_join(thread, void** retval) retval - 指向pthread_exit()传出数据的地址
注意子线程与主线程间的信息传递过程
子线程和主线程共用一个栈区,当子线程退出时其在栈区的内存就会被回收,因此exit()传出的数据最好用堆区数据
(也可以全局变量保存 - 主线程栈,将数据传入arg中或者static关键字)保存
5、线程分离(不阻塞) int pthread_detach(thread)
程序中的主线程有属于自己的业务处理流程,
如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,
主要线程的任务也就不能被执行了
线程分离主线程不会被阻塞,仍会出现子线程还没执行完主线程执行完的情况,因此可以在主线程中设置退出时间
6、线程取消 int pthread_cancel(thread)
使用该函数杀死一个线程需要两步:
1)、在主线程A中调用线程曲线函数,指定杀死子线程B(此时B不会被直接杀死)
2)、在子线程B中执行一次系统调用(从用户区切换到内核区),否则子线程B会一直执行
7、线程ID比较 int pthread_equal(pthread1, pthread2)
在Linux中线程ID本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的ID,
但是线程库是可以跨平台使用的,在某些平台上 pthread_t可能不是一个单纯的整形,
这中情况下比较两个线程的ID必须要使用比较函数
*/
Q1:Linux中编译失败
注意:Compile and link with -pthread
gcc FILENAME –lpthread -o APPNAME
Q2:主线程和子线程间的信息接受
注意子线程中不要把局部变量返回,解决 – 应当设为全局变量或者在堆区创建数据
解决办法1 – 全局变量
利用static关键字,申明变量为全局变量
或者:main函数外设置全局变量
解决办法2 – 堆区创建数据
使用malloc在堆区创建数据
解决办法3 – 利用主函数栈
在主线程中创建变量,然后再将其地址传入到子线程中
#include<pthread.h>
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
struct ThreadArgs{
char *message;
int number;
};
void* User1(void* args){
printf("ChildThread ID: %ld, starting.....\n", pthread_self());
struct ThreadArgs *Args = (struct ThreadArgs *)args;
printf("MAIN SEND MESSAGE:\n%s\n", Args->message);
Args->number = 2;
// int* number = malloc(sizeof(int));
// *number = 2;
// static int number1 = 2;
int *number = &Args->number;
pthread_exit(number);
}
void* User2(void* args){
printf("ChildThread ID: %ld, starting.....\n", pthread_self());
struct ThreadArgs* Args = (struct ThreadArgs*) args;
printf("MAIN MEND MESSAGE:\n%s\n", Args->message);
Args->number = 6;
// int* number = malloc(sizeof(int));
// *number = 6;
// static int number2 = 6;
int *number = &Args->number;
pthread_exit(number);
}
int main(){
pthread_t U1;
pthread_t U2;
int number1, number2;
char sendtoU1[] = "Hello, U1, are you ready to send me a number?\n";
char sendtoU2[] = "Hello, U2, are you ready to send me a number?\n";
struct ThreadArgs Args1 = {sendtoU1, number1};
struct ThreadArgs Args2 = {sendtoU2, number2};
pthread_create(&U1,NULL,User1, &Args1);
pthread_create(&U2,NULL,User2,&Args2);
// 接受User1和User2发来的信息
void *U1message = NULL, *U2message = NULL;
pthread_join(U1, &U1message);
pthread_join(U2, &U2message);
int *U1number = (int *)U1message;
int *U2number = (int *)U2message;
// 输出结果
printf("U1 send Message: %d\nU2 send Message: %d\n", *U1number, *U2number);
printf("Result: %d\n", *U1number + *U2number);
// free(U1number);
// free(U2number);
printf("number1: %d\n", number1);
printf("number2: %d\n", number2);
return 0;
}
Q3:pthread_cancel 子线程退出时机
在子线程B中执行一次系统调用(从用户区切换到内核区),否则子线程B会一直执行
或者设置显式cancel点
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void *new_thread_start(void *arg) {
for(;;){
printf("waiting.......\n");
pthread_testcancel(); // 显式取消点
sleep(1);
}
return (void *)0;
}
int main(void) {
pthread_t tid;
void *tret;
int ret;
// 创建新线程
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if(ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
sleep(1);
// 向新线程发送取消请求
ret = pthread_cancel(tid);
if(ret) {
fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
exit(-1);
}
// 等待新线程终止
ret = pthread_join(tid, &tret);
if(ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
printf("新线程终止, code=%ld\n", (unsigned long long)tret);
exit(0);
}
C++
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<chrono>
using namespace std;
/*
1、构造函数
· thread() noexcept; - 构造线程对象,在该线程中不进行任何操作
· thread(thread&& other) noexcept; - 移动构造函数,将other线程的所有权转让给新的thread对象,other不再执行线程
· thread(const thread& ) = delete; - 显式删除拷贝构造,两个线程是无法共享栈空间的
· // explicit关键字用于声明 构造函数 或类型转换运算符,阻止编译器执行非预期的隐式类型转换
template<class Function, class... Args>
explicit Thread( Function&& f, Args&& ... args);
Thread t1 = {f, args}; 错误
Thread t1(f, args); 正确,explicit表明只能显式构造
2、公共成员函数
· get_id(); - 获取线程ID
· join(); - 主线程主动等待子线程的终止(线程阻塞): join函数在哪个线程中执行,就阻塞哪个线程
· detach(); - 线程分离; 在线程分离后就无法在主线程中对子线程做任何控制(get_id(), join()...)
· joinable(); - 判断主线程和子线程是否关联
· operator=; - 只能移动赋值(资源所有权转移), 而不能拷贝赋值
3、静态函数
· hardware_concurrency(); - 返回CPU核心
根据该结果创建出数量相等的线程,并发效率最高 (p: m / (n + m) > i / (n + i) - i < m )
m - cpu核心数, n - 除当前线程外正在运行的全部线程数
*/
// 模拟 thread 库
class SimulateThread{
public:
// 构造函数
SimulateThread() noexcept;
SimulateThread(SimulateThread&& other) noexcept;
SimulateThread(const SimulateThread& ) = delete;
// 可以直接从构造函数中引用传值,从而获取到子线程的相关数据
template<class Function, class... Args>
explicit SimulateThread(Function&& f, Args&& ... args){
// 在此处启动新线程
// 此处演示如何将 可变模板参数args传值于函数f中 (完美转发)
// std::apply 的用处是将一个元组(std::tuple)展开,并将其元素作为参数传递给一个可调用对象
auto _M_thread = [f = std::forward<Function>(f),
args = std::make_tuple(std::forward<Args>(args)...)](){
std::apply(f, args);
};
_M_thread();
}
// 公共成员函数
// get_id() - 获取线程ID
// this_thread::get_id() - 获取当前线程ID
std::thread::id get_id() const noexcept{
return this_thread::get_id();
};
// join() - 阻塞主线程并能返回子线程中的数据,注意此处和C有区别!!! - 无法从join中获取子线程的返回值
void join();
// detach() - 线程分离
void detach();
// joinable()
bool joinable() const noexcept;
// operator=
// 移动赋值
SimulateThread& operator= (SimulateThread&& other) noexcept;
// 禁止拷贝赋值
SimulateThread& operator= (const SimulateThread& other) = delete;
// 静态成员函数
// hardware_concurrency() - 获取cpu核心数
static unsigned int hardware_concurrency() noexcept;
};
mutex cout_mutex;
// 来点有趣的事情:模拟同时下载两个文件,都下载好后再进行一些额外操作 (显示总大小)
void download(const string& URL, int& SIZE){
{
// 由于多线程同时向标准输出写入,如果没有锁,输出的结果是非常混乱的(实际下载过程中无需互斥锁 - 抢时间片进行下载)
lock_guard<mutex> lock(cout_mutex);
cout << "ID: " << this_thread::get_id() << ", DOWNLOADING......" << "\tURL: " << URL <<endl;
}
SIZE = rand();
// 模拟进度条
for(int i = 0; i < SIZE; ++i){
this_thread::sleep_for(chrono::milliseconds(100));
if(i % (SIZE / 10) == 0)
{
lock_guard<mutex> lock(cout_mutex);
cout << "ID: "<< this_thread::get_id() << "\tALREADY DOWNLOAD: " << (100 * i / SIZE) << "%" << endl;
}
}
}
// 下载完成后操作,返回总文件大小
void AfterDownload(int& SIZE1, int& SIZE2){
cout << "DownLOAD COMPLETE ! " << endl << "TOTAL SIZE: " << SIZE1 + SIZE2 << "(G)";
}
int main(){
int file1_size, file2_size;
string url1 = "www.baidu.com";
string url2 = "https://leetcode.com";
thread t1([&](){
download(url1, file1_size);
});
thread t2([&](){
download(url2, file2_size);
});
t1.join();
t2.join();
AfterDownload(file1_size, file2_size);
return 0;
}