C++11并发与多线程(线程传参详解)

一、多线程

问题:C++ 线程detach之后 程序退出后子线程还会执行嘛在linux上和windows上?

答案:C++线程detach之后,主程序退出子线程也会退出,不会继续执行。

但是注意:如果你是对main线程使用了 (pthread_exit(NULL))C语言的线程退出函数,那么主程序退出后,子线程还是会执行的。

如果在子线程调用了pthread_exit会对子线程做退出操作,不会对主线程做退出操作。

默认的main函数返回的return 0 其实相当于调用了exit(0)代表程序的正常退出。

C++中的线程默认是以拷贝/移动构造的方式进行传递的,要想传递原始数据,使用std::ref进行包装。

常见的多线程使用实例

值传递

cpp 复制代码
#include <thread>
#include <iostream>

void threadFunction(int value) {
    std::cout << "Value in thread: " << value << std::endl;
}

int main() {
    int myValue = 42;
    std::thread t(threadFunction, myValue); // 值传递
    t.join();
    return 0;
}

地址传递

cpp 复制代码
#include <thread>
#include <iostream>

void threadFunction(int* value) {
    std::cout << "Value in thread: " << *value << std::endl;
    *value = 24; // 修改原始数据
}

int main() {
    int myValue = 42;
    std::thread t(threadFunction, &myValue); // 地址传递
    t.join();
    std::cout << "Value in main: " << myValue << std::endl; // 输出修改后的值
    return 0;
}

在这个例子中,myValue 的地址被传递给 threadFunction,线程函数接收的是指向 myValue 的指针,并可以修改它。

注意事项

  1. 线程安全:如果你使用地址传递并允许线程修改数据,确保考虑线程安全问题,使用互斥锁或其他同步机制来避免数据竞争。

  2. 生命周期:确保传递给线程的参数在其生命周期内是有效的。对于值传递,这通常不是问题,因为数据被复制。但对于地址传递,如果数据在线程结束前被销毁,将会导致未定义行为。(本文重点讨论这个问题)

  3. 性能考虑:对于大型数据结构,值传递可能会因为复制而产生不必要的性能开销。在这种情况下,地址传递可能是更好的选择。

  4. std::refstd::cref :对于引用传递,你可以使用 std::ref 来传递非常量引用,或使用 std::cref 来传递常量引用。这允许你在线程函数中修改(或安全地观察)原始数据。

引用传递

cpp 复制代码
#include <thread>
#include <functional>
#include <iostream>

void threadFunction(int& value) {
    value = 24; // 修改原始数据
}

int main() {
    int myValue = 42;
    std::thread t(threadFunction, std::ref(myValue)); // 引用传递
    t.join();
    std::cout << "Value in main: " << myValue << std::endl; // 输出修改后的值
    return 0;
}

在这个例子中,std::ref 用于传递 myValue 的引用,允许线程函数修改原始数据。

使用仿函数调用多线程程序

仿函数:就是一个类重载了operator() 的函数就称为仿函数。 先引入一个简单的例子来看下怎么使用

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;


class  TA
{
public:
	void operator()()
	{
		cout << "my functor start " << endl;
		cout << "my functor end " << endl;

	}
};


int main()
{
	std::cout << "Hello World! " << sizeof(char*) << endl;
	
	TA ta;
	thread t(ta);


	t.join();
		
	
	cout << "============================================" << endl;
	
 


	return 0;
}

使用detach要注意,如果子线程传递参数使用的是主线程中的引入变量,主线程退出 子线程会产生不可以预料的效果。

deatch调用后,主线程结束后,TA对象应该会销毁。还能调用TA对象嘛?

(对象不在了,实际上被复制到了线程中去) 线程执行完毕TA销毁,但是复制的TA对象还存在。

结论:thread 在创建时候,会调用拷贝构造函数(默认是先浅拷贝哦),如果这里我们使用join就会发现,会调用两次析构函数。

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;


class  TA
{
public:
	int& m_i;
	TA(int& i) :m_i(i)   // 列表初始化来初始化对象。
	{
		cout << "TA()构造函数被执行"<<this  << endl;
	}
	TA(const TA& ta):m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" << endl;
	}
	~TA()
	{
		cout << "TA析构函数"<<this << endl;
	}
	void operator()()
	{
		//cout << "my functor start " << endl;
		//cout << "my functor end " << endl;


		for (int i = 0; i < 100; i++)
		{
			cout << "mi" << i<< "的值为:" << m_i << endl;

		}
		/*cout << "mi1的值为:" << m_i << endl;
		cout << "mi2的值为:" << m_i << endl;
		cout << "mi3的值为:" << m_i << endl;
		cout << "mi4的值为:" << m_i << endl;*/

	}
};


