一、线程创建

每个程序都有一个主线程,主线程的入口函数就是main函数,一般情况下,主线程结束,无论子线程是否执行完毕,子线程都将结束

#include <iostream>
#include <thread>
#include <string> using std::cout;
using std::thread; //线程入口函数,函数完毕,线程结束
void MyPrint() {
cout << "This is MyThread!\n"; cout << "MyThread is finish!\n";
} int main() {
cout << "你好\n"; thread MyThread1(MyPrint);//创建子线程,接收可调用对象
cout<<"Main finish!\n";
return 0;
}

二、线程的相关操作

2.1 join

阻塞函数
join: 阻塞函数,在这段代码中表示,主线程阻塞,等待MyThread1运行完后再结束

int main() {
cout << "你好\n"; thread MyThread1(MyPrint);//接收可调用对象 MyThread1.join();//jion,阻塞,等待MyThread运行结束后在继续运行主线程 cout<<"Main finish!\n";
return 0;
}

2.2 detach

分离函数
detach:分离函数,分离子线程和主线程,调用后,主线程是否结束不会影响子线程的运行
用的比较少,因为子线程不可控

int main() {
cout << "你好\n"; thread MyThread1(MyPrint); MyThread1.detach(); cout<<"Main finish!\n";
return 0;
}

调用detach后,线程转入后台运行。此时被C++运行时库接管,当子线程执行完毕后,由运行时库负责清理线程资源。
一旦detach,就无法join,因为失去了关联。
所以detach应用的情况相对较少

2.3 joinable

判断函数
joinable:判断线程是否能join或者detach,返回turefalse

因为很多时候我们不知道某个线程能否join或者detach,所以需要用这个进行判断

三、线程参数

3.1传参所引发的资源回收问题

void myprint(const int &i,char* p){
cout<<i<<endl<<p<<endl;
}
int main(){
int a=10;
char mystr[]="hello world";
thread myth(myprint,myI,mystr);//线程传参
myth.join()
//myth.detach();
}

看上述例子,如果detach分离了线程的话,有可能造成主线程已经结束,但是myth未结束的情况,则此时str已经被系统回收,p所指向的内容就不确定,程序出错
所以,为了避免资源被系统回收,使用临时变量

thread myth(myprint,myI,string(mystr));//线程传参

当使用了临时对象后,便没有出现资源被回收程序出问题的情况


为什么使用临时对象就不会出现问题?
猜想:使用临时对象是进行了资源的拷贝?
为了验证猜想,自己写一个类

class TA {
int a;
public:
void operator()() {//仿函数
cout << "线程TA开始执行了" << endl;
}
TA() = default; TA(int b) {
this->a = b; //获取当前线程的id
cout << "构造函数!TA() id = " << std::this_thread::get_id() << "\taddr = " << this << endl;
} TA(const TA &t) {
this->a = t.a;
cout << "拷贝构造TA(const TA & t) id = " << std::this_thread::get_id() << "\taddr = " << this << endl;
}
void work(int num){
cout<<"子线程work执行 "<< this<<" id = "<<std::this_thread::get_id()<<endl;
}
~TA() {
cout << "析构函数 id = " << std::this_thread::get_id() << "\taddr = " << this << endl;
}
};
void myprint2(const TA &ta) {//class类型参数 即使是引用,仍会进行拷贝,值传递
cout << "子线程开始 " << "addr = " << &ta << "\t子线程id = " << std::this_thread::get_id() << endl;
}

传入a为int类型,可是函数参数类型为TA,此时会发生隐式类型转化,调用构造函数,将int转为TA类型

int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
thread myobj(myprint2, a);//发现对象构造是在子线程中
myobj.join();
}

运行结果:


main id = 139656476180864
构造函数!TA() id = 139656476165696 addr = 0x7f044ea6fd74
子线程开始 addr = 0x7f044ea6fd74 子线程id = 139656476165696
析构函数 id = 139656476165696 addr = 0x7f044ea6fd74

可以看出,由于入口函数参数为类型TA,程序发生隐式转换,用变量a构造了一个临时对象,再将临时对象传入入口函数。并且可以观察到,构造对象的过程并不是在主线程完成的,而是在子线程中进行。所以我们可以推测,如果使用detach,就有可能造成主线程比子线程先结束,导致a被回收,从而构造出现问题,引发程序异常!大家不妨可以试试join改为detach,多运行几次可能就会发现问题所在。

