Linux线程控制

443c51730ff34b22adc24222e1b63d49.jpeg

目录

一、线程的简单控制

1.多线程并行

2.线程结束

3.线程等待

(1)系统调用

(2)返回值

4.线程取消

5.线程分离

二、C++多线程小组件

三、线程库TCB

1.tid

2.局部储存


 

一、线程的简单控制

1.多线程并行

我们之前学过pthread_create可以创建线程,而且要使用原生线程库必须在编译时加上-lpthread。

我们用这个接口一次性创建五个进程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;#define NUM 5void* start_routine(void* args)
{string s;s += "running->";s += (char*)args;while(1){cout << "running->" << s;sleep(1);}
}int main()
{for(int i = 0; i<NUM; ++i){pthread_t tid;char buffer[64];snprintf(buffer, sizeof(buffer), "new thread:%d\n",i+1);pthread_create(&tid, nullptr, start_routine, (void*)buffer);}while(1){cout << "main pthread running.\n";sleep(1);}return 0;
}

我们运行三次程序:

fdc90e7b38a2414a85e05f0faaafd9cd.png

我们可能会想,按照代码的逻辑,我们想要看到的结果是:

running->new thread:1

running->new thread:2

running->new thread:3

running->new thread:4

running->new thread:5

main pthread running.

可是为什么运行两次,main pthread running.打印的位置不断变化,而且后面的数字都是5。

首先解决第一个问题,main pthread running.打印的位置为什么不断变化?

这是因为线程的运行顺序是由调度器决定的,各个线程的执行进度不同,主线程不一定在最后才能打印main pthread running.

第二个问题,为什么后面的数字都是5?

这是因为,三次执行中主线程执行顺序靠前。主线程将5次创建线程的代码跑完了,每循环一次sprintf就会将上一次的buffer内容覆盖掉,循环5次,i+1变为5。此时,又因为包括主线程的6个线程共享同一个地址空间,所以它们同时能看到buffer。五个新线程通过参数传递的地址找到buffer并打印出来。

既然你说buffer作为缓冲区被覆盖掉了,那我们为每一个线程构建一个自己的缓冲区不就解决问题了?

这样的思想是没有问题的,我们可以使用一个pthread_data类管理线程,内部包含线程标识符tid和缓冲区buffer。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;while(1){cout << s;sleep(1);}
}int main()
{for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);}while(1){cout << "main pthread running.\n";sleep(1);}return 0;
}

虽然确实1到5都出现了,但是因为我们控制不了线程运行的顺序,所以还是不能保证按数字顺序打印。

6d9f12067d11497f990c8b96598a6ed9.png

2.线程结束

线程执行的函数有一个void*返回值,我们返回空指针就能终止该线程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 5)return nullptr;}
}int main()
{for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)(pd->buffer));}while(1){cout << "main pthread running.\n";sleep(1);}return 0;
}

五个线程都能正常退出,最后只剩下主线程运行。

4f8fa1af7301499bae9b46f139f30061.png

POSIX线程库也提供了一个接口用于结束线程

void pthread_exit(void* retval);

头文件:pthread.h

功能:终止当前线程。

参数:void* retval是线程的结束信息,设置为空指针就可以了。

void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 5)pthread_exit(nullptr);}
}

将return nullptr替换成该函数也能实现线程退出。

3.线程等待

(1)系统调用

和进程一样,线程在执行完毕时如果task_struct结构体不回收,就会导致内存泄漏(类似未被回收的僵尸进程)。所以我们需要使用pthread_join函数将线程加入等待队列,加入等待队列的线程会被回收,但是回收的现象我们是看不到的。

int pthread_join(pthread_t thread, void** retval);

头文件:pthread.h

功能:将标识符为tid的线程加入等待队列。

参数:pthread_t thread是需要等待的线程标识符,void** retval是线程结束返回的信息,是一个输出型参数。

返回值:等待成功返回0,等待失败返回错误码。

实际上,这个加入等待队列和我们之前的进程等待现象差不多,我们写一段代码,让五个新线程运行3秒终止,主线程负责回收五个新线程,观察一下结果。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 3)return nullptr;}
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){pthread_join(e->tid, nullptr);printf("wait success:%d\n", e->tid);}return 0;
}

