Linux 高性能服务器编程——多线程编程
1 创建线程和结束线程;
2 读取和设置线程属性;
3 线程同步方式:POSIX信号量,互斥锁和条件变量。
- 创建和调度线程都无须内核的干预,因此速度相当快。
- 不占用额外的内核资源,很多线程不会对系统性能造成明显影响。
- (缺点)一个进程的多个线程无法运行在不同的CPU上
- 内核调度M个内核线程,线程库调度N个用户线程
- 不会过度消耗内核资源,又可以充分利用多处理器的优势
#include <pthread.h>
int pthread_create ( pthread_t* thread, const pthread_attr_t* attr,void * (*start_routine)(void*) , void* arg);
#include <pthread.h>
void pthread_exit ( void* retval );
#include <pthread.h>
int pthread_join( pthread_t thread, void** retval );
#include <pthread.h>
int pthread_cancel ( pthread_t thread );
接收到取消请求的目标线程可以决定是否允许被取消以及如何取消。这分别由如下两个函数完成:
#include <pthread.h>
int pthread_setcancelstate( int state, int *oldstate );
int pthread_setcanceltype ( int type, int *oldtype );
- PTHREAD_CANCEL_ENABLE:允许线程被取消。它是线程被创建时默认取消状态。
- PTHREAD_CANCEL_DISABLE:禁止线程被取消。这种情况下,如果一个线程收到取消请求,则它会将请求挂起,直到该线程允许被取消。
- PTHREAD_CANCEL_ASYNCHRONOUS:线程随时都可以被取消。它将使得接收到取消请求的目标线程立即采取行动。
- PTHREAD_CANCEL_DEFERRED:允许目标线程推迟行动,直到它调用了下面几个所谓的取消点函数中的一个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timewait、sem_wait和sigwait。
#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{char __size[__SIZEOF_PTHREAD_ATTR_T];long int __align;
} pthread_attr_t;
#include <pthread.h>
/*初始化线程属性对象*/
int pthread_attr_init( pthread_attr_t* attr );
/*销毁线程属性对象,被销毁的线程属性对象只有再次初始化之后才能继续使用*/
int pthread_attr_destroy( pthread_attr_t* attr);
/*下面这些函数用于获取和设置线程属性对象的某个属性*/
int pthread_attr_getdetachstate( const pthread_attr_t* attr ,int* detachstate );
int pthread_attr_setdetachstate( pthread_attr_t* attr,int detachstate );
int pthread_attr_getstackaddr( const pthread_attr_t* attr,void **stackaddr );
int pthread_attr_setstackaddr( pthread_attr_t* attr,void* stackaddr );
int pthread_attr_getstacksize( const pthread_attr_t* attr,size_t* stacksize );
int pthread_attr_setstacksize( pthread_attr_t* attr,size_t stackszie );
int pthread_attr_getstack( const pthread_attr_t* attr,void** stackaddr,size_t* stacksize );
int pthread_attr_setstack( pthread_attr_t* attr,void* stackaddr,size_t stacksize );
int pthread_attr_getguardsize( const pthread_attr_t* attr,size_t* guarsize );
int pthread_attr_setguardsize( pthread_attr_t* attr,size_t guarsize );
int pthread_attr_getschedparam( const pthread_attr_t* attr,struct sched_param* param );
int pthread_attr_setschedparam( pthread_attr_t* attr,const struct sched_param* param );
int pthread_attr_getschedpolicy( const pthread_attr_t* attr,int* policy );
int pthread_attr_setschedpolicy( pthread_attr_t* attr,int policy );
int pthread_attr_getinheritsched( const pthread_attr_t* attr,int* inherit );
int pthread_attr_setinheritsched( pthread_attr_t* attr,int inherit );
int pthread_attr_getscope( const pthread_attr_t* attr,int* scope );
int pthread_attr_setscope( pthread_attr_t* attr,int scope );
- detachstate:线程的脱离状态。它有PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACH两个可选值。前者指定线程是可以被回收的,后者使调用线程脱离与进程中其他线程的同步。脱离了与其他线程同步的线程称为“脱离线程”。脱离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE。
- stackaddr和stacksize:线程堆栈的起始地址和大小。一般来说,我们不需要自己来管理线程堆栈,因为Linux默认为每个线程分配了足够的堆栈空间(一般是8 MB)。我们可以使用ulimt -s 命令查看或修改这个默认值。
- guardsize:保护区域大小。如果guardsize大于0,则系统创建线程的时候会在其堆栈的尾部额外分配guardsize字节的空间,作为保护堆栈不被错误的覆盖的区域。如果guardsize等于0,则系统不为新创建的线程设置堆栈保护区。如果使用者通过pthread_attr_setstackaddr或pthread_attr_setstack函数手动设置线程的堆栈,则guardsize属性将被忽略。
- schedparam:线程调度参数。其类型时sched_param结构体。该结构体面前还只有一个整型类型的成员——sched_priority,该成员表示线程的运行优先级。
- schedpolicy:线程调度策略。该属性有SCHED_FIFO、SCHED_RR和SCHED_OTHER三个可选值,其中SCHED_OTHER是默认值。SCHED_RR表示采用轮转算法调度,SCHED_FIFO表示使用先进先出的方法调度,这两种调度方法都具备实时调度功能,但只能用于以超级用户身份运行的进程。
- inheritsched:是否继承调用线程的调度属性。该属性有PTHREAD_INHERIT_SCHED和PTHREAD_EXPLICIT_SCHED 两个可选值。前者表示新线程沿用其创建者的线程调度参数,这种情况下再设置新线程的调度参数将没有任何效果。后者表示调用者要明确的指定新线程的调度参数。
- scope:线程间竞争CPU的范围,即线程优先级的有效范围。POSIX标准定义了该属性的PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS两个可选值,前者表示目标线程与系统中所有线程一起竞争CPU,后者表示目标线程仅与其他隶属于同一进程的线程竞争CPU的使用。面前Linux 只支持PTHREAD_SCOPE_SYSTEM这一种取值。
#include <semaphore.h>
int sem_init ( sem_t* sem, int pshared, unsigned int value ); // 初始化一个未命名的信号量
int sem_destroy ( sem_t* sem ); // 用于销毁信号量,以释放其占用的内核资源
int sem_wait ( sem_t*sem ); // 以原子操作的方式将信号量减1,如果信号量的值为0,则阻塞,直到该值不为0.
int sem_trywait ( sem_t* sem ); // sem_wait的非阻塞版本
int sem_post ( sem_t* sem ); // 以原子操作的方式将信号量的值加1
- sem_init函数用于初始化一个未命名的信号量。pshared参数指定信号量的类型。如果其值是0,就表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的结果。
- sem_destroy函数用于销毁信号量,以释放其占用的内核资源。如果销毁一个正被其他线程等待的信号量,则将导致不可预期的结果。
- sem_wait函数以原子操作的方式将信号量的值减1.如果信号量的值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。
- sem_trywait与sem_wait函数相似,不过它始终立即返回,而不论被操作的信号量是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值大于0值时,sem_trywait对信号量执行减1操作。当信号量的值为0时,它将返回-1,并设置errno为EAGAIN。
- sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
#include <pthread.h>
int pthread_mutex_init ( pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr );
int pthread_mutex_destroy ( pthread_mutex_t* mutex );
int pthread_mutex_lock ( pthread_mutex_t* mutex );
int pthread_mutex_trylock ( pthread_mutex_t* mutex );
int pthread_mutex_unlock ( pthread_mutex_t* mutex );
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
宏PTHREAD_MUTEX_INITIALIZER实际上只是把互斥锁的各个字段都初始化为0.
#include <pthread.h>
/* 初始化互斥锁属性对象 */
int pthread_mutexattr_init ( pthread_mutexattr_t* attr );
/* 销毁互斥锁属性对象 */
int pthread_mutexattr_destroy ( pthread_mutexattr_t* attr );
/* 获取和设置互斥锁的pshared属性 */
int pthread_mutexattr_getpshared ( const pthread_mutexattr_t* attr, int * pshared );
int pthread_mutexattr_setpshared ( pthread_mutexattr_t* attr, int pthread );
/* 获取和设置互斥锁的type属性 */
int pthread_mutexattr_gettype ( const pthread_mutexattr_t* atr, int * type );
int pthread_mutexattr_settype ( pthread_mutexattr_t* attr, int type );
- PTHREAD_PROCESS_SHARED:互斥锁可以被跨进程共享。
- PTHREAD_PROCESS_PRIVATE:互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
- PTHREAD_MUTEX_DEFAULT:默认锁(缺省的互斥锁类型属性)。如果一个线程试图对一个默认锁重复锁定或者试图解锁一个由别的线程锁定的默认锁或者试图解锁已经被解锁的默认锁会引发不可预料的结果。
- PTHREAD_MUTEX_NORMAL:普通锁。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获得锁。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题:(1)一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁。(2)对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
- PTHREAD_MUTEX_ERRORCHECK:检错锁。 如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误码EDEADLK。 如果试图解锁一个由别的线程锁定的互斥锁或者试图解锁已经被解锁的互斥锁,则解锁操作返回EPERM。
- PTHREAD_MUTEX_RECURSIVE:嵌套锁。如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,不过一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;void* another( void* arg )
{pthread_mutex_lock( &mutex_b );printf( "in child thread, got mutex b, waiting for mutex a\n" );sleep( 5 );++b;pthread_mutex_lock( &mutex_a );b += a++;pthread_mutex_unlock( &mutex_a );pthread_mutex_unlock( &mutex_b );pthread_exit( NULL );
}int main()
{pthread_t id;pthread_mutex_init( &mutex_a, NULL );pthread_mutex_init( &mutex_b, NULL );pthread_create( &id, NULL, another, NULL );pthread_mutex_lock( &mutex_a );printf( "in parent thread, got mutex a, waiting for mutex b\n" );sleep( 5 );++a;pthread_mutex_lock( &mutex_b );a += b++; pthread_mutex_unlock( &mutex_b );pthread_mutex_unlock( &mutex_a );pthread_join( id, NULL );pthread_mutex_destroy( &mutex_a );pthread_mutex_destroy( &mutex_b );return 0;
}
#include <pthread.h>
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
int pthread_cond_destroy ( pthread_cond_t* cond );
int pthread_cond_broadcast ( pthread_cond_t* cond ); //以广播的形式唤醒一个等待目标条件变量的线程
int pthread_cond_signal ( pthread_cond_t* cond ); //唤醒一个等待目标条件变量的线程
int pthread_cond_wait ( pthread_cond_t* cond, pthread_mutex_t* mutex ); // 等待目标条件变量,mutex参数保证对条件变量及其等待队列的操作原子性。
- pthread_cond_init函数用于初始化条件变量。cond_attr参数指定条件变量的属性。如果将它设置为NULL,则表示使用默认属性。除了pthread_cond_init函数外,还可以如下初始化一个条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
宏PTHREAD_COND_INITIALIZER实际上只是把条件变量的各个字段都初始化为0.
- pthread_cond_destroy 用于销毁条件变量,以释放其占用的内核资源。销毁一个正在被等待的条件变量将失败并返回EBUSY。
- pthread_cond_broadcast 函数以广播的方式唤醒所有等待目标条件变量的线程。
- pthread_cond_signal 函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。有时候我们可能想唤醒一个指定线程,但pthread没有对该需求提供解决方案。不过我们可以间接实现该需求:定义一个能够唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后采用广播方式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己,如果是就开始执行后续代码,如果不是则返回继续等待。
#ifndef LOCKER_H
#define LOCKER_H#include <exception>
#include <pthread.h>
#include <semaphore.h>class sem
{
public:sem(){if( sem_init( &m_sem, 0, 0 ) != 0 ){throw std::exception();}}~sem(){sem_destroy( &m_sem );}bool wait(){return sem_wait( &m_sem ) == 0;}bool post(){return sem_post( &m_sem ) == 0;}private:sem_t m_sem;
};class locker
{
public:locker(){if( pthread_mutex_init( &m_mutex, NULL ) != 0 ){throw std::exception();}}~locker(){pthread_mutex_destroy( &m_mutex );}bool lock(){return pthread_mutex_lock( &m_mutex ) == 0;}bool unlock(){return pthread_mutex_unlock( &m_mutex ) == 0;}private:pthread_mutex_t m_mutex;
};class cond
{
public:cond(){if( pthread_mutex_init( &m_mutex, NULL ) != 0 ){throw std::exception();}if ( pthread_cond_init( &m_cond, NULL ) != 0 ){pthread_mutex_destroy( &m_mutex );throw std::exception();}}~cond(){pthread_mutex_destroy( &m_mutex );pthread_cond_destroy( &m_cond );}bool wait(){int ret = 0;pthread_mutex_lock( &m_mutex );ret = pthread_cond_wait( &m_cond, &m_mutex );pthread_mutex_unlock( &m_mutex );return ret == 0;}bool signal(){return pthread_cond_signal( &m_cond ) == 0;}private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;
};#endif
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>pthread_mutex_t mutex;/*子线程运行的函数。它首先获得互斥锁mutex,然后暂停5s ,再释放该互斥锁*/
void* another( void* arg )
{printf( "in child thread, lock the mutex\n" );pthread_mutex_lock( &mutex );sleep( 5 );pthread_mutex_unlock( &mutex );
}void prepare()
{pthread_mutex_lock( &mutex );
}void infork()
{pthread_mutex_unlock( &mutex );
}int main()
{pthread_mutex_init( &mutex, NULL );pthread_t id;pthread_create( &id, NULL, another, NULL );//pthread_atfork( prepare, infork, infork );/*父进程中的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex*/sleep( 1 );int pid = fork();if( pid < 0 ){pthread_join( id, NULL );pthread_mutex_destroy( &mutex );return 1;}else if( pid == 0 ){printf( "I anm in the child, want to get the lock\n" );/*子进程从父进程继承了互斥锁mutex的状态,该互斥锁处于锁住的状态,这是由父进程中的子线程执行pthread_mutex_lock引起的,因此,下面这句加锁操作会一直阻塞,尽管从逻辑上来说它是不应该阻塞的*/pthread_mutex_lock( &mutex );printf( "I can not run to here, oop...\n" );pthread_mutex_unlock( &mutex );exit( 0 );}else{pthread_mutex_unlock( &mutex );wait( NULL );}pthread_join( id, NULL );pthread_mutex_destroy( &mutex );return 0;
}
#include <pthread.h>
int pthread_atfork( void (*prepare)(void), void (*parent)(void),void (*child)(void) );
该函数将建立3个fork句柄来帮助我们清理互斥锁的状态。该函数成功时返回0,失败则返回错误码。
- prepare 句柄将在fork调用创建出子进程之前被执行。它可以用来锁住父进程中的互斥锁。
- parent 句柄则是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行。它的作用是释放所有在prepare 句柄中被锁住的互斥锁。
- child 句柄是在fork 返回之前,在子进程中被执行。和parent句柄一样,child 句柄也是用于释放所有在prepare 句柄中被锁住的互斥锁。
void prepare()
{pthread_mutex_lock( &mutex );
}void infork()
{pthread_mutex_unlock( &mutex );
}pthread_atfork( prepare, infork, infork );
#include <pthread.h>
include <signal.h>
int pthread_sigmask( int how, const sigset_t* newmask, sigset_t* oldmask );
由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。这可以通过如下两个步骤来实现:
- 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会响应被屏蔽的信号了。
- 在某个线程中调用如下函数来等待信号并处理之:
#include <signal.h> int sigwait( const sigset_t* set, int* sig );
set 参数指定需要等待的信号的集合。我们可以简单的将其指定为在第一步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig 指向的整数用于存储该函数返回的信号值。sigwait成功时返回0,失败则返回错误码。一旦sigwait正确返回,我们就可以对接收到的信号做处理了。很显然,如果我们使用了sigwait,就不应该再为信号设置信号处理函数了。这是因为当程序接收到了信号时,二者中只能有一个起作用。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>/* Simple error handling functions */#define handle_error_en(en, msg) \do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)static void *sig_thread(void *arg)
{printf( "yyyyy, thread id is: %ld\n", pthread_self() );sigset_t *set = (sigset_t *) arg;int s, sig;for (;;) {/*第二步骤,调用sigwait等待信号*/s = sigwait(set, &sig);if (s != 0)handle_error_en(s, "sigwait");printf("Signal handling thread got signal %d\n", sig);}
}int main(int argc, char *argv[])
{pthread_t thread;sigset_t set;int s;/*第一步骤,在主线程中设置信号掩码*/sigemptyset(&set);sigaddset(&set, SIGQUIT);sigaddset(&set, SIGUSR1);s = pthread_sigmask(SIG_BLOCK, &set, NULL);if (s != 0)handle_error_en(s, "pthread_sigmask");s = pthread_create(&thread, NULL, &sig_thread, (void *) &set);if (s != 0)handle_error_en(s, "pthread_create");printf( "sub thread with id: %ld\n", thread );pause(); /* Dummy pause so we can test program */
}
pthread 还提供了下面的方法,使得我们可以明确的将一个信号发送给指定的线程:
#include <signal.h>
int pthread_kill( pthread_t thread, int sig );
thread参数指定目标线程。
转载于:https://www.cnblogs.com/hehehaha/p/6332335.html
Linux 高性能服务器编程——多线程编程相关推荐
- Linux高性能server规划——多线程编程(在)
多线程编程 Linux主题概述 线程模型 线程是程序中完毕一个独立任务的完整执行序列.即一个可调度的实体. 依据执行环境和调度者的身份.线程可分为内核线程和用户线程.内核线程,在有的系统上也称为LWP ...
- Linux 高性能服务器开发笔记:Reactor 模型定时器 | 网络编程定时器
本文主要根据游双书本 Linux 高性能服务器开发 学习分析 linux 网络编程常用到的定时器模型,配备详细理解和分析,同时分析了 Linux 内核中定时器的低精度时间轮和高精度定时器实现思路还有 ...
- 《Linux高性能服务器编程》——导读
前 言 为什么要写这本书 目前国内计算机书籍的一个明显弊病就是内容宽泛而空洞.很多书籍长篇大论,恨不得囊括所有最新的技术,但连一个最基本的技术细节也无法解释清楚.有些书籍给读者展现的是网络上随处可见的 ...
- 《Linux高性能服务器编程》学习笔记
<Linux高性能服务器编程>学习笔记 Linux高性能服务器编程 TCP/IP协议族 TCP/IP协议族体系结构以及主要协议 数据链路层 网络层 传输层 应用层 封装 分用 测试网络 A ...
- 【Todo】【读书笔记】Linux高性能服务器编程
在读 /Users/baidu/Documents/Data/Interview/服务器-检索端/<Linux高性能服务器编程.pdf> 其实之前读过,要面试了,需要温习. P260 So ...
- Linux高性能服务器编程——书籍阅读笔记
目录 前言 正文 第一章 1. 零拷贝函数 2. TCP/IP协议族 3. OSPF 4. ARP协议 5. RARP 6. ICMP协议 7. TCP协议 8. UDP协议 9. 封装 第四章 TC ...
- Linux 高性能服务器网络编程(一)
Linux 高性能服务器网络编程 Linux网络编程基础API Socket 地址API 通用socket 地址 专用Sokect地址 IP地址转换函数 创建socket(socket) 命名(绑定) ...
- Linux与C++11多线程编程(学习笔记)
多线程编程与资源同步 在Windows下,主线程退出后,子线程也会被关闭; 在Linux下,主线程退出后,系统不会关闭子线程,这样就产生了僵尸进程 3.2.1创建线程 Linux 线程的创建 #inc ...
- Linux高性能服务器开发——进程篇
本文主要是学习Linux高性能服务器开发需要提前了解的知识,后续还会涉及到虚拟内存方面的内容,各位看官可以多了解了解,看到文章内有将的不清楚或者讲错的地方请各位一定留言,我看到后会第一时间验证并修正的 ...
最新文章
- c++读取utf8文件_【Python】File文件对象
- Elasticsearch-01CentOS7单节点部署ES5.6.16
- mac os 录屏快捷键_Mac跨平台自动同步记事本
- centos 7 快速安装nginx
- 第13章 程序的动态加载和执行(二,用户代码)
- 二叉树的遍历实验报告C语言,二叉树的建立与遍历实验报告(c语言编写,附源代码)...
- 可能是国内最火的开源项目 —— C/C++ 篇
- 学在信息——初窥门径
- 浅谈Java对接阿里IOT
- pandas之链式索引问题(chained indexing)
- <Zhuuu_ZZ>Map接口和equals重写
- [小o地图-数据] - 城市交通态势数据(实时路况)
- java防止注册刷短信攻击_java面试(1)如何防止恶意攻击短信验证码接口
- Jupyter Notebook 五大效率插件
- usb转串口,232电平,TTL电平
- 马科维茨投资组合理论(均方模型)学习笔记——基于Matlab(二)
- 1、Proteus仿真STM32流水灯实验例程、详细步骤。
- 局域网语音对讲系统_安全防范系统--楼宇对讲系统
- GDUT 2.25 A
- Single Headed Attention RNN: Stop Thinking With Your Head
热门文章
- java se5和8_javaSE_8系列博客——Java语言的特性(三)--类和对象(5)--定义方法...
- docker添加新的环境变量_Docker环境变量
- oracle00333,Oracle数据库REDO损坏ora-00333修复手札
- java标量替换_JAVA逃逸分析、栈上分配、标量替换、同步消除
- oracle 24756,关于ORA-24756: transaction does not exist的问题
- css 友情链接效果,友链样式与位置很重要!
- 【算法篇】八种内排序算法
- java运算符优先级举例_列举出java运算符的优先级
- 安卓mysql插入数据_【11-25求助】关于Android 的SQLite数据库插入数据报错问题
- mysql key_len_浅谈mysql explain中key_len的计算方法