那我们如何解决这类问题呢


不要使用隐式转换,自己完成强转操作

int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
thread myobj(myprint2, TA(a));
myobj.join();
}

运行结果:


main id = 139705515577728
构造函数!TA() id = 139705515577728 addr = 0x7ffd013a24a8
拷贝构造TA(const TA & t) id = 139705515577728 addr = 0x55a7c06ad2c8
析构函数 id = 139705515577728 addr = 0x7ffd013a24a8
子线程开始 addr = 0x55a7c06ad2c8 子线程id = 139705515562560
析构函数 id = 139705515562560 addr = 0x55a7c06ad2c8

通过上一例子可以看出,首先在主线程构造了一个临时变量,由 TA(a)引起,因为这一对象很快被析构了,所以我们判断它是临时对象,这很容易理解;然后发生了拷贝构造,并且拷贝构造依然发生在主线程,并且可以看到,拷贝出来的对象进入到了子线程去执行,我们就可以推测,临时对象作为入口函数的参数,子线程会对其进行拷贝。
因此,在detach一个线程时,我们尽量不要让编译器对参数进行隐式类型转化,这样有可能引发传入资源被回收的问题!对应的解决办法就是自己对其进行转化


我们还可以看出,即使子线程入口函数的的参数是引用类型,传参是仍然是拷贝构造,相当于就是值传递,那如果我们需要子线程对某一数据进行更改,需要传入引用怎么办呢?
使用std::ref()

int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
TA ta(a);
thread myobj(myprint2, std::ref(ta));
myobj.join();
}

运行结果:


main id = 139781778977152
构造函数!TA() id = 139781778977152 addr = 0x7fffd0ec9d00
子线程开始 addr = 0x7fffd0ec9d00 子线程id = 139781778961984
析构函数 id = 139781778977152 addr = 0x7fffd0ec9d00

可以很清除的看到,当我们使用了std::ref()时,构造函数在主线程中进行,并且子线程没有发生拷贝构造。传给子线程的是一个引用。此时我们将入口函数参数列表中的const修饰符去掉,就可以修改对象中的数据,编译器也不会报错。这种情况下应该注意被引用对象回收时间的问题。

3.2 将对象的成员函数作为入口函数

int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
TA ta(a);
thread myobj(&TA::work,ta, 1);//类的成员函数作线程入口,第三个参数传入的对象,第四个参数是work函数的参数 // 也可以使用引用
//thread myobj(&TA::work,&ta, 1); 等价于std::ref(ta) 不会调用拷贝构造
myobj.join();
}

类内线程创建

class Ta{
public:
Ta(){
std::thread t(std::bind(&Ta::task,this));
}
void task(); }

四、线程的互斥量的使用

很多时候,多个线程需要对同一数据进行操作,同时读取某一数据往往不会出现问题,而当由某些线程读取数据,某些线程又会写入数据时,往往会发生读写冲突。
       我们试想一下,线程A在读取读取数据Data时,时间片结束,系统开始运行线程B,而线程B恰好要对Data进行修改,那么是不是会发生冲突。再想一下,一个线程对文件进行写入操作,结果写入结束还没保存并关闭文件时,时间片结束,另一个线程也访问这个文件,可是由于写文件线程没有保存文件,那么访问线程不就拿不到数据了吗。
       所以引入了互斥量这一概念

当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。多线程程序需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据

举个栗子

/*  假如说我们有很多用户,他对我们的系统下了很多指令,使用一个线程接收指令,将指令存入一个list中
* 再由另一个线程读取list中的指令
*/
class ReadWrite {
private:
std::list<int> M;
public:
void RecvMsg() { //接收指令的线程
for (size_t i = 0; i < 10000; ++i) {
cout << "RecvMsg insert a num=" << i << endl;
M.emplace_back(i);
}
}
void GetFromM() { //读取指令的线程
for (size_t i = 0; i < 10000; ++i) {
if (!M.empty()) {
int com = M.front(); //返回地一个元素,无论是否为空
M.pop_front(); //取出后移除
cout<<"cmd="<<com<<endl;
} else {
cout << "list is null\ti=" << i << endl;
}
}
cout << "Get end" << endl;
}
}; int main(){
ReadWrite r;
thread out(&ReadWrite::GetFromM, std::ref(r));
thread get(&ReadWrite::RecvMsg, std::ref(r));
get.join();
out.join();
cout << "main end" << endl;
}