int main()
{
	//std::cout << "Hello World! " << sizeof(char*) << endl;

	int num = 10;
	TA ta(num); 
	thread t(ta);
	//deatch调用后,主线程结束后,TA对象应该会销毁。还能调用TA对象嘛?
	// (对象不在了,实际上被复制到了线程中去) 线程执行完毕TA销毁,但是复制的TA对象还存在。


	//t.detach(); //使用detach要注意,如果子线程传递参数使用的是主线程中的引入变量,主线程退出	子线程会产生不可以预料的效果。
	t.join();
	//std::this_thread::sleep_for(std::chrono::milliseconds(1));

	cout << "============================================" << endl;

	return 0;
}

使用lambda表达式调用多线程程序

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;


class  TA
{
public:
	int& m_i;
	TA(int& i) :m_i(i)   // 列表初始化来初始化对象。
	{
		cout << "TA()构造函数被执行"<<this  << endl;
	}
	TA(const TA& ta):m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" << endl;
	}
	~TA()
	{
		cout << "TA析构函数"<<this << endl;
	}
	void operator()()
	{
		//cout << "my functor start " << endl;
		//cout << "my functor end " << endl;


		for (int i = 0; i < 100; i++)
		{
			cout << "mi" << i<< "的值为:" << m_i << endl;

		}
		/*cout << "mi1的值为:" << m_i << endl;
		cout << "mi2的值为:" << m_i << endl;
		cout << "mi3的值为:" << m_i << endl;
		cout << "mi4的值为:" << m_i << endl;*/

	}
};


int main()
{
	auto mylambda = []() {
		cout << "我的lambda线程开始执行了" << endl;
		cout << "我的lambda线程执行结束了" << endl;

	};
	thread myobj(mylambda);

	cout << "main 线程开始执行" << endl;
	
	myobj.join();

	cout << "============================================" << endl;

	return 0;
}

传递临时对象作为线程参数

示例一

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;

void myprint( const int i, const string& ptr)
//void myprint(const int i, char* ptr)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << i << "|"<<&i  << endl;
	cout << ptr.c_str() <<"|"<<&ptr  << endl;
}

int main()
{
	int mvar = 1;
	int& myvary = mvar; //这里使用的是引用,所以mvar和myvary的地址是一样的。

	char mybuf[] = "this is tesst";

	cout << mvar << "|" << &mvar << endl;

	cout << myvary << "|" << &myvary << endl;
	cout << mybuf  << "|" << &mybuf << endl;

	// mvar和mubuf 两个都是不同地址传递。
	thread myobj(myprint,mvar,mybuf); //这里虽然用引用了,但是内部实现没有用引用,是使用的值传递,地址是不一样的。

	//要避免的陷阱,使用detach
	myobj.join();
	//myobj.detach();
	cout << "=========================================== " << endl;
	cout << "myvar = " << mvar << endl;

	return 0;
}

示例二

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;

void myprint( const int i, const string& ptr)
//void myprint(const int i, char* ptr)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << i << "|"<<&i  << endl;
	cout << ptr.c_str() <<"|"<<&ptr  << endl;
}

int main()
{
	int mvar = 1;
	int& myvary = mvar; //这里使用的是引用,所以mvar和myvary的地址是一样的。

	char mybuf[] = "this is tesst";

	cout << mvar << "|" << &mvar << endl;

	cout << myvary << "|" << &myvary << endl;
	cout << mybuf  << "|" << &mybuf << endl;

	// 但是mybuf到底是在什么时候转为string的
	//thread myobj(myprint,mvar,mybuf); //此时,如果程序执行完毕后,系统才将mybuf转为string,会有BUG
		thread myobj(myprint,mvar,(string)mybuf);  //这样提前转换为一个临时对象,会保证没有问题。

	//要避免的陷阱,使用detach
	myobj.join();
	//myobj.detach();
	cout << "=========================================== " << endl;
	cout << "myvar = " << mvar << endl;

	return 0;
}

求证示例二

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;




