扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
线程是操作系统进行运算调度的基本单位,进程是操作系统进行资源分配的基本单位。
不同进程分配不同的内存区域,同一个进程中的不同线程共享同一片内存。
线程的不同状态:
并行(Parallel)是真正意义上的同时运行,在同一时间点有多个程序在同时运行,比如多核CPU多个核同时运行着不同的程序。并发(ConCurrent)是在同一个时间段上多个程序同时运行,在宏观上看是同时运行,微观上是交替运行。
之所以设计多线程是为了充分的利用CPU资源,比如线程A正在等待磁盘数据的时候,可以将CPU让出来,线程B就能占用CPU进行计算。
c++11新增了与操作系统无关的线程类thread
,使用方法如下:
void func(int i) {cout<< i<< endl;
}
int main() {thread t1(func, 10); //传入线程绑定的函数和函数所需参数
t1.join();
return 0;
}
thread
对象创建之后,线程就已经进入就绪态,可以占用CPU进行运算。
线程的运行需要以具体的函数作为入口,而函数的参数传递无非值传递,引用传递和指针传递3种形式,值传递和指针传递没什么好说的,直接复制值或者指针放入线程入口函数中,而线程的使用中需要特别注意⚠️⚠️⚠️引用传递,普通函数使用引用传递,底层使用的还是指针,所以函数中的引用会关联到函数外的变量。但是在线程的使用中,函数引用传递是引用的复制,函数中的引用和函数外的值没有关联关系。如果想关联,必须传递参数的时候使用std::ref()
语义进行包裹。join()
和detach()
的用法:调用线程的join()
方法意味着,主线程会阻塞在该语句处,等待子线程运行结束的时候才会继续运行主线程,而detach()
方法意味着,子线程从主线程中分离出来各自运行,子线程变为后台守护线程(deamon thread),如此这般,子线程可能会结束在主线程之后,可能会出现不可预知的问题。
当存在多个线程共同访问同一资源(比如队列)的时候,如果操作的顺序不当可能会出现不可预知的错误,该资源成为临界资源,此时C++使用互斥量mutex
进行资源访问的控制。
通常为了方便理解多线程操作,我们将mutex
称为锁🔒,对资源的访问称为对资源上锁和解锁。实际上mutex
是一个标记量,不同的线程对同一mutex
对象进行lock()
和unlock()
可以达到控制资源访问次序的目的。lock()
方法是尝试获取锁并加锁,如果获取不到锁,线程就会阻塞在此语句处,mutex
调用了lock()
就必须unlock()
,否则就有可能是其他线程不能成功lock()
而一直阻塞。
为了解决这种可能忘记unlock()
的问题,引入lock_guard
模版类,lock_guard
类似于智能指针,出作用域会自动unlock()
。和lock_guard
相似的一个模版类是unique_lock
,使用方法和lock_guard
类似。
多个线程需要通信的时候会用到cv
,比如消费者和生产者线程模型,这两个线程并不是单单的对资源解锁和加锁。而是需要通信,比如,消费者没有资源可以消费的时候需要将生产者叫醒。cv
有两个方法wait()
和wait_for()
,wait
方法会让当前线程释放锁然后阻塞,直到被唤醒;wait_for
方法需要传递一个时间参数,当前线程释放锁然后等待被唤醒,但是不会一直等待,而是吵过时间参数自动苏醒。cv
还有两个方法notify_one()
和notify_all()
,notify_one
会唤醒线程等待队列的第一个线程,而notify_all
会唤醒所有等待的线程。wait
和sleep
方法的区别:
wait
是cv
的方法,sleep
是this_thread
的方法;wait
会释放锁然后再阻塞,而sleep
直接阻塞,不会释放锁。前面提到的线程入口函数都是实现一系列的操作但是没有返回值,如果需要线程的返回值,我们需要使用std::future<>
这样一个模版类来接受返回值,而普通的用thread
创建线程的方式不能获得future
对象,此时有数种方法:
std::async()
,顾名思义,异步任务就是启动一个和主线程并发的任务,该方法会返回一个future对象,通过该对象的get()方法就能获得线程返回的对象,使用方法如下:int func() {return 1234;
}
int main() {std::futurefval = std::async(func);
std::cout<< fval.get()<< std::endl;
return 0;
}
主线程会阻塞在get()处,直到线程返回返回值。async()
方法还可以传入一个枚举标志量std::launch
,如果将此标记量设置为deferred
,意味着方法调用处不会立即创建线程,直到调用get()
方法才会创建方法并运行,而此时和直接在主线程调用线程入口函数是一样的,是串行运行。注意⚠️:get()
方法只能调用一次;
2. 使用std::package_task<>
包裹一个函数,然后传递给一个线程对象,线程运行起来之后用package_task
对象的get_future()
方法获得future
对象,然后再用get()
方法拿到返回值。package_task
使用方法如下:
int func() {return 1234;
}
int main() {std::packaged_tasktask(func);
std::thread t1(std::ref(task));
t1.join();
std::cout<< task.get_future().get()<< std::endl;
return 0;
}
std::promise<>
模版类可以从线程中将值带到线程外,promise
对象会传递到线程入口函数中去,然后在线程运行过程中,使用set_value()
方法给promise
对象赋值,然后在异步线程之外,使用promise
对象的get_future()
方法获得future
对象,之后的操作和以上就一样了。使用方法如下:void func(std::promise&pro) {pro.set_value(100);
}
int main() {std::promisepro;
std::thread t1(func, std::ref(pro));
std::cout<< pro.get_future().get()<< std::endl;
t1.join(); //此处使用join其实意义不大,因为get已经获取到值说明异步线程已经结束
return 0;
}
六、线程的其他使用wait
被唤醒之后第一件事是会反复尝试直到拿到锁再往下执行,wait
被唤醒拿到锁之后一般还要再次检查阻塞条件保证多线程共享的正确性;unique_lock
所有权的转移类似于unique_ptr
,使用move
进行所有权转移;std::lock(mutex1, mutex2)
方法可以尝试同时锁住多个互斥量,如果有一个没锁成功,就不会对任何互斥量进行加锁;release()
方法将unique_lock
和mutex
解绑,并返回之前绑定的mutex
;mutex
,lock()
和unlock()
之间包裹的语句多少称为锁的粒度,粒度要合适,如果太小可能不能达到上锁的预期,如果太大,会降低多线程运行效率;std::adopt_lock
可以传递给lock_guard
和unique_lock
的构造函数,表示这个互斥量已经被lock了(你必须要把互斥量提前lock了 ,否者会报异常);std::adopt_lock
标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);通知lock_guard
不需要再构造函数中lock这个互斥量了。unique_lock
也可以带std::adopt_lock
标记,含义相同,就是不希望再unique_lock
的构造函数中lock这个mutex
。用std::adopt_lock
的前提是,自己需要先把mutex
lock上;用法与lock_guard
相同;std::try_to_lock
会尝试用mutex
的lock去锁定这个mutex
,但如果没有锁定成功,也会立即返回,并不会阻塞在那里,用这个try_to_lock
的前提是你自己不能先lock;std::defer_lock
的前提是,你不能自己先lock,否则会报异常,std::defer_lock
的意思就是并没有给mutex
加锁,初始化了一个没有加锁的mutex
。你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流