上述代码在运行时会发生问题,由于涉及到了同步读写同一块内存区域的问题,要解决这个问题,需要引用mutex互斥量这个概念

#include<mutex>
class ReadWrite {
private:
std::list<int> M;
public:
void RecvMsg() { //接收指令的线程
for (size_t i = 0; i < 10000; ++i) {
cout << "RecvMsg insert a num=" << i << endl;
mymutex.lock();
M.emplace_back(i);
mymutex.unlock();
}
}
void GetFromM() { //读取指令的线程
for (size_t i = 0; i < 10000; ++i) {
mymutex.lock();
if (!M.empty()) {
int com = M.front(); //返回第一个元素,无论是否为空
M.pop_front(); //取出后移除
cout<<"cmd="<<com<<endl;
} else {
cout << "list is null\ti=" << i << endl;
}
mymutex.lock();
}
cout << "Get end" << endl;
}
}; int main(){
ReadWrite r;
thread out(&ReadWrite::GetFromM, std::ref(r));
thread get(&ReadWrite::RecvMsg, std::ref(r));
get.join();
out.join();
cout << "main end" << endl;
}

引入mutex,在对数据进行读写的时候,将把锁锁上,然后再操作数据,这样其他线程在试图访问的时候,由于锁被锁住了,其他线程也无法进行上锁操作,就会被阻塞,数据操作结束,将锁释放,让其他线程可以操作数据。
说的通俗一点,就像上厕所一样,上厕所之前要把门(mutex)先锁好(lock),这样其他人访问厕所就会被门拦住,上完了后将门打开(unlock),厕所就能被其他人用了
有一点一定要记住,就是加锁和解锁一定是成对出现的,否则其他线程就会进入无限等待的状态。所以C++标准库提供了类似智能指针的锁std::lock_guard,可以自动上锁自动解锁,当锁变量离开作用域时就会自动解锁,除此之外还可以使用std::unique_lock,相较于lock_guard更为灵活,它由不同的成员函数由用户自己控制何时加锁,能否加锁
unique_lock详解

在使用互斥量的时候,一定要注意死锁,死锁就是两个线程同时等待对方持有的某一资源,导致程序无法正常推进
比如说我们由两个锁AB线程1线程2线程一需要先锁A,然后锁B才能继续进行数据访问操作,而线程二需要先锁B再锁A才能进行数据访问操作。假设程序运行到某一时间点,线程一锁住了A线程二锁住了B,而此时,线程一需要将B上锁,才能继续推进,可是B已经被线程二锁住了,于是线程一阻塞了,必须等待B被解锁,同时线程二也是要等待A被解锁才能继续推进,程序再此刻便发生了死锁
解决死锁,只需要上锁的顺序一致便可,即线程一和线程二都是先上锁A再上锁B。使用std::lock就可以保证多个锁的顺序一致

除了std::mutex外,C++还提供了递归的独占互斥量std::recursive_mutex,它允许一个线程,同一个互斥量被lock多次

timed_mutex和recursive_timed_mutex

带超时功能的独占互斥量和带超时功能的递归独占互斥量

两个接口:try_lock_fortry_lock_until
try_lock_for:等待一段时间,超时未拿到锁则放弃,程序继续执行,参数为std::chrono::*

std::timed_mutex my_mutex;
std::chrono::milliseconds my_times(100);//100毫秒
my_mutex.try_lock_for(my_times);//等待100毫秒,如果100毫秒内获得锁,则返回false

try_lock_until:等待到某一个时间点

std::timed_mutex my_mutex;
std::chrono::milliseconds my_times(100);//100毫秒
my_mutex.try_lock_until(std::chrono::steady_clock::now()+my_times);//等待100毫秒,如果100毫秒内获得锁,则返回false
shared_mutex读写锁