class  TA
{
public:
	int m_i;
	 TA(int i) :m_i(i)   // 列表初始化来初始化对象。  explicit可以禁止隐式转换
	{
		cout << "TA()构造函数被执行" << this << endl;
	}
	TA(const TA& ta) :m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" <<this << endl;
	}
	~TA()
	{
		cout << "TA析构函数" << this << endl;
	}
};



void myprint1( const  int& i, const TA& ptr) //要用引用去接收,不然会拷贝构造两次对象
//void myprint(const int i, char* ptr)
{

	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << "func = " << i << "|" << &i << endl;
	cout <<"func = " << ptr.m_i << "|" << &ptr << endl;
}

void myprint( const int i, const string& ptr)
//void myprint(const int i, char* ptr)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << i << "|"<<&i  << endl;
	cout << ptr.c_str() <<"|"<<&ptr  << endl;
}

int main()
{
	int mvar = 1;
	int& myvary = mvar; //这里使用的是引用,所以mvar和myvary的地址是一样的。

	int mysecondpar = 12;

	cout << mvar << "|" << &mvar << endl;

	cout << myvary << "|" << &myvary << endl;
	cout << mysecondpar << "|" << &mysecondpar << endl;

		//thread myobj(myprint1,mvar, mysecondpar);//我们希望将mysecondpar转为TA对象传递给myprint1第二个参数,使用了隐式转换。
		//thread myobj(myprint1, mvar, (TA)mysecondpar);//我们希望将mysecondpar转为TA对象传递给myprint1第二个参数,使用了隐式转换。
		thread myobj(myprint1, ( mvar) , TA(mysecondpar) );//我们希望将mysecondpar转为TA对象传递给myprint1第二个参数,使用了隐式转换。

		//上述两种方式,方式一,在执行完毕后不会调用构造函数,证明main线程结束之后,才进行构造,导致子线程使用了析构完的变量会有问题
		// 方式二,进行了临时构造,会先执行构造函数
		// 方式三,会多一个拷贝构造
	//要避免的陷阱,使用detach
	myobj.join();
	//myobj.detach();
	cout << "=========================================== " << endl;
	cout << mvar << endl;

	return 0;
}

临时对象构造时机捕获

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;




class  TA
{
public:
	mutable int m_i; //mutable可以修改变量的值

	 TA(int i) :m_i(i)   // 列表初始化来初始化对象。  explicit可以禁止隐式转换
	{
		cout << "TA()构造函数被执行" << this<< " threadid = "<< this_thread::get_id() << endl;
	}
	TA(const TA& ta) :m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" <<this << " threadid = " << this_thread::get_id() << endl;
	}
	TA(const TA&& ta) :m_i(ta.m_i)
	{
		//m_i = 0;
		ta.m_i = -100;
		cout << "TA移动构造函数" << this << " threadid = " << this_thread::get_id() << endl;
	}
	~TA()
	{
		cout << "TA析构函数" << this << " threadid = " << this_thread::get_id() <<  endl;
	}
};



void myprint1( const  int& i, const TA& ptr) //要用引用去接收,不然会拷贝构造两次对象,第二次的拷贝构造函数会在子线程中执行
//void myprint(const int i, char* ptr)
{
	ptr.m_i += 199;

	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << "func = " << i << "|" << &i << endl;
	cout <<"func = " << ptr.m_i << "|" << &ptr << endl;
}

void myprint( const int i, const string& ptr)
//void myprint(const int i, char* ptr)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << i << "|"<<&i  << endl;
	cout << ptr.c_str() <<"|"<<&ptr  << endl;
}

int main()
{
	 int mvar = 1;
	int& myvary = mvar; //这里使用的是引用,所以mvar和myvary的地址是一样的。

	int mysecondpar = 12;

	cout << mvar << "|" << &mvar << endl;

	cout << myvary << "|" << &myvary << endl;
	cout << mysecondpar << "|" << &mysecondpar << endl;

		//thread myobj(myprint1,mvar, mysecondpar);//这种方式对象在子线程被构造出来了
		thread myobj(myprint1, mvar, (TA)mysecondpar);//这样是在主线程被构造,拷贝构造的析构函数是在子线程
		//thread myobj(myprint1,mvar , TA(mysecondpar) );//这样也是在主线程被构造
		// 
		//
		//上述两种方式,方式一,在执行完毕后不会调用构造函数,证明main线程结束之后,才进行构造,导致子线程使用了析构完的变量会有问题
		// 方式二,进行了临时构造,会先执行构造函数
		// 方式三,会多一个拷贝构造
	//要避免的陷阱,使用detach
	myobj.join();
	//myobj.detach();
	cout << "=========================================== " << " threadid = " << this_thread::get_id() << endl;
	cout << "mysecondpar= "<< mysecondpar << endl;

	return 0;
}