运行结果:

1b82015e855a4b78b4695847e1711fd8.png

主线程在执行至线程等待代码时,主线程并不会继续往下执行。这也证明了,线程等待是阻塞式等待。

(2)返回值

在之前我们写的start_routine线程函数有一个void*的返回值,它可以返回线程退出相关的信息。

比如说,下图的最后一行就可以以void*的格式返回1。

5d48364d895947d1b89062791473cb59.png

不过这里有个问题,它虽然能返回结束的信息,但是这个变量要怎么让主线程接收到呢?

pthread_join函数有一个输出型参数void** retval,我们在主线程内定义一个void*类型的ret指针变量。当一个线程被回收的时候,将ret传参,它的返回值就会被放进这个ret里。

又因为返回值类型为void*,如果只将ret传参,那么只会将ret的临时拷贝改变。所以参数必须为void**,这也是使用二级指针的原因。

我让每一个线程都返回1,运行代码。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 3)break;}return (void*)1;
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){void* ret = nullptr;pthread_join(e->tid, &ret);printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);}return 0;
}

运行结果:

ed8c2c87bac8407aaa89e3fd093a1fb7.png

把return换成只有一个参数void* retval的pthread_exit,它也可以将结果输出到变量中。

04afb8e6dfb74c579c227feef585a3e7.png

结果与上面的一致。

如果想要让每一个线程都返回各自的错误码,我们可以在pthread_data类中增加一个储存返回值的变量。

我下面就修改代码让每一个线程返回自己的编号。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];int num;
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 3)break;}//return (void*)1;pthread_exit((void*)(p->num));
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pd->num = i+1;pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){void* ret = nullptr;pthread_join(e->tid, &ret);printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);}return 0;
}

运行结果:

ec3268040b0d4c7fae4a2aeecd4371e5.png

4.线程取消

线程取消也是终止线程的一种方式,可使用下面的系统调用。

int pthread_cancel(pthread_t thread);

头文件:pthread.h

功能:取消标识符为thread的线程。

参数:pthread_t thread是需要取消的线程标识符。

返回值:取消成功返回0,取消失败返回错误码。

我们创建10个线程,在中途取消前五个线程,查看具体的现象。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 10class pthread_data
{
public:pthread_t tid;char buffer[64];int num;
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;int j = 1;while(1){sleep(1);if(j++ == 3)break;}//return (void*)1;pthread_exit((void*)(p->num));
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pd->num = i+1;pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(int i = 0; i<NUM; ++i){printf("new thread:%d,address:%p\n", i+1, vpd[i]);}for(int i = 0; i<vpd.size()/2; ++i){pthread_cancel(vpd[i]->tid);}for(auto& e : vpd){void* ret = nullptr;pthread_join(e->tid, &ret);printf("join success:%d,exit code:%d\n", e->tid, (long long)ret);}return 0;
}

运行结果:

ec5c5145ddc94211a995cc2ad0ea4521.png

对于取消的前五个线程,等待会直接成功,返回值是-1。

未被取消的后五个线程,仍然阻塞等待,等待成功后返回的是设定的线程编号。

如果一个线程是被取消结束的,退出码就是-1,是一个宏PTHREAD_CANCELED。

5.线程分离

默认情况下,在线程退出后,需要使用pthread_join将它加入等待队列,否则就会造成内存泄漏。

但是主线程只能阻塞式线程,阻塞时主线程只能干等着。而且我们有时根本不关心线程的返回值,那阻塞式等待就是巨大的负担。

那么,能不能模仿之前的轮询检测让主线程也继续干活吗?

可以,我们可以将需要释放的线程变为分离状态。我们将一个进程的所有线程做成一个组,如果将一个进程移出这个组,我们就说该线程处于分离状态。此时,主线程不用再关心该线程的状态,它会由系统自动释放。

我在这里也在说明一下,只要是让线程加入等待队列,那就必须要阻塞式等待;只要是分离状态,就不能再加入等待队列。所以说,可加入阻塞队列和分离状态是相互矛盾的,这也解释了为什么不能轮询非阻塞等待。