可以理解为读写锁,允许多人读,单人写,但是写和读,写和写不能同时进行

可以直接locklock_shared,前者表示写的方式,一旦上锁,其他线程无法进入,后者表示读的方式,上锁后其他线程只能读,无法写


五、条件变量

5.1condition_variable

条件变量condition_vriable需要搭配互斥量使用,它是一个类,包含在同名的头文件中,有一些自己的成员函数,这里主要介绍notify_onewait函数

首先看一个例子

class MsgList {
private:
list<int> Command;
std::mutex mutex1;
std::condition_variable MyCod;
public:
void GetMsg() {
int cmd = 0;
while (true) {
std::unique_lock<std::mutex> guard(this->mutex1);//条件变量要与互斥量配合使用
this->MyCod.wait(guard, [this]() {
return !this->Command.empty();
});
cmd = Command.front();
Command.pop_front();
guard.unlock();
cout << "读取指令" << cmd << endl;
}
}
void PutMsg() {
for (size_t i = 0; i < 100000; ++i) {
std::unique_lock<std::mutex> guard(this->mutex1);
cout << "插入指令:" << i << endl;
Command.emplace_back(i);
this->MyCod.notify_one();
guard.unlock();//可要可不要,unique_lock的好处就在于可随时控制
}
}
};
int main(){
MsgList msgList;
thread Get(&MsgList::GetMsg, &msgList);
thread Put(&MsgList::PutMsg, &msgList);
Get.join();
Put.join();
cout << "main end" << endl;
}

两个线程GetPut分别访问一个共享数据区,Get从共享区拿数据,Put往共享区放数据

5.2成员函数wait

this->MyCod.wait(guard, [this]() {return !this->Command.empty();});,这行代码表示,若list中没有数据则阻塞当前线程。
这行代码第一个参数是互斥量mutex,第二个参数是一个lambda表达式,用来判断list是否有数据

wait用于等一个信号,如果第二个参数返回值为false,那么wait将解锁互斥量并阻塞,即取消对临界区的占用并阻塞自己,直到有线程调用notify_one,如果其他线程notify了,当前线程唤醒,那么wait就会不断获取mutex(临界区的访问权),直到获取到,然后往下走
如果没有参数,只需要看是否有线程唤醒自己,唤醒则往下走
wait外, 条件变量还提供了wait_forwait_until,前者用于等待指定时长,后者用于等待到指定的时间。

5.3成员函数notify_one

this->MyCod.notify_one();这行代码就是在Put线程完成了数据插入后,将Get线程唤醒,只能唤醒一个线程。

5.4 存在的问题

Put线程在执行notify的时候,如果Get没有被 wait() 所阻塞,那么唤醒操作就无效,此时Put就没有将Get叫起。
当线程Put获取到的时间片比较多时,list 中积压的数据就会比较多,这个时候是否考虑要对存数据的线程进行限流呢?


六、线程返回值

6.1future和async

std::futurestd::async 创建后台任务并返回值

std::future可以放置线程执行结束返回的结果值,可以调用get成员函数获取线程的返回值
std::async与创建线程用法无异

#include<future>
int myTh5_2(){
cout<<"mythread() start"<<" thread id="<<std::this_thread::get_id()<<endl;
std::chrono::milliseconds milliseconds(1000);
std::this_thread::sleep_for(milliseconds);//休息1000毫秒
cout<<"mythread() end"<<" thread id="<<std::this_thread::get_id()<<endl;
return 1;
}
int main(){
cout<<"main start; id="<<std::this_thread::get_id()<<endl;
std::future<int> result=std::async(myTh5_2);//未来的值
cout<<"continue"<<endl;
//get一定会等待子线程执行结束,仅能执行一次
cout<<"return "<<result.get()<<endl;//卡住,等待myTh执行结束
cout<<"main end"<<endl;
}

运行结果:

main start; id=140234514006400
continue
return mythread() start thread id=140234513991232
mythread() end thread id=140234513991232
1
main end

get()函数不拿到返回值,就会一直阻塞,还有wait()函数,只是会等待返回,不会获取返回值

使用std::launch::deferred延迟调用

std::future<int> result=std::async(std::launch::deferred,myTh5_2);
cout<<result.get();