std::ref函数可以传递真正的数据

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;




class  TA
{
public:
	mutable int m_i; //mutable可以修改变量的值

	 TA(int i) :m_i(i)   // 列表初始化来初始化对象。  explicit可以禁止隐式转换
	{
		cout << "TA()构造函数被执行" << this<< " threadid = "<< this_thread::get_id() << endl;
	}
	TA(const TA& ta) :m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" <<this << " threadid = " << this_thread::get_id() << endl;
	}
	TA(const TA&& ta) :m_i(ta.m_i)
	{
		//m_i = 0;
		//ta.m_i = -100;
		cout << "TA移动构造函数" << this << " threadid = " << this_thread::get_id() << endl;
	}
	~TA()
	{
		cout << "TA析构函数" << this << " threadid = " << this_thread::get_id() <<  endl;
	}
};



void myprint1( const  int& i, const TA& ptr) //要用引用去接收,不然会拷贝构造两次对象,第二次的拷贝构造函数会在子线程中执行
//void myprint(const int i, char* ptr)
{
	ptr.m_i += 199;

	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << "func = " << i << "|" << &i << endl;
	cout <<"func = " << ptr.m_i << "|" << &ptr << endl;
}

void myprint( const int i, const string& ptr)
//void myprint(const int i, char* ptr)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << i << "|"<<&i  << endl;
	cout << ptr.c_str() <<"|"<<&ptr  << endl;
}

int main()
{
	 int mvar = 1;

	cout << mvar << "|" << &mvar << endl;

		TA obj(12);
		//thread myobj(myprint1,mvar, obj);
		thread myobj(myprint1, mvar, std::ref(obj) ); //使用ref之后可以看到是同一块内容
		//
		
	myobj.join();
	//myobj.detach();
	cout << "=========================================== " << " threadid = " << this_thread::get_id() << endl;
	cout << "obj.m_i = "<< obj.m_i << endl;

	return 0;
}

使用智能指针进行参数传递

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;




class  TA
{
public:
	mutable int m_i; //mutable可以修改变量的值

	 TA(int i) :m_i(i)   // 列表初始化来初始化对象。  explicit可以禁止隐式转换
	{
		cout << "TA()构造函数被执行" << this<< " threadid = "<< this_thread::get_id() << endl;
	}
	TA(const TA& ta) :m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" <<this << " threadid = " << this_thread::get_id() << endl;
	}
	TA(const TA&& ta) :m_i(ta.m_i)
	{
		//m_i = 0;
		//ta.m_i = -100;
		cout << "TA移动构造函数" << this << " threadid = " << this_thread::get_id() << endl;
	}
	~TA()
	{
		cout << "TA析构函数" << this << " threadid = " << this_thread::get_id() <<  endl;
	}
};



void myprint2(unique_ptr<int>ptr ) 
{

	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << "func = " << ptr << "|" << *ptr << endl;
	cout <<"func = " << ptr.get() << "|" << *(ptr.get() ) << endl;
}



int main()
{


	unique_ptr<int> myptr(new int(100));

	//thread myobj(myprint2,myptr); //不能直接把智能指针传递过去
	thread myobj(myprint2, std::move(myptr) );  //使用move传递,此时myptr已经空了
		
	myobj.join();
	//myobj.detach();
	cout << "=========================================== " << " threadid = " << this_thread::get_id() << endl;
	cout << "myptr = "<< myptr << endl;

	return 0;
}

用成员函数指针做线程函数

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;




class  TA
{
public:
	mutable int m_i; //mutable可以修改变量的值

	 TA(int i) :m_i(i)   // 列表初始化来初始化对象。  explicit可以禁止隐式转换
	{
		cout << "TA()构造函数被执行" << this<< " threadid = "<< this_thread::get_id() << endl;
	}
	TA(const TA& ta) :m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" <<this << " threadid = " << this_thread::get_id() << endl;
	}
	TA(const TA&& ta) :m_i(ta.m_i)
	{
		//m_i = 0;
		//ta.m_i = -100;
		cout << "TA移动构造函数" << this << " threadid = " << this_thread::get_id() << endl;
	}
	void thread_work(int num)
	{
		cout << num << "TA thread_work "  << this << " threadid = " << this_thread::get_id() << endl;

	}
	void operator()(int num)
	{
		cout << num << "TA operator " << this << " threadid = " << this_thread::get_id() << endl;

	}
	~TA()
	{
		cout << "TA析构函数" << this << " threadid = " << this_thread::get_id() <<  endl;
	}
};