int pthread_detach(pthread_t thread);

头文件:pthread.h

功能:设置标识符为thread的线程分离状态。

参数:pthread_t thread是需要分离的线程标识符。

返回值:取消成功返回0,取消失败返回错误码。

这个函数既可以分离线程组内的其他线程,也可以分离自己,但分离自己就需要用到自己的线程标识符tid,线程自己的tid可以由下面的函数获取。

pthread_t pthread_self(void);

头文件:pthread.h

功能:获取线程自己的tid。

返回值:返回自己的tid。

我们创建一个新线程,让新进程在第一步就分离执行,观察主线程能否回收它。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;#define NUM 10void* start_routine(void* args)
{pthread_detach(pthread_self());string s = static_cast<const char*>(args);for(int i = 0; i<5; ++i){cout << s;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;int n = pthread_join(tid, nullptr);cout << "error: " << n << ":" << strerror(n) << endl;return 0;
}

线程只要分离,主线程就管不了它了,而且我们发现确实不能回收该分离的线程了,与预期效果一致。

09190419c3034f939c202bdeeab3b90a.png

我们把start_routine的前两个语句调换位置。让线程先去构造string对象,然后将其分离。

e0e2659fddf34973be4e0386dd3909c2.png

此时运行观察,我们发现主线程把新线程回收了。这又是怎么回事呢?

4e7c935cfe9c413ab20fa4c926fdc571.png

这是因为主线程优先运行,在新线程开始运行时,主线程已经在阻塞等待新线程了,这样设置分离也就没有意义了。

既然在新线程中分离线程并不保险,我们就将分离操作全部放在主线程中。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;#define NUM 10void* start_routine(void* args)
{string s = static_cast<const char*>(args);  for(int i = 0; i<5; ++i){cout << s;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");//在主线程分离新线程pthread_detach(tid);cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;int n = pthread_join(tid, nullptr);cout << "error: " << n << ":" << strerror(n) << endl;return 0;
}

最终结果就不会出错了。

15b63b2798bb4e42a652c67eee091933.png

二、C++多线程小组件

既然我们也学习了一些系统调用了,我们可以试着写一个小组件。这个小组件将线程的创建、执行和等待等都封装起来。我们在程序中指定一个函数,让多个线程不断地执行该函数。

thread_handler.h

#include<pthread.h>
#include<assert.h>
#define NUM 64
class Thread;//前置声明class Context
{
public:Context():_this(nullptr),_args(nullptr){}Thread* _this;//线程this指针void* _args;//pthread_create需要传递的args
};class Thread
{typedef std::function<void*(void*)> func_t;
public://构造函数创建线程Thread(func_t func, void* args, int number = 0):_func(func),_args(args){//对线程进行规范化命名char buffer[NUM];snprintf(buffer, sizeof(buffer), "thread-%d", number);_name = buffer;//将线程信息保存到Context变量Context* p = new Context;p->_this = this;p->_args = args;int n = pthread_create(&_tid, nullptr, start_routine, (void*)p);}//运行函数void* run(void* args){return _func(args);}//线程执行start_routine,但是它如果是普通成员函数,则参数多了一个this指针//将其变为static成员函数就能消除this指针//但同时我们又需要执行线程内部成员_func函数,此时既不能访问this指针的内容,之前创建线程传递的void* args也传递不到//我们再次构建一个类Context,将这些内容包含进去,将它们通过一个类指针传过去就好了static void* start_routine(void* args){Context* cp = static_cast<Context*>(args);void* ret = cp->_this->run(cp->_args);}//将加入等待队列void join(){int n = pthread_join(this->_tid, nullptr);assert(n == 0);}private:std::string _name;pthread_t _tid;func_t _func; void* _args;
};

test.cc

#include<iostream>
#include<memory>
#include<unistd.h>
#include"thread_handler.h"
using namespace std;void* handler(void* args)
{string s = "new thread:";s += static_cast<const char*>(args);s += '\n';while(1){cout << s;sleep(1);}
}int main()
{unique_ptr<Thread> t1(new Thread(handler, (void*)"thread1", 1));unique_ptr<Thread> t2(new Thread(handler, (void*)"thread2", 2));unique_ptr<Thread> t3(new Thread(handler, (void*)"thread3", 3));t1->join();t2->join();t3->join();return 0;
}

可以看到三个线程在不停地执行我们的函数。

三、线程库TCB

1.tid

我们编写代码让新线程打印自己的 tid,主线程打印自己和新线程的tid。

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;void* start_routine(void* args)
{string s = (char*)args;while(1){cout << s << " tid:0x" << pthread_self() << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");assert(n == 0);while(1){printf("main thread tid:0x%p,new thread tid:0x%p\n", pthread_self(), tid);sleep(1);}return 0;
}

我们发现主线程和新线程打印的新线程tid相同,而且它们是一个地址。

db4bff194bab4e5a941cbdd4c7188084.png

我们一开始就强调,Linux内核中没有线程概念,只有轻量级进程的PCB,更没有TCB这样的数据结构。我们宏观看到的线程是系统的通过clone创造出的轻量级进程,只是这些轻量级进程共用了地址空间等资源。然后这些轻量级进程再通过POSIX原生线程库模拟出我们看到的线程。

既然原生线程库可以保证大量的线程同时工作,那么原生线程库中就必定有能管理这些线程的数据结构,换句话说,TCB结构一定在线程库中。

由于存储线程属性的TCB不在内核中,所以Linux的TCB也叫做用户级线程。

结论:Linux内核只负责调度执行流,用户关心的线程及其属性都由原生线程库维护。Linux的用户级线程和内核轻量级进程都保存着线程的属性,二者基本做到一一对应。

我们将视线转向地址空间:

原生线程库加载到内存后,页表会对虚拟地址空间和其内存物理地址建立映射关系。根据地址空间的分区,线程库会映射在虚拟地址空间中的共享区中,其中TCB等结构和数据当然也在共享区。

看下面的图片

3b7b3217cda041a2b5d83fe3e6a90125.png

我这里严谨一点:

由于局部储存和栈结构空间都只是在TCB存储指针,虽然这些都是TCB拥有的数据,但它们的内容肯定不会直接保存在TCB里,所以我就把它们和TCB分开了。

你要是认为全部的数据就是TCB也可以接受。

我们能得到以下结论:

  • 共享区内的每个线程都有自己的TCB、局部储存和独立栈,它们由载入内存的原生线程库维护。
  • 主线程的栈就是地址空间的栈区,而新线程的独立栈结构都在共享区,所以线程间才能有独立栈结构。
  • tid是指针类型,TCB在地址空间中的虚拟地址就是tid的值。

2.局部储存

我们知道同一进程的线程相互共用地址空间和页表,所以它们都可以使用一个全局变量。

可如果新线程仍然想用这个变量名,但又不想影响其他线程。这时需要让这个全局变量在每个进程中独立使用,此时就可以使用线程的局部存储属性了。

不使用局部储存6e5f86e6d09b493e96db80a75f3bdb92.png

 主线程和新线程都用了同一个g_val变量。

ccb69a3de0594cbbb61ea5f1107bdb3b.png

在int g_val = 0;前面加上__thread就能将该变量独立出去。

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;__thread int g_val = 0;void* start_routine(void* args)
{string s = (char*)args;while(1){++g_val;printf("new thread g_val:%d,address:%p\n", g_val, &g_val);sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");assert(n == 0);while(1){++g_val;printf("main thread g_val:%d,address:%p\n", g_val, &g_val);sleep(1);}return 0;
}

主线程和新线程使用的不再是同一个g_val变量,互相之间也不会受到影响。

10f71944cba14102a43dddc99536b0cf.png

结论:在全局变量或static变量前添加 __thread,可以让每个线程的TCB中都有一份独立的变量,不会互相影响。

 

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

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

相关文章

使用openpyxl来创建一个月的日程表

首先你心里要有一张表的样子&#xff0c;openpyxl才能帮你创建出其余的29张。 import openpyxl from openpyxl.styles import Alignment, Font import calendar from datetime import datework_path rXX\YY\ZZ\日报-九月.xlsxtry:workbook openpyxl.load_workbook(work_path…

AttributeError: ‘ConfigDict‘ object has no attribute ‘log_level‘

运行 python tools/train.py configs/pspnet/pspnet_r50-d8_512x512_80k_ade20k.py 时出现 问题 Traceback (most recent call last):File "tools/train.py", line 242, in <module>main()File "tools/train.py", line 167, in mainlogger get_ro…

基于java swing和mysql实现的仓库商品管理系统(源码+数据库+运行指导视频)

一、项目简介 本项目是一套基于java swing和mysql实现的仓库商品管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经…

GPU编程(基于Python和CUDA)(二)——显示GPU信息

系列文章目录 GPU编程&#xff08;基于Python和CUDA&#xff09;&#xff08;一&#xff09;——零基础安装pycuda GPU编程&#xff08;基于Python和CUDA&#xff09;&#xff08;二&#xff09;——显示GPU信息 显示GPU信息 系列文章目录前言通过CUDA查看GPU信息使用pycuda查…

c++(8.28)菱形继承,虚继承,多态,抽象类,模板+Xmind

xmind: 作业&#xff1a; 1.编程题&#xff1a; 以下是一个简单的比喻&#xff0c;将多态概念与生活中的实际情况相联系&#xff1a; 比喻&#xff1a;动物园的讲解员和动物表演 想象一下你去了一家动物园&#xff0c;看到了许多不同种类的动物&#xff0c;如狮子、大象、猴…

web功能测试方法大全—完整!全面!(纯干货,建议收藏哦~)

本文通过六个部分为大家梳理了web功能测试过程中&#xff0c;容易出现的遗漏的部分&#xff0c;用以发掘自己工作中的疏漏。&#xff08;纯干货&#xff0c;建议收藏哦~&#xff09; 一、输入框 1、字符型输入框 2、数值型输入框 3、日期型输入框 4、信息重复 在一些需要命名…

vue v-for 例子

vue v-for 例子 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head&…

React 使用 useRef() 获取循环中所有子组件实例

目录 背景思考实现完整代码&#xff1a;成功运行后的界面如下&#xff1a; 知识点总结uesRef() 作对象处理useImperativeHandle() 父组件操作引入子组件的内部方法最后 背景 之前项目中使用了antd pro 中的 可编辑表格 (EditableProTable)&#xff0c;在页面中表格要经过多层遍…

Java 多线程系列Ⅰ(创建线程+查看线程+Thread方法+线程状态)

多线程基础 一、创建线程的五种方法前置知识1、方法一&#xff1a;使用继承Thread类&#xff0c;重写run方法2、方法二&#xff1a;实现Runnable接口&#xff0c;重写run方法3、方法三&#xff1a;继承Thread&#xff0c;使用匿名内部类4、方法四&#xff1a;实现Runnable&…

C#---第21: partial修饰类的特性及应用

0.知识背景 局部类型适用于以下情况&#xff1a; 类型特别大&#xff0c;不宜放在一个文件中实现。一个类型中的一部分代码为自动化工具生成的代码&#xff0c;不宜与我们自己编写的代码混合在一起。需要多人合作编写一个类 局部类型的限制: 局部类型只适用于类、接口、结构&am…

铁威马教程丨铁威马NAS如何使用安全顾问工具

在使用NAS的过程中&#xff0c;我们时常可能忽略了一些小细节&#xff0c;久而久之可能造成一定的风险&#xff0c;影响着我们NAS的健康。而使用铁威马NAS的安全顾问工具&#xff0c;可以快速地帮我们扫描系统设置是否安全&#xff0c;让我们更放心更安心地使用NAS。 安全顾问…

MFC -- Date Time Picker 控件使用

当前环境&#xff1a;VS2015 Windows 10 //&#xff08;一&#xff09;使用普通函数&#xff0c; 获取当前时间CString strCurrentTime; COleDateTime m_time COleDateTime::GetCurrentTime(); strCurrentTime m_time.Format(_T("%Y-%m-%d %H:%M:%S")); SetDlgIt…