当使用了deferred,只有执行了result.get()线程才会被执行

使用了deferred运行结果:
main start; id=140553532236160
continue
return mythread() start thread id=140553532236160
mythread() end thread id=140553532236160
1
main end

可以看见,没有创建主线程,入口函数是在主线程中执行
与之相对应的是std::launch::async,加入这个参数,表示线程会立即执行,而不是等到调用了get()后执行

6.2package_task

打包任务,一个类模板,参数是各种可调用对象,作用是将可调用对象包装起来,方便作为线程入口函数

用法示例:

    cout << "main start; id=" << std::this_thread::get_id() << endl;
//表示要求可调用对象的返回值为int,参数类型为int
std::packaged_task<int(int)> packagedTask(myTh5_3);
thread th(std::ref(packagedTask), 1);
th.join();
std::future<int> res = packagedTask.get_future();
cout<<"res.get()="<<res.get()<<endl;

packaged_task还可以独自调用

std::packaged_task<int(int)> packagedTask1([](int a) -> int { cout << "a=" << a << endl; });
packagedTask1(5);//想当于函数调用
std::future<int> res1 = packagedTask1.get_future();
cout << "res1.get()=" << res1.get() << endl;

6.3std::promise

获取线程运行结果
通过promise保存一个值,在将来某一时刻通过future获取这个值

void myTh4(std::promise<int> &temp, int res) {
srand((int)time(0));
res++;
res *= (rand()%100+1);
std::chrono::milliseconds d(3000);
std::this_thread::sleep_for(d);
int result = res;
temp.set_value(result);
}
int main() {
std::promise<int> promise;
thread th(myTh4, std::ref(promise), 1);//使用这个,就可以在不同的线程传参
th.join();
std::future<int> fu = promise.get_future();
cout << "get res=" << fu.get() << endl;
} //多线程应用
void myTh5(std::future<int> &f) {
cout << "thread id=" << std::this_thread::get_id() << "\tstart" << endl;
auto res = f.get();//等到有运算结果时才会继续
cout << "other thread return=" << res << endl;
} void myTh4(std::promise<int> &temp, int res) {
srand((int) time(0));
res++;
res *= (rand() % 100 + 1);
cout << "thread id=" << std::this_thread::get_id() << "正在计算。" << endl;
std::chrono::milliseconds d(3000);
std::this_thread::sleep_for(d);
int result = res;
temp.set_value(result);
} int main() {
std::promise<int> promise;
std::future<int> fu = promise.get_future();
thread th(myTh4, std::ref(promise), 1);//使用这个,就可一在不同的线程传参
thread th1(myTh5, std::ref(fu));
th1.join();
th.join();
}

使用promisefuture可以实现线程之间的通信,两个线程,一个线程计算,另一个线程等待计算结束后再继续运行

运行结果:

thread id=139657918248512	start
thread id=139657926641216 正在计算。
other thread return=88

但是一个future对象只能get一次,如果有多个线程需要获取这个值,可以使用shared_future,这样就可以get多次

void myTh4(std::promise<int> &temp, int res) {
srand((int) time(0));
res++;
res *= (rand() % 100 + 1);
cout << "thread id=" << std::this_thread::get_id() << "正在计算。" << endl;
std::chrono::milliseconds d(3000);
std::this_thread::sleep_for(d);
int result = res;
temp.set_value(result);
}
void myTh6(std::shared_future<int> &f) {
cout << "thread id=" << std::this_thread::get_id() << "\tstart" << endl;
auto res = f.get();
cout << "other thread return=" << res << endl;
}
void main() {
std::promise<int> promise;
std::shared_future<int> future = promise.get_future();
thread th1(myTh4, std::ref(promise), 1);
thread th3(myTh6, std::ref(future));
thread th2(myTh6, std::ref(future));
th1.join();
th2.join();
th3.join();
}

6.4future及其他
std::future<int> fu= std::async(std::launch::deferred, myTh5_2);
std::future_status status=fu.wait_for(std::chrono::seconds(1));//等待线程1秒
if(status==std::future_status::timeout)//超时 ready执行完了 deferred 延迟执行

七、原子操作