void myprint2(unique_ptr<int>ptr ) 
{

	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << "func = " << ptr << "|" << *ptr << endl;
	cout <<"func = " << ptr.get() << "|" << *(ptr.get() ) << endl;
}



int main()
{


	TA ta(10);
	//thread myobj(ta, 100);  //函数地址 ,类对象,实际参数

	thread myobj(&TA::thread_work, (ta), 15);  //函数地址 ,类对象,实际参数
		//thread myobj(&TA::thread_work, (&ta), 15);  //等价于  &ta ==  ref(ta)
	//thread myobj(&TA::thread_work,std::ref(ta),15);  //等价于  &ta ==  ref(ta),因为属于this指针可以取地址



	//myobj.join();
	myobj.detach();

	cout << "=========================================== " << " threadid = " << this_thread::get_id() << endl;
	cout << "ta = " << ta.m_i << endl;


	return 0;
}

回顾使用仿函数调用多线程

cpp 复制代码
// ConsoleApplication10.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <thread>
#include <mutex>
#include <cstdint>

using  namespace std;




class  TA
{
public:
	mutable int m_i; //mutable可以修改变量的值

	 TA(int i) :m_i(i)   // 列表初始化来初始化对象。  explicit可以禁止隐式转换
	{
		cout << "TA()构造函数被执行" << this<< " threadid = "<< this_thread::get_id() << endl;
	}
	TA(const TA& ta) :m_i(ta.m_i)
	{
		cout << "TA拷贝构造函数" <<this << " threadid = " << this_thread::get_id() << endl;
	}
	TA(const TA&& ta) :m_i(ta.m_i)
	{
		//m_i = 0;
		//ta.m_i = -100;
		cout << "TA移动构造函数" << this << " threadid = " << this_thread::get_id() << endl;
	}
	void thread_work(int num)
	{
		cout << num << "TA thread_work "  << this << " threadid = " << this_thread::get_id() << endl;

	}
	void operator()(int num)
	{
		cout << num << "TA operator " << this << " threadid = " << this_thread::get_id() << endl;

	}
	~TA()
	{
		cout << "TA析构函数" << this << " threadid = " << this_thread::get_id() <<  endl;
	}
};



void myprint2(unique_ptr<int>ptr ) 
{

	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << "func = " << ptr << "|" << *ptr << endl;
	cout <<"func = " << ptr.get() << "|" << *(ptr.get() ) << endl;
}



int main()
{


	TA ta(10);
	//thread myobj(ta, 100);  //函数地址 ,类对象,实际参数
	thread myobj(std::ref(ta), 100);  //不会调用拷贝构造函数
	//thread myobj(&ta, 100);  //不可用,因为ta已经是一个地址了


	//myobj.join();
	myobj.detach();

	cout << "=========================================== " << " threadid = " << this_thread::get_id() << endl;
	cout << "ta = " << ta.m_i << endl;


	return 0;
}
相关推荐
葛雨龙10 分钟前
visual studio 2022 c++使用教程
c++·ide·visual studio
yannan2019031314 分钟前
【数据结构】(Python)树状数组+离散化
开发语言·python·算法
PieroPc15 分钟前
Python 写的《桌面时钟》屏保
开发语言·python
鲁班相信爱情21 分钟前
QT中静态变量无法翻译的问题
开发语言·qt
飞飞-躺着更舒服23 分钟前
C/C++ 文件处理详解
c语言·c++·算法
AI人H哥会Java26 分钟前
【JAVA】Java项目实战—分布式微服务项目:分布式消息队列
java·开发语言
山茶花开时。29 分钟前
[SAP ABAP] 序列化与反序列化
开发语言·sap·abap
横冲直撞de33 分钟前
客户端(浏览器)vue3本地预览txt,doc,docx,pptx,pdf,xlsx,csv,
开发语言·javascript·pdf
坐井观老天38 分钟前
使用 WPF 和 C# 绘制覆盖有阴影高度图的 3D 表面
开发语言·c#·wpf
梧桐树042943 分钟前
python:单元测试
开发语言·python·单元测试