void myth6_1(int &a) {
cout << "write start! address:" << &a << endl;
for (size_t i = 0; i < 100000; ++i) {
a++;
}
} void myth6_2(int &a) {
cout << "read start! address:" << &a;
for (size_t i = 0; i < 100000; ++i) {
cout << a << endl;
}
} void test6_1() {
int a = 8;
cout << "address " << &a << "\ta=" << a << endl;
thread th1(myth6_1, std::ref(a));
thread th2(myth6_1, std::ref(a));
th2.join();
th1.join();
cout << "address " << &a << "\ta=" << a << endl; }

运行结果:

address 0x7fff7c796f3c	a=8
write start! address:0x7fff7c796f3c
write start! address:0x7fff7c796f3c
address 0x7fff7c796f3c a=192558

上述代码就是两个线程,对同一个变量执行+1操作
多运行几次会发现,出现的结果可能和我们预期不一样

因为+1操作不是原子操作,线程在进行+1操作的时候,可能突然失去时间片,导致操作未完成,就造成了最终结果比预期小的现象

原子操作:不会被打断的程序执行片段
原子操作效率比互斥量更胜一筹,互斥量一般是针对某一片代码,原子操作通常针对某一变量
原子对象关键字std::atomic

void myth6_1(std::atomic<int> &a) {
cout << "write start! address:" << &a << endl;
for (size_t i = 0; i < 1000000; ++i) {
a++;
}
}
void myth6_2(std::atomic<int> &a) {
cout << "read start! address:" << &a;
for (size_t i = 0; i < 1000000; ++i) {
cout << a << endl;
}
}
void main() {
std::atomic<int> a = 8;
cout << "address " << &a << "\ta=" << a << endl;
thread th1(myth6_1, std::ref(a));
thread th2(myth6_1, std::ref(a));
th2.join();
th1.join();
cout << "address " << &a << "\ta=" << a << endl;
}

将共享关键字封装为原子变量后,操作就不不会被打断,这样每次运行的结果就都正确了
一般用于统计,计数
使用原子操作实现操控线程结束

std::atomic<bool> gFlag = false;

void myth6_3() {
std::chrono::milliseconds d(1000);
while (!gFlag) { //为假时不让停
cout << "thread id = " << std::this_thread::get_id() << " running..." << endl;
std::this_thread::sleep_for(d);//休息一秒
}cout << "thread id = " << std::this_thread::get_id() << " stop..." << endl;
}
void main() {
thread th1(myth6_3);
thread th2(myth6_3);
std::chrono::milliseconds s(5000);
std::this_thread::sleep_for(s);
gFlag = true;//停止两个线程
th1.join();
th2.join();
}

运行结果:

thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 stop...
thread id = 140360114566720 stop...

在上上个例子中,如果将原子变量的a++操作改为a=a+1,那么结果又会不符合我们预期
一般只针对++,--,+=,&=,|=,^=适用


八、async深入

上文讲过,std::launch::deferred是延迟调用,std::launch::async是强制创建一个线程

在使用std::thread()创建线程时,如果系统资源紧张,那么可能会创建失败,程序崩溃
std::async一般称之为创建一个异步任务
上文提到过,使用了deferred延迟调用后,甚至不会创建一个新的线程,只有调用了get或者wait才会开始执行入口函数
当使用sync创建任务时std::future<int> result = std::async(std::launch::deferred, myTh5_2);,若第一个参数没有指定,默认为std::launch::deferred | std::launch::async,意思就是让系统决定是异步(创建)还是同步(不创建)运行。

C++编程笔记(多线程学习)的更多相关文章

  1. Java7编程高级进阶学习笔记

    本书PDF 下载地址: http://pan.baidu.com/s/1c141KGS 密码:v6i1 注:本文有空会跟新: 讲述的是jdk7的内容: 注关于java 更详细的内容请进入:<Ja ...

  2. 数据结构(逻辑结构,物理结构,特点) C#多线程编程的同步也线程安全 C#多线程编程笔记 String 与 StringBuilder (StringBuffer) 数据结构与算法-初体验(极客专栏)

    数据结构(逻辑结构,物理结构,特点) 一.数据的逻辑结构:指反映数据元素之间的逻辑关系的数据结构,其中的逻辑关系是指数据元素之间的前后件关系,而与他们在计算机中的存储位置无关.逻辑结构包括: 集合 数 ...

  3. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  4. Java学习笔记-多线程-创建线程的方式

    创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...

  5. DSP28377S - ADC学习编程笔记

    DSP28377S -  ADC学习编程笔记 彭会锋 2016-08-04  20:19:52 1 ADC类型导致的配置区别 F28377S的ADC类型是Type 4类型,我的理解是不同类型的ADC采 ...

  6. java多线程学习笔记——详细

    一.线程类  1.新建状态(New):新创建了一个线程对象.        2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ...

  7. Asp.net MVC4高级编程学习笔记-视图学习第一课20171009

    首先解释下:本文只是对Asp.net MVC4高级编程这本书学习记录的学习笔记,书本内容感觉挺简单的,但学习容易忘记,因此在边看的同时边作下了笔记,可能其它朋友看的话没有情境和逻辑顺序还请谅解! 一. ...

  8. JAVA多线程学习笔记(1)

    JAVA多线程学习笔记(1) 由于笔者使用markdown格式书写,后续copy到blog可能存在格式不美观的问题,本文的.mk文件已经上传到个人的github,会进行同步更新.github传送门 一 ...

  9. 多线程学习笔记九之ThreadLocal

    目录 多线程学习笔记九之ThreadLocal 简介 类结构 源码分析 ThreadLocalMap set(T value) get() remove() 为什么ThreadLocalMap的键是W ...

  10. java进阶-多线程学习笔记

    多线程学习笔记 1.什么是线程 操作系统中 打开一个程序就是一个进程 一个进程可以创建多个线程 现在系统中 系统调度的最小单元是线程 2.多线程有什么用? 发挥多核CPU的优势 如果使用多线程 将计算 ...

随机推荐

  1. .Net7 内容汇总(1)

    .Net7 RC1发布 在9月14号,.Net7 RC1正式发布了. 按照微软的说法 This is the first of two release candidates (RC) for .NET ...

  2. Rust变量用法与特征

    变量用法与特征 变量绑定 let a = "hello world"  为何不用赋值而用绑定呢(其实你也可以称之为赋值,但是绑定的含义更清晰准确)?这里就涉及 Rust 最核心的原 ...

  3. LVGL 模拟仿真(Windows+CodeBlocks)

    一.准备材料 Code Blocks官网:https://www.codeblocks.org/ Code Blocks 汉化包:链接: https://pan.baidu.com/s/12zB5bD ...

  4. Dockerfile中ADD命令详细解读

    ADD指令的功能是将主机构建环境(上下文)目录中的文件和目录.以及一个URL标记的文件 拷贝到镜像中. 其格式是: ADD 源路径 目标路径 #test FROM ubuntu MAINTAINER ...

  5. 5.第四篇 Etcd存储组件高可用部署

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247483792&idx=1&sn=b991443c ...

  6. Spring boot定义多个配置文件并自由切换

    在resource目录下定义三个配置文件 (properties文件已被我注销,配置文件建议用yml,如果properties文件与yml文件同时存在,SpringBoot会优选加载propertie ...

  7. PAT (Basic Level) Practice 1024 科学计数法 分数 20

    科学计数法是科学家用来表示很大或很小的数字的一种方便的方法,其满足正则表达式 [+-][1-9].[0-9]+E[+-][0-9]+,即数字的整数部分只有 1 位,小数部分至少有 1 位,该数字及其指 ...

  8. 「Chroot环境」Debian Testing amd64 on arm64

    这个是适用于ARM64环境的AMD64 Debian Testing系统.基于FEX转译.这个系统运行在ARM64的手机和电脑上,运行的软件是AMD64(X64)格式.下载链接提供桌面版和基础版.适用 ...

  9. 前端微信登录获取code,userInfo,openid

    getUser(e) { wx.getUserProfile({ desc: '用户完善会员资料', success: res => { let userInfo = res.userInfo; ...

  10. ECMAScript6 ES6 ES2015新语法总结

    1.let定义变量:不能重复定义.作用域 2.const:定义常量 3.解构赋值:let [a,b,c] = [1,2,3];// a=1 b=2 c=3 4.箭头函数: function fn(